diff --git a/README.md b/README.md
index 68ee88d6..2d787739 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,18 @@ npm test --workspace packages/cultmesh-ts
npm test --workspace packages/cultmesh-browser
```
+Measured reactive, retained-session, and shared-memory hot-body gates:
+
+```powershell
+pwsh -File ./scripts/measure-reactive-document-performance.ps1 -Quick
+pwsh -File ./scripts/verify-eve-getting-started.ps1
+```
+
+The first command measures the same 16 KiB document workload in C#,
+TypeScript, and Python and includes a mapped hot-body probe with explicit copy
+telemetry. The second includes 10,000 real WebSocket operations on one retained
+identity-owned C# session after provider route replacement.
+
Rust package tests:
```powershell
diff --git a/src/GameCult.Mesh/CultMeshFrameBodies.cs b/src/GameCult.Mesh/CultMeshFrameBodies.cs
index 5c4d26d0..9f8464fd 100644
--- a/src/GameCult.Mesh/CultMeshFrameBodies.cs
+++ b/src/GameCult.Mesh/CultMeshFrameBodies.cs
@@ -12,6 +12,27 @@ using Microsoft.Win32.SafeHandles;
namespace GameCult.Mesh
{
+ /// Reports bounded shared-memory frame publication and explicit copy telemetry.
+ public sealed class CultMeshFrameBodyPublisherStats
+ {
+ internal CultMeshFrameBodyPublisherStats(
+ ulong publishedFrames,
+ ulong blockedWrites,
+ ulong unavoidableCopyCount)
+ {
+ PublishedFrames = publishedFrames;
+ BlockedWrites = blockedWrites;
+ UnavoidableCopyCount = unavoidableCopyCount;
+ }
+
+ /// Gets the number of atomically committed generations.
+ public ulong PublishedFrames { get; }
+ /// Gets the number of write reservations refused by bounded slots.
+ public ulong BlockedWrites { get; }
+ /// Gets copies explicitly declared by copy-fallback publication.
+ public ulong UnavoidableCopyCount { get; }
+ }
+
// Owner: this publisher owns mapping lifetime, logical generation, and slot commit.
// Consumers own only read leases; mappings, capabilities, and slot selection remain CultMesh state.
public sealed class CultMeshFrameBodyPublisher : IDisposable
@@ -34,6 +55,9 @@ namespace GameCult.Mesh
private readonly string _token;
private int _cursor;
private long _nextSequence;
+ private ulong _publishedFrames;
+ private ulong _blockedWrites;
+ private ulong _unavoidableCopyCount;
private CultMeshFrameBodyWriteLease? _activeWrite;
private bool _disposed;
@@ -88,7 +112,7 @@ namespace GameCult.Mesh
using (write)
{
body.CopyTo(write.Span);
- descriptor = write.Commit(body.Length, nowUtc);
+ descriptor = write.Commit(body.Length, nowUtc, unavoidableCopyCount: 1);
return true;
}
}
@@ -103,6 +127,7 @@ namespace GameCult.Mesh
ThrowIfDisposed();
if (_activeWrite != null)
{
+ _blockedWrites++;
lease = null!;
return false;
}
@@ -124,6 +149,7 @@ namespace GameCult.Mesh
});
if (slot < 0)
{
+ _blockedWrites++;
lease = null!;
return false;
}
@@ -149,7 +175,8 @@ namespace GameCult.Mesh
internal CultMeshBodyDescriptor Commit(
CultMeshFrameBodyWriteLease lease,
int byteLength,
- DateTimeOffset nowUtc)
+ DateTimeOffset nowUtc,
+ int unavoidableCopyCount)
{
lock (_gate)
{
@@ -158,6 +185,8 @@ namespace GameCult.Mesh
throw new InvalidOperationException("The CultMesh frame write lease is stale or is not owned by this publisher.");
if (byteLength < 0 || byteLength > SlotByteLength)
throw new ArgumentOutOfRangeException(nameof(byteLength));
+ if (unavoidableCopyCount < 0)
+ throw new ArgumentOutOfRangeException(nameof(unavoidableCopyCount));
var semanticHash = CultMeshBodyDescriptorValidator.ComputeSemanticHash(lease.Span[..byteLength]);
WithMutex(() =>
{
@@ -174,6 +203,8 @@ namespace GameCult.Mesh
});
var descriptor = Descriptor(lease.SlotIndex, byteLength, lease.Sequence, semanticHash, nowUtc);
_nextSequence++;
+ _publishedFrames++;
+ _unavoidableCopyCount += (ulong)Math.Max(0, unavoidableCopyCount);
_activeWrite = null;
lease.Complete();
return descriptor;
@@ -194,6 +225,19 @@ namespace GameCult.Mesh
}
}
+ /// Gets current publication, backpressure, and explicit fallback-copy counters.
+ public CultMeshFrameBodyPublisherStats Stats()
+ {
+ lock (_gate)
+ {
+ ThrowIfDisposed();
+ return new CultMeshFrameBodyPublisherStats(
+ _publishedFrames,
+ _blockedWrites,
+ _unavoidableCopyCount);
+ }
+ }
+
private CultMeshBodyDescriptor Descriptor(
int slot,
int byteLength,
@@ -514,9 +558,12 @@ namespace GameCult.Mesh
public Memory Memory => _memory.Memory;
public Span Span => Memory.Span;
- public CultMeshBodyDescriptor Commit(int byteLength, DateTimeOffset nowUtc) =>
+ public CultMeshBodyDescriptor Commit(
+ int byteLength,
+ DateTimeOffset nowUtc,
+ int unavoidableCopyCount = 0) =>
(_owner ?? throw new ObjectDisposedException(nameof(CultMeshFrameBodyWriteLease)))
- .Commit(this, byteLength, nowUtc);
+ .Commit(this, byteLength, nowUtc, unavoidableCopyCount);
internal void Complete()
{
diff --git a/src/GameCult.Mesh/docs/public-api.md b/src/GameCult.Mesh/docs/public-api.md
index 174ff84a..e5706595 100644
--- a/src/GameCult.Mesh/docs/public-api.md
+++ b/src/GameCult.Mesh/docs/public-api.md
@@ -208,6 +208,13 @@ portable machinery that makes it feel native.
slices, or browser typed arrays. `CultMesh.DescribeNativeSliceView(...)`
exposes the full view id, schema id, row count, columns, route, native handle,
and dense stride for tools and adapters.
+- `CultMeshFrameBodyPublisher` and `CultMeshMappedFrameBodyCursor` carry
+ fixed-layout hot bodies through a triple-buffered memory mapping. Producers
+ reserve a writable slot and commit in place; consumers retain one bootstrap
+ capability and acquire only newer read leases. `Stats()` reports committed
+ frames, blocked writes, and unavoidable copies. Direct slot writes report
+ zero copies; the convenience `TryPublish(ReadOnlySpan)` path reports
+ one explicit fallback copy instead of laundering it as zero-copy transport.
- `CultMeshRouteHint`, `CultMeshRouteRecord`, and `CultMeshLocalityKind` name
locality choices without forcing application code to choose transport-specific
APIs. `CultMesh.RouteRecord(...)` is the shared way to flatten and rehydrate
diff --git a/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs b/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs
index 2d69015d..ede0e3c4 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs
@@ -30,6 +30,22 @@ public sealed class CultMeshFrameBodyTests
lease.ReadByte(0).Should().Be(1);
}
+ [Test]
+ public void PublisherTelemetryDistinguishesDirectWritesFromCopyFallback()
+ {
+ using var publisher = Publisher();
+ publisher.TryPublish(new byte[] { 1, 2, 3 }, DateTimeOffset.UtcNow, out _).Should().BeTrue();
+
+ publisher.TryAcquireWrite(out var direct).Should().BeTrue();
+ direct.Span[..3].Fill(7);
+ direct.Commit(3, DateTimeOffset.UtcNow);
+
+ var stats = publisher.Stats();
+ stats.PublishedFrames.Should().Be(2);
+ stats.BlockedWrites.Should().Be(0);
+ stats.UnavoidableCopyCount.Should().Be(1);
+ }
+
[Test]
public void MappedCursorObservesNewGenerationsWithoutNewControlDescriptors()
{
diff --git a/tools/GameCult.Mesh.PerformanceProbe/Program.cs b/tools/GameCult.Mesh.PerformanceProbe/Program.cs
index 948d8e90..0a367d36 100644
--- a/tools/GameCult.Mesh.PerformanceProbe/Program.cs
+++ b/tools/GameCult.Mesh.PerformanceProbe/Program.cs
@@ -14,6 +14,7 @@ var results = new List();
foreach (var documentCount in documentCounts)
results.Add(await MeasureAsync(documentCount, idleDuration, activeDuration));
+var hotBody = await MeasureHotBodyAsync(activeDuration);
Console.WriteLine(JsonSerializer.Serialize(new
{
@@ -26,17 +27,98 @@ Console.WriteLine(JsonSerializer.Serialize(new
updateRateHz = 60,
changedFraction = 0.01
},
- results
+ results,
+ hotBody
}, new JsonSerializerOptions { WriteIndented = true }));
return results.All(result =>
result.ActualPublishes == result.ExpectedPublishes &&
result.PayloadBytesPublished > 0 &&
result.AllocationToPayloadRatio < 10 &&
- result.P99PublishLatencyMilliseconds < 250)
+ result.P99PublishLatencyMilliseconds < 250) &&
+ hotBody.ActualFrames == hotBody.ExpectedFrames &&
+ hotBody.UnavoidableCopyCount == 0 &&
+ hotBody.BlockedWrites == 0 &&
+ hotBody.P99RoundTripMilliseconds < 250
? 0
: 1;
+static async Task MeasureHotBodyAsync(TimeSpan activeDuration)
+{
+ const int payloadBytes = 16 * 1024;
+ using var publisher = new CultMeshFrameBodyPublisher(
+ "performance:hot-body",
+ "gamecult.mesh.performance_hot_body.v1",
+ layoutVersion: 1,
+ capacity: payloadBytes,
+ producerEpoch: 1,
+ slotByteLength: payloadBytes,
+ leaseDuration: TimeSpan.FromMinutes(1));
+ if (!publisher.TryAcquireWrite(out var bootstrapWrite))
+ throw new InvalidOperationException("Hot-body probe could not reserve its bootstrap slot.");
+ CultMeshBodyDescriptor bootstrap;
+ using (bootstrapWrite)
+ {
+ bootstrapWrite.Span[..payloadBytes].Fill(1);
+ bootstrap = bootstrapWrite.Commit(payloadBytes, DateTimeOffset.UtcNow);
+ }
+ using var cursor = new CultMeshMappedFrameBodyCursor(bootstrap);
+ if (!cursor.TryAcquireLatest(out var bootstrapRead))
+ throw new InvalidOperationException("Hot-body probe could not acquire its bootstrap generation.");
+ bootstrapRead.Dispose();
+
+ ForceCollection();
+ using var process = Process.GetCurrentProcess();
+ process.Refresh();
+ var allocatedStart = GC.GetTotalAllocatedBytes(precise: true);
+ var privateBytesStart = process.PrivateMemorySize64;
+ var cpuStart = process.TotalProcessorTime;
+ var clock = Stopwatch.StartNew();
+ var frameInterval = TimeSpan.FromSeconds(1.0 / 60.0);
+ var latencies = new List();
+ var frames = 0;
+ while (clock.Elapsed < activeDuration)
+ {
+ var started = Stopwatch.GetTimestamp();
+ if (!publisher.TryAcquireWrite(out var write))
+ throw new InvalidOperationException("Hot-body probe encountered unexpected write backpressure.");
+ var marker = (byte)(frames % 251 + 1);
+ using (write)
+ {
+ write.Span[..payloadBytes].Fill(marker);
+ write.Commit(payloadBytes, DateTimeOffset.UtcNow);
+ }
+ if (!cursor.TryAcquireLatest(out var read))
+ throw new InvalidOperationException("Hot-body cursor missed a committed generation.");
+ using (read)
+ {
+ if (read.Descriptor.ByteSize != payloadBytes || read.ReadByte(0) != marker || read.ReadByte(payloadBytes - 1) != marker)
+ throw new InvalidOperationException("Hot-body cursor observed torn or mismatched bytes.");
+ }
+ latencies.Add(Stopwatch.GetElapsedTime(started).TotalMilliseconds);
+ frames++;
+ var remaining = frameInterval * frames - clock.Elapsed;
+ if (remaining > TimeSpan.Zero) await Task.Delay(remaining);
+ }
+
+ process.Refresh();
+ var stats = publisher.Stats();
+ var ordered = latencies.OrderBy(value => value).ToArray();
+ return new HotBodyProbeResult(
+ ExpectedFrames: frames + 1,
+ ActualFrames: checked((int)stats.PublishedFrames),
+ PayloadBytes: payloadBytes,
+ P50RoundTripMilliseconds: Percentile(ordered, 0.50),
+ P95RoundTripMilliseconds: Percentile(ordered, 0.95),
+ P99RoundTripMilliseconds: Percentile(ordered, 0.99),
+ AllocatedBytes: GC.GetTotalAllocatedBytes(precise: true) - allocatedStart,
+ PrivateBytesBefore: privateBytesStart,
+ PrivateBytesAfter: process.PrivateMemorySize64,
+ CpuMilliseconds: (process.TotalProcessorTime - cpuStart).TotalMilliseconds,
+ BlockedWrites: stats.BlockedWrites,
+ UnavoidableCopyCount: stats.UnavoidableCopyCount);
+}
+
static async Task MeasureAsync(
int documentCount,
TimeSpan idleDuration,
@@ -201,6 +283,20 @@ internal sealed record ProbeResult(
double P99PublishLatencyMilliseconds,
double ScenarioWallSeconds);
+internal sealed record HotBodyProbeResult(
+ int ExpectedFrames,
+ int ActualFrames,
+ int PayloadBytes,
+ double P50RoundTripMilliseconds,
+ double P95RoundTripMilliseconds,
+ double P99RoundTripMilliseconds,
+ long AllocatedBytes,
+ long PrivateBytesBefore,
+ long PrivateBytesAfter,
+ double CpuMilliseconds,
+ ulong BlockedWrites,
+ ulong UnavoidableCopyCount);
+
[CultDocument("gamecult.mesh.performance_probe", "gamecult.mesh.performance_probe.v1")]
[MessagePackObject]
public sealed class PerformanceDocument