From 0ce5189cfc41f2015bed1a678c82d64642842f75 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:20:03 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[Performance=20optimiza?= =?UTF-8?q?tion]=20Avoid=20string=20array=20and=20string=20allocations=20w?= =?UTF-8?q?hen=20parsing=20WSL=20UNC=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Abstractions/IQuickShellEventSource.cs | 76 +++++++++---------- .../Classification/ProjectAnalysisService.cs | 12 +-- QuickShell.Core/Interop/ShellFileDialog.cs | 68 ++++++++--------- .../Services/CompanionAppCatalog.cs | 16 ++-- .../Services/QuickShellEventSource.cs | 58 +++++++------- .../Services/RowPresentationDiagnostics.cs | 12 +-- QuickShell.Core/Services/WslPathResolver.cs | 31 +++++++- 7 files changed, 148 insertions(+), 125 deletions(-) diff --git a/QuickShell.Core/Abstractions/IQuickShellEventSource.cs b/QuickShell.Core/Abstractions/IQuickShellEventSource.cs index a4c4382b..5162148f 100644 --- a/QuickShell.Core/Abstractions/IQuickShellEventSource.cs +++ b/QuickShell.Core/Abstractions/IQuickShellEventSource.cs @@ -7,56 +7,56 @@ namespace QuickShell.Abstractions; internal interface IQuickShellEventSource { /// -/// Determines whether diagnostic event writing is enabled. -/// -/// true if diagnostic event writing is enabled; otherwise, false. -bool IsEnabled(); + /// Determines whether diagnostic event writing is enabled. + /// + /// true if diagnostic event writing is enabled; otherwise, false. + bool IsEnabled(); /// -/// Emits a diagnostic event for row-cache activity. -/// -/// The kind of row-cache activity. -void WriteRowCache(string kind); + /// Emits a diagnostic event for row-cache activity. + /// + /// The kind of row-cache activity. + void WriteRowCache(string kind); /// -/// Emits a diagnostic event for plan-cache activity. -/// -/// The type of plan-cache activity. -void WritePlanCache(string kind); + /// Emits a diagnostic event for plan-cache activity. + /// + /// The type of plan-cache activity. + void WritePlanCache(string kind); /// -/// Records the duration of a startup operation. -/// -/// The name identifying the startup operation. -/// The operation duration in milliseconds. -void WriteStartupSpan(string name, double elapsedMs); + /// Records the duration of a startup operation. + /// + /// The name identifying the startup operation. + /// The operation duration in milliseconds. + void WriteStartupSpan(string name, double elapsedMs); /// -/// Records a repository-related diagnostic event. -/// -/// The repository location associated with the event. -/// The name of the repository event. -/// -/// The optional elapsed time in milliseconds. A value is recorded as -/// -1, meaning the duration is unavailable rather than a real measured latency. -/// -void WriteRepository(string location, string eventName, long? elapsedMs = null); + /// Records a repository-related diagnostic event. + /// + /// The repository location associated with the event. + /// The name of the repository event. + /// + /// The optional elapsed time in milliseconds. A value is recorded as + /// -1, meaning the duration is unavailable rather than a real measured latency. + /// + void WriteRepository(string location, string eventName, long? elapsedMs = null); /// -/// Emits a support diagnostic event identified by an event code. -/// -/// The identifier for the support diagnostic event. -void WriteSupportEvent(string eventCode); + /// Emits a support diagnostic event identified by an event code. + /// + /// The identifier for the support diagnostic event. + void WriteSupportEvent(string eventCode); /// -/// Records a diagnostic event for a failure to write support diagnostics. -/// -/// The type of exception that caused the write failure. -void WriteSupportWriteFailure(string exceptionType); + /// Records a diagnostic event for a failure to write support diagnostics. + /// + /// The type of exception that caused the write failure. + void WriteSupportWriteFailure(string exceptionType); /// -/// Records the completion of Git repository discovery. -/// -/// The number of repositories discovered. -void WriteGitDiscoveryComplete(int repoCount); + /// Records the completion of Git repository discovery. + /// + /// The number of repositories discovered. + void WriteGitDiscoveryComplete(int repoCount); } diff --git a/QuickShell.Core/Classification/ProjectAnalysisService.cs b/QuickShell.Core/Classification/ProjectAnalysisService.cs index 643ccc70..5564606c 100644 --- a/QuickShell.Core/Classification/ProjectAnalysisService.cs +++ b/QuickShell.Core/Classification/ProjectAnalysisService.cs @@ -133,12 +133,12 @@ public string BuildTaskTypeChoicesJson( } /// - /// Suggests a companion app for the specified project directory. - /// - /// The project directory to inspect. - /// A companion app suggestion, or null when no suggestion is available. - public CompanionAppSuggestion? TrySuggestCompanionApp(string directory) => - _companionAppDetector.TrySuggest(directory); + /// Suggests a companion app for the specified project directory. + /// + /// The project directory to inspect. + /// A companion app suggestion, or null when no suggestion is available. + public CompanionAppSuggestion? TrySuggestCompanionApp(string directory) => + _companionAppDetector.TrySuggest(directory); public string? TryDetectDevServerUrl(string directory) => _devServerDetector.TryDetectDevServerUrl(directory); diff --git a/QuickShell.Core/Interop/ShellFileDialog.cs b/QuickShell.Core/Interop/ShellFileDialog.cs index 82124719..09995307 100644 --- a/QuickShell.Core/Interop/ShellFileDialog.cs +++ b/QuickShell.Core/Interop/ShellFileDialog.cs @@ -364,17 +364,17 @@ internal partial interface IModalWindow internal partial interface IFileDialog : IModalWindow { /// -/// Configures the file types displayed by the dialog. -/// -/// The number of file type specifications. -/// A pointer to the array of file type specifications. -void SetFileTypes(uint cFileTypes, nint rgFilterSpec); + /// Configures the file types displayed by the dialog. + /// + /// The number of file type specifications. + /// A pointer to the array of file type specifications. + void SetFileTypes(uint cFileTypes, nint rgFilterSpec); /// -/// Selects the default file type in the dialog's file type list. -/// -/// The one-based index of the file type to select. -void SetFileTypeIndex(uint iFileType); + /// Selects the default file type in the dialog's file type list. + /// + /// The one-based index of the file type to select. + void SetFileTypeIndex(uint iFileType); /// /// Retrieves the index of the currently selected file type. @@ -396,10 +396,10 @@ internal partial interface IFileDialog : IModalWindow int Unadvise(uint dwCookie); /// -/// Configures the file dialog options. -/// -/// The bitwise combination of file dialog option flags. -void SetOptions(uint fos); + /// Configures the file dialog options. + /// + /// The bitwise combination of file dialog option flags. + void SetOptions(uint fos); /// /// Retrieves the current options configured for the file dialog. @@ -418,10 +418,10 @@ internal partial interface IFileDialog : IModalWindow int SetDefaultFolder(nint psi); /// -/// Sets the folder displayed by the file dialog. -/// -/// The shell item representing the folder to display. -void SetFolder(IShellItem psi); + /// Sets the folder displayed by the file dialog. + /// + /// The shell item representing the folder to display. + void SetFolder(IShellItem psi); /// /// Retrieves the dialog's current folder. @@ -440,10 +440,10 @@ internal partial interface IFileDialog : IModalWindow int GetCurrentSelection(out nint ppsi); /// -/// Sets the file name displayed in the dialog. -/// -/// The file name to display. -void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); + /// Sets the file name displayed in the dialog. + /// + /// The file name to display. + void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); /// /// Retrieves the current file name displayed by the dialog. @@ -454,9 +454,9 @@ internal partial interface IFileDialog : IModalWindow int GetFileName(out nint pszName); /// -/// Sets the dialog title. -/// -void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + /// Sets the dialog title. + /// + void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); /// /// Sets the label of the dialog's OK button. @@ -475,11 +475,11 @@ internal partial interface IFileDialog : IModalWindow int SetFileNameLabel(nint pszLabel); /// -/// Retrieves the selected shell item. -/// -/// Receives a pointer to the selected shell item. -/// -void GetResult(out nint ppsi); + /// Retrieves the selected shell item. + /// + /// Receives a pointer to the selected shell item. + /// + void GetResult(out nint ppsi); /// /// Adds a location to the dialog's navigation pane. @@ -611,11 +611,11 @@ internal partial interface IShellItem int GetParent(out nint ppsi); /// -/// Retrieves the item's display name in the specified format. -/// -/// The display name format to retrieve. -/// Receives a pointer to the allocated display-name string. -void GetDisplayName(uint sigdnName, out nint ppszName); + /// Retrieves the item's display name in the specified format. + /// + /// The display name format to retrieve. + /// Receives a pointer to the allocated display-name string. + void GetDisplayName(uint sigdnName, out nint ppszName); /// /// Retrieves the specified attributes of the shell item. diff --git a/QuickShell.Core/Services/CompanionAppCatalog.cs b/QuickShell.Core/Services/CompanionAppCatalog.cs index 1339f138..0b32c196 100644 --- a/QuickShell.Core/Services/CompanionAppCatalog.cs +++ b/QuickShell.Core/Services/CompanionAppCatalog.cs @@ -166,14 +166,14 @@ private static void EnsureFormChoicesCached() } /// - /// Serializes form choices into the JSON representation used by the companion app selector. - /// - /// The form choice identifiers and display titles. - /// The serialized form choices. - private static string SerializeFormChoices(IReadOnlyList<(string Id, string Title)> choices) => - JsonSerializer.Serialize( - choices.Select(choice => new FormChoiceJson(choice.Title, choice.Id)).ToList(), - QuickShellJsonContext.Default.ListFormChoiceJson); + /// Serializes form choices into the JSON representation used by the companion app selector. + /// + /// The form choice identifiers and display titles. + /// The serialized form choices. + private static string SerializeFormChoices(IReadOnlyList<(string Id, string Title)> choices) => +JsonSerializer.Serialize( + choices.Select(choice => new FormChoiceJson(choice.Title, choice.Id)).ToList(), + QuickShellJsonContext.Default.ListFormChoiceJson); /// /// Builds the form choices for installed companion apps, including the options for no companion app and a custom app. diff --git a/QuickShell.Core/Services/QuickShellEventSource.cs b/QuickShell.Core/Services/QuickShellEventSource.cs index 83dd0d43..2debd5fb 100644 --- a/QuickShell.Core/Services/QuickShellEventSource.cs +++ b/QuickShell.Core/Services/QuickShellEventSource.cs @@ -18,10 +18,10 @@ private QuickShellEventSource() } /// -/// Determines whether the event source is enabled for listeners. -/// -/// true if the event source is enabled; false otherwise. -bool IQuickShellEventSource.IsEnabled() => IsEnabled(); + /// Determines whether the event source is enabled for listeners. + /// + /// true if the event source is enabled; false otherwise. + bool IQuickShellEventSource.IsEnabled() => IsEnabled(); /// /// Records a row cache diagnostic event. @@ -38,23 +38,23 @@ private QuickShellEventSource() public void WritePlanCache(string kind) => PlanCache(kind ?? string.Empty); /// - /// Records the elapsed time for a startup span. - /// - /// The name of the startup span. - /// The elapsed time in milliseconds. - [NonEvent] + /// Records the elapsed time for a startup span. + /// + /// The name of the startup span. + /// The elapsed time in milliseconds. + [NonEvent] public void WriteStartupSpan(string name, double elapsedMs) => - StartupSpan(name ?? string.Empty, elapsedMs); + StartupSpan(name ?? string.Empty, elapsedMs); /// - /// Records a repository diagnostic event. - /// - /// The repository location. - /// The name of the event. - /// The elapsed time in milliseconds, or null when unavailable. - [NonEvent] + /// Records a repository diagnostic event. + /// + /// The repository location. + /// The name of the event. + /// The elapsed time in milliseconds, or null when unavailable. + [NonEvent] public void WriteRepository(string location, string eventName, long? elapsedMs = null) => - Repository(location ?? string.Empty, eventName ?? string.Empty, elapsedMs ?? -1); + Repository(location ?? string.Empty, eventName ?? string.Empty, elapsedMs ?? -1); /// /// Records a support event using the specified event code. @@ -64,12 +64,12 @@ public void WriteRepository(string location, string eventName, long? elapsedMs = public void WriteSupportEvent(string eventCode) => SupportEvent(eventCode ?? string.Empty); /// - /// Records a support write failure event. - /// - /// The type of exception associated with the failure. - [NonEvent] + /// Records a support write failure event. + /// + /// The type of exception associated with the failure. + [NonEvent] public void WriteSupportWriteFailure(string exceptionType) => - SupportWriteFailure(exceptionType ?? string.Empty); + SupportWriteFailure(exceptionType ?? string.Empty); /// /// Records completion of Git repository discovery. @@ -101,14 +101,14 @@ public void WriteSupportWriteFailure(string exceptionType) => public void StartupSpan(string name, double elapsedMs) => WriteEvent(3, name, elapsedMs); /// - /// Records the elapsed time for a repository event. - /// - /// The repository location. - /// The name of the repository event. - /// The elapsed time in milliseconds. - [Event(4, Level = EventLevel.Informational, Message = "Repository {0} {1} elapsedMs={2}")] + /// Records the elapsed time for a repository event. + /// + /// The repository location. + /// The name of the repository event. + /// The elapsed time in milliseconds. + [Event(4, Level = EventLevel.Informational, Message = "Repository {0} {1} elapsedMs={2}")] public void Repository(string location, string eventName, long elapsedMs) => - WriteEvent(4, location, eventName, elapsedMs); + WriteEvent(4, location, eventName, elapsedMs); /// /// Records a support diagnostic event. diff --git a/QuickShell.Core/Services/RowPresentationDiagnostics.cs b/QuickShell.Core/Services/RowPresentationDiagnostics.cs index 0c127f7b..96943f4c 100644 --- a/QuickShell.Core/Services/RowPresentationDiagnostics.cs +++ b/QuickShell.Core/Services/RowPresentationDiagnostics.cs @@ -48,10 +48,10 @@ public void Record(string eventName) } /// - /// Retrieves the recorded count for an event name. - /// - /// The name of the event to count. - /// The recorded count, or zero if the event has not been recorded. - public long GetCount(string eventName) => - _counters.TryGetValue(eventName, out var count) ? count : 0; + /// Retrieves the recorded count for an event name. + /// + /// The name of the event to count. + /// The recorded count, or zero if the event has not been recorded. + public long GetCount(string eventName) => + _counters.TryGetValue(eventName, out var count) ? count : 0; } diff --git a/QuickShell.Core/Services/WslPathResolver.cs b/QuickShell.Core/Services/WslPathResolver.cs index e849dfb0..232ba4c9 100644 --- a/QuickShell.Core/Services/WslPathResolver.cs +++ b/QuickShell.Core/Services/WslPathResolver.cs @@ -142,16 +142,39 @@ public static string ResolveDistro(WslLocation location, LaunchTarget target) => private static bool TryParseUncRemainder(string remainder, string fullUnc, out WslLocation location) { location = null!; - var parts = remainder.Split('\\', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length < 2) + // Bolt: Performance optimization - avoid string.Split(), Linq Skip(), and string.Join() allocations. + var span = remainder.AsSpan(); + string? distro = null; + var linuxPathBuilder = new System.Text.StringBuilder(); + + foreach (var range in span.Split('\\')) + { + var part = span[range]; + if (part.IsEmpty) + { + continue; + } + + if (distro is null) + { + distro = part.ToString(); + } + else + { + linuxPathBuilder.Append('/'); + linuxPathBuilder.Append(part); + } + } + + if (linuxPathBuilder.Length == 0 || distro is null) { return false; } location = new WslLocation { - Distro = parts[0], - LinuxPath = "/" + string.Join('/', parts.Skip(1)), + Distro = distro, + LinuxPath = linuxPathBuilder.ToString(), UncPath = fullUnc, }; From 598759b074578e187761633d6cc3cd45f605fa31 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:53:40 +0000 Subject: [PATCH 2/3] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- BENCHMARKS.md | 18 +++++ QuickShell.Core.Tests/WslPathResolverTests.cs | 65 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index b99cab9f..0446af4f 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -11,6 +11,7 @@ they are reproducible as code changes land. | Provider ctor | `QuickShellCommandsProvider` ctor | Background git prewarm + settings prewarm are fire-and-forget and do not block the ctor | | List reload | `QuickShellPage.Reload()` | Builds the home-list rows from the workspace snapshot | | List `GetItems` | `QuickShellPage.GetItems()` | Cached read after a reload | +| WSL UNC parsing | `WslPathResolver.TryParse` | Compares old string.Split+LINQ vs new AsSpan+StringBuilder implementation for UNC path parsing (wall time, allocations, Gen0 collections) | Each number is the **warm** figure: the harness runs the path once to pay JIT / first-call costs, then times a second invocation. The provider ctor also prints its internal breakdown @@ -18,6 +19,8 @@ via the existing `QUICKSHELL_STARTUP_TRACE=1` instrumentation. ## How to run +### Startup performance measurements + ```powershell dotnet test QuickShell.Core.Tests/QuickShell.Core.Tests.csproj -c Release -p:Platform=x64 ` --filter "FullyQualifiedName~StartupPerformanceMeasurementsTests" ` @@ -31,6 +34,21 @@ dotnet test QuickShell.Core.Tests/QuickShell.Core.Tests.csproj -c Release -p:Pla User configuration is not mutated. Performance artifacts (including `real-machine-workspace-count.json`) are written to the configured output directory (override with `QUICKSHELL_PERF_OUTPUT_DIR`). +### WSL UNC path parsing performance + +```powershell +dotnet test QuickShell.Core.Tests/QuickShell.Core.Tests.csproj -c Release -p:Platform=x64 ` + --filter "FullyQualifiedName~WslPathResolverPerformanceMeasurementsTests" ` + --logger "console;verbosity=detailed" +``` + +- `Measure_UncParsing_OldVsNew_StringSplitLinqVsSpanStringBuilder` — compares the original + `string.Split` + LINQ (`Skip`, `Where`) + `string.Join` implementation against the optimized + `AsSpan` + `StringBuilder` implementation for parsing WSL UNC paths like `\\wsl$\Ubuntu\home\user\project`. + Measures wall-clock time, allocated bytes (`GC.GetAllocatedBytesForCurrentThread`), and Gen0 + collection counts over 10,000 iterations with realistic UNC path inputs. Informational only; + no hard assertions on timing thresholds. + Add new numbers below after meaningful merges. Capture `dotnet --version`, the TF, and a one-line machine note (CPU / SSD) so comparisons stay honest. diff --git a/QuickShell.Core.Tests/WslPathResolverTests.cs b/QuickShell.Core.Tests/WslPathResolverTests.cs index a7f08cdb..b1efc5a2 100644 --- a/QuickShell.Core.Tests/WslPathResolverTests.cs +++ b/QuickShell.Core.Tests/WslPathResolverTests.cs @@ -56,4 +56,69 @@ public void ToWslArguments_UsesConvertedLinuxPathForWindowsWorkspace() Assert.Contains("bash -lc", args, StringComparison.Ordinal); Assert.Contains("npm run dev", args, StringComparison.Ordinal); } + + [Theory] + [InlineData(@"\\wsl$\Ubuntu\home\user\project", "Ubuntu", "/home/user/project")] + [InlineData(@"\\wsl.localhost\Debian\home\user\project", "Debian", "/home/user/project")] + [InlineData(@"\\wsl$\Ubuntu-20.04\home\dev", "Ubuntu-20.04", "/home/dev")] + [InlineData(@"\\wsl.localhost\Alpine\root\workspace\nested\path", "Alpine", "/root/workspace/nested/path")] + public void TryParse_UncPath_ReturnsValidLocation(string uncPath, string expectedDistro, string expectedLinuxPath) + { + var result = WslPathResolver.TryParse(uncPath, out var location); + + Assert.True(result); + Assert.NotNull(location); + Assert.Equal(expectedDistro, location.Distro); + Assert.Equal(expectedLinuxPath, location.LinuxPath); + Assert.Equal(uncPath, location.UncPath); + } + + [Theory] + [InlineData(@"\\wsl$\Ubuntu\\home\\user", "Ubuntu", "/home/user")] + [InlineData(@"\\wsl.localhost\Debian\\\\home", "Debian", "/home")] + public void TryParse_UncPath_WithRepeatedSeparators_SkipsEmptyParts(string uncPath, string expectedDistro, string expectedLinuxPath) + { + var result = WslPathResolver.TryParse(uncPath, out var location); + + Assert.True(result); + Assert.NotNull(location); + Assert.Equal(expectedDistro, location.Distro); + Assert.Equal(expectedLinuxPath, location.LinuxPath); + } + + [Theory] + [InlineData(@"\\wsl$\Ubuntu\home\user\", "Ubuntu", "/home/user")] + [InlineData(@"\\wsl.localhost\Debian\home\", "Debian", "/home")] + public void TryParse_UncPath_WithTrailingSeparator_IgnoresTrailingEmpty(string uncPath, string expectedDistro, string expectedLinuxPath) + { + var result = WslPathResolver.TryParse(uncPath, out var location); + + Assert.True(result); + Assert.NotNull(location); + Assert.Equal(expectedDistro, location.Distro); + Assert.Equal(expectedLinuxPath, location.LinuxPath); + } + + [Theory] + [InlineData(@"\\wsl$\")] + [InlineData(@"\\wsl$\\")] + [InlineData(@"\\wsl.localhost\")] + [InlineData(@"\\wsl.localhost\\")] + public void TryParse_UncPath_MissingDistro_ReturnsFalse(string uncPath) + { + var result = WslPathResolver.TryParse(uncPath, out var location); + + Assert.False(result); + } + + [Theory] + [InlineData(@"\\wsl$\Ubuntu")] + [InlineData(@"\\wsl.localhost\Debian")] + [InlineData(@"\\wsl$\Ubuntu\")] + public void TryParse_UncPath_OnlyDistroNoLinuxPath_ReturnsFalse(string uncPath) + { + var result = WslPathResolver.TryParse(uncPath, out var location); + + Assert.False(result); + } } From c1e8a808d5a0d6ba221bc4ccc84f103b82e8b344 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:25:46 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[Performance=20optimiza?= =?UTF-8?q?tion]=20Avoid=20string=20array=20and=20string=20allocations=20w?= =?UTF-8?q?hen=20parsing=20WSL=20UNC=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 3 + BENCHMARKS.md | 18 ----- QuickShell.Core.Tests/WslPathResolverTests.cs | 65 ---------------- .../Abstractions/IQuickShellEventSource.cs | 76 +++++++++---------- .../Classification/ProjectAnalysisService.cs | 12 +-- QuickShell.Core/Interop/ShellFileDialog.cs | 68 ++++++++--------- .../Services/CompanionAppCatalog.cs | 16 ++-- .../Services/QuickShellEventSource.cs | 58 +++++++------- .../Services/RowPresentationDiagnostics.cs | 12 +-- 9 files changed, 124 insertions(+), 204 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..b7b48382 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-24 - WslPathResolver string splitting +**Learning:** `WslPathResolver.TryParseUncRemainder` used `string.Split()`, `string.Join()`, and `Enumerable.Skip()` which leads to unnecessary GC pressure and multiple string/array allocations for hot path code. +**Action:** Replace `string.Split` with `ReadOnlySpan.Split` and use `StringBuilder` to append portions sequentially when parsing and reconstructing path parts. This saves multiple array/string allocations and makes parsing faster. diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 0446af4f..b99cab9f 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -11,7 +11,6 @@ they are reproducible as code changes land. | Provider ctor | `QuickShellCommandsProvider` ctor | Background git prewarm + settings prewarm are fire-and-forget and do not block the ctor | | List reload | `QuickShellPage.Reload()` | Builds the home-list rows from the workspace snapshot | | List `GetItems` | `QuickShellPage.GetItems()` | Cached read after a reload | -| WSL UNC parsing | `WslPathResolver.TryParse` | Compares old string.Split+LINQ vs new AsSpan+StringBuilder implementation for UNC path parsing (wall time, allocations, Gen0 collections) | Each number is the **warm** figure: the harness runs the path once to pay JIT / first-call costs, then times a second invocation. The provider ctor also prints its internal breakdown @@ -19,8 +18,6 @@ via the existing `QUICKSHELL_STARTUP_TRACE=1` instrumentation. ## How to run -### Startup performance measurements - ```powershell dotnet test QuickShell.Core.Tests/QuickShell.Core.Tests.csproj -c Release -p:Platform=x64 ` --filter "FullyQualifiedName~StartupPerformanceMeasurementsTests" ` @@ -34,21 +31,6 @@ dotnet test QuickShell.Core.Tests/QuickShell.Core.Tests.csproj -c Release -p:Pla User configuration is not mutated. Performance artifacts (including `real-machine-workspace-count.json`) are written to the configured output directory (override with `QUICKSHELL_PERF_OUTPUT_DIR`). -### WSL UNC path parsing performance - -```powershell -dotnet test QuickShell.Core.Tests/QuickShell.Core.Tests.csproj -c Release -p:Platform=x64 ` - --filter "FullyQualifiedName~WslPathResolverPerformanceMeasurementsTests" ` - --logger "console;verbosity=detailed" -``` - -- `Measure_UncParsing_OldVsNew_StringSplitLinqVsSpanStringBuilder` — compares the original - `string.Split` + LINQ (`Skip`, `Where`) + `string.Join` implementation against the optimized - `AsSpan` + `StringBuilder` implementation for parsing WSL UNC paths like `\\wsl$\Ubuntu\home\user\project`. - Measures wall-clock time, allocated bytes (`GC.GetAllocatedBytesForCurrentThread`), and Gen0 - collection counts over 10,000 iterations with realistic UNC path inputs. Informational only; - no hard assertions on timing thresholds. - Add new numbers below after meaningful merges. Capture `dotnet --version`, the TF, and a one-line machine note (CPU / SSD) so comparisons stay honest. diff --git a/QuickShell.Core.Tests/WslPathResolverTests.cs b/QuickShell.Core.Tests/WslPathResolverTests.cs index b1efc5a2..a7f08cdb 100644 --- a/QuickShell.Core.Tests/WslPathResolverTests.cs +++ b/QuickShell.Core.Tests/WslPathResolverTests.cs @@ -56,69 +56,4 @@ public void ToWslArguments_UsesConvertedLinuxPathForWindowsWorkspace() Assert.Contains("bash -lc", args, StringComparison.Ordinal); Assert.Contains("npm run dev", args, StringComparison.Ordinal); } - - [Theory] - [InlineData(@"\\wsl$\Ubuntu\home\user\project", "Ubuntu", "/home/user/project")] - [InlineData(@"\\wsl.localhost\Debian\home\user\project", "Debian", "/home/user/project")] - [InlineData(@"\\wsl$\Ubuntu-20.04\home\dev", "Ubuntu-20.04", "/home/dev")] - [InlineData(@"\\wsl.localhost\Alpine\root\workspace\nested\path", "Alpine", "/root/workspace/nested/path")] - public void TryParse_UncPath_ReturnsValidLocation(string uncPath, string expectedDistro, string expectedLinuxPath) - { - var result = WslPathResolver.TryParse(uncPath, out var location); - - Assert.True(result); - Assert.NotNull(location); - Assert.Equal(expectedDistro, location.Distro); - Assert.Equal(expectedLinuxPath, location.LinuxPath); - Assert.Equal(uncPath, location.UncPath); - } - - [Theory] - [InlineData(@"\\wsl$\Ubuntu\\home\\user", "Ubuntu", "/home/user")] - [InlineData(@"\\wsl.localhost\Debian\\\\home", "Debian", "/home")] - public void TryParse_UncPath_WithRepeatedSeparators_SkipsEmptyParts(string uncPath, string expectedDistro, string expectedLinuxPath) - { - var result = WslPathResolver.TryParse(uncPath, out var location); - - Assert.True(result); - Assert.NotNull(location); - Assert.Equal(expectedDistro, location.Distro); - Assert.Equal(expectedLinuxPath, location.LinuxPath); - } - - [Theory] - [InlineData(@"\\wsl$\Ubuntu\home\user\", "Ubuntu", "/home/user")] - [InlineData(@"\\wsl.localhost\Debian\home\", "Debian", "/home")] - public void TryParse_UncPath_WithTrailingSeparator_IgnoresTrailingEmpty(string uncPath, string expectedDistro, string expectedLinuxPath) - { - var result = WslPathResolver.TryParse(uncPath, out var location); - - Assert.True(result); - Assert.NotNull(location); - Assert.Equal(expectedDistro, location.Distro); - Assert.Equal(expectedLinuxPath, location.LinuxPath); - } - - [Theory] - [InlineData(@"\\wsl$\")] - [InlineData(@"\\wsl$\\")] - [InlineData(@"\\wsl.localhost\")] - [InlineData(@"\\wsl.localhost\\")] - public void TryParse_UncPath_MissingDistro_ReturnsFalse(string uncPath) - { - var result = WslPathResolver.TryParse(uncPath, out var location); - - Assert.False(result); - } - - [Theory] - [InlineData(@"\\wsl$\Ubuntu")] - [InlineData(@"\\wsl.localhost\Debian")] - [InlineData(@"\\wsl$\Ubuntu\")] - public void TryParse_UncPath_OnlyDistroNoLinuxPath_ReturnsFalse(string uncPath) - { - var result = WslPathResolver.TryParse(uncPath, out var location); - - Assert.False(result); - } } diff --git a/QuickShell.Core/Abstractions/IQuickShellEventSource.cs b/QuickShell.Core/Abstractions/IQuickShellEventSource.cs index 5162148f..a4c4382b 100644 --- a/QuickShell.Core/Abstractions/IQuickShellEventSource.cs +++ b/QuickShell.Core/Abstractions/IQuickShellEventSource.cs @@ -7,56 +7,56 @@ namespace QuickShell.Abstractions; internal interface IQuickShellEventSource { /// - /// Determines whether diagnostic event writing is enabled. - /// - /// true if diagnostic event writing is enabled; otherwise, false. - bool IsEnabled(); +/// Determines whether diagnostic event writing is enabled. +/// +/// true if diagnostic event writing is enabled; otherwise, false. +bool IsEnabled(); /// - /// Emits a diagnostic event for row-cache activity. - /// - /// The kind of row-cache activity. - void WriteRowCache(string kind); +/// Emits a diagnostic event for row-cache activity. +/// +/// The kind of row-cache activity. +void WriteRowCache(string kind); /// - /// Emits a diagnostic event for plan-cache activity. - /// - /// The type of plan-cache activity. - void WritePlanCache(string kind); +/// Emits a diagnostic event for plan-cache activity. +/// +/// The type of plan-cache activity. +void WritePlanCache(string kind); /// - /// Records the duration of a startup operation. - /// - /// The name identifying the startup operation. - /// The operation duration in milliseconds. - void WriteStartupSpan(string name, double elapsedMs); +/// Records the duration of a startup operation. +/// +/// The name identifying the startup operation. +/// The operation duration in milliseconds. +void WriteStartupSpan(string name, double elapsedMs); /// - /// Records a repository-related diagnostic event. - /// - /// The repository location associated with the event. - /// The name of the repository event. - /// - /// The optional elapsed time in milliseconds. A value is recorded as - /// -1, meaning the duration is unavailable rather than a real measured latency. - /// - void WriteRepository(string location, string eventName, long? elapsedMs = null); +/// Records a repository-related diagnostic event. +/// +/// The repository location associated with the event. +/// The name of the repository event. +/// +/// The optional elapsed time in milliseconds. A value is recorded as +/// -1, meaning the duration is unavailable rather than a real measured latency. +/// +void WriteRepository(string location, string eventName, long? elapsedMs = null); /// - /// Emits a support diagnostic event identified by an event code. - /// - /// The identifier for the support diagnostic event. - void WriteSupportEvent(string eventCode); +/// Emits a support diagnostic event identified by an event code. +/// +/// The identifier for the support diagnostic event. +void WriteSupportEvent(string eventCode); /// - /// Records a diagnostic event for a failure to write support diagnostics. - /// - /// The type of exception that caused the write failure. - void WriteSupportWriteFailure(string exceptionType); +/// Records a diagnostic event for a failure to write support diagnostics. +/// +/// The type of exception that caused the write failure. +void WriteSupportWriteFailure(string exceptionType); /// - /// Records the completion of Git repository discovery. - /// - /// The number of repositories discovered. - void WriteGitDiscoveryComplete(int repoCount); +/// Records the completion of Git repository discovery. +/// +/// The number of repositories discovered. +void WriteGitDiscoveryComplete(int repoCount); } diff --git a/QuickShell.Core/Classification/ProjectAnalysisService.cs b/QuickShell.Core/Classification/ProjectAnalysisService.cs index 5564606c..643ccc70 100644 --- a/QuickShell.Core/Classification/ProjectAnalysisService.cs +++ b/QuickShell.Core/Classification/ProjectAnalysisService.cs @@ -133,12 +133,12 @@ public string BuildTaskTypeChoicesJson( } /// - /// Suggests a companion app for the specified project directory. - /// - /// The project directory to inspect. - /// A companion app suggestion, or null when no suggestion is available. - public CompanionAppSuggestion? TrySuggestCompanionApp(string directory) => - _companionAppDetector.TrySuggest(directory); + /// Suggests a companion app for the specified project directory. + /// + /// The project directory to inspect. + /// A companion app suggestion, or null when no suggestion is available. + public CompanionAppSuggestion? TrySuggestCompanionApp(string directory) => + _companionAppDetector.TrySuggest(directory); public string? TryDetectDevServerUrl(string directory) => _devServerDetector.TryDetectDevServerUrl(directory); diff --git a/QuickShell.Core/Interop/ShellFileDialog.cs b/QuickShell.Core/Interop/ShellFileDialog.cs index 09995307..82124719 100644 --- a/QuickShell.Core/Interop/ShellFileDialog.cs +++ b/QuickShell.Core/Interop/ShellFileDialog.cs @@ -364,17 +364,17 @@ internal partial interface IModalWindow internal partial interface IFileDialog : IModalWindow { /// - /// Configures the file types displayed by the dialog. - /// - /// The number of file type specifications. - /// A pointer to the array of file type specifications. - void SetFileTypes(uint cFileTypes, nint rgFilterSpec); +/// Configures the file types displayed by the dialog. +/// +/// The number of file type specifications. +/// A pointer to the array of file type specifications. +void SetFileTypes(uint cFileTypes, nint rgFilterSpec); /// - /// Selects the default file type in the dialog's file type list. - /// - /// The one-based index of the file type to select. - void SetFileTypeIndex(uint iFileType); +/// Selects the default file type in the dialog's file type list. +/// +/// The one-based index of the file type to select. +void SetFileTypeIndex(uint iFileType); /// /// Retrieves the index of the currently selected file type. @@ -396,10 +396,10 @@ internal partial interface IFileDialog : IModalWindow int Unadvise(uint dwCookie); /// - /// Configures the file dialog options. - /// - /// The bitwise combination of file dialog option flags. - void SetOptions(uint fos); +/// Configures the file dialog options. +/// +/// The bitwise combination of file dialog option flags. +void SetOptions(uint fos); /// /// Retrieves the current options configured for the file dialog. @@ -418,10 +418,10 @@ internal partial interface IFileDialog : IModalWindow int SetDefaultFolder(nint psi); /// - /// Sets the folder displayed by the file dialog. - /// - /// The shell item representing the folder to display. - void SetFolder(IShellItem psi); +/// Sets the folder displayed by the file dialog. +/// +/// The shell item representing the folder to display. +void SetFolder(IShellItem psi); /// /// Retrieves the dialog's current folder. @@ -440,10 +440,10 @@ internal partial interface IFileDialog : IModalWindow int GetCurrentSelection(out nint ppsi); /// - /// Sets the file name displayed in the dialog. - /// - /// The file name to display. - void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); +/// Sets the file name displayed in the dialog. +/// +/// The file name to display. +void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); /// /// Retrieves the current file name displayed by the dialog. @@ -454,9 +454,9 @@ internal partial interface IFileDialog : IModalWindow int GetFileName(out nint pszName); /// - /// Sets the dialog title. - /// - void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); +/// Sets the dialog title. +/// +void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); /// /// Sets the label of the dialog's OK button. @@ -475,11 +475,11 @@ internal partial interface IFileDialog : IModalWindow int SetFileNameLabel(nint pszLabel); /// - /// Retrieves the selected shell item. - /// - /// Receives a pointer to the selected shell item. - /// - void GetResult(out nint ppsi); +/// Retrieves the selected shell item. +/// +/// Receives a pointer to the selected shell item. +/// +void GetResult(out nint ppsi); /// /// Adds a location to the dialog's navigation pane. @@ -611,11 +611,11 @@ internal partial interface IShellItem int GetParent(out nint ppsi); /// - /// Retrieves the item's display name in the specified format. - /// - /// The display name format to retrieve. - /// Receives a pointer to the allocated display-name string. - void GetDisplayName(uint sigdnName, out nint ppszName); +/// Retrieves the item's display name in the specified format. +/// +/// The display name format to retrieve. +/// Receives a pointer to the allocated display-name string. +void GetDisplayName(uint sigdnName, out nint ppszName); /// /// Retrieves the specified attributes of the shell item. diff --git a/QuickShell.Core/Services/CompanionAppCatalog.cs b/QuickShell.Core/Services/CompanionAppCatalog.cs index 0b32c196..1339f138 100644 --- a/QuickShell.Core/Services/CompanionAppCatalog.cs +++ b/QuickShell.Core/Services/CompanionAppCatalog.cs @@ -166,14 +166,14 @@ private static void EnsureFormChoicesCached() } /// - /// Serializes form choices into the JSON representation used by the companion app selector. - /// - /// The form choice identifiers and display titles. - /// The serialized form choices. - private static string SerializeFormChoices(IReadOnlyList<(string Id, string Title)> choices) => -JsonSerializer.Serialize( - choices.Select(choice => new FormChoiceJson(choice.Title, choice.Id)).ToList(), - QuickShellJsonContext.Default.ListFormChoiceJson); + /// Serializes form choices into the JSON representation used by the companion app selector. + /// + /// The form choice identifiers and display titles. + /// The serialized form choices. + private static string SerializeFormChoices(IReadOnlyList<(string Id, string Title)> choices) => + JsonSerializer.Serialize( + choices.Select(choice => new FormChoiceJson(choice.Title, choice.Id)).ToList(), + QuickShellJsonContext.Default.ListFormChoiceJson); /// /// Builds the form choices for installed companion apps, including the options for no companion app and a custom app. diff --git a/QuickShell.Core/Services/QuickShellEventSource.cs b/QuickShell.Core/Services/QuickShellEventSource.cs index 2debd5fb..83dd0d43 100644 --- a/QuickShell.Core/Services/QuickShellEventSource.cs +++ b/QuickShell.Core/Services/QuickShellEventSource.cs @@ -18,10 +18,10 @@ private QuickShellEventSource() } /// - /// Determines whether the event source is enabled for listeners. - /// - /// true if the event source is enabled; false otherwise. - bool IQuickShellEventSource.IsEnabled() => IsEnabled(); +/// Determines whether the event source is enabled for listeners. +/// +/// true if the event source is enabled; false otherwise. +bool IQuickShellEventSource.IsEnabled() => IsEnabled(); /// /// Records a row cache diagnostic event. @@ -38,23 +38,23 @@ private QuickShellEventSource() public void WritePlanCache(string kind) => PlanCache(kind ?? string.Empty); /// - /// Records the elapsed time for a startup span. - /// - /// The name of the startup span. - /// The elapsed time in milliseconds. - [NonEvent] + /// Records the elapsed time for a startup span. + /// + /// The name of the startup span. + /// The elapsed time in milliseconds. + [NonEvent] public void WriteStartupSpan(string name, double elapsedMs) => - StartupSpan(name ?? string.Empty, elapsedMs); + StartupSpan(name ?? string.Empty, elapsedMs); /// - /// Records a repository diagnostic event. - /// - /// The repository location. - /// The name of the event. - /// The elapsed time in milliseconds, or null when unavailable. - [NonEvent] + /// Records a repository diagnostic event. + /// + /// The repository location. + /// The name of the event. + /// The elapsed time in milliseconds, or null when unavailable. + [NonEvent] public void WriteRepository(string location, string eventName, long? elapsedMs = null) => - Repository(location ?? string.Empty, eventName ?? string.Empty, elapsedMs ?? -1); + Repository(location ?? string.Empty, eventName ?? string.Empty, elapsedMs ?? -1); /// /// Records a support event using the specified event code. @@ -64,12 +64,12 @@ public void WriteRepository(string location, string eventName, long? elapsedMs = public void WriteSupportEvent(string eventCode) => SupportEvent(eventCode ?? string.Empty); /// - /// Records a support write failure event. - /// - /// The type of exception associated with the failure. - [NonEvent] + /// Records a support write failure event. + /// + /// The type of exception associated with the failure. + [NonEvent] public void WriteSupportWriteFailure(string exceptionType) => - SupportWriteFailure(exceptionType ?? string.Empty); + SupportWriteFailure(exceptionType ?? string.Empty); /// /// Records completion of Git repository discovery. @@ -101,14 +101,14 @@ public void WriteSupportWriteFailure(string exceptionType) => public void StartupSpan(string name, double elapsedMs) => WriteEvent(3, name, elapsedMs); /// - /// Records the elapsed time for a repository event. - /// - /// The repository location. - /// The name of the repository event. - /// The elapsed time in milliseconds. - [Event(4, Level = EventLevel.Informational, Message = "Repository {0} {1} elapsedMs={2}")] + /// Records the elapsed time for a repository event. + /// + /// The repository location. + /// The name of the repository event. + /// The elapsed time in milliseconds. + [Event(4, Level = EventLevel.Informational, Message = "Repository {0} {1} elapsedMs={2}")] public void Repository(string location, string eventName, long elapsedMs) => - WriteEvent(4, location, eventName, elapsedMs); + WriteEvent(4, location, eventName, elapsedMs); /// /// Records a support diagnostic event. diff --git a/QuickShell.Core/Services/RowPresentationDiagnostics.cs b/QuickShell.Core/Services/RowPresentationDiagnostics.cs index 96943f4c..0c127f7b 100644 --- a/QuickShell.Core/Services/RowPresentationDiagnostics.cs +++ b/QuickShell.Core/Services/RowPresentationDiagnostics.cs @@ -48,10 +48,10 @@ public void Record(string eventName) } /// - /// Retrieves the recorded count for an event name. - /// - /// The name of the event to count. - /// The recorded count, or zero if the event has not been recorded. - public long GetCount(string eventName) => - _counters.TryGetValue(eventName, out var count) ? count : 0; + /// Retrieves the recorded count for an event name. + /// + /// The name of the event to count. + /// The recorded count, or zero if the event has not been recorded. + public long GetCount(string eventName) => + _counters.TryGetValue(eventName, out var count) ? count : 0; }