Skip to content

perf: track sorted state incrementally in ReplaceSource#241

Closed
xiaoxiaojx wants to merge 1 commit into
mainfrom
perf/replace-source-incremental-sort-tracking
Closed

perf: track sorted state incrementally in ReplaceSource#241
xiaoxiaojx wants to merge 1 commit into
mainfrom
perf/replace-source-incremental-sort-tracking

Conversation

@xiaoxiaojx

Copy link
Copy Markdown
Member

Summary

  • Instead of unconditionally setting _isSorted = false on every replace()/insert() call, compare the new element against the last one
  • When replacements are added in source order (the common case in webpack, since AST traversal is position-ordered), the array stays marked as sorted and _sortReplacements() becomes a no-op flag check
  • For genuinely unordered input, behavior is identical to before — the flag gets set to false and the normal Array.prototype.sort() runs

Benchmark results (ops/s, higher is better)

Benchmark Before After Change
getReplacements() 7,637 13,643 +78.7%
source() (1 replacement) 101,919 107,040 +5.0%
source() (1000 small replacements) 6,135 6,609 +7.7%
source() (few large replacements) 10,158 10,497 +3.3%
replace() x1000 244,693 218,742 -10.6%
insert() x1000 244,665 216,147 -11.7%

The replace()/insert() per-call overhead (~11ns) is a deliberate trade-off: downstream consumers (source(), map(), streamChunks()) skip an entire O(n log n) sort when data is already ordered.

All 89,869 tests pass.

Test plan

  • yarn lint passes
  • npx jest --no-coverage — 89,869 tests pass
  • Benchmark verified with npm run benchmark

🤖 Generated with Claude Code

Instead of unconditionally setting _isSorted = false on every
replace()/insert() call, compare the new element against the last
one. When replacements are added in source order (the common case
in webpack, since AST traversal is position-ordered), the array
stays marked as sorted and _sortReplacements() becomes a no-op
flag check.

Benchmark results (ops/s, higher is better):

  getReplacements():         7,637 → 13,643  (+78.7%)
  source() (1 replacement): 101,919 → 107,040  (+5.0%)
  source() (1000 small):    6,135 → 6,609  (+7.7%)
  replace() x1000:          244,693 → 218,742  (-10.6%, per-call check)
  insert() x1000:           244,665 → 216,147  (-11.7%, per-call check)

The replace/insert per-call overhead (~11ns) is a deliberate
trade-off: downstream consumers (source, map, streamChunks) skip
an entire O(n log n) sort when data is already ordered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bf5f0f3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-sources Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.54%. Comparing base (e02802e) to head (bf5f0f3).

Files with missing lines Patch % Lines
lib/ReplaceSource.js 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #241      +/-   ##
==========================================
- Coverage   97.58%   97.54%   -0.04%     
==========================================
  Files          25       25              
  Lines        2069     2078       +9     
  Branches      668      672       +4     
==========================================
+ Hits         2019     2027       +8     
- Misses         47       48       +1     
  Partials        3        3              
Flag Coverage Δ
integration 97.54% <92.30%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 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.

@xiaoxiaojx xiaoxiaojx marked this pull request as draft June 23, 2026 16:06
@codspeed-hq

codspeed-hq Bot commented Jun 23, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×2.3

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 15 improved benchmarks
❌ 11 regressed benchmarks
✅ 185 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory compat-source memory: delegated source() + map() through wrapper 11.9 KB 30.3 KB -60.83%
Memory original-source memory: sourceAndMap({ columns: true }) 1.3 MB 2.6 MB -50.38%
Memory clear-cache memory: shared modules (visited set — single allocation) 322.1 KB 511.3 KB -37%
Memory raw-source memory: new RawSource(buffer) 784 B 1,168 B -32.88%
Memory cached-source memory: getCachedData() allocates BufferedMap 520 B 712 B -26.97%
Memory clear-cache memory: unique tasks (clearCache default) 2.8 MB 3.6 MB -23.97%
Simulation replace-source: insert() x1000 622 µs 739.8 µs -15.93%
Memory source-map-source memory: map({ columns: true }) 880 B 1,040 B -15.38%
Simulation replace-source: replace() x1000 636 µs 745.8 µs -14.73%
Simulation cached-source: new CachedSource() 375.1 µs 432.7 µs -13.31%
Memory concat-source memory: map({ columns: true }) composes child maps 3.3 MB 3.7 MB -10.53%
Memory original-source memory: map({ columns: false }) line-only mappings 772.3 KB 2 KB ×380
Memory prefix-source memory: new PrefixSource() 151 KB 1.4 KB ×110
Memory source-map-source memory: new SourceMapSource(simple) 40.8 KB 1.2 KB ×35
Memory original-source memory: new OriginalSource() 160.5 KB 7.8 KB ×21
Memory replace-source memory: construct + 100 insertions 11.2 KB 2.2 KB ×5.1
Memory size-only-source memory: new SizeOnlySource() 346 KB 104.6 KB ×3.3
Memory clear-cache memory: shared modules (no visited set — allocates per chunk) 995 KB 392.8 KB ×2.5
Memory concat-source memory: source() concatenates children 276.9 KB 115 KB ×2.4
Memory compat-source memory: new CompatSource(sourceLike) 96.8 KB 41 KB ×2.4
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/replace-source-incremental-sort-tracking (bf5f0f3) with main (e02802e)

Open in CodSpeed

@xiaoxiaojx

Copy link
Copy Markdown
Member Author

Closing this PR after further analysis — the optimization is not net-positive in realistic scenarios.

Why:

  • The per-call overhead added to replace()/insert() (~11ns each) is universal — it applies to every call regardless of whether the sort is later needed
  • The benefit (skipping _sortReplacements()) is conditional — it only helps when replacements happen to be added in order AND the consumer calls source()/map()/getReplacements() afterward
  • In the realistic pipeline benchmark (which simulates actual webpack chunk compilation), the net effect was slightly negative (-2.4%), meaning the per-call cost outweighs the sort savings in practice
  • CodSpeed Simulation confirms: replace() x1000 regressed -14.7%, insert() x1000 regressed -15.9%, with no offsetting improvements in the end-to-end paths

The _sortReplacements() call on already-sorted data is O(n) in V8's TimSort, which turns out to be cheap enough that it's not worth adding per-call tracking overhead to avoid.

@xiaoxiaojx xiaoxiaojx closed this Jun 23, 2026
@xiaoxiaojx xiaoxiaojx deleted the perf/replace-source-incremental-sort-tracking branch June 23, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant