Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions Jint.Benchmark/GuardComparisonBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using BenchmarkDotNet.Attributes;
using Jint.Native;

namespace Jint.Benchmark;

/// <summary>
/// Guard-style strict comparisons over LCG-mixed inputs the branch predictor cannot memorize:
/// `v === undefined`, `v === null`, `v == null` and `typeof v === '...'` — the idioms defensive
/// library code (linq.js, lodash, handlebars) runs on nearly every call.
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "Gen0", "Gen1", "Gen2")]
public class GuardComparisonBenchmark
{
private Engine _engine = null!;
private Prepared<Script> _isUndefinedGuard;
private Prepared<Script> _isNullGuard;
private Prepared<Script> _looseNullGuard;
private Prepared<Script> _typeofStringGuard;
private Prepared<Script> _typeofUndefinedGuard;

private const string SetupSource = """
var mixedVals = [];
var order = [];
(function () {
var seed = 20260713;
for (var i = 0; i < 1024; i++) {
seed = (seed * 1664525 + 1013904223) | 0;
var pick = (seed >>> 4) & 3;
if (pick === 0) { mixedVals.push(seed & 255); }
else if (pick === 1) { mixedVals.push('s' + (seed & 15)); }
else if (pick === 2) { mixedVals.push(undefined); }
else { mixedVals.push(((seed >>> 6) & 1) === 0 ? null : { v: i }); }
}
for (var i = 0; i < 8192; i++) {
seed = (seed * 1664525 + 1013904223) | 0;
order.push((seed >>> 7) & 1023);
}
})();
""";

[GlobalSetup]
public void Setup()
{
_engine = new Engine();
_engine.Execute(SetupSource);

_isUndefinedGuard = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 100000; i++) {
var v = mixedVals[order[i & 8191]];
if (v === undefined) { s++; }
if (v !== undefined) { s += 2; }
}
return s;
}
f();
""");

_isNullGuard = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 100000; i++) {
var v = mixedVals[order[i & 8191]];
if (v === null) { s++; }
if (v !== null) { s += 2; }
}
return s;
}
f();
""");

_looseNullGuard = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 100000; i++) {
var v = mixedVals[order[i & 8191]];
if (v == null) { s++; }
}
return s;
}
f();
""");

_typeofStringGuard = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 100000; i++) {
var v = mixedVals[order[i & 8191]];
if (typeof v === 'string') { s++; }
if (typeof v !== 'number') { s += 2; }
}
return s;
}
f();
""");

_typeofUndefinedGuard = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 100000; i++) {
var v = mixedVals[order[i & 8191]];
if (typeof v === 'undefined') { s++; }
}
return s;
}
f();
""");

_engine.Evaluate(_isUndefinedGuard);
_engine.Evaluate(_isNullGuard);
_engine.Evaluate(_looseNullGuard);
_engine.Evaluate(_typeofStringGuard);
_engine.Evaluate(_typeofUndefinedGuard);
}

[Benchmark]
public JsValue IsUndefinedGuard() => _engine.Evaluate(_isUndefinedGuard);

[Benchmark]
public JsValue IsNullGuard() => _engine.Evaluate(_isNullGuard);

[Benchmark]
public JsValue LooseNullGuard() => _engine.Evaluate(_looseNullGuard);

[Benchmark]
public JsValue TypeofStringGuard() => _engine.Evaluate(_typeofStringGuard);

[Benchmark]
public JsValue TypeofUndefinedGuard() => _engine.Evaluate(_typeofUndefinedGuard);
}
117 changes: 117 additions & 0 deletions Jint.Tests/Runtime/GuardFusionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// Pins the build-time fused guard comparisons (`x === undefined`, `x === null`,
/// `typeof x === "literal"` and the !== forms) against the semantics of the generic
/// strict-equality path they replace.
/// </summary>
public class GuardFusionTests
{
[Fact]
public void UndefinedAndNullGuardsMatchGenericSemantics()
{
var engine = new Engine();
var result = engine.Evaluate("""
var u; var n = null; var s = 'x'; var zero = 0; var empty = '';
[
u === undefined, undefined === u, u !== undefined,
n === null, null === n, n !== null,
s === undefined, zero === null, empty === undefined,
n === undefined, u === null
].join(',');
""").AsString();

Assert.Equal("true,true,false,true,true,false,false,false,false,false,false", result);
}

[Fact]
public void TypeofGuardsCoverEveryTypeofResult()
{
var engine = new Engine();
var result = engine.Evaluate("""
[
typeof undefined === 'undefined',
typeof null === 'object',
typeof true === 'boolean',
typeof 1 === 'number',
typeof 1n === 'bigint',
typeof 'x' === 'string',
typeof Symbol() === 'symbol',
typeof (function () {}) === 'function',
typeof {} === 'object',
'string' === typeof 'x',
typeof 'x' !== 'number'
].join(',');
""").AsString();

Assert.Equal("true,true,true,true,true,true,true,true,true,true,true", result);
}

[Fact]
public void TypeofUndeclaredIdentifierDoesNotThrow()
{
var engine = new Engine();
Assert.True(engine.Evaluate("typeof notDeclaredAnywhere === 'undefined'").AsBoolean());
Assert.False(engine.Evaluate("typeof notDeclaredAnywhere !== 'undefined'").AsBoolean());
}

[Fact]
public void ImpossibleTypeofLiteralStillEvaluatesOperandOnce()
{
var engine = new Engine();
var result = engine.Evaluate("""
var calls = 0;
function probe() { calls++; return 'x'; }
var matched = typeof probe() === 'bogus';
[matched, calls].join(',');
""").AsString();

Assert.Equal("false,1", result);
}

[Fact]
public void TypeofClrFunctionIsFunction()
{
var engine = new Engine();
engine.SetValue("clrCallback", new Func<int>(() => 42));
Assert.True(engine.Evaluate("typeof clrCallback === 'function'").AsBoolean());
Assert.False(engine.Evaluate("typeof clrCallback === 'object'").AsBoolean());
}

[Fact]
public void FusedGuardsWorkAcrossAwaitSuspension()
{
var engine = new Engine();
var result = engine.Evaluate("""
async function f() {
var a = (await Promise.resolve(undefined)) === undefined;
var b = (await Promise.resolve(null)) === null;
var c = typeof (await Promise.resolve('s')) === 'string';
return [a, b, c].join(',');
}
f();
""").UnwrapIfPromise().AsString();

Assert.Equal("true,true,true", result);
}

[Fact]
public void GuardsDriveIfStatementBooleanFastPath()
{
var engine = new Engine();
var result = engine.Evaluate("""
var log = [];
var vals = [undefined, null, 'x', 5];
for (var i = 0; i < vals.length; i++) {
var v = vals[i];
if (v === undefined) { log.push('u'); }
else if (v === null) { log.push('n'); }
else if (typeof v === 'string') { log.push('s'); }
else { log.push('o'); }
}
log.join('');
""").AsString();

Assert.Equal("unso", result);
}
}
Loading
Loading