Skip to content

Build debug code frames only when debug output is enabled (faster text-domain stamping) - #51086

Merged
louwie17 merged 3 commits into
trunkfrom
fix/textdomain-stamp-code-frame-perf
Aug 6, 2026
Merged

louwie17 merged 3 commits into
trunkfrom
fix/textdomain-stamp-code-frame-perf

Conversation

@louwie17

@louwie17 louwie17 commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Proposed changes

Stamping translation text domains onto built JavaScript is the single most expensive step in a production build of packages/premium-analytics — more expensive than all of esbuild put together. In the unmodified CI control it takes 11m54s of a 16m29s build; with this change, 2m44s, taking the whole package build to 8m29s.

(Development builds are barely affected — see "What you will not see" below before measuring.)

  • Build Babel's "code frame" debug output only when debug output is actually switched on.
  • Add tests pinning that behaviour, including one that guards the underlying complexity.

In plain terms

When something looks unusual during text-domain stamping, the tool wants to log a helpful message showing the offending line, like a compiler error:

  12 | __( 'Hello world' );
     | ^ Domain argument (index 2) is missing

To produce that little excerpt, Babel has to scan the entire file to work out where line 12 is and what surrounds it.

The code asked for the excerpt like this:

debug( path.buildCodeFrameError( 'Domain argument (index 2) is missing' ).message );

debug(...) does nothing unless you have debug logging switched on. But JavaScript always evaluates the arguments before calling a function — so the excerpt was built every single time, then handed to a function that immediately discarded it.

An analogy: printing and binding a 300-page report, walking it to the shredder, and repeating that 249 times — because the person you meant to give it to isn't in the building.

For one real bundle:

  • the file is 2.4 MB
  • it has 249 translation calls
  • each one triggered a full scan of all 2.4 MB
  • roughly 600 MB of scanning, to build messages nobody reads

The fix checks whether anyone is listening first:

if ( debug.enabled ) {
    debug( path.buildCodeFrameError( 'Domain argument (index 2) is missing' ).message );
}

Debug output is unchanged when you switch it on (DEBUG='@automattic/babel-plugin-replace-textdomain', or DEBUG='*'). The built files are byte-for-byte identical either way — this is purely wasted work being removed.

Why it got bad now, and why it wasn't a problem before

This eager call has been in the plugin since #22077 (Dec 2021), where it was genuinely harmless: the plugin ran over hand-written source files, which are small and whose translation calls already have a text domain — so the expensive branch almost never ran.

#50762 added the stamp-textdomains step, which runs this same plugin over the bundles @wordpress/build (esbuild) emits. Two things changed at once:

  1. The files got huge — bundles are megabytes, because they inline their dependencies.
  2. The expensive branch became the normal case — @wordpress/build strips the text domain from gettext calls, which is the entire reason stamp-textdomains exists. "Domain argument is missing" is no longer an edge case; it is every call.

So this is not a regression in #50762. It is a long-dormant inefficiency that #50762's new (and correct) usage turned into the dominant cost of a production build.

Measurements

Two end-to-end measurements of the real stamp-textdomains step, both on packages/premium-analytics production builds. They differ, and the gap is informative, so both are given.

Author's local measurement, timing the step on its own (342 bundles, nothing else running; results are machine-specific):

Time
trunk 221.1s
this branch 10.6s
20.9x

Independent local reproduction on the exact PR commit (350 stamped files, same unstamped production output for both implementations):

Time
before 21.38s
this branch 11.29s
1.9x

The independent run also confirmed that all 562 generated files were byte-for-byte identical between implementations. The wide difference between local ratios shows that the absolute local speedup depends heavily on the machine and workload.

In CI, in situ — the same step inside Build all projects, where other projects are building concurrently and competing for the runner's cores:

Before After Saved
stamp-textdomains step 714s 164s 550s (4.35x)
Whole packages/premium-analytics build 16m29s 8m29s ~8m00s

Runs: unmodified control · after

