Support mapped frame bodies on Unix
This commit is contained in:
parent
f65352fa73
commit
35d31ae908
5 changed files with 60 additions and 11 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue