Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single pass ReplaceSource .node performance improvement #23

Merged
merged 7 commits into from
Aug 29, 2018

Conversation

brentwilton
Copy link
Contributor

@brentwilton brentwilton commented Jul 21, 2017

What kind of change does this PR introduce?
Performance improvement. Up to 35% faster webpack builds on large solutions.
(small solutions do not spend as much time in ReplaceSource)

Did you add tests for your changes?
Added tests to improve coverage around prepending and appending replaces as these were not covered by existing tests.

If relevant, link to documentation update:
N/A

Summary
Replaces the .node method in ReplaceSource with a version which performs all replacements in a single pass.

Does this PR introduce a breaking change?
No

Other information
This PR can produce significantly faster build times when processing large files with devtool: 'source-map'

The existing .node method walks the SourceNode tree for every replace each time, twice splitting the SourceNode tree at the replace start and end. In large source maps this can take significant time as the SourceNode constructor will individually .add each child node on each split.

The new method processes all replaces inline, turning on and off an emit flag depending on if you are in the middle of a replace or not.
The PR emits identical module and source map output as webpack 3.4.1 / webpack-sources 1.0.1. (Including use with ModuleConcatenationPlugin which chains ReplaceSource's)

For our current Angular prod build using devtool: 'source-map' and 2481 modules, this PR reduces the chunk asset optimization time (average over 3 runs) from ~314 seconds to ~127 seconds, and reduces total webpack build time from ~485 seconds to ~314 seconds. (38.5% or 187 seconds faster)

Update ReplaceSource .node to process all replacements in a single pass
@codecov
Copy link

codecov bot commented Jul 21, 2017

Codecov Report

Merging #23 into master will increase coverage by 4.04%.
The diff coverage is 96.15%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #23      +/-   ##
==========================================
+ Coverage      71%   75.04%   +4.04%     
==========================================
  Files          11       11              
  Lines         438      505      +67     
  Branches       66       83      +17     
==========================================
+ Hits          311      379      +68     
+ Misses        127      126       -1
Impacted Files Coverage Δ
lib/ReplaceSource.js 95.85% <96.15%> (+2.99%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b643bfb...cf11460. Read the comment docs.

@brentwilton
Copy link
Contributor Author

@sokra @TheLarkInn please let me know if there is anything additional I can provide to help clarify the issue or to explain how the updated ReplaceSource.node() method works.

We are keen to get this PR merged as it makes our CI webpack builds 3 minutes faster.

@TheLarkInn
Copy link
Member

Hey @brentwilton I'm sorry for not seeing this right away. I'm slowly migrating to Octobox for tracking all of these issues and PR's across our orgs' 100+ repos. :-D

Let me see if I can get this prioritized.

@TheLarkInn TheLarkInn requested a review from sokra August 29, 2017 19:32
@alan-agius4
Copy link

alan-agius4 commented Nov 16, 2017

@sokra Can we have this merged this?

#29

@TheLarkInn
Copy link
Member

This is on my weekend hotlist to merge. Again so sorry for the delay. We want to make sure this doesn't break upstream.

@alan-agius4
Copy link

Reminder!

@brentwilton
Copy link
Contributor Author

@alan-agius4 We have been using a postinstall script in the package.json to manually patch ReplaceSource.js for the last few months while we have been waiting for this PR to be merged.
(I highly recommend implementing this workaround if you have not already. The performance benefits outweigh the hackiness)

With webpack 3.8.1 and the 1.1.0 version of ReplaceSource our CI build takes 47 minutes. With the PR version, it takes 17 minutes. (the patch version was temporarily not being applied on our CI build yesterday and everyone thought that the CI server had broken due to builds taking forever)

@alan-agius4
Copy link

alan-agius4 commented Nov 30, 2017

I dont understand why this pr is not being prioritized and it’s frustrating! I have the same problem. My CI build it takes approx 40mins because of this method.

@sokra
Copy link
Member

sokra commented Dec 1, 2017

This is difficult to review, but I take a look soon.

@brentwilton
Copy link
Contributor Author

Thanks for the update @sokra. I know you guys have been busy the last few months with progressing webpack 4.

To give a bit more information about the cause of the slowdown and the solution:

The current algorithm does the following:
For each replace we:

  1. Traverse the entire source map tree until we find the replace end point
  2. Split the source map tree at that point
  3. Traverse the entire source map tree until we find the replace start point
  4. Split the source map tree at that point
  5. Add the new replace text and the end section
  6. Move onto the next replacement with the remainder of the tree

There are some performance issues with this approach

  • We traverse the entire remaining tree twice per replace
  • The tree is split twice per replace, at the start and end point of the replace
  • When the tree is split, it adds the split sections as children to 2 new SourceNodes
  • When you create a new SourceNode, internally it will type check and array.push each child individually to its children array
  • On large source maps with a lot of replaces this can take minutes and it thrashes memory while doing so.

The new algorithm changes this to only traverse the source node tree a single time making a copy of it to a new tree.

  1. We first sort the list of replacements by start location, and find the first location where we want to replace.
  2. When traversing the tree we look for the first replacements start point, and when we hit that point, we turn an emit flag off and stop copying across to the new tree.
  3. We add the replacement text as the next node in the copy.
  4. We then continue the traversal and look for the replacement end point. On hitting the end point we turn back on the emit flag and start copying the tree again.
  5. When we hit the end point and start emitting again, we then move onto the next replacement in the list and repeat the above until the entire tree is copied.

The ReplacementEnumerator class handles the state of the emit flag and current replacement index.

There are some special cases we also deal with:

  • Some of the replacements occur outside of the original source map. This is usually webpack adding the imports or require wrappers. We run the _prependStartNodes and _appendRemainingNodes steps to handle this and make the main traversal copy code less complex.
  • Replaces can only occur on leaf nodes, and multiple replaces can occur on the same leaf node. Sometimes the start/end points will even overlap so we actually keep recursing into replaceInStringNode after we finish a replace to handle this.
  • If you look in the header() method I included some code which tweaks the output to generate identical source maps to the previous version. Identical source map output is good to show we didn't break anything in the change.

We had a situation where a single ReplaceSource.node call was taking 15 minutes on a complex build. The new algorithm reduced that call to 0.12 seconds.

@vladimir-tikhonov
Copy link

Hello, @brentwilton! I've tried use your branch on our project, but unfortunately found a bug. We use redux-action package and now webpack produces invalid js code when devtool is set to "source-map". Seems like this file is causing issues: https://github.com/reduxactions/redux-actions/blob/master/src/camelCase.js. I've created a repro repository for you here: https://github.com/vladimir-tikhonov/webpack-sources-bug. Let me know if you need something else.

@brentwilton
Copy link
Contributor Author

Hi @vladimir-tikhonov. Thanks for trying this PR out and submitting a repro of your issue.
I will pull it down and isolate the cause.

@brentwilton
Copy link
Contributor Author

@vladimir-tikhonov I have pushed a new version. Can you check to see if it resolves your issue ?

The code was double emitting the ); if it reached the end of the source map while in the middle of a replace.

@vladimir-tikhonov
Copy link

@brentwilton yes, the issue is resolved now. Great job!

@alan-agius4
Copy link

Can we have this re-reviewed please?

@TheLarkInn
Copy link
Member

TheLarkInn commented Jan 10, 2018 via email

Copy link
Member

@ooflorent ooflorent left a comment

Choose a reason for hiding this comment

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

Looks good to me. It also passes all webpack's integration tests.

}

