返回提交历史
Modified
Source/XFESystem.Browser/BrowserCssCascade.cs
+48
-16
Modified
Source/XFESystem.Browser/BrowserInteractivePage.cs
+5
-2
Modified
Source/XFESystem.Browser/CssComputedStyle.cs
+8
-3
Added
Source/XFESystem.Browser/CssPropertyMap.cs
+22
-0
Modified
Source/XFESystem.Browser/DomBridge.cs
+9
-2
Modified
THIRD_PARTY.md
+1
-1
Added
Tests/XFESystem.Browser.Tests/CssPropertySharingTests.cs
+46
-0
Added
Tests/XFESystem.JintCleanupPatch.Tests/RegexSearchTests.cs
+68
-0
Modified
Tools/BrowserCompatibility/PreviewProfile/CompareMonaco.ps1
+11
-5
Modified
Tools/BrowserCompatibility/PreviewProfile/MonacoProfile.cs
+26
-6
Added
Tools/BrowserCompatibility/PreviewProfile/MonacoRegexProfile.cs
+44
-0
Modified
Tools/BrowserCompatibility/PreviewProfile/Program.cs
+1
-1
Modified
Tools/IndependentKernel/TestMonacoStartup.py
+43
-5
Modified
Tools/JintCleanupPatch/BoundTargets.cs
+12
-1
Modified
Tools/JintCleanupPatch/Program.cs
+1
-0
Added
Tools/JintCleanupPatch/RegexSearch.cs
+92
-0
XFEstudio/XFESystem
Reduce Monaco regex rescanning and theme inheritance allocations
653f059
代码差异
16 个文件
+437
-42
@@ -225,8 +225,8 @@ internal sealed class BrowserCssCascade
225
225
bool initialFontSize = values.ContainsKey("font-size");
226
226
if (parent is not null)
227
227
{
228
foreach (var pair in parent.Properties)
229
if ((Inherited.Contains(pair.Key) || pair.Key.StartsWith("--", StringComparison.Ordinal)) && !values.ContainsKey(pair.Key)) values[pair.Key] = pair.Value;
228
foreach (var pair in parent.OwnProperties)
229
if (Inherited.Contains(pair.Key) && !values.ContainsKey(pair.Key)) values[pair.Key] = pair.Value;
230
230
if (!initialFontSize) values["font-size"] = CssValue.Pixels(parent.Font.Size);
231
231
if (values.TryGetValue("line-height", out string? inheritedLine) && inheritedLine != "normal" &&
232
232
!double.IsFinite(CssValue.Number(inheritedLine, double.NaN))) values["line-height"] = CssValue.Pixels(parent.LineHeight);
@@ -251,15 +251,14 @@ internal sealed class BrowserCssCascade
251
251
if (value is "inherit" or "unset" && (value == "inherit" || Inherited.Contains(pair.Key)))
252
252
value = parent is null ? "initial" : pair.Key == "font-size" ? CssValue.Pixels(parent.Font.Size) :
253
253
pair.Key == "line-height" && !double.IsFinite(CssValue.Number(parent.Get("line-height", "normal"), double.NaN)) && parent.Get("line-height", "normal") != "normal" ? CssValue.Pixels(parent.LineHeight) : parent.GetPropertyValue(pair.Key);
254
if (value == "initial" || value == "unset") values.Remove(pair.Key);
254
if (value == "initial" || value == "unset")
255
{
256
if (pair.Key.StartsWith("--", StringComparison.Ordinal)) values[pair.Key] = value;
257
else values.Remove(pair.Key);
258
}
255
259
else values[pair.Key] = value;
256
260
}
257
foreach (string key in values.Keys.ToArray())
258
{
259
string? resolved = Variables(values[key], values, 0);
260
if (resolved is null) values.Remove(key); else values[key] = resolved;
261
}
262
return new(values, parent, viewport);
261
return CompleteStyle(values, parent, parent?.CustomProperties);
263
262
}
264
263
265
264
public CssComputedStyle ComputeMarker(IElement element, CssComputedStyle style)=>ComputePseudo(element,"marker",style)??style;
@@ -280,8 +279,8 @@ internal sealed class BrowserCssCascade
280
279
// Content is not inherited into this values map, so without a winning
281
280
// content declaration the result below would always be "normal"/null.
282
281
if (pseudo!="marker" && !winning.ContainsKey("content")) return null;
283
var values = pseudo=="marker"?new Dictionary<string, string>(style.Properties, StringComparer.OrdinalIgnoreCase):
284
style.Properties.Where(pair=>Inherited.Contains(pair.Key)||pair.Key.StartsWith("--",StringComparison.Ordinal)).ToDictionary(pair=>pair.Key,pair=>pair.Value,StringComparer.OrdinalIgnoreCase);
282
var values = pseudo=="marker"?new Dictionary<string, string>(style.OwnProperties, StringComparer.OrdinalIgnoreCase):
283
style.OwnProperties.Where(pair=>Inherited.Contains(pair.Key)).ToDictionary(pair=>pair.Key,pair=>pair.Value,StringComparer.OrdinalIgnoreCase);
285
284
if(pseudo!="marker")values["display"]="inline";
286
285
values["font-size"] = CssValue.Pixels(style.Font.Size);
287
286
if(values.TryGetValue("line-height",out string? line)&&line!="normal"&&!double.IsFinite(CssValue.Number(line,double.NaN)))values["line-height"]=CssValue.Pixels(style.LineHeight);
@@ -289,12 +288,45 @@ internal sealed class BrowserCssCascade
289
288
{
290
289
string value=pair.Value.Value;
291
290
if(value is "inherit" or "unset" &&(value=="inherit"||Inherited.Contains(pair.Key)))value=pair.Key=="font-size"?CssValue.Pixels(style.Font.Size):style.GetPropertyValue(pair.Key);
292
if(value is "initial" or "unset")values.Remove(pair.Key);else values[pair.Key]=value;
291
if(value is "initial" or "unset")
292
{ if(pair.Key.StartsWith("--",StringComparison.Ordinal))values[pair.Key]=value;else values.Remove(pair.Key); }
293
else values[pair.Key]=value;
293
294
}
295
var computed = CompleteStyle(values, style, style.CustomProperties);
296
if(pseudo!="marker"&&computed.Get("content","normal") is "normal" or "none")return null;
297
return computed;
298
}
299
300
private static readonly IReadOnlyDictionary<string, string> EmptyCustomProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
301
private CssComputedStyle CompleteStyle(Dictionary<string, string> values, CssComputedStyle? parent,
302
IReadOnlyDictionary<string, string>? inherited)
303
{
304
inherited ??= EmptyCustomProperties;
305
Dictionary<string, string>? custom = null;
306
foreach (string key in values.Keys.ToArray())
307
{
308
if (!key.StartsWith("--", StringComparison.Ordinal)) continue;
309
custom ??= new(inherited, StringComparer.OrdinalIgnoreCase);
310
string value = values[key];
311
if (value is "initial" or "unset") custom.Remove(key); else custom[key] = value;
312
values.Remove(key);
313
}
314
// Only a declaring element owns a mutable copy. Descendants and pseudo
315
// elements can safely reuse the resolved environment; no parent map is
316
// ever edited, including across subsequent layouts and page sessions.
317
var properties = new CssPropertyMap(values, custom ?? inherited);
318
if (custom is not null)
319
foreach (string key in custom.Keys.ToArray())
320
{
321
string? resolved = Variables(custom[key], properties, 0);
322
if (resolved is null) custom.Remove(key); else custom[key] = resolved;
323
}
294
324
foreach (string key in values.Keys.ToArray())
295
{ string? resolved = Variables(values[key], values, 0); if (resolved is null) values.Remove(key); else values[key] = resolved; }
296
if(pseudo!="marker"&&values.GetValueOrDefault("content","normal") is "normal" or "none")return null;
297
return new(values, style, viewport);
325
{
326
string? resolved = Variables(values[key], properties, 0);
327
if (resolved is null) values.Remove(key); else values[key] = resolved;
328
}
329
return new(values, parent, viewport, custom ?? inherited);
298
330
}
299
331
300
332
private void Apply(List<Declaration> source, bool inline, int specificity, int ruleOrder, Dictionary<string, Winner> winners)
@@ -529,7 +561,7 @@ internal sealed class BrowserCssCascade
529
561
if (tag is "strong" or "b" or "h1" or "h2" or "h3") values["font-weight"] = "700";
530
562
return values;
531
563
}
532
private static string? Variables(string value, Dictionary<string, string> values, int depth)
564
private static string? Variables(string value, IReadOnlyDictionary<string, string> values, int depth)
533
565
{
534
566
if (depth > 8) return null;
535
567
int start = value.IndexOf("var(", StringComparison.Ordinal); if (start < 0) return value;
@@ -18,6 +18,7 @@ public sealed partial class BrowserService
18
18
internal BrowserViewport BackgroundViewport => viewport;
19
19
public long Revision { get; private set; }
20
20
public long LayoutPassCount { get; private set; }
21
internal long LayoutTicks { get; private set; }
21
22
public long ScrollLayoutReuseCount { get; private set; }
22
23
public long RasterPaintCount { get; private set; }
23
24
public long CachedPaintCount { get; private set; }
@@ -105,13 +106,15 @@ public sealed partial class BrowserService
105
106
if (layoutDirty || displayList is null)
106
107
{
107
108
LayoutPassCount++;
108
long layoutStarted = awaitingDocumentPresentation ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
109
long layoutStarted = System.Diagnostics.Stopwatch.GetTimestamp();
109
110
displayList = layoutEngine.Layout(document, viewport,
110
111
linkedStyles.Select(item => new BrowserStyleSheet(item.Css, item.Owner)).ToArray(), textMetrics,
111
112
dom is null ? null : dom.NodeIdentity, cspBlocksAuthorStyles, new(hoveredNode, pointerDownNode, dom?.FocusedNode));
113
long elapsed = System.Diagnostics.Stopwatch.GetTimestamp() - layoutStarted;
114
LayoutTicks += elapsed;
112
115
if (awaitingDocumentPresentation)
113
116
{
114
documentLayoutTicks += System.Diagnostics.Stopwatch.GetTimestamp() - layoutStarted;
117
documentLayoutTicks += elapsed;
115
118
documentStylesheetParseTicks += layoutEngine.LastStylesheetParseTicks;
116
119
}
117
120
layoutDirty = false;
@@ -6,9 +6,12 @@ public readonly record struct CssColor(uint Rgb, double Alpha = 1);
6
6
7
7
public sealed class CssComputedStyle
8
8
{
9
internal CssComputedStyle(Dictionary<string, string> properties, CssComputedStyle? parent, BrowserViewport viewport)
9
internal CssComputedStyle(Dictionary<string, string> properties, CssComputedStyle? parent, BrowserViewport viewport,
10
IReadOnlyDictionary<string, string> customProperties)
10
11
{
11
12
values = properties;
13
CustomProperties = customProperties;
14
Properties = new CssPropertyMap(properties, customProperties);
12
15
double inheritedSize = parent?.Font.Size ?? 16;
13
16
double fontSize = CssValue.Length(Get("font-size", inheritedSize.ToString(CultureInfo.InvariantCulture) + "px"), inheritedSize, inheritedSize, viewport, 16) ?? inheritedSize;
14
17
Font = new(Math.Clamp(fontSize, 1, 256), Get("font-family", parent?.Font.Family ?? "monospace").Trim(' ', '\'', '"'),
@@ -25,7 +28,9 @@ public sealed class CssComputedStyle
25
28
private readonly Dictionary<string, string> values;
26
29
private readonly Dictionary<string, string> used = new(StringComparer.OrdinalIgnoreCase);
27
30
internal BrowserViewport Viewport { get; }
28
public IReadOnlyDictionary<string, string> Properties => values;
31
internal IReadOnlyDictionary<string, string> OwnProperties => values;
32
internal IReadOnlyDictionary<string, string> CustomProperties { get; }
33
public IReadOnlyDictionary<string, string> Properties { get; }
29
34
public BrowserFont Font { get; }
30
35
public CssColor Color { get; }
31
36
public CssColor Background { get; }
@@ -41,7 +46,7 @@ public sealed class CssComputedStyle
41
46
public double Opacity => Math.Clamp(CssValue.Number(Get("opacity", "1"), 1), 0, 1);
42
47
public int ZIndex => (int)Math.Clamp(CssValue.Number(Get("z-index", "0"), 0), -1000000, 1000000);
43
48
public string GetPropertyValue(string name) => used.TryGetValue(name, out string? resolved) ? resolved : Get(name, "");
44
internal string Get(string name, string fallback) => values.TryGetValue(name, out string? value) ? value : fallback;
49
internal string Get(string name, string fallback) => Properties.TryGetValue(name, out string? value) ? value : fallback;
45
50
internal double? Length(string name, double basis) => CssValue.Length(Get(name, "auto"), basis, Font.Size, Viewport, 16);
46
51
internal BrowserEdges Edges(string prefix, double basis, bool border = false)
47
52
{
@@ -0,0 +1,22 @@
1
using System.Collections;
2
3
namespace XFESystem.Browser;
4
5
/// <summary>A computed declaration map with a shared, immutable custom-property
6
/// environment. Descendants without overrides do not copy the whole theme.</summary>
7
internal sealed class CssPropertyMap(IReadOnlyDictionary<string, string> own, IReadOnlyDictionary<string, string> custom)
8
: IReadOnlyDictionary<string, string>
9
{
10
public int Count => own.Count + custom.Count;
11
public IEnumerable<string> Keys => own.Keys.Concat(custom.Keys);
12
public IEnumerable<string> Values => own.Values.Concat(custom.Values);
13
public string this[string key] => TryGetValue(key, out var value) ? value : throw new KeyNotFoundException();
14
public bool ContainsKey(string key) => own.ContainsKey(key) || custom.ContainsKey(key);
15
public bool TryGetValue(string key, out string value) => own.TryGetValue(key, out value!) || custom.TryGetValue(key, out value!);
16
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
17
{
18
foreach (var pair in own) yield return pair;
19
foreach (var pair in custom) yield return pair;
20
}
21
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
22
}
@@ -24,6 +24,9 @@ public sealed partial class DomBridge
24
24
private int listenerCount;
25
25
private int nextNodeId;
26
26
private string readyState = "loading";
27
internal long BudgetValidationCount { get; private set; }
28
internal long BudgetValidationTicks { get; private set; }
29
internal long BudgetNodeVisits { get; private set; }
27
30
28
31
public DomBridge(JavaScriptRuntime runtime, IDocument document, Uri address, BrowserLimits limits, BrowserDomHost? host = null)
29
32
{
@@ -192,13 +195,16 @@ public sealed partial class DomBridge
192
195
private static IEnumerable<IElement> Descendants(INode node) { foreach (var child in node.ChildNodes) { if (child is IElement element) yield return element; foreach (var descendant in Descendants(child)) yield return descendant; } }
193
196
private void ValidateBudget(IEnumerable<INode>? additionalDetachedRoots = null)
194
197
{
198
long started = System.Diagnostics.Stopwatch.GetTimestamp();
199
BudgetValidationCount++;
195
200
// Dirty form values are not HTML attributes/text nodes and need their own combined budget.
196
201
long formCharacters = 0;
197
202
foreach (var element in wrappers.Keys.OfType<IElement>())
198
203
if (element is AngleSharp.Html.Dom.IHtmlInputElement or AngleSharp.Html.Dom.IHtmlTextAreaElement)
199
204
if ((formCharacters += FormValue(element).Length) > limits.MaximumHtmlCharacters)
200
205
throw new BrowserLimitException("The editable controls exceed their combined character limit.");
201
ValidateNodes(document, limits, wrappers.Keys.Where(node => node != document && node.Parent is null).Concat(additionalDetachedRoots ?? []), formCharacters);
206
BudgetNodeVisits += ValidateNodes(document, limits, wrappers.Keys.Where(node => node != document && node.Parent is null).Concat(additionalDetachedRoots ?? []), formCharacters);
207
BudgetValidationTicks += System.Diagnostics.Stopwatch.GetTimestamp() - started;
202
208
}
203
209
private void Function(ObjectInstance target, string name, Func<JsValue[], JsValue> callback) => target.DefineOwnProperty(name, new PropertyDescriptor(new ClrFunction(engine, name, (_, args) => callback(args)), false, true, false));
204
210
private static void Data(ObjectInstance target, string name, JsValue value, bool writable = false) => target.DefineOwnProperty(name, new PropertyDescriptor(value, writable, true, false));
@@ -219,7 +225,7 @@ public sealed partial class DomBridge
219
225
private static JsValue Text(string? value) => value is null ? JsValue.Null : value;
220
226
private static bool Bool(JsValue value) => !value.IsNull() && !value.IsUndefined() && (!value.IsBoolean() || value.AsBoolean()) && (!value.IsNumber() || value.AsNumber() != 0) && (!value.IsString() || value.AsString().Length != 0);
221
227
internal static void ValidateDocument(IDocument document, BrowserLimits limits) => ValidateNodes(document, limits, []);
222
private static void ValidateNodes(IDocument document, BrowserLimits limits, IEnumerable<INode> detachedRoots, long additionalCharacters = 0)
228
private static int ValidateNodes(IDocument document, BrowserLimits limits, IEnumerable<INode> detachedRoots, long additionalCharacters = 0)
223
229
{
224
230
var pending = new Stack<(INode Node, int Depth)>(); pending.Push((document, 0)); foreach (var node in detachedRoots) pending.Push((node, 0));
225
231
var count = 0; var characters = additionalCharacters;
@@ -231,5 +237,6 @@ public sealed partial class DomBridge
231
237
if (characters > limits.MaximumHtmlCharacters) throw new BrowserLimitException("The document exceeds its DOM text limit.");
232
238
foreach (var child in entry.Node.ChildNodes) pending.Push((child, entry.Depth + 1));
233
239
}
240
return count;
234
241
}
235
242
}
@@ -54,7 +54,7 @@ QEMU 安装包按发布站 SHA512 验证后仅解包;Limine/LLVM 和固件下
54
54
55
55
`assets/Browser/WebsiteCode/` 原字节保留网站 `highlight.min.js`、自定义 `csharp.js`、`code-style.css` 和网站许可证。原站使用 **highlight.js,并未使用 Monaco**。网站 `csharp.js` 保留原 Highlight.js C# grammar 的作者/贡献者声明,自家符号状态与语法增强属于改编,不能把整份 grammar 说成全新自研。`CodePalette.js` 是从原 CSS 生成的派生调色板,`WebsiteMonaco.js` 是 XFESystem 新增的显式适配器;来源提交、原字节、派生输出及许可边界见该目录 `Manifest.json` 和 `Readme.md`。发行包同时提供 Highlight.js 完整 BSD-3-Clause 文本;同源语法与颜色不意味着编辑界面或网页逐像素相同。
56
56
57
独立应用的 Jint 输出副本仍按 `Tools/JintCleanupPatch/HostJint.targets` 应用绑定调用与 OwnedWeakCollections 适配,补丁在项目私有输出内,并未改变全局 NuGet 包。补丁工具使用 Mono.Cecil 0.11.6(MIT);其锁文件/许可证位于 `Tools/JintCleanupPatch` 与旧收集清单。旧 Cosmos 专用 finally/GC/EH 清理补丁不构成独立 Microsoft 运行时的实现。Monaco、原网站 JS/CSS 仍保留其来源与原字节。
57
独立应用的 Jint 输出副本仍按 `Tools/JintCleanupPatch/HostJint.targets` 应用绑定调用与 OwnedWeakCollections 适配,另对自定义正则引擎增加按实例保存的、最多 128 KiB 输入的非粘连失败后缀复用;匹配语义与原超时路径保留,派生 DLL 的回执记录该补丁版本。补丁在项目私有输出内,并未改变全局 NuGet 包。补丁工具使用 Mono.Cecil 0.11.6(MIT);其锁文件/许可证位于 `Tools/JintCleanupPatch` 与旧收集清单。旧 Cosmos 专用 finally/GC/EH 清理补丁不构成独立 Microsoft 运行时的实现。Monaco、原网站 JS/CSS 仍保留其来源与原字节;未移植 V8 JIT。
58
58
59
59
独立 NativeApplication 另显式导入 [TlsEntropyPatch](Tools/TlsEntropyPatch/README.md),同样使用固定 Mono.Cecil 0.11.6。它只在应用私有 BC 输出中把 12 个 BCL RNG 调用接入真实 `xfe_runtime_random_fill` 适配库,完整保留原 BC 的 MIT 与版权,并记录派生 DLL/源码/适配库哈希。原 NuGet 和普通宿主 TLS 不改;不伪造 OpenSSL 初始化,不改证书/TLS 算法或以时间种子回退。许可证清单同时绑定该补丁工具锁,真正裸机握手仍须另看同一镜像的验收。
60
60
@@ -0,0 +1,46 @@
1
using AngleSharp.Dom;
2
using AngleSharp.Html.Parser;
3
4
namespace XFESystem.Browser.Tests;
5
6
public sealed class CssPropertySharingTests
7
{
8
[Fact]
9
public void LargeInheritedThemeIsSharedButOverridesAndOldLayoutsStayIsolated()
10
{
11
string theme = string.Concat(Enumerable.Range(0, 500).Select(i => $"--theme-{i}:#{i:X6};"));
12
var page = new HtmlParser().ParseDocument($"<style>:root{{{theme}--ink:#123456;--label:'Theme'}}span{{color:var(--ink)}}#Override{{--ink:#654321}}#A::before{{content:var(--label);color:var(--ink)}}</style><div id=Parent><span id=A>A</span><span id=Override><b id=Child>B</b></span><span id=C>C</span></div>");
13
var engine = new BrowserLayoutEngine();
14
var before = engine.Layout(page, new(800, 600));
15
CssComputedStyle Style(BrowserDisplayList list, string id) => list.GetComputedStyle(page.GetElementById(id)!)!;
16
var inherited = Style(before, "Parent").CustomProperties;
17
Assert.Same(inherited, Style(before, "A").CustomProperties);
18
Assert.Same(inherited, Style(before, "C").CustomProperties);
19
Assert.NotSame(inherited, Style(before, "Override").CustomProperties);
20
Assert.Same(Style(before, "Override").CustomProperties, Style(before, "Child").CustomProperties);
21
Assert.Equal(502, inherited.Count);
22
Assert.Equal("#123456", Style(before, "A").GetPropertyValue("--ink"));
23
Assert.Equal("rgb(101, 67, 33)", Style(before, "Child").GetPropertyValue("color"));
24
Assert.Contains(before.Commands, command => command.Text == "Theme" && command.Color == 0x123456);
25
page.GetElementById("Parent")!.SetAttribute("style", "--ink:#abcdef");
26
var after = engine.Layout(page, new(800, 600));
27
Assert.Equal("#123456", Style(before, "A").GetPropertyValue("--ink"));
28
Assert.Equal("#abcdef", Style(after, "A").GetPropertyValue("--ink"));
29
Assert.Equal("#654321", Style(after, "Override").GetPropertyValue("--ink"));
30
}
31
32
[Fact]
33
public void ResetInvalidAndInheritedCustomPropertiesDoNotMutateSiblings()
34
{
35
var page = new HtmlParser().ParseDocument("<style>:root{--ink:#123456;--bad:var(--missing)}#A{--ink:initial;color:var(--ink,red)}#B{--ink:inherit;color:var(--ink)}</style><i id=A>A</i><i id=B>B</i><i id=C>C</i>");
36
var layout = new BrowserLayoutEngine().Layout(page, new(800, 600));
37
var a = layout.GetComputedStyle(page.GetElementById("A")!)!;
38
var b = layout.GetComputedStyle(page.GetElementById("B")!)!;
39
var c = layout.GetComputedStyle(page.GetElementById("C")!)!;
40
Assert.Equal("rgb(255, 0, 0)", a.GetPropertyValue("color"));
41
Assert.False(a.Properties.ContainsKey("--ink"));
42
Assert.False(c.Properties.ContainsKey("--bad"));
43
Assert.Equal("#123456", b.GetPropertyValue("--ink"));
44
Assert.Equal("#123456", c.GetPropertyValue("--ink"));
45
}
46
}
@@ -0,0 +1,68 @@
1
using System.Reflection;
2
using Jint;
3
using Jint.Native;
4
using XFESystem.Collections;
5
6
namespace XFESystem.JintCleanupPatch.Tests;
7
8
public sealed class RegexSearchTests
9
{
10
private static long Hits(JsValue value) => (long)typeof(JsRegExp).GetField("_xfeNoMatchHits", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(value)!;
11
private static Engine Create() => new(options => { options.DisableStringCompilation(); options.Constraints.RegexTimeout = TimeSpan.FromMilliseconds(50); });
12
13
[Fact]
14
public void RepeatedFailedSuffixSkipsSearchButEarlierStartStillFindsMatches()
15
{
16
var engine = Create();
17
try
18
{
19
engine.Execute("const input='baaa',r=/b/gu;r.lastIndex=1;r.exec(input);r.lastIndex=2;r.exec(input)");
20
Assert.Equal(1, Hits(engine.Evaluate("r")));
21
Assert.Equal("b,0,1", engine.Evaluate("r.lastIndex=0;const m=r.exec(input);[m[0],m.index,r.lastIndex].join(',')").AsString());
22
}
23
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
24
}
25
26
[Theory]
27
[InlineData("const r=/a/uy;r.exec('ba');r.lastIndex=1;const m=r.exec('ba');[m[0],m.index,r.lastIndex].join(',')", "a,1,2")]
28
[InlineData("const r=/z/gu;r.exec('aaa');r.lastIndex=2;const a=r.exec('aaaz');[a[0],a.index,r.lastIndex].join(',')", "z,3,4")]
29
[InlineData("const r=/z/gu;r.exec('aaa');r.compile('a','gu');const m=r.exec('aaa');[m[0],m.index,r.lastIndex].join(',')", "a,0,1")]
30
[InlineData("const r=/z/gu;r.exec('aaa');r.exec=function(){return ['custom']};r.exec('aaa')[0]", "custom")]
31
[InlineData("const r=/(?<x>a)|(b)/dgu;r.exec('cc');r.lastIndex=0;const a=r.exec('ab');a[0]='changed';r.lastIndex=1;const b=r.exec('ab');JSON.stringify([b[0],b[1],b[2],b.groups.x,b.indices[0],r.lastIndex])", "[\"b\",null,\"b\",null,[1,2],2]")]
32
[InlineData("const r=/(?<=😀)x/gu;r.exec('😀y');r.lastIndex=1;r.exec('😀y');r.lastIndex=1;const m=r.exec('😀x');[m[0],m.index,r.lastIndex].join(',')", "x,2,3")]
33
[InlineData("const r=/z/gu;r.exec('aaa');let reads=0;r.lastIndex={valueOf(){reads++;return 1}};r.exec('aaa');[reads,r.lastIndex].join(',')", "1,0")]
34
public void StickyInputCompileCapturesUnicodeAndObservableLastIndexKeepOriginalSemantics(string source, string expected)
35
{
36
var engine = Create();
37
try { Assert.Equal(expected, engine.Evaluate(source).AsString()); }
38
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
39
}
40
41
[Fact]
42
public void RetainedInputIsBoundedAndRegexInstancesDoNotShareSearchFacts()
43
{
44
var engine = Create();
45
try
46
{
47
engine.Execute("const input='a'.repeat(131073),r=/z/u;r.exec(input);r.exec(input);const other=/z/u;other.exec('aaa')");
48
Assert.Equal(0, Hits(engine.Evaluate("r")));
49
Assert.Null(typeof(JsRegExp).GetField("_xfeNoMatchInput", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(engine.Evaluate("r")));
50
Assert.Equal(0, Hits(engine.Evaluate("other")));
51
}
52
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
53
}
54
55
[Fact]
56
public void TimedOutSearchesRemainFailuresAndAreNeverMemoized()
57
{
58
var engine = Create();
59
try
60
{
61
engine.Execute("const r=/(a+)+$/u,input='a'.repeat(32)+'!'");
62
for (int attempt = 0; attempt < 2; attempt++)
63
Assert.Throws<System.Text.RegularExpressions.RegexMatchTimeoutException>(() => engine.Evaluate("r.exec(input)"));
64
Assert.Equal(0, Hits(engine.Evaluate("r")));
65
}
66
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
67
}
68
}
@@ -13,21 +13,25 @@ try {
13
13
& git archive --format=zip ('--output='+(Join-Path $output 'Baseline.zip')) $commit
14
14
if($LASTEXITCODE -ne 0){throw 'Could not export the baseline.'}
15
15
Expand-Archive -LiteralPath (Join-Path $output 'Baseline.zip') -DestinationPath $baseline
16
$project=Join-Path $output 'BaselineProbe.csproj'
16
# The probe must inherit the archived Directory.Build.targets and patch
17
# tool. A project outside Baseline would silently apply today's Jint patch
18
# to the old browser, contaminating an interpreter before/after comparison.
19
$project=Join-Path $baseline 'MonacoBaselineProbe.csproj'
17
20
$reference=[Security.SecurityElement]::Escape((Join-Path $baseline 'Source/XFESystem.Browser/XFESystem.Browser.csproj'))
18
21
$profile=[Security.SecurityElement]::Escape((Join-Path $PSScriptRoot 'MonacoProfile.cs'))
22
$regexProfile=[Security.SecurityElement]::Escape((Join-Path $PSScriptRoot 'MonacoRegexProfile.cs'))
19
23
$canvas=[Security.SecurityElement]::Escape((Join-Path $PSScriptRoot '../ProductionProbe/WindowsTextCanvas.cs'))
20
24
@"
21
25
<Project Sdk="Microsoft.NET.Sdk">
22
26
<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>
27
<ItemGroup><ProjectReference Include="$reference"/><Compile Include="$profile"/><Compile Include="$regexProfile"/><Compile Include="$canvas"/><Compile Include="Main.cs"/></ItemGroup>
24
28
</Project>
25
29
"@ | 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
30
'MonacoProfile.Run(Path.GetFullPath(args[0]), args[1]);' | Set-Content -LiteralPath (Join-Path $baseline 'Main.cs') -Encoding UTF8
27
31
# Suppress tier promotion variance; initial JIT still belongs to cold host
28
32
# startup. The separate native image comparison contains no host JIT.
29
33
$env:DOTNET_TieredCompilation='0'
30
& dotnet run --project $project -c Release --artifacts-path (Join-Path $output 'BeforeArtifacts') -- $baseline Before *> (Join-Path $output 'Before.log')
34
& dotnet run --project $project -c Release --artifacts-path (Join-Path $baseline 'build/BeforeArtifacts') -- $baseline Before *> (Join-Path $output 'Before.log')
31
35
if($LASTEXITCODE -ne 0){throw ('Baseline Monaco profile failed: '+$output)}
32
36
& 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
37
if($LASTEXITCODE -ne 0){throw ('Current Monaco profile failed: '+$output)}
@@ -50,7 +54,9 @@ try {
50
54
BeforeAllocatedBytes=$old[0].AllocatedBytes;AfterAllocatedBytes=$new[0].AllocatedBytes;ActualViewTokensEqual=$true}
51
55
}
52
56
[ordered]@{Kind='MonacoFullInitializationHostComparison';Passed=$true;HostOnly=$true;NativeAcceptance=$false;
53
BaselineCommit=$commit;Before=$beforePath;After=$afterPath;Rows=$rows} | ConvertTo-Json -Depth 5 |
57
BaselineCommit=$commit;Before=$beforePath;After=$afterPath;
58
BeforeRuntimeJintSha256=$before.RuntimeJintSha256;AfterRuntimeJintSha256=$after.RuntimeJintSha256;
59
BeforeRuntimePatches=$before.RuntimePatches;AfterRuntimePatches=$after.RuntimePatches;Rows=$rows} | ConvertTo-Json -Depth 5 |
54
60
Set-Content -LiteralPath (Join-Path $output 'Comparison.json') -Encoding UTF8
55
61
Write-Output ('Host Monaco comparison: '+(Join-Path $output 'Comparison.json'))
56
62
} finally {$env:DOTNET_TieredCompilation=$oldTiering;Pop-Location}
@@ -8,11 +8,11 @@ using XFESystem.Browser;
8
8
// Editing checks are outside the timed interval. No pre-warming or host browser.
9
9
internal static class MonacoProfile
10
10
{
11
internal static void Run(string root, string label)
11
internal static void Run(string root, string label, bool profileRegex = false)
12
12
{
13
13
string output = Path.Combine(root, "build/MonacoInit", label + "-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
14
14
Directory.CreateDirectory(output);
15
var inputs = new[] { "Source/XFESystem.Browser", "assets/Browser/WebsiteCode", "assets/Browser/Monaco/0.52.2/Core" }
15
var inputs = new[] { "Source/XFESystem.Browser", "Source/XFESystem.Collections", "Tools/JintCleanupPatch", "assets/Browser/WebsiteCode", "assets/Browser/Monaco/0.52.2/Core" }
16
16
.SelectMany(directory => Directory.EnumerateFiles(Path.Combine(root, directory)))
17
17
.Where(path => Path.GetExtension(path) is ".cs" or ".js" or ".css" or ".json")
18
18
.ToDictionary(path => Path.GetRelativePath(root, path).Replace('\\', '/'), Hash);
@@ -26,9 +26,18 @@ internal static class MonacoProfile
26
26
using var canvas = new WindowsTextCanvas(Path.Combine(root, "assets/fonts/cascadia-mono-2407.24/CascadiaMono-Regular.ttf"), 796, 404);
27
27
var timer = Stopwatch.StartNew();
28
28
var trace = new List<object>();
29
var browser = new BrowserService { Trace = line => trace.Add(new { Ticks = timer.ElapsedTicks, Line = line.TrimEnd() }) };
29
var regex = profileRegex ? new MonacoRegexProfile() : null;
30
BrowserService browser = null!;
31
bool installed = false;
32
browser = new BrowserService { Trace = line =>
33
{
34
trace.Add(new { Ticks = timer.ElapsedTicks, Line = line.TrimEnd() });
35
if (regex is not null && !installed && line.StartsWith("XFE_BROWSER_SCRIPT_BEGIN", StringComparison.Ordinal))
36
{ installed = true; regex.Install(browser); }
37
} };
30
38
browser.SetCanvas(canvas); browser.ResizeViewport(796, 404);
31
39
long allocated = GC.GetTotalAllocatedBytes();
40
int[] collections = Enumerable.Range(0, 3).Select(GC.CollectionCount).ToArray();
32
41
timer.Restart();
33
42
browser.NavigateEditor(csharp, "Test.cs");
34
43
double navigationMs = timer.Elapsed.TotalMilliseconds;
@@ -37,6 +46,12 @@ internal static class MonacoProfile
37
46
browser.Paint(canvas, new(0, 0, 796, 404), 0); browser.FramePresented();
38
47
double readyMs = timer.Elapsed.TotalMilliseconds;
39
48
long bytes = GC.GetTotalAllocatedBytes() - allocated;
49
var dom = typeof(BrowserService).GetField("dom", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(browser)!;
50
long Counter(object instance, string name) => (long?)instance.GetType().GetProperty(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)?.GetValue(instance) ?? 0;
51
var workCounters = new { browser.LayoutPassCount, LayoutMilliseconds = Counter(browser, "LayoutTicks") * 1000.0 / Stopwatch.Frequency,
52
BudgetValidations = Counter(dom, "BudgetValidationCount"), BudgetNodeVisits = Counter(dom, "BudgetNodeVisits"),
53
BudgetMilliseconds = Counter(dom, "BudgetValidationTicks") * 1000.0 / Stopwatch.Frequency,
54
Collections = Enumerable.Range(0, 3).Select(i => GC.CollectionCount(i) - collections[i]).ToArray() };
40
55
using var parsed = JsonDocument.Parse(proof);
41
56
var data = parsed.RootElement;
42
57
string[] visible = data.GetProperty("text").EnumerateArray().Select(e => e.GetString()!.TrimEnd()).ToArray();
@@ -51,19 +66,24 @@ internal static class MonacoProfile
51
66
using var work = JsonDocument.Parse(tokenWork);
52
67
var row = new { Kind = kind, Attempt = attempt, Cold = kind == "ShortCSharp" && attempt == 1, Passed = ready && edited, NavigationMilliseconds = navigationMs,
53
68
FullReadyMilliseconds = readyMs, AllocatedBytes = bytes, browser.PackagedScriptCacheHits, browser.PackagedScriptPreparations,
54
RenderSha256 = renderHash, Proof = data.Clone(), TokenizationWork = work.RootElement.Clone(), Trace = trace };
69
RenderSha256 = renderHash, Proof = data.Clone(), TokenizationWork = work.RootElement.Clone(), Work = workCounters, Regex = regex?.Results(), Trace = trace };
55
70
passed &= row.Passed;
56
71
cases.Add(row);
57
Console.WriteLine(JsonSerializer.Serialize(new { row.Kind, row.Attempt, row.Passed, navigationMs, readyMs, bytes, renderHash, row.TokenizationWork }));
72
Console.WriteLine(JsonSerializer.Serialize(new { row.Kind, row.Attempt, row.Passed, navigationMs, readyMs, bytes, renderHash, row.TokenizationWork, row.Work }));
58
73
browser.Release();
59
74
}
60
75
bool unchanged = inputs.All(pair => Hash(Path.Combine(root, pair.Key)) == pair.Value);
61
76
string result = Path.Combine(output, "Result.json");
62
77
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 }));
78
HostOnly = true, NativeAcceptance = false, Frequency = Stopwatch.Frequency, Sources = inputs,
79
ProbeSha256 = Hash(ProbeSource()),
80
RuntimeJintSha256 = Hash(typeof(Jint.Engine).Assembly.Location),
81
RuntimePatches = new { FailedRegexSuffix = typeof(Jint.Engine).Assembly.GetType("XFESystem.JintFailedRegexSuffixV1") is not null },
82
Cases = cases }, new JsonSerializerOptions { WriteIndented = true }));
64
83
Console.WriteLine(result);
65
84
if (!passed || !unchanged) throw new IOException("The full Monaco initialization profile failed.");
66
85
}
67
86
private static string Hash(string path) => Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path)));
87
private static string ProbeSource([System.Runtime.CompilerServices.CallerFilePath] string source = "") => source;
68
88
private static string HashText(string value) => Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(value)));
69
89
}
@@ -0,0 +1,44 @@
1
using System.Diagnostics;
2
using System.Reflection;
3
using Jint;
4
using Jint.Native;
5
using Jint.Runtime.Interop;
6
using XFESystem.Browser;
7
8
// Host-only diagnostic. Calls the original intrinsic directly, under the
9
// existing evaluation budget; never replaces regex results or resets limits.
10
internal sealed class MonacoRegexProfile
11
{
12
private sealed class Work
13
{
14
public long Calls, Ticks, InputCharacters, FailedSuffixHits;
15
}
16
private readonly Dictionary<string, Work> patterns = new(StringComparer.Ordinal);
17
internal void Install(BrowserService browser)
18
{
19
var runtime = (JavaScriptRuntime)typeof(BrowserService).GetField("runtime", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(browser)!;
20
var engine = (Engine)typeof(JavaScriptRuntime).GetField("engine", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(runtime)!;
21
var prototype = engine.GetValue("RegExp").AsObject().Get("prototype").AsObject();
22
var original = (Func<JsValue, JsValue[], JsValue>)prototype.Get("exec").ToObject()!;
23
var hitCounter = typeof(Jint.Native.JsRegExp).GetField("_xfeNoMatchHits", BindingFlags.Instance | BindingFlags.NonPublic);
24
prototype.Set("exec", new ClrFunction(engine, "exec", (receiver, arguments) =>
25
{
26
var regex = receiver.AsObject();
27
string key = regex.Get("source").AsString() + "/" + regex.Get("flags").AsString();
28
if (!patterns.TryGetValue(key, out var work)) patterns.Add(key, work = new());
29
work.Calls++;
30
if (arguments.Length > 0 && arguments[0].IsString()) work.InputCharacters += arguments[0].AsString().Length;
31
long started = Stopwatch.GetTimestamp();
32
long beforeHits = (long?)hitCounter?.GetValue(regex) ?? 0;
33
var result = original(receiver, arguments);
34
work.Ticks += Stopwatch.GetTimestamp() - started;
35
work.FailedSuffixHits += ((long?)hitCounter?.GetValue(regex) ?? 0) - beforeHits;
36
return result;
37
}), false);
38
}
39
internal object[] Results() => patterns.OrderByDescending(pair => pair.Value.Ticks).Take(20).Select(pair => (object)new
40
{
41
Pattern = pair.Key, pair.Value.Calls, pair.Value.InputCharacters, pair.Value.FailedSuffixHits,
42
Milliseconds = pair.Value.Ticks * 1000.0 / Stopwatch.Frequency
43
}).ToArray();
44
}
@@ -11,7 +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
if (args.Contains("--monaco", StringComparer.Ordinal)) { MonacoProfile.Run(root, label, args.Contains("--regex", StringComparer.Ordinal)); return; }
15
15
var output = Path.Combine(root, "build/PreviewPerformance", label + "-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
16
16
Directory.CreateDirectory(output);
17
17
var inputs = new[] { "Source/XFESystem.Browser", "Source/XFESystem.Documents" }