Skip to content

assert: fix slow error message generation for minified code - #62338

Open
Felipeness wants to merge 1 commit into
nodejs:mainfrom
Felipeness:fix/assert-ok-perf
Open

assert: fix slow error message generation for minified code#62338
Felipeness wants to merge 1 commit into
nodejs:mainfrom
Felipeness:fix/assert-ok-perf

Conversation

@Felipeness

Copy link
Copy Markdown
Contributor

Summary

When assert.ok(falsy) is called in minified/bundled code (common in Lambda deployments via Webpack/Terser), the error message generation can take seconds to minutes. The getFirstExpression function in lib/internal/errors/error_source.js tokenizes the entire source line with acorn to extract the failing expression, but for minified single-line files the source line is the entire file (potentially megabytes).

This PR fixes the performance issue by windowing the tokenization:

  • When the source line exceeds 2048 characters, extract a ~1024 character window around the target column instead of tokenizing the entire line
  • The window uses semicolons as safe cut points (statement boundaries) to give acorn valid-ish input
  • A try-catch is added to gracefully handle edge cases where the windowed code starts mid-token (e.g., inside a string literal)
  • The detailed error message (including member access like assert.ok) is preserved

Benchmark results (tested manually)

Scenario Before After
3.5MB minified code (100K var declarations) ~150ms tokenizing ~0ms (166 tokens in window)
7MB minified code ~700ms + stack overflow risk ~0ms

Test plan

  • Added test/parallel/test-assert-long-line-perf.js with two tests:
    • Verifies assert.ok does not hang on a large synthetic minified file (100K declarations + assert)
    • Verifies the error message still correctly contains the expression (ok(false))
  • Manually verified the existing test-assert-first-line.js test (which tests assert-long-line.js, a 9.4KB single-line fixture) still produces the correct error message with windowing enabled
  • Manually verified windowing produces identical expression extraction for assert.ok(false), assert['ok'](false), and plain ;-padded assert calls

Fixes: #52677
Jira: N/A (open source contribution)

@nodejs-github-bot nodejs-github-bot added errors Issues and PRs related to JavaScript errors originated in Node.js core. needs-ci PRs that need a full CI run. labels Mar 19, 2026
@jasnell

jasnell commented Mar 22, 2026

Copy link
Copy Markdown
Member

... the error message generation can take seconds to minutes.

Do you have a reproduction case that demonstrates it taking minutes? I find that extremely difficult to believe. Specifically, acorn is generally quite fast and there are hard practical limits already in place that would prevent it from taking "minutes". I think at best this would shave a couple hundred milliseconds off an error path, which is of questionable value given the added complexity.

@jasnell jasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really seeing this as a valid optimization. See prior comment for reasoning.

@Felipeness

Copy link
Copy Markdown
Contributor Author

Hey @jasnell, fair question. I'm working on putting together a proper reproduction case for the timing claim. Will update here once I have it.

Been a bit sidetracked dealing with Windows build issues on another PR, but this is on my list. Thanks for the patience.

@elemental-mind

Copy link
Copy Markdown

@jasnell It's a valid known issue and merging would be highly appreciated.

There is a repro here: #52677 (comment)

Spent an hour trying to figure out why my console freezes for minutes while running my tests when I use assert.ok.

There are a few issues based on this:
privatenumber/tsx#548
#52677
#52962

getFirstExpression() tokenizes the whole source line to find the
expression at the error column. In bundled or minified code that line
is the entire file, so every failing assertion pays an O(line)
tokenize, and the result is not cached.

Extract a window around the target column for lines longer than 2048
characters, cutting at the previous statement boundary so acorn still
receives usable input, and tolerate a window that starts mid-token.

On a single 930KB line, 20 failing assertions go from 1118ms to 34ms.

Refs: nodejs#52677
Refs: nodejs#52962
@Felipeness

Copy link
Copy Markdown
Contributor Author

I owed a repro since March and never delivered it. Here it is, with numbers.

The repro is the one @elemental-mind linked: https://gist.github.com/martinjlowm/aa7d02905f7c49935be0a3bf4819a5f3

Both builds are from b328bf7, same commit, same config. The file is a single 930KB line with the assert call inside that line. "sparse" is semicolons, "dense" is minified code with real identifiers and calls. Each run touches e.message, otherwise the message is never built and the slow path never runs, which is how I fooled myself twice while measuring this.

case without the patch with the patch
1 failure, sparse 64.1ms 17.7ms
1 failure, dense 93.2ms 21.8ms
20 failures, sparse 1118ms (55.9ms each) 34ms (1.7ms each)
20 failures, dense 1175ms (58.8ms each) 54ms (2.7ms each)

@jasnell your estimate is right for a single failure, it is 64ms here, not minutes. What I did not say in March is that the cost is paid per failure and is not cached. A suite with 1000 failing assertions on this file spends about 59s inside getFirstExpression, and it scales linearly with the line length. That is where the "minutes" reports come from.

Assert tests pass, including test-assert-long-line-perf.js.

I squashed the two commits into one and changed the prefix to lib:, which is what the history of error_source.js uses. The diff is unchanged.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.91209% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.12%. Comparing base (b328bf7) to head (05010a4).
⚠️ Report is 1645 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/errors/error_source.js 87.91% 10 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #62338      +/-   ##
==========================================
+ Coverage   89.68%   90.12%   +0.44%     
==========================================
  Files         676      752      +76     
  Lines      206689   252250   +45561     
  Branches    39579    47458    +7879     
==========================================
+ Hits       185370   227343   +41973     
- Misses      13450    16220    +2770     
- Partials     7869     8687     +818     
Files with missing lines Coverage Δ
lib/internal/errors/error_source.js 82.12% <87.91%> (+0.30%) ⬆️

... and 494 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

errors Issues and PRs related to JavaScript errors originated in Node.js core. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

assert.ok: extremely slow error message generation for some code

4 participants