Skip to content

[dotnet] Improve logging interpolated string allocation#17391

Merged
nvborisenko merged 3 commits into
SeleniumHQ:trunkfrom
nvborisenko:dotnet-logging-allocation
Apr 25, 2026
Merged

[dotnet] Improve logging interpolated string allocation#17391
nvborisenko merged 3 commits into
SeleniumHQ:trunkfrom
nvborisenko:dotnet-logging-allocation

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

Now compiler can guard IsEnabled pattern.

💥 What does this PR do?

This pull request introduces support for interpolated string handlers to the logging infrastructure, allowing log messages to defer string construction until it's confirmed that the corresponding log level is enabled. This improves performance by avoiding unnecessary string allocations when logging is disabled. The changes include new interpolated string handler types, interface and implementation updates, and compatibility shims for older .NET versions.

Interpolated String Handler Support

  • Added new interpolated string handler types (TraceLogStringHandler, DebugLogStringHandler, InfoLogStringHandler, WarnLogStringHandler, ErrorLogStringHandler, and the core LogInterpolatedStringHandler) to LogInterpolatedStringHandler.cs, enabling deferred string formatting for log messages.
  • Updated the ILogger interface in ILogger.cs to add overloads for each log level method that accept the corresponding interpolated string handler, allowing client code to use the new logging pattern.
  • Updated the Logger implementation to support the new handler-based overloads, ensuring messages are only constructed and logged if the log level is enabled.

.NET Compatibility

  • Added InterpolatedStringHandlerAttribute and InterpolatedStringHandlerArgumentAttribute to the Properties folder for compatibility with .NET versions prior to 8.0, ensuring the code can use interpolated string handlers across supported frameworks. [1] [2]

Minor Updates

  • Added System.Runtime.CompilerServices using directive to ILogger.cs to support the new attributes and handler types.

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s):
    • What was generated:
    • I reviewed all AI output and can explain the change

🔄 Types of changes

  • New feature (non-breaking change which adds functionality and tests!)

Copilot AI review requested due to automatic review settings April 25, 2026 10:32
@selenium-ci selenium-ci added the C-dotnet .NET Bindings label Apr 25, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add interpolated string handler support to logging infrastructure

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add interpolated string handler support for deferred log message construction
• Implement level-specific handlers (Trace, Debug, Info, Warn, Error) to avoid allocations
• Add compatibility shims for .NET versions prior to 8.0
• Guard string construction with IsEnabled checks in Logger implementation

Grey Divider

File Changes

1. dotnet/src/webdriver/Internal/Logging/ILogger.cs ✨ Enhancement +32/-0

Add interpolated string handler method overloads

• Added System.Runtime.CompilerServices using directive
• Added new overloads for each log level method accepting interpolated string handlers
• Each handler method uses [InterpolatedStringHandlerArgument("")] attribute
• Handlers defer string construction until log level is confirmed enabled

dotnet/src/webdriver/Internal/Logging/ILogger.cs


2. dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs ✨ Enhancement +304/-0

Implement interpolated string handler types for all log levels

• Created five level-specific handler structs: TraceLogStringHandler, DebugLogStringHandler,
 InfoLogStringHandler, WarnLogStringHandler, ErrorLogStringHandler
• Implemented core LogInterpolatedStringHandler with StringBuilder-based deferred string
 construction
• Each handler wraps the core handler and delegates AppendLiteral and AppendFormatted methods
• Handlers only allocate StringBuilder when log level is enabled

dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs


3. dotnet/src/webdriver/Internal/Logging/Logger.cs ✨ Enhancement +45/-5

Add handler-based log methods with IsEnabled guards

• Added IsEnabled checks to existing string-based log methods (Trace, Debug, Info, Warn, Error)
• Implemented new handler-based overloads for each log level
• Handler methods extract formatted string via ToStringAndClear() and log if enabled
• Prevents unnecessary LogMessage calls when log level is disabled

dotnet/src/webdriver/Internal/Logging/Logger.cs


View more (2)
4. dotnet/src/webdriver/Properties/InterpolatedStringHandlerAttribute.cs Compatibility +29/-0

Add InterpolatedStringHandlerAttribute compatibility shim

• Created compatibility shim for InterpolatedStringHandlerAttribute
• Conditionally compiled for .NET versions prior to 8.0
• Allows use of interpolated string handlers across supported frameworks

