From 3e66e31d9943a683b8a685e4dc39cb74e855a749 Mon Sep 17 00:00:00 2001 From: Timothy Makkison Date: Thu, 6 Nov 2025 23:49:06 +0000 Subject: [PATCH 1/2] perf: optimise `IgnoreFile` using `ValueTask` and `Span.Replace` --- Src/CSharpier.Cli/IgnoreFile.cs | 28 +++++++++-- Src/CSharpier.Cli/Options/OptionsProvider.cs | 53 ++++++++++++-------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/Src/CSharpier.Cli/IgnoreFile.cs b/Src/CSharpier.Cli/IgnoreFile.cs index 23c23edd6..886bb6206 100644 --- a/Src/CSharpier.Cli/IgnoreFile.cs +++ b/Src/CSharpier.Cli/IgnoreFile.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Diagnostics; using System.IO.Abstractions; using System.Text.RegularExpressions; @@ -31,7 +32,15 @@ private IgnoreFile(List ignores) public bool IsIgnored(string filePath) { - filePath = filePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + Span pathRelativeToIgnoreFile = + filePath.Length <= 256 ? stackalloc char[256] : new char[filePath.Length]; + + pathRelativeToIgnoreFile = pathRelativeToIgnoreFile[..filePath.Length]; + filePath.CopyTo(pathRelativeToIgnoreFile); + pathRelativeToIgnoreFile.Replace( + Path.AltDirectorySeparatorChar, + Path.DirectorySeparatorChar + ); foreach (var ignore in this.Ignores) { @@ -185,10 +194,19 @@ private class IgnoreWithBasePath(string basePath) return (false, false); } - var pathRelativeToIgnoreFile = - path.Length > basePath.Length - ? path[basePath.Length..].Replace('\\', '/') - : string.Empty; + var relativePathLength = path.Length - basePath.Length; + Span pathRelativeToIgnoreFile = + relativePathLength <= 256 ? stackalloc char[256] : new char[path.Length]; + if (path.Length > basePath.Length) + { + path.AsSpan()[basePath.Length..].CopyTo(pathRelativeToIgnoreFile); + pathRelativeToIgnoreFile = pathRelativeToIgnoreFile[..relativePathLength]; + pathRelativeToIgnoreFile.Replace('\\', '/'); + } + else + { + pathRelativeToIgnoreFile = []; + } var isIgnored = false; var hasMatchingRule = false; diff --git a/Src/CSharpier.Cli/Options/OptionsProvider.cs b/Src/CSharpier.Cli/Options/OptionsProvider.cs index 0eb094614..b8ff22628 100644 --- a/Src/CSharpier.Cli/Options/OptionsProvider.cs +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -34,7 +34,7 @@ ILogger logger this.logger = logger; } - public static async Task Create( + public static async ValueTask Create( string directoryName, string? configPath, string? ignorePath, @@ -109,7 +109,7 @@ await EditorConfigLocator.FindForDirectoryNameAsync( return optionsProvider; } - public async Task GetPrinterOptionsForAsync( + public async ValueTask GetPrinterOptionsForAsync( string filePath, CancellationToken cancellationToken ) @@ -147,30 +147,32 @@ CancellationToken cancellationToken return formatter != Formatter.Unknown ? new PrinterOptions(formatter) : null; } - private Task FindCSharpierConfigAsync(string directoryName) + private ValueTask FindCSharpierConfigAsync(string directoryName) { return this.FindFileAsync( directoryName, this.csharpierConfigsByDirectory, - searchingDirectory => - this.fileSystem.Directory.EnumerateFiles( + (searchingDirectory, cancellationToken) => + this + .fileSystem.Directory.EnumerateFiles( searchingDirectory, ".csharpierrc*", SearchOption.TopDirectoryOnly ) .Any(), - searchingDirectory => + (searchingDirectory, cancellationToken) => Task.FromResult( CSharpierConfigParser.FindForDirectoryName( searchingDirectory, this.fileSystem, this.logger ) - ) + ), + CancellationToken.None ); } - private async Task FindEditorConfigAsync( + private async ValueTask FindEditorConfigAsync( string directoryName, CancellationToken cancellationToken ) @@ -178,19 +180,20 @@ CancellationToken cancellationToken return await this.FindFileAsync( directoryName, this.editorConfigByDirectory, - searchingDirectory => + (searchingDirectory, cancellationToken) => this.fileSystem.File.Exists(Path.Combine(searchingDirectory, ".editorconfig")), - async searchingDirectory => + async (searchingDirectory, cancellationToken) => await EditorConfigLocator.FindForDirectoryNameAsync( searchingDirectory, this.fileSystem, await this.FindIgnoreFileAsync(searchingDirectory, cancellationToken), cancellationToken - ) + ), + cancellationToken ); } - private async Task FindIgnoreFileAsync( + private async ValueTask FindIgnoreFileAsync( string directoryName, CancellationToken cancellationToken ) @@ -198,13 +201,19 @@ CancellationToken cancellationToken var ignoreFile = await this.FindFileAsync( directoryName, this.ignoreFilesByDirectory, - (searchingDirectory) => + (searchingDirectory, cancellationToken) => this.fileSystem.File.Exists(Path.Combine(searchingDirectory, ".gitignore")) || this.fileSystem.File.Exists( Path.Combine(searchingDirectory, ".csharpierignore") ), - (searchingDirectory) => - IgnoreFile.CreateAsync(searchingDirectory, this.fileSystem, null, cancellationToken) + (searchingDirectory, cancellationToken) => + IgnoreFile.CreateAsync( + searchingDirectory, + this.fileSystem, + null, + cancellationToken + ), + cancellationToken ); #pragma warning disable IDE0270 @@ -223,11 +232,12 @@ CancellationToken cancellationToken /// When trying to format a file in a given subdirectory if we've already found the appropriate file type then return it /// otherwise track it down (parsing if we need to) and set the references for any parent directories /// - private async Task FindFileAsync( + private async ValueTask FindFileAsync( string directoryName, ConcurrentDictionary dictionary, - Func shouldConsiderDirectory, - Func> createFileAsync + Func shouldConsiderDirectory, + Func> createFileAsync, + CancellationToken cancellationToken ) { if (dictionary.TryGetValue(directoryName, out var result)) @@ -242,10 +252,11 @@ searchingDirectory is not null && !dictionary.TryGetValue(searchingDirectory.FullName, out result) ) { - if (shouldConsiderDirectory(searchingDirectory.FullName)) + if (shouldConsiderDirectory(searchingDirectory.FullName, cancellationToken)) { dictionary[searchingDirectory.FullName] = result = await createFileAsync( - searchingDirectory.FullName + searchingDirectory.FullName, + cancellationToken ); break; } @@ -262,7 +273,7 @@ searchingDirectory is not null return result; } - public async Task IsIgnoredAsync( + public async ValueTask IsIgnoredAsync( string actualFilePath, CancellationToken cancellationToken ) From 1232ee69c2d92c05bab9dfa77e61189619df9c7c Mon Sep 17 00:00:00 2001 From: Timothy Makkison Date: Sat, 8 Nov 2025 17:42:09 +0000 Subject: [PATCH 2/2] perf: move `async` to separate methods --- Src/CSharpier.Cli/Options/OptionsProvider.cs | 93 +++++++++++++++----- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/Src/CSharpier.Cli/Options/OptionsProvider.cs b/Src/CSharpier.Cli/Options/OptionsProvider.cs index b8ff22628..ecf1e9405 100644 --- a/Src/CSharpier.Cli/Options/OptionsProvider.cs +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -172,12 +172,12 @@ CancellationToken cancellationToken ); } - private async ValueTask FindEditorConfigAsync( + private ValueTask FindEditorConfigAsync( string directoryName, CancellationToken cancellationToken ) { - return await this.FindFileAsync( + return this.FindFileAsync( directoryName, this.editorConfigByDirectory, (searchingDirectory, cancellationToken) => @@ -193,15 +193,15 @@ await this.FindIgnoreFileAsync(searchingDirectory, cancellationToken), ); } - private async ValueTask FindIgnoreFileAsync( + private ValueTask FindIgnoreFileAsync( string directoryName, CancellationToken cancellationToken ) { - var ignoreFile = await this.FindFileAsync( + var ignoreFileTask = this.FindFileAsync( directoryName, this.ignoreFilesByDirectory, - (searchingDirectory, cancellationToken) => + (searchingDirectory, _) => this.fileSystem.File.Exists(Path.Combine(searchingDirectory, ".gitignore")) || this.fileSystem.File.Exists( Path.Combine(searchingDirectory, ".csharpierignore") @@ -216,15 +216,37 @@ CancellationToken cancellationToken cancellationToken ); -#pragma warning disable IDE0270 - if (ignoreFile is null) + if (ignoreFileTask.IsCompletedSuccessfully) { - // should never happen - throw new Exception("Unable to locate an IgnoreFile for " + directoryName); - } +#pragma warning disable IDE0270 + if (ignoreFileTask.Result is null) + { + // should never happen + throw new Exception("Unable to locate an IgnoreFile for " + directoryName); + } #pragma warning restore IDE0270 - return ignoreFile; + return ignoreFileTask!; + } + + return new ValueTask(FindIgnoreFileAsyncInner(ignoreFileTask, directoryName)); + + static async Task FindIgnoreFileAsyncInner( + ValueTask ignoreFileTask, + string directoryName + ) + { + var ignoreFile = await ignoreFileTask; + +#pragma warning disable IDE0270 + if (ignoreFile is null) + { + // should never happen + throw new Exception("Unable to locate an IgnoreFile for " + directoryName); + } +#pragma warning restore IDE0270 + return ignoreFile; + } } /// @@ -232,7 +254,7 @@ CancellationToken cancellationToken /// When trying to format a file in a given subdirectory if we've already found the appropriate file type then return it /// otherwise track it down (parsing if we need to) and set the references for any parent directories /// - private async ValueTask FindFileAsync( + private ValueTask FindFileAsync( string directoryName, ConcurrentDictionary dictionary, Func shouldConsiderDirectory, @@ -242,9 +264,27 @@ CancellationToken cancellationToken { if (dictionary.TryGetValue(directoryName, out var result)) { - return result; + return new ValueTask(result); } + return FindFileAsyncInner( + directoryName, + dictionary, + shouldConsiderDirectory, + createFileAsync, + cancellationToken + ); + } + + private async ValueTask FindFileAsyncInner( + string directoryName, + ConcurrentDictionary dictionary, + Func shouldConsiderDirectory, + Func> createFileAsync, + CancellationToken cancellationToken + ) + { + T? result = default; var directoriesToSet = new List(); var searchingDirectory = this.fileSystem.DirectoryInfo.New(directoryName); while ( @@ -273,17 +313,30 @@ searchingDirectory is not null return result; } - public async ValueTask IsIgnoredAsync( + public ValueTask IsIgnoredAsync( string actualFilePath, CancellationToken cancellationToken ) { - return ( - await this.FindIgnoreFileAsync( - Path.GetDirectoryName(actualFilePath)!, - cancellationToken - ) - ).IsIgnored(actualFilePath); + var ignoredTask = this.FindIgnoreFileAsync( + Path.GetDirectoryName(actualFilePath)!, + cancellationToken + ); + + if (ignoredTask.IsCompletedSuccessfully) + { + return new ValueTask(ignoredTask.Result.IsIgnored(actualFilePath)); + } + + return new ValueTask(IsIgnoredAsyncInner(ignoredTask, actualFilePath)); + + static async Task IsIgnoredAsyncInner( + ValueTask task, + string actualFilePath + ) + { + return (await task).IsIgnored(actualFilePath); + } } public string Serialize()