mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-19 15:20:25 +08:00
402 lines
17 KiB
C#
402 lines
17 KiB
C#
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using AtomUI.Localization;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Styling;
|
|
|
|
namespace AtomUI.Localization.Performance;
|
|
|
|
internal static class LocalizationScenarios
|
|
{
|
|
private const int MemoryRetentionSampleCount = 16;
|
|
|
|
private static readonly LanguageTag[] s_languagePool =
|
|
[
|
|
LanguageTags.EnUS,
|
|
LanguageTags.ZhCN,
|
|
LanguageTags.ZhTW,
|
|
LanguageTags.JaJP,
|
|
LanguageTags.ArSA,
|
|
LanguageTags.FrFR
|
|
];
|
|
|
|
internal static int Run(int count, string? markdownPath, bool verifyStates)
|
|
{
|
|
if (verifyStates && !LocalizationStateVerification.Run())
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
var startup = MeasureStartupBuild();
|
|
var hotPath = MeasureGetHotPath(Math.Max(1_000, count * 1_000));
|
|
var switching = MeasureLanguageSwitching(Math.Max(10, count * 10));
|
|
var memory = MeasureSnapshotMemory();
|
|
|
|
Console.WriteLine("Localization performance");
|
|
Console.WriteLine("------------------------");
|
|
Console.WriteLine($"Snapshot startup build: {startup.Elapsed.TotalMilliseconds:0.00} ms, " +
|
|
$"{startup.AllocatedBytes / 1024.0:0.0} KiB allocated " +
|
|
$"({startup.CatalogCount} catalogs, {startup.UnitCount} units, {startup.LanguageCount} languages)");
|
|
Console.WriteLine($"ILocalizer.Get hot path: {hotPath.Elapsed.TotalMilliseconds:0.00} ms, " +
|
|
$"{hotPath.AllocatedBytes / 1024.0:0.0} KiB for {hotPath.OperationCount:N0} calls " +
|
|
$"({hotPath.Elapsed.TotalNanoseconds() / hotPath.OperationCount:0.0} ns/call)");
|
|
Console.WriteLine($"Language switch commit: {switching.Elapsed.TotalMilliseconds:0.00} ms, " +
|
|
$"{switching.AllocatedBytes / 1024.0:0.0} KiB for {switching.OperationCount:N0} changes, " +
|
|
$"{switching.ResourceNotificationCount:N0} resource notifications, " +
|
|
$"{switching.LanguageEventCount:N0} language events");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Snapshot memory matrix");
|
|
Console.WriteLine("----------------------");
|
|
Console.WriteLine("Catalogs Units Languages RetainedKiB LogicalTextKiB SnapshotSlots");
|
|
foreach (var point in memory)
|
|
{
|
|
Console.WriteLine($"{point.CatalogCount,7} {point.UnitCount,5} {point.LanguageCount,9} " +
|
|
$"{point.RetainedBytes / 1024.0,12:0.0} {point.LogicalTextBytes / 1024.0,14:0.0} {point.SnapshotSlotCount,13}");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(markdownPath))
|
|
{
|
|
var fullPath = Path.GetFullPath(markdownPath);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
|
File.WriteAllText(fullPath, RenderMarkdown(startup, hotPath, switching, memory),
|
|
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Wrote markdown baseline: {fullPath}");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
internal static LocalizationRuntime BuildRuntime(
|
|
int catalogCount,
|
|
int unitCount,
|
|
int languageCount)
|
|
{
|
|
return BuildRuntime(CreateFixture(catalogCount, unitCount, languageCount));
|
|
}
|
|
|
|
private static LocalizationFixture CreateFixture(
|
|
int catalogCount,
|
|
int unitCount,
|
|
int languageCount)
|
|
{
|
|
if (catalogCount is < 1 or > 8)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(catalogCount), catalogCount, "Use 1..8 catalogs.");
|
|
}
|
|
if (unitCount is < 1 or > 128)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(unitCount), unitCount, "Use 1..128 units.");
|
|
}
|
|
if (languageCount is < 1 or > 6)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(languageCount), languageCount, "Use 1..6 languages.");
|
|
}
|
|
|
|
var languages = s_languagePool[..languageCount];
|
|
var catalogs = new LanguageCatalogDescriptor[catalogCount];
|
|
var bundles = new List<TranslationBundleDescriptor>(catalogCount * languageCount);
|
|
for (var catalogIndex = 0; catalogIndex < catalogCount; catalogIndex++)
|
|
{
|
|
var catalogId = $"Perf:AtomUI.Performance.LocalizationCatalog{catalogIndex}";
|
|
var units = Enumerable.Range(0, unitCount)
|
|
.Select(static unitIndex =>
|
|
new LanguageCatalogUnitDescriptor(unitIndex + 1, $"Unit{unitIndex}"))
|
|
.ToArray();
|
|
catalogs[catalogIndex] = CreateCatalog(catalogIndex, catalogId, units);
|
|
|
|
foreach (var language in languages)
|
|
{
|
|
var values = Enumerable.Range(0, unitCount)
|
|
.Select(unitIndex =>
|
|
$"{language.Value}:{catalogIndex}:{unitIndex}")
|
|
.Cast<string?>()
|
|
.ToArray();
|
|
bundles.Add(new TranslationBundleDescriptor(
|
|
catalogId,
|
|
1,
|
|
language,
|
|
TranslationSourceKind.ModuleBuiltIn,
|
|
$"LocalizationPerf.Catalog{catalogIndex}.{language.Value}",
|
|
values));
|
|
}
|
|
}
|
|
|
|
return new LocalizationFixture(catalogs, bundles, languages);
|
|
}
|
|
|
|
private static LocalizationRuntime BuildRuntime(LocalizationFixture fixture)
|
|
{
|
|
var builder = new LocalizationBuilder();
|
|
foreach (var catalog in fixture.Catalogs)
|
|
{
|
|
builder.AddCatalog(catalog);
|
|
}
|
|
foreach (var bundle in fixture.Bundles)
|
|
{
|
|
builder.AddTranslationBundle(bundle);
|
|
}
|
|
builder.ConfigureLanguages(fixture.Languages[0], fixture.Languages);
|
|
return builder.Build(static () => true);
|
|
}
|
|
|
|
private static LocalizationStartupResult MeasureStartupBuild()
|
|
{
|
|
var fixture = CreateFixture(catalogCount: 4, unitCount: 32, languageCount: 4);
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
var before = GC.GetAllocatedBytesForCurrentThread();
|
|
var stopwatch = Stopwatch.StartNew();
|
|
using var runtime = BuildRuntime(fixture);
|
|
stopwatch.Stop();
|
|
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
GC.KeepAlive(runtime);
|
|
return new LocalizationStartupResult(stopwatch.Elapsed, allocated, 4, 32, 4);
|
|
}
|
|
|
|
private static LocalizationHotPathResult MeasureGetHotPath(int operationCount)
|
|
{
|
|
using var runtime = BuildRuntime(catalogCount: 4, unitCount: 32, languageCount: 4);
|
|
ILocalizer localizer = runtime.Localizer;
|
|
_ = localizer.Get(Catalog0ResourceKind.Value);
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
var before = GC.GetAllocatedBytesForCurrentThread();
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var checksum = 0;
|
|
for (var index = 0; index < operationCount; index++)
|
|
{
|
|
checksum += localizer.Get(Catalog0ResourceKind.Value).Length;
|
|
}
|
|
stopwatch.Stop();
|
|
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
if (checksum == 0)
|
|
{
|
|
throw new InvalidOperationException("The localization hot path returned an empty value.");
|
|
}
|
|
return new LocalizationHotPathResult(stopwatch.Elapsed, allocated, operationCount);
|
|
}
|
|
|
|
private static LocalizationSwitchResult MeasureLanguageSwitching(int operationCount)
|
|
{
|
|
using var runtime = BuildRuntime(catalogCount: 4, unitCount: 32, languageCount: 4);
|
|
var host = new Border();
|
|
host.Resources.MergedDictionaries.Add(runtime.ResourceProvider);
|
|
var resourceNotifications = 0;
|
|
var languageEvents = 0;
|
|
((IResourceHost)host).ResourcesChanged += (_, _) => resourceNotifications++;
|
|
runtime.LanguageManager.LanguageChanged += (_, _) => languageEvents++;
|
|
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
var before = GC.GetAllocatedBytesForCurrentThread();
|
|
var stopwatch = Stopwatch.StartNew();
|
|
for (var index = 0; index < operationCount; index++)
|
|
{
|
|
var language = s_languagePool[(index % 3) + 1];
|
|
runtime.LanguageManager.ChangeLanguage(language);
|
|
}
|
|
stopwatch.Stop();
|
|
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
return new LocalizationSwitchResult(
|
|
stopwatch.Elapsed,
|
|
allocated,
|
|
operationCount,
|
|
resourceNotifications,
|
|
languageEvents);
|
|
}
|
|
|
|
private static IReadOnlyList<LocalizationMemoryPoint> MeasureSnapshotMemory()
|
|
{
|
|
var matrix = new (int Catalogs, int Units, int Languages)[]
|
|
{
|
|
(1, 8, 2),
|
|
(4, 8, 2),
|
|
(8, 8, 2),
|
|
(1, 32, 2),
|
|
(1, 64, 2),
|
|
(1, 8, 4),
|
|
(1, 8, 6),
|
|
(4, 32, 4),
|
|
(8, 64, 6)
|
|
};
|
|
var points = new List<LocalizationMemoryPoint>(matrix.Length);
|
|
foreach (var dimensions in matrix)
|
|
{
|
|
points.Add(MeasureSnapshotMemoryPoint(dimensions));
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static LocalizationMemoryPoint MeasureSnapshotMemoryPoint(
|
|
(int Catalogs, int Units, int Languages) dimensions)
|
|
{
|
|
var runtimes = new LocalizationRuntime[MemoryRetentionSampleCount];
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
var before = GC.GetTotalMemory(forceFullCollection: true);
|
|
try
|
|
{
|
|
for (var index = 0; index < runtimes.Length; index++)
|
|
{
|
|
runtimes[index] = BuildRuntime(
|
|
dimensions.Catalogs,
|
|
dimensions.Units,
|
|
dimensions.Languages);
|
|
}
|
|
|
|
var after = GC.GetTotalMemory(forceFullCollection: true);
|
|
var logicalTextBytes = CountSnapshotTextBytes(runtimes[0]);
|
|
var snapshotSlotCount =
|
|
(long)runtimes[0].Snapshots.Count * dimensions.Catalogs * dimensions.Units;
|
|
GC.KeepAlive(runtimes);
|
|
return new LocalizationMemoryPoint(
|
|
dimensions.Catalogs,
|
|
dimensions.Units,
|
|
dimensions.Languages,
|
|
Math.Max(0, (after - before) / MemoryRetentionSampleCount),
|
|
logicalTextBytes,
|
|
snapshotSlotCount);
|
|
}
|
|
finally
|
|
{
|
|
foreach (var runtime in runtimes)
|
|
{
|
|
runtime?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static long CountSnapshotTextBytes(LocalizationRuntime runtime)
|
|
{
|
|
long total = 0;
|
|
for (var snapshotIndex = 0; snapshotIndex < runtime.Snapshots.Count; snapshotIndex++)
|
|
{
|
|
var snapshot = runtime.Snapshots.Values.ElementAt(snapshotIndex);
|
|
for (var catalogSlot = 0; catalogSlot < runtime.Registry.Catalogs.Count; catalogSlot++)
|
|
{
|
|
var catalog = runtime.Registry.Catalogs[catalogSlot];
|
|
for (var unitSlot = 0; unitSlot < catalog.Units.Count; unitSlot++)
|
|
{
|
|
total += snapshot.GetEntry(catalogSlot, unitSlot).Text.Length * sizeof(char);
|
|
}
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
private static LanguageCatalogDescriptor CreateCatalog(
|
|
int catalogIndex,
|
|
string catalogId,
|
|
IReadOnlyList<LanguageCatalogUnitDescriptor> units)
|
|
{
|
|
return catalogIndex switch
|
|
{
|
|
0 => new LanguageCatalogDescriptor<Catalog0ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
1 => new LanguageCatalogDescriptor<Catalog1ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
2 => new LanguageCatalogDescriptor<Catalog2ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
3 => new LanguageCatalogDescriptor<Catalog3ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
4 => new LanguageCatalogDescriptor<Catalog4ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
5 => new LanguageCatalogDescriptor<Catalog5ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
6 => new LanguageCatalogDescriptor<Catalog6ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
7 => new LanguageCatalogDescriptor<Catalog7ResourceKind>(catalogId, 1, units, static _ => 0),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(catalogIndex))
|
|
};
|
|
}
|
|
|
|
private static string RenderMarkdown(
|
|
LocalizationStartupResult startup,
|
|
LocalizationHotPathResult hotPath,
|
|
LocalizationSwitchResult switching,
|
|
IReadOnlyList<LocalizationMemoryPoint> memory)
|
|
{
|
|
var builder = new StringBuilder();
|
|
builder.AppendLine("# Localization Baseline");
|
|
builder.AppendLine();
|
|
builder.AppendLine($"- Date: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss zzz}");
|
|
builder.AppendLine("- Runner: `tools/performances/AtomUI.Performance`");
|
|
builder.AppendLine("- Runtime input: generated Catalog descriptors and compiled translation strings; no XLIFF parsing");
|
|
builder.AppendLine();
|
|
builder.AppendLine("| Metric | Value |");
|
|
builder.AppendLine("| --- | ---: |");
|
|
builder.AppendLine($"| Snapshot startup build (ms) | {startup.Elapsed.TotalMilliseconds:0.00} |");
|
|
builder.AppendLine($"| Snapshot startup allocation (KiB) | {startup.AllocatedBytes / 1024.0:0.0} |");
|
|
builder.AppendLine($"| ILocalizer.Get calls | {hotPath.OperationCount} |");
|
|
builder.AppendLine($"| ILocalizer.Get (ns/call) | {hotPath.Elapsed.TotalNanoseconds() / hotPath.OperationCount:0.0} |");
|
|
builder.AppendLine($"| ILocalizer.Get allocation (KiB) | {hotPath.AllocatedBytes / 1024.0:0.0} |");
|
|
builder.AppendLine($"| Language switch changes | {switching.OperationCount} |");
|
|
builder.AppendLine($"| Language switch (ns/change) | {switching.Elapsed.TotalNanoseconds() / switching.OperationCount:0.0} |");
|
|
builder.AppendLine($"| Language switch allocation (KiB) | {switching.AllocatedBytes / 1024.0:0.0} |");
|
|
builder.AppendLine($"| Resource notifications | {switching.ResourceNotificationCount} |");
|
|
builder.AppendLine($"| Language events | {switching.LanguageEventCount} |");
|
|
builder.AppendLine();
|
|
builder.AppendLine("## Snapshot Memory");
|
|
builder.AppendLine();
|
|
builder.AppendLine("| Catalogs | Units/Catalog | Languages | Retained KiB | Logical text KiB | Snapshot slots |");
|
|
builder.AppendLine("| ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
foreach (var point in memory)
|
|
{
|
|
builder.AppendLine($"| {point.CatalogCount} | {point.UnitCount} | {point.LanguageCount} | " +
|
|
$"{point.RetainedBytes / 1024.0:0.0} | {point.LogicalTextBytes / 1024.0:0.0} | {point.SnapshotSlotCount} |");
|
|
}
|
|
builder.AppendLine();
|
|
builder.AppendLine("Notes:");
|
|
builder.AppendLine();
|
|
builder.AppendLine("- Measurements run in a deterministic in-process runtime with UI-thread checks disabled only for the benchmark harness.");
|
|
builder.AppendLine($"- Retained memory is a coarse process GC measurement averaged across {MemoryRetentionSampleCount} live runtimes; logical text bytes and slot counts show the expected scaling signal.");
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static double TotalNanoseconds(this TimeSpan timeSpan)
|
|
{
|
|
return timeSpan.TotalMilliseconds * 1_000_000;
|
|
}
|
|
|
|
private sealed record LocalizationStartupResult(
|
|
TimeSpan Elapsed,
|
|
long AllocatedBytes,
|
|
int CatalogCount,
|
|
int UnitCount,
|
|
int LanguageCount);
|
|
|
|
private sealed record LocalizationHotPathResult(TimeSpan Elapsed, long AllocatedBytes, int OperationCount);
|
|
|
|
private sealed record LocalizationSwitchResult(
|
|
TimeSpan Elapsed,
|
|
long AllocatedBytes,
|
|
int OperationCount,
|
|
int ResourceNotificationCount,
|
|
int LanguageEventCount);
|
|
|
|
internal sealed record LocalizationMemoryPoint(
|
|
int CatalogCount,
|
|
int UnitCount,
|
|
int LanguageCount,
|
|
long RetainedBytes,
|
|
long LogicalTextBytes,
|
|
long SnapshotSlotCount);
|
|
|
|
private sealed record LocalizationFixture(
|
|
IReadOnlyList<LanguageCatalogDescriptor> Catalogs,
|
|
IReadOnlyList<TranslationBundleDescriptor> Bundles,
|
|
IReadOnlyList<LanguageTag> Languages);
|
|
|
|
internal enum Catalog0ResourceKind { Value }
|
|
private enum Catalog1ResourceKind { Value }
|
|
private enum Catalog2ResourceKind { Value }
|
|
private enum Catalog3ResourceKind { Value }
|
|
private enum Catalog4ResourceKind { Value }
|
|
private enum Catalog5ResourceKind { Value }
|
|
private enum Catalog6ResourceKind { Value }
|
|
private enum Catalog7ResourceKind { Value }
|
|
}
|