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

TestSequencer heuristic using dependency tree file sizes #7553

Closed

Conversation

jeysal
Copy link
Contributor

@jeysal jeysal commented Dec 26, 2018

Summary

In https://www.youtube.com/watch?v=3YDiloj8_d0, @cpojer mentioned that the TestSequencer heuristics aside from failures/durations from previous runs could use some improvements.
This PR improves the existing "file size" heuristic that is used as a fallback when there is no information from previous runs available by calculating the size of the whole dependency tree of a test in order to schedule more complex tests first.
Note that this PR does not touch on the idea of "test priority" based on changed files that is also mentioned in the video.

Test plan

Edit: See comments below for more up-to-date information.

I used this script on a few small & large open source projects that came to my mind that use Jest.
The script diffs the test order without a cache (which is determined by the file size heuristic) against the test order after one full Jest run (which is determined by the test durations) and counts the number of lines that diff prints, which serves as a rough approximation of how close we got to the best test order (lower is better)

project: master (median of 4 runs) / this PR (median of 4 runs)
redux: 14 / 12
downshift: 27 / 25
recompose: 75.5 / 75
gatsby: 225.5 / 225
react: 370 / 363.5
jest: 598 / 585.5
prettier: 1816 / 1812.5

The difference might not seem like a lot (and tbh I initially hoped for more), but in hindsight I actually think this is pretty good. The larger projects showed quite consistently improved numbers. I hacked together the alternative approaches "counting test/it occurrences" and "number of files in dependency graph" and they showed no significant improvements over master at all, so I think this is pretty much as good as you can get with a reasonably simple heuristic.

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch from 8e2a26a to 1e0f863 Compare December 26, 2018 22:02
@cpojer
Copy link
Member

cpojer commented Dec 27, 2018

Wow this is awesome. I’ll take a closer look after the holidays. Thank you for building this, and nice wins!

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch from 1e0f863 to 9e26ade Compare December 27, 2018 20:27
@jeysal
Copy link
Contributor Author

jeysal commented Dec 27, 2018

fixed tests on Windows (hopefully)

@codecov-io
Copy link

codecov-io commented Dec 27, 2018

Codecov Report

Merging #7553 into master will increase coverage by 0.06%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #7553      +/-   ##
==========================================
+ Coverage   68.27%   68.33%   +0.06%     
==========================================
  Files         252      252              
  Lines        9682     9703      +21     
  Branches        6        5       -1     
==========================================
+ Hits         6610     6631      +21     
  Misses       3070     3070              
  Partials        2        2
Impacted Files Coverage Δ
packages/jest-cli/src/TestSequencer.js 100% <100%> (ø) ⬆️
packages/jest-resolve-dependencies/src/index.js 98.14% <100%> (+0.42%) ⬆️

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 9243e69...07cbcc8. Read the comment docs.

@jeysal
Copy link
Contributor Author

jeysal commented Dec 28, 2018

I just realized that the metric I used to measure the gains is garbage.
Diffing the test order before and after always produces roughly 2 * number of test files lines.
I'll come up with a better metric and then try a few heuristic approaches again.
Marking this as WIP meanwhile

@jeysal jeysal changed the title TestSequencer heuristic using dependency tree file sizes WIP TestSequencer heuristic using dependency tree file sizes Dec 28, 2018
@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch from 9e26ade to 8ea7d91 Compare December 31, 2018 00:10
@jeysal
Copy link
Contributor Author

jeysal commented Dec 31, 2018

Alright, so the new metric in the gist uses the sum of the difference between the index of each test file in the cold vs warm sequence.
The current version after adding handling for tests that depend on child_process, fs etc. produces the following results (all medians of 5 runs):

          downshift redux recompose gatsby  prettier  react jest
master    20        14    228       2862    188328    5550  28860
PR	      20        12    228       2312    187066    6854  17352

@jeysal jeysal changed the title WIP TestSequencer heuristic using dependency tree file sizes TestSequencer heuristic using dependency tree file sizes Dec 31, 2018
@SimenB
Copy link
Member

SimenB commented Dec 31, 2018

