ZLogger v2 架构:利用 .NET 8 最大限度地提高性能

e4db4bc497e4f4753f90ae2e0bd428c8.png

这是一个用于 C# 和 .NET 的新的超快速和低分配日志记录库。它已从 v1 开始完全重新设计,以与最新的 C# 功能保持一致。虽然它最适合 .NET 8,但它支持 .NET Standard 2.0 及更高版本,以及 Unity 2022.2 及更高版本。.NET 和 Unity 版本都支持文本消息和结构化日志记录(默认为 JSON 和 MessagePack)。

  • Cysharp/ZLogger

  • https://github.com/Cysharp/ZLogger

新设计的关键点是全面采用字符串插值,实现了语法和性能的简洁。

logger.ZLogInformation($"Hello my name is {name}, {age} years old.");

像这样编写的代码被编译成:

if (logger.IsEnabled(LogLvel.Information))
{
    var handler = new ZLoggerInformationInterpolatedStringHandler(30, 2, logger);
    handler.AppendLiteral("Hello my name is ");
    handler.AppendFormatted<string>(name, 0, null, "name");
    handler.AppendLiteral(", ");
    handler.AppendFormatted<int>(age, 0, null, "age");
    handler.AppendLiteral(" years old.");
}

从代码中可以明显看出效率:格式字符串在编译时而不是运行时扩展,并且参数以 的形式作为泛型接收,避免了装箱。顺便说一句,构造函数中的 30 表示字符串长度,2 是参数的数量,这通过计算所需的初始缓冲区大小来提高效率。

字符串插值本身自 C# 6.0 以来一直是一个功能,但 C# 10.0 中增强的字符串插值允许自定义字符串插值。

以这种方式得到的字符串片段和参数,最终通过Cysharp/Utf8StringInterpolation,不经过字符串化,直接写入到Stream中,实现了高速低分配。

对于结构化日志记录,通过与 System.Text.Json 的 Utf8JsonWriter 紧密耦合:

// For example, write {"name":"foo",age:33} to Utf8JsonWriter
// Source Generator version, very easy to understand what's actually happening
public void WriteJsonParameterKeyValues(Utf8JsonWriter writer, JsonSerializerOptions jsonSerializerOptions)
{
    writer.WriteString(_jsonParameter_name, this.name);
    writer.WriteNumber(_jsonParameter_age, this.age);
}

// StringInterpolation version, seems a bit roundabout but does the same thing
public void WriteJsonParameterKeyValues(Utf8JsonWriter writer, JsonSerializerOptions jsonSerializerOptions)
{
    for (var i = 0; i < ParameterCount; i++)
    {
        ref var p = ref parameters[i];
        writer.WritePropertyName(p.Name.AsSpan());
        // Explanation of MagicalBox will come later
        if (!magicalBox.TryReadTo(p.Type, p.BoxOffset, jsonWriter, jsonSerializerOptions))
        {
            // ....
        }
    }
}

它再次直接编写为 UTF8。结构化日志记录是最近的趋势,因此它在各种语言的记录器中实现,但我认为没有任何其他实现可以在保持性能的同时实现如此干净的语法!

那么,实际的基准测试结果如何?分配至少是压倒性的低。

dffb1d85177afff934fd8a1e3ecbefd5.png

关于分配犹豫不决的原因是,为高速精心设置的 NLog 比预期的要快,grrr...

现在,ZLogger 的另一个功能是它直接构建在 Microsoft.Extensions.Logging 之上。通常,记录器有自己的系统,并使用桥接器与 Microsoft.Extensions.Logging 连接。在实际应用程序中,几乎不可能避免Microsoft.Extensions.Logging,例如在使用 ASP.NET 时。从 .NET 8 开始,借助增强的 OpenTelemetry 支持和 Aspire,Microsoft.Extensions.Logging 的重要性正在增加。与 ZLogger v1 不同,v2 支持 Microsoft.Extensions.Logging 的所有功能,包括 Scope。

例如,Serilog 的桥接库的质量相当低(我也检查了源代码),这反映在实际的性能数字上。ZLogger 不会产生此类开销。

