Clarify Well source health and process clocks

This commit is contained in:
Meta Vens 2026-06-03 04:41:01 +01:00
commit 2fa008aee4
7 changed files with 25 additions and 41 deletions

View file

@ -108,8 +108,6 @@ public sealed class MimirAsioStreamSource : IMimirMultiplexedStreamSource
started.Set();
var sampleBuffer = new float[Math.Max(1, maxFrames)];
long firstDeviceTimestampNs = 0;
long firstCanonicalEndNs = 0;
while (!cancellation.IsCancellationRequested)
{
if (!Native.Read(
@ -134,16 +132,7 @@ public sealed class MimirAsioStreamSource : IMimirMultiplexedStreamSource
var bytes = new byte[frameCount * sizeof(float)];
var arrivalNs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
var durationNs = checked((long)Math.Ceiling(frameCount * 1_000_000_000.0 / Math.Max(1, sampleRate)));
if (timestampNs > 0 && firstDeviceTimestampNs == 0)
{
firstDeviceTimestampNs = timestampNs;
firstCanonicalEndNs = arrivalNs;
}
var endNs = timestampNs > 0 && firstDeviceTimestampNs > 0
? checked(firstCanonicalEndNs + (timestampNs - firstDeviceTimestampNs))
: arrivalNs;
var startNs = Math.Max(0, endNs - durationNs);
var startNs = Math.Max(0, arrivalNs - durationNs);
MemoryMarshal.AsBytes(sampleBuffer.AsSpan(0, frameCount)).CopyTo(bytes);
samples.Enqueue(new MimirStreamSample(
sourceId,

View file

@ -96,7 +96,7 @@ public sealed class MimirFfmpegPcmAudioStreamSource : IMimirStreamSource
offset += read;
}
var timestampNs = StopwatchTicksToNs(Stopwatch.GetTimestamp());
var timestampNs = UtcNowNs();
samples.Enqueue(new MimirStreamSample(
Descriptor.SourceId,
Descriptor.Kind,
@ -137,10 +137,7 @@ public sealed class MimirFfmpegPcmAudioStreamSource : IMimirStreamSource
_ => 0,
};
private static long StopwatchTicksToNs(long ticks)
{
return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)));
}
private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
public void Dispose()
{

View file

@ -98,7 +98,7 @@ public sealed class MimirFfmpegRawVideoStreamSource : IMimirStreamSource
offset += read;
}
var timestampNs = StopwatchTicksToNs(Stopwatch.GetTimestamp());
var timestampNs = UtcNowNs();
var videoFrame = new MimirVideoFrameDescriptor(
Options.Width,
Options.Height,
@ -152,10 +152,7 @@ public sealed class MimirFfmpegRawVideoStreamSource : IMimirStreamSource
};
}
private static long StopwatchTicksToNs(long ticks)
{
return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)));
}
private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
public void Dispose()
{

View file

@ -118,8 +118,8 @@ public sealed class MimirFrameEventProcessStreamSource : IMimirStreamSource
var timestampNs = frameEvent.TimestampNs > 0
? frameEvent.TimestampNs
: StopwatchTicksToNs(Stopwatch.GetTimestamp());
var arrivalNs = StopwatchTicksToNs(Stopwatch.GetTimestamp());
: UtcNowNs();
var arrivalNs = UtcNowNs();
var byteLength = Math.Max(0, frameEvent.ByteLength);
var payloadHandle = frameEvent.NativeHandle;
var sampleKind = frameEvent.IsAudioBlock ? MimirStreamKind.Audio : Descriptor.Kind;
@ -271,10 +271,7 @@ public sealed class MimirFrameEventProcessStreamSource : IMimirStreamSource
};
}
private static long StopwatchTicksToNs(long ticks)
{
return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)));
}
private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
public void Dispose()
{

View file

@ -74,8 +74,7 @@ public sealed class MimirProcessStreamSource : IMimirStreamSource
Array.Resize(ref buffer, read);
}
var now = Stopwatch.GetTimestamp();
var timestampNs = StopwatchTicksToNs(now);
var timestampNs = UtcNowNs();
samples.Enqueue(new MimirStreamSample(
Descriptor.SourceId,
Descriptor.Kind,
@ -102,10 +101,7 @@ public sealed class MimirProcessStreamSource : IMimirStreamSource
}
}
private static long StopwatchTicksToNs(long ticks)
{
return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)));
}
private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
public void Dispose()
{

View file

@ -1558,11 +1558,18 @@ internal static class MimirWellSnapshot
TryReadAnonymousString(error, "SourceId", "sourceId") is { } id &&
string.Equals(id, descriptor.SourceId, StringComparison.Ordinal));
var elapsedMs = Math.Max(0.0, (DateTimeOffset.UtcNow - startedAt).TotalMilliseconds);
var latestAgeMs = latest.HasValue
? Math.Round(Math.Max(0.0, (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000.0 - latest.Value.TimestampNs) / 1_000_000.0), 1)
: Math.Round(elapsedMs, 1);
var status = errored
? "create-error"
: samples > 0
? "live"
: "created-no-samples";
: samples > 0 && latestAgeMs > 2_000.0
? "stalled"
: samples > 0
? "live"
: descriptor.Origin == MimirStreamOrigin.Network
? "waiting-network"
: "waiting-device-samples";
return new
{
@ -1574,15 +1581,15 @@ internal static class MimirWellSnapshot
reason = status switch
{
"create-error" => "source creation failed; see sourceErrors",
"created-no-samples" => "source was created but has not emitted a sample",
"stalled" => "source emitted samples earlier but latest sample is stale",
"waiting-network" => "network source listener is configured but no producer samples have arrived",
"waiting-device-samples" => "local device source is configured but has not emitted a sample",
_ => "samples arriving",
},
buffers = matchingBuffers.Length,
samples,
latestTimestampNs = latest?.TimestampNs ?? 0L,
latestAgeMs = latest.HasValue
? Math.Round(Math.Max(0.0, (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000.0 - latest.Value.TimestampNs) / 1_000_000.0), 1)
: Math.Round(elapsedMs, 1),
latestAgeMs,
latestByteLength = latest?.ByteLength ?? 0,
};
}

View file

@ -367,3 +367,4 @@
{"ts": "2026-06-02T19:17:54+01:00", "type": "decision", "status": "ok", "note": "Online Verse shakedown: canonical timestamps are owned by MimirCanonicalClockMapper, which now rebases streams whose projected source clock drifts more than 10s from arrival after ASIO/device resets; MimirSynchronizationHub no longer reapplies raw clock-domain corrections to already-canonical buffers. Fensalir Eve surface now mirrors sibling daemon PID liveness so Odin can lower relay, recorder, Well, Move sync, and Fensalir statuses without taking control authority."}
{"ts": "2026-06-02T23:49:45+01:00", "type": "decision", "status": "ok", "note": "Mimir Well/Fensalir live repair: rolling-buffer authority now uses canonical arrival UTC timestamps for ASIO/native camera samples while preserving device timestamps as calibration evidence; Fensalir CultCache persistence is best-effort so a Windows File.Replace sharing failure cannot kill the live Eve daemon."}
{"ts": "2026-06-03T04:13:02+01:00", "type": "implementation", "status": "ok", "note": "2026-06-03: ASIO readiness repair: Mimir now derives ASIO block times from native device timestamps, preserves local-epoch timestamps in the canonical mapper, sorts planner samples chronologically, keeps frames visible when overlap pinches, and lets the latency controller chase FutureSampleOnly slices downward. Live Well reached repeated 6/6 Ready frames at roughly 75-80 ms presentation delay with ASIO distances reported around 32-36 ms."}
{"ts": "2026-06-03T04:40:48+01:00", "type": "implementation", "status": "ok", "note": "2026-06-03: Source health and process-clock repair: FFmpeg/process/frame-event sources now use UTC arrival timestamps instead of Stopwatch-domain timestamps; Well sourceHealth distinguishes live, stalled, waiting-network, and waiting-device-samples. Live verification after restart reached repeated 6/6 Ready frames, while streamPressure exposed the next bottleneck: inline WebSocket body lane published ~66 MB across 53 Well snapshots at ~82 ms average publish cost."}