using Acornima.Ast;
using Jint;
namespace XFESystem.Browser;
/// <summary>
/// Bounded LRU for immutable packaged script syntax. Jint's Prepared instance is
/// documented as reusable across engines; no Engine, realm, JS object, global or
/// evaluated result is retained here. Ordinary network/user scripts never enter.
/// Exact source equality guards changed resources, independent of key collisions.
/// </summary>
internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCharacters = 8_388_608, int maximumNodes = 2_000_000)
{
internal const int MinimumSourceCharacters = 1_024;
internal static PreparedScriptCache Shared { get; } = new();
private sealed record Entry(string Key, string Source, Prepared<Script> Script, int Nodes);
private readonly object gate = new();
private readonly List<Entry> entries = [];
private int characters, nodes;
internal (int Entries, int Characters, int Nodes) Snapshot
{
get
{
Monitor.Enter(gate);
var snapshot = (entries.Count, characters, nodes);
Monitor.Exit(gate);
return snapshot;
}
}
internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused)
=> GetOrPrepare(source, key, out reused, out _, out _);
internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused, out long preparationTicks, out long admissionTicks)
{
reused = false;
preparationTicks = admissionTicks = 0;
Monitor.Enter(gate);
var found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
return found.Script;
}
Monitor.Exit(gate);
// Parse outside the global cache gate. Jint performs function analysis
// only when needed, as in its ordinary Evaluate(source) path. Count nodes
// as the parser creates them, rather than traversing the whole AST again.
long started = System.Diagnostics.Stopwatch.GetTimestamp();
var prepared = PackagedScriptParser.Parse(source, key, maximumNodes, out int count);
preparationTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
if (source.Length > maximumCharacters || key.Length > 2_048 || maximumEntries <= 0) return prepared;
if (count > maximumNodes) return prepared;
started = System.Diagnostics.Stopwatch.GetTimestamp();
Monitor.Enter(gate);
found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
admissionTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
return found.Script;
}
while (entries.Count >= maximumEntries || characters > maximumCharacters - source.Length || nodes > maximumNodes - count)
RemoveAt(0);
entries.Add(new(key, source, prepared, count));
characters += source.Length; nodes += count;
Monitor.Exit(gate);
admissionTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
return prepared;
}
private Entry? Find(string key, string source)
{
for (int index = 0; index < entries.Count; index++)
{
var entry = entries[index];
if (!string.Equals(entry.Key, key, StringComparison.Ordinal)) continue;
if (!string.Equals(entry.Source, source, StringComparison.Ordinal)) { RemoveAt(index); return null; }
entries.RemoveAt(index); entries.Add(entry);
return entry;
}
return null;
}
private void RemoveAt(int index)
{
var entry = entries[index]; entries.RemoveAt(index);
characters -= entry.Source.Length; nodes -= entry.Nodes;
}
}
using Acornima.Ast;
using Jint;
namespace XFESystem.Browser;
/// <summary>
/// Bounded LRU for immutable packaged script syntax. Jint's Prepared instance is
/// documented as reusable across engines; no Engine, realm, JS object, global or
/// evaluated result is retained here. Ordinary network/user scripts never enter.
/// Exact source equality guards changed resources, independent of key collisions.
/// </summary>
internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCharacters = 8_388_608, int maximumNodes = 2_000_000)
{
internal const int MinimumSourceCharacters = 1_024;
internal static PreparedScriptCache Shared { get; } = new();
private sealed record Entry(string Key, string Source, Prepared<Script> Script, int Nodes);
private readonly object gate = new();
private readonly List<Entry> entries = [];
private int characters, nodes;
internal (int Entries, int Characters, int Nodes) Snapshot
{
get
{
Monitor.Enter(gate);
var snapshot = (entries.Count, characters, nodes);
Monitor.Exit(gate);
return snapshot;
}
}
internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused)
=> GetOrPrepare(source, key, out reused, out _, out _);
internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused, out long preparationTicks, out long admissionTicks)
{
reused = false;
preparationTicks = admissionTicks = 0;
Monitor.Enter(gate);
var found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
return found.Script;
}
Monitor.Exit(gate);
// Parse outside the global cache gate. Jint performs function analysis
// only when needed, as in its ordinary Evaluate(source) path. Count nodes
// as the parser creates them, rather than traversing the whole AST again.
long started = System.Diagnostics.Stopwatch.GetTimestamp();
var prepared = PackagedScriptParser.Parse(source, key, maximumNodes, out int count);
preparationTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
if (source.Length > maximumCharacters || key.Length > 2_048 || maximumEntries <= 0) return prepared;
if (count > maximumNodes) return prepared;
started = System.Diagnostics.Stopwatch.GetTimestamp();
Monitor.Enter(gate);
found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
admissionTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
return found.Script;
}
while (entries.Count >= maximumEntries || characters > maximumCharacters - source.Length || nodes > maximumNodes - count)
RemoveAt(0);
entries.Add(new(key, source, prepared, count));
characters += source.Length; nodes += count;
Monitor.Exit(gate);
admissionTicks = System.Diagnostics.Stopwatch.GetTimestamp() - started;
return prepared;
}
private Entry? Find(string key, string source)
{
for (int index = 0; index < entries.Count; index++)
{
var entry = entries[index];
if (!string.Equals(entry.Key, key, StringComparison.Ordinal)) continue;
if (!string.Equals(entry.Source, source, StringComparison.Ordinal)) { RemoveAt(index); return null; }
entries.RemoveAt(index); entries.Add(entry);
return entry;
}
return null;
}
private void RemoveAt(int index)
{
var entry = entries[index]; entries.RemoveAt(index);
characters -= entry.Source.Length; nodes -= entry.Nodes;
}
}