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

Speed up npm ci by caching node_modules #45932

Merged
merged 8 commits into from
Mar 16, 2023

Conversation

kevin940726
Copy link
Member

@kevin940726 kevin940726 commented Nov 21, 2022

What?

Speed up the CI pipeline by caching node_modules.

Why?

Faster CI === Happier developers? ❤️
Slightly related to #33532, #44679, and other similar performance related issues.

The official setup-node action by default only caches the global cache (~/.npm in *nix). However, it's still taking roughly 2 minutes to install all the packages in every workflow. By also caching node_modules, we can dramatically cut down the installation time by 75%.

Note that in the official document, it's not recommended to cache node_modules.

Note: It is not recommended to cache node_modules, as it can break across Node versions and won't work with npm ci

However,

  1. We rarely change our Node versions.
  2. It won't work with npm ci because npm ci will delete node_modules before installing the modules. We can simply avoid running npm ci if there's a cache hit.

TBH, I'm not really sure why it's not more of an established pattern. Probably someone knows better?

How?

I'm abstracting the steps into a composite action called setup-node. It roughly does the following:

  1. Setup Node.js to the desired version.
  2. Cache global npm cache (~/.npm) to speed up download time.
  3. Cache node_modules to speed up installation time.
  4. If the cache missed in step 3, run npm ci --ignore-scripts to download the modules.
  5. Rebuild the dependencies (npm rebuild) if necessary.
  6. Run npm run postinstall.

Then, we just have to replace every actions/setup-node + npm ci duo with this composite action setup-node in each workflow. Note that this action doesn't help if the workflow doesn't need to run npm ci though. What's good is that such workflows can continue using the original action/setup-node since they share the same global npm cache.

Testing Instructions

The result can be verified by comparing the installation time in the workflows.

An example run in this PR took 1m 57s to run the composite action when the cache missed. Re-running the job again, it then only took 20s to run the same composite action when the cache hit.

Compare that to trunk. The equivalent actions duo (actions/setup-node + npm ci) took 2m 5s to run when the cache hit. We can draw this table below with limited source data:

Step duration in seconds Cache miss Cache hit
Caching only global npm cache (trunk) 138s 125s
Also caching node_modules (this PR) 117s1 20s

Even though sometimes the duration varies a lot between runs, the performance improvement is still undeniably outstanding.

Trade-offs

The improvement is obviously not free, there's always a cost. In this case, the main cost is the cache size. The original global npm cache has a size of (roughly) 270MB per key. On top of that, this PR adds an additional 270MB of node_modules cache per key. Because node_modules won't work across different OSes or Node versions, we will have to also cache them for Windows, macOS, etc. They are adding up and will soon exceed the 10GB threshold of the maximum cache size in a repository.

At the time of writing, we currently have 77 caches (counting these node_modules cache) and the least used cache is only 3 days old. If we're not careful, we could cause cache thrashing and defeat the whole purpose of caching.

One possible solution to this is to avoid caching node_modules or fallback to a common cache in some of the less-used situations. For instance, we could try grouping all OSes in the same key, but require Windows and macOS to always re-run npm rebuild after restoring the cache. The same could also apply to different Node versions as well. This technique is potentially unsafe though, and might cause confusion to contributors if the cache fails.

With that being said, I think the additional total of 1GB of cache is still worth the time save. We can monitor the impact of these caches for a while to be sure.

Screenshots or screencast

image

Footnotes

  1. There's a post action below that took 1m 29s. It's likely a race condition issue when multiple jobs were trying to save the same cache.

@kevin940726 kevin940726 added the GitHub Actions Pull requests that update GitHub Actions code label Nov 21, 2022
@codesandbox
Copy link

codesandbox bot commented Nov 21, 2022

CodeSandbox logoCodeSandbox logo  Open in CodeSandbox Web Editor | VS Code | VS Code Insiders

@github-actions
Copy link

github-actions bot commented Nov 21, 2022

Size Change: +223 B (0%)

Total Size: 1.34 MB

