diff --git a/src/SharpFM/AvaloniaNLogSink.cs b/src/SharpFM/AvaloniaNLogSink.cs index cf42171..15224a8 100644 --- a/src/SharpFM/AvaloniaNLogSink.cs +++ b/src/SharpFM/AvaloniaNLogSink.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Logging; @@ -8,14 +9,38 @@ namespace SharpFM; /// /// Avalonia Log Sink that writes to NLog Loggers. +/// +/// Hot path note: Avalonia's framework code emits high-volume +/// Verbose/Debug/Info events during layout, input, and rendering. The +/// app's nlog.config blackholes Avalonia* events at those +/// levels via a final="true" rule, so they have nowhere to go — +/// but if says true, Avalonia still +/// formats arguments and calls , which then walks NLog's +/// rule engine before discarding. Profiling on a small script during +/// typing showed this sink consuming ~318ms of inclusive CPU over a 30s +/// trace — more than any other SharpFM-attributed path. +/// +/// Two-part fix: gate at Warning so Avalonia +/// short-circuits before the formatting work, and cache the per-source +/// so the surviving events skip +/// on every call. /// [ExcludeFromCodeCoverage] public class AvaloniaNLogSink : ILogSink { + private static readonly ILogger DefaultLogger = + LogManager.GetLogger(typeof(AvaloniaNLogSink).ToString()); + + private static readonly ConcurrentDictionary LoggerCache = new(); + /// - /// AvaloniaNLogSink is always enabled. + /// Gate at Warning to match the intent of nlog.config's + /// Avalonia* blackhole rule. Avalonia framework code routinely + /// emits hundreds of Verbose/Debug/Info events per second during + /// typing — none of which would reach a target anyway. /// - public bool IsEnabled(LogEventLevel level, string area) => true; + public bool IsEnabled(LogEventLevel level, string area) => + level >= LogEventLevel.Warning; public void Log(LogEventLevel level, string area, object? source, string messageTemplate) { @@ -24,8 +49,9 @@ public void Log(LogEventLevel level, string area, object? source, string message public void Log(LogEventLevel level, string area, object? source, string messageTemplate, params object?[] propertyValues) { - ILogger? logger = source is not null ? LogManager.GetLogger(source.GetType().ToString()) - : LogManager.GetLogger(typeof(AvaloniaNLogSink).ToString()); + ILogger logger = source is null + ? DefaultLogger + : LoggerCache.GetOrAdd(source.GetType(), static t => LogManager.GetLogger(t.ToString())); logger.Log(LogLevelToNLogLevel(level), $"{area}: {messageTemplate}", propertyValues); }