Skip to content

Commit 8e44385

Browse files
lahmaclaude
andauthored
Fix prepared script performance regression from shared identifier cache thrashing (#2324)
When using Engine.PrepareScript(), the AstAnalyzer stored JintIdentifierExpression instances in AST node UserData. Since JintExpression.Build() returns these cached instances directly, ALL references to the same identifier across all scopes and all engine instances shared a single JintIdentifierExpression with its mutable _cachedEnvironment field. Different scopes continuously overwrote each other's cached environment, causing the fast-path lookup to always miss. The fix stores only the immutable BindingName in UserData instead. Each engine execution now creates fresh JintIdentifierExpression instances (with independent environment caches) while still reusing the pre-computed BindingName to avoid repeated JsString/BindingName allocation. This also fixes a minor bug in the single-parameter JintIdentifierExpression constructor that created two redundant BindingName instances (one in the this() chain, one in the body). --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 950d59c commit 8e44385

File tree

14 files changed

+5731
-8
lines changed

14 files changed

+5731
-8
lines changed

Jint.Benchmark/Jint.Benchmark.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
</PropertyGroup>
2222
<ItemGroup>
2323
<None Include=".\Scripts\*.*" CopyToOutputDirectory="PreserveNewest" />
24+
<None Include=".\Scripts\template-rendering\**\*.*" CopyToOutputDirectory="PreserveNewest" LinkBase="Scripts\template-rendering" />
2425
<None Include="..\Jint.Tests.CommonScripts\Scripts\**" CopyToOutputDirectory="PreserveNewest" LinkBase="Scripts" />
2526
</ItemGroup>
2627
<ItemGroup>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
namespace Jint.Benchmark;
4+
5+
/// <summary>
6+
/// Replicates the exact pattern from JavaScriptEngineSwitcher's JsExecutionHeavyBenchmark:
7+
/// a Handlebars template rendering library is loaded on 4 separate engine instances,
8+
/// each calling renderTemplate() with different content items.
9+
/// See: https://github.com/Taritsyn/JavaScriptEngineSwitcher/blob/master/test/JavaScriptEngineSwitcher.Benchmarks/JsExecutionHeavyBenchmark.cs
10+
/// </summary>
11+
[MemoryDiagnoser]
12+
public class PreparedScriptBenchmark
13+
{
14+
private string _libraryCode = null!;
15+
private Prepared<Script> _preparedLibrary;
16+
private ContentItem[] _contentItems = null!;
17+
18+
[GlobalSetup]
19+
public void Setup()
20+
{
21+
var handlebars = File.ReadAllText("Scripts/template-rendering/lib/handlebars.js");
22+
var helpers = File.ReadAllText("Scripts/template-rendering/lib/helpers.js");
23+
_libraryCode = handlebars + "\n" + helpers;
24+
_preparedLibrary = Engine.PrepareScript(_libraryCode);
25+
26+
var contentDir = "Scripts/template-rendering/content";
27+
string[] names = ["hello-world", "contacts", "js-engines", "web-browser-family-tree"];
28+
_contentItems = names.Select(name => new ContentItem(
29+
File.ReadAllText(Path.Combine(contentDir, name, "template.handlebars")),
30+
File.ReadAllText(Path.Combine(contentDir, name, "data.json"))
31+
)).ToArray();
32+
}
33+
34+
[Benchmark(Baseline = true)]
35+
public void ExecuteStringOnMultipleEngines()
36+
{
37+
RenderTemplates(withPrecompilation: false);
38+
}
39+
40+
[Benchmark]
41+
public void ExecutePreparedOnMultipleEngines()
42+
{
43+
RenderTemplates(withPrecompilation: true);
44+
}
45+
46+
private void RenderTemplates(bool withPrecompilation)
47+
{
48+
// First engine: load library and render first item
49+
var engine = new Engine();
50+
if (withPrecompilation)
51+
engine.Execute(_preparedLibrary);
52+
else
53+
engine.Execute(_libraryCode);
54+
55+
engine.Invoke("renderTemplate", _contentItems[0].Template, _contentItems[0].Data);
56+
57+
// Remaining engines: each loads the same library and renders one item
58+
for (int i = 1; i < _contentItems.Length; i++)
59+
{
60+
engine = new Engine();
61+
if (withPrecompilation)
62+
engine.Execute(_preparedLibrary);
63+
else
64+
engine.Execute(_libraryCode);
65+
66+
engine.Invoke("renderTemplate", _contentItems[i].Template, _contentItems[i].Data);
67+
}
68+
}
69+
70+
private sealed record ContentItem(string Template, string Data);
71+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"lastName": "Пупкин",
3+
"firstName": "Василий",
4+
"patronymic": "Иванович",
5+
"address": {
6+
"postalCode": 392032,
7+
"country": "Россия",
8+
"state": "Тамбовская область",
9+
"city": "Тамбов",
10+
"street": "Магистральная",
11+
"houseNumber": "41к7",
12+
"floor": 8,
13+
"apartmentNumber": 115
14+
},
15+
"homePhone": "+7 (4752) 555-55-55",
16+
"mobilePhone": "+7 (999) 689-99-99",
17+
"email": "pupkin@mail.ru",
18+
"website": "http:\/\/pupkin.narod.ru",
19+
"icq": 698426795
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<address>
2+
<strong><abbr title="Фамилия Имя Отчество">Ф.И.О.</abbr>:</strong> {{lastName}} {{firstName}} {{patronymic}}<br />
3+
<strong>Адрес:</strong>
4+
{{#with address}}
5+
{{postalCode}}, {{country}}, {{state}}, {{city}}, ул. {{street}}, д. {{houseNumber}}, кв. {{apartmentNumber}}
6+
{{/with}}
7+
<br />
8+
<strong>Дом. тел.:</strong> {{homePhone}}<br />
9+
<strong>Моб. тел.:</strong> {{mobilePhone}}<br />
10+
<strong>Email:</strong> <a href="mailto:{{email}}">{{email}}</a><br />
11+
<strong>Сайт:</strong> {{{link website website false}}}<br />
12+
<strong>ICQ:</strong> <img src="http://status.icq.com/online.gif?icq={{icq}}&img=26" alt="ICQ статус" />{{icq}}
13+
</address>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"lastName": "Pupkin",
3+
"firstName": "Vasilii",
4+
"patronymic": "Ivanovich",
5+
"occupation": "Enikeyschik"
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="greeting">
2+
{{!-- Welcome by first name and patronymic --}}
3+
Hello, {{firstName}} {{patronymic}}!
4+
</div>

0 commit comments

Comments
 (0)