From 4d5021ae662194f0b8713d2071513ad932775419 Mon Sep 17 00:00:00 2001 From: GA Date: Sun, 25 Oct 2026 15:34:06 +0100 Subject: [PATCH] perf: stop the previous preview before matching the new path The previous plugin was only cleaned up after FindMatch had probed the new path, so its background work (folder size counting, text loading...) kept competing for the disk with the new probe. Unload it first so it stops as soon as the selection changes. Also log the phases that run on the UI thread (unload, plugin matching, Prepare, View and Cleanup) when they take 100 ms or more, with the plugin and the file extension (never the file name), to QuickLook.Keyboard.log, to find which viewers still block switching to the next file. KeyboardLog.cs (the QuickLook.Keyboard.log writer) is included because the timings are written there; it is identical to the copy added by the Raw Input keyboard change. Co-Authored-By: Claude Haiku 5.5 --- QuickLook/Helpers/KeyboardLog.cs | 102 ++++++++++++++++++++++++++++++ QuickLook/ViewWindowManager.cs | 11 ++++ QuickLook/ViewerWindow.Actions.cs | 7 ++ 3 files changed, 120 insertions(+) create mode 100644 QuickLook/Helpers/KeyboardLog.cs diff --git a/QuickLook/Helpers/KeyboardLog.cs b/QuickLook/Helpers/KeyboardLog.cs new file mode 100644 index 000000000..b92d2d510 --- /dev/null +++ b/QuickLook/Helpers/KeyboardLog.cs @@ -0,0 +1,102 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using QuickLook.Common.Helpers; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Text; +using System.Threading; + +namespace QuickLook.Helpers; + +/// +/// Diagnostic log for the keyboard pipeline, written to QuickLook.Keyboard.log in the data folder. +/// Only QuickLook's own keys and anomalies are logged, never other keystrokes. +/// +internal static class KeyboardLog +{ + private const long MAX_SIZE = 512 * 1024; + + /// Preview phases slower than this block switching to the next file and get logged. + private const long SLOW_PREVIEW_PHASE_MS = 100; + + private static readonly string LogPath = Path.Combine(SettingHelper.LocalDataPath, "QuickLook.Keyboard.log"); + + // Bounded so that a stuck disk can never make the keyboard hook wait or grow memory. + private static readonly BlockingCollection Queue = new(new ConcurrentQueue(), 1000); + + static KeyboardLog() + { + new Thread(WriteLoop) + { + IsBackground = true, + Name = "QuickLook.KeyboardLog", + Priority = ThreadPriority.BelowNormal, + }.Start(); + } + + /// + /// Never blocks; safe to call from inside the low-level keyboard hook. + /// + public static void Write(string message) + { + Queue.TryAdd($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [T{Thread.CurrentThread.ManagedThreadId}] {message}"); + } + + /// + /// Logs a preview phase that ran on the UI thread only when it was slow enough to delay + /// the next selection change, naming the plugin and file type, never the file name. + /// + public static void WriteIfSlow(string phase, long elapsedMs) + { + if (elapsedMs >= SLOW_PREVIEW_PHASE_MS) + Write($"Slow preview {phase}: {elapsedMs} ms"); + } + + private static void WriteLoop() + { + var batch = new StringBuilder(); + + foreach (var line in Queue.GetConsumingEnumerable()) + { + batch.AppendLine(line); + while (Queue.TryTake(out var more)) + batch.AppendLine(more); + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!); + + var file = new FileInfo(LogPath); + if (file.Exists && file.Length > MAX_SIZE) + { + File.Delete(LogPath + ".old"); + File.Move(LogPath, LogPath + ".old"); + } + + File.AppendAllText(LogPath, batch.ToString()); + } + catch + { + // logging must never take QuickLook down + } + + batch.Clear(); + } + } +} diff --git a/QuickLook/ViewWindowManager.cs b/QuickLook/ViewWindowManager.cs index e18fc7004..7faa45f43 100644 --- a/QuickLook/ViewWindowManager.cs +++ b/QuickLook/ViewWindowManager.cs @@ -164,8 +164,19 @@ public void InvokePreview(string path = null) RunFocusMonitor(); + // Stop the previous preview's background work (file reads, folder counting...) before + // matching the new path, so it doesn't keep competing for the disk while plugins probe it. + var sw = Stopwatch.StartNew(); + _viewerWindow.UnloadPlugin(); + var unloadMs = sw.ElapsedMilliseconds; + + sw.Restart(); var matchedPlugin = PluginManager.GetInstance().FindMatch(path); + var fileType = isDirectory ? "" : Path.GetExtension(path); + KeyboardLog.WriteIfSlow("unload", unloadMs); + KeyboardLog.WriteIfSlow($"match ({fileType} -> {matchedPlugin.GetType().Namespace})", sw.ElapsedMilliseconds); + BeginShowNewWindow(path, matchedPlugin); } diff --git a/QuickLook/ViewerWindow.Actions.cs b/QuickLook/ViewerWindow.Actions.cs index c5fb50506..89cb4e6cf 100644 --- a/QuickLook/ViewerWindow.Actions.cs +++ b/QuickLook/ViewerWindow.Actions.cs @@ -253,7 +253,10 @@ internal void UnloadPlugin() try { + var cleanupWatch = Stopwatch.StartNew(); Plugin?.Cleanup(); + if (Plugin != null) + KeyboardLog.WriteIfSlow($"cleanup ({Plugin.GetType().Namespace})", cleanupWatch.ElapsedMilliseconds); } catch (Exception e) { @@ -284,9 +287,11 @@ internal void BeginShow(IViewer matchedPlugin, string path, ContextObject.ColorProfileName = DisplayDeviceHelper.GetMonitorColorProfileFromWindow(this); // Get window size before showing it + var prepareWatch = Stopwatch.StartNew(); try { Plugin.Prepare(path, ContextObject); + KeyboardLog.WriteIfSlow($"prepare ({Plugin.GetType().Namespace})", prepareWatch.ElapsedMilliseconds); } catch (Exception e) { @@ -360,7 +365,9 @@ internal void BeginShow(IViewer matchedPlugin, string path, // Avoid main thread scheduling lag if (Plugin is null) return; + var viewWatch = Stopwatch.StartNew(); Plugin.View(path, ContextObject); + KeyboardLog.WriteIfSlow($"view ({Plugin.GetType().Namespace})", viewWatch.ElapsedMilliseconds); // Initial the more menu ClearMoreMenuUnpin();