dotnet/src/webdriver/Properties/InterpolatedStringHandlerAttribute.cs


5. dotnet/src/webdriver/Properties/InterpolatedStringHandlerArgumentAttribute.cs Compatibility +32/-0

Add InterpolatedStringHandlerArgumentAttribute compatibility shim

• Created compatibility shim for InterpolatedStringHandlerArgumentAttribute
• Conditionally compiled for .NET versions prior to 8.0
• Supports parameter-level attribute marking for handler arguments

dotnet/src/webdriver/Properties/InterpolatedStringHandlerArgumentAttribute.cs


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. ILogger adds new methods 📘 Rule violation ⚙ Maintainability
Description
New overloads were added to the public ILogger interface, which is a source-breaking change for
any external implementers of ILogger. This violates the API compatibility requirement unless a
compatibility strategy (e.g., new interface + adapter, default interface methods, or deprecation
plan) is provided.
Code

dotnet/src/webdriver/Internal/Logging/ILogger.cs[R35-52]

+    /// <summary>
+    /// Writes a trace-level log message using an interpolated string handler that defers string construction.
+    /// </summary>
+    /// <param name="handler">The interpolated string handler.</param>
+    void Trace([InterpolatedStringHandlerArgument("")] ref TraceLogStringHandler handler);
+
    /// <summary>
    /// Writes a debug-level log message.
    /// </summary>
    /// <param name="message">The log message.</param>
    void Debug(string message);

+    /// <summary>
+    /// Writes a debug-level log message using an interpolated string handler that defers string construction.
+    /// </summary>
+    /// <param name="handler">The interpolated string handler.</param>
+    void Debug([InterpolatedStringHandlerArgument("")] ref DebugLogStringHandler handler);
+
Evidence
PR Compliance ID 5 requires preserving public API/ABI compatibility. ILogger is declared as
public interface ILogger, and this PR adds new members like `Trace(ref TraceLogStringHandler
handler) and Debug(ref DebugLogStringHandler handler)`, which will break compilation for any code
that implements ILogger but is not updated in lockstep.

