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
32 changes: 32 additions & 0 deletions Jint/Native/Function/ScriptFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ public sealed class ScriptFunction : Function, IConstructor
internal List<PrivateElement>? _privateMethods;
internal List<ClassFieldDefinition>? _fields;

// Allocation-site feedback for shaping `new T()` instances. A constructor's first
// CtorShapePromoteThreshold instances build dictionaries (so a constructor called once or twice — the
// overwhelming norm, e.g. across the Test262 suite — never grows the shared per-prototype transition
// tree); once it proves "hot" it is promoted to shape mode so repeated `new T()` with a stable layout
// reuse one interned hidden class. _ctorEmptyShape caches the prototype's empty root to avoid a
// per-construct lookup (revalidated when .prototype is reassigned).
private const int CtorShapePromoteThreshold = 16;
private bool _ctorShaped;
private int _ctorSampleCount;
private Shape? _ctorEmptyShape;
private ObjectInstance? _ctorEmptyShapeProto;

/// <summary>
/// http://www.ecma-international.org/ecma-262/5.1/#sec-13.2
/// </summary>
Expand Down Expand Up @@ -202,6 +214,26 @@ ObjectInstance IConstructor.Construct(JsCallArguments arguments, JsValue newTarg
static intrinsics => intrinsics.Object.PrototypeObject,
static (Engine engine, Realm _, object? _) => new JsObject(engine));
}

// Once the constructor is hot, start each fresh `this` in shape-building mode so this.x= /
// class fields transition a shared interned hidden class instead of building a dictionary.
// Cold constructors (below the promote threshold) stay on the dictionary path.
if (thisArgument is JsObject thisObject && thisObject.Prototype is { } proto)
{
if (_ctorShaped)
{
if (!ReferenceEquals(_ctorEmptyShapeProto, proto))
{
_ctorEmptyShape = _engine.GetEmptyShape(proto);
_ctorEmptyShapeProto = proto;
}
thisObject.StartShapeBuilding(_ctorEmptyShape!);
}
else if (++_ctorSampleCount >= CtorShapePromoteThreshold)
{
_ctorShaped = true;
}
}
}

ref readonly var calleeContext = ref PrepareForOrdinaryCall(newTarget);
Expand Down
48 changes: 47 additions & 1 deletion Jint/Native/JsObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,52 @@ internal void SetSlot(int slot, JsValue value)
internal void ClearShape()
{
_store = null;
_type &= ~InternalTypes.ShapeMode;
_type &= ~(InternalTypes.ShapeMode | InternalTypes.ShapeBuilding);
}

// store[0] is the shape; values start at [1], so a fresh builder has room for 3 properties before regrow.
private const int InitialBuildCapacity = 4;

/// <summary>
/// Starts incremental shape mode for a still-empty object (a constructor's <c>this</c>): installs the
/// prototype's empty root shape and a small growable store, so subsequent <c>this.x=</c> adds transition
/// the shape (interned, shared across <c>new T()</c> instances) instead of building a dictionary.
/// </summary>
internal void StartShapeBuilding(Shape emptyRoot)
{
var store = new object[InitialBuildCapacity];
store[0] = emptyRoot;
_store = store;
_type |= InternalTypes.ShapeMode | InternalTypes.ShapeBuilding;
unchecked { _propertiesVersion++; }
}

