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();