Skip to content

Commit 02bac35

Browse files
committed
perf_hooks: do not record timerify entry for async throw
`timerify` used `result.finally(processComplete)` for thenables, which recorded a histogram sample and enqueued a `function` PerformanceEntry on async rejection. The sync-throw path never reached `processComplete` because the throw happens inside ReflectApply, so async- and sync-throwing variants of the same function produced different observable outputs. Use `result.then(onFulfilledTimerified)` with no onRejected so rejections propagate unchanged and `processComplete` runs only on fulfillment. Relax the thenable check from `finally` to `then`, since a thenable is only required to implement `then`. Fixes: #42743 Signed-off-by: Maruthan G <maruthang4@gmail.com>
1 parent 10ae641 commit 02bac35

2 files changed

Lines changed: 120 additions & 3 deletions

File tree

lib/internal/perf/timerify.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ function processComplete(name, start, args, histogram) {
5252
enqueue(entry);
5353
}
5454

55+
function onFulfilledTimerified(name, start, args, histogram, value) {
56+
processComplete(name, start, args, histogram);
57+
return value;
58+
}
59+
5560
function timerify(fn, options = kEmptyObject) {
5661
validateFunction(fn, 'fn');
5762

@@ -74,10 +79,18 @@ function timerify(fn, options = kEmptyObject) {
7479
const result = isConstructorCall ?
7580
ReflectConstruct(fn, args, fn) :
7681
ReflectApply(fn, this, args);
77-
if (!isConstructorCall && typeof result?.finally === 'function') {
78-
return result.finally(
82+
if (!isConstructorCall && typeof result?.then === 'function') {
83+
// Use `then` (not `finally`) so that a rejected thenable does NOT
84+
// produce a performance entry or histogram record. This mirrors the
85+
// behavior of synchronously-thrown errors, which never reach
86+
// `processComplete`. Refs: https://github.com/nodejs/node/issues/42743
87+
// Also, thenables are only required to implement `then`; relying on
88+
// `finally` would break for minimal thenable implementations.
89+
// Pass only an `onFulfilled` handler so any rejection propagates
90+
// unchanged through the returned promise.
91+
return result.then(
7992
FunctionPrototypeBind(
80-
processComplete,
93+
onFulfilledTimerified,
8194
result,
8295
fn.name,
8396
start,
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Regression test for https://github.com/nodejs/node/issues/42743
2+
// Test that a timerified function which throws asynchronously (i.e. returns
3+
// a rejected promise / thenable) behaves consistently with one that throws
4+
// synchronously: no 'function' performance entry is produced, and no
5+
// histogram record is added.
6+
7+
'use strict';
8+
9+
const common = require('../common');
10+
const assert = require('assert');
11+
const {
12+
createHistogram,
13+
performance,
14+
PerformanceObserver,
15+
timerify,
16+
} = require('perf_hooks');
17+
18+
// 1. Sync vs async throw should record the same number of histogram samples.
19+
{
20+
const h1 = createHistogram();
21+
const h2 = createHistogram();
22+
23+
function syncThrow() { throw new Error('sync'); }
24+
25+
async function asyncThrow() { throw new Error('async'); }
26+
27+
const g1 = timerify(syncThrow, { histogram: h1 });
28+
const g2 = timerify(asyncThrow, { histogram: h2 });
29+
30+
assert.throws(() => g1(), /^Error: sync$/);
31+
32+
assert.rejects(g2(), /^Error: async$/).then(common.mustCall(() => {
33+
// Both histograms must agree: neither should have recorded a sample,
34+
// because the throwing async function should behave like the throwing
35+
// sync function.
36+
assert.strictEqual(h1.count, 0);
37+
assert.strictEqual(h2.count, 0);
38+
assert.strictEqual(h1.count, h2.count);
39+
}));
40+
}
41+
42+
// 2. No 'function' PerformanceEntry should be produced for either path.
43+
{
44+
const obs = new PerformanceObserver(common.mustNotCall(
45+
'no function entry should be enqueued for a throwing timerified fn',
46+
));
47+
obs.observe({ entryTypes: ['function'] });
48+
49+
const sync = timerify(() => { throw new Error('sync2'); });
50+
const async_ = timerify(async () => { throw new Error('async2'); });
51+
52+
assert.throws(() => sync(), /^Error: sync2$/);
53+
assert.rejects(async_(), /^Error: async2$/).then(common.mustCall(() => {
54+
// Give the observer a tick to flush any (incorrectly) enqueued entries
55+
// before disconnecting.
56+
setImmediate(common.mustCall(() => obs.disconnect()));
57+
}));
58+
}
59+
60+
// 3. A custom thenable that only implements `then` (no `finally`) and
61+
// fulfills should still produce a performance entry and resolve to the
62+
// original value. This guards against a regression where the wrapper
63+
// relied on `result.finally`.
64+
{
65+
const obs = new PerformanceObserver(common.mustCall((list, observer) => {
66+
const entries = list.getEntries();
67+
assert.strictEqual(entries.length, 1);
68+
const entry = entries[0];
69+
assert.strictEqual(entry.entryType, 'function');
70+
assert.strictEqual(typeof entry.duration, 'number');
71+
assert.strictEqual(typeof entry.startTime, 'number');
72+
observer.disconnect();
73+
}));
74+
obs.observe({ entryTypes: ['function'] });
75+
76+
function thenableOnly() {
77+
return {
78+
then(onFulfilled) {
79+
// Resolve asynchronously to mimic real promise semantics.
80+
setImmediate(() => onFulfilled('value'));
81+
},
82+
};
83+
}
84+
85+
const wrapped = timerify(thenableOnly);
86+
const ret = wrapped();
87+
// The wrapper must return something thenable; `.then` must work and
88+
// the resolved value must be propagated unchanged.
89+
assert.strictEqual(typeof ret.then, 'function');
90+
ret.then(common.mustCall((value) => {
91+
assert.strictEqual(value, 'value');
92+
}));
93+
}
94+
95+
// 4. Async fulfillment must still propagate the resolved value.
96+
{
97+
const wrapped = timerify(async () => 42);
98+
wrapped().then(common.mustCall((value) => {
99+
assert.strictEqual(value, 42);
100+
}));
101+
}
102+
103+
// Reference `performance` so the binding is exercised in this file too.
104+
assert.strictEqual(performance.timerify, timerify);

0 commit comments

Comments
 (0)