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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop;

namespace XFESystem.Browser;

/// <summary>A single-threaded, explicitly pumped browser task queue; never sleeps or starts host threads.</summary>
internal sealed class BrowserEventLoop
{
    private sealed record Task(int Id, JsValue Callback, JsValue[] Arguments, long Due, long Interval, bool Animation);
    private readonly Engine engine;
    private readonly BrowserLimits limits;
    private readonly Dictionary<int, Task> tasks = [];
    private readonly Func<JsValue, JsValue[], JsValue> isCallable;
    private int nextId;
    private int pendingMicrotasks;

    public BrowserEventLoop(Engine engine, BrowserLimits limits)
    {
        this.engine = engine;
        this.limits = limits;
        isCallable = (Func<JsValue, JsValue[], JsValue>)engine.Evaluate("(function(value){return typeof value==='function';})").ToObject()!;
        Install("setTimeout", args => Schedule(args, false, false));
        Install("setInterval", args => Schedule(args, true, false));
        Install("requestAnimationFrame", args => Schedule(args, false, true));
        foreach (var name in new[] { "clearTimeout", "clearInterval", "cancelAnimationFrame" })
            Install(name, args => { tasks.Remove(Integer(Value(args, 0))); return JsValue.Undefined; });
        Install("__xfeMicrotaskEnter", _ =>
        {
            if (++pendingMicrotasks > limits.MaximumPendingTasks)
            {
                --pendingMicrotasks;
                throw new BrowserLimitException("The browser microtask queue is full.");
            }
            return JsValue.Undefined;
        });
        Install("__xfeMicrotaskLeave", _ => { if (pendingMicrotasks > 0) --pendingMicrotasks; return JsValue.Undefined; });
        _ = new BrowserPerformance(engine, limits, () => Now);
        engine.Execute("""
            (function(enter,leave){ globalThis.queueMicrotask = function(callback) {
              if (typeof callback !== 'function') throw new TypeError('queueMicrotask requires a function');
              enter();
              Promise.resolve().then(function() { try { callback(); } finally { leave(); } });
            };
            delete globalThis.__xfeMicrotaskEnter;delete globalThis.__xfeMicrotaskLeave;})(__xfeMicrotaskEnter,__xfeMicrotaskLeave);
            """);
    }

    public long Now { get; private set; }
    public int PendingCount => tasks.Count + pendingMicrotasks;

    public int Pump(long monotonicMs, int maximumCallbacks)
    {
        if (monotonicMs < 0) throw new ArgumentOutOfRangeException(nameof(monotonicMs));
        Now = Math.Max(Now, monotonicMs);
        var maximum = Math.Clamp(maximumCallbacks, 1, limits.MaximumEventCallbacksPerPump);
        // Snapshot: tasks queued by a callback wait for the next pump (RAF never recursively runs in one frame).
        var due = tasks.Values.Where(task => task.Due <= Now).OrderBy(task => task.Due).ThenBy(task => task.Id)
            .Take(maximum).ToArray();
        var executed = 0;
        foreach (var task in due)
        {
            if (!tasks.Remove(task.Id)) continue;
            if (task.Interval > 0) tasks.Add(task.Id, task with { Due = checked(Now + task.Interval) });
            engine.Invoke(task.Callback, engine.Global, task.Animation ? [(JsValue)(double)Now] : task.Arguments);
            // Evaluate drains Jint's actual promise job queue under statement/time/memory constraints.
            engine.Evaluate("void 0");
            ++executed;
        }
        return executed;
    }

    private JsValue Schedule(JsValue[] args, bool repeat, bool animation)
    {
        var callback = Value(args, 0);
        if (!isCallable(JsValue.Undefined, [callback]).AsBoolean())
            throw new ArgumentException("Browser timers require a function; source-string timers are disabled.");
        if (tasks.Count + pendingMicrotasks >= limits.MaximumPendingTasks)
            throw new BrowserLimitException("The browser task queue is full.");
        var delay = animation ? 1 : Math.Clamp(Number(Value(args, 1)), 0, int.MaxValue);
        var interval = repeat ? Math.Max(1, (long)delay) : 0;
        var id = nextId == int.MaxValue ? nextId = 1 : ++nextId;
        while (tasks.ContainsKey(id)) id = ++nextId;
        tasks.Add(id, new(id, callback, animation ? [] : args.Skip(2).ToArray(), checked(Now + (long)delay), interval, animation));
        return id;
    }

    private void Install(string name, Func<JsValue[], JsValue> callback) => engine.SetValue(name,
        new ClrFunction(engine, name, (_, args) => callback(args)));
    private static JsValue Value(JsValue[] args, int index) => index < args.Length ? args[index] : JsValue.Undefined;
    private static double Number(JsValue value) => value.IsNumber() && double.IsFinite(value.AsNumber()) ? value.AsNumber() : 0;
    private static int Integer(JsValue value) => (int)Math.Clamp(Number(value), int.MinValue, int.MaxValue);
}