CultLib/packages/cultnet-py/tools/GameCult.Caching.Benchmark/Program.cs
Meta Vens 95493862bd Split the Python runtime into cultcache-py, cultnet-py, cultmesh-py
One distribution named cultcache-py carried three libraries. Rust and
TypeScript layer them as cultcache < cultnet < cultmesh; Python now does
the same, one package per layer, module names unchanged so every
`from cultmesh_py import ...` keeps working.

Two imports ran against that layering and are resolved by moving code to
where the dependency direction says it lives, not by adding a shim:

- cultnet_py.interop_peer imported the Verse/peer catalog, authority
  lease, stream, and simulation-fact contracts from cultmesh_py. They are
  wire contracts a bare peer has to speak without owning a node, which is
  why cultnet-rs and cultnet-ts keep them in the net layer. They now live
  in cultnet_py.cultmesh_contracts; cultmesh_py re-exports them.
- cultcache_py.benchmark imported cultnet_py. It measures framed CultNet
  parse beside cache paths, so it and compare_csharp move to cultnet_py.
  verify checks all three packages and moves to cultmesh_py.

The single 6,693-line test module is split by what each test touches: a
test using any cultmesh_py name lands in cultmesh-py, else cultnet_py in
cultnet-py, else cultcache-py. Each suite runs with only its layer and the
layers below on the path, so a test that reaches up fails to import
rather than quietly binding the packages back together. 11 / 53 / 78 =
the same 142.

Workflows install all three; the publish job is one matrix over the
three packages keyed by tag prefix. The TS test harnesses put all three
src directories on PYTHONPATH. cultcache-py bumps to 0.2.0 because it no
longer contains cultnet_py or cultmesh_py; the new packages start at
0.1.0. Nothing was ever published, so no PyPI consumer moves.

  cultcache-py  11 tests, path: cultcache-py/src
  cultnet-py    53 tests, path: + cultnet-py/src
  cultmesh-py   78 tests, path: + cultmesh-py/src
  python -m cultmesh_py.verify -> ok, all five checks
  cultnet_py.interop_peer imports with cultmesh_py absent

Not run here: the cultnet-ts interop ring and cultcache-ts store matrix
need dotnet/kotlin peers; their PYTHONPATH change is four lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:10:47 +02:00

104 lines
2.9 KiB
C#

using System.Diagnostics;
using System.Text.Json;
using GameCult.Caching;
var records = 5000;
var emitJson = false;
for (var index = 0; index < args.Length; index++)
{
if (args[index] == "--records" && index + 1 < args.Length)
{
records = int.Parse(args[++index]);
}
else if (args[index] == "--json")
{
emitJson = true;
}
}
if (records <= 0)
{
throw new ArgumentOutOfRangeException(nameof(records), "--records must be greater than zero.");
}
var result = Benchmark.Run(records);
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
if (emitJson)
{
Console.WriteLine(JsonSerializer.Serialize(result, options));
}
else
{
Console.WriteLine($"records: {result.Records}");
foreach (var metric in result.Metrics)
{
Console.WriteLine($"{metric.Name}: {metric.OpsPerSecond:F0} ops/s ({metric.ElapsedMs:F2} ms)");
}
}
internal static class Benchmark
{
public static BenchmarkResult Run(int records)
{
_ = CultDocumentRegistry.Shared.GetRequired<BenchItem>();
var values = Enumerable.Range(0, records)
.Select(index => new BenchItem
{
Name = $"item-{index}",
Category = $"cat-{index % 8}",
Value = index
})
.ToArray();
var cache = new CultCache();
var keys = Enumerable.Range(0, records)
.Select(index => new CultRecordKey($"item:{index}"))
.ToArray();
var upsertMetric = Measure("cache_upsert", records, () =>
{
for (var index = 0; index < records; index++)
{
cache.UpsertAsync(values[index], new CultRecordHandle<BenchItem>(keys[index]))
.GetAwaiter()
.GetResult();
}
});
var getMetric = Measure("cache_get", records, () =>
{
for (var index = 0; index < records; index++)
{
_ = cache.Get<BenchItem>(keys[index]);
}
});
return new BenchmarkResult("csharp", records, new[] { upsertMetric, getMetric });
}
private static BenchmarkMetric Measure(string name, int operations, Action action)
{
var started = Stopwatch.StartNew();
action();
started.Stop();
return new BenchmarkMetric(
name,
operations,
started.Elapsed.TotalMilliseconds,
operations / started.Elapsed.TotalSeconds);
}
}
internal sealed record BenchmarkResult(string Runtime, int Records, IReadOnlyList<BenchmarkMetric> Metrics);
internal sealed record BenchmarkMetric(string Name, int Operations, double ElapsedMs, double OpsPerSecond);
[CultDocument("bench.item", "bench.item.v1")]
internal sealed class BenchItem
{
[CultName]
public string Name { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public int Value { get; set; }
}