_sortReplacementsAscending() {
this.replacements.sort(function(a, b) {
Copy link
Member

Choose a reason for hiding this comment

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

Could you define the sort function outside of the class?

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@brentwilton brentwilton Mar 2, 2018

Choose a reason for hiding this comment

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

@ooflorent Moved sort function outside of class as requested.

@alan-agius4
Copy link

Can we merge this obe? Super imporant for me as I have a huge app

@TheLarkInn
Copy link
Member

I think we are waiting on @ooflorent's request, and I'd like @sokra to review as well for this. Given we just released [email protected] we need to make sure this doesn't cause any breaking changes (even if tests passing).

@abhinavsingi
Copy link

abhinavsingi commented Mar 31, 2018

Any idea when will it be merged?
@TheLarkInn @sokra

@ghost
Copy link

ghost commented Apr 12, 2018

Is there any update on getting this issue merged?

@TheLarkInn @sokra

Thanks!

@alan-agius4
Copy link

Ping!

@ghost
Copy link

ghost commented Apr 24, 2018

:tumbleweed:

@vohzd
Copy link

vohzd commented Apr 26, 2018

Is this getting merged? I mean who doesn't want faster compilation times :D

@rpellerin
Copy link

Would love to see this merged too!

@montogeek
Copy link
Member

@ooflorent @sokra Can we merge this one? It seems it works :)

@alan-agius4
Copy link

1yr later and this PR is not merged yet! This is just ridiculous.

@TheAlexLichter
Copy link

Would be awesome to see this merged! 🙏

@mlavina
Copy link

mlavina commented Aug 27, 2018

Hey @sokra @ooflorent

Just checking in as well if this can be merged in and released soon?

@sokra sokra merged commit b141232 into webpack:master Aug 29, 2018
@sokra
Copy link
Member

sokra commented Aug 29, 2018

🎉 finally merged

Sorry for the delay, this was difficult to review

@sokra
Copy link
Member

sokra commented Aug 29, 2018

and released as 1.2.0

@alan-agius4
Copy link

alan-agius4 commented Aug 29, 2018 via email

@eranimo
Copy link

eranimo commented Aug 29, 2018

Is it possible to create a beta release version of webpack with this new version?

@alexander-akait
Copy link
Member

@eranimo why? It is not major release, just update your lock file. Look on https://github.com/webpack/webpack/blob/master/package.json#L32

@edmorley
Copy link

This decreased the time taken for our warm (babel-loader and uglify-webpack-plugin caches enabled) webpack 4 --mode production devtool: 'source-map' builds by 30%. Much appreciated - thank you :-)

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.