此外,默认设置也非常重要。大多数记录器的标准设置都相当慢,例如每次写入文件流时都会刷新。为了加快此速度,您需要正确调整异步和缓冲设置,并确保最后进行可靠的刷新以避免丢失,这是非常困难的。那么,很多人可能会将其保留为默认设置?默认情况下,ZLogger 被调整为最快的,并且最终刷新会随着 Microsoft.Extensions 的 DI 的生命周期自动应用,因此在使用 ApplicationBuilder 等构建应用程序时不会有任何损失,无需任何有意识的努力。

请注意,每次刷新的性能很大程度上取决于存储写入性能,因此,在最近使用 M.2 SSD 的计算机上进行本地基准测试时,您可能会发现刷新速度并不慢,这些 M.2 SSD 的速度非常快。但是,最好不要过于相信本地结果,因为您实际部署应用程序的云服务器的存储性能不太可能那么高。

魔术盒

在这里,我将介绍一些用于实现性能的技巧。从 v1 继承下来的是利用 System.Threading.Channels 创建异步异步写入过程,并有效使用 buffered through 来优化对 Stream 的写入,但我将跳过解释。

对于 JSON 转换,参数在 InterpolatedStringHandler 中暂时保留为值。在这种情况下,问题就出现了如何保持 的值。通常,您会考虑将其作为对象类型保存,例如 .<T>List<object>

[InterpolatedStringHandler]
public ref struct ZLoggerInterpolatedStringHandler
{
    // Using object to store values of any <T> type, not good as it causes boxing
    List<object> parameters = new ();

    public void AppendFormatted<T>(T value, int alignment = 0, string? format = null, [CallerArgumentExpression("value")] string? argumentName = null)
    {
        parameters.Add((object)value);
    }
}

为了避免这种情况,ZLogger准备了一种称为MagicalBox的机制。

[InterpolatedStringHandler]
public ref struct ZLoggerInterpolatedStringHandler
{
    // Pack infinitely into the magic box
    MagicalBox magicalBox;
    List<int> boxOffsets = new (); // Actually, this part is carefully cached

    public void AppendFormatted<T>(T value, int alignment = 0, string? format = null, [CallerArgumentExpression("value")] string? argumentName = null)
    {
        if (magicalBox.TryWrite(value, out var offset)) // No boxing occurs!
        {
            boxOffsets.Add(offset);
        }
    }
}

MagicalBox 基于这样一个概念,即它可以编写任何类型(仅限于未托管的类型)而无需装箱。它的实际实现只是使用Unsafe.Write写入,并使用基于偏移量的Unsafe.Read进行读取。

internal unsafe partial struct MagicalBox
{
    byte[] storage;
    int written;

    public MagicalBox(byte[] storage)
    {
        this.storage = storage;
    }

    public bool TryWrite<T>(T value, out int offset)
    {
        if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
        {
            offset = 0;
            return false;
        }
        Unsafe.WriteUnaligned(ref storage[written], value);
        offset = written;
        written += Unsafe.SizeOf<T>();
        return true;
    }

    public bool TryRead<T>(int offset, out T value)
    {
        if (!RuntimeHelpers.IsReferenceOrContainsReferences<T>())
        {
            value = default!;
            return false;
        }
        value = Unsafe.ReadUnaligned<T>(ref storage[offset]);
        return true;
    }
}

这是基于 MemoryPack 序列化程序的实现经验,效果很好。

请注意,在实际代码中,它变成了一个稍微复杂的代码,包括高效重用、非泛型读取支持、对枚举的特殊处理等。

自定义格式字符串

ZLogger 的字符串插值的一个优点是,如果在参数值中包含方法调用,则会在 LogLevel 检查之后调用它们,从而防止不必要的执行。

// This  
logger.ZLogDebug($"Id {obj.GetId()}: Data: {obj.GetData()}.");  
  
