Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Build and Test

on:
push:
branches: [ main ]
pull_request:

env:
BUILD_CONFIG: Release

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build -c Release --no-restore

- name: Test
run: dotnet test -c Release --no-build --verbosity normal
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
.DS_Store

/packages/*

BenchmarkDotNet.Artifacts/
163 changes: 163 additions & 0 deletions ModernUO.Serialization.Generator.Benchmarks/GeneratorBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GeneratorBenchmarks.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/

using System.Collections.Immutable;
using System.Text;
using BenchmarkDotNet.Attributes;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using ModernUO.Serialization.Generator;
using ModernUO.Serialization.Generator.Tests.Helpers;

namespace ModernUO.Serialization.Generator.Benchmarks;

/// <summary>
/// Measures the generator over a synthetic corpus: a cold full run, and the incremental
/// re-run cost after editing a single file - the operation an IDE performs constantly.
/// A correctly incremental pipeline keeps the re-run near the cost of one class; a broken
/// one re-runs the whole corpus.
/// </summary>
[MemoryDiagnoser]
public class GeneratorBenchmarks
{
[Params(150)]
public int ClassCount { get; set; }

private CSharpCompilation _compilation = null!;
private ImmutableArray<AdditionalText> _additionalTexts;
private List<MetadataReference> _references = null!;

private GeneratorDriver _warmDriver = null!;
private CSharpCompilation _warmCompilation = null!;
private SyntaxTree _editTarget = null!;
private bool _editToggle;

[GlobalSetup]
public void Setup()
{
var trustedAssemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator);

_references = trustedAssemblies
.Where(p => !string.IsNullOrEmpty(p))
.Select(p => (MetadataReference)MetadataReference.CreateFromFile(p))
.Concat([MetadataReference.CreateFromFile(typeof(SerializationGeneratorAttribute).Assembly.Location)])
.ToList();

var trees = new List<SyntaxTree>
{
CSharpSyntaxTree.ParseText(SourceGeneratorTestHelper.ServerStubs)
};

var additionalTexts = ImmutableArray.CreateBuilder<AdditionalText>();

for (var i = 0; i < ClassCount; i++)
{
trees.Add(CSharpSyntaxTree.ParseText(BuildClassSource(i)));
additionalTexts.Add(
new InMemoryAdditionalText($"Server.TestContent.BenchItem{i}.v0.json", BuildMigrationJson(i))
);
}

_additionalTexts = additionalTexts.ToImmutable();
_compilation = CSharpCompilation.Create(
"BenchmarkAssembly",
trees,
_references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

// Warm state for the incremental benchmark: one full run, then per-invocation edits.
_warmDriver = CSharpGeneratorDriver
.Create(new EntitySerializationGenerator())
.AddAdditionalTexts(_additionalTexts)
.RunGenerators(_compilation);
_warmCompilation = _compilation;
_editTarget = _warmCompilation.SyntaxTrees.Last();
}

[Benchmark]
public GeneratorDriver ColdFullRun() =>
CSharpGeneratorDriver
.Create(new EntitySerializationGenerator())
.AddAdditionalTexts(_additionalTexts)
.RunGenerators(_compilation);

[Benchmark]
public GeneratorDriver WarmRerunAfterSingleEdit()
{
// Toggle one file between two whitespace variants so every invocation is a real edit.
_editToggle = !_editToggle;
var text = _editTarget.GetText().ToString();
var edited = _editToggle ? text + "\n// edit\n" : text.Replace("\n// edit\n", "");
var newTree = CSharpSyntaxTree.ParseText(edited);

_warmCompilation = _warmCompilation.ReplaceSyntaxTree(_editTarget, newTree);
_editTarget = newTree;

_warmDriver = _warmDriver.RunGenerators(_warmCompilation);
return _warmDriver;
}

private static string BuildClassSource(int index)
{
var sb = new StringBuilder();
sb.AppendLine("using System;");
sb.AppendLine("using ModernUO.Serialization;");
sb.AppendLine("using Server;");
sb.AppendLine();
sb.AppendLine("namespace Server.TestContent");
sb.AppendLine("{");
sb.AppendLine(" [SerializationGenerator(1)]");
sb.AppendLine($" public partial class BenchItem{index} : ISerializable");
sb.AppendLine(" {");

for (var f = 0; f < 6; f++)
{
sb.AppendLine($" [SerializableField({f})]");
sb.AppendLine($" private {(f % 2 == 0 ? "int" : "string")} _field{f};");
sb.AppendLine();
}

sb.AppendLine(" public DateTime Created { get; set; }");
sb.AppendLine(" public Serial Serial { get; }");
sb.AppendLine(" public bool Deleted => false;");
sb.AppendLine(" public void Delete() { }");
sb.AppendLine();
sb.AppendLine(" private void MigrateFrom(V0Content content)");
sb.AppendLine(" {");
sb.AppendLine(" _field0 = content.Field0;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");

return sb.ToString();
}

private static string BuildMigrationJson(int index) =>
$$"""
{
"version": 0,
"type": "Server.TestContent.BenchItem{{index}}",
"properties": [
{
"name": "Field0",
"type": "int",
"rule": "PrimitiveTypeMigrationRule"
}
]
}
""";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ModernUO.Serialization.Generator\ModernUO.Serialization.Generator.csproj" />
<ProjectReference Include="..\ModernUO.Serialization.Annotations\ModernUO.Serialization.Annotations.csproj" />
</ItemGroup>

<ItemGroup>
<!-- Shared Server stubs and driver plumbing from the test project. -->
<Compile Include="..\ModernUO.Serialization.Generator.Tests\Helpers\SourceGeneratorTestHelper.cs" Link="SourceGeneratorTestHelper.cs" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions ModernUO.Serialization.Generator.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(ModernUO.Serialization.Generator.Benchmarks.GeneratorBenchmarks).Assembly).Run(args);
114 changes: 114 additions & 0 deletions ModernUO.Serialization.Generator.DiffTool/Application.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Application.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/

using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using ModernUO.Serialization.Generator;
using ModernUO.Serialization.SchemaGenerator;

namespace ModernUO.Serialization.DiffTool;

/// <summary>
/// Runs the generator against every project in a solution and writes a deterministic manifest
/// of hint names and content hashes. Running it before and after a generator change and
/// diffing the manifests proves the change is output-identical across the real corpus.
/// </summary>
public static class Application
{
public static async Task<int> Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine(
"Usage: ModernUO.Serialization.Generator.DiffTool <path to solution> <output manifest file>"
);
return 1;
}

var solutionPath = args[0];
var outputFile = args[1];

var stopwatch = Stopwatch.StartNew();
var lines = new List<string>();

foreach (var project in await SourceCodeAnalysis.GetProjectsAsync(solutionPath))
{
var compilation = await project.GetCompilationAsync();
if (compilation == null)
{
Console.WriteLine($"Skipped {project.Name}: no compilation.");
continue;
}

var additionalTexts = ImmutableArray.CreateBuilder<AdditionalText>();
foreach (var document in project.AdditionalDocuments)
{
if (document.FilePath != null)
{
additionalTexts.Add(new DocumentAdditionalText(document.FilePath, await document.GetTextAsync()));
}
}

GeneratorDriver driver = CSharpGeneratorDriver
.Create(new EntitySerializationGenerator())
.AddAdditionalTexts(additionalTexts.ToImmutable());

driver = driver.RunGenerators(compilation);
var result = driver.GetRunResult().Results[0];

foreach (var source in result.GeneratedSources)
{
var normalized = Normalize(source.SourceText.ToString());
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(normalized)));
lines.Add($"{project.Name}\t{source.HintName}\t{hash}");
}

foreach (var group in result.Diagnostics.GroupBy(d => d.Id).OrderBy(g => g.Key, StringComparer.Ordinal))
{
lines.Add($"{project.Name}\t#diagnostics\t{group.Key}x{group.Count()}");
}

Console.WriteLine($"Hashed {result.GeneratedSources.Length} sources for {project.Name}.");
}

lines.Sort(StringComparer.Ordinal);
await File.WriteAllTextAsync(outputFile, string.Join("\n", lines) + "\n");

Console.WriteLine($"Wrote {lines.Count} manifest lines to {outputFile} in {stopwatch.Elapsed.TotalSeconds:N2}s.");
return 0;
}

// Version-stamped headers and line endings are normalized so manifests compare across
// generator versions and checkout styles.
private static string Normalize(string content) =>
Regex.Replace(
content.Replace("\r\n", "\n"),
"""(Version: |"ModernUO\.Serialization\.Generator", ")\d+\.\d+\.\d+\.\d+""",
"$1{VERSION}"
);

private sealed class DocumentAdditionalText(string path, SourceText text) : AdditionalText
{
public override string Path => path;

public override SourceText GetText(CancellationToken cancellationToken = default) => text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\ModernUO.Serialization.SchemaGenerator\ModernUO.Serialization.SchemaGenerator.csproj" />
</ItemGroup>

<ItemGroup>
<!-- Same MSBuildLocator guard the schema generator uses. -->
<PackageReference Include="Microsoft.Build.Framework" Version="18.0.2" ExcludeAssets="runtime" PrivateAssets="all" />
</ItemGroup>

</Project>
Loading
Loading