Skip to content

Commit 2ac41b2

Browse files
committed
perf: add ES5-compatible cache for deep hierarchy method resolution
When the instance shortcut optimization cannot be applied (frozen/readonly targets, Proxy objects, or when the instance already owns the property), every dynamic method call previously re-entered _getInstFunc with repeated property lookups on every invocation. This change adds a per-instance resolved-function cache stored directly in the existing instFuncTable object (keyed by className + funcName). Since instFuncTable is created with objCreate(null) during construction and Object.freeze is shallow, it remains writable even when the instance itself is frozen. Subsequent calls to the same proxy for the same instance return the cached function directly, eliminating the _getInstFunc overhead entirely. Additionally, the prototype chain walk inside _getInstFunc is now guarded behind the canAddInst check, avoiding unnecessary visited-array allocation when the instance already owns the property. Fully ES5 compatible - no WeakMap or other ES6+ APIs required. Impact: Reduces per-call overhead from O(property lookups) to O(1) cache hit for all cases where the shortcut optimization cannot be applied, particularly benefiting deep inheritance hierarchies (10+ levels).
1 parent 71ee0e5 commit 2ac41b2

3 files changed

Lines changed: 620 additions & 17 deletions

File tree

‎lib/src/DynamicProto.ts‎

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ const DynInstChkTag = '_dynInstChk';
7171
* tag name as the function level but a different const name for readability only.
7272
*/
7373
const DynAllowInstChkTag = DynInstChkTag;
74+
75+
/**
76+
* Prefix used for per-instance resolved function cache keys stored in the instFuncTable
77+
* @ignore
78+
*/
79+
const DynResolvedCachePrefix = '_dyn_r$';
7480

7581
/**
7682
* The global (imported) instances where the global performance options are stored
@@ -346,22 +352,25 @@ function _getInstFunc(target: any, funcName: string, proto: any, currentDynProto
346352
// If the instance already has an instance function we can't replace it
347353
let canAddInst = !objHasOwnProperty(target, funcName);
348354

349-
// Get current prototype
350-
let objProto = _getObjProto(target);
351-
let visited:any[] = [];
352-
353-
// Lookup the function starting at the top (instance level prototype) and traverse down, if the first matching function
354-
// if nothing is found or if the first hit is a dynamic proto instance then we can safely add an instance shortcut
355-
while (canAddInst && objProto && !_isObjectArrayOrFunctionPrototype(objProto) && !_hasVisited(visited, objProto)) {
356-
let protoFunc = objProto[funcName];
357-
if (protoFunc) {
358-
canAddInst = (protoFunc === currentDynProtoProxy);
359-
break;
360-
}
355+
// Only perform the prototype chain walk if canAddInst is still viable
356+
if (canAddInst) {
357+
// Get current prototype
358+
let objProto = _getObjProto(target);
359+
let visited:any[] = [];
360+
361+
// Lookup the function starting at the top (instance level prototype) and traverse down, if the first matching function
362+
// if nothing is found or if the first hit is a dynamic proto instance then we can safely add an instance shortcut
363+
while (objProto && !_isObjectArrayOrFunctionPrototype(objProto) && !_hasVisited(visited, objProto)) {
364+
let protoFunc = objProto[funcName];
365+
if (protoFunc) {
366+
canAddInst = (protoFunc === currentDynProtoProxy);
367+
break;
368+
}
361369

362-
// We need to find all possible initial functions to ensure that we don't bypass a valid override function
363-
visited.push(objProto);
364-
objProto = _getObjProto(objProto);
370+
// We need to find all possible initial functions to ensure that we don't bypass a valid override function
371+
visited.push(objProto);
372+
objProto = _getObjProto(objProto);
373+
}
365374
}
366375

367376
try {
@@ -411,9 +420,25 @@ function _getProtoFunc(funcName: string, proto: any, currentDynProtoProxy: any)
411420
*/
412421
function _populatePrototype(proto:any, className:string, target:any, baseInstFuncs:any, setInstanceFunc:boolean) {
413422
function _createDynamicPrototype(proto:any, funcName:string) {
423+
// Pre-compute a cache key for storing the resolved instance function directly in the
424+
// instFuncTable (which is per-instance and NOT frozen even when the instance is).
425+
// This avoids repeated _getInstFunc lookups when the shortcut optimization cannot be applied.
426+
let cacheKey = DynResolvedCachePrefix + className + "." + funcName;
427+
414428
let dynProtoProxy = function() {
415-
// Use the instance or prototype function
416-
let instFunc = _getInstFunc(this, funcName, proto, dynProtoProxy) || _getProtoFunc(funcName, proto, dynProtoProxy);
429+
// Fast path: check the per-instance resolved cache to avoid _getInstFunc overhead on repeated calls
430+
let instFuncTable = this[DynInstFuncTable];
431+
let instFunc = instFuncTable ? instFuncTable[cacheKey] : undefined;
432+
if (!instFunc) {
433+
// Use the instance or prototype function
434+
instFunc = _getInstFunc(this, funcName, proto, dynProtoProxy) || _getProtoFunc(funcName, proto, dynProtoProxy);
435+
// Cache the resolved function for subsequent calls from this instance.
436+
// instFuncTable is created with objCreate(null) during construction and is not frozen
437+
// even when the instance itself is frozen (Object.freeze is shallow).
438+
if (instFuncTable) {
439+
instFuncTable[cacheKey] = instFunc;
440+
}
441+
}
417442
// eslint-disable-next-line prefer-rest-params
418443
return instFunc.apply(this, arguments);
419444
};
@@ -441,6 +466,10 @@ function _populatePrototype(proto:any, className:string, target:any, baseInstFun
441466
// Save the instance Function to the lookup table and remove it from the instance as it's not a dynamic proto function
442467
instFuncs[name] = target[name];
443468
delete target[name];
469+
470+
// Invalidate any cached resolved function for this entry so stale
471+
// values are not returned after re-registration via dynamicProto
472+
delete instFuncTable[DynResolvedCachePrefix + className + "." + name];
444473

445474
// Add a dynamic proto if one doesn't exist or if a prototype function exists and it's not a dynamic one
446475
if (!objHasOwnProperty(proto, name) || (proto[name] && !proto[name][DynProxyTag])) {

0 commit comments

Comments
 (0)