// Is checked for LogLevel validity before methods are called, like this  
if (logger.IsEnabled(LogLvel.Debug))  
{  
    // snip...  
    writer.AppendFormatterd(obj.GetId());  
    writer.AppendFormatterd(obj.GetData());  
}

但是,在将方法调用输出到结构化日志记录时,ZLogger 使用从 C# 10.0 开始添加的 CallerArgumentExpression 来获取参数名称,因此在方法调用的情况下,它的输出具有相当尴尬的名称“obj”。GetId()”。因此,您可以使用特殊的自定义格式字符串指定别名。

// You can give an alias with @name  
logger.ZLogDebug($"Id {obj.GetId():@id}: Data: {obj.GetData():@data}.");

在 ZLogger 中,按照字符串插值的原始表达式,您可以指定与“,”的对齐方式,并使用“:”设置字符串的格式。此外,作为特殊名称,如果格式字符串以 @ 开头,则将其输出为参数名称。

@参数名称规格和格式字符串可以一起使用。

// Today is 2023-12-19.  
// {"date":"2023-12-19T11:25:34.3642389+09:00"}  
logger.ZLogDebug($"Today is {DateTime.Now:@date:yyyy-MM-dd}.");

另一个常见的特殊格式字符串是“json”,它允许以 JsonSerialized 形式输出(此功能的灵感来自 Serilog 的功能)

var position = new { Latitude = 25, Longitude = 134 };  
var elapsed = 34;  
  
// {"position":{"Latitude":25,"Longitude":134},"elapsed":34}  
// Processed {"Latitude":25,"Longitude":134} in 034 ms.  
logger.ZLogInformation($"Processed {position:json} in {elapsed:000} ms.");

还为 PrefixFormatter/SuffixFormatter 准备了特殊格式的字符串,用于将日志级别、类别、日期添加到开头/结尾。

logging.AddZLoggerConsole(options =>  
{  
    options.UsePlainTextFormatter(formatter =>  
    {  
        // 2023-12-19 02:46:14.289 [DBG]......  
        formatter.SetPrefixFormatter($"{0:utc-longdate} [{1:short}]", (template, info) => template.Format(info.Timestamp, info.LogLevel));  
    });  
});

对于时间戳,有 、 、 等。对于 LogLevel,转换为 3 个字符的对数级别表示法(开头的长度匹配,以便在编辑器中打开时更易于阅读)。这些内置的特殊格式字符串也具有性能优化的含义。例如,LogLevel 的代码如下所示,因此使用预构建的 UTF8 字符串编写绝对比手动创建格式更有效。

static void AppendLogLevel(ref Utf8StringWriter<IBufferWriter<byte>> writer, ref LogLevel value, ref MessageTemplateChunk chunk)  
{  
    if (!chunk.NoAlignmentAndFormat)  
    {  
        if (chunk.Format == "short")  
        {  
            switch (value)  
            {  
                case LogLevel.Trace:  
                    writer.AppendUtf8("TRC"u8);  
                    return;  
                case LogLevel.Debug:  
                    writer.AppendUtf8("DBG"u8);  
                    return;  
                case LogLevel.Information:  
                    writer.AppendUtf8("INF"u8);  
                    return;  
                case LogLevel.Warning:  
                    writer.AppendUtf8("WRN"u8);  
                    return;  
                case LogLevel.Error:  
                    writer.AppendUtf8("ERR"u8);  
                    return;  
                case LogLevel.Critical:  
                    writer.AppendUtf8("CRI"u8);  
                    return;  
                case LogLevel.None:  
                    writer.AppendUtf8("NON"u8);  
                    return;  
                default:  
                    break;  
            }  
        }  
  
        writer.AppendFormatted(value, chunk.Alignment, chunk.Format);  
        return;  
    }  
  
    switch (value)  
    {  
        case LogLevel.Trace:  
            writer.AppendUtf8("Trace"u8);  
            break;  
        case LogLevel.Debug:  
            writer.AppendUtf8("Debug"u8);  
            break;  
        case LogLevel.Information:  
            writer.AppendUtf8("Information"u8);  
            break;  
        case LogLevel.Warning:  
            writer.AppendUtf8("Warning"u8);  
            break;  
        case LogLevel.Error:  
            writer.AppendUtf8("Error"u8);  
            break;  
        case LogLevel.Critical:  
            writer.AppendUtf8("Critical"u8);  
            break;  
        case LogLevel.None:  
            writer.AppendUtf8("None"u8);  
            break;  
        default:  
            writer.AppendFormatted(value);  
            break;  
    }  
}

