Support mapped frame bodies on Unix

This commit is contained in:
Meta Vens 2026-08-18 04:18:46 +02:00
commit 35d31ae908
5 changed files with 60 additions and 11 deletions

View file

@ -175,7 +175,8 @@ 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
telemetry on Windows named maps and Unix file-backed maps. The second includes
10,000 real WebSocket operations on one retained
identity-owned C# session after provider route replacement.
Rust package tests:

View file

@ -3,6 +3,7 @@ using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@ -53,6 +54,7 @@ namespace GameCult.Mesh
private readonly Mutex _mutex;
private readonly TimeSpan _leaseDuration;
private readonly string _token;
private readonly string? _mappingPath;
private int _cursor;
private long _nextSequence;
private ulong _publishedFrames;
@ -83,7 +85,21 @@ namespace GameCult.Mesh
_leaseDuration = leaseDuration ?? TimeSpan.FromSeconds(5);
if (_leaseDuration <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(leaseDuration));
_token = CapabilityPrefix + RandomToken();
_mapping = MemoryMappedFile.CreateNew(MappingName(_token), checked(HeaderSize + CultMeshFrameRegion.SlotCount * slotByteLength));
var mappingByteLength = checked(HeaderSize + CultMeshFrameRegion.SlotCount * slotByteLength);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_mapping = MemoryMappedFile.CreateNew(MappingName(_token), mappingByteLength);
}
else
{
_mappingPath = MappingPath(_token);
_mapping = MemoryMappedFile.CreateFromFile(
_mappingPath,
FileMode.CreateNew,
null,
mappingByteLength,
MemoryMappedFileAccess.ReadWrite);
}
_control = _mapping.CreateViewAccessor(0, HeaderSize, MemoryMappedFileAccess.ReadWrite);
_control.Write(0, slotByteLength);
_control.Write(4, CultMeshFrameRegion.SlotCount);
@ -263,7 +279,7 @@ namespace GameCult.Mesh
Mutex mutex;
try
{
mapping = MemoryMappedFile.OpenExisting(MappingName(token), MemoryMappedFileRights.ReadWrite);
mapping = OpenMapping(token);
mutex = Mutex.OpenExisting(MutexName(token));
}
catch (Exception error) when (error is FileNotFoundException || error is WaitHandleCannotBeOpenedException)
@ -307,7 +323,7 @@ namespace GameCult.Mesh
{
try
{
using var mapping = MemoryMappedFile.OpenExisting(MappingName(token), MemoryMappedFileRights.ReadWrite);
using var mapping = OpenMapping(token);
using var mutex = Mutex.OpenExisting(MutexName(token));
WithMutex(mutex, () =>
{
@ -331,6 +347,12 @@ namespace GameCult.Mesh
_control.Dispose();
_mapping.Dispose();
_mutex.Dispose();
if (_mappingPath != null)
{
try { File.Delete(_mappingPath); }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
}
}
@ -343,6 +365,11 @@ namespace GameCult.Mesh
}
internal static long SlotOffset(int slot, int slotByteLength) => HeaderSize + (long)slot * slotByteLength;
internal static string MappingName(string token) => "cultmesh-map-" + token;
internal static string MappingPath(string token) => Path.Combine(Path.GetTempPath(), MappingName(token) + ".map");
internal static MemoryMappedFile OpenMapping(string token) =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? MemoryMappedFile.OpenExisting(MappingName(token), MemoryMappedFileRights.ReadWrite)
: MemoryMappedFile.CreateFromFile(MappingPath(token), FileMode.Open, null, 0, MemoryMappedFileAccess.ReadWrite);
internal static string MutexName(string token) => "cultmesh-lock-" + token;
internal static void ParseCapability(string capability, out string token, out int slot)
{
@ -393,9 +420,7 @@ namespace GameCult.Mesh
CultMeshFrameBodyPublisher.ParseCapability(descriptor.CapabilityToken, out _token, out _);
try
{
_mapping = MemoryMappedFile.OpenExisting(
CultMeshFrameBodyPublisher.MappingName(_token),
MemoryMappedFileRights.ReadWrite);
_mapping = CultMeshFrameBodyPublisher.OpenMapping(_token);
_mutex = Mutex.OpenExisting(CultMeshFrameBodyPublisher.MutexName(_token));
}
catch (Exception error) when (error is FileNotFoundException || error is WaitHandleCannotBeOpenedException)
@ -503,9 +528,7 @@ namespace GameCult.Mesh
};
try
{
var mapping = MemoryMappedFile.OpenExisting(
CultMeshFrameBodyPublisher.MappingName(_token),
MemoryMappedFileRights.ReadWrite);
var mapping = CultMeshFrameBodyPublisher.OpenMapping(_token);
lease = new CultMeshMappedBodyLease(
descriptor,
mapping,

View file

@ -388,6 +388,11 @@ receives one shared-memory bootstrap descriptor, then observes later commits
through the mapped ring header. Per-frame CultNet documents are neither needed
nor desirable.
The mapped ring uses a named memory map on Windows and a capability-named,
publisher-owned temporary backing file on Unix. This distinction belongs to
the CultMesh adapter; providers and consumers use the same descriptor/cursor
contract on both platforms.
```csharp
using var entities = await CultMesh.SubscribeLiveBodyAsync(
subscriptions,

View file

@ -211,7 +211,10 @@ portable machinery that makes it feel native.
- `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
capability and acquire only newer read leases. Windows uses a named memory
map; Unix uses a private file-backed map because .NET does not support named
maps there. The capability and cursor API is identical, and publisher
disposal revokes new opens and removes the Unix backing file. `Stats()` reports committed
frames, blocked writes, and unavoidable copies. Direct slot writes report
zero copies; the convenience `TryPublish(ReadOnlySpan<byte>)` path reports
one explicit fallback copy instead of laundering it as zero-copy transport.

View file

@ -167,6 +167,23 @@ public sealed class CultMeshFrameBodyTests
reopen.Should().Throw<InvalidDataException>();
}
[Test]
public void UnixPublisherOwnsAndDeletesItsFileBackedMap()
{
if (OperatingSystem.IsWindows())
Assert.Ignore("Windows uses a named memory map rather than a backing file.");
var publisher = Publisher();
publisher.TryPublish(new byte[] { 42 }, DateTimeOffset.UtcNow, out var descriptor).Should().BeTrue();
var token = descriptor.CapabilityToken[..descriptor.CapabilityToken.LastIndexOf('.')];
var path = Path.Combine(Path.GetTempPath(), "cultmesh-map-" + token + ".map");
File.Exists(path).Should().BeTrue();
publisher.Dispose();
File.Exists(path).Should().BeFalse();
}
[Test]
public void PublishesOneAtomicSchemaVersionedGenerationPerCommit()
{