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);
private sealed record Completion(int Id, string? Source, string? Error);
private sealed record Size(int Width, int Height);
private sealed record Snapshot(long Sequence, 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, nextReadback;
private long requestedTime, lastQueuedTime, observedSequence, nextSequence;
private long capturedPageRevision;
private Size size;
private Snapshot snapshot;
private string? uiError;
private static long nextPaintIdentity;
private readonly long paintIdentity = Interlocked.Increment(ref nextPaintIdentity);
public long RasterPaintCount { get; private set; }
public long CachedPaintCount { get; private set; }
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 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) => throw new NotSupportedException("This background session owns one code document.");
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 => { browser.Pointer(input); }, PointerMove: input.Type == "mousemove"));
public bool Key(BrowserKeyboardInput input) => Enqueue(new(browser => { browser.Key(input); }));
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)
{
DrainCompletions();
long sequence = Volatile.Read(ref snapshot).Sequence;
bool changed = sequence != observedSequence; observedSequence = sequence;
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();
}
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++; return; }
frame.Paint.Paint(canvas, bounds);
RasterPaintCount++;
cache?.StoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0);
}
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 (Volatile.Read(ref cancelled) != 0 || 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>();
try
{ WorkCore(finishedReadbacks); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
if (Volatile.Read(ref cancelled) != 0) { Volatile.Write(ref scheduled, 0); return; }
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) });
}
// The pinned native dispatcher does not support arbitrary finally
// unwinding. WorkCore may return early on cancellation; its caller
// always clears this publication flag on the explicit common path.
Volatile.Write(ref scheduled, 0);
}
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;
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)!;
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;
var previous = Volatile.Read(ref snapshot);
bool changed = processed != 0 || page.Revision != capturedPageRevision ||
previous.Paint.Viewport.Width != target.Width || previous.Paint.Viewport.Height != target.Height;
var completed = changed ? Capture() : previous;
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);
page.Layout(new(target.Width, target.Height));
var paint = page.CaptureEditorPaintSnapshot();
string source = page.ReadEditorText() ?? 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());
capturedPageRevision = page.Revision;
return new(++nextSequence, page.CurrentPage, source, paint, page.DocumentHeight, page.TextCursor, diagnostics);
}
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);
private sealed record Completion(int Id, string? Source, string? Error);
private sealed record Size(int Width, int Height);
private sealed record Snapshot(long Sequence, 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, nextReadback;
private long requestedTime, lastQueuedTime, observedSequence, nextSequence;
private long capturedPageRevision;
private Size size;
private Snapshot snapshot;
private string? uiError;
private static long nextPaintIdentity;
private readonly long paintIdentity = Interlocked.Increment(ref nextPaintIdentity);
public long RasterPaintCount { get; private set; }
public long CachedPaintCount { get; private set; }
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 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) => throw new NotSupportedException("This background session owns one code document.");
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 => { browser.Pointer(input); }, PointerMove: input.Type == "mousemove"));
public bool Key(BrowserKeyboardInput input) => Enqueue(new(browser => { browser.Key(input); }));
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)
{
DrainCompletions();
long sequence = Volatile.Read(ref snapshot).Sequence;
bool changed = sequence != observedSequence; observedSequence = sequence;
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();
}
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++; return; }
frame.Paint.Paint(canvas, bounds);
RasterPaintCount++;
cache?.StoreWebFrame(paintIdentity, frame.Sequence, frame.Diagnostics.LayoutPassCount, bounds, 0, 0);
}
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 (Volatile.Read(ref cancelled) != 0 || 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>();
try
{ WorkCore(finishedReadbacks); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
if (Volatile.Read(ref cancelled) != 0) { Volatile.Write(ref scheduled, 0); return; }
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) });
}
// The pinned native dispatcher does not support arbitrary finally
// unwinding. WorkCore may return early on cancellation; its caller
// always clears this publication flag on the explicit common path.
Volatile.Write(ref scheduled, 0);
}
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;
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)!;
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;
var previous = Volatile.Read(ref snapshot);
bool changed = processed != 0 || page.Revision != capturedPageRevision ||
previous.Paint.Viewport.Width != target.Width || previous.Paint.Viewport.Height != target.Height;
var completed = changed ? Capture() : previous;
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);
page.Layout(new(target.Width, target.Height));
var paint = page.CaptureEditorPaintSnapshot();
string source = page.ReadEditorText() ?? 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());
capturedPageRevision = page.Revision;
return new(++nextSequence, page.CurrentPage, source, paint, page.DocumentHeight, page.TextCursor, diagnostics);
}
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);
}
}
}