返回提交历史
Modified
Source/XFESystem.Browser/BrowserPageResources.cs
+4
-0
Modified
Source/XFESystem.Browser/BrowserResourceSource.cs
+15
-6
Modified
Source/XFESystem.Browser/BrowserService.cs
+33
-8
Modified
Source/XFESystem.Browser/JavaScriptRuntime.cs
+5
-1
Added
Source/XFESystem.Browser/PackagedScriptParser.cs
+74
-0
Modified
Source/XFESystem.Browser/PreparedScriptCache.cs
+13
-17
Added
Tests/XFESystem.Browser.Tests/MonacoInitializationTests.cs
+84
-0
Added
Tests/XFESystem.Browser.Tests/PackagedScriptParserTests.cs
+62
-0
Modified
Tests/XFESystem.Browser.Tests/WebsiteCodeAssetsTests.cs
+2
-0
Added
Tools/BrowserCompatibility/PreviewProfile/CompareMonaco.ps1
+56
-0
Added
Tools/BrowserCompatibility/PreviewProfile/MonacoProfile.cs
+69
-0
Modified
Tools/BrowserCompatibility/PreviewProfile/Program.cs
+1
-0
Added
Tools/IndependentKernel/TestMonacoStartup.py
+234
-0
Modified
assets/Browser/WebsiteCode/Manifest.json
+1
-1
Modified
assets/Browser/WebsiteCode/WebsiteMonaco.js
+26
-5
XFEstudio/XFESystem
Defer Jint function analysis and streamline Monaco initialization
1303a92
代码差异
15 个文件
+679
-38
@@ -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);
@@ -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>
@@ -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");
@@ -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);
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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()!;
@@ -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}
@@ -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
}
@@ -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" }