using XFESystem.Desktop;
namespace XFESystem.Browser;
/// <summary>
/// The UI owns this mailbox, never the underlying Jint engine. Its paint and
/// data getters read complete immutable snapshots. All script, DOM, layout and
/// input work continues on the one verified worker after the first ready frame.
/// </summary>
public sealed class BackgroundBrowserEditorSession : IBackgroundEditorSession
{
private const int Capacity = 512;
private sealed record Command(Action<BrowserService>? Apply, int ReadbackId = 0, bool PointerMove = false, bool ModelInput = false);
private sealed record Completion(int Id, string? Source, string? Error);
private sealed record Size(int Width, int Height);
private sealed record Snapshot(long Sequence, long DocumentGeneration, bool NeedsPresentation, BrowserPage Page, string? Source, BrowserPaintSnapshot Paint,
double Height, bool TextCursor, BackgroundEditorDiagnostics Diagnostics);
private readonly BrowserService page;
private readonly IEditorBackgroundWorkScheduler scheduler;
private readonly Func<long> clock;
private readonly Func<int> threadIdentity;
private readonly Action<string>? trace;
private readonly EditorPreparationRequest request;
private readonly Command?[] commands = new Command?[Capacity];
private readonly Completion?[] completions = new Completion?[Capacity];
private readonly Dictionary<int, Action<string?, string?>> readbacks = [];
private uint commandPublished, commandConsumed, completionPublished, completionConsumed;
private int scheduled, cancelled, retired, nextReadback;
private long lastLifetimeReport;
private long requestedTime, lastQueuedTime, observedSequence, nextSequence;
private long capturedPageRevision;
private long nextDocumentProof;
private string lastDocumentProof = "{}";
private Size size;
private Snapshot snapshot;
private bool observedTextCursor;
private string? uiError;
private readonly long paintIdentity = BrowserPaintIdentity.Next();
private long acknowledgedGeneration, paintedGeneration;
private bool paintedNeedsPresentation;
private DesktopBounds paintedBounds;
private int paintedScale;
private IDesktopPresentedPixelsCanvas? paintedCanvas;
private long paintedPresentationBefore, paintedSequence;
private double paintedScrollY;
private string paintedAddress = "";
private bool paintAwaitingPresentation;
public long RasterPaintCount { get; private set; }
public long CachedPaintCount { get; private set; }
public bool IsRetired=>Volatile.Read(ref retired)!=0;
internal BackgroundBrowserEditorSession(BrowserService page, IEditorBackgroundWorkScheduler scheduler,
EditorPreparationRequest request, Func<long> clock, Func<int> threadIdentity, Action<string>? trace, Func<bool>? preparationCancellation = null)
{
this.page = page; this.scheduler = scheduler; this.request = request;
this.clock = clock; this.threadIdentity = threadIdentity; this.trace = trace;
size = new(request.ViewportWidth, request.ViewportHeight);
// Installed on the worker: publication must not disconnect the
// resource/script boundary checks from a later UI-side close.
page.SetOperationCancellation(() => Volatile.Read(ref cancelled) != 0 || preparationCancellation?.Invoke() == true);
requestedTime = lastQueuedTime = clock();
snapshot = Capture();
}
public BrowserPage CurrentPage => Volatile.Read(ref snapshot).Page;
public long Revision => Volatile.Read(ref snapshot).Sequence;
public double DocumentHeight => Volatile.Read(ref snapshot).Height;
public bool TextCursor => Volatile.Read(ref snapshot).TextCursor;
public double ScrollOffsetY => Volatile.Read(ref snapshot).Paint.Viewport.ScrollY;
public BackgroundEditorDiagnostics Diagnostics
{
get { var value = Volatile.Read(ref snapshot).Diagnostics; return uiError is null ? value : value with { Error = uiError }; }
}
public string? ReadEditorText() => Volatile.Read(ref snapshot).Source;
public void SetCanvas(IDesktopCanvas canvas) { }
public void CompleteEditorPreparationHandoff(IDesktopCanvas canvas, bool equivalentTextMetrics) { }
public BrowserPage Navigate(string address)
{ ArgumentNullException.ThrowIfNull(address); Enqueue(new(browser => browser.Navigate(address))); return CurrentPage; }
public void ResizeViewport(int width, int height)
{
var current = Volatile.Read(ref size);
if (current.Width == width && current.Height == height) return;
Volatile.Write(ref size, new(Math.Max(1, width), Math.Max(1, height)));
EnsureQueued();
}
public bool Pointer(BrowserPointerInput input) => Enqueue(new(browser =>
{
bool prevented = browser.Pointer(input);
if (input.Type == "wheel" && !prevented) browser.ScrollBackgroundDocument(input.DeltaY);
}, PointerMove: input.Type == "mousemove"));
public bool Key(BrowserKeyboardInput input) => Enqueue(new(browser => { browser.Key(input); }, ModelInput: true));
public bool RequestEditorReadback(Action<string?, string?> completed)
{
ArgumentNullException.ThrowIfNull(completed);
if (readbacks.Count >= 128 || Volatile.Read(ref cancelled) != 0) return false;
int id = ++nextReadback; readbacks.Add(id, completed);
if (Enqueue(new(null, id))) return true;
readbacks.Remove(id); return false;
}
public bool AdvanceTime(long monotonicMilliseconds)
{
if(Volatile.Read(ref cancelled)!=0){EnsureQueued();return false;}
DrainCompletions();
var current = Volatile.Read(ref snapshot);
long sequence = current.Sequence;
bool changed = sequence != observedSequence || current.TextCursor != observedTextCursor;
observedSequence = sequence; observedTextCursor = current.TextCursor;
Volatile.Write(ref requestedTime, Math.Max(Volatile.Read(ref requestedTime), monotonicMilliseconds));
if (Volatile.Read(ref commandPublished) != Volatile.Read(ref commandConsumed) || monotonicMilliseconds - lastQueuedTime >= 50)
EnsureQueued();
return changed;
}
public void Cancel()
{
Volatile.Write(ref cancelled, 1);
readbacks.Clear();
EnsureQueued();
}
public void Release()=>Cancel();
public void Paint(IDesktopCanvas canvas, DesktopBounds bounds, double scrollY)
{
var frame = Volatile.Read(ref snapshot);
var cache = canvas as IDesktopWebFrameCache;
if (cache is not null && cache.TryRestoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0))
{ CachedPaintCount++; RecordPaint(); return; }
frame.Paint.Paint(canvas, bounds);
RasterPaintCount++;
cache?.StoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0);
RecordPaint();
void RecordPaint()
{
paintedGeneration = frame.DocumentGeneration; paintedNeedsPresentation = frame.NeedsPresentation; paintedBounds = bounds; paintedScale = canvas.ScalePercent;
paintedCanvas = canvas as IDesktopPresentedPixelsCanvas; paintedPresentationBefore = paintedCanvas?.FramesPresented ?? 0;
paintedSequence = frame.Sequence; paintedScrollY = frame.Paint.Viewport.ScrollY; paintedAddress = frame.Page.Address;
paintAwaitingPresentation = true;
}
}
public void FramePresented()
{
if (!paintAwaitingPresentation || paintedCanvas is { } scanout && scanout.FramesPresented <= paintedPresentationBefore) return;
paintAwaitingPresentation = false;
long sequence = paintedCanvas?.FramesPresented ?? 0, ticks = paintedCanvas?.LastPresentTicks ?? 0;
if (sequence > 0)
trace?.Invoke("XFE_BROWSER_BACKGROUND_PRESENTED|Window|" + request.WindowId + "|Generation|" + request.Generation +
"|Address|" + paintedAddress + "|SnapshotSequence|" + paintedSequence + "|PresentationSequence|" + sequence +
"|PresentationTicks|" + ticks + "|ScrollY|" + paintedScrollY.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture) + "\n");
if (!paintedNeedsPresentation || paintedGeneration == acknowledgedGeneration) return;
var bounds = paintedBounds; int scale = paintedScale; long generation = paintedGeneration;
if (Enqueue(new(browser => browser.BackgroundFramePresented(bounds, scale, generation, sequence, ticks)))) acknowledgedGeneration = generation;
}
private bool Enqueue(Command command)
{
if (Volatile.Read(ref cancelled) != 0) return false;
uint writer = Volatile.Read(ref commandPublished), reader = Volatile.Read(ref commandConsumed);
// Atomically replace only an unconsumed tail move. Exchanging the slot
// with null on the consumer makes a failed replacement enqueue a new
// event instead, so the final position cannot disappear in a race.
if (command.PointerMove && writer != reader)
{
int tail = (int)(unchecked(writer - 1) & (Capacity - 1));
var previous = Volatile.Read(ref commands[tail]);
if (previous is { PointerMove: true } && ReferenceEquals(Interlocked.CompareExchange(ref commands[tail], command, previous), previous))
{ EnsureQueued(); return true; }
}
if (unchecked(writer - reader) >= Capacity) { uiError = "Editor input queue is full; input was rejected."; return false; }
commands[writer & (Capacity - 1)] = command;
Volatile.Write(ref commandPublished, unchecked(writer + 1));
EnsureQueued(); return true;
}
private void EnsureQueued()
{
if (IsRetired || Interlocked.CompareExchange(ref scheduled, 1, 0) != 0) return;
lastQueuedTime = Volatile.Read(ref requestedTime);
if (scheduler.TryQueue(Work)) { uiError = null; return; }
Volatile.Write(ref scheduled, 0); uiError = "Editor worker queue is busy; queued input will retry.";
}
private void Work()
{
var finishedReadbacks = new List<Completion>();
bool succeeded = false;
try
{ WorkCore(finishedReadbacks); succeeded = true; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
if(Volatile.Read(ref cancelled)==0)
{
var previous = Volatile.Read(ref snapshot);
Volatile.Write(ref snapshot, previous with { Sequence = ++nextSequence, Diagnostics = previous.Diagnostics with { Error = BoundedText.Safe(error.Message, 512) } });
trace?.Invoke("XFE_EDITOR_BACKGROUND|SessionError|Window|" + request.WindowId + "|Generation|" + request.Generation +
"|Thread|" + threadIdentity() + "\n");
foreach (var completion in finishedReadbacks) PublishCompletion(completion with { Error = BoundedText.Safe(error.Message, 512) });
}
}
// Keep publication cleanup on a common path, including cancellation
// and the explicit Legacy runtime entry, which has limited unwinding.
Volatile.Write(ref scheduled, 0);
if(Volatile.Read(ref cancelled)!=0)RetireOnOwner();
// Continue finite loading work without waiting for the UI's 50 ms
// timer between every library. Each script remains a separate FIFO
// work item, so input and other pages can run between them. Never
// spin while waiting for the first actual presentation or after error.
else if (succeeded && page.DocumentEnhancementsPending && !page.NeedsDocumentFirstPresentation) EnsureQueued();
}
internal void RetireOnOwner()
{
if(IsRetired)return;
page.Release();Array.Clear(commands);Array.Clear(completions);
Volatile.Write(ref retired,1);
trace?.Invoke("XFE_EDITOR_BACKGROUND|Retired|Window|"+request.WindowId+"|Generation|"+request.Generation+"|Thread|"+threadIdentity()+"\n");
}
private void WorkCore(List<Completion> finishedReadbacks)
{
if (Volatile.Read(ref cancelled) != 0) return;
var target = Volatile.Read(ref size);
page.ResizeViewport(target.Width, target.Height);
int processed = 0;
bool modelInput = false;
while (processed < Capacity)
{
uint reader = Volatile.Read(ref commandConsumed);
if (reader == Volatile.Read(ref commandPublished)) break;
processed++;
var command = Interlocked.Exchange(ref commands[reader & (Capacity - 1)], null)!;
modelInput |= command.ModelInput || command.ReadbackId != 0;
Volatile.Write(ref commandConsumed, unchecked(reader + 1));
if (Volatile.Read(ref cancelled) != 0) return;
if (command.ReadbackId != 0)
{
try { finishedReadbacks.Add(new(command.ReadbackId, page.ReadEditorText(), null)); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ finishedReadbacks.Add(new(command.ReadbackId, null, BoundedText.Safe(error.Message, 512))); }
}
else command.Apply!(page);
}
page.AdvanceTime(Math.Max(clock(), Volatile.Read(ref requestedTime)));
if (Volatile.Read(ref cancelled) != 0) return;
long now=clock();
ReportDocumentProof(now);
if(now-lastLifetimeReport>=5000&&page.ReadLifetimeDiagnostics() is { } lifetime)
{
lastLifetimeReport=now;
var weak=page.ReadWeakCollectionLifetime();
trace?.Invoke("XFE_BROWSER_RETENTION|Window|"+request.WindowId+"|Wrappers|"+lifetime.Wrappers+
"|Identities|"+lifetime.Identities+"|Canvases|"+lifetime.Canvases+"|Ranges|"+lifetime.Ranges+
"|Created|"+lifetime.HandlesCreated+"|Disposed|"+lifetime.HandlesDisposed+"|Outstanding|"+lifetime.OutstandingHandles+
"|WeakTables|"+(weak?.Tables??0)+"|WeakEntries|"+(weak?.Entries??0)+
"|WeakCreated|"+(weak?.Created??0)+"|WeakDisposed|"+(weak?.Disposed??0)+"\n");
}
var previous = Volatile.Read(ref snapshot);
string? source = modelInput ? page.ReadEditorText() : previous.Source;
bool changed = page.Revision != capturedPageRevision || source != previous.Source ||
previous.Paint.Viewport != page.BackgroundViewport || previous.DocumentGeneration != page.DocumentGeneration;
// An input acknowledgement is not a new painted frame. No-op
// mouse motion and readback retain the immutable paint identity,
// native glyph coverage and UI frame cache. Real hover/DOM/canvas
// mutations, model changes and scrolling still publish new paint.
var completed = previous;
if (changed) completed = Capture();
else
{
bool textCursor = page.TextCursor, needsPresentation = page.NeedsDocumentFirstPresentation;
if (textCursor != previous.TextCursor || needsPresentation != previous.NeedsPresentation ||
page.LastInputTrace != previous.Diagnostics.LastInputTrace)
completed = previous with
{
TextCursor = textCursor, NeedsPresentation = needsPresentation,
Diagnostics = page.LastInputTrace == previous.Diagnostics.LastInputTrace ? previous.Diagnostics :
previous.Diagnostics with { LastInputTrace = page.LastInputTrace }
};
}
if (Volatile.Read(ref cancelled) == 0)
{
Volatile.Write(ref snapshot, completed);
// A save acknowledgement cannot overtake the model snapshot
// containing the input that this FIFO readback already saw.
foreach (var completion in finishedReadbacks) PublishCompletion(completion);
}
}
private Snapshot Capture()
{
var target = Volatile.Read(ref size);
// Resize may have arrived during a long callback. Dispatch the real
// resize before layout, rather than letting Layout alone consume the
// new size and suppress Monaco's next resize notification.
page.ResizeViewport(target.Width, target.Height);
var paint = page.CaptureEditorPaintSnapshot();
string? source = page.ReadEditorText();
if (request.Kind == EditorPreparationKind.Editor && source is null) throw new IOException("The actual editor model is unavailable.");
var diagnostics = new BackgroundEditorDiagnostics(page.ReadMonacoProof(), page.LastInputTrace, page.LayoutPassCount,
page.ScrollLayoutReuseCount, page.PackagedScriptCacheHits, page.PackagedScriptPreparations, threadIdentity());
// Bind changed DOM readback to this immutable capture before release
// publication. A delayed unchanged poll cannot prove a later scanout.
ReportDocumentProof(clock(), force: true);
if (trace is not null)
{
string remoteProof = page.ReadRemotePageProof(nextSequence + 1);
if (remoteProof != "{}") trace("XFE_REMOTE_PAGE_PROOF_BEGIN\n" + remoteProof + "\nXFE_REMOTE_PAGE_PROOF_END\n");
}
capturedPageRevision = page.Revision;
return new(++nextSequence, page.DocumentGeneration, page.NeedsDocumentFirstPresentation, page.CurrentPage, source, paint, page.DocumentHeight, page.TextCursor, diagnostics);
}
private void ReportDocumentProof(long now, bool force = false)
{
if (trace is not null && (force || now >= nextDocumentProof))
{
nextDocumentProof = now + 500;
string proof = page.ReadWebsiteDocumentProof();
if (proof != "{}" && proof != lastDocumentProof)
{ lastDocumentProof = proof; trace?.Invoke("XFE_WEBSITE_DOCUMENT_PROOF_BEGIN\n" + proof + "\nXFE_WEBSITE_DOCUMENT_PROOF_END\n"); }
}
}
private void PublishCompletion(Completion completion)
{
uint writer = Volatile.Read(ref completionPublished), reader = Volatile.Read(ref completionConsumed);
if (unchecked(writer - reader) >= Capacity) throw new IOException("Editor completion queue is full.");
completions[writer & (Capacity - 1)] = completion;
Volatile.Write(ref completionPublished, unchecked(writer + 1));
}
private void DrainCompletions()
{
while (true)
{
uint reader = Volatile.Read(ref completionConsumed);
if (reader == Volatile.Read(ref completionPublished)) return;
var completion = completions[reader & (Capacity - 1)]!;
completions[reader & (Capacity - 1)] = null;
Volatile.Write(ref completionConsumed, unchecked(reader + 1));
if (readbacks.Remove(completion.Id, out var callback)) callback(completion.Source, completion.Error);
}
}
}
using XFESystem.Desktop;
namespace XFESystem.Browser;
/// <summary>
/// The UI owns this mailbox, never the underlying Jint engine. Its paint and
/// data getters read complete immutable snapshots. All script, DOM, layout and
/// input work continues on the one verified worker after the first ready frame.
/// </summary>
public sealed class BackgroundBrowserEditorSession : IBackgroundEditorSession
{
private const int Capacity = 512;
private sealed record Command(Action<BrowserService>? Apply, int ReadbackId = 0, bool PointerMove = false, bool ModelInput = false);
private sealed record Completion(int Id, string? Source, string? Error);
private sealed record Size(int Width, int Height);
private sealed record Snapshot(long Sequence, long DocumentGeneration, bool NeedsPresentation, BrowserPage Page, string? Source, BrowserPaintSnapshot Paint,
double Height, bool TextCursor, BackgroundEditorDiagnostics Diagnostics);
private readonly BrowserService page;
private readonly IEditorBackgroundWorkScheduler scheduler;
private readonly Func<long> clock;
private readonly Func<int> threadIdentity;
private readonly Action<string>? trace;
private readonly EditorPreparationRequest request;
private readonly Command?[] commands = new Command?[Capacity];
private readonly Completion?[] completions = new Completion?[Capacity];
private readonly Dictionary<int, Action<string?, string?>> readbacks = [];
private uint commandPublished, commandConsumed, completionPublished, completionConsumed;
private int scheduled, cancelled, retired, nextReadback;
private long lastLifetimeReport;
private long requestedTime, lastQueuedTime, observedSequence, nextSequence;
private long capturedPageRevision;
private long nextDocumentProof;
private string lastDocumentProof = "{}";
private Size size;
private Snapshot snapshot;
private bool observedTextCursor;
private string? uiError;
private readonly long paintIdentity = BrowserPaintIdentity.Next();
private long acknowledgedGeneration, paintedGeneration;
private bool paintedNeedsPresentation;
private DesktopBounds paintedBounds;
private int paintedScale;
private IDesktopPresentedPixelsCanvas? paintedCanvas;
private long paintedPresentationBefore, paintedSequence;
private double paintedScrollY;
private string paintedAddress = "";
private bool paintAwaitingPresentation;
public long RasterPaintCount { get; private set; }
public long CachedPaintCount { get; private set; }
public bool IsRetired=>Volatile.Read(ref retired)!=0;
internal BackgroundBrowserEditorSession(BrowserService page, IEditorBackgroundWorkScheduler scheduler,
EditorPreparationRequest request, Func<long> clock, Func<int> threadIdentity, Action<string>? trace, Func<bool>? preparationCancellation = null)
{
this.page = page; this.scheduler = scheduler; this.request = request;
this.clock = clock; this.threadIdentity = threadIdentity; this.trace = trace;
size = new(request.ViewportWidth, request.ViewportHeight);
// Installed on the worker: publication must not disconnect the
// resource/script boundary checks from a later UI-side close.
page.SetOperationCancellation(() => Volatile.Read(ref cancelled) != 0 || preparationCancellation?.Invoke() == true);
requestedTime = lastQueuedTime = clock();
snapshot = Capture();
}
public BrowserPage CurrentPage => Volatile.Read(ref snapshot).Page;
public long Revision => Volatile.Read(ref snapshot).Sequence;
public double DocumentHeight => Volatile.Read(ref snapshot).Height;
public bool TextCursor => Volatile.Read(ref snapshot).TextCursor;
public double ScrollOffsetY => Volatile.Read(ref snapshot).Paint.Viewport.ScrollY;
public BackgroundEditorDiagnostics Diagnostics
{
get { var value = Volatile.Read(ref snapshot).Diagnostics; return uiError is null ? value : value with { Error = uiError }; }
}
public string? ReadEditorText() => Volatile.Read(ref snapshot).Source;
public void SetCanvas(IDesktopCanvas canvas) { }
public void CompleteEditorPreparationHandoff(IDesktopCanvas canvas, bool equivalentTextMetrics) { }
public BrowserPage Navigate(string address)
{ ArgumentNullException.ThrowIfNull(address); Enqueue(new(browser => browser.Navigate(address))); return CurrentPage; }
public void ResizeViewport(int width, int height)
{
var current = Volatile.Read(ref size);
if (current.Width == width && current.Height == height) return;
Volatile.Write(ref size, new(Math.Max(1, width), Math.Max(1, height)));
EnsureQueued();
}
public bool Pointer(BrowserPointerInput input) => Enqueue(new(browser =>
{
bool prevented = browser.Pointer(input);
if (input.Type == "wheel" && !prevented) browser.ScrollBackgroundDocument(input.DeltaY);
}, PointerMove: input.Type == "mousemove"));
public bool Key(BrowserKeyboardInput input) => Enqueue(new(browser => { browser.Key(input); }, ModelInput: true));
public bool RequestEditorReadback(Action<string?, string?> completed)
{
ArgumentNullException.ThrowIfNull(completed);
if (readbacks.Count >= 128 || Volatile.Read(ref cancelled) != 0) return false;
int id = ++nextReadback; readbacks.Add(id, completed);
if (Enqueue(new(null, id))) return true;
readbacks.Remove(id); return false;
}
public bool AdvanceTime(long monotonicMilliseconds)
{
if(Volatile.Read(ref cancelled)!=0){EnsureQueued();return false;}
DrainCompletions();
var current = Volatile.Read(ref snapshot);
long sequence = current.Sequence;
bool changed = sequence != observedSequence || current.TextCursor != observedTextCursor;
observedSequence = sequence; observedTextCursor = current.TextCursor;
Volatile.Write(ref requestedTime, Math.Max(Volatile.Read(ref requestedTime), monotonicMilliseconds));
if (Volatile.Read(ref commandPublished) != Volatile.Read(ref commandConsumed) || monotonicMilliseconds - lastQueuedTime >= 50)
EnsureQueued();
return changed;
}
public void Cancel()
{
Volatile.Write(ref cancelled, 1);
readbacks.Clear();
EnsureQueued();
}
public void Release()=>Cancel();
public void Paint(IDesktopCanvas canvas, DesktopBounds bounds, double scrollY)
{
var frame = Volatile.Read(ref snapshot);
var cache = canvas as IDesktopWebFrameCache;
if (cache is not null && cache.TryRestoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0))
{ CachedPaintCount++; RecordPaint(); return; }
frame.Paint.Paint(canvas, bounds);
RasterPaintCount++;
cache?.StoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0);
RecordPaint();
void RecordPaint()
{
paintedGeneration = frame.DocumentGeneration; paintedNeedsPresentation = frame.NeedsPresentation; paintedBounds = bounds; paintedScale = canvas.ScalePercent;
paintedCanvas = canvas as IDesktopPresentedPixelsCanvas; paintedPresentationBefore = paintedCanvas?.FramesPresented ?? 0;
paintedSequence = frame.Sequence; paintedScrollY = frame.Paint.Viewport.ScrollY; paintedAddress = frame.Page.Address;
paintAwaitingPresentation = true;
}
}
public void FramePresented()
{
if (!paintAwaitingPresentation || paintedCanvas is { } scanout && scanout.FramesPresented <= paintedPresentationBefore) return;
paintAwaitingPresentation = false;
long sequence = paintedCanvas?.FramesPresented ?? 0, ticks = paintedCanvas?.LastPresentTicks ?? 0;
if (sequence > 0)
trace?.Invoke("XFE_BROWSER_BACKGROUND_PRESENTED|Window|" + request.WindowId + "|Generation|" + request.Generation +
"|Address|" + paintedAddress + "|SnapshotSequence|" + paintedSequence + "|PresentationSequence|" + sequence +
"|PresentationTicks|" + ticks + "|ScrollY|" + paintedScrollY.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture) + "\n");
if (!paintedNeedsPresentation || paintedGeneration == acknowledgedGeneration) return;
var bounds = paintedBounds; int scale = paintedScale; long generation = paintedGeneration;
if (Enqueue(new(browser => browser.BackgroundFramePresented(bounds, scale, generation, sequence, ticks)))) acknowledgedGeneration = generation;
}
private bool Enqueue(Command command)
{
if (Volatile.Read(ref cancelled) != 0) return false;
uint writer = Volatile.Read(ref commandPublished), reader = Volatile.Read(ref commandConsumed);
// Atomically replace only an unconsumed tail move. Exchanging the slot
// with null on the consumer makes a failed replacement enqueue a new
// event instead, so the final position cannot disappear in a race.
if (command.PointerMove && writer != reader)
{
int tail = (int)(unchecked(writer - 1) & (Capacity - 1));
var previous = Volatile.Read(ref commands[tail]);
if (previous is { PointerMove: true } && ReferenceEquals(Interlocked.CompareExchange(ref commands[tail], command, previous), previous))
{ EnsureQueued(); return true; }
}
if (unchecked(writer - reader) >= Capacity) { uiError = "Editor input queue is full; input was rejected."; return false; }
commands[writer & (Capacity - 1)] = command;
Volatile.Write(ref commandPublished, unchecked(writer + 1));
EnsureQueued(); return true;
}
private void EnsureQueued()
{
if (IsRetired || Interlocked.CompareExchange(ref scheduled, 1, 0) != 0) return;
lastQueuedTime = Volatile.Read(ref requestedTime);
if (scheduler.TryQueue(Work)) { uiError = null; return; }
Volatile.Write(ref scheduled, 0); uiError = "Editor worker queue is busy; queued input will retry.";
}
private void Work()
{
var finishedReadbacks = new List<Completion>();
bool succeeded = false;
try
{ WorkCore(finishedReadbacks); succeeded = true; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
if(Volatile.Read(ref cancelled)==0)
{
var previous = Volatile.Read(ref snapshot);
Volatile.Write(ref snapshot, previous with { Sequence = ++nextSequence, Diagnostics = previous.Diagnostics with { Error = BoundedText.Safe(error.Message, 512) } });
trace?.Invoke("XFE_EDITOR_BACKGROUND|SessionError|Window|" + request.WindowId + "|Generation|" + request.Generation +
"|Thread|" + threadIdentity() + "\n");
foreach (var completion in finishedReadbacks) PublishCompletion(completion with { Error = BoundedText.Safe(error.Message, 512) });
}
}
// Keep publication cleanup on a common path, including cancellation
// and the explicit Legacy runtime entry, which has limited unwinding.
Volatile.Write(ref scheduled, 0);
if(Volatile.Read(ref cancelled)!=0)RetireOnOwner();
// Continue finite loading work without waiting for the UI's 50 ms
// timer between every library. Each script remains a separate FIFO
// work item, so input and other pages can run between them. Never
// spin while waiting for the first actual presentation or after error.
else if (succeeded && page.DocumentEnhancementsPending && !page.NeedsDocumentFirstPresentation) EnsureQueued();
}
internal void RetireOnOwner()
{
if(IsRetired)return;
page.Release();Array.Clear(commands);Array.Clear(completions);
Volatile.Write(ref retired,1);
trace?.Invoke("XFE_EDITOR_BACKGROUND|Retired|Window|"+request.WindowId+"|Generation|"+request.Generation+"|Thread|"+threadIdentity()+"\n");
}
private void WorkCore(List<Completion> finishedReadbacks)
{
if (Volatile.Read(ref cancelled) != 0) return;
var target = Volatile.Read(ref size);
page.ResizeViewport(target.Width, target.Height);
int processed = 0;
bool modelInput = false;
while (processed < Capacity)
{
uint reader = Volatile.Read(ref commandConsumed);
if (reader == Volatile.Read(ref commandPublished)) break;
processed++;
var command = Interlocked.Exchange(ref commands[reader & (Capacity - 1)], null)!;
modelInput |= command.ModelInput || command.ReadbackId != 0;
Volatile.Write(ref commandConsumed, unchecked(reader + 1));
if (Volatile.Read(ref cancelled) != 0) return;
if (command.ReadbackId != 0)
{
try { finishedReadbacks.Add(new(command.ReadbackId, page.ReadEditorText(), null)); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ finishedReadbacks.Add(new(command.ReadbackId, null, BoundedText.Safe(error.Message, 512))); }
}
else command.Apply!(page);
}
page.AdvanceTime(Math.Max(clock(), Volatile.Read(ref requestedTime)));
if (Volatile.Read(ref cancelled) != 0) return;
long now=clock();
ReportDocumentProof(now);
if(now-lastLifetimeReport>=5000&&page.ReadLifetimeDiagnostics() is { } lifetime)
{
lastLifetimeReport=now;
var weak=page.ReadWeakCollectionLifetime();
trace?.Invoke("XFE_BROWSER_RETENTION|Window|"+request.WindowId+"|Wrappers|"+lifetime.Wrappers+
"|Identities|"+lifetime.Identities+"|Canvases|"+lifetime.Canvases+"|Ranges|"+lifetime.Ranges+
"|Created|"+lifetime.HandlesCreated+"|Disposed|"+lifetime.HandlesDisposed+"|Outstanding|"+lifetime.OutstandingHandles+
"|WeakTables|"+(weak?.Tables??0)+"|WeakEntries|"+(weak?.Entries??0)+
"|WeakCreated|"+(weak?.Created??0)+"|WeakDisposed|"+(weak?.Disposed??0)+"\n");
}
var previous = Volatile.Read(ref snapshot);
string? source = modelInput ? page.ReadEditorText() : previous.Source;
bool changed = page.Revision != capturedPageRevision || source != previous.Source ||
previous.Paint.Viewport != page.BackgroundViewport || previous.DocumentGeneration != page.DocumentGeneration;
// An input acknowledgement is not a new painted frame. No-op
// mouse motion and readback retain the immutable paint identity,
// native glyph coverage and UI frame cache. Real hover/DOM/canvas
// mutations, model changes and scrolling still publish new paint.
var completed = previous;
if (changed) completed = Capture();
else
{
bool textCursor = page.TextCursor, needsPresentation = page.NeedsDocumentFirstPresentation;
if (textCursor != previous.TextCursor || needsPresentation != previous.NeedsPresentation ||
page.LastInputTrace != previous.Diagnostics.LastInputTrace)
completed = previous with
{
TextCursor = textCursor, NeedsPresentation = needsPresentation,
Diagnostics = page.LastInputTrace == previous.Diagnostics.LastInputTrace ? previous.Diagnostics :
previous.Diagnostics with { LastInputTrace = page.LastInputTrace }
};
}
if (Volatile.Read(ref cancelled) == 0)
{
Volatile.Write(ref snapshot, completed);
// A save acknowledgement cannot overtake the model snapshot
// containing the input that this FIFO readback already saw.
foreach (var completion in finishedReadbacks) PublishCompletion(completion);
}
}
private Snapshot Capture()
{
var target = Volatile.Read(ref size);
// Resize may have arrived during a long callback. Dispatch the real
// resize before layout, rather than letting Layout alone consume the
// new size and suppress Monaco's next resize notification.
page.ResizeViewport(target.Width, target.Height);
var paint = page.CaptureEditorPaintSnapshot();
string? source = page.ReadEditorText();
if (request.Kind == EditorPreparationKind.Editor && source is null) throw new IOException("The actual editor model is unavailable.");
var diagnostics = new BackgroundEditorDiagnostics(page.ReadMonacoProof(), page.LastInputTrace, page.LayoutPassCount,
page.ScrollLayoutReuseCount, page.PackagedScriptCacheHits, page.PackagedScriptPreparations, threadIdentity());
// Bind changed DOM readback to this immutable capture before release
// publication. A delayed unchanged poll cannot prove a later scanout.
ReportDocumentProof(clock(), force: true);
if (trace is not null)
{
string remoteProof = page.ReadRemotePageProof(nextSequence + 1);
if (remoteProof != "{}") trace("XFE_REMOTE_PAGE_PROOF_BEGIN\n" + remoteProof + "\nXFE_REMOTE_PAGE_PROOF_END\n");
}
capturedPageRevision = page.Revision;
return new(++nextSequence, page.DocumentGeneration, page.NeedsDocumentFirstPresentation, page.CurrentPage, source, paint, page.DocumentHeight, page.TextCursor, diagnostics);
}
private void ReportDocumentProof(long now, bool force = false)
{
if (trace is not null && (force || now >= nextDocumentProof))
{
nextDocumentProof = now + 500;
string proof = page.ReadWebsiteDocumentProof();
if (proof != "{}" && proof != lastDocumentProof)
{ lastDocumentProof = proof; trace?.Invoke("XFE_WEBSITE_DOCUMENT_PROOF_BEGIN\n" + proof + "\nXFE_WEBSITE_DOCUMENT_PROOF_END\n"); }
}
}
private void PublishCompletion(Completion completion)
{
uint writer = Volatile.Read(ref completionPublished), reader = Volatile.Read(ref completionConsumed);
if (unchecked(writer - reader) >= Capacity) throw new IOException("Editor completion queue is full.");
completions[writer & (Capacity - 1)] = completion;
Volatile.Write(ref completionPublished, unchecked(writer + 1));
}
private void DrainCompletions()
{
while (true)
{
uint reader = Volatile.Read(ref completionConsumed);
if (reader == Volatile.Read(ref completionPublished)) return;
var completion = completions[reader & (Capacity - 1)]!;
completions[reader & (Capacity - 1)] = null;
Volatile.Write(ref completionConsumed, unchecked(reader + 1));
if (readbacks.Remove(completion.Id, out var callback)) callback(completion.Source, completion.Error);
}
}
}