.NET 8 XxHash3 + 非 GC 堆

XxHash3 已从 .NET 8 添加。它是 XxHash 的最新系列,是最快的哈希算法,其性能如此之高,几乎可以毫不犹豫地用于从小数据到大数据的所有内容。请注意,它需要 NuGet,因此它甚至可以与 .NET Standard 2.0 一起使用,而不仅仅是 .NET 8。

ZLogger 在多个地方使用它,但作为一个示例,以下是从 String Interpolation 字符串文字中检索缓存的过程:

// LiteralList generated by $"Hello my name is {name}, {age} years old."
// ["Hello my name is ", "name", ", ", "age", " years old."]
// Process to retrieve UTF8 converted cache (MessageSequence) from this
static readonly ConcurrentDictionary<LiteralList, MessageSequence> cache = new();

// Non-.NET 8 version
#if !NET8_0_OR_GREATER
struct LiteralList(List<string?> literals) : IEquatable<LiteralList>
{
    [ThreadStatic]
    static XxHash3? xxhash;

    public override int GetHashCode()
    {
        var h = xxhash;
        if (h == null)
        {
            h = xxhash = new XxHash3();
        }
        else
        {
            h.Reset();
        }

        var span = CollectionsMarshal.AsSpan(literals);
        foreach (var item in span)
        {
            h.Append(MemoryMarshal.AsBytes(item.AsSpan()));
        }

        // https://github.com/Cyan4973/xxHash/issues/453
        // XXH3 64bit -> 32bit, okay to simple cast answered by XXH3 author.
        return unchecked((int)h.GetCurrentHashAsUInt64());
    }

    public bool Equals(LiteralList other)
    {
        var xs = CollectionsMarshal.AsSpan(literals);
        var ys = CollectionsMarshal.AsSpan(other.literals);
        if (xs.Length == ys.Length)
        {
            for (int i = 0; i < xs.Length; i++)
            {
                if (xs[i] != ys[i]) return false;
            }
            return true;
        }
        return false;
    }
}
#endif

XxHash3 是一个类(如果它是像 System.HashCode 这样的结构,那就太好了),因此在生成 GetHashCode 时,它与 ThreadStatic 一起重用。XxHash3 只输出 ulong,但根据作者的说法,当下降到 32 位时,可以直接丢弃而不使用 XOR 或任何东西。

这是正常用法,但对于 .NET 8 版本,我们实施了极端的优化。

#if NET8_0_OR_GREATER

struct LiteralList(List<string?> literals) : IEquatable<LiteralList>
{
    // literals are all const string, in .NET 8 it is allocated in Non-GC Heap so can compare by address.
    // https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#non-gc-heap
    static ReadOnlySpan<byte> AsBytes(ReadOnlySpan<string?> literals)
    {
        return MemoryMarshal.CreateSpan(
            ref Unsafe.As<string?, byte>(ref MemoryMarshal.GetReference(literals)),
            literals.Length * Unsafe.SizeOf<string>());
    }

    public override int GetHashCode()
    {
        return unchecked((int)XxHash3.HashToUInt64(AsBytes(CollectionsMarshal.AsSpan(literals))));
    }

    public bool Equals(LiteralList other)
    {
        var xs = CollectionsMarshal.AsSpan(literals);
        var ys = CollectionsMarshal.AsSpan(other.literals);
        return AsBytes(xs).SequenceEqual(AsBytes(ys));
    }
}
#endif

它转换为 ,然后一次性调用 XxHash3.HashToUInt64 或 SequenceEqual。这显然更有效,但转换为合法吗?在这种情况下,string 的转换意味着转换为 ,也就是说,它旨在转换为堆中字符串的地址列表。

到目前为止,这还不错,但问题是比较地址是否不太危险。首先,即使字符串与字符串相同,它们通常也可以位于不同的地址。其次,堆中字符串的地址不是固定的,它们可以移动。如果我们要求 GetHashCode 或 Equals 作为字典键,则必须在应用程序执行期间完全修复它。

但是,着眼于此使用示例,String Interpolation 调用的 AppendLiteral 在编译时始终作为常量传递,例如 .因此,可以保证它指向同一实体。

[InterpolatedStringHandler]
public ref struct ZLoggerInterpolatedStringHandler
{
    public void AppendLiteral([ConstantExpected] string s)
}

作为预防措施,我们明确指出,仅应使用 ConstantExpected(已从 .NET 8 启用)传递常量。

另一点是,这样的常量字符串已经被隔离了,但不能保证它们被隔离的地方在 .NET 8 之前不会移动。但是,随着 .NET 8 中 Non-GC Heap 的引入,可以说可以保证它不会移动。

// From .NET 8, the result of GC.GetGeneration for constants is int.MaxValue (in Non-GC Heap)  
var str = "foo";  
Console.WriteLine(GC.GetGeneration(str)); // 2147483647

这使我们能够最大限度地提高从 UTF16 字符串到 UTF8 字符串的转换速度,这在 C# 中是不可避免的。请注意,Source Generator 版本本身可以消除这种查找成本,因此正如基准测试结果所示,它甚至更快。

.NET 8 IUtf8SpanFormattable

ZLogger 使用直接写入 UTF8 而不通过字符串作为性能支柱。从 .NET 8 开始,添加了 IUtf8SpanFormattable,它允许将值泛型直接转换为 UTF8。ZLogger 在 .NET 8 之前支持 .NET Standard 2.0,因此像 int 和 double 这样的基本原语是通过特殊处理直接写入 UTF8 的,但在 .NET 8 的情况下,支持范围更广,因此如果可能的话,建议使用 .NET 8。

请注意,IUtf8SpanFormattable 不关心格式字符串的对齐方式,因此 Cysharp/Utf8StringInterpolation(一个单独的库)是一个在支持 .NET Standard 2.0 的同时添加对齐支持的库。

.NET 8 TimeProvider

TimeProvider 是从 .NET 8 添加的时间相关 API(包括 TimeZone、Timer 等)的抽象,对于单元测试等非常有用,将来会是必不可少的类。TimeProvider 也可通过 Microsoft.Bcl.TimeProvider 用于 .NET Standard 2.0 和 Unity,甚至适用于 .NET 8 以下的版本。

所以在ZLogger中,你可以通过在ZLoggerOptions中指定TimerProvider来修复日志输出的时间。

// It's better to use FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing
class FakeTime : TimeProvider
{
    public override DateTimeOffset GetUtcNow()
    {
        return new DateTimeOffset(1999, 12, 30, 11, 12, 33, TimeSpan.Zero);
    }
    
    public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
}

public class TimestampTest
{
    [Fact]
    public void LogInfoTimestamp()
    {
        var result = new List<string>();
        using var factory = LoggerFactory.Create(builder =>
        {
            builder.AddZLoggerInMemory((options, _) =>
            {
                options.TimeProvider = new FakeTime(); // Set TimeProvider to a custom one
                options.UsePlainTextFormatter(formatter =>
                {
                    // Add Timestamp to the beginning
                    formatter.SetPrefixFormatter($"{0} | ", (template, info) => template.Format(info.Timestamp));
                });
            }, x =>
            {
                x.MessageReceived += msg => result.Add(msg);
            });
        });

        var logger = factory.CreateLogger<TimestampTest>();
        logger.ZLogInformation($"Foo");

        Assert.Equal("1999-12-30 11:12:33.000 | Foo", result[0]);
    }
}

当您需要使用日志输出的精确匹配进行测试时,可以有效地使用此功能。

源生成器

Microsoft.Extensions.Logging 提供 LoggerMessageAttribute 和源生成器作为高性能日志输出的标准。

虽然这对于生成 UTF16 字符串确实非常出色,但结构化日志记录生成部分存在一个问号。

// This partial method
[LoggerMessage(LogLevel.Information, "My name is {name}, age is {age}.")]
public static partial void MSLog(this ILogger logger, string name, int age, int other);

// Generates this class
private readonly struct __MSLogStruct : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>
{
    private readonly global::System.String _name;
    private readonly global::System.Int32 _age;

    public __MSLogStruct(global::System.String name, global::System.Int32 age)
    {
        this._name = name;
        this._age = age;
    }

    public override string ToString()
    {
        var name = this._name;
        var age = this._age;
        return $"My name is {name}, age is {age}."; // String generation seems fast (it's riding on C# 10.0's String Interpolation Improvements, so no complaints!)
    }

    public static readonly global::System.Func<__MSLogStruct, global::System.Exception?, string> Format = (state, ex) => state.ToString();

    public int Count => 4;

    // This is the code for Structured Logging, but hmm...?
    public global::System.Collections.Generic.KeyValuePair<string, object?> this[int index]
    {
        get => index switch
        {
            0 => new global::System.Collections.Generic.KeyValuePair<string, object?>("name", this._name),
            1 => new global::System.Collections.Generic.KeyValuePair<string, object?>("age", this._age),
            2 => new global::System.Collections.Generic.KeyValuePair<string, object?>("other", this._other),
            3 => new global::System.Collections.Generic.KeyValuePair<string, object?>("{OriginalFormat}", "My name is {name}, age is {age}."),
            _ => throw new global::System.IndexOutOfRangeException(nameof(index)),  // return the same exception LoggerMessage.Define returns in this case
        };
    }

    public global::System.Collections.Generic.IEnumerator<global::System.Collections.Generic.KeyValuePair<string, object?>> GetEnumerator()
    {
        for (int i = 0; i < 4; i++)
        {
            yield return this[i];
        }
    }

    global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
public static partial void MSLog(this global::Microsoft.Extensions.Logging.ILogger logger, global::System.String name, global::System.Int32 age)
{
    if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
    {
        logger.Log(
            global::Microsoft.Extensions.Logging.LogLevel.Information,
            new global::Microsoft.Extensions.Logging.EventId(764917357, nameof(MSLog)),
            new __MSLogStruct(name, age),
            null,
            __MSLogStruct.Format);
    }
}

有了,拳击在正常创建时无法避免,也没办法。KeyValuePair<string, object?>

因此,ZLogger 提供了一个类似的 Source Generator 属性,称为 。这将启用 UTF8 优化和无装箱 JSON 日志记录。

// Just change LoggerMessage to ZLoggerMessage
// Note that in the format string part of ZLoggerMessage, you can use @ for aliases and json for JSON conversion, just like in the String Interpolation version
[ZLoggerMessage(LogLevel.Information, "My name is {name}, age is {age}.")]
static partial void ZLoggerLog(this ILogger logger, string name, int age);

// This kind of code is generated
readonly struct ZLoggerLogState : IZLoggerFormattable
{
    // Pre-generate JsonEncodedText for JSON
    static readonly JsonEncodedText _jsonParameter_name = JsonEncodedText.Encode("name");
    static readonly JsonEncodedText _jsonParameter_age = JsonEncodedText.Encode("age");

    readonly string name;
    readonly int age;

    public ZLoggerLogState(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public IZLoggerEntry CreateEntry(LogInfo info)
    {
        return ZLoggerEntry<ZLoggerLogState>.Create(info, this);
    }

    public int ParameterCount => 2;
    public bool IsSupportUtf8ParameterKey => true;
    public override string ToString() => $"My name is {name}, age is {age}.";

    // Text messages are directly written to UTF8
    public void ToString(IBufferWriter<byte> writer)
    {
        var stringWriter = new Utf8StringWriter<IBufferWriter<byte>>(literalLength: 21, formattedCount: 2, bufferWriter: writer);
        stringWriter.AppendUtf8("My name is "u8); // Write literals directly with u8
        stringWriter.AppendFormatted(name, 0, null);
        stringWriter.AppendUtf8(", age is "u8);
        stringWriter.AppendFormatted(age, 0, null);
        stringWriter.AppendUtf8("."u8);            
        stringWriter.Flush();
    }

    // For JSON output, write directly to Utf8JsonWriter to completely avoid boxing
    public void WriteJsonParameterKeyValues(Utf8JsonWriter writer, JsonSerializerOptions jsonSerializerOptions, IKeyNameMutator? keyNameMutator = null)
    {
        // The method called differs depending on the type (WriteString, WriteNumber, etc...)
        writer.WriteString(_jsonParameter_name, this.name);
        writer.WriteNumber(_jsonParameter_age, this.age);
    }

    // Methods for extensions such as MessagePack support are actually generated below, but omitted
} 

static partial void ZLoggerLog(this global::Microsoft.Extensions.Logging.ILogger logger, string name, int age)
{
    if (!logger.IsEnabled(LogLevel.Information)) return;
    logger.Log(
        LogLevel.Information,
        new EventId(-1, nameof(ZLoggerLog)),
        new ZLoggerLogState(name, age),
        null,
        (state, ex) => state.ToString()
    );
}

通过直接写入 Utf8JsonWriter 并将键名称预生成为 JsonEncodedText,我们可以最大限度地提高 JSON 转换的性能。

此外,结构化日志记录不仅限于 JSON,还可以使用其他格式。例如,使用 MessagePack 可以使其更小、更快。ZLogger 定义了接口以避免装箱,即使对于输出到非内置协议(如特定于 JSON 的协议)也是如此。

public interface IZLoggerFormattable : IZLoggerEntryCreatable
{
    int ParameterCount { get; }

    // Used for message output
    void ToString(IBufferWriter<byte> writer);
    
    // Used for JSON output
    void WriteJsonParameterKeyValues(Utf8JsonWriter jsonWriter, JsonSerializerOptions jsonSerializerOptions, IKeyNameMutator? keyNameMutator = null);

    // Used for other structured log outputs
    ReadOnlySpan<byte> GetParameterKey(int index);
    ReadOnlySpan<char> GetParameterKeyAsString(int index);
    object? GetParameterValue(int index);
    T? GetParameterValue<T>(int index);
    Type GetParameterType(int index);
}

这是一个有点不寻常的接口,但是通过运行这样的循环,我们可以消除装箱的发生:

for (var i in ParameterCount)  
{  
    var key = GetParameterKey(i);  
    var value = GetParameterValue<int>();  
}

此设计与 ADO.NET 中 IDataRecord 的用法相同。此外,在 Unity 中,通过索引进行检索是很常见的,以避免将数组从原生分配到托管数组。

统一

即使使用 Unity 2023,官方支持的 C# 版本也是 9.0。ZLogger 假定 C# 10.0 或更高版本的字符串插值作为先决条件,因此它将无法正常工作。通常。然而,虽然还没有正式公布,但我们发现,从 ,包含的编译器的版本已经提高,内部可以使用 C# 10.0 进行编译。

您可以通过文件传递编译器选项,因此,如果您在那里明确指定语言版本,则所有 C# 10.0 语法都将可用。

-langVersion:10

实际上,输出 csproj 仍然指定 ,因此您无法在 IDE 上使用 C# 10.0 编写。因此,让我们使用 Cysharp/CsprojModifier 覆盖 LangVersion。如果您创建一个名为这样的文件,并让 CsprojModifier 混合其中,您也可以在 IDE 上编写为 C# 10.0。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <LangVersion>10</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

对于 Unity,我们添加了一个名为 的扩展,因此AddZLoggerUnityDebug

// Prepare such a global utility
public static class LogManager
{
    static ILoggerFactory loggerFactory;

    public static ILogger<T> CreateLogger<T>() => loggerFactory.CreateLogger<T>();
    public static readonly Microsoft.Extensions.Logging.ILogger Global;
    
    static LogManager()
    {
        loggerFactory = LoggerFactory.Create(logging =>
        {
            logging.SetMinimumLevel(LogLevel.Trace);
            logging.AddZLoggerUnityDebug(); // log to UnityDebug
        });
        Global = loggerFactory.CreateLogger("Logger");
        Application.exitCancellationToken.Register(() =>
        {
            loggerFactory.Dispose(); // flush when application exit.
        });
    }
}

// Try using it like this, for example
public class NewBehaviourScript : MonoBehaviour
{
    static readonly ILogger<NewBehaviourScript> logger = LogManager.CreateLogger<NewBehaviourScript>();

    void Start()
    {
        var name = "foo";
        var hp = 100;
        logger.ZLogInformation($"{name} HP is {hp}.");
    }
}

请注意,C# 10.0 字符串插值的性能改进仅适用于使用 ZLog 时,使用字符串插值进行正常字符串生成不会提高性能。这是因为运行时需要 DefaultInterpolatedStringHandler 来提高字符串生成性能,这仅在 .NET 6 及更高版本中包含在。如果 DefaultInterpolatedStringHandler 不存在,它将回退到传统字符串。格式,因此装箱照常进行。

它支持所有JSON结构化日志、输出定制、文件输出等。

var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.AddZLoggerFile("/path/to/logfile", options =>
    {
        options.UseJsonFormatter();
    });
});

还有一个好处,在及更高版本中,C# 编译器版本要高一些,如果您指定 ,则可以使用 C# 11.0。此外,ZLogger 的源生成器会自动启用,因此您可以使用它来生成。

public static partial class LogExtensions
{
    [ZLoggerMessage(LogLevel.Debug, "Hello, {name}")]
    public static partial void Hello(this ILogger<NewBehaviourScript> logger, string name);
}

由于源生成器生成的代码需要 C# 11.0(因为它大量使用 UTF8 字符串文字),因此仅限于及以上。

顺便说一句,Unity 已经发布了 com.unity.logging 作为同类的标准日志库。它允许以相同的方式进行结构化日志记录和文件输出,并且它有一个有趣的设计,即使用 Source Generator 自动生成类本身并根据参数生成方法重载以避免值装箱。关于 Burst 的讨论很多,但我认为这种对 Source Generator 的大胆使用是性能的关键。ZLogger 正在使用 C# 10.0 的字符串插值,但我没有考虑过这种方法作为解决方法。真是让人大开眼界。性能也相当精致。

由于字符串插值,ZLogger 具有更好的写作感觉,我认为性能是一个很好的匹配......你觉得怎么样?

如果你喜欢我的文章,请给我一个赞!谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
java.net.ConnectException: Connection refused: connect 是一个Java编程中常见的异常错误。这个错误通常表示在客户端尝试连接到服务器时发生了连接拒绝的情况。 这个错误可能有几种可能的原因。一种可能的原因是服务器端没有在指定的端口上监听连接请求,或者服务器端被防火墙或其他网络安全设备阻止了连接请求。另一种可能的原因是客户端与服务器之间的网络连接出现了问题,可能是由于网络故障、配置错误或者服务器负载过高导致的。无论是哪种原因,这个错误都表示客户端无法与服务器建立连接。 为了解决这个问题,你可以尝试以下几个步骤: 1. 检查服务器是否正常运行,并且在指定的端口上监听连接请求。 2. 确保客户端和服务器之间的网络连接正常,并且没有被防火墙或其他网络安全设备阻止。 3. 检查客户端和服务器之间的网络配置,确保它们能够正确地进行通信。 4. 如果服务器负载过高,可以尝试等待一段时间后再次尝试连接。 希望以上的解释和建议对你有所帮助。如果还有其他问题,请随时提问。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【Eureka】java.net.ConnectException: Connection refused: connect](https://blog.csdn.net/qq_41570658/article/details/114094384)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.269^v2^control"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Socket异常与MINA异常](https://blog.csdn.net/iteye_5372/article/details/81614982)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.269^v2^control"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值