Filename Size Change
build/block-editor/index.min.js 197 kB +50 B (0%)
build/block-library/index.min.js 201 kB +61 B (0%)
build/core-data/index.min.js 16.3 kB +48 B (0%)
build/edit-post/index.min.js 34.8 kB -2 B (0%)
build/edit-post/style-rtl.css 7.55 kB +18 B (0%)
build/edit-post/style.css 7.54 kB +17 B (0%)
build/edit-site/index.min.js 64.9 kB -3 B (0%)
build/edit-site/style-rtl.css 10.1 kB +8 B (0%)
build/edit-site/style.css 10.1 kB +8 B (0%)
build/edit-widgets/style-rtl.css 4.56 kB +9 B (0%)
build/edit-widgets/style.css 4.56 kB +9 B (0%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 993 B
build/annotations/index.min.js 2.78 kB
build/api-fetch/index.min.js 2.27 kB
build/autop/index.min.js 2.15 kB
build/blob/index.min.js 483 B
build/block-directory/index.min.js 7.2 kB
build/block-directory/style-rtl.css 1.04 kB
build/block-directory/style.css 1.04 kB
build/block-editor/content-rtl.css 4.11 kB
build/block-editor/content.css 4.1 kB
build/block-editor/default-editor-styles-rtl.css 403 B
build/block-editor/default-editor-styles.css 403 B
build/block-editor/style-rtl.css 14.4 kB
build/block-editor/style.css 14.4 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 90 B
build/block-library/blocks/archives/style.css 90 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 122 B
build/block-library/blocks/audio/style.css 122 B
build/block-library/blocks/audio/theme-rtl.css 138 B
build/block-library/blocks/audio/theme.css 138 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 91 B
build/block-library/blocks/avatar/style.css 91 B
build/block-library/blocks/block/editor-rtl.css 305 B
build/block-library/blocks/block/editor.css 305 B
build/block-library/blocks/button/editor-rtl.css 587 B
build/block-library/blocks/button/editor.css 587 B
build/block-library/blocks/button/style-rtl.css 628 B
build/block-library/blocks/button/style.css 627 B
build/block-library/blocks/buttons/editor-rtl.css 337 B
build/block-library/blocks/buttons/editor.css 337 B
build/block-library/blocks/buttons/style-rtl.css 332 B
build/block-library/blocks/buttons/style.css 332 B
build/block-library/blocks/calendar/style-rtl.css 239 B
build/block-library/blocks/calendar/style.css 239 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 100 B
build/block-library/blocks/categories/style.css 100 B
build/block-library/blocks/code/editor-rtl.css 53 B
build/block-library/blocks/code/editor.css 53 B
build/block-library/blocks/code/style-rtl.css 121 B
build/block-library/blocks/code/style.css 121 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 199 B
build/block-library/blocks/comment-template/style.css 198 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 840 B
build/block-library/blocks/comments/editor.css 839 B
build/block-library/blocks/comments/style-rtl.css 637 B
build/block-library/blocks/comments/style.css 636 B
build/block-library/blocks/cover/editor-rtl.css 612 B
build/block-library/blocks/cover/editor.css 613 B
build/block-library/blocks/cover/style-rtl.css 1.6 kB
build/block-library/blocks/cover/style.css 1.59 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 410 B
build/block-library/blocks/embed/style.css 410 B
build/block-library/blocks/embed/theme-rtl.css 138 B
build/block-library/blocks/embed/theme.css 138 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 265 B
build/block-library/blocks/file/style.css 265 B
build/block-library/blocks/file/view.min.js 353 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 984 B
build/block-library/blocks/gallery/editor.css 988 B
build/block-library/blocks/gallery/style-rtl.css 1.55 kB
build/block-library/blocks/gallery/style.css 1.55 kB
build/block-library/blocks/gallery/theme-rtl.css 122 B
build/block-library/blocks/gallery/theme.css 122 B
build/block-library/blocks/group/editor-rtl.css 654 B
build/block-library/blocks/group/editor.css 654 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 332 B
build/block-library/blocks/html/editor.css 333 B
build/block-library/blocks/image/editor-rtl.css 830 B
build/block-library/blocks/image/editor.css 829 B
build/block-library/blocks/image/style-rtl.css 652 B
build/block-library/blocks/image/style.css 652 B
build/block-library/blocks/image/theme-rtl.css 137 B
build/block-library/blocks/image/theme.css 137 B
build/block-library/blocks/latest-comments/style-rtl.css 357 B
build/block-library/blocks/latest-comments/style.css 357 B
build/block-library/blocks/latest-posts/editor-rtl.css 213 B
build/block-library/blocks/latest-posts/editor.css 212 B
build/block-library/blocks/latest-posts/style-rtl.css 478 B
build/block-library/blocks/latest-posts/style.css 478 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 507 B
build/block-library/blocks/media-text/style.css 505 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 716 B
build/block-library/blocks/navigation-link/editor.css 715 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 299 B
build/block-library/blocks/navigation-submenu/editor.css 299 B
build/block-library/blocks/navigation/editor-rtl.css 2.13 kB
build/block-library/blocks/navigation/editor.css 2.14 kB
build/block-library/blocks/navigation/style-rtl.css 2.22 kB
build/block-library/blocks/navigation/style.css 2.2 kB
build/block-library/blocks/navigation/view-modal.min.js 2.81 kB
build/block-library/blocks/navigation/view.min.js 447 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 401 B
build/block-library/blocks/page-list/editor.css 401 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 174 B
build/block-library/blocks/paragraph/editor.css 174 B
build/block-library/blocks/paragraph/style-rtl.css 279 B
build/block-library/blocks/paragraph/style.css 281 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 501 B
build/block-library/blocks/post-comments-form/style.css 501 B
build/block-library/blocks/post-date/style-rtl.css 61 B
build/block-library/blocks/post-date/style.css 61 B
build/block-library/blocks/post-excerpt/editor-rtl.css 71 B
build/block-library/blocks/post-excerpt/editor.css 71 B
build/block-library/blocks/post-excerpt/style-rtl.css 134 B
build/block-library/blocks/post-excerpt/style.css 134 B
build/block-library/blocks/post-featured-image/editor-rtl.css 586 B
build/block-library/blocks/post-featured-image/editor.css 584 B
build/block-library/blocks/post-featured-image/style-rtl.css 322 B
build/block-library/blocks/post-featured-image/style.css 322 B
build/block-library/blocks/post-navigation-link/style-rtl.css 153 B
build/block-library/blocks/post-navigation-link/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 282 B
build/block-library/blocks/post-template/style.css 282 B
build/block-library/blocks/post-terms/style-rtl.css 96 B
build/block-library/blocks/post-terms/style.css 96 B
build/block-library/blocks/post-title/style-rtl.css 100 B
build/block-library/blocks/post-title/style.css 100 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 135 B
build/block-library/blocks/pullquote/editor.css 135 B
build/block-library/blocks/pullquote/style-rtl.css 326 B
build/block-library/blocks/pullquote/style.css 325 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 288 B
build/block-library/blocks/query-pagination/style.css 284 B
build/block-library/blocks/query-title/style-rtl.css 63 B
build/block-library/blocks/query-title/style.css 63 B
build/block-library/blocks/query/editor-rtl.css 463 B
build/block-library/blocks/query/editor.css 463 B
build/block-library/blocks/quote/style-rtl.css 222 B
build/block-library/blocks/quote/style.css 222 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 149 B
build/block-library/blocks/rss/editor.css 149 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 409 B
build/block-library/blocks/search/style.css 406 B
build/block-library/blocks/search/theme-rtl.css 114 B
build/block-library/blocks/search/theme.css 114 B
build/block-library/blocks/separator/editor-rtl.css 146 B
build/block-library/blocks/separator/editor.css 146 B
build/block-library/blocks/separator/style-rtl.css 234 B
build/block-library/blocks/separator/style.css 234 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 474 B
build/block-library/blocks/shortcode/editor.css 474 B
build/block-library/blocks/site-logo/editor-rtl.css 489 B
build/block-library/blocks/site-logo/editor.css 489 B
build/block-library/blocks/site-logo/style-rtl.css 203 B
build/block-library/blocks/site-logo/style.css 203 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 116 B
build/block-library/blocks/site-title/editor.css 116 B
build/block-library/blocks/site-title/style-rtl.css 57 B
build/block-library/blocks/site-title/style.css 57 B
build/block-library/blocks/social-link/editor-rtl.css 184 B
build/block-library/blocks/social-link/editor.css 184 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.4 kB
build/block-library/blocks/social-links/style.css 1.39 kB
build/block-library/blocks/spacer/editor-rtl.css 332 B
build/block-library/blocks/spacer/editor.css 332 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 433 B
build/block-library/blocks/table/editor.css 433 B
build/block-library/blocks/table/style-rtl.css 651 B
build/block-library/blocks/table/style.css 650 B
build/block-library/blocks/table/theme-rtl.css 157 B
build/block-library/blocks/table/theme.css 157 B
build/block-library/blocks/tag-cloud/style-rtl.css 251 B
build/block-library/blocks/tag-cloud/style.css 253 B
build/block-library/blocks/template-part/editor-rtl.css 404 B
build/block-library/blocks/template-part/editor.css 404 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 99 B
build/block-library/blocks/verse/style.css 99 B
build/block-library/blocks/video/editor-rtl.css 552 B
build/block-library/blocks/video/editor.css 555 B
build/block-library/blocks/video/style-rtl.css 179 B
build/block-library/blocks/video/style.css 179 B
build/block-library/blocks/video/theme-rtl.css 139 B
build/block-library/blocks/video/theme.css 139 B
build/block-library/classic-rtl.css 179 B
build/block-library/classic.css 179 B
build/block-library/common-rtl.css 1.11 kB
build/block-library/common.css 1.11 kB
build/block-library/editor-elements-rtl.css 75 B
build/block-library/editor-elements.css 75 B
build/block-library/editor-rtl.css 11.6 kB
build/block-library/editor.css 11.6 kB
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/style-rtl.css 12.7 kB
build/block-library/style.css 12.7 kB
build/block-library/theme-rtl.css 698 B
build/block-library/theme.css 703 B
build/block-serialization-default-parser/index.min.js 1.13 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/blocks/index.min.js 51 kB
build/components/index.min.js 208 kB
build/components/style-rtl.css 11.7 kB
build/components/style.css 11.7 kB
build/compose/index.min.js 12.4 kB
build/customize-widgets/index.min.js 12.2 kB
build/customize-widgets/style-rtl.css 1.41 kB
build/customize-widgets/style.css 1.41 kB
build/data-controls/index.min.js 663 B
build/data/index.min.js 8.58 kB
build/date/index.min.js 40.4 kB
build/deprecated/index.min.js 518 B
build/dom-ready/index.min.js 336 B
build/dom/index.min.js 4.72 kB
build/edit-post/classic-rtl.css 571 B
build/edit-post/classic.css 571 B
build/edit-widgets/index.min.js 17.3 kB
build/editor/index.min.js 45.8 kB
build/editor/style-rtl.css 3.54 kB
build/editor/style.css 3.53 kB
build/element/index.min.js 4.95 kB
build/escape-html/index.min.js 548 B
build/format-library/index.min.js 7.26 kB
build/format-library/style-rtl.css 557 B
build/format-library/style.css 556 B
build/hooks/index.min.js 1.66 kB
build/html-entities/index.min.js 454 B
build/i18n/index.min.js 3.79 kB
build/is-shallow-equal/index.min.js 535 B
build/keyboard-shortcuts/index.min.js 1.79 kB
build/keycodes/index.min.js 1.94 kB
build/list-reusable-blocks/index.min.js 2.14 kB
build/list-reusable-blocks/style-rtl.css 865 B
build/list-reusable-blocks/style.css 865 B
build/media-utils/index.min.js 2.99 kB
build/notices/index.min.js 977 B
build/plugins/index.min.js 1.95 kB
build/preferences-persistence/index.min.js 2.23 kB
build/preferences/index.min.js 1.35 kB
build/primitives/index.min.js 960 B
build/priority-queue/index.min.js 1.52 kB
build/private-apis/index.min.js 937 B
build/react-i18n/index.min.js 702 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.75 kB
build/reusable-blocks/index.min.js 2.26 kB
build/reusable-blocks/style-rtl.css 265 B
build/reusable-blocks/style.css 265 B
build/rich-text/index.min.js 11 kB
build/server-side-render/index.min.js 2.09 kB
build/shortcode/index.min.js 1.52 kB
build/style-engine/index.min.js 1.53 kB
build/token-list/index.min.js 650 B
build/url/index.min.js 3.74 kB
build/vendors/inert-polyfill.min.js 2.48 kB
build/vendors/react-dom.min.js 41.8 kB
build/vendors/react.min.js 4.02 kB
build/viewport/index.min.js 1.09 kB
build/warning/index.min.js 280 B
build/widgets/index.min.js 7.3 kB
build/widgets/style-rtl.css 1.18 kB
build/widgets/style.css 1.18 kB
build/wordcount/index.min.js 1.06 kB

compressed-size-action

@kevin940726 kevin940726 force-pushed the try/setup-node-composite-action branch 3 times, most recently from 52e2887 to 38797f9 Compare November 21, 2022 10:38
@kevin940726 kevin940726 changed the title Try custom setup-node composite action for aggressive caching Speed up npm ci by caching node_modules Nov 21, 2022
.github/setup-node/action.yml Outdated Show resolved Hide resolved
.github/setup-node/action.yml Show resolved Hide resolved
# Comparing against strings is necessary because of a known bug in composite actions.
# See https://github.com/actions/runner/issues/1483#issuecomment-1279933184
if: ${{ inputs.should-rebuild == 'true' }}
run: npm rebuild
Copy link
Member Author

Choose a reason for hiding this comment

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

This is normally not needed because most of the packages don't have postinstall scripts. However, some react-native related packages do need them so we make this an opt-in.

This is not future-proof as we might include a package that needs postinstall in the future. We can run npx can-i-ignore-scripts to manually inspect these cases.

Copy link
Member

Choose a reason for hiding this comment

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

can we not run npm ci above instead of npm ci --ignore-scripts?

also I wonder how much of the impact on performance is related to --ignore-scripts - I might measure it and find out.

this seems more of a danger on this change: is there a way we can make it more obvious to someone when their PRs are failing multiple test suites that the reason is that they need to supply should-rebuild: true?

I'm not sure how I would expect someone to start from the failure and work back up to this setting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, as I said above, it's not entirely "safe", but I think it's good enough for most cases. We will invalidate the cache whenever OS/Node/package-lock.json changes so it's more or less like what we have during local development.

also I wonder how much of the impact on performance is related to --ignore-scripts

Probably not much, because npm ci is only run when there isn't a cache hit. Most runs in our measurements have cache hit.

However, npm rebuild does contribute to some running time. We use it here to trigger any underlying postinstall script in the dependencies that gets ignored by -ignore-scripts. Tested with can-i-ignore-scripts, most dependencies that have the postinstall scripts are react-native-related (and local development related), hence I think it makes sense to make it an opt-in when we're building for react-native.

We can try to just use npm ci without the --ignore-scripts and always run npm rebuild though. I think that's unnecessary in most cases and probably will add up 20 to 30 seconds of runtime but it's technically "safer". Caching node_modules won't behave exactly like running npm ci every time and I think that's okay.

I'm not sure how I would expect someone to start from the failure and work back up to this setting.

I'd expect it to be pretty visible as it would probably fail the CI and we can look into that. I think it'll only happen when there's a new dependency with postinstall get added though, which should be pretty uncommon.

Copy link
Member

Choose a reason for hiding this comment

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

Most runs in our measurements have cache hit.

where did you measure this? and what was the ratio for pull_request events in PRs?

probably will add up 20 to 30 seconds of runtime but it's technically "safer"

it's usually faster to do the wrong thing 😉
I'm really nervous about introducing a very subtle behavior that could break things if you don't already happen to know it exists and remember that you need to apply a workaround when it's appropriate.

it would probably fail the CI and we can look into that

I agree here, but how is it that you would propose someone moves from "my PR failed and I don't know why" to finding this flag?

Copy link
Member

@dmsnell dmsnell Dec 6, 2022

Choose a reason for hiding this comment

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

I think that's unnecessary in most cases and probably will add up 20 to 30 seconds of runtime

Not that I have investigated why yet, but if I did the test right it doesn't seem to make much difference beyond a couple seconds. There is not enough of an effect with the data collected so far to conclude that there is any measurable impact. I could have done something wrong, or the flag might not be having the impact we expect it to.

Granted, for the unit tests and end-to-end tests there's still that issue of having the multi-modal distribution which could be throwing off the statistics. I haven't investigated the impact that has on the results.

My test branch is #46318

Unit Tests workflow

branch-compare-45932-46318

Performance tests workflow

branch-compare-45932-46318

Copy link
Member Author

Choose a reason for hiding this comment

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

where did you measure this? and what was the ratio for pull_request events in PRs?

I directly looked into the details of each run and see if there's a cache hit. I only looked into this PR's runs as this is where we're measuring.

it's usually faster to do the wrong thing 😉

It depends on what you think is "wrong". I'm just saying that upon my research this is the trade-off that I'm willing to take. It's obviously subjective and it's totally "right" to think otherwise.

but how is it that you would propose someone moves from "my PR failed and I don't know why" to finding this flag?

I think we should be able to find this out in a code review. Just like how people work in different OSes than in CI, things are gonna be different and weird things happen in CI. We can also write some guide or log about this if necessary. I agree we should minimize the breaking changes whenever possible but I believe this is something that rarely breaks. However, if the added time of removing --ignore-scripts is so little that it doesn't matter then I'll immediately stand on the other side of the fence 😝 .

Copy link
Member Author

Choose a reason for hiding this comment

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

I removed --ignore-scripts and npm rebuild in 1949d5f. Seems like it's still working nicely!

I updated the postinstall step to also run the postinstall scripts in the packages folder too. This technically doesn't match the behavior of npm ci entirely though as it only runs in the packages folder but not recursively for all dependencies.

If the postinstall scripts only update the node_modules folder (which is what most dependencies are doing) then it should be safely cached. However, in react-native-editor, the i18n-cache files are generated both in node_modules and in the packages/ folder because of linking. During the tests, the i18n-cache files are read from the packages folder for some reason, hence we need to re-run the postinstall script to populate it again. I think this can be seen as an edge case, and possibly there are other ways to solve it too (for example reading from node_modules as a fallback).

package.json Outdated Show resolved Hide resolved
@kevin940726 kevin940726 self-assigned this Nov 21, 2022
@kevin940726
Copy link
Member Author

@dmsnell Pinging you as seems like you have worked on similar things before and might be interested in this 😉 .

@dmsnell
Copy link
Member

dmsnell commented Nov 25, 2022

@kevin940726 I can monitor this PR and start collecting data on if it has the intended impact.

is two minutes something we think is worth the added complexity of the cache and the predictable cache invalidation problems?

I haven't looked at the other workflows, but there was plenty of performance improvement available in the Performance workflow that didn't involve adding new complexity.

for reference, while working on the performance test suite I was attempting to collect data on at least thirty test runs in the branch to compare against trunk and determine if the changes were worth it; not everything I thought would help did.

@dmsnell
Copy link
Member

dmsnell commented Nov 25, 2022

Impact on Performance Tests Workflow

branch-compare-45932-44907

  • this branch, 64 samples, µ = 21.02 min
  • trunk, 26 samples, µ = 23.76 min
  • the data strongly suggests this branch is faster for the performance test suite, with a significance at P = 0.0001
  • the measured decrease in average runtime is 2m 44s
Original comment @kevin940726 after around 30 runs of the performance tests here is some initial data; I'm starting to sequence the E2E tests and compare there

branch-compare-45932-44907

There's not enough evidence after 30 runs to conclude if this makes a difference or not. The results we're seeing are close enough that if we ran this experiment over and over, assuming there were no change in the PR and we weren't caching the npm dependencies, then in approximately half of the test runs we'd expect to see a speedup of at least as much as we're seeing with this actual PR. In other words, there's not a definite clear speedup observed in the test runs.

The data does seem like it's a bit more normal than the test runs in trunk, evidenced on the normality graphs. That suggests there could be an element at play whereby this change cuts down on some long-tail skew, but as mentioned above, there is not sufficient evidence to draw any conclusion from this work.

The graphs show that in the approximately thirty test runs, the mean test run was almost identical (a measured difference of 5.3 seconds), marginally shorter in this branch, while the median runtime was measured higher than in trunk. Of course, this is just an interesting observation, because again, if there's a real impact here so far, it's indistinguishable from the random variability in the test runs themselves. I'll keep the chart updated as I continue to re-run the performance test suite, and if I can get some E2E test suite data I'll post that as well.

@dmsnell
Copy link
Member

dmsnell commented Nov 25, 2022

Impact on End-to-End Tests Workflow

branch-compare-45932-44907

  • this branch, 1868 samples, µ = 15.57 min
  • trunk, 608 samples, µ = 17.22 min
  • the data conclusively suggests this branch is faster for the end-to-end tests workflow
  • the measured decrease in average runtime is 1m 39s

Right now we're jumbling all four end-to-end tests into one. This explains the divergence from normality. I'll see if I can separate out the four test jobs since they are independent tests. I don't believe this invalidates the results overall.

Original comment With the E2E tests, at around 10 test runs the difference is looking more likely, at a measured decrease in mean test run time of 1 minute, 7 seconds

branch-compare-45932-44907

Something look a little fishy about this data, which may be some oversight on my part when using the scripts I wrote for the Performance workflow with the E2E (which have all four E2E jobs). I'll update of course as I can.

@kevin940726
Copy link
Member Author

@dmsnell Thanks for testing it out! I'm curious how you tested this though. Is there code for the testing script used here?

I think you're missing some important variables in the performance test, we have to take caching into account. If we want to compare it against trunk, we will need to make sure they have the same baseline, do runs with both cache hit and cache miss and compare the results.

In addition, the main motivation of this PR is to increase the performance for running npm ci on CI, which isn't directly related to the performance test. Taking the performance test into the equation skews the results and makes it harder to compare, especially when the performance test itself has a very wide range of run time. Since there is no evidence that caching node_modules would slow down the performance of the test (we can do a separate test on that to make sure), we should just compare npm ci specifically. I think this is why the metrics for E2E tests have a 1-minute improvement while the performance test which is less stable shows less valuable results.

@dmsnell
Copy link
Member

dmsnell commented Dec 1, 2022

I think you're missing some important variables in the performance test,

I'm sure I am!

I'm curious how you tested this though.

What I'm doing is having GitHub re-run the test workflows once they complete. I'm not running the tests locally or in any way that's different than in any typical PR experience. That is, the results I'm measuring are looking at the wall-clock runtime of the CI workflow as reported by GitHub's API which gives the result for each run.

If we want to compare it against trunk, we will need to make sure they have the same baseline, do runs with both cache hit and cache miss and compare the results.

I'm not sure I agree with this. What I'm trying to measure is the real impact on how long the CI workflows run, and if we make the claim that the caching will help then I think we have to test it against real-world test runs.

There's room to argue that if we merge this than more PRs will start with cached data, but I think we'd still have the possibility that that assumption doesn't hold because among our 1700 branches there are plenty of PRs that won't be forked from the latest trunk, and therefore the diversity of merge bases will invalidate the caches in the same way that running the tests in this branch vs. against trunk will.

Since there is no evidence that caching node_modules would slow down the performance of the test (we can do a separate test on that to make sure), we should just compare npm ci specifically.

There are plenty of optimizations that I've investigated in improving the CI workflows that looked promising in isolation but which don't have the same effect when considered inside the system. Some things seem unintuitive, which is why I stress measuring the real results so much.

One possibility here is that GitHub's own npm caching is performant enough when it runs in our test containers that it's just as fast (or almost as fast) as having to deal with the cache, or in some cases the cache can cause more harm.


My script is quite tacky, as I was developing it while exploring the problem and not focusing on building maintainable code. Mostly it amounts to polling GitHub for CI workflow runs and parsing the data; some of what seems unnecessary in the script is the result of a bug in GitHub's API response that I reported and they are working on fixing (when requesting jobs for a given workflow run attempt they only report a link to the jobs for the most-recent run).

I accidentally stopped running the script so I'll pick it up again. Happy to take advice!

@kevin940726
Copy link
Member Author

First of all, I don't think the comparison of performance tests makes sense in this context. They are not even doing the same job in trunk and in PRs. In PRs, the performance test is running "Compare performance with trunk" while in trunk it's running "Compare performance with base branch". Comparing them is not an apples-to-apples comparison, and we should at least control our variables to make them as close as possible.

However, I don't think we need to take the full performance test into our equation. We split the job into steps so that we can debug them individually. The steps "Use desired version of NodeJS" and "Npm install" in trunk are responsible for setting up Node.js and installing the dependencies. We replace them with "Setup Node and install dependencies" in this PR. That's the only change in the workflow, anything else doesn't matter. We should tweak our benchmark to only include them (and their cleanup steps if any).

among our 1700 branches there are plenty of PRs that won't be forked from the latest trunk, and therefore the diversity of merge bases will invalidate the caches in the same way that running the tests in this branch vs. against trunk will.

Yep! This is a valid concern but this is already how it currently works for npm caching in CI. This enhancement won't benefit every PR but it should be beneficial when the cache hits. In the end, that's how caching works, isn't it 😆?

Worth noting that we'll only revalidate the cache when package-lock.json is changed, so PRs don't have to be based on the latest trunk. This also follows the same logic of how the official NPM caching works.

One possibility here is that GitHub's own npm caching is performant enough when it runs in our test containers that it's just as fast (or almost as fast) as having to deal with the cache, or in some cases the cache can cause more harm.

Well, if you look at the installation time reported by GH Actions, you can see that the time reduces significantly when node_modules is cached, so this assumption is not valid.

There might be situations where cached node_modules has a longer I/O runtime overhead than the installed one, but I highly doubt that 🤔.

Thank you for sharing the script! I'm having trouble understanding some of the contexts though. Which trunk are you using when running the test? Are you re-running the same commit on trunk too? Or are you taking the latest 30 commits on trunk. GH Actions by default runs on a "merge commit", so we should use the base commit as "trunk" for the comparison. Note that the base will change if there's a new commit pushed to trunk in the meantime.

@dmsnell
Copy link
Member

dmsnell commented Dec 2, 2022

First of all, I don't think the comparison of performance tests makes sense in this context.

Maybe there's a misunderstanding. I'm measuring the runtimes of the "End-to-End Tests" and also the "Performance Tests" workflows in CI. I chose these because they are two of the workflows affected by this PR, so I assume if the PR changes them, we should expect an impact there.

I'm not comparing End-to-End tests against Performance tests.

That's the only change in the workflow, anything else doesn't matter. We should tweak our benchmark to only include them (and their cleanup steps if any).

I'm still challenged by this assertion, as the goal seems to be "Speed up the CI pipeline by caching node_modules." My measurements are looking at the CI pipeline runtime for currently two of the workflows, the End-to-End tests (which take the longest) and the Performance tests (which used to take the longest). We can start measuring any or all of the workflows to see how this affects everything, but the perf tests were convenient since I've been monitoring those for the past month or so, and the end-to-end tests seem relevant since any speedup in the overall process will have to speed those up, or else the improvements won't be felt by anyone (as they will still be waiting for the slowest workflow to complete).

So I don't want to limit ourselves by only looking at a piece in isolation when that piece sits inside a more complicated system where the effects may be different.

you can see that the time reduces significantly when node_modules is cached, so this assumption is not valid.

This appears to be accurate for the E2E tests, but inaccurate for the performance tests.

This enhancement won't benefit every PR but it should be beneficial when the cache hits.

That's absolutely right in theory, and given that we're just sitting here telling GitHub to re-run the CI tests as soon as they finish, we should expect the best-case of cache hits when testing the impact of this PR. If the theory is right then it should be clear from the measurements that this accomplishes its goal.

it's still taking roughly 2 minutes to install all the packages in every workflow. By also caching node_modules, we can dramatically cut down the installation time by 75%.

According to my latest numbers with 70 test runs, it looks fairly conclusive that it is faster in the end-to-end tests, and the measured difference in average runtime is 1m 37s. After around 30 test runs on the performance tests though, it's not conclusive, but it's looking to move that way in showing that this is slower in the performance tests with a current measured slowdown of adding 47s to the average CI workflow runtime.

I just started re-running the Playwright workflow (last completion took 26min) and the JS Unit Tests workflow (last completion took 4min). It'll be interesting to see what impact this has on those workflows.

but I highly doubt that 🤔.

All that is to say, given how many false assumptions I found while optimizing the performance tests workflow, I want us to be able to have a credible reason to claim that a change we are making is worth the additional complexity vs. arguing from opinions, guesses, or doubts 😄

It seems clear that this will cut off some time from the E2E workflow, but probably add time to the Performance Tests. When more data comes in it just gives us a better picture to say "I want this tradeoff" or "I don't want this tradeoff."

Which trunk are you using when running the test? Are you re-running the same commit on trunk too?

I'm using #44907 as a base against which to compare. It's two weeks behind trunk, but I'll update it. I became aware of the merge-branch elements when working on the perf tests, but found that practically there it didn't matter much if a little lag came into play as most commits upstream didn't meaningfully impact the tests. We'll find out, as I can easily filter to only use data from the updated trunk.

@dmsnell
Copy link
Member

dmsnell commented Dec 3, 2022

@kevin940726 it seems like many of the workflows are failing due to a caniuse issue. I'm not sure if the branch simply needs to be rebased or not, but I hope it's not an issue where caching npm ci fails because of non-determinism during npm ci

@kevin940726
Copy link
Member Author

Maybe there's a misunderstanding.

Ahh yes definitely. I thought that you were using trunk's workflow which isn't the same as PR's workflow. While in fact, you're using #44907 as the baseline for "trunk".

I'm still challenged by this assertion

That's fair! My argument will be that the enhancement in this PR can be viewed in isolation while many of the improvements contributed to the perf test alone cannot be easily split out. I think this PR can still improve the perf test run time, just that we don't have enough data to prove that yet. The run time of the perf test varies a lot and the previous runs might just underperform because of the random factors. We might get a different result if we have more samples. That's just my guess though because the result is counter-intuitive to me and I don't have any explanation for now.

I want us to be able to have a credible reason to claim that a change we are making is worth the additional complexity vs. arguing from opinions, guesses, or doubts 😄

This is fair too! But I think we should not stop after we create a benchmark, we should try to figure out what happened there. The benchmark is just a tool to help verify that something works. If it creates unexpected results, it's our opportunity to debug that, either in our code (TN) or in the benchmark itself (FN).

When more data comes in it just gives us a better picture to say "I want this tradeoff" or "I don't want this tradeoff."

Absolutely! I'm also curious about it and might spend some time creating a benchmark myself too. I'll share more info.

it seems like many of the workflows are failing due to a caniuse issue. I'm not sure if the branch simply needs to be rebased or not, but I hope it's not an issue where caching npm ci fails because of non-determinism during npm ci

Yeah, that's a very interesting observation! I'll look into that! Thanks! 🙇

@kevin940726 kevin940726 force-pushed the try/setup-node-composite-action branch from d8363fc to c42f384 Compare December 5, 2022 06:06
@kevin940726
Copy link
Member Author

it seems like many of the workflows are failing due to a caniuse issue. I'm not sure if the branch simply needs to be rebased or not

Oh, seems like we just need a rebase after #46093!

@dmsnell
Copy link
Member

dmsnell commented Dec 5, 2022

Impact on Unit Tests workflow

branch-compare-45932-44907

  • this branch, 1061 samples, µ = 5.18 min
  • trunk, 871 samples, µ = 6.81 min
  • the data conclusively suggests this branch is faster for the unit test suite
  • the measured decrease in average runtime is 1m 38s

@dmsnell
Copy link
Member

dmsnell commented Dec 5, 2022

@kevin940726 not sure why things changed after the rebase, but now all three of the tests I've been measuring are reporting improvements in this branch.

I'd like to try comparing it against a version without --ignore-script. That flag seems at least a little suspect and now I wonder if it was related to the caniuse errors. Maybe it's possible that the speedup is partly due to the cache and partly due to the fact that we're skipping steps that we previously relied upon.

Out of curiosity, what led you to add that flag to npm ci?

@kevin940726 kevin940726 force-pushed the try/setup-node-composite-action branch 2 times, most recently from bb38b16 to 1949d5f Compare December 7, 2022 10:38
@dmsnell dmsnell force-pushed the try/setup-node-composite-action branch from 947ce6a to 136bbd0 Compare January 16, 2023 21:49
@kevin940726 kevin940726 force-pushed the try/setup-node-composite-action branch from 136bbd0 to 504da63 Compare February 7, 2023 07:40
@dmsnell
Copy link
Member

dmsnell commented Feb 8, 2023

@kevin940726 I think one of the best things that can be done for this is simply to monitor it after deploy to measure if it has the expected impact. I expect the impact of the caching to drop once this is in trunk and we find a plurality of versions of the dependencies vs. this branch where the dependencies remain mostly unchanged.

how much I expect it to drop I don't know, and I don't know a good way to test this before merging. the more important question is how much we want it to drop in order to balance the costs of increased maintenance, additional debugging when the caching fails us, and the frustration induced when someone is trying to understand why something isn't right.

looks like some recent React-native tests are completing in times similar to the E2E suites. this is good news because as long as those are taking upwards of 30-40 minutes to run, the benefit this caching brings for all the other suites would be locked, but if the longest workflow is taking 20 min right now, then an estimated speedup by around 1 minute and a half could translate into around an 8% speedup, which is pretty good.

we can probably test by iterating over all the open PRs where all workflows have completed, and looking at the one that took the longest to complete. in fact, we may even get this information from the check run or checks API endpoints from Github. I'm still unclear on the terminology even after absorbing myself in it for a while. we should have a reasonable guess within a day or two of data as long as it's mid-week, I would imagine. the determining factor in experiment length is probably how frequently any PR involves changes in the node modules or versions, as that's the thing on which all value in this PR depends. also I think old data sticks around for a month or so? so we're likely limited to a maximum two-week experiment period and that starting immediately after deploy, since after that time we start losing data from the before times.

at some point the benefit this brings won't be worth the development costs, either in a raw reduction of check suite runtime duration or a percentage thereof. I'm curious to know what your target goal is and what you think a reasonable threshold should be for that tradeoff point.

@kevin940726
Copy link
Member Author

I expect the impact of the caching to drop once this is in trunk and we find a plurality of versions of the dependencies vs. this branch where the dependencies remain mostly unchanged.

We rarely change the dependencies, at least compared to the number of workflow runs we run. I did a quick search on runs on trunk, out of 642 runs (the workflows that will be impacted by this PR), only 10 of them have changed the package-lock.json file. I'm not even counting multiple jobs in a given run (a end2end-test run can have 6 jobs for instance). That's about 98% of cache hit. This is only on trunk, I expect the number to be larger if we're also counting PRs.

the more important question is how much we want it to drop in order to balance the costs of increased maintenance, additional debugging when the caching fails us, and the frustration induced when someone is trying to understand why something isn't right.

What's the cost of maintenance? There's no debugging needed if cache misses for regular contributors, it will simply fallback to what we have now: reinstall everything.

at some point the benefit this brings won't be worth the development costs, either in a raw reduction of check suite runtime duration or a percentage thereof. I'm curious to know what your target goal is and what you think a reasonable threshold should be for that tradeoff point.

The goal is simple: reduce the time spent on CI, this is only the first step of it. I still fail to understand why you think this is not worth it. If the cache hits, which I expect to be 99% of the cases, then there's going to be roughly 90 to 120 seconds of improvement. If the cache misses, then nothing is changed, it's just going to do whatever we are doing now.

All the numbers collected in this PR show positive results. Unless you have a concrete case against it, I suggest we try this on trunk to test it out. We can always revert it if it's not behaving like what we thought anyway 🙂 . WDYT?

@dmsnell
Copy link
Member

dmsnell commented Feb 10, 2023

What's the cost of maintenance?

The cost is in understanding and modifying the run scripts.

There's no debugging needed if cache misses

There are two costs here:

  • if the cache suddenly never hits, then someone will need to figure out why
  • the bigger problem is the cache hitting when it's not supposed to

why you think this is not worth it

this is definitely not what I said or meant. what I said and meant is that there is a point in all optimizations which come by adding complexity where the benefits and costs balance. I suggested not that this isn't worth it, but that we measure the impact and use that knowledge to confirm it did what we wanted, and if not, that is a clue we can revert it.

why do I keep bringing this up? am I just a fuddy-duddy? 🙃
no, it's because I've found multiple cases where something was added to speed things up and when measured, it either didn't speed things up or it actually slowed things down. this was all done in good faith, but because the impact wasn't measured we were left worse off with more complicated code and nobody realized it.

so the question is a good one, because if you say "90s is a success" and we measure 120s then you know your work made a positive impact. if we measure 10s what do we make of that? or if unintentionally for some quirky reason this slows things down, how do we know?

I suggest we try this on trunk to test it out

this was literally how I started my last comment, so we are in agreement 😄
I do not believe there is a practical way to measure the full impact of this before merging, or the impact of measuring anything that involves caching at the platform level.

Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

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

this is still marked as a draft, but it's ready for merge.

would still be good to set a target performance impact for this so that after merge we can measure and see if it did what we expected it to.

@kevin940726 kevin940726 marked this pull request as ready for review February 14, 2023 07:39
@kevin940726
Copy link
Member Author

Thank you @dmsnell !

would still be good to set a target performance impact for this so that after merge we can measure and see if it did what we expected it to.

I'd expect all the workflows affected by this PR to get a 1m30s to 2m performance boost if the cache hits. Let's see if that's the case then!

Yeah, I forgot to mark this ready for review 😅. I'll wait for a few other folks to chime in once they get time though!

@kevin940726
Copy link
Member Author

Note that the "Report to GitHub" workflow currently fails because it depends on the workflow on trunk, which doesn't have this composite action yet. The issue should go away once this is merged.

Copy link
Contributor

@desrosj desrosj left a comment

Choose a reason for hiding this comment

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

This is looking great! Thanks for working on this. I've been meaning to look at this for the Core repo because a few people have mentioned that they still cache their node_modules directory in their other projects and it does significantly improve the speed of the related commands.

A bit about caching that I thought would be good to detail. Each repository is allowed 10GB of total cache storage. Once that storage is exceeded, then GHA works to evict caches until bringing the total storage under the limit. When I looked yesterday, the Gutenberg repository was at ~25 GB and GHA had not cleaned up after itself. But looking today, we're at 8.67 GB. The oldest cache was last used 12 hours ago.

I think that we do need to spend some time holistically examining cache key practices to see if we are creating multiple cache entries for the same things anywhere across different workflows. I do think that avoiding any repetition will help us keep things consistent across workflows and potentially help with this.

I made some suggestions and posted a few questions. Would love to give it a second look after these are addressed!

.github/setup-node/action.yml Outdated Show resolved Hide resolved
.github/setup-node/action.yml Show resolved Hide resolved
uses: actions/cache@v3
with:
path: '**/node_modules'
key: node_modules-${{ runner.os }}-${{ steps.node-npm-version.outputs.NODE_VERSION }}/${{ steps.node-npm-version.outputs.NPM_VERSION }}-${{ hashFiles('package-lock.json') }}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm thinking that we don't need to include the npm version in the cache key here.

I can't think of a scenario where the version of npm would change independent of the Node.js version.

Copy link
Member

Choose a reason for hiding this comment

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

doesn't this happen regularly in Gutenberg since our npm version is still gated at <7?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we need to include npm version here too. Including it will potentially bust the cache more often.

doesn't this happen regularly in Gutenberg since our npm version is still gated at <7?

That's because we're using Node 14, which comes with npm v6 by default. npm versions could change independently from Node.js, but they should just be minor releases and backward-compatible.

Copy link
Contributor

Choose a reason for hiding this comment

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

When a new version of Node.js is released, a specific version of npm is bundled with that version. You'll find an all inclusive manifest here. As long ass the npm install -g npm command is not run, then the version originally bundled with that version of Node.js will be the one installed.

I looked through the code of actions/setup-node. While I wasn't able to confirm this 100%, it seems that new version are pulled and built for use within Action runners using this manifest through the actions/node-versions and actions/versions-package-tools.

Because we only install Node 14.x, the version of npm will always be < 7, but the specific version of npm 6.x installed will be tied to the specific version of Node installed.

Copy link
Member

Choose a reason for hiding this comment

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

Right, but according to the project's package.json file we're good to use node@latest, which is I think what most people end up doing locally.

gutenberg/package.json

Lines 17 to 20 in 6434e0b

"engines": {
"node": ">=14.0.0",
"npm": ">=6.9.0 <7"
},

so maybe by happenstance we're only using node@14 in the test suites, but even then, are we guaranteed to run the specific version of 14? or are we pinning it entirely at a specific version?

Copy link
Member Author

Choose a reason for hiding this comment

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

so maybe by happenstance we're only using node@14 in the test suites

.nvmrc pins the Node.js version to 14 on CI. FWIW, there's an ongoing PR (#48950) that hopefully can be merged soon to upgrade Node.js and npm to latest on CI.

but even then, are we guaranteed to run the specific version of 14

Nope, if there's a minor/patch release then we'll automatically use the newer version on CI (basically ^14). But that's okay since we include the full node version in the cache key.

Note that this PR doesn't affect anyone's local setup whatsoever, they can continue using whatever supported Node.js/npm version they like locally. And the cache only exists on CI, so they won't be affected by it either. This PR only caches node_modules on CI, which is a controlled environment.

Copy link
Member

Choose a reason for hiding this comment

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

if there's a minor/patch release then we'll automatically use the newer version on CI (basically ^14). But that's okay since we include the full node version in the cache key.

I think we're in agreement here. my comment was specifically commenting on how we do expect changes to node to occur that don't match npm. we're intentionally not using the bundled version of npm in the project, but we build the project with changing versions of node quite often, since we pin node at >= a major version.

.github/setup-node/action.yml Show resolved Hide resolved
.github/setup-node/action.yml Outdated Show resolved Hide resolved
.github/setup-node/action.yml Outdated Show resolved Hide resolved
.github/setup-node/action.yml Outdated Show resolved Hide resolved
.github/setup-node/action.yml Outdated Show resolved Hide resolved
.github/workflows/unit-test.yml Show resolved Hide resolved
@kevin940726
Copy link
Member Author

@desrosj Thanks for the reviews! I addressed the feedback 🙇‍♂️

When I looked yesterday, the Gutenberg repository was at ~25 GB and GHA had not cleaned up after itself. But looking today, we're at 8.67 GB. The oldest cache was last used 12 hours ago.

Yeah, the caching limits trade-off is mentioned in the PR description too. Note that there are going to be two separate caches while we're still experimenting with this PR though. Caching size will also increase when we're in between updates to the package-lock.json file. I think this wouldn't be a problem in the long run, though we should still monitor the cache effect that it may bring.

@kevin940726 kevin940726 force-pushed the try/setup-node-composite-action branch 2 times, most recently from 2cbcac3 to 7446f9f Compare March 13, 2023 09:06
@kevin940726 kevin940726 force-pushed the try/setup-node-composite-action branch from 7446f9f to 1c9ca27 Compare March 13, 2023 09:24
Copy link
Contributor

@desrosj desrosj left a comment

Choose a reason for hiding this comment

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

Looking good! Here's a few more things that popped out on re-review.

.github/setup-node/action.yml Show resolved Hide resolved
if: ${{ steps.cache-node_modules.outputs.cache-hit == 'true' }}
run: |
npm run postinstall
npx lerna run postinstall
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between running postinstall for npm and npx lerna? Do we need both?

If so, I'm wondering if we should split into two steps. That would make it more clear which of the two fails should either encounter a problem.

Copy link
Member Author

Choose a reason for hiding this comment

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

I added some comments explaining this ❤️ .

I don't think we need to split it into two steps though, as they have the same goal.

.github/workflows/build-plugin-zip.yml Outdated Show resolved Hide resolved
@@ -20,20 +20,17 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [14]
node: ['14']
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why this is needed. Does this cause a failure in the composite workflow because the type is defined as string?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think either works, but everywhere else uses string. Just want to keep it consistent.

.github/workflows/publish-npm-packages.yml Outdated Show resolved Hide resolved
.github/workflows/pull-request-automation.yml Show resolved Hide resolved

- name: Npm install and build
- name: Npm build
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, this seems to be running the Lerna build script.

Suggested change
- name: Npm build
- name: Build Lerna

Copy link
Member Author

@kevin940726 kevin940726 Mar 14, 2023

Choose a reason for hiding this comment

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

"Build Lerna" isn't really descriptive either though. I think this should be "Build packages" and just run npm run build:packages. But it's already the way it is in trunk, so maybe we should change it in another PR?

.github/workflows/unit-test.yml Show resolved Hide resolved
run: |
npm ci
npm run build
- name: Npm build
Copy link
Contributor

Choose a reason for hiding this comment

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

We're running npm run build, but it's not just an npm build. Maybe "Run build scripts" would be more general and accurate. If we change this, it should get changed everywhere.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, let's do this in another PR though.

Copy link
Contributor

@desrosj desrosj left a comment

Choose a reason for hiding this comment

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

Thanks @kevin940726 I'm OK with merging this to try it out!

@kevin940726
Copy link
Member Author

Thanks! Let's try it and revert it if anything goes wrong 😅 !

@kevin940726 kevin940726 merged commit cc11957 into trunk Mar 16, 2023
@kevin940726 kevin940726 deleted the try/setup-node-composite-action branch March 16, 2023 02:49
@github-actions github-actions bot added this to the Gutenberg 15.5 milestone Mar 16, 2023
@desrosj
Copy link
Contributor

desrosj commented Mar 20, 2023

@kevin940726 I know it's only been ~4 days, but wanted to circle back to see if you'd observed any impacts of this change, either positive or negative.

@desrosj
Copy link
Contributor

desrosj commented Mar 20, 2023

One thing I noticed that's a little annoying is that there is an annotation added when both node-version and node-version-file are present.

Screenshot 2023-03-20 at 13 39 12

@kevin940726
Copy link
Member Author

Sure! I did some basic analysis on the "Unit Tests" workflow before and after the commit on trunk.

I took 30 runs each and here are the results:

Before:

Mean: 10m 18s
Median: 10m 21s
Standard deviation: 42s

After:

Mean: 8m 51s
Median: 8m 29s
Standard deviation: 1m 17s

Will be interesting to do more analysis on other workflows as well, but I don't have the time rn 😅 .

One thing I noticed that's a little annoying is that there is an annotation added when both node-version and node-version-file are present.

Yeah, turns out we do want the if statement afterall 😅 .

ockham pushed a commit to ockham/gutenberg that referenced this pull request Mar 22, 2023
* Try custom setup-node composite action for aggressive caching

* Add comment about GHA bug

* Try without rebuild and ignore-scripts

* Include npm version to the cache key

* Try reverting the change of graceful-fs

* Update step name

* Code review

* Add some comments
ockham pushed a commit to ockham/gutenberg that referenced this pull request Mar 22, 2023
* Try custom setup-node composite action for aggressive caching

* Add comment about GHA bug

* Try without rebuild and ignore-scripts

* Include npm version to the cache key

* Try reverting the change of graceful-fs

* Update step name

* Code review

* Add some comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
GitHub Actions Pull requests that update GitHub Actions code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants