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

XFESystem

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

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

XFEstudio/XFESystem

Reduce Monaco regex rescanning and theme inheritance allocations

653f059
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

16 个文件 +437 -42
Modified Source/XFESystem.Browser/BrowserCssCascade.cs +48 -16
@@ -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;
Modified Source/XFESystem.Browser/BrowserInteractivePage.cs +5 -2
@@ -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;
Modified Source/XFESystem.Browser/CssComputedStyle.cs +8 -3
@@ -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 {
Added Source/XFESystem.Browser/CssPropertyMap.cs +22 -0
@@ -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 }
Modified Source/XFESystem.Browser/DomBridge.cs +9 -2
@@ -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 }
Modified THIRD_PARTY.md +1 -1
@@ -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
Added Tests/XFESystem.Browser.Tests/CssPropertySharingTests.cs +46 -0
@@ -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 }
Added Tests/XFESystem.JintCleanupPatch.Tests/RegexSearchTests.cs +68 -0
@@ -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 }
Modified Tools/BrowserCompatibility/PreviewProfile/CompareMonaco.ps1 +11 -5
@@ -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}
Modified Tools/BrowserCompatibility/PreviewProfile/MonacoProfile.cs +26 -6
@@ -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 }
Added Tools/BrowserCompatibility/PreviewProfile/MonacoRegexProfile.cs +44 -0
@@ -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 }
Modified Tools/BrowserCompatibility/PreviewProfile/Program.cs +1 -1
@@ -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" }
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