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);
}