2 wrapmethod perf probe
GOAL
Write a disciplined `wrapMethod(proto, name, { before, after })` that plants a perf probe around a method on a fake engine Player: hooks on either side of the original, a restore() that puts back the exact reference, and a guard so nobody piles a second wrapper on top of yours.
CONCEPT
Monkey-patching that you can live with comes down to four rules: 1. hold on to the ORIGINAL function, 2. the wrapper passes `this` and the args through and hands back the original's return value untouched, 3. restore() puts the exact original reference back (===, never a clone), 4. tag the wrapper (a Symbol property on the wrapper function is ideal) so a second wrap is refused rather than quietly nesting. Skip rule 4 and probe-on-probe means every frame is timed twice; skip rule 3 and the order you un-patch in starts to matter, which turns removing the probe on a live game server into a gamble.
HINT
proto[name] = function (...args) { hooks.before?.(this, args); const result = original.apply(this, args); hooks.after?.(this, args, result); return result; } — then tag it: wrapper[WRAPPED] = true.
MIRRORS
Every game engine and UI toolkit ships a `wrap(Class.prototype, 'method', ...)` helper for exactly this, and every profiler plugin uses it to time Entity.updatePosition / Scene.render to find the slow actors. The double-wrap guard is the part those helpers almost never give you.
Run
node 02-wrapmethod-perf-probe.cjsSource
// ---------------------------------------------------------------------
// 04-patching-foreign-code / 02-wrapmethod-perf-probe.cjs
//
// GOAL: Write a disciplined `wrapMethod(proto, name, { before, after
// })` that plants a perf probe around a method on a fake engine
// Player: hooks on either side of the original, a restore() that
// puts back the exact reference, and a guard so nobody piles a
// second wrapper on top of yours.
//
// CONCEPT: Monkey-patching that you can live with comes down to four
// rules: 1. hold on to the ORIGINAL function, 2. the wrapper
// passes `this` and the args through and hands back the
// original's return value untouched, 3. restore() puts the
// exact original reference back (===, never a clone), 4. tag
// the wrapper (a Symbol property on the wrapper function is
// ideal) so a second wrap is refused rather than quietly
// nesting. Skip rule 4 and probe-on-probe means every frame is
// timed twice; skip rule 3 and the order you un-patch in
// starts to matter, which turns removing the probe on a live
// game server into a gamble.
//
// HINT: proto[name] = function (...args) { hooks.before?.(this, args);
// const result = original.apply(this, args); hooks.after?.(this,
// args, result); return result; } — then tag it: wrapper[WRAPPED]
// = true.
//
// MIRRORS: Every game engine and UI toolkit ships a
// `wrap(Class.prototype, 'method', ...)` helper for exactly
// this, and every profiler plugin uses it to time
// Entity.updatePosition / Scene.render to find the slow
// actors. The double-wrap guard is the part those helpers
// almost never give you.
// Run: node 02-wrapmethod-perf-probe.cjs
// ---------------------------------------------------------------------
'use strict';
const assert = require('node:assert');
// -- Third-party engine class (pretend node_modules — do not edit) ----
class Player {
constructor(name, hits) {
this.name = name;
this.hits = hits;
}
updatePosition(scale) {
this.offsets = this.hits.map((h) => h * scale);
return this.offsets;
}
}
// -- TODO 1: implement wrapMethod -------------------------------------
// Contract:
// - replaces proto[name] with a wrapper
// - hooks.before(target, argsArray) — called first, if
// provided
// - hooks.after(target, argsArray, result) — called last, if
// provided
// - wrapper preserves `this`, forwards args, returns the original result
// - returns { restore() } — reinstalls the EXACT original function
// reference
// - wrapping an already-wrapped method THROWS /already wrapped/
// - after restore(), the method may be wrapped again (guard resets)
function wrapMethod(proto, name, hooks) {
// TODO 1: your implementation here
return { restore() {} };
}
// -- Checks (do not edit) ---------------------------------------------
const log = [];
const original = Player.prototype.updatePosition;
const probe = wrapMethod(Player.prototype, 'updatePosition', {
before(target, args) { log.push(`before:${target.name}:scale=${args[0]}`); },
after(target, args, result) { log.push(`after:${target.name}:len=${result.length}`); },
});
assert.notStrictEqual(Player.prototype.updatePosition, original,
'TODO: wrapMethod must REPLACE proto[name] with a wrapper function');
const knight = new Player('knight', [1, 2, 3]);
const result = knight.updatePosition(10);
assert.deepStrictEqual(result, [10, 20, 30],
'TODO: the wrapper must return whatever the ORIGINAL returned — a probe ' +
'that changes results is not a probe');
assert.deepStrictEqual(knight.offsets, [10, 20, 30],
'TODO: the original must run with the CALLER\'s `this` — apply(this, args), ' +
'not a bare call');
assert.deepStrictEqual(log, ['before:knight:scale=10', 'after:knight:len=3'],
'TODO: before fires first with (target, args); after fires last with ' +
'(target, args, result)');
assert.throws(
() => wrapMethod(Player.prototype, 'updatePosition', { before() {} }),
/already wrapped/i,
'TODO: double-wrap guard — a second wrap on a wrapped method must throw ' +
'/already wrapped/, not stack silently (tag your wrapper, e.g. with a Symbol)');
probe.restore();
assert.strictEqual(Player.prototype.updatePosition, original,
'TODO: restore() must reinstall the EXACT original reference (===) — not a ' +
'rebuilt lookalike');
log.length = 0;
knight.updatePosition(2);
assert.deepStrictEqual(log, [],
'TODO: after restore, hooks must be fully disconnected');
// Guard must reset after restore: wrapping again is legitimate.
const probe2 = wrapMethod(Player.prototype, 'updatePosition', {
before() { log.push('probe2'); },
});
knight.updatePosition(3);
assert.deepStrictEqual(log, ['probe2'],
'TODO: after restore() the method is clean — a fresh wrap must be allowed ' +
'and functional');
probe2.restore();
assert.strictEqual(Player.prototype.updatePosition, original,
'TODO: second restore also lands back on the pristine original');
console.log('OK — 02-wrapmethod-perf-probe: wrap, observe, restore — leave no trace.');