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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Runtime.CompilerServices;
using AngleSharp.Html.Parser;
using XFESystem.Core;
using XFESystem.Desktop;
using Xunit.Abstractions;

namespace XFESystem.Browser.Tests;

[CollectionDefinition("Browser lifetime ownership",DisableParallelization=true)]
public sealed class BrowserLifetimeCollection;

[Collection("Browser lifetime ownership")]
public sealed class BrowserLifetimeTests(ITestOutputHelper output)
{
    private sealed class Key;
    private sealed class Value(Key key){public Key BackReference=key;}
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static (WeakReference<Key>,WeakReference<Value>) AddCycle(DependentIdentityMap<Key,Value> map)
    {var key=new Key();var value=new Value(key);map.Add(key,value);return(new(key),new(value));}
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static WeakReference<Value> AddWithAliveKey(DependentIdentityMap<Key,Value> map,Key key)
    {var value=new Value(key);map.Add(key,value);return new(value);}
    private static void Collect(){GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();}

    [Fact]
    public void OwnedEphemeronsCollectUnreachableCyclesAndExplicitlyFreePrunedHandles()
    {
        var map=new DependentIdentityMap<Key,Value>();var weak=AddCycle(map);Collect();
        Assert.False(weak.Item1.TryGetTarget(out _));Assert.False(weak.Item2.TryGetTarget(out _));
        Assert.Equal(0,map.Count);Assert.Equal(1,map.HandlesCreated);Assert.Equal(1,map.HandlesDisposed);
        map.Clear();map.Clear();Assert.Equal(1,map.HandlesDisposed);
        var key=new Key();var live=AddWithAliveKey(map,key);Collect();
        Assert.True(live.TryGetTarget(out var value));Assert.Same(value,map[key]);Assert.Same(key,value!.BackReference);
        Assert.True(map.Remove(key));Assert.False(map.Remove(key));Assert.Equal(2,map.HandlesDisposed);GC.KeepAlive(key);
    }

    [Fact]
    public void DetachedJavascriptReferencesKeepIdentityListenersAndCanReattachAfterCollection()
    {
        var runtime=new JavaScriptRuntime();var document=new HtmlParser().ParseDocument("<body></body>");
        var dom=new DomBridge(runtime,document,new("http://lifetime.test/"),new());
        Assert.True(runtime.Evaluate("globalThis.saved=document.createElement('button');saved.id='Saved';document.body.append(saved);saved.remove();globalThis.clicked=0;saved.addEventListener('click',()=>clicked++);globalThis.originalId=saved.__nodeId;'ready'").Success);
        Collect();
        Assert.Equal("true",runtime.Evaluate("document.body.append(saved);saved.click();document.getElementById('Saved')===saved&&saved.__nodeId===originalId&&clicked===1").Text);
        int identity=int.Parse(runtime.Evaluate("saved.__nodeId").Text);
        Assert.Same(document.GetElementById("Saved"),dom.ResolveNode(identity));
        dom.ReleaseRetainedState();Assert.Equal(0,dom.Lifetime.OutstandingHandles);GC.KeepAlive(runtime);
    }

    [Fact]
    public void DetachedDomCanvasFormAndListenerCyclesDoNotRemainPageRoots()
    {
        var runtime=new JavaScriptRuntime();var document=new HtmlParser().ParseDocument("<body></body>");
        var dom=new DomBridge(runtime,document,new("http://lifetime.test/"),new());
        var created=runtime.Evaluate("for(let i=0;i<20;i++){let c=document.createElement('canvas');c.width=4;c.height=4;document.body.append(c);c.getContext('2d').fillRect(0,0,2,2);c.remove();let a=document.createElement('textarea');a.value='temporary';a.setSelectionRange(1,2);document.body.append(a);a.remove();let n=document.createElement('span');n.textContent='gone';n.addEventListener('click',(function(node){return()=>node.textContent})(n));document.body.append(n);n.remove();c=null;a=null;n=null;} 'done'");
        Assert.True(created.Success,created.Error);
        Assert.True(runtime.Evaluate("document.body.textContent='';void 0").Success);
        var before=dom.Lifetime;Collect();var after=dom.Lifetime;
        output.WriteLine("Detached before={0}; after={1}",before,after);
        Assert.InRange(after.Wrappers,1,12);Assert.InRange(after.Identities,1,15);
        Assert.Equal(0,after.Canvases);Assert.Equal(0,after.FormSelections);
        Assert.True(after.HandlesDisposed>100);Assert.True(after.OutstandingHandles<before.OutstandingHandles/4);
        dom.ReleaseRetainedState();Assert.Equal(dom.Lifetime.HandlesCreated,dom.Lifetime.HandlesDisposed);GC.KeepAlive(runtime);
    }

    [Fact]
    public void OriginalMonacoHelloCSharpIdleKeepsLiveDomAndMemoryBoundedWhileTimersContinue()
    {
        var storage=new MemoryWorkspace();string source=storage.ReadText(SystemPaths.Combine(SystemPaths.Documents,"Hello.cs"));
        var browser=new BrowserService();browser.ResizeViewport(796,404);browser.NavigateEditor(source,"Hello.cs");
        long baseline=0;long firstRevision=0;BrowserDomLifetime? first=null;
        for(int step=0;step<=200;step++)
        {
            browser.AdvanceTime(step*500);browser.Layout(new(796,404));browser.ReadMonacoProof();
            if(step%25!=0)continue;
            Collect();var lifetime=browser.ReadLifetimeDiagnostics()!;long memory=GC.GetTotalMemory(true);
            var weak=browser.ReadWeakCollectionLifetime()!;
            output.WriteLine("JS weak tables={0}, entries={1}, created={2}, disposed={3}",weak.Tables,weak.Entries,weak.Created,weak.Disposed);
            if(step>=50)Assert.InRange(weak.Created-weak.Disposed,0,4096);
            output.WriteLine("Step {0}: bytes={1}, wrappers={2}, identities={3}, created={4}, disposed={5}, layout={6}, revision={7}",
                step,memory,lifetime.Wrappers,lifetime.Identities,lifetime.HandlesCreated,lifetime.HandlesDisposed,browser.LayoutPassCount,browser.Revision);
            if(step==50){baseline=memory;firstRevision=browser.Revision;first=lifetime;}
            if(step>=50)
            {
                Assert.InRange(lifetime.Wrappers,1,400);Assert.InRange(lifetime.Identities,1,500);
                Assert.InRange(lifetime.OutstandingHandles,1,1024);Assert.InRange(memory-baseline,-8_388_608,8_388_608);
            }
        }
        var last=browser.ReadLifetimeDiagnostics()!;
        Assert.True(browser.Revision>firstRevision+100);Assert.True(browser.LayoutPassCount>100);
        // With a working innerText setter Monaco can measure its nonbreaking
        // space probe once. It no longer retries zero-width font measurements
        // throughout idle, so disposal churn itself is not a lifetime invariant.
        Assert.InRange(last.OutstandingHandles-first!.OutstandingHandles,-1024,32);
        Assert.Equal(source,browser.ReadEditorText());
        browser.Release();Assert.Null(browser.Document);GC.KeepAlive(browser);
    }

    [Fact]
    public void ReadyActorRetriesRejectedRetirementAndReleasesOnlyOnItsWorker()
    {
        var scheduler=new Scheduler();var factory=new Factory(scheduler);long clock=0;
        var service=new BrowserEditorPreparationService(factory,scheduler,new CanvasFactory(),threadIdentity:()=>scheduler.Running?2:1,monotonicMilliseconds:()=>++clock);
        var operation=service.Begin(new(1,1,"const answer=42;","Hello.js",796,404));scheduler.RunAll();
        Assert.True(operation.TryTakeResult(out var result));Assert.Null(result.Error);var actor=Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        var backend=factory.Page!;Assert.NotNull(backend.Document);
        scheduler.Reject=true;actor.Cancel();Assert.False(actor.IsRetired);actor.AdvanceTime(++clock);Assert.False(actor.IsRetired);
        Assert.NotNull(backend.Document);scheduler.Reject=false;actor.AdvanceTime(++clock);scheduler.RunAll();
        Assert.True(actor.IsRetired);Assert.Null(backend.Document);Assert.Equal(0,scheduler.WorkerEnqueues);Assert.True(factory.ReleaseObservedOnWorker);
    }

    [Fact]
    public void CancelledPublishedOperationRetainsCleanupUntilQueueAcceptsIt()
    {
        var scheduler=new Scheduler();var factory=new Factory(scheduler);long clock=0;
        var service=new BrowserEditorPreparationService(factory,scheduler,new CanvasFactory(),monotonicMilliseconds:()=>++clock);
        var operation=service.Begin(new(1,1,"const answer=42;","Hello.js",796,404));scheduler.RunAll();
        Assert.NotNull(factory.Page!.Document);
        scheduler.Reject=true;operation.Cancel();Assert.False(operation.TryTakeResult(out _));Assert.False(operation.IsRetired);
        operation.AdvanceRetirement(++clock);Assert.False(operation.IsRetired);Assert.NotNull(factory.Page!.Document);
        scheduler.Reject=false;operation.AdvanceRetirement(++clock);scheduler.RunAll();operation.AdvanceRetirement(++clock);
        Assert.True(operation.IsRetired);Assert.Null(factory.Page.Document);Assert.Equal(0,scheduler.WorkerEnqueues);
    }

    [Fact]
    public void CancellationAtPreparedPublicationBoundaryReleasesOwnedBackendWithoutWorkerEnqueue()
    {
        var scheduler=new Scheduler();var factory=new Factory(scheduler);IEditorPreparationOperation? operation=null;bool prepared=false;
        var service=new BrowserEditorPreparationService(factory,scheduler,new CanvasFactory(),line=>
        {if(line.StartsWith("XFE_EDITOR_BACKGROUND|Prepared|",StringComparison.Ordinal)){prepared=true;operation!.Cancel();}});
        operation=service.Begin(new(1,1,"const answer=42;","Hello.js",796,404));scheduler.RunAll();
        Assert.True(prepared);Assert.False(operation.TryTakeResult(out _));Assert.True(operation.IsRetired);Assert.Null(factory.Page!.Document);
        Assert.True(factory.ReleaseObservedOnWorker);Assert.Equal(0,scheduler.WorkerEnqueues);
    }

    private sealed class Scheduler:IEditorBackgroundWorkScheduler
    {
        private readonly Queue<Action> queue=[];public bool Reject,Running;public int WorkerEnqueues;
        public bool TryQueue(Action work){if(Running){WorkerEnqueues++;throw new InvalidOperationException("The scheduler has one UI producer.");}if(Reject)return false;queue.Enqueue(work);return true;}
        public void RunAll(){int count=0;while(queue.TryDequeue(out var work)){Assert.True(++count<20);Running=true;try{work();}finally{Running=false;}}}
    }
    private sealed class Factory(Scheduler scheduler):IInteractiveBrowserFactory
    {
        public BrowserService? Page;public bool ReleaseObservedOnWorker;
        public IInteractiveBrowserSession CreatePageSession()=>Page=new(){Trace=line=>{if(line.StartsWith("XFE_BROWSER_RELEASE|",StringComparison.Ordinal)){Assert.True(scheduler.Running);Assert.Contains("|HandlesAfter|0",line,StringComparison.Ordinal);Assert.Contains("XFE_BROWSER_WEAK_RELEASE|",line,StringComparison.Ordinal);Assert.Contains("|TablesAfter|0|EntriesAfter|0|",line,StringComparison.Ordinal);ReleaseObservedOnWorker=true;}}};
    }
    private sealed class CanvasFactory:IEditorPreparationCanvasFactory{public IDesktopCanvas CreateEditorPreparationCanvas()=>new Canvas();}
    private sealed class Canvas:IDesktopWebCanvas
    {
        public int Width=>796;public int Height=>404;
        public void Fill(uint rgb,int x,int y,int w,int h){}public void Line(uint rgb,int x1,int y1,int x2,int y2){}
        public void Text(string text,uint rgb,int x,int y){}public void Present(){}public void PushClip(DesktopBounds clip){}public void PopClip(){}
        public double MeasureWebText(string text,double size,string family,int weight)=>new BrowserFallbackTextMetrics().Measure(text,new(size,family,weight));
        public (int Width,int Height,byte[] Alpha) RasterizeWebText(string text,double size,string family,int weight)=>(0,0,[]);
        public void WebText(string text,uint color,double x,double y,double size,string family,int weight,double opacity=1){}
    }
}