/// <summary>
/// Adds a brand-new configurable+enumerable+writable data property to a shape-mode object by
/// transitioning to the interned child shape and growing the store. Returns <c>false</c> — leaving the
/// object untouched — when a megamorphic guard trips (too many own properties, or this shape already
/// has too many distinct child transitions), so the caller deopts to the dictionary representation. The
/// key must be known-absent (callers check).
/// </summary>
internal bool TryShapeAdd(in Key key, JsValue value)
{
var store = _store!;
var shape = Unsafe.As<Shape>(store[0]);
if (shape.SlotCount >= Shape.MaxShapeProperties || shape.TransitionCount >= Shape.MaxFanout)
{
return false;
}

var valueIndex = shape.SlotCount + 1; // store[0] is the shape; the new value goes at SlotCount + 1
if (store.Length <= valueIndex)
{
System.Array.Resize(ref store, store.Length * 2);
_store = store;
}

store[valueIndex] = value;
store[0] = shape.Add(key);
unchecked { _propertiesVersion++; }
return true;
}
}
13 changes: 9 additions & 4 deletions Jint/Native/Object/ObjectInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1627,10 +1627,15 @@ public bool CreateDataProperty(JsValue p, JsValue v)
return true;
}

// Brand-new property added to an already-shaped object (e.g. an Object.assign / spread
// target, or assigning a new key to a literal). Object literals build their shape up front
// via JsObject.SetStore, so shape mode is never grown incrementally one property at a time;
// such adds fall back to the dictionary representation.
// Brand-new property: a hot constructor's `this` (ShapeBuilding) grows its shape via an
// interned transition shared across instances. Plain shaped objects (literals) lack the flag
// and fall through to deopt, since a one-off literal gaining a key is not a reused layout.
// The megamorphic guard inside TryShapeAdd also deopts object-as-hashmap usage.
if ((_type & InternalTypes.ShapeBuilding) != InternalTypes.Empty && jo.TryShapeAdd(key, v))
{
return true;
}

ConvertToDictionaryMode();
SetOwnProperty(p, new PropertyDescriptor(v, PropertyFlag.ConfigurableEnumerableWritable));
return true;
Expand Down
10 changes: 10 additions & 0 deletions Jint/Native/Object/Shape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ internal sealed class Shape
// At or above it, a lazily-built key->slot index is used instead.
private const int LinearScanLimit = 16;

// Megamorphic guards for incremental shape growth (constructor this.x= via TryShapeAdd). An object
// that grows past MaxShapeProperties, or a shape that sprouts more than MaxFanout distinct child
// transitions (the object-used-as-a-hashmap pattern), deopts to the dictionary representation so the
// shared per-prototype transition tree cannot grow without bound. Ordinary constructors never hit these.
internal const int MaxShapeProperties = 64;
internal const int MaxFanout = 64;

/// <summary>Parent shape (one fewer property), or null for the empty root shape.</summary>
internal readonly Shape? Parent;

Expand All @@ -39,6 +46,9 @@ internal sealed class Shape
/// <summary>Number of own string properties (= slot count). The added property lives at slot <c>SlotCount - 1</c>.</summary>
internal readonly int SlotCount;

/// <summary>Number of distinct add-property transitions out of this shape (fan-out).</summary>
internal int TransitionCount => _transitions?.Count ?? 0;

// Add-property transitions: key -> child shape. Lazily allocated; this is what interns layouts.
private Dictionary<Key, Shape>? _transitions;

Expand Down
6 changes: 5 additions & 1 deletion Jint/Runtime/InternalTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ internal enum InternalTypes
// dictionary mode. Lets the hot property paths discriminate shape vs dictionary storage with a single
// flag test on the already-loaded _type instead of a `this is JsObject` type-check plus a field read.
ShapeMode = 65536,
// a shape-mode JsObject that is still being built up incrementally (a hot constructor's `this`), so a
// brand-new property grows the shape via a transition. Plain shaped objects (object literals) lack this
// flag and deopt to a dictionary when they gain a key, since they aren't a reused allocation site.
ShapeBuilding = 131072,

Primitive = Boolean | String | Number | Integer | BigInt | Symbol,
InternalFlags = ObjectEnvironmentRecord | RequiresCloning | PlainObject | Array | Module | IsHTMLDDA | ShapeMode
InternalFlags = ObjectEnvironmentRecord | RequiresCloning | PlainObject | Array | Module | IsHTMLDDA | ShapeMode | ShapeBuilding
}
Loading