The CI ratio is the one worth planning against: a build agent is busy with other projects, so the saved CPU partly goes to whatever else is running rather than straight to wall time. Take ~4x on the step and ~8 minutes off this package as the observed CI figure.

Note the job totals on those two runs are not comparable — the build matrix differs by which projects each PR touches (140 projects vs 122). The per-project comparison is more useful, but is not perfectly controlled either: the control stamped 342 files while the later run stamped 350. Treat the CI figures as strong directional evidence rather than a laboratory benchmark.

Isolating the transform

On the author's machine, the Babel transform alone across all 342 built bundles, current plugin vs patched, with byte-identical output for every file, measured 211.8s → 6.2s (34x). An independent check of the current 2.48 MB dashboard bundle measured 1.52s → a median 0.53s (2.9x), while instrumenting buildCodeFrameError() calls from 234 → 0. These results confirm the mechanism while also showing that the transform ratio is machine-specific.

Once the transform stops dominating, the rest of stamp-textdomains becomes the cost — writing files, re-hashing each sibling .asset.php, regenerating source maps, and the separate manifest pass. That residual is the obvious next lever (parallelising the file loop) if this step needs to get faster still. Out of scope here.

Worst individual bundles, transform only:

Bundle Size Before After
routes/post-detail/content.js 2.4 MB 36.4s 0.54s
routes/dashboard/content.js 2.4 MB 34.9s 0.48s
modules/externals/index.js 2.8 MB 34.0s 0.65s

Two controls confirm the diagnosis rather than merely correlating with it:

  • Parsing and regenerating that same 2.4 MB bundle with no plugin at all takes 0.58s — so Babel is not the problem, the frame-building is.
  • routes/reports/content.js improves only 2.4x, because its translation calls already carry a domain, so the expensive branch rarely fires — exactly what the explanation predicts.

What you will not see

A development build barely changes. jetpack build packages/premium-analytics --deps runs the development build, which does not emit the ~171 minified bundles or run strip-unminified-prod, so there is far less to stamp — @manzoorwanijk measured 95s → 81s that way. All the figures above are production builds (build-production), which is what CI runs and what ships. If you are checking this change by eye, use the production commands in the testing instructions below, or you will conclude it does almost nothing.

The same plugin runs for every package that stamps text domains — forms, activity-log, newsletter, publicize, videopress, seo, scan, jetpack-mu-wpcom and others — so the saving should be broad, but I have not measured those individually.

Does this pull request change what data or activity we track or use?

No.

Testing instructions

1. Unit tests

jetpack test js js-packages/babel-plugin-replace-textdomain

All 43 tests and 51 existing snapshots pass. The snapshots being unchanged is itself the evidence that debug output and transform output are unaffected.

tests/code-frame.test.js covers:

  • no code frames are built when debug output is disabled — while the text domain is still correctly stamped;
  • code frames are still built, and messages still logged, when debug output is enabled;
  • code-frame work does not grow with the number of call sites (the complexity guard — this is the one that would have caught the original problem);
  • output is identical whether debug is on or off;
  • the two rarer logging branches (unmapped domain, non-literal domain) are also covered.

To confirm the tests are not vacuous, revert the if ( debug.enabled ) guard in src/index.js and re-run: 4 of the 6 new tests fail.

2. Timing a real build

Use a production build. A development build will show almost no difference, for the reason given above.

The package's normal build script chains four steps, and the one this PR changes is the third:

clean → build:wp-build → build:stamp-textdomains → build:boot-asset

So run the first two as two separate commands. build:wp-build only bundles — it does not stamp, and on its own will print no stamping output at all. That is deliberate: it leaves the bundles unstamped so the stamp step can then be timed by itself, instead of being buried inside one combined number.
Run this command on both trunk and this branch:

time jetpack build packages/premium-analytics --deps --production

Test the textdomains stamp specifically:

cd projects/packages/premium-analytics

# 1. Build the bundles. This does NOT stamp — no stamping output is expected here.
rm -rf build
NODE_ENV=production pnpm run build:wp-build

# 2. Now stamp them, and time just this. THIS is the step the PR changes.

