Skip to content

perf: use Date.now() in enabled path (−8.8% on 200K-call benchmark)#1027

Open
alanzabihi wants to merge 1 commit intodebug-js:masterfrom
alanzabihi:perf/date-now
Open

perf: use Date.now() in enabled path (−8.8% on 200K-call benchmark)#1027
alanzabihi wants to merge 1 commit intodebug-js:masterfrom
alanzabihi:perf/date-now

Conversation

@alanzabihi
Copy link
Copy Markdown

Motivation

Every enabled debug() call in src/common.js builds a timestamp this way:

const curr = Number(new Date());

That allocates a Date just to coerce it to a number. Date.now() returns the same value without the allocation. Both evaluate to (new Date()).valueOf().

Change

One line in src/common.js:

 // Set `diff` timestamp
-const curr = Number(new Date());
+const curr = Date.now();

No other file is touched. No new dependencies. Output is byte-identical; fingerprints at the bottom.

Compatibility

Date.now() is ES5 (2009). The repo pins engines.node: ">=6.0", well under the floor. src/common.js is also used by src/browser.js; Date.now() works in Chrome 5+, Firefox 3+, Safari 4+, and IE 9+.

Measurements

Run on an Intel Xeon W-2295 (18c/36t @ 3.0 GHz), Ubuntu 24.04, Node v25.9.0. Server idle, one Node process per sample.

Bench 1: isolated microbenchmark

50M iterations in a tight loop, median of 5 rounds, no other debug machinery in the loop.

// date-micro.mjs
const ITERS = 50_000_000;
for (let w = 0; w < 2; w++) { benchOld(); benchNew(); } // warmup
const oldMs = benchOld(); // inner loop: sum += Number(new Date())
const newMs = benchNew(); // inner loop: sum += Date.now()
expression ns/call
Number(new Date()) 128.6
Date.now() 37.0

The operation itself runs 3.48× faster. About 92 ns saved per call.

Bench 2: in-repo enabled-path benchmark

The repo's own .polyresearch/bench.js. A single run is 50K iterations × 4 debug-call variants = 200K enabled calls, 7 internal samples, internal median. Outer loop runs 10 times to smooth out inter-process noise; I report the median of those per-run medians.

Setup:

git worktree add ../debug-baseline upstream/master   # f405ade
git worktree add ../debug-t1 cecb529                 # baseline + this 1-line change
# Both worktrees share node_modules (dependencies unchanged).

Run (in each worktree):

for i in {1..10}; do
  node .polyresearch/bench.js | grep -E '^(PRIMARY_MS|GUARD_MS):'
done
metric (ms) baseline median patched median delta
PRIMARY_MS (200K enabled calls) 299.26 272.88 −26.38 ms (−8.82%)
GUARD_MS (2M disabled calls) 57.69 56.23 −1.46 ms, noise (this change never runs on the disabled path)

Worst patched run across the 10 samples was 289.55 ms, still below the baseline median of 299.26 ms. No overlap in the middle of the distribution, so the gain is clear of run-to-run noise at n=10.

Caveats

  • Bench 1 is an upper bound on what this change can possibly deliver.
  • Bench 2 is a synthetic mix of plain strings, %s/%d, %O, and multiline messages. Real apps hit those at different rates, so their share of the saving will differ.
  • Bench 1's 3.48× does not translate 1:1 to the hot path; the timestamp is one step among several in debug(). The observed 132 ns/call saving in Bench 2 is close to Bench 1's 92 ns/call prediction, with a small extra slice probably from V8 no longer needing to allocate.

Gates

Fresh patched worktree:

  • npx mocha test.js test.node.js: 16/16 passing.
  • npx xo src/common.js src/node.js src/index.js: no errors; pre-existing warnings count unchanged.
  • Enabled-path output is byte-identical to upstream/master (SHA-256 over captured output across all four call types, reported by .polyresearch/bench.js):
fingerprint upstream patched
FINGERPRINT_A (enabled) 1990ffa01cb11326 same
FINGERPRINT_B (disabled) 4ac7e68fb1172f4b same

Risks

None I can see. The two expressions differ only in whether a Date gets allocated; the number they return is the same IEEE 754 double (current epoch ms, rounded to integer at call time). debug.diff, debug.prev, and debug.curr all see the same values.

Replaces Number(new Date()) with Date.now() in the enabled debug() hot
path. Both expressions produce the same epoch-ms value; Date.now() returns
it without allocating a Date object.
@alanzabihi alanzabihi marked this pull request as ready for review April 17, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant