XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text;

namespace XFESystem.Kernel;

public readonly record struct NativePointerTimingCounters(long Observed, long Consumed, long Lost);

/// <summary>Opt-in, UI-owned timing of real completed presents; never logs or allocates in its hot path.</summary>
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;
        }
    }

    /// <summary>Call after Canvas.FramesPresented has really advanced, using that counter and the actual copy bounds.</summary>
    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++;
    }

    /// <summary>Stop first, then allocate/format the bounded dump outside the measurement window.</summary>
    public string Stop(long endTicks, long framesPresented, NativePointerTimingCounters counters)
    {
        var aggregate = new StringBuilder();
        Stop(endTicks, framesPresented, counters, chunk => aggregate.Append(chunk));
        return aggregate.ToString();
    }

    /// <summary>Native callers use this overload: each emitted UTF-16 chunk is at most 32 KiB.</summary>
    public void Stop(long endTicks, long framesPresented, NativePointerTimingCounters counters, Action<string> 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);
}