time pnpm run build:stamp-textdomains

Step 2 should end with a line like:

stamp-textdomains: stamped "jetpack-premium-analytics-pkg" onto 342 file(s) in …; 102 string-bearing bundle(s) listed in the i18n manifest
pnpm run build:stamp-textdomains  15.90s user 0.87s system 158% cpu 10.554 total

Run both steps on trunk and again on this branch, comparing only the step 2 timing. The exact local ratio is machine-dependent: the author's measurement was 221.1s → 10.6s, while an independent reproduction was 21.38s → 11.29s. Inside the measured CI job the improvement was around 4x.

Re-run step 1 before each step 2: stamping is idempotent, so timing it a second time on already-stamped output measures the cheap no-op path and will look fast on both branches.

For the in-situ figure, compare the stamp-textdomains line in the CI Build all projects log between the unmodified control and this branch; that is where 714s → 164s above comes from.

3. Confirming debug output still works

DEBUG='@automattic/babel-plugin-replace-textdomain' pnpm run build:stamp-textdomains

The "Domain argument (index 2) is missing" messages, with code frames, should appear exactly as before. Note that this deliberately restores the slow path — building those frames is the expensive work, and you have asked for them. (DEBUG=1, which Jetpack tooling uses elsewhere, does not match this namespace and leaves the fast path in place.)

`buildCodeFrameError()` renders an excerpt of the surrounding source, so its
cost scales with the size of the file. Passing it straight to `debug()` reads
as free when debug output is off, but JavaScript evaluates call arguments
eagerly, so the frame was built for every matched call either way.

That was cheap while this plugin ran over hand-written source with domains
already present. Since #50762 it also runs over the esbuild bundles that
`stamp-textdomains` post-processes, where the domain is absent by construction
— so the missing-domain branch, the one that builds a frame, became the hot
path on multi-megabyte files.

Measured over premium-analytics' 342 built bundles: 211.8s -> 6.2s (34x), with
byte-identical output for every file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDM1RKuvDmbfyMZspnghaa
@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the fix/textdomain-stamp-code-frame-perf branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/textdomain-stamp-code-frame-perf
bin/jetpack-downloader test jetpack-mu-wpcom-plugin fix/textdomain-stamp-code-frame-perf

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Aug 6, 2026
@jp-launch-control

jp-launch-control Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/js-packages/babel-plugin-replace-textdomain/src/index.js 63/63 (100.00%) 0.00% 0 💚

Full summary · PHP report · JS report

@louwie17
louwie17 marked this pull request as ready for review August 6, 2026 08:40
@louwie17
louwie17 requested a review from a team as a code owner August 6, 2026 08:41
@louwie17
louwie17 requested a review from a team August 6, 2026 08:41
@louwie17 louwie17 changed the title Build debug code frames only when debug output is enabled (34x faster text-domain stamping) Build debug code frames only when debug output is enabled (faster text-domain stamping) Aug 6, 2026
manzoorwanijk
manzoorwanijk previously approved these changes Aug 6, 2026

@manzoorwanijk manzoorwanijk 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.

Thank you. This looks good

Comment thread projects/js-packages/babel-plugin-replace-textdomain/src/index.js Outdated
Fixes the two jsdoc/require-hyphen-before-param-description warnings that
failed the lint check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDM1RKuvDmbfyMZspnghaa
@louwie17
louwie17 merged commit 18e5e56 into trunk Aug 6, 2026
111 of 112 checks passed
@louwie17
louwie17 deleted the fix/textdomain-stamp-code-frame-perf branch August 6, 2026 10:43
@github-actions github-actions Bot removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. [Status] In Progress labels Aug 6, 2026
anomiex added a commit that referenced this pull request Aug 18, 2026
PR #50762 caused a significant rise in build times, which was fixed
by #51086. Let's turn the build times back down after the fix.

This effectively reverts #51029. As for #51059, the progression is
25 before → 40 during the rise → 30 now, to leave some leeway since
full builds are up to ~18 minutes now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants