diff --git a/YamlDotNet.Benchmark/AliasBenchmarks.cs b/YamlDotNet.Benchmark/AliasBenchmarks.cs new file mode 100644 index 000000000..164206591 --- /dev/null +++ b/YamlDotNet.Benchmark/AliasBenchmarks.cs @@ -0,0 +1,66 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Benchmark; + +// Exercises the anchor/alias path, which the rest of the suite does not touch. The graph holds +// many references to a small set of shared objects, so serialization (with aliases enabled, the +// default) emits anchors + aliases, and deserialization resolves them via the alias value +// deserializer. This isolates the anchor-assignment, anchor-name and alias-resolution hot paths. +public class AliasBenchmarks +{ + private const int ReferenceCount = 5000; + + private readonly ISerializer serializer = new SerializerBuilder().Build(); + private readonly IDeserializer deserializer = new DeserializerBuilder().Build(); + + private List
graph = null!; + private string yaml = ""; + + [GlobalSetup] + public void Setup() + { + var shared = new[] + { + new Address { Street = "1 Shared Way", City = "Common", State = "CA", Zip = "90001", Country = "US" }, + new Address { Street = "2 Shared Way", City = "Common", State = "CA", Zip = "90002", Country = "US" }, + new Address { Street = "3 Shared Way", City = "Common", State = "CA", Zip = "90003", Country = "US" }, + }; + + graph = new List
(ReferenceCount); + for (var i = 0; i < ReferenceCount; i++) + { + graph.Add(shared[i % shared.Length]); + } + + yaml = serializer.Serialize(graph); + } + + [Benchmark] + public string SerializeWithAliases() => serializer.Serialize(graph); + + [Benchmark] + public List
DeserializeWithAliases() => deserializer.Deserialize>(yaml); +} diff --git a/YamlDotNet.Benchmark/BigFileBenchmark.cs b/YamlDotNet.Benchmark/BigFileBenchmark.cs index efad79e7c..e0a1b9af3 100644 --- a/YamlDotNet.Benchmark/BigFileBenchmark.cs +++ b/YamlDotNet.Benchmark/BigFileBenchmark.cs @@ -19,16 +19,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using System.IO.Compression; using System.Text; using BenchmarkDotNet.Attributes; -using FastSerialization; -using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; namespace YamlDotNet.Benchmark; -[MemoryDiagnoser] public class BigFileBenchmark { private string yamlString = ""; diff --git a/YamlDotNet.Benchmark/ObjectGraphBenchmarks.cs b/YamlDotNet.Benchmark/ObjectGraphBenchmarks.cs new file mode 100644 index 000000000..54286263c --- /dev/null +++ b/YamlDotNet.Benchmark/ObjectGraphBenchmarks.cs @@ -0,0 +1,60 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using BenchmarkDotNet.Attributes; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Benchmark; + +// Exercises the full serialize / deserialize object-mapping pipeline over a realistic, +// heterogeneous typed graph (see SampleModel). This is the read-path-weighted core of the +// campaign: DeserializeTyped and RoundtripTyped drive the reflection/property-lookup hot paths, +// SerializeTyped drives object-graph traversal + the emitter, and DeserializeUntyped drives the +// scalar/collection/dictionary deserializers with runtime type resolution. +public class ObjectGraphBenchmarks +{ + private const int EmployeeCount = 2000; + + private readonly ISerializer serializer = new SerializerBuilder().Build(); + private readonly IDeserializer deserializer = new DeserializerBuilder().Build(); + + private Company company = null!; + private string yaml = ""; + + [GlobalSetup] + public void Setup() + { + company = SampleModel.CreateCompany(EmployeeCount); + yaml = serializer.Serialize(company); + } + + [Benchmark] + public string SerializeTyped() => serializer.Serialize(company); + + [Benchmark] + public Company DeserializeTyped() => deserializer.Deserialize(yaml); + + [Benchmark] + public object? DeserializeUntyped() => deserializer.Deserialize(yaml); + + [Benchmark] + public Company RoundtripTyped() => deserializer.Deserialize(serializer.Serialize(company)); +} diff --git a/YamlDotNet.Benchmark/ProfilingDriver.cs b/YamlDotNet.Benchmark/ProfilingDriver.cs new file mode 100644 index 000000000..bc96832a1 --- /dev/null +++ b/YamlDotNet.Benchmark/ProfilingDriver.cs @@ -0,0 +1,86 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Diagnostics; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Benchmark; + +// Steady-state CPU-profiling driver, kept out of BenchmarkDotNet so a sampling profiler (ultra) +// sees only YamlDotNet work rather than the BDN harness. It reuses a cached serializer / +// deserializer and a single pre-serialized document (mirroring real reuse), then loops the unit of +// work for a fixed wall-clock window so the profile settles into steady state. The default "both" +// mode is naturally deserialize-weighted because the read path dominates the per-iteration cost. +// +// Capture (elevated shell; profile the built exe directly, not `dotnet run`): +// ultra profile -o baseline --delay 3 -- YamlDotNet.Benchmark.exe profile [both|deser|ser] [seconds] +internal static class ProfilingDriver +{ + public static void Run(string[] args) + { + var mode = args.Length > 1 ? args[1].ToLowerInvariant() : "both"; + var seconds = (args.Length > 2 && int.TryParse(args[2], out var s)) ? s : 25; + + var serializer = new SerializerBuilder().Build(); + var deserializer = new DeserializerBuilder().Build(); + + var company = SampleModel.CreateCompany(2000); + var yaml = serializer.Serialize(company); + + // Validate the workload up front so a broken run fails fast (ultra runs the target silently). + var check = deserializer.Deserialize(yaml); + if (check.Departments.Count == 0 || check.Departments[0].Employees.Count == 0) + { + throw new InvalidOperationException("Profiling workload produced an empty graph."); + } + + var stopwatch = Stopwatch.StartNew(); + long iterations = 0; + long checksum = 0; + + while (stopwatch.Elapsed.TotalSeconds < seconds) + { + if (mode != "ser") + { + var round = deserializer.Deserialize(yaml); + checksum += round.Departments.Count; + } + + if (mode != "deser") + { + var text = serializer.Serialize(company); + checksum += text.Length; + } + + iterations++; + } + + // Guard against dead-code elimination of the loop body. + if (checksum < 0) + { + throw new InvalidOperationException("unreachable"); + } + + Console.Error.WriteLine( + $"Profiling driver finished: mode={mode}, iterations={iterations}, elapsed={stopwatch.Elapsed.TotalSeconds:F1}s"); + } +} diff --git a/YamlDotNet.Benchmark/Program.cs b/YamlDotNet.Benchmark/Program.cs index 834f606bf..18006eefb 100644 --- a/YamlDotNet.Benchmark/Program.cs +++ b/YamlDotNet.Benchmark/Program.cs @@ -19,7 +19,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Running; using YamlDotNet.Benchmark; -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); +// Profiling driver mode (for a CPU sampling profiler such as ultra) — a steady-state loop that +// bypasses the BenchmarkDotNet harness. See ProfilingDriver for usage. +if (args.Length > 0 && args[0].Equals("profile", StringComparison.OrdinalIgnoreCase)) +{ + ProfilingDriver.Run(args); + return; +} + +// Shared config for every benchmark: add MemoryDiagnoser for allocation tracking. DefaultConfig +// already emits a GitHub-flavoured markdown export, so before/after tables can be pasted straight +// into a PR. Run on a quiet machine so the numbers stay clean: +// dotnet run -c Release --project YamlDotNet.Benchmark -f net10.0 -- --filter '*' +var config = DefaultConfig.Instance + .AddDiagnoser(MemoryDiagnoser.Default); + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); diff --git a/YamlDotNet.Benchmark/Resources/saltern.yml.gz b/YamlDotNet.Benchmark/Resources/saltern.yml.gz deleted file mode 100644 index abf868e13..000000000 Binary files a/YamlDotNet.Benchmark/Resources/saltern.yml.gz and /dev/null differ diff --git a/YamlDotNet.Benchmark/SampleModel.cs b/YamlDotNet.Benchmark/SampleModel.cs new file mode 100644 index 000000000..8fa0c5c9d --- /dev/null +++ b/YamlDotNet.Benchmark/SampleModel.cs @@ -0,0 +1,159 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; + +namespace YamlDotNet.Benchmark; + +// A plausible, heterogeneous configuration-shaped object graph used by the serialization +// benchmarks. It deliberately mixes the scalar kinds and container shapes that exercise the +// real hot paths: strings, ints, decimals, bools, DateTimes, enums, nested objects, string +// lists, string dictionaries and nullable references. Generation is fully deterministic +// (index-derived, no randomness) so the produced YAML is byte-stable across runs. + +public enum EmployeeRole +{ + Engineer, + Manager, + Director, + Executive, +} + +public sealed class Address +{ + public string Street { get; set; } = ""; + public string City { get; set; } = ""; + public string State { get; set; } = ""; + public string Zip { get; set; } = ""; + public string Country { get; set; } = ""; +} + +public sealed class Employee +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Email { get; set; } = ""; + public decimal Salary { get; set; } + public bool Active { get; set; } + public DateTime HireDate { get; set; } + public EmployeeRole Role { get; set; } + public List Skills { get; set; } = new(); + public Address? Home { get; set; } +} + +public sealed class Department +{ + public string Name { get; set; } = ""; + public Address Location { get; set; } = new(); + public Dictionary Metadata { get; set; } = new(); + public List Employees { get; set; } = new(); +} + +public sealed class Company +{ + public string Name { get; set; } = ""; + public DateTime Founded { get; set; } + public long Revenue { get; set; } + public bool Public { get; set; } + public List Departments { get; set; } = new(); +} + +public static class SampleModel +{ + private static readonly string[] SkillsPool = + { + "csharp", "yaml", "docker", "kubernetes", "sql", "azure", "react", "python", + }; + + private static readonly DateTime Epoch = new(2010, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public static Company CreateCompany(int employeeCount, int departmentCount = 8) + { + var company = new Company + { + Name = "Contoso Ltd", + Founded = new DateTime(1998, 3, 14, 0, 0, 0, DateTimeKind.Utc), + Revenue = 1_250_000_000L, + Public = true, + Departments = new List(departmentCount), + }; + + var perDepartment = Math.Max(1, employeeCount / departmentCount); + + for (var d = 0; d < departmentCount; d++) + { + var department = new Department + { + Name = "Department " + d, + Location = new Address + { + Street = (100 + d) + " Main Street", + City = "City" + d, + State = "State" + (d % 4), + Zip = (10000 + d).ToString(), + Country = "US", + }, + Metadata = new Dictionary + { + ["costCenter"] = "CC-" + d, + ["region"] = "region-" + (d % 3), + ["tier"] = (d % 2 == 0) ? "primary" : "secondary", + }, + Employees = new List(perDepartment), + }; + + for (var e = 0; e < perDepartment; e++) + { + var i = (d * perDepartment) + e; + department.Employees.Add(new Employee + { + Id = i, + Name = "Employee " + i, + Email = "employee" + i + "@contoso.example", + Salary = 50_000m + ((i % 500) * 137.5m), + Active = (i % 7) != 0, + HireDate = Epoch.AddDays(i % 4000), + Role = (EmployeeRole)(i % 4), + Skills = new List + { + SkillsPool[i % SkillsPool.Length], + SkillsPool[(i + 3) % SkillsPool.Length], + }, + Home = (i % 5) == 0 + ? null + : new Address + { + Street = i + " Residential Ave", + City = "Town" + (i % 50), + State = "State" + (i % 4), + Zip = (20000 + (i % 9000)).ToString(), + Country = "US", + }, + }); + } + + company.Departments.Add(department); + } + + return company; + } +} diff --git a/YamlDotNet.Benchmark/SerializationBenchmarks.cs b/YamlDotNet.Benchmark/SerializationBenchmarks.cs index 41f518492..7e622e2b9 100644 --- a/YamlDotNet.Benchmark/SerializationBenchmarks.cs +++ b/YamlDotNet.Benchmark/SerializationBenchmarks.cs @@ -19,16 +19,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using System.Text; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Jobs; using YamlDotNet.Serialization; namespace YamlDotNet.Benchmark; -[MemoryDiagnoser] -[MediumRunJob(RuntimeMoniker.Net80)] -[MediumRunJob(RuntimeMoniker.Net47)] public class SerializationBenchmarks { public class SampleRecord diff --git a/YamlDotNet.Benchmark/YamlStreamBenchmark.cs b/YamlDotNet.Benchmark/YamlStreamBenchmark.cs index 4c3a6a666..1625bea08 100644 --- a/YamlDotNet.Benchmark/YamlStreamBenchmark.cs +++ b/YamlDotNet.Benchmark/YamlStreamBenchmark.cs @@ -24,7 +24,6 @@ namespace YamlDotNet.Benchmark; -[MemoryDiagnoser] public class YamlStreamBenchmark { private string yamlString = "";