What's up with React? Do you know if any test(s) in particular fools the heuristic?

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch 2 times, most recently from c2817d8 to 9adb791 Compare December 31, 2018 13:37
// If a test depends on one of these core modules,
// we assume that the test may be slower because it is an "integration test"
// that spawn child processes, accesses the file system, etc.
const coreModuleWeights = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any ideas on other core modules we could include here?
I just picked some arbitrary modules that came to my mind and they caused quite an improvement, unfortunately running the benchmark on a few projects takes quite some time so I can't try out all the modules.

Copy link
Member

Choose a reason for hiding this comment

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

https if http is worth it

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we extend this comment with the information on how's 10000 different than 100000? It's not obvious if the weight is size in bytes or maybe something else?

@jeysal
Copy link
Contributor Author

jeysal commented Dec 31, 2018

What's up with React? Do you know if any test(s) in particular fools the heuristic?

I'll take a look

@jeysal
Copy link
Contributor Author

jeysal commented Dec 31, 2018

Looks like for the most part the new heuristic overestimates the duration of the react-dom tests and severely underestimates the duration of the react-reconciler as well as some ESLint-related tests.

I think it's just that ReactDOM is huge (almost all the ReactDOM tests have size >800000 in recursive dependencies) and most of its tests depend on the whole thing, but of course don't use all of it.
Most of the reconciler tests are between 600000 and 800000.

@cpojer
Copy link
Member

cpojer commented Jan 2, 2019

I just took a look at this and I love that it brings real speed ups for some projects, that's awesome. I'm a little bit worried about projects with large amounts of tests and gigantic dependency trees as we'd stat for the file size of the entire repo every single time Jest is invoked. What if we cap the file size calls at one or two levels in? What if we apply some weight to tests with large dependency trees (if a file has more dependencies than 90% (?) of all tests, it is probably slower, etc.)? I want us to be conscious about the time Jest optimizing a test run as it can be counter productive on really large repos.

@SimenB
Copy link
Member

SimenB commented Jan 2, 2019

I'm a little bit worried about projects with large amounts of tests and gigantic dependency trees as we'd stat for the file size of the entire repo every single time Jest is invoked.

Thoughts on my suggestion to stick it in hastefs? We already stat as part of jest-haste-map's crawl. Or would that use too much memory? A number is just 64 bits so maybe not

@cpojer
Copy link
Member

cpojer commented Jan 2, 2019 via email

@SimenB
Copy link
Member

SimenB commented Jan 2, 2019

Yeah, watchman includes the file size, while the node crawler (and watch) includes stat

@jeysal
Copy link
Contributor Author

jeysal commented Jan 2, 2019

Alright, I can try to implement @SimenB's suggestion to eliminate the I/O overhead.
If we still have problems with the actual computation time it takes to recurse into the dependency graph, I see two possible optimizations.

The easy one: Cache _fileSizeRecurseDependencies results (Because it is used as a comparator in sort, it is called with the same path multiple times).

The hard one: Implement a proper algorithm to calculate all the recursive sizes, something like:

  1. Condensate the dependency graph to a DAG of strongly connected components - O(|V|+|E|)
  2. Traverse from leaf nodes to root nodes, setting the size of each node to the sum of its successors plus its own file size(s) - O(|V|+|E|) because of topological sort

@cpojer
Copy link
Member

cpojer commented Jan 3, 2019

Let's see where we land with file sizes stored in the haste map first, and then we can optimize things in a separate PR further :)

@jeysal jeysal mentioned this pull request Jan 5, 2019
@jeysal
Copy link
Contributor Author

jeysal commented Jan 5, 2019

opened #7580 for storing file sizes in haste map file metadata

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch from 9adb791 to 95d2c7a Compare January 9, 2019 12:35
@jeysal
Copy link
Contributor Author

jeysal commented Jan 9, 2019

Rebased on master, integrating the hasteFS file sizes.
So do we merge it this way and then see if anyone complains about experiencing a significant performance hit?
On projects like React, the TestSequencer does noticeably take a few hundred ms (only on cold runs ofc), but those are also the projects where the heuristic can achieve the biggest gains, for example a few seconds for React by, umm, not scheduling the longest running test ReactClassEquivalence as one of the last tests to run like Jest on master currently does :D

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch from 95d2c7a to 2d93ff4 Compare January 9, 2019 16:00
@jeysal
Copy link
Contributor Author

