From b175ddfefab9d29058e898cda4a16f8dcd7b87ad Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 21:06:22 +0300 Subject: [PATCH] Cache parsed scripts instead of re-parsing them for every document Every document gets its own engine, and `RunScript` hands the raw source to `Engine.Evaluate(string)`, which parses it again from scratch. A page that loads a library therefore pays the full parse of that library once per document, even though the result is identical every time. Prepare the script once and evaluate the prepared form. Jint documents a prepared script as reusable and thread-safe, so one entry can serve every document that runs the same source. Sources that fail to prepare are handed to the engine as text, so a syntax error is still reported by the engine and not turned into a different exception type. The cache is capped, because nothing bounds how many distinct scripts a process may see; the entries kept are the ones encountered first, which is where shared libraries show up. Also drops `RunScript`'s `context` parameter and the overload that fed it. The parameter was never read, and the overload that converted a node for it had no callers. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/InteractionTests.cs | 24 +++++++++ src/AngleSharp.Js/Cache/ScriptCache.cs | 54 +++++++++++++++++++ src/AngleSharp.Js/EngineInstance.cs | 8 ++- .../Extensions/EngineExtensions.cs | 6 --- 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 src/AngleSharp.Js/Cache/ScriptCache.cs diff --git a/src/AngleSharp.Js.Tests/InteractionTests.cs b/src/AngleSharp.Js.Tests/InteractionTests.cs index eff1a33..178524a 100644 --- a/src/AngleSharp.Js.Tests/InteractionTests.cs +++ b/src/AngleSharp.Js.Tests/InteractionTests.cs @@ -113,6 +113,30 @@ public async Task RunScriptSnippetDirectlyGetSimpleValueFromCalculation() Assert.AreEqual(3.0, result); } + [Test] + public async Task RunSameScriptSourceInSeveralDocumentsKeepsStateSeparate() + { + var html = "Test"; + var config = Configuration.Default.WithJs(); + var source = "(function () { window.counter = (window.counter || 0) + 1; return window.counter; })()"; + var first = await BrowsingContext.New(config).OpenAsync(m => m.Content(html)); + var second = await BrowsingContext.New(config).OpenAsync(m => m.Content(html)); + + Assert.AreEqual(1.0, first.ExecuteScript(source)); + Assert.AreEqual(1.0, second.ExecuteScript(source)); + Assert.AreEqual(2.0, first.ExecuteScript(source)); + Assert.AreEqual(2.0, second.ExecuteScript(source)); + } + + [Test] + public async Task RunScriptSnippetWithSyntaxErrorThrows() + { + var html = "Test"; + var config = Configuration.Default.WithJs(); + var document = await BrowsingContext.New(config).OpenAsync(m => m.Content(html)); + Assert.Throws(() => document.ExecuteScript("function (")); + } + [Test] public async Task RunScriptAtPressingLink_Issue47() { diff --git a/src/AngleSharp.Js/Cache/ScriptCache.cs b/src/AngleSharp.Js/Cache/ScriptCache.cs new file mode 100644 index 0000000..99faff4 --- /dev/null +++ b/src/AngleSharp.Js/Cache/ScriptCache.cs @@ -0,0 +1,54 @@ +namespace AngleSharp.Js.Cache +{ + using Acornima.Ast; + using Jint; + using Jint.Runtime; + using System; + using System.Collections.Concurrent; + + /// + /// Caches the parsed and analyzed form of scripts. A prepared script is + /// documented by Jint as reusable and thread-safe, so the same one can serve + /// every document that runs the same source - a page loading a library ends up + /// parsing it once instead of once per document. + /// + static class ScriptCache + { + // There is no bound on how many distinct scripts a process may see, so the + // cache does not grow without end. The scripts worth keeping are the ones + // seen first: libraries are referenced early and by many documents. + private const Int32 Capacity = 32; + + private static readonly ConcurrentDictionary> _scripts = + new ConcurrentDictionary>(StringComparer.Ordinal); + + /// + /// Gets the prepared form of the given source. An invalid result means the + /// source could not be prepared and should be handed to the engine as text, + /// so that the engine reports the syntax error itself. + /// + public static Prepared