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

XFESystem

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

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFESystem

Defer Jint function analysis and streamline Monaco initialization

1303a92
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

15 个文件 +679 -38
Modified Source/XFESystem.Browser/BrowserPageResources.cs +4 -0
@@ -62,6 +62,10 @@ public sealed partial class BrowserService
62 62 Trace?.Invoke("XFE_BROWSER_SCRIPT_END|" + (script.GetAttribute("src") ?? "Inline") + "|Success|" + result.Success + "\n");
63 63 Trace?.Invoke("XFE_BROWSER_SCRIPT_TIMING|" + (script.GetAttribute("src") ?? "Inline") +
64 64 "|Ticks|" + (System.Diagnostics.Stopwatch.GetTimestamp() - scriptStarted) +
65 "|PreparationTicks|" + runtime!.LastScriptTiming.PreparationTicks +
66 "|AdmissionTicks|" + runtime.LastScriptTiming.AdmissionTicks +
67 "|ExecutionTicks|" + runtime.LastScriptTiming.ExecutionTicks +
68 "|CacheHit|" + runtime.LastScriptTiming.CacheHit +
65 69 "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
66 70 CheckOperationCancellation();
67 71 if (result.ConsoleOutput.Length != 0) Diagnostic(result.ConsoleOutput);
Modified Source/XFESystem.Browser/BrowserResourceSource.cs +15 -6
@@ -71,8 +71,18 @@ public sealed class PackagedBrowserResources : IBrowserResourceSource
71 71 if(monaco.languages.typescript) monaco.languages.typescript.javascriptDefaults.setModeConfiguration({completionItems:false,hovers:false,documentSymbols:false,
72 72 definitions:false,references:false,documentHighlights:false,rename:false,diagnostics:false,
73 73 documentRangeFormattingEdits:false,documentFormattingEdits:false,codeActions:false,inlayHints:false});
74 const defaultJavaScript='// XFESystem | XFEstudio\nconst answer = 6 * 7;\nconsole.log(answer);';
75 const defaultCSharp='using System;\nnamespace XFEstudio;\npublic sealed class Preview\n{\n public string Message { get; set; } = "Hello XFESystem";\n public static int Run(int value)\n {\n if (value > 0) return Math.Max(value, 42);\n Console.WriteLine("Ready");\n return 0;\n }\n}';
76 const initialLanguage=typeof __xfeMonacoInitialLanguage==='string'?__xfeMonacoInitialLanguage:location.search.indexOf('language=csharp')>=0?'csharp':'javascript';
77 const initialSource=typeof __xfeMonacoInitialSource==='string'?__xfeMonacoInitialSource:initialLanguage==='csharp'?defaultCSharp:defaultJavaScript;
78 delete globalThis.__xfeMonacoInitialSource;delete globalThis.__xfeMonacoInitialLanguage;
79 let initialModel=null;
80 xfeWebsiteHighlight.install(function(){return globalThis.xfeMonacoEditor?xfeMonacoEditor.getModel():initialModel;});
81 const initialTheme=initialLanguage==='csharp'?'xfe-website':'vs-dark';
82 monaco.editor.setTheme(initialTheme);
83 initialModel=monaco.editor.createModel(initialSource,initialLanguage);
74 84 globalThis.xfeMonacoEditor=monaco.editor.create(document.getElementById('Editor'),{
75 value:'// XFESystem | XFEstudio\nconst answer = 6 * 7;\nconsole.log(answer);',language:'javascript',theme:'vs-dark',
85 model:initialModel,theme:initialTheme,
76 86 fontSize:13,lineHeight:20,fontFamily:'Consolas',minimap:{enabled:false},automaticLayout:true,
77 87 accessibilitySupport:'off',renderWhitespace:'none',scrollBeyondLastLine:false,overviewRulerLanes:0,overviewRulerBorder:false,
78 88 wordBasedSuggestions:'off',links:false,colorDecorators:false,codeLens:false,quickSuggestions:false,suggestOnTriggerCharacters:false,
@@ -81,19 +91,18 @@ public sealed class PackagedBrowserResources : IBrowserResourceSource
81 91 guides:{indentation:false,highlightActiveIndentation:false,bracketPairs:false},renderLineHighlight:'none'
82 92 });
83 93 xfeMonacoEditor.getModel().updateOptions({bracketColorizationOptions:{enabled:false,independentColorPoolPerBracketType:false}});
84 document.getElementById('Status').textContent='Official Monaco 0.52.2 - ready';
85 xfeWebsiteHighlight.install(function(){return xfeMonacoEditor.getModel();});
94 const status=function(language){document.getElementById('Status').textContent=language==='csharp'?'Monaco C# - website grammar and palette':language==='plaintext'?'Monaco plain text - ready':'Official Monaco 0.52.2 - ready';};
95 status(initialLanguage);
86 96 globalThis.xfeMonacoSetLanguage=function(language,source){
87 97 if(language!=='csharp' && language!=='javascript' && language!=='plaintext')throw new Error('Unsupported editor language');
88 98 monaco.editor.setModelLanguage(xfeMonacoEditor.getModel(),language);
89 99 monaco.editor.setTheme(language==='csharp'?'xfe-website':'vs-dark');
90 100 if(typeof source==='string')xfeMonacoEditor.setValue(source);
91 else if(!globalThis.xfeMonacoFileMode)xfeMonacoEditor.setValue(language==='csharp'?'using System;\nnamespace XFEstudio;\npublic sealed class Preview\n{\n public string Message { get; set; } = "Hello XFESystem";\n public static int Run(int value)\n {\n if (value > 0) return Math.Max(value, 42);\n Console.WriteLine("Ready");\n return 0;\n }\n}':'// XFESystem | XFEstudio\nconst answer = 6 * 7;\nconsole.log(answer);');
92 document.getElementById('Status').textContent=language==='csharp'?'Monaco C# - website grammar and palette':language==='plaintext'?'Monaco plain text - ready':'Official Monaco 0.52.2 - ready';
101 else if(!globalThis.xfeMonacoFileMode)xfeMonacoEditor.setValue(language==='csharp'?defaultCSharp:defaultJavaScript);
102 status(language);
93 103 };
94 104 document.getElementById('JavaScriptLanguage').addEventListener('click',function(){xfeMonacoSetLanguage('javascript');});
95 105 document.getElementById('CSharpLanguage').addEventListener('click',function(){xfeMonacoSetLanguage('csharp');});
96 if(location.search.indexOf('language=csharp')>=0)xfeMonacoSetLanguage('csharp');
97 106 xfeMonacoEditor.onDidChangeModelContent(function(){document.getElementById('Status').textContent='Monaco - edited '+xfeMonacoEditor.getValue().length+' characters';});
98 107 } catch(error) {document.getElementById('Status').textContent='Monaco failed: '+String(error);console.error(String(error),error.stack);}
99 108 </script></body></html>
Modified Source/XFESystem.Browser/BrowserService.cs +33 -8
@@ -175,15 +175,29 @@ public sealed partial class BrowserService : IJavaScriptPreviewService, IInterac
175 175 CheckOperationCancellation();
176 176 ArgumentNullException.ThrowIfNull(source);
177 177 if (source.Length > 131_072) throw new BrowserLimitException("The Monaco document exceeds 128 KiB.");
178 Navigate(PackagedBrowserResources.MonacoPageAddress);
179 CheckOperationCancellation();
180 178 string language = path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ? "csharp" :
181 179 path.EndsWith(".js", StringComparison.OrdinalIgnoreCase) ? "javascript" : "plaintext";
182 // JSON encoding is a data boundary, never concatenate an unescaped file into JavaScript.
183 var initialized = ExecuteJavaScript("globalThis.xfeMonacoFileMode=true;xfeMonacoSetLanguage(" + JavaScriptString(language) + "," + JavaScriptString(source) + ");xfeMonacoEditor.focus();'ready'");
184 CheckOperationCancellation();
185 if (initialized != "ready") throw new InvalidOperationException("The Monaco editor could not initialize: " + initialized);
186 return RefreshDocument();
180 return WithGate(() =>
181 {
182 try
183 {
184 RecordRemoteResponseEvidence(null, null, false);
185 trustedDocument = false;
186 LoadHtml(new(PackagedBrowserResources.MonacoPageAddress), PackagedBrowserResources.MonacoPage,
187 packagedMonaco: true, editor: new(source, language));
188 CheckOperationCancellation();
189 var initialized = ExecuteJavaScript("xfeMonacoEditor.focus();'ready'");
190 CheckOperationCancellation();
191 if (initialized != "ready") throw new InvalidOperationException("The Monaco editor could not initialize: " + initialized);
192 return RefreshDocument();
193 }
194 catch (OperationCanceledException) when (OperationCancelled) { throw; }
195 catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
196 {
197 NavigationFailed(path, error);
198 throw new IOException("The Monaco editor could not initialize: " + error.Message, error);
199 }
200 });
187 201 }
188 202
189 203 public string? ReadEditorText() => WithGate(() => IsMonacoAddress(currentAddress)
@@ -277,7 +291,10 @@ public sealed partial class BrowserService : IJavaScriptPreviewService, IInterac
277 291 return result;
278 292 }
279 293
280 private BrowserPage LoadHtml(Uri target, string html, string? contentSecurityPolicy = null, bool packagedMonaco = false, bool deferDocumentScripts = false)
294 private sealed record EditorInitialization(string Source, string Language);
295
296 private BrowserPage LoadHtml(Uri target, string html, string? contentSecurityPolicy = null, bool packagedMonaco = false, bool deferDocumentScripts = false,
297 EditorInitialization? editor = null)
281 298 {
282 299 CheckOperationCancellation();
283 300 if (html.Length > limits.MaximumHtmlCharacters)
@@ -307,6 +324,14 @@ public sealed partial class BrowserService : IJavaScriptPreviewService, IInterac
307 324 tracePumps = tracePaints = 0;
308 325 pageDiagnostics = new(activeLimits.MaximumOutputCharacters);
309 326 dom = deferDocumentScripts ? null : new DomBridge(runtime!, document, target, activeLimits, CreateDomHost());
327 if (editor is not null)
328 {
329 // Trusted primitive data, installed only for our packaged editor. No
330 // source interpolation/parsing and no transient default model first.
331 runtime!.Engine.SetValue("__xfeMonacoInitialSource", editor.Source);
332 runtime.Engine.SetValue("__xfeMonacoInitialLanguage", editor.Language);
333 runtime.Engine.SetValue("xfeMonacoFileMode", true);
334 }
310 335 long runtimeTicks = System.Diagnostics.Stopwatch.GetTimestamp() - runtimeStarted;
311 336 Trace?.Invoke("XFE_BROWSER_LOAD_TIMING|HtmlParseTicks|" + htmlTicks + "|RuntimeAndDomTicks|" + runtimeTicks +
312 337 "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
Modified Source/XFESystem.Browser/JavaScriptRuntime.cs +5 -1
@@ -25,6 +25,7 @@ public sealed class JavaScriptRuntime : IDisposable
25 25
26 26 internal long PackagedScriptCacheHits { get; private set; }
27 27 internal long PackagedScriptPreparations { get; private set; }
28 internal (long PreparationTicks, long AdmissionTicks, long ExecutionTicks, bool CacheHit) LastScriptTiming { get; private set; }
28 29
29 30 public JavaScriptRuntime(BrowserLimits? limits = null) : this(limits, PreparedScriptCache.Shared) { }
30 31
@@ -151,14 +152,17 @@ public sealed class JavaScriptRuntime : IDisposable
151 152 Monitor.Enter(gate);
152 153 output.Clear();
153 154 JavaScriptResult evaluation;
155 LastScriptTiming = default;
154 156 try
155 157 {
156 158 JsValue result;
157 159 if (resourceKey is not null && source.Length >= PreparedScriptCache.MinimumSourceCharacters)
158 160 {
159 var prepared = packagedScripts.GetOrPrepare(source, resourceKey, out bool reused);
161 var prepared = packagedScripts.GetOrPrepare(source, resourceKey, out bool reused, out long preparationTicks, out long admissionTicks);
160 162 if (reused) PackagedScriptCacheHits++; else PackagedScriptPreparations++;
163 long started = System.Diagnostics.Stopwatch.GetTimestamp();
161 164 result = engine.Evaluate(prepared);
165 LastScriptTiming = (preparationTicks, admissionTicks, System.Diagnostics.Stopwatch.GetTimestamp() - started, reused);
162 166 }
163 167 else result = engine.Evaluate(source);
164 168 var value = PrimitiveText(result);
Added Source/XFESystem.Browser/PackagedScriptParser.cs +74 -0
@@ -0,0 +1,74 @@
1 using System.Runtime.CompilerServices;
2 using Acornima;
3 using Acornima.Ast;
4 using Jint;
5 using Jint.Native;
6
7 namespace XFESystem.Browser;
8
9 /// <summary>
10 /// Fully validates syntax, while leaving Jint's function/declaration analysis on
11 /// its normal first-use path. Large packages contain many unused function bodies.
12 /// The AST and analysis contain no realm; interpreter handlers remain engine owned.
13 /// </summary>
14 internal static class PackagedScriptParser
15 {
16 // Obtain the pinned engine's own grammar and regexp adapter. Prepared scripts
17 // otherwise default to compiled regex and a ten-second regexp timeout.
18 private static readonly ParserOptions options = Engine.PrepareScript(string.Empty,
19 options: new ScriptPreparationOptions
20 {
21 FoldConstants = false,
22 ParsingOptions = new() { CompileRegex = false, RegexTimeout = TimeSpan.FromMilliseconds(100) }
23 }).ParserOptions! with { OnNode = null };
24
25 internal static Prepared<Script> Parse(string source, string key, int maximumNodes, out int nodes)
26 {
27 var preparation = new SyntaxPreparation(maximumNodes);
28 var parser = new Parser(options with { OnNode = preparation.Visit });
29 var script = parser.ParseScript(source, sourceFile: key, strict: false);
30 nodes = preparation.Count;
31 return PreparedFactory<Script>.Create(script, options);
32 }
33
34 // Jint 4.11.0 exposes evaluation of Prepared<Script>, but its AST wrapper
35 // constructor is internal. This exact signature is pinned by Packages.lock;
36 // UnsafeAccessor emits a direct AOT call, with no reflection or layout cast.
37 private static class PreparedFactory<TProgram> where TProgram : Acornima.Ast.Program
38 {
39 [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
40 internal static extern Prepared<TProgram> Create(TProgram program, ParserOptions parserOptions);
41 }
42
43 // The same immutable identifier memento used by Jint's AstAnalyzer. Only its
44 // name/key is shared; scope resolution and mutable inline caches remain on
45 // each engine's interpreter nodes. This signature also pins Jint 4.11.0.
46 [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
47 [return: UnsafeAccessorType("Jint.Runtime.Environments.Environment+BindingName, Jint")]
48 private static extern object CreateBindingName(string name);
49
50 private sealed class SyntaxPreparation(int maximum)
51 {
52 private readonly Dictionary<string, object> bindingNames = new(StringComparer.Ordinal);
53 private readonly Dictionary<string, JsString> propertyNames = new(StringComparer.Ordinal);
54 internal int Count { get; private set; }
55 internal void Visit(Node node, in OnNodeContext context)
56 {
57 // An over-budget script is still parsed and executed, but not cached.
58 if (Count > maximum) return;
59 Count++;
60 if (node is Identifier identifier)
61 {
62 if (!bindingNames.TryGetValue(identifier.Name, out var binding))
63 bindingNames.Add(identifier.Name, binding = CreateBindingName(identifier.Name));
64 node.UserData = binding;
65 }
66 else if (node is MemberExpression { Computed: false, Property: Identifier property })
67 {
68 if (!propertyNames.TryGetValue(property.Name, out var key))
69 propertyNames.Add(property.Name, key = new JsString(property.Name));
70 node.UserData = key;
71 }
72 }
73 }
74 }
Modified Source/XFESystem.Browser/PreparedScriptCache.cs +13 -17
@@ -30,8 +30,12 @@ internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCh
30 30 }
31 31
32 32 internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused)
33 => GetOrPrepare(source, key, out reused, out _, out _);
34
35 internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused, out long preparationTicks, out long admissionTicks)
33 36 {
34 37 reused = false;
38 preparationTicks = admissionTicks = 0;
35 39 Monitor.Enter(gate);
36 40 var found = Find(key, source);
37 41 if (found is not null)
@@ -42,22 +46,23 @@ internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCh
42 46 }
43 47 Monitor.Exit(gate);
44 48
45 // Parse outside the global cache gate. There is no engine/realm capture.
46 // Disabling constant folding avoids retaining prepared JS literal values;
47 // only syntax/static analysis is reused. Execution constraints are applied
48 // by Engine.Evaluate(prepared) exactly as for Engine.Evaluate(source).
49 var prepared = Engine.PrepareScript(source, key, strict: false,
50 options: new ScriptPreparationOptions { FoldConstants = false });
49 // Parse outside the global cache gate. Jint performs function analysis
50 // only when needed, as in its ordinary Evaluate(source) path. Count nodes
51 // as the parser creates them, rather than traversing the whole AST again.
52 long started = System.Diagnostics.Stopwatch.GetTimestamp();
53 var prepared = PackagedScriptParser.Parse(source, key, maximumNodes, out int count);
54 preparationTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
51 55 if (source.Length > maximumCharacters || key.Length > 2_048 || maximumEntries <= 0) return prepared;
52 int count = CountNodes(prepared.Program!, maximumNodes);
53 56 if (count > maximumNodes) return prepared;
54 57
58 started = System.Diagnostics.Stopwatch.GetTimestamp();
55 59 Monitor.Enter(gate);
56 60 found = Find(key, source);
57 61 if (found is not null)
58 62 {
59 63 reused = true;
60 64 Monitor.Exit(gate);
65 admissionTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
61 66 return found.Script;
62 67 }
63 68 while (entries.Count >= maximumEntries || characters > maximumCharacters - source.Length || nodes > maximumNodes - count)
@@ -65,6 +70,7 @@ internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCh
65 70 entries.Add(new(key, source, prepared, count));
66 71 characters += source.Length; nodes += count;
67 72 Monitor.Exit(gate);
73 admissionTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
68 74 return prepared;
69 75 }
70 76
@@ -85,14 +91,4 @@ internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCh
85 91 var entry = entries[index]; entries.RemoveAt(index);
86 92 characters -= entry.Source.Length; nodes -= entry.Nodes;
87 93 }
88 private static int CountNodes(Node root, int maximum)
89 {
90 var pending = new Stack<Node>(); pending.Push(root); int count = 0;
91 while (pending.TryPop(out var node))
92 {
93 if (++count > maximum) return count;
94 foreach (var child in node.ChildNodes) pending.Push(child);
95 }
96 return count;
97 }
98 94 }
Added Tests/XFESystem.Browser.Tests/MonacoInitializationTests.cs +84 -0
@@ -0,0 +1,84 @@
1 namespace XFESystem.Browser.Tests;
2
3 public sealed class MonacoInitializationTests
4 {
5 [Theory]
6 [InlineData("Code.cs", "csharp")]
7 [InlineData("Script.js", "javascript")]
8 [InlineData("Notes.txt", "plaintext")]
9 public void FileCreatesExactlyOneModelWithFinalTextAndLanguageBeforeEditing(string path, string language)
10 {
11 const string source = "// 中文 </script><script>globalThis.injected=true</script>\nconst text = '\\\"&';";
12 var browser = new BrowserService();
13 try
14 {
15 browser.ResizeViewport(796, 404);
16 browser.NavigateEditor(source, path);
17 Assert.Equal(source, browser.ReadEditorText());
18 Assert.Equal(language, browser.ExecuteJavaScript("xfeMonacoEditor.getModel().getLanguageId()"));
19 Assert.Equal("1,1,undefined,undefined,undefined,true", browser.ExecuteJavaScript("[monaco.editor.getModels().length,xfeMonacoEditor.getModel().getVersionId(),typeof __xfeMonacoInitialSource,typeof __xfeMonacoInitialLanguage,typeof injected,xfeMonacoEditor.hasTextFocus()].join(',')"));
20 browser.ExecuteJavaScript("xfeMonacoSetLanguage('javascript')");
21 Assert.Equal(source, browser.ReadEditorText());
22 browser.Key(new("keydown", "x", "KeyX", "x")); browser.Key(new("keyup", "x", "KeyX"));
23 Assert.Equal("x" + source, browser.ReadEditorText());
24 browser.Navigate("about:Monaco");
25 Assert.Equal("undefined", browser.ExecuteJavaScript("typeof xfeMonacoFileMode"));
26 Assert.Contains("const answer", browser.ReadEditorText());
27 }
28 finally { browser.Release(); }
29 }
30
31 [Fact]
32 public void DirectCSharpNavigationUsesItsRealSampleAndWebsiteTokensAtInitialCreation()
33 {
34 var browser = new BrowserService();
35 try
36 {
37 browser.ResizeViewport(796, 404);
38 browser.Navigate(PackagedBrowserResources.MonacoPageAddress + "?language=csharp");
39 for (int time = 20; time <= 160; time += 20) browser.AdvanceTime(time);
40 Assert.Contains("namespace XFEstudio", browser.ReadEditorText());
41 Assert.Equal("csharp,1,1", browser.ExecuteJavaScript("[xfeMonacoEditor.getModel().getLanguageId(),monaco.editor.getModels().length,xfeMonacoEditor.getModel().getVersionId()].join(',')"));
42 Assert.Contains("ActualMonacoWebsiteCSharpDomReadback", browser.ReadMonacoProof());
43 Assert.DoesNotContain("Monaco failed:", browser.CurrentPage.Text);
44 Assert.True(browser.Document!.QuerySelectorAll(".view-lines .view-line span[class]").Length > 20);
45 }
46 finally { browser.Release(); }
47 }
48
49 [Fact]
50 public void LongCSharpModelReusesOneVersionAcrossAllLinesAndRetokenizesAfterEditing()
51 {
52 string source = "using System;\nclass Example {\n" + string.Concat(Enumerable.Range(0, 256).Select(i => $" public int Value{i} => 42;\n")) + "}";
53 var browser = new BrowserService();
54 try
55 {
56 browser.ResizeViewport(796, 404); browser.NavigateEditor(source, "Long.cs");
57 string result = browser.ExecuteJavaScript("""
58 (function(){
59 const model=xfeMonacoEditor.getModel(), source=model.getValue();
60 const original=model.getValue, highlight=hljs.highlight;
61 let reads=0, highlights=0;
62 model.getValue=function(){reads++;return original.apply(this,arguments);};
63 hljs.highlight=function(){highlights++;return highlight.apply(this,arguments);};
64 try {
65 const lines=monaco.editor.tokenize(source,'csharp');
66 if(lines.length!==259 || reads>1 || highlights>1)throw new Error('Repeated full-model work '+[lines.length,reads,highlights]);
67 const changed=source+'\n// changed';
68 model.setValue(changed);reads=0;highlights=0;
69 const updated=monaco.editor.tokenize(changed,'csharp');
70 if(updated.length!==260 || reads>1 || highlights>1)throw new Error('Repeated changed-model work '+[updated.length,reads,highlights]);
71 const proof=xfeWebsiteHighlight.getProof(xfeMonacoEditor);
72 if(proof.ModelValue!==changed || proof.TokenLines.length!==260 || highlights>1)throw new Error('Stale model or repeated proof highlighting');
73 const fresh=xfeWebsiteHighlight.tokenizeSource(changed);
74 if(JSON.stringify(fresh.lines)!==JSON.stringify(proof.TokenLines))throw new Error('Grammar/context changed');
75 return 'PASS';
76 } finally {model.getValue=original;hljs.highlight=highlight;}
77 })()
78 """);
79 Assert.Equal("PASS", result);
80 Assert.EndsWith("// changed", browser.ReadEditorText());
81 }
82 finally { browser.Release(); }
83 }
84 }
Added Tests/XFESystem.Browser.Tests/PackagedScriptParserTests.cs +62 -0
@@ -0,0 +1,62 @@
1 using Acornima;
2 using Acornima.Ast;
3 using Jint;
4
5 namespace XFESystem.Browser.Tests;
6
7 public sealed class PackagedScriptParserTests
8 {
9 [Theory]
10 [InlineData("function f(a=2,...rest){let x=a;return ()=>++x+rest.length}var g=f(40,1);g()+g()")]
11 [InlineData("class A{#x=40;static count=0;constructor(){A.count++}get value(){return this.#x+A.count}}new A().value+new A().value")]
12 [InlineData("function f(a){arguments[0]=42;return a}f(1)")]
13 [InlineData("'use strict';function f(a){arguments[0]=42;return a}f(1)")]
14 [InlineData("function f(){if(true){function g(){return 42}}return g()}f()")]
15 [InlineData("let x=1;function f({a=x}={}){let x=2;return a}f()")]
16 [InlineData("function* gen(){try{yield 40}finally{yield 2}}Array.from(gen()).join(',')")]
17 [InlineData("var r=/x/g;var a=[r.test('xx'),r.lastIndex];a.join(',')")]
18 [InlineData("var a=[];for(let i=0;i<3;i++)a.push(()=>i);a.map(f=>f()).join(',')")]
19 [InlineData("var value=40;({value}).value+2")]
20 [InlineData("function f(a){try{throw a}finally{a++}}try{f(42)}catch(e){e}")]
21 [InlineData("const f=()=>({get value(){return 42}});f().value")]
22 [InlineData("var a={value:40};a.value++;a['value']++;a?.value")]
23 [InlineData("class A{get value(){return 40}}class B extends A{get value(){return super.value+2}}new B().value")]
24 public void LazyAnalysisMatchesPinnedEagerEngineAcrossFreshEngines(string source)
25 {
26 string expected = new Engine().Evaluate(Engine.PrepareScript(source)).ToString();
27 var prepared = PackagedScriptParser.Parse(source, "package:semantics", 10_000, out int nodes);
28 Assert.True(prepared.IsValid); Assert.InRange(nodes, 1, 10_000);
29 for (int instance = 0; instance < 3; instance++)
30 Assert.Equal(expected, new Engine().Evaluate(prepared).ToString());
31 }
32
33 [Fact]
34 public void UnusedFunctionsAreValidatedButTheirRuntimeAnalysisWaitsForUse()
35 {
36 var prepared = PackagedScriptParser.Parse("function used(a){return a+2}function unused(a){return a*2}used(40)", "package:lazy", 1000, out _);
37 var used = Assert.IsType<FunctionDeclaration>(prepared.Program!.Body[0]);
38 var unused = Assert.IsType<FunctionDeclaration>(prepared.Program.Body[1]);
39 Assert.Null(used.UserData); Assert.Null(unused.UserData);
40 var engine = new Engine(); Assert.Equal("42", engine.Evaluate(prepared).ToString());
41 Assert.NotNull(used.UserData); Assert.Null(unused.UserData);
42 Assert.Equal("42", engine.Evaluate("unused(21)").ToString());
43 Assert.NotNull(unused.UserData);
44 Assert.ThrowsAny<ParseErrorException>(() => PackagedScriptParser.Parse("function unused(){let a;let a;}", "package:early-error", 1000, out _));
45 Assert.ThrowsAny<ParseErrorException>(() => PackagedScriptParser.Parse("function unused(){return /[/;}", "package:bad-regexp", 1000, out _));
46 }
47
48 [Fact]
49 public void ParserRetainsSourceLocationsAndExplicitRegexpTimeoutWithoutCompiledRegex()
50 {
51 var prepared = PackagedScriptParser.Parse("\n /x+/g;", "package:locations", 1000, out _);
52 var statement = Assert.IsAssignableFrom<ExpressionStatement>(prepared.Program!.Body[0]);
53 var literal = Assert.IsType<RegExpLiteral>(statement.Expression);
54 Assert.Null(literal.Value); // Pattern is validated now; conversion is first-use work.
55 var regex = Assert.IsType<Jint.Native.JsRegExp>(new Engine().Evaluate(prepared)).Value;
56 Assert.Equal(TimeSpan.FromMilliseconds(100), regex.MatchTimeout);
57 Assert.False(regex.Options.HasFlag(System.Text.RegularExpressions.RegexOptions.Compiled));
58 Assert.Equal("package:locations", literal.Location.SourceFile);
59 Assert.Equal(2, literal.Location.Start.Line);
60 Assert.Equal(new Version(4, 11, 0, 0), typeof(Engine).Assembly.GetName().Version);
61 }
62 }
Modified Tests/XFESystem.Browser.Tests/WebsiteCodeAssetsTests.cs +2 -0
@@ -13,6 +13,8 @@ public sealed class WebsiteCodeAssetsTests
13 13 Assert.True(resources.TryGet(new(PackagedBrowserResources.WebsiteCodeRoot+"Manifest.json"),out var manifest));
14 14 using var json=JsonDocument.Parse(manifest.Body);
15 15 Assert.False(json.RootElement.GetProperty("WebsiteHasMonaco").GetBoolean());
16 using (var adapter = typeof(PackagedBrowserResources).Assembly.GetManifestResourceStream("WebsiteCode/WebsiteMonaco.js")!)
17 Assert.Equal(json.RootElement.GetProperty("Adapter").GetProperty("Sha256").GetString(), Convert.ToHexStringLower(SHA256.HashData(adapter)));
16 18 foreach(var entry in json.RootElement.GetProperty("Files").EnumerateArray())
17 19 {
18 20 var path=entry.GetProperty("Path").GetString()!;
Added Tools/BrowserCompatibility/PreviewProfile/CompareMonaco.ps1 +56 -0
@@ -0,0 +1,56 @@
1 [CmdletBinding()]
2 param([string]$BaselineRef='1abf972d6338db143c3b83808a83e1756f8ad16c')
3 $ErrorActionPreference='Stop'
4 $taskRoot=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../..'))
5 $output=Join-Path $taskRoot ('build/MonacoInit/HostComparison-'+[DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
6 [void][IO.Directory]::CreateDirectory($output)
7 $baseline=Join-Path $output 'Baseline'
8 $oldTiering=$env:DOTNET_TieredCompilation
9 Push-Location $taskRoot
10 try {
11 $commit=(& git rev-parse ($BaselineRef+'^{commit}')).Trim()
12 if($LASTEXITCODE -ne 0){throw 'The baseline must resolve to a Git commit.'}
13 & git archive --format=zip ('--output='+(Join-Path $output 'Baseline.zip')) $commit
14 if($LASTEXITCODE -ne 0){throw 'Could not export the baseline.'}
15 Expand-Archive -LiteralPath (Join-Path $output 'Baseline.zip') -DestinationPath $baseline
16 $project=Join-Path $output 'BaselineProbe.csproj'
17 $reference=[Security.SecurityElement]::Escape((Join-Path $baseline 'Source/XFESystem.Browser/XFESystem.Browser.csproj'))
18 $profile=[Security.SecurityElement]::Escape((Join-Path $PSScriptRoot 'MonacoProfile.cs'))
19 $canvas=[Security.SecurityElement]::Escape((Join-Path $PSScriptRoot '../ProductionProbe/WindowsTextCanvas.cs'))
20 @"
21 <Project Sdk="Microsoft.NET.Sdk">
22 <PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0-windows</TargetFramework><UseWindowsForms>true</UseWindowsForms><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><EnableDefaultCompileItems>false</EnableDefaultCompileItems></PropertyGroup>
23 <ItemGroup><ProjectReference Include="$reference"/><Compile Include="$profile"/><Compile Include="$canvas"/><Compile Include="Main.cs"/></ItemGroup>
24 </Project>
25 "@ | Set-Content -LiteralPath $project -Encoding UTF8
26 'MonacoProfile.Run(Path.GetFullPath(args[0]), args[1]);' | Set-Content -LiteralPath (Join-Path $output 'Main.cs') -Encoding UTF8
27 # Suppress tier promotion variance; initial JIT still belongs to cold host
28 # startup. The separate native image comparison contains no host JIT.
29 $env:DOTNET_TieredCompilation='0'
30 & dotnet run --project $project -c Release --artifacts-path (Join-Path $output 'BeforeArtifacts') -- $baseline Before *> (Join-Path $output 'Before.log')
31 if($LASTEXITCODE -ne 0){throw ('Baseline Monaco profile failed: '+$output)}
32 & dotnet run --project (Join-Path $PSScriptRoot 'PreviewProfile.csproj') -c Release --artifacts-path (Join-Path $output 'AfterArtifacts') -- $taskRoot After --monaco *> (Join-Path $output 'After.log')
33 if($LASTEXITCODE -ne 0){throw ('Current Monaco profile failed: '+$output)}
34 $beforePath=Get-Content -LiteralPath (Join-Path $output 'Before.log') -Encoding UTF8 | Select-Object -Last 1
35 $afterPath=Get-Content -LiteralPath (Join-Path $output 'After.log') -Encoding UTF8 | Select-Object -Last 1
36 $before=Get-Content -LiteralPath $beforePath -Raw -Encoding UTF8 | ConvertFrom-Json
37 $after=Get-Content -LiteralPath $afterPath -Raw -Encoding UTF8 | ConvertFrom-Json
38 if(!$before.Passed -or !$after.Passed){throw 'A full initialization/editing profile failed.'}
39 $rows=@()
40 foreach($kind in @('ShortCSharp','LongCSharp')) {
41 $old=@($before.Cases | Where-Object {$_.Kind -ceq $kind})
42 $new=@($after.Cases | Where-Object {$_.Kind -ceq $kind})
43 if($old.Count -ne 5 -or $new.Count -ne 5){throw 'Missing repeated cases.'}
44 for($i=0;$i -lt 5;$i++) {
45 if($old[$i].RenderSha256 -cne $new[$i].RenderSha256){throw ('Actual token view changed: '+$kind)}
46 }
47 $oldTime=@($old.FullReadyMilliseconds | Sort-Object)[2];$newTime=@($new.FullReadyMilliseconds | Sort-Object)[2]
48 $rows+=[ordered]@{Kind=$kind;BeforeMedianMilliseconds=$oldTime;AfterMedianMilliseconds=$newTime;Speedup=$oldTime/$newTime;
49 BeforeTokenizationFullModelReads=$old[0].TokenizationWork.FullModelReads;AfterTokenizationFullModelReads=$new[0].TokenizationWork.FullModelReads;
50 BeforeAllocatedBytes=$old[0].AllocatedBytes;AfterAllocatedBytes=$new[0].AllocatedBytes;ActualViewTokensEqual=$true}
51 }
52 [ordered]@{Kind='MonacoFullInitializationHostComparison';Passed=$true;HostOnly=$true;NativeAcceptance=$false;
53 BaselineCommit=$commit;Before=$beforePath;After=$afterPath;Rows=$rows} | ConvertTo-Json -Depth 5 |
54 Set-Content -LiteralPath (Join-Path $output 'Comparison.json') -Encoding UTF8
55 Write-Output ('Host Monaco comparison: '+(Join-Path $output 'Comparison.json'))
56 } finally {$env:DOTNET_TieredCompilation=$oldTiering;Pop-Location}
Added Tools/BrowserCompatibility/PreviewProfile/MonacoProfile.cs +69 -0
@@ -0,0 +1,69 @@
1 using System.Diagnostics;
2 using System.Security.Cryptography;
3 using System.Text;
4 using System.Text.Json;
5 using XFESystem.Browser;
6
7 // Cold and warm full editor initialization, including real view text and paint.
8 // Editing checks are outside the timed interval. No pre-warming or host browser.
9 internal static class MonacoProfile
10 {
11 internal static void Run(string root, string label)
12 {
13 string output = Path.Combine(root, "build/MonacoInit", label + "-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
14 Directory.CreateDirectory(output);
15 var inputs = new[] { "Source/XFESystem.Browser", "assets/Browser/WebsiteCode", "assets/Browser/Monaco/0.52.2/Core" }
16 .SelectMany(directory => Directory.EnumerateFiles(Path.Combine(root, directory)))
17 .Where(path => Path.GetExtension(path) is ".cs" or ".js" or ".css" or ".json")
18 .ToDictionary(path => Path.GetRelativePath(root, path).Replace('\\', '/'), Hash);
19 const string shortSource = "using System;\nnamespace XFE;\npublic class Test { int value = 42; }";
20 string longSource = "using System;\nclass Example {\n" + string.Concat(Enumerable.Range(0, 256).Select(i => $" public int Value{i} => 42;\n")) + "}";
21 var cases = new List<object>();
22 bool passed = true;
23 foreach (var (kind, csharp) in new[] { ("ShortCSharp", shortSource), ("LongCSharp", longSource) })
24 for (int attempt = 1; attempt <= 5; attempt++)
25 {
26 using var canvas = new WindowsTextCanvas(Path.Combine(root, "assets/fonts/cascadia-mono-2407.24/CascadiaMono-Regular.ttf"), 796, 404);
27 var timer = Stopwatch.StartNew();
28 var trace = new List<object>();
29 var browser = new BrowserService { Trace = line => trace.Add(new { Ticks = timer.ElapsedTicks, Line = line.TrimEnd() }) };
30 browser.SetCanvas(canvas); browser.ResizeViewport(796, 404);
31 long allocated = GC.GetTotalAllocatedBytes();
32 timer.Restart();
33 browser.NavigateEditor(csharp, "Test.cs");
34 double navigationMs = timer.Elapsed.TotalMilliseconds;
35 for (int frame = 1; frame <= 8; frame++) browser.AdvanceTime(frame * 20);
36 string proof = browser.ExecuteJavaScript("xfeMonacoEditor.render(true);JSON.stringify({text: Array.from(document.querySelectorAll('.view-lines .view-line')).map(e=>e.textContent.replace(/\\u00a0/g,' ')),language:xfeMonacoEditor.getModel().getLanguageId(),version:xfeMonacoEditor.getModel().getVersionId(),value:xfeMonacoEditor.getValue()})");
37 browser.Paint(canvas, new(0, 0, 796, 404), 0); browser.FramePresented();
38 double readyMs = timer.Elapsed.TotalMilliseconds;
39 long bytes = GC.GetTotalAllocatedBytes() - allocated;
40 using var parsed = JsonDocument.Parse(proof);
41 var data = parsed.RootElement;
42 string[] visible = data.GetProperty("text").EnumerateArray().Select(e => e.GetString()!.TrimEnd()).ToArray();
43 bool ready = data.GetProperty("value").GetString() == csharp && data.GetProperty("language").GetString() == "csharp" &&
44 visible.Length >= 3 && visible.SequenceEqual(csharp.Split('\n').Take(visible.Length));
45 string renderHash = HashText(browser.ExecuteJavaScript("JSON.stringify(Array.from(document.querySelectorAll('.view-lines .view-line')).map(e=>e.innerHTML))"));
46 string tokenWork = browser.ExecuteJavaScript("(function(){const model=xfeMonacoEditor.getModel(),value=model.getValue(),original=model.getValue;let reads=0;model.getValue=function(){reads++;return original.apply(this,arguments)};try{const lines=monaco.editor.tokenize(value,'csharp');return JSON.stringify({Lines:lines.length,FullModelReads:reads})}finally{model.getValue=original}})()");
47 browser.ExecuteJavaScript("xfeMonacoEditor.focus();xfeMonacoEditor.setPosition({lineNumber:1,column:1});'ready'");
48 browser.Key(new("keydown", "x", "KeyX", "x")); browser.Key(new("keyup", "x", "KeyX"));
49 bool edited = browser.ReadEditorText() == "x" + csharp;
50 canvas.SavePng(Path.Combine(output, kind + "-" + attempt + ".png"));
51 using var work = JsonDocument.Parse(tokenWork);
52 var row = new { Kind = kind, Attempt = attempt, Cold = kind == "ShortCSharp" && attempt == 1, Passed = ready && edited, NavigationMilliseconds = navigationMs,
53 FullReadyMilliseconds = readyMs, AllocatedBytes = bytes, browser.PackagedScriptCacheHits, browser.PackagedScriptPreparations,
54 RenderSha256 = renderHash, Proof = data.Clone(), TokenizationWork = work.RootElement.Clone(), Trace = trace };
55 passed &= row.Passed;
56 cases.Add(row);
57 Console.WriteLine(JsonSerializer.Serialize(new { row.Kind, row.Attempt, row.Passed, navigationMs, readyMs, bytes, renderHash, row.TokenizationWork }));
58 browser.Release();
59 }
60 bool unchanged = inputs.All(pair => Hash(Path.Combine(root, pair.Key)) == pair.Value);
61 string result = Path.Combine(output, "Result.json");
62 File.WriteAllText(result, JsonSerializer.Serialize(new { Kind = "MonacoFullInitializationHostProfile", Passed = passed && unchanged,
63 HostOnly = true, NativeAcceptance = false, Frequency = Stopwatch.Frequency, Sources = inputs, Cases = cases }, new JsonSerializerOptions { WriteIndented = true }));
64 Console.WriteLine(result);
65 if (!passed || !unchanged) throw new IOException("The full Monaco initialization profile failed.");
66 }
67 private static string Hash(string path) => Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path)));
68 private static string HashText(string value) => Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(value)));
69 }
Modified Tools/BrowserCompatibility/PreviewProfile/Program.cs +1 -0
@@ -11,6 +11,7 @@ using XFESystem.Documents;
11 11 var root = Path.GetFullPath(args.Length > 0 ? args[0] : ".");
12 12 var label = args.Length > 1 ? args[1] : "Current";
13 13 if (args.Contains("--loading", StringComparer.Ordinal)) { LoadingProfile.Run(root, label); return; }
14 if (args.Contains("--monaco", StringComparer.Ordinal)) { MonacoProfile.Run(root, label); return; }
14 15 var output = Path.Combine(root, "build/PreviewPerformance", label + "-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
15 16 Directory.CreateDirectory(output);
16 17 var inputs = new[] { "Source/XFESystem.Browser", "Source/XFESystem.Documents" }
Added Tools/IndependentKernel/TestMonacoStartup.py +234 -0
Modified assets/Browser/WebsiteCode/Manifest.json +1 -1
Modified assets/Browser/WebsiteCode/WebsiteMonaco.js +26 -5