AGENTS.md
dotnet/src/webdriver/Internal/Logging/ILogger.cs[27-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ILogger` is a `public interface`, and adding new members is a breaking change for any downstream implementers.

## Issue Context
This PR adds interpolated string handler overloads directly onto `ILogger`.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/Logging/ILogger.cs[27-88]
- dotnet/src/webdriver/Internal/Logging/Logger.cs[38-101]

## Suggested directions (pick one)
- Introduce a new interface (e.g., `IInterpolatedStringLogger`) for the handler overloads and keep `ILogger` unchanged; update internal usage to use the new interface when available.
- If language/runtime support allows, use default interface implementations for the new methods to preserve binary/source compatibility.
- If `ILogger` is not intended to be public API, consider making it `internal` (only if it does not affect public surface area) and/or providing a public, stable abstraction.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Handlers hide rendered text 🐞 Bug ≡ Correctness
Description
The new handler-based ILogger overloads are not meaningfully implementable by external ILogger
implementations because the handlers’ rendered message is only available via `internal
ToStringAndClear()`. Downstream implementations are forced to add these methods (due to the
interface change) but cannot access the formatted message to log it.
Code

dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[R55-62]

+    /// <inheritdoc cref="LogInterpolatedStringHandler.AppendFormatted{T}(T, int)"/>
+    public void AppendFormatted<T>(T value, int alignment) => _inner.AppendFormatted(value, alignment);
+
+    /// <inheritdoc cref="LogInterpolatedStringHandler.AppendFormatted{T}(T, int, string?)"/>
+    public void AppendFormatted<T>(T value, int alignment, string? format) => _inner.AppendFormatted(value, alignment, format);
+
+    internal string ToStringAndClear() => _inner.ToStringAndClear();
+}
Evidence
The handler types are public and are required by the public ILogger interface, but the only way to
obtain the formatted string (ToStringAndClear) is internal, which is inaccessible to consumers
in other assemblies. This makes it impossible for external implementations of ILogger to correctly
implement the new overloads.

dotnet/src/webdriver/Internal/Logging/ILogger.cs[27-88]
dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[25-62]
dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[220-304]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ILogger` now requires overloads taking `ref TraceLogStringHandler` (etc.), but the handler types do not publicly expose the rendered message, preventing external `ILogger` implementations from logging the constructed message.

### Issue Context
`ToStringAndClear()` is currently `internal` on both the wrapper handlers and the core handler.

### How to fix
Choose one (consistent with intended API surface):
- If handler overloads remain part of a **public** contract, make a **public** method to retrieve the rendered content (e.g., `public string ToStringAndClear()` or `public override string ToString()`), and consider also exposing an `IsEnabled`/`HasValue` flag so implementations can no-op safely.
- If these overloads are intended to be internal-only, remove them from the public `ILogger` contract (see also the interface-breaking-change finding).

### Fix Focus Areas
- dotnet/src/webdriver/Internal/Logging/ILogger.cs[35-88]
- dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[25-62]
- dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[220-304]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. No tests for handler overloads 📘 Rule violation ☼ Reliability
Description
This PR introduces new interpolated string handler types and new ILogger overloads, but no
corresponding tests are added/updated to validate formatting correctness and the enabled/disabled
behavior. Lack of coverage increases regression risk for this new logging API surface.
Code

dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[R25-62]

+/// <summary>
+/// Interpolated string handler for <see cref="LogEventLevel.Trace"/> log messages.
+/// Defers string construction until the log level is confirmed enabled.
+/// </summary>
+[InterpolatedStringHandler]
+public ref struct TraceLogStringHandler
+{
+    private LogInterpolatedStringHandler _inner;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="TraceLogStringHandler"/> struct.
+    /// </summary>
+    /// <param name="literalLength">The number of literal characters in the interpolated string.</param>
+    /// <param name="formattedCount">The number of interpolation holes in the interpolated string.</param>
+    /// <param name="logger">The logger to check for enabled status.</param>
+    /// <param name="isEnabled">When this method returns, indicates whether the handler is enabled.</param>
+    public TraceLogStringHandler(int literalLength, int formattedCount, ILogger logger, out bool isEnabled)
+    {
+        _inner = new LogInterpolatedStringHandler(literalLength, formattedCount, logger, LogEventLevel.Trace, out isEnabled);
+    }
+
+    /// <inheritdoc cref="LogInterpolatedStringHandler.AppendLiteral(string)"/>
+    public void AppendLiteral(string s) => _inner.AppendLiteral(s);
+
+    /// <inheritdoc cref="LogInterpolatedStringHandler.AppendFormatted{T}(T)"/>
+    public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
+
+    /// <inheritdoc cref="LogInterpolatedStringHandler.AppendFormatted{T}(T, string?)"/>
+    public void AppendFormatted<T>(T value, string? format) => _inner.AppendFormatted(value, format);
+
+    /// <inheritdoc cref="LogInterpolatedStringHandler.AppendFormatted{T}(T, int)"/>
+    public void AppendFormatted<T>(T value, int alignment) => _inner.AppendFormatted(value, alignment);
+
+    /// <inheritdoc cref="LogInterpolatedStringHandler.AppendFormatted{T}(T, int, string?)"/>
+    public void AppendFormatted<T>(T value, int alignment, string? format) => _inner.AppendFormatted(value, alignment, format);
+
+    internal string ToStringAndClear() => _inner.ToStringAndClear();
+}
Evidence
PR Compliance ID 6 requires functional changes/additions to include tests. The PR adds new
handler-based logging APIs (TraceLogStringHandler etc.) and relies on compiler-enabled IsEnabled
guarding, but the PR diff contains only production logging changes and does not include test updates
for these new code paths.

AGENTS.md
dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[25-245]
dotnet/src/webdriver/Internal/Logging/ILogger.cs[35-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New interpolated string handler overloads and handler types were added without tests covering the new API paths.

## Issue Context
The feature depends on the interpolated string handler pattern correctly deferring construction when `IsEnabled` is false, and formatting correctly when enabled.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[25-304]
- dotnet/src/webdriver/Internal/Logging/Logger.cs[38-101]
- dotnet/src/webdriver/Internal/Logging/ILogger.cs[35-88]

## Test expectations
- Add unit tests that call the new overloads via interpolated strings (e.g., `logger.Info($"value={expensive()}" )`) and assert `expensive()` is not evaluated when level is disabled.
- Add unit tests asserting formatted output matches expected strings when enabled (including `alignment` and `format` variants).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. AppendFormat per-hole allocations🐞 Bug ➹ Performance
Description
When logs are enabled, the handler’s formatted/aligned append paths allocate a new composite format
string per interpolation hole (via $"{...}") and then route through StringBuilder.AppendFormat,
adding avoidable allocations/CPU in enabled logging scenarios. This can partially offset the
performance benefit for heavily-used logs with alignment/format specifiers.
Code

dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[R272-298]

+    public void AppendFormatted<T>(T value, string? format)
+    {
+        _builder?.AppendFormat($"{{0:{format}}}", value);
+    }
+
+    /// <summary>
+    /// Appends a formatted value with alignment to the handler.
+    /// </summary>
+    /// <typeparam name="T">The type of the value to format.</typeparam>
+    /// <param name="value">The value to format and append.</param>
+    /// <param name="alignment">The alignment for the formatted value.</param>
+    public void AppendFormatted<T>(T value, int alignment)
+    {
+        _builder?.AppendFormat($"{{0,{alignment}}}", value);
+    }
+
+    /// <summary>
+    /// Appends a formatted value with alignment and a format string to the handler.
+    /// </summary>
+    /// <typeparam name="T">The type of the value to format.</typeparam>
+    /// <param name="value">The value to format and append.</param>
+    /// <param name="alignment">The alignment for the formatted value.</param>
+    /// <param name="format">The format string.</param>
+    public void AppendFormatted<T>(T value, int alignment, string? format)
+    {
+        _builder?.AppendFormat($"{{0,{alignment}:{format}}}", value);
+    }
Evidence
The handler constructs a new string for the composite format pattern on every AppendFormatted call
with a format and/or alignment and then uses AppendFormat, which implies repeated
parsing/formatting work in the enabled path.

dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[266-298]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AppendFormatted` overloads build composite format strings (e.g., `$"{{0:{format}}}"`) per hole and call `StringBuilder.AppendFormat`, introducing extra allocations and format parsing.

### Issue Context
This affects the enabled-logging path for interpolations that use alignment and/or a format string.

### How to fix
Implement formatting without building composite format strings:
- Use `IFormattable` / `ISpanFormattable` to format directly into the builder (or temporary stack buffer) and apply alignment via padding.
- On `net8.0`, consider conditionally using `DefaultInterpolatedStringHandler` or `StringBuilder.AppendInterpolatedStringHandler` to avoid per-hole composite-format-string creation.

### Fix Focus Areas
- dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs[266-298]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread dotnet/src/webdriver/Internal/Logging/ILogger.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds interpolated string handler support to the .NET internal logging API so log messages can defer formatting/allocation until the relevant log level is enabled, with polyfills to support older target frameworks.

Changes:

  • Introduces per-log-level interpolated string handler types plus a shared LogInterpolatedStringHandler.
  • Extends ILogger/Logger with handler-based overloads and adds IsEnabled guards for the string overloads.
  • Adds compatibility shims for interpolated string handler attributes for non-net8 TFMs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
dotnet/src/webdriver/Internal/Logging/LogInterpolatedStringHandler.cs Adds new interpolated string handler types used to defer message construction.
dotnet/src/webdriver/Internal/Logging/ILogger.cs Adds new handler-based overloads for each log level.
dotnet/src/webdriver/Internal/Logging/Logger.cs Implements the new overloads and adds enabled checks for existing string overloads.
dotnet/src/webdriver/Properties/InterpolatedStringHandlerAttribute.cs Adds a pre-net8 attribute shim required for handler support on older TFMs.
dotnet/src/webdriver/Properties/InterpolatedStringHandlerArgumentAttribute.cs Adds a pre-net8 attribute shim required for handler argument binding.

Comment thread dotnet/src/webdriver/Internal/Logging/Logger.cs
Comment thread dotnet/src/webdriver/Internal/Logging/Logger.cs
Comment thread dotnet/src/webdriver/Internal/Logging/ILogger.cs
Comment thread dotnet/src/webdriver/Internal/Logging/Logger.cs
Comment thread dotnet/src/webdriver/Internal/Logging/Logger.cs
Comment thread dotnet/src/webdriver/Internal/Logging/Logger.cs
@nvborisenko nvborisenko merged commit fe921b5 into SeleniumHQ:trunk Apr 25, 2026
18 of 19 checks passed
@nvborisenko nvborisenko deleted the dotnet-logging-allocation branch April 25, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-dotnet .NET Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants