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
47 changes: 47 additions & 0 deletions Jint.Benchmark/FunctionDeclarationAllocBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using BenchmarkDotNet.Attributes;
using Jint.Native;

namespace Jint.Benchmark;

/// <summary>
/// Isolates the allocation cost of creating functions that are never used as constructors and whose
/// <c>.prototype</c> is never read — the linq-js cold-start shape, where hundreds of helper functions
/// are declared but essentially none are <c>new</c>-ed. With a lazy <c>.prototype</c> each such function
/// skips its prototype object + two descriptor allocations. <see cref="ConstructEach"/> is the guard:
/// when the functions ARE used as constructors the prototype must still materialize (no allocation win,
/// no regression).
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "Gen0", "Gen1", "Gen2")]
public class FunctionDeclarationAllocBenchmark
{
private Engine _engine = null!;
private Prepared<Script> _declareMany;
private Prepared<Script> _constructEach;

[GlobalSetup]
public void Setup()
{
_declareMany = Engine.PrepareScript("""
var arr = [];
for (var i = 0; i < 500; i++) { arr.push(function (a, b) { return a + b; }); }
arr.length;
""", strict: true);

_constructEach = Engine.PrepareScript("""
var arr = [];
for (var i = 0; i < 500; i++) { var F = function () { this.x = 1; }; arr.push(new F()); }
arr.length;
""", strict: true);

_engine = new Engine(static options => options.Strict());
_engine.Evaluate(_declareMany);
_engine.Evaluate(_constructEach);
}

[Benchmark]
public JsValue DeclareMany() => _engine.Evaluate(_declareMany);

[Benchmark]
public JsValue ConstructEach() => _engine.Evaluate(_constructEach);
}
27 changes: 23 additions & 4 deletions Jint/Native/Function/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Jint.Native.Object;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Descriptors.Specialized;
using Jint.Runtime.Environments;
using Jint.Runtime.Interpreter;
using Environment = Jint.Runtime.Environments.Environment;
Expand Down Expand Up @@ -351,13 +352,31 @@ internal void MakeConstructor(bool writableProperty = true, ObjectInstance? prot
_constructorKind = ConstructorKind.Base;
if (prototype is null)
{
prototype = new ObjectInstanceWithConstructor(_engine, this)
// Lazily create the .prototype object. Functions that are never used as a constructor and
// whose .prototype is never read (e.g. the hundreds of helper functions declared by
// linq-js) then skip the ObjectInstanceWithConstructor + its two descriptor allocations.
// The value is memoized on first access, so .prototype identity is stable. Flags match the
// eager descriptor below: writable=writableProperty, enumerable=false, configurable=false.
var flags = PropertyFlag.WritableSet | PropertyFlag.EnumerableSet | PropertyFlag.ConfigurableSet;
if (writableProperty)
{
_prototype = _realm.Intrinsics.Object.PrototypeObject
};
flags |= PropertyFlag.Writable;
}

_prototypeDescriptor = new LazyPropertyDescriptor<Function>(this, static f => f.CreateConstructorPrototype(), flags);
}
else
{
_prototypeDescriptor = new PropertyDescriptor(prototype, writableProperty, enumerable: false, configurable: false);
}
}

_prototypeDescriptor = new PropertyDescriptor(prototype, writableProperty, enumerable: false, configurable: false);
private ObjectInstanceWithConstructor CreateConstructorPrototype()
{
return new ObjectInstanceWithConstructor(_engine, this)
{
_prototype = _realm.Intrinsics.Object.PrototypeObject
};
}

internal void SetFunctionLength(JsNumber length)
Expand Down
Loading