jeysal commented Jan 9, 2019

@SimenB implemented both of your suggestions, thanks!

@SimenB SimenB requested review from cpojer, mjesun and rubennorte January 9, 2019 16:15

const fileSize = (path): number => {
const cachedSize = sizes.get(path);
if (cachedSize != null) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (cachedSize != null) {
if (sizes.has(path)) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem with that is the Flow types - Flow will think the size can be null.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The problem with that is the Flow types - Flow will think the size can be null

Not sure why you need cachedSize at all, sizes is always defined and you should be good using @SimenB's code (plus removing line 79), no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what I mean. (linked issue is for TS, but applies to Flow as well).
The return type of Map<Path, number>.get is number | void, not number, even if you've done a has check previously.

@rubennorte
Copy link
Contributor

I'll release a new alpha now and test it internally to check the impact of the size addition to the haste map. I'll come back to you once it's done.

_fileSizeRecurseDependencies(test: Test, sizes: Map<Path, number>): number {
const {resolver, hasteFS, config} = test.context;

const fileSize = (path): number => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

by passing sizes to this fn, you could extract it outside of _fileSizeRecurseDependencies, so it doesn't have to be recreated multiple times

Copy link
Contributor Author

@jeysal jeysal Jan 9, 2019

Choose a reason for hiding this comment

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

Note that it also uses resolver and hasteFS, so fileSize would need the whole test instead of just the path as its argument as well. Not sure if that's worth it?
Edit: Actually the resolver and hasteFS themselves as arguments, because fileSize is also called on non-test paths.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yup, so it can be a class method, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Converted it to a class method, although IMHO it's less readable now.


const fileSize = (path): number => {
const cachedSize = sizes.get(path);
if (cachedSize != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The problem with that is the Flow types - Flow will think the size can be null

Not sure why you need cachedSize at all, sizes is always defined and you should be good using @SimenB's code (plus removing line 79), no?

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch 2 times, most recently from fe7a4cd to 350e12a Compare January 10, 2019 13:04
@jeysal
Copy link
Contributor Author

jeysal commented Jan 16, 2019

test it internally to check the impact of the size addition to the haste map

@rubennorte do you have any news on this yet? :)

@jeysal jeysal mentioned this pull request Jan 21, 2019
@rubennorte
Copy link
Contributor

@jeysal sorry it took me so long. The change in the haste map is good perf-wise. I'd wait for the release of 24 before merging this. We can release it as an alpha to test it and then as a minor if everything looks good. Thanks for working on this!

@jeysal
Copy link
Contributor Author

jeysal commented Jan 23, 2019

Agreed, best to include this in a minor, no reason for it to be in 24.0 👍

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch from 350e12a to 07cbcc8 Compare January 27, 2019 12:55
@jeysal
Copy link
Contributor Author

jeysal commented Jan 27, 2019

Rebased on master.
@rubennorte do you have a Jest project at FB with an absurdly large (much bigger than React) dependency tree and amount of tests that you could use to check if sorting the tests on an empty cache takes too long? It would be noticeable because the gray "Determining test suites to run..." message appears longer than it does with Jest master.

@jeysal
Copy link
Contributor Author

jeysal commented Feb 7, 2019

To avoid this stalling much longer: Given that there were no significant performance issues with the large open source projects using Jest that I tested, shall we merge it and address the potential perf improvements suggested if someone reports a problem with some project? (I'd give it a quick rebase) @SimenB

@cpojer
Copy link
Member

cpojer commented Feb 8, 2019

Seems like this needs another rebase.

I would advise against merging this for now. I trust that perf is good, but we should verify this on the largest codebase using Jest in the world first, otherwise we'll stall Jest releases later. I'd prefer to stall a PR until we get around to testing it instead of stalling a release of Jest when we realize something is causing problems. Hope that makes sense to you.

@jeysal jeysal force-pushed the test-sequencer-dependency-file-sizes branch from 07cbcc8 to 2920417 Compare February 8, 2019 10:58
@jeysal
Copy link
Contributor Author

jeysal commented Feb 8, 2019

rebased

@jeysal jeysal closed this Jun 20, 2019
@github-actions
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants