From 5f87491fc2f6ac11e1396636a098e0dbf8676d31 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:48:36 +0900 Subject: [PATCH 1/5] feat: show log file path on benchmark finished --- .../ConsoleArguments/ConfigParser.cs | 3 + src/BenchmarkDotNet/Helpers/ConsoleHelper.cs | 168 ++++++++++++++++++ src/BenchmarkDotNet/Helpers/PathHelper.cs | 83 +++++++++ .../Loggers/CompositeLogger.cs | 13 ++ .../Running/BenchmarkRunnerClean.cs | 17 +- .../Helpers/PathHelperTests.cs | 59 ++++++ 6 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 src/BenchmarkDotNet/Helpers/ConsoleHelper.cs create mode 100644 src/BenchmarkDotNet/Helpers/PathHelper.cs create mode 100644 tests/BenchmarkDotNet.Tests/Helpers/PathHelperTests.cs diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 292b27cca8..6211330336 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -763,6 +763,9 @@ private static IEnumerable GetFilters(CommandLineOptions options) private static int GetMaximumDisplayWidth() { + if (Console.IsOutputRedirected) + return MinimumDisplayWidth; + try { return Console.WindowWidth; diff --git a/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs b/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs new file mode 100644 index 0000000000..125afb8a68 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs @@ -0,0 +1,168 @@ +using BenchmarkDotNet.Detectors; +using BenchmarkDotNet.Loggers; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; + +namespace BenchmarkDotNet.Helpers; + +#nullable enable + +internal static class ConsoleHelper +{ + private const string ESC = "\e"; // Escape sequence. + private const string OSC8 = $"{ESC}]8;;"; // Operating System Command 8 + private const string ST = ESC + @"\"; // String Terminator + + /// + /// Write clickable link to console. + /// If console doesn't support OSC 8 hyperlinks. It writes plain link with markdown syntax. + /// + public static void WriteLineAsClickableLink(ILogger consoleLogger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info, string prefixText = "", string suffixText = "") + { + if (prefixText != "") + consoleLogger.Write(logKind, prefixText); + + WriteAsClickableLink(consoleLogger, link, linkCaption, logKind); + + // On Windows Terminal environment. + // It need to write extra space to avoid link style corrupted issue that occurred when window resized. + if (IsWindowsTerminal.Value && IsClickableLinkSupported.Value && suffixText == "") + suffixText = " "; + + if (suffixText != "") + consoleLogger.Write(logKind, suffixText); + + consoleLogger.WriteLine(); + } + + /// + /// Write clickable link to console. + /// If console doesn't support OSC 8 hyperlinks. It writes plain link with markdown syntax. + /// + public static void WriteAsClickableLink(ILogger consoleLogger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info) + { + if (consoleLogger.Id != nameof(ConsoleLogger)) + throw new NotSupportedException("This method is expected logger that has ConsoleLogger id."); + + // If clickable link supported. Write clickable link with OSC8. + if (IsClickableLinkSupported.Value) + { + consoleLogger.Write(logKind, @$"{OSC8}{link}{ST}{linkCaption ?? link}{OSC8}{ST}"); + return; + } + + // If link caption is specified. Write link as plain text with markdown link syntax. + if (!string.IsNullOrEmpty(linkCaption)) + { + consoleLogger.Write(logKind, $"[{linkCaption}]({link})"); + return; + } + + // Write link as plain text. + consoleLogger.Write(logKind, link); + } + + private static readonly Lazy IsWindowsTerminal = new(() + => Environment.GetEnvironmentVariable("WT_SESSION") != null); + + private static readonly Lazy IsClickableLinkSupported = new(() => + { + if (Console.IsOutputRedirected) + return false; + + // The current console doesn't have a valid buffer size, which means it is not a real console. + if (Console.BufferHeight == 0 || Console.BufferWidth == 0) + return false; + + // Disable clickable link on CI environment. + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) + return false; + + // dumb terminal don't support ANSI escape sequence. + var term = Environment.GetEnvironmentVariable("TERM") ?? ""; + if (term == "dumb") + return false; + + if (OsDetector.IsWindows()) + { + try + { + // conhost.exe don't support clickable link with OSC8. + if (IsRunningOnConhost()) + return false; + + // ConEmu and don't support OSC8. + var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI"); + if (conEmu != null) + return false; + + // Return true if Virtual Terminal Processing mode is enabled. + return IsVirtualTerminalProcessingEnabled(); + } + catch + { + return false; // Ignore unexpected exception. + } + } + else + { + // screen don't support OSC8 clickable link. + if (Regex.IsMatch(term, "^screen")) + return false; + + // Other major terminal supports OSC8 by default. https://github.com/Alhadis/OSC8-Adoption + return true; + } + }); + + [SupportedOSPlatform("windows")] + private static bool IsVirtualTerminalProcessingEnabled() + { + // Try to get Virtual Terminal Processing enebled or not. + const uint STD_OUTPUT_HANDLE = unchecked((uint)-11); + IntPtr handle = NativeMethods.GetStdHandle(STD_OUTPUT_HANDLE); + if (handle == IntPtr.Zero) + return false; + + if (NativeMethods.GetConsoleMode(handle, out uint consoleMode)) + { + const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + if ((consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) > 0) + { + return true; + } + } + return false; + } + + [SupportedOSPlatform("windows")] + private static bool IsRunningOnConhost() + { + IntPtr hwnd = NativeMethods.GetConsoleWindow(); + if (hwnd == IntPtr.Zero) + return false; + + NativeMethods.GetWindowThreadProcessId(hwnd, out uint pid); + using var process = Process.GetProcessById((int)pid); + return process.ProcessName == "conhost"; + } + + [SupportedOSPlatform("windows")] + private static class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetStdHandle(uint nStdHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + } +} diff --git a/src/BenchmarkDotNet/Helpers/PathHelper.cs b/src/BenchmarkDotNet/Helpers/PathHelper.cs new file mode 100644 index 0000000000..2ce51a3ea7 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/PathHelper.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace BenchmarkDotNet.Helpers; + +#nullable enable + +internal static class PathHelper +{ + public static string GetRelativePath(string relativeTo, string path) + { +#if !NETSTANDARD2_0 + return Path.GetRelativePath(relativeTo, path); +#else + return GetRelativePathCompat(relativeTo, path); +#endif + } + +#if NETSTANDARD2_0 + private static string GetRelativePathCompat(string relativeTo, string path) + { + // Get absolute full paths + string basePath = Path.GetFullPath(relativeTo); + string targetPath = Path.GetFullPath(path); + + // Normalize base to directory (Path.GetRelativePath treats base as directory always) + if (!basePath.EndsWith(Path.DirectorySeparatorChar.ToString())) + basePath += Path.DirectorySeparatorChar; + + // If roots differ, return the absolute target + string baseRoot = Path.GetPathRoot(basePath)!; + string targetRoot = Path.GetPathRoot(targetPath)!; + if (!string.Equals(baseRoot, targetRoot, StringComparison.OrdinalIgnoreCase)) + return targetPath; + + // Break into segments + var baseSegments = SplitPath(basePath); + var targetSegments = SplitPath(targetPath); + + // Find common prefix + int i = 0; + while (i < baseSegments.Count && i < targetSegments.Count && string.Equals(baseSegments[i], targetSegments[i], StringComparison.OrdinalIgnoreCase)) + { + i++; + } + + // Build relative parts + var relativeParts = new List(); + + // For each remaining segment in base -> go up one level + for (int j = i; j < baseSegments.Count; j++) + relativeParts.Add(".."); + + // For each remaining in target -> add those segments + for (int j = i; j < targetSegments.Count; j++) + relativeParts.Add(targetSegments[j]); + + // If nothing added, it is the same directory + if (relativeParts.Count == 0) + return "."; + + // Join with separator and return + return string.Join(Path.DirectorySeparatorChar.ToString(), relativeParts); + } + + private static List SplitPath(string path) + { + var segments = new List(); + string[] raw = path.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries); + + foreach (var seg in raw) + { + // Skip root parts like "C:\" + if (seg.EndsWith(":")) + continue; + segments.Add(seg); + } + + return segments; + } +#endif +} diff --git a/src/BenchmarkDotNet/Loggers/CompositeLogger.cs b/src/BenchmarkDotNet/Loggers/CompositeLogger.cs index 9906a6d4a1..e5c8cb3b3c 100644 --- a/src/BenchmarkDotNet/Loggers/CompositeLogger.cs +++ b/src/BenchmarkDotNet/Loggers/CompositeLogger.cs @@ -1,4 +1,8 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +#nullable enable namespace BenchmarkDotNet.Loggers { @@ -57,5 +61,14 @@ public void Flush() } } } + + /// + /// Try to gets logger that has id of ConsoleLogger. + /// + public bool TryGetConsoleLogger([NotNullWhen(true)] out ILogger? consoleLogger) + { + consoleLogger = loggers.FirstOrDefault(x => x.Id == nameof(ConsoleLogger)); + return consoleLogger != null; + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index 0c096b6c9e..25aa650f3f 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -173,6 +173,7 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) var totalTime = globalChronometer.GetElapsed().GetTimeSpan(); int totalNumberOfExecutedBenchmarks = results.Sum(summary => summary.GetNumberOfExecutedBenchmarks()); LogTotalTime(compositeLogger, totalTime, totalNumberOfExecutedBenchmarks, "Global total time"); + compositeLogger.WriteLine(); return results.ToArray(); } @@ -191,6 +192,20 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) compositeLogger.WriteLineInfo("Artifacts cleanup is finished"); compositeLogger.Flush(); + // Output additional information to console. + var logFileEnabled = benchmarkRunInfos.All(info => !info.Config.Options.IsSet(ConfigOptions.DisableLogFile)); + if (logFileEnabled && compositeLogger.TryGetConsoleLogger(out var consoleLogger)) + { + var artifactDirectoryFullPath = Path.GetFullPath(rootArtifactsFolderPath); + var logFileFullPath = Path.GetFullPath(logFilePath); + var logFileRelativePath = PathHelper.GetRelativePath(artifactDirectoryFullPath, logFileFullPath); + + consoleLogger.WriteLine(); + consoleLogger.WriteLineHeader("// * Benchmark LogFile *"); + ConsoleHelper.WriteLineAsClickableLink(consoleLogger, artifactDirectoryFullPath); + ConsoleHelper.WriteLineAsClickableLink(consoleLogger, logFileFullPath, linkCaption: logFileRelativePath, prefixText: " "); + } + eventProcessor.OnEndRunStage(); } } @@ -752,7 +767,7 @@ private static StreamWriter GetLogFileStreamWriter(BenchmarkRunInfo[] benchmarkR return new StreamWriter(logFilePath, append: false); } - private static ILogger CreateCompositeLogger(BenchmarkRunInfo[] benchmarkRunInfos, StreamLogger streamLogger) + private static CompositeLogger CreateCompositeLogger(BenchmarkRunInfo[] benchmarkRunInfos, StreamLogger streamLogger) { var loggers = new Dictionary(); diff --git a/tests/BenchmarkDotNet.Tests/Helpers/PathHelperTests.cs b/tests/BenchmarkDotNet.Tests/Helpers/PathHelperTests.cs new file mode 100644 index 0000000000..f1d68a30bd --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Helpers/PathHelperTests.cs @@ -0,0 +1,59 @@ +using BenchmarkDotNet.Helpers; +using System.IO; +using Xunit; + +namespace BenchmarkDotNet.Tests.Helpers +{ + // Using test patterns of Path.GetRelativePath + // https://github.com/dotnet/runtime/blob/v10.0.0/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/IO/Path.GetRelativePath.cs + public class PathHelperTests + { +#if NETFRAMEWORK + [Theory] + [InlineData(@"C:\", @"C:\", @".")] + [InlineData(@"C:\a", @"C:\a\", @".")] + [InlineData(@"C:\A", @"C:\a\", @".")] + [InlineData(@"C:\a\", @"C:\a", @".")] + [InlineData(@"C:\", @"C:\b", @"b")] + [InlineData(@"C:\a", @"C:\b", @"..\b")] + // [InlineData(@"C:\a", @"C:\b\", @"..\b\")] // This test failed with GetRelativePathCompat. + [InlineData(@"C:\a\b", @"C:\a", @"..")] + [InlineData(@"C:\a\b", @"C:\a\", @"..")] + [InlineData(@"C:\a\b\", @"C:\a", @"..")] + [InlineData(@"C:\a\b\", @"C:\a\", @"..")] + [InlineData(@"C:\a\b\c", @"C:\a\b", @"..")] + [InlineData(@"C:\a\b\c", @"C:\a\b\", @"..")] + [InlineData(@"C:\a\b\c", @"C:\a", @"..\..")] + [InlineData(@"C:\a\b\c", @"C:\a\", @"..\..")] + [InlineData(@"C:\a\b\c\", @"C:\a\b", @"..")] + [InlineData(@"C:\a\b\c\", @"C:\a\b\", @"..")] + [InlineData(@"C:\a\b\c\", @"C:\a", @"..\..")] + [InlineData(@"C:\a\b\c\", @"C:\a\", @"..\..")] + [InlineData(@"C:\a\", @"C:\b", @"..\b")] + [InlineData(@"C:\a", @"C:\a\b", @"b")] + [InlineData(@"C:\a", @"C:\A\b", @"b")] + [InlineData(@"C:\a", @"C:\b\c", @"..\b\c")] + [InlineData(@"C:\a\", @"C:\a\b", @"b")] + [InlineData(@"C:\", @"D:\", @"D:\")] + [InlineData(@"C:\", @"D:\b", @"D:\b")] + [InlineData(@"C:\", @"D:\b\", @"D:\b\")] + [InlineData(@"C:\a", @"D:\b", @"D:\b")] + [InlineData(@"C:\a\", @"D:\b", @"D:\b")] + [InlineData(@"C:\ab", @"C:\a", @"..\a")] + [InlineData(@"C:\a", @"C:\ab", @"..\ab")] + [InlineData(@"C:\", @"\\LOCALHOST\Share\b", @"\\LOCALHOST\Share\b")] + [InlineData(@"\\LOCALHOST\Share\a", @"\\LOCALHOST\Share\b", @"..\b")] + public void GetRelativePathTest_Windows(string relativeTo, string path, string expected) + { + // Arrange + expected = expected.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + // Act + var result = PathHelper.GetRelativePath(relativeTo, path); + + // Assert + Assert.Equal(expected, result); + } +#endif + } +} From d7d539c40a0e7a17f71d0f2e6b23eaf36a154e82 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:07:26 +0900 Subject: [PATCH 2/5] chore: remove code that outpu markdown link syntax --- src/BenchmarkDotNet/Helpers/ConsoleHelper.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs b/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs index 125afb8a68..d515d86350 100644 --- a/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs @@ -18,7 +18,7 @@ internal static class ConsoleHelper /// /// Write clickable link to console. - /// If console doesn't support OSC 8 hyperlinks. It writes plain link with markdown syntax. + /// If console doesn't support OSC 8 hyperlinks. It writes plain link. /// public static void WriteLineAsClickableLink(ILogger consoleLogger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info, string prefixText = "", string suffixText = "") { @@ -40,7 +40,7 @@ public static void WriteLineAsClickableLink(ILogger consoleLogger, string link, /// /// Write clickable link to console. - /// If console doesn't support OSC 8 hyperlinks. It writes plain link with markdown syntax. + /// If console doesn't support OSC 8 hyperlinks. It writes plain link. /// public static void WriteAsClickableLink(ILogger consoleLogger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info) { @@ -54,14 +54,7 @@ public static void WriteAsClickableLink(ILogger consoleLogger, string link, stri return; } - // If link caption is specified. Write link as plain text with markdown link syntax. - if (!string.IsNullOrEmpty(linkCaption)) - { - consoleLogger.Write(logKind, $"[{linkCaption}]({link})"); - return; - } - - // Write link as plain text. + // Write link as plain text. (linkCaption is ignored) consoleLogger.Write(logKind, link); } From 3d4179597e59ea955524ee59ae695d1a1161bb43 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:53:51 +0900 Subject: [PATCH 3/5] chore: refactor codes that write clickable link --- src/BenchmarkDotNet/Helpers/ConsoleHelper.cs | 51 +++++-------------- .../Loggers/CompositeLogger.cs | 7 +-- src/BenchmarkDotNet/Loggers/ConsoleLogger.cs | 2 +- src/BenchmarkDotNet/Loggers/IConsoleLogger.cs | 48 +++++++++++++++++ .../Running/BenchmarkRunnerClean.cs | 5 +- 5 files changed, 68 insertions(+), 45 deletions(-) create mode 100644 src/BenchmarkDotNet/Loggers/IConsoleLogger.cs diff --git a/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs b/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs index d515d86350..5b76735400 100644 --- a/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ConsoleHelper.cs @@ -17,51 +17,29 @@ internal static class ConsoleHelper private const string ST = ESC + @"\"; // String Terminator /// - /// Write clickable link to console. - /// If console doesn't support OSC 8 hyperlinks. It writes plain link. + /// Try to gets clickable link text for console. + /// If console doesn't support clickable link, it returns false. /// - public static void WriteLineAsClickableLink(ILogger consoleLogger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info, string prefixText = "", string suffixText = "") + public static bool TryGetClickableLink(string link, string? linkCaption, out string result) { - if (prefixText != "") - consoleLogger.Write(logKind, prefixText); - - WriteAsClickableLink(consoleLogger, link, linkCaption, logKind); - - // On Windows Terminal environment. - // It need to write extra space to avoid link style corrupted issue that occurred when window resized. - if (IsWindowsTerminal.Value && IsClickableLinkSupported.Value && suffixText == "") - suffixText = " "; - - if (suffixText != "") - consoleLogger.Write(logKind, suffixText); - - consoleLogger.WriteLine(); - } - - /// - /// Write clickable link to console. - /// If console doesn't support OSC 8 hyperlinks. It writes plain link. - /// - public static void WriteAsClickableLink(ILogger consoleLogger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info) - { - if (consoleLogger.Id != nameof(ConsoleLogger)) - throw new NotSupportedException("This method is expected logger that has ConsoleLogger id."); - - // If clickable link supported. Write clickable link with OSC8. - if (IsClickableLinkSupported.Value) + if (!IsClickableLinkSupported) { - consoleLogger.Write(logKind, @$"{OSC8}{link}{ST}{linkCaption ?? link}{OSC8}{ST}"); - return; + result = ""; + return false; } - // Write link as plain text. (linkCaption is ignored) - consoleLogger.Write(logKind, link); + result = @$"{OSC8}{link}{ST}{linkCaption ?? link}{OSC8}{ST}"; + return true; } - private static readonly Lazy IsWindowsTerminal = new(() + public static bool IsWindowsTerminal => _isWindowsTerminal.Value; + + public static bool IsClickableLinkSupported => _isClickableLinkSupported.Value; + + private static readonly Lazy _isWindowsTerminal = new(() => Environment.GetEnvironmentVariable("WT_SESSION") != null); - private static readonly Lazy IsClickableLinkSupported = new(() => + private static readonly Lazy _isClickableLinkSupported = new(() => { if (Console.IsOutputRedirected) return false; @@ -114,7 +92,6 @@ public static void WriteAsClickableLink(ILogger consoleLogger, string link, stri [SupportedOSPlatform("windows")] private static bool IsVirtualTerminalProcessingEnabled() { - // Try to get Virtual Terminal Processing enebled or not. const uint STD_OUTPUT_HANDLE = unchecked((uint)-11); IntPtr handle = NativeMethods.GetStdHandle(STD_OUTPUT_HANDLE); if (handle == IntPtr.Zero) diff --git a/src/BenchmarkDotNet/Loggers/CompositeLogger.cs b/src/BenchmarkDotNet/Loggers/CompositeLogger.cs index e5c8cb3b3c..6d128fdc04 100644 --- a/src/BenchmarkDotNet/Loggers/CompositeLogger.cs +++ b/src/BenchmarkDotNet/Loggers/CompositeLogger.cs @@ -62,12 +62,9 @@ public void Flush() } } - /// - /// Try to gets logger that has id of ConsoleLogger. - /// - public bool TryGetConsoleLogger([NotNullWhen(true)] out ILogger? consoleLogger) + public bool TryGetConsoleLogger([NotNullWhen(true)] out IConsoleLogger? consoleLogger) { - consoleLogger = loggers.FirstOrDefault(x => x.Id == nameof(ConsoleLogger)); + consoleLogger = loggers.OfType().SingleOrDefault(); return consoleLogger != null; } } diff --git a/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs b/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs index 52c33b27eb..65c07a03ad 100644 --- a/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs +++ b/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs @@ -9,7 +9,7 @@ namespace BenchmarkDotNet.Loggers { - public sealed class ConsoleLogger : ILogger + public sealed class ConsoleLogger : ILogger, IConsoleLogger { private const ConsoleColor DefaultColor = ConsoleColor.Gray; diff --git a/src/BenchmarkDotNet/Loggers/IConsoleLogger.cs b/src/BenchmarkDotNet/Loggers/IConsoleLogger.cs new file mode 100644 index 0000000000..04014dd594 --- /dev/null +++ b/src/BenchmarkDotNet/Loggers/IConsoleLogger.cs @@ -0,0 +1,48 @@ +using BenchmarkDotNet.Helpers; +using System.Text; + +namespace BenchmarkDotNet.Loggers; + +/// +/// Marker interface for logger that support clickable link with ANSI escape sequence. +/// +internal interface IConsoleLogger : ILogger +{ +} + +/// +/// ExtensionMethods for IConsoleLogger interface. +/// +internal static class IConsoleLoggerExtensions +{ + /// + /// Write clickable link to console. + /// If console doesn't support clickable link. It writes plain link. + /// + public static void WriteLineLink(this IConsoleLogger logger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info, string prefixText = "", string suffixText = "") + { + var sb = new StringBuilder(); + + if (prefixText != "") + sb.Append(prefixText); + + if (ConsoleHelper.TryGetClickableLink(link, linkCaption, out var linkText)) + { + sb.Append(linkText); + + // Temporary workaround for Windows Terminal. + // To avoid link style corruption issue when output ends with a clickable link and window is resized. + if (ConsoleHelper.IsWindowsTerminal && suffixText == "") + sb.Append(' '); + } + else + { + sb.Append(link); // Write plain link. + } + + if (suffixText != "") + sb.Append(suffixText); + + logger.WriteLine(logKind, sb.ToString()); + } +} diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index 25aa650f3f..71152f3f82 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -202,8 +202,9 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) consoleLogger.WriteLine(); consoleLogger.WriteLineHeader("// * Benchmark LogFile *"); - ConsoleHelper.WriteLineAsClickableLink(consoleLogger, artifactDirectoryFullPath); - ConsoleHelper.WriteLineAsClickableLink(consoleLogger, logFileFullPath, linkCaption: logFileRelativePath, prefixText: " "); + consoleLogger.WriteLineLink(artifactDirectoryFullPath); + consoleLogger.WriteLineLink(logFileFullPath, linkCaption: logFileRelativePath, prefixText: " "); + consoleLogger.Flush(); } eventProcessor.OnEndRunStage(); From 68666e5920207a419c561dc3a4db0935fb616388 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:03:56 +0900 Subject: [PATCH 4/5] chore: update code based on review comment --- .../Loggers/CompositeLogger.cs | 8 +--- src/BenchmarkDotNet/Loggers/ConsoleLogger.cs | 2 +- src/BenchmarkDotNet/Loggers/IConsoleLogger.cs | 48 ------------------- src/BenchmarkDotNet/Loggers/ILinkLogger.cs | 8 ++++ .../Loggers/ILoggerExtensions.cs | 45 +++++++++++++++++ .../Running/BenchmarkRunnerClean.cs | 14 +++--- 6 files changed, 62 insertions(+), 63 deletions(-) delete mode 100644 src/BenchmarkDotNet/Loggers/IConsoleLogger.cs create mode 100644 src/BenchmarkDotNet/Loggers/ILinkLogger.cs create mode 100644 src/BenchmarkDotNet/Loggers/ILoggerExtensions.cs diff --git a/src/BenchmarkDotNet/Loggers/CompositeLogger.cs b/src/BenchmarkDotNet/Loggers/CompositeLogger.cs index 6d128fdc04..d8f14377ee 100644 --- a/src/BenchmarkDotNet/Loggers/CompositeLogger.cs +++ b/src/BenchmarkDotNet/Loggers/CompositeLogger.cs @@ -6,7 +6,7 @@ namespace BenchmarkDotNet.Loggers { - internal class CompositeLogger : ILogger + internal class CompositeLogger : ILogger, ILinkLogger { private readonly ImmutableHashSet loggers; @@ -61,11 +61,5 @@ public void Flush() } } } - - public bool TryGetConsoleLogger([NotNullWhen(true)] out IConsoleLogger? consoleLogger) - { - consoleLogger = loggers.OfType().SingleOrDefault(); - return consoleLogger != null; - } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs b/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs index 65c07a03ad..cc363daece 100644 --- a/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs +++ b/src/BenchmarkDotNet/Loggers/ConsoleLogger.cs @@ -9,7 +9,7 @@ namespace BenchmarkDotNet.Loggers { - public sealed class ConsoleLogger : ILogger, IConsoleLogger + public sealed class ConsoleLogger : ILogger, ILinkLogger { private const ConsoleColor DefaultColor = ConsoleColor.Gray; diff --git a/src/BenchmarkDotNet/Loggers/IConsoleLogger.cs b/src/BenchmarkDotNet/Loggers/IConsoleLogger.cs deleted file mode 100644 index 04014dd594..0000000000 --- a/src/BenchmarkDotNet/Loggers/IConsoleLogger.cs +++ /dev/null @@ -1,48 +0,0 @@ -using BenchmarkDotNet.Helpers; -using System.Text; - -namespace BenchmarkDotNet.Loggers; - -/// -/// Marker interface for logger that support clickable link with ANSI escape sequence. -/// -internal interface IConsoleLogger : ILogger -{ -} - -/// -/// ExtensionMethods for IConsoleLogger interface. -/// -internal static class IConsoleLoggerExtensions -{ - /// - /// Write clickable link to console. - /// If console doesn't support clickable link. It writes plain link. - /// - public static void WriteLineLink(this IConsoleLogger logger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info, string prefixText = "", string suffixText = "") - { - var sb = new StringBuilder(); - - if (prefixText != "") - sb.Append(prefixText); - - if (ConsoleHelper.TryGetClickableLink(link, linkCaption, out var linkText)) - { - sb.Append(linkText); - - // Temporary workaround for Windows Terminal. - // To avoid link style corruption issue when output ends with a clickable link and window is resized. - if (ConsoleHelper.IsWindowsTerminal && suffixText == "") - sb.Append(' '); - } - else - { - sb.Append(link); // Write plain link. - } - - if (suffixText != "") - sb.Append(suffixText); - - logger.WriteLine(logKind, sb.ToString()); - } -} diff --git a/src/BenchmarkDotNet/Loggers/ILinkLogger.cs b/src/BenchmarkDotNet/Loggers/ILinkLogger.cs new file mode 100644 index 0000000000..92a7a8c5ec --- /dev/null +++ b/src/BenchmarkDotNet/Loggers/ILinkLogger.cs @@ -0,0 +1,8 @@ +namespace BenchmarkDotNet.Loggers; + +/// +/// Marker interface for a logger that supports clickable link with ANSI escape sequence. +/// +internal interface ILinkLogger : ILogger +{ +} diff --git a/src/BenchmarkDotNet/Loggers/ILoggerExtensions.cs b/src/BenchmarkDotNet/Loggers/ILoggerExtensions.cs new file mode 100644 index 0000000000..8439812522 --- /dev/null +++ b/src/BenchmarkDotNet/Loggers/ILoggerExtensions.cs @@ -0,0 +1,45 @@ +using BenchmarkDotNet.Helpers; + +namespace BenchmarkDotNet.Loggers; + +#nullable enable + +public static class ILoggerExtensions +{ + /// + /// Write clickable link to logger. + /// If the logger doesn't implement . It's written as plain text. + /// + public static void WriteLink(this ILogger logger, string link, string? linkCaption = null, LogKind logKind = LogKind.Info) + { + if (logger is ILinkLogger) + { + if (ConsoleHelper.TryGetClickableLink(link, linkCaption, out var clickableLink)) + link = clickableLink; + } + + logger.Write(logKind, link); + } + + /// + /// Write clickable link to logger. + /// If the logger doesn't implement . It's written as plain text. + /// + public static void WriteLineLink(this ILogger logger, string link, string? linkCaption = null, string prefixText = "", string suffixText = "", LogKind logKind = LogKind.Info) + { + if (logger is ILinkLogger) + { + if (ConsoleHelper.TryGetClickableLink(link, linkCaption, out var clickableLink)) + { + link = clickableLink; + + // Temporary workaround for Windows Terminal. + // To avoid link style corruption issue when output ends with a clickable link and window is resized. + if (ConsoleHelper.IsWindowsTerminal && suffixText == "") + suffixText = " "; + } + } + + logger.WriteLine(logKind, $"{prefixText}{link}{suffixText}"); + } +} diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index 71152f3f82..467fc9f900 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -194,17 +194,17 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) // Output additional information to console. var logFileEnabled = benchmarkRunInfos.All(info => !info.Config.Options.IsSet(ConfigOptions.DisableLogFile)); - if (logFileEnabled && compositeLogger.TryGetConsoleLogger(out var consoleLogger)) + if (logFileEnabled) { var artifactDirectoryFullPath = Path.GetFullPath(rootArtifactsFolderPath); var logFileFullPath = Path.GetFullPath(logFilePath); var logFileRelativePath = PathHelper.GetRelativePath(artifactDirectoryFullPath, logFileFullPath); - consoleLogger.WriteLine(); - consoleLogger.WriteLineHeader("// * Benchmark LogFile *"); - consoleLogger.WriteLineLink(artifactDirectoryFullPath); - consoleLogger.WriteLineLink(logFileFullPath, linkCaption: logFileRelativePath, prefixText: " "); - consoleLogger.Flush(); + compositeLogger.WriteLine(); + compositeLogger.WriteLineHeader("// * Benchmark LogFile *"); + compositeLogger.WriteLineLink(artifactDirectoryFullPath); + compositeLogger.WriteLineLink(logFileFullPath, linkCaption: logFileRelativePath, prefixText: " "); + compositeLogger.Flush(); } eventProcessor.OnEndRunStage(); @@ -768,7 +768,7 @@ private static StreamWriter GetLogFileStreamWriter(BenchmarkRunInfo[] benchmarkR return new StreamWriter(logFilePath, append: false); } - private static CompositeLogger CreateCompositeLogger(BenchmarkRunInfo[] benchmarkRunInfos, StreamLogger streamLogger) + private static ILogger CreateCompositeLogger(BenchmarkRunInfo[] benchmarkRunInfos, StreamLogger streamLogger) { var loggers = new Dictionary(); From 19c8f03289fc831a8dd9250572c81f61bc961133 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:58:42 +0900 Subject: [PATCH 5/5] chore: remove documentation comment based on review comment --- src/BenchmarkDotNet/Loggers/ILinkLogger.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/BenchmarkDotNet/Loggers/ILinkLogger.cs b/src/BenchmarkDotNet/Loggers/ILinkLogger.cs index 92a7a8c5ec..5c0c328711 100644 --- a/src/BenchmarkDotNet/Loggers/ILinkLogger.cs +++ b/src/BenchmarkDotNet/Loggers/ILinkLogger.cs @@ -1,8 +1,5 @@ namespace BenchmarkDotNet.Loggers; -/// -/// Marker interface for a logger that supports clickable link with ANSI escape sequence. -/// internal interface ILinkLogger : ILogger { }