using System.Globalization;
using System.Text;
namespace XFESystem.Kernel;
public readonly record struct NativePointerTimingCounters(long Observed, long Consumed, long Lost);
/// Opt-in, UI-owned timing of real completed presents; never logs or allocates in its hot path.
public sealed class NativePointerFrameProbe
{
public const int Capacity = 4096;
private const int FramesPerChunk = 256;
private readonly Frame[][] frames = PrepareChunks();
private int count;
private long frequency, startTicks, framesBefore, lastFrameSequence, droppedFrames, overflow, invalidInputs, duplicateCalls;
private long latestInputSequence, latestInputTicks, latestDispatchTicks, firstInputTicks, lastInputTicks;
private int latestX, latestY;
private NativePointerTimingCounters countersBefore;
public bool IsCapturing { get; private set; }
public void Start(long frequency, long startTicks, long framesPresented, NativePointerTimingCounters counters)
{
if (IsCapturing) throw new InvalidOperationException("A native pointer timing sample is already active.");
if (frequency <= 0 || startTicks < 0 || framesPresented < 0 || counters.Observed < 0 || counters.Consumed < 0 || counters.Lost < 0 || counters.Consumed > counters.Observed)
throw new ArgumentOutOfRangeException(nameof(counters));
this.frequency = frequency; this.startTicks = startTicks; framesBefore = lastFrameSequence = framesPresented;
countersBefore = counters;
count = 0; droppedFrames = overflow = invalidInputs = duplicateCalls = 0;
latestInputSequence = latestInputTicks = latestDispatchTicks = firstInputTicks = lastInputTicks = 0;
latestX = latestY = 0;
IsCapturing = true;
}
public void ObserveConsumed(long sequence, long reportTicks, long dispatchTicks, int x, int y)
{
if (!IsCapturing) return;
if (sequence <= 0 || reportTicks < 0 || dispatchTicks < reportTicks || x < 0 || y < 0 || sequence < latestInputSequence)
{ invalidInputs++; return; }
latestInputSequence = sequence; latestInputTicks = reportTicks; latestDispatchTicks = dispatchTicks; latestX = x; latestY = y;
if (sequence > countersBefore.Observed)
{
if (reportTicks < startTicks) { invalidInputs++; return; }
if (firstInputTicks == 0) firstInputTicks = reportTicks;
lastInputTicks = reportTicks;
}
}
/// Call after Canvas.FramesPresented has really advanced, using that counter and the actual copy bounds.
public void CapturePresent(long framesPresented, long beginTicks, long endTicks)
{
if (!IsCapturing) return;
// An aborted/yielded render or no-op Present must not fabricate a frame.
if (framesPresented == lastFrameSequence) { duplicateCalls++; return; }
if (framesPresented < lastFrameSequence || beginTicks < startTicks || endTicks < beginTicks ||
count > 0 && beginTicks < FrameAt(count - 1).EndTicks)
{ droppedFrames++; return; }
droppedFrames += framesPresented - lastFrameSequence - 1;
lastFrameSequence = framesPresented;
if (count == Capacity) { droppedFrames++; overflow++; return; }
frames[count / FramesPerChunk][count % FramesPerChunk] = new(framesPresented, beginTicks, endTicks, latestInputSequence, latestInputTicks, latestDispatchTicks, latestX, latestY);
count++;
}
/// Stop first, then allocate/format the bounded dump outside the measurement window.
public string Stop(long endTicks, long framesPresented, NativePointerTimingCounters counters)
{
var aggregate = new StringBuilder();
Stop(endTicks, framesPresented, counters, chunk => aggregate.Append(chunk));
return aggregate.ToString();
}
/// Native callers use this overload: each emitted UTF-16 chunk is at most 32 KiB.
public void Stop(long endTicks, long framesPresented, NativePointerTimingCounters counters, Action emit)
{
if (!IsCapturing) throw new InvalidOperationException("No native pointer timing sample is active.");
ArgumentNullException.ThrowIfNull(emit);
IsCapturing = false;
const int maximumChunkCharacters = 16_000;
var output = new StringBuilder(maximumChunkCharacters);
output.Append("XFE_POINTER_TIMING_BEGIN|1\nMETA");
Add(output, "Frequency", frequency); Add(output, "Capacity", Capacity);
Add(output, "StartTicks", startTicks); Add(output, "EndTicks", endTicks);
Add(output, "FramesBefore", framesBefore); Add(output, "FramesAfter", framesPresented);
Add(output, "FramesPresentedDelta", framesPresented - framesBefore); Add(output, "CapturedFrames", count); Add(output, "Overflow", overflow);
Add(output, "InputsBefore", countersBefore.Observed); Add(output, "InputsAfter", counters.Observed);
Add(output, "ObservedBefore", countersBefore.Observed); Add(output, "ObservedAfter", counters.Observed);
Add(output, "ConsumedBefore", countersBefore.Consumed); Add(output, "ConsumedAfter", counters.Consumed);
Add(output, "FirstInputTicks", firstInputTicks); Add(output, "LastInputTicks", lastInputTicks);
Add(output, "LostBefore", countersBefore.Lost); Add(output, "LostAfter", counters.Lost);
Add(output, "DroppedFrames", droppedFrames); Add(output, "InvalidInputReports", invalidInputs);
Add(output, "IgnoredDuplicatePresentCalls", duplicateCalls); output.Append('\n');
for (int index = 0; index < count; index++)
{
Frame frame = FrameAt(index); var row = new StringBuilder(320); row.Append("FRAME");
Add(row, "Seq", frame.Sequence); Add(row, "BeginTicks", frame.BeginTicks); Add(row, "EndTicks", frame.EndTicks);
Add(row, "InputSeq", frame.InputSequence); Add(row, "InputTicks", frame.InputTicks); Add(row, "DispatchTicks", frame.DispatchTicks);
Add(row, "X", frame.X); Add(row, "Y", frame.Y); row.Append('\n');
if (output.Length + row.Length > maximumChunkCharacters) { emit(output.ToString()); output.Clear(); }
output.Append(row);
}
if (output.Length + 23 > maximumChunkCharacters) { emit(output.ToString()); output.Clear(); }
output.Append("XFE_POINTER_TIMING_END\n");
emit(output.ToString());
}
private Frame FrameAt(int index) => frames[index / FramesPerChunk][index % FramesPerChunk];
private static Frame[][] PrepareChunks()
{
var chunks = new Frame[Capacity / FramesPerChunk][];
for (int index = 0; index < chunks.Length; index++) chunks[index] = new Frame[FramesPerChunk];
return chunks;
}
private static void Add(StringBuilder output, string name, long value) =>
output.Append('|').Append(name).Append('|').Append(value.ToString(CultureInfo.InvariantCulture));
private readonly record struct Frame(long Sequence, long BeginTicks, long EndTicks, long InputSequence, long InputTicks,
long DispatchTicks, int X, int Y);
}