Only cache node_modules on main (#45544)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/45544

## This diff now does 5 things:
1. removes the old way we used `actions/setup-node` to manage the cache itself.
2. it creates a new `update-node-modules-cache` workflow, which is the only job that will update the node modules cache
3. it create a `yarn-install-with-cache` action that should be used install of directly calling `yarn install --non-interactive`.  This will load a cache against a hash of `package.json`.
4. updated the cache reaper to aggressively remove everything but the latest `npm-{{ hash('package.json') }}`.
5. removed a `cache-setup`, which couldn't be used (we're using artefacts now).

## Why are we doing this:
The various `node-cache-` keys for platforms and on various branches accounts for a very large proportion of the cache (10-20%).

We don't frequently change these dependencies, and even when we do running `yarn install` after loading the cache will resolve any issues.

Limiting the cache to `main` and aggressively pruning older cache entries will clean up a lot of "small win" caching.

Changelog: [Internal]

Reviewed By: cortinico

Differential Revision: D59917944

fbshipit-source-id: 4be6f1959e8fde642a4f208f7d19aceba2c3262f
This commit is contained in:
Blake Friedman 2024-07-19 13:04:07 -07:00 committed by Facebook GitHub Bot
parent d9263fbdbb
commit 4410899ec7
15 changed files with 94 additions and 123 deletions

View File

@ -12,9 +12,8 @@ runs:
run: git config --global --add safe.directory '*'
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Install dependencies
shell: bash
run: yarn install --non-interactive
- name: Install node dependencies
uses: ./.github/actions/yarn-install-with-cache
- name: Set React Native Version
shell: bash
run: node ./scripts/releases/set-rn-artifacts-version.js --build-type ${{ inputs.release-type }}

View File

@ -47,8 +47,7 @@ runs:
fi
- name: Yarn- Install Dependencies
if: ${{ steps.check_if_apple_artifacts_are_there.outputs.ARTIFACTS_EXIST != 'true' }}
shell: bash
run: yarn install --non-interactive
uses: ./.github/actions/yarn-install-with-cache
- name: Slice cache macosx
if: ${{ steps.check_if_apple_artifacts_are_there.outputs.ARTIFACTS_EXIST != 'true' }}
uses: actions/download-artifact@v4

View File

@ -103,8 +103,7 @@ runs:
- name: Setup gradle
uses: ./.github/actions/setup-gradle
- name: Install dependencies
shell: bash
run: yarn install --non-interactive
uses: ./.github/actions/yarn-install-with-cache
- name: Build packages
shell: bash
run: yarn build

View File

@ -1,88 +0,0 @@
name: cache-setup
description: "Cache setup"
inputs:
hermes-version:
description: "Hermes version"
required: true
react-native-version:
description: "React Native version"
required: true
outputs:
cache-hit-hermes-tarball-release:
description: "Whether the hermes tarball release cache was hit"
value: ${{ steps.cache_hermes_tarball_release.outputs.cache-hit }}
cache-hit-hermes-tarball-debug:
description: "Whether the hermes tarball debug cache was hit"
value: ${{ steps.cache_hermes_tarball_debug.outputs.cache-hit }}
cache-hit-macos-bin-release:
description: "Whether the macos bin release cache was hit"
value: ${{ steps.cache_macos_bin_release.outputs.cache-hit }}
cache-hit-macos-bin-debug:
description: "Whether the macos bin debug cache was hit"
value: ${{ steps.cache_macos_bin_debug.outputs.cache-hit }}
cache-hit-dsym-release:
description: "Whether the dsym release cache was hit"
value: ${{ steps.cache_dsym_release.outputs.cache-hit }}
cache-hit-dsym-debug:
description: "Whether the dsym debug cache was hit"
value: ${{ steps.cache_dsym_debug.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Cache hermes tarball release
id: cache_hermes_tarball_release
uses: actions/cache@v4
with:
path: /tmp/hermes/hermes-runtime-darwin/hermes-ios-Release.tar.gz
key: v4-hermes-tarball-release-${{ inputs.hermes-version }}-${{ inputs.react-native-version }}-${{ hashfiles('packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh') }}
enableCrossOsArchive: true
- name: Cache hermes tarball debug
id: cache_hermes_tarball_debug
uses: actions/cache@v4
with:
path: /tmp/hermes/hermes-runtime-darwin/hermes-ios-Debug.tar.gz
key: v4-hermes-tarball-debug-${{ inputs.hermes-version }}-${{ inputs.react-native-version }}-${{ hashfiles('packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh') }}
enableCrossOsArchive: true
- name: Cache macos bin release
id: cache_macos_bin_release
uses: actions/cache@v4
with:
path: /tmp/hermes/osx-bin/Release
key: v2-hermes-release-macosx-${{ inputs.hermes-version }}-${{ inputs.react-native-version }}
enableCrossOsArchive: true
- name: Cache macos bin debug
id: cache_macos_bin_debug
uses: actions/cache@v4
with:
path: /tmp/hermes/osx-bin/Debug
key: v2-hermes-debug-macosx-${{ inputs.hermes-version }}-${{ inputs.react-native-version }}
enableCrossOsArchive: true
- name: Cache dsym release
id: cache_dsym_release
uses: actions/cache@v4
with:
path: /tmp/hermes/dSYM/Release
key: v2-hermes-release-dsym-${{ inputs.hermes-version }}-${{ inputs.react-native-version }}
enableCrossOsArchive: true
- name: Cache dsym debug
id: cache_dsym_debug
uses: actions/cache@v4
with:
path: /tmp/hermes/dSYM/Debug
key: v2-hermes-debug-dsym-${{ inputs.hermes-version }}-${{ inputs.react-native-version }}
enableCrossOsArchive: true
- name: HermesC Apple
id: hermesc_apple
uses: actions/cache@v4
with:
path: /tmp/hermes/hermesc-apple
key: v2-hermesc-apple-${{ inputs.hermes-version }}-${{ inputs.react-native-version }}
enableCrossOsArchive: true
- name: Cache hermes workspace
uses: actions/cache@v4
with:
path: |
/tmp/hermes/download/
/tmp/hermes/hermes/
key: v1-hermes-${{ inputs.hermes-version }}
enableCrossOsArchive: true

View File

@ -15,8 +15,7 @@ runs:
using: composite
steps:
- name: Yarn install
shell: bash
run: yarn install --non-interactive
uses: ./.github/actions/yarn-install-with-cache
- name: Configure Git
shell: bash
run: |

View File

@ -69,8 +69,7 @@ runs:
- name: Yarn- Install Dependencies
if: ${{ steps.meaningful-cache.outputs.HERMES_CACHED != 'true' }}
shell: bash
run: yarn install --non-interactive
uses: ./.github/actions/yarn-install-with-cache
- name: Download Hermes tarball
if: ${{ steps.meaningful-cache.outputs.HERMES_CACHED != 'true' }}

View File

@ -5,10 +5,6 @@ inputs:
description: 'The node.js version to use'
required: false
default: '18'
cache:
description: 'The package manager to use for caching dependencies'
required: false
default: 'yarn'
runs:
using: "composite"
steps:
@ -16,4 +12,3 @@ runs:
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.cache }}

View File

@ -41,8 +41,7 @@ runs:
shell: bash
run: ls -lR "$HERMES_WS_DIR"
- name: Run yarn
shell: bash
run: yarn install --non-interactive
uses: ./.github/actions/yarn-install-with-cache
- name: Setup ruby
uses: ruby/setup-ruby@v1.170.0
with:

View File

@ -37,8 +37,7 @@ runs:
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Run yarn
shell: bash
run: yarn install --non-interactive
uses: ./.github/actions/yarn-install-with-cache
- name: Download Hermes
uses: actions/download-artifact@v4
with:

View File

@ -13,8 +13,7 @@ runs:
with:
node-version: ${{ inputs.node-version }}
- name: Yarn install
shell: bash
run: yarn install --non-interactive
uses: ./.github/actions/yarn-install-with-cache
- name: Run Tests - JavaScript Tests
shell: bash
run: node ./scripts/run-ci-javascript-tests.js --maxWorkers 2

View File

@ -0,0 +1,28 @@
name: yarn-install-with-cache
inputs:
update-cache:
description: Update the cache, only do this if you are update-node-modules-cache.yml
default: "false"
description: Only update node_modules if on main
runs:
using: composite
steps:
- name: Load node_modules from cache
# Restore for all branches, but save for 'main'.
uses: actions/cache/restore@v4
with:
path: node_modules/
key: node-modules-${{ hashFiles('package.json') }}
- name: Install dependencies
shell: bash
run: yarn install --non-interactive
- name: Save node_modules to the cache
if: github.ref == 'refs/heads/main' && inputs.update-cache == 'true'
uses: actions/cache/save@v4
with:
path: node_modules/
# We're assuming that variations on branches will slightly vary from main,
# so it's always important to run yarn install --non-interactive after this
# cache is restored.
key: node-modules-v1-${{ hashFiles('package.json') }}
enableCrossOsArchive: true

View File

@ -17,7 +17,7 @@ jobs:
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Run Yarn Install
run: yarn install
uses: ./.github/actions/yarn-install-with-cache
- name: Build packages
run: yarn build
- name: Set NPM auth token

View File

@ -291,9 +291,8 @@ jobs:
path: /tmp/maven-local
- name: Setup gradle
uses: ./.github/actions/setup-gradle
- name: Run yarn
shell: bash
run: yarn install --non-interactive
- name: Run yarn install
uses: ./.github/actions/yarn-install-with-cache
- name: Prepare the Helloworld application
shell: bash
run: node ./scripts/e2e/init-project-e2e.js --useHelloWorld --pathToLocalReactNative "$GITHUB_WORKSPACE/build/$(cat build/react-native-package-version)"

View File

@ -0,0 +1,16 @@
name: Update node modules cache
on:
workflow_dispatch:
push:
branches:
- main
jobs:
update_node_modules_cache:
runs-on: ubuntu-latest
steps:
- name: Install yarn dependencies and update cache
uses: ./.github/actions/yarn-install-with-cache
with:
update-cache: "true"

View File

@ -8,6 +8,8 @@
const {execSync} = require('node:child_process');
const CACHE_LIMIT = (10 * 1024 ** 3) * 0.9;
// Doing this to capture node-modules- and the pre-existing node-cache-<platform>-yarn-<sha> entries
const NODE_CACHE_KEY = 'node-';
function cleanData(rawStr) {
const now = new Date();
@ -15,8 +17,10 @@ function cleanData(rawStr) {
return json
.map(raw => ({
...raw,
createdAt: now - new Date(raw.createdAt),
msSinceLastAccessed: now - new Date(raw.lastAccessedAt),
}))
// Order: oldest last access time first
.sort((a, b) => b.msSinceLastAccessed - a.msSinceLastAccessed);
}
@ -25,26 +29,51 @@ const hrs = ms => (ms / (1000 * 60 * 60)).toFixed(1) + 'hrs';
const dryRun = process.argv.indexOf('--dry') !== -1;
function cacheToString(entry) {
return `[${hrs(entry.msSinceLastAccessed).padStart(7)}] ${mb(entry.sizeInBytes).padStart(7)} -> ${entry.key}`;
}
function main() {
const cacheUsage = cleanData(execSync(
'gh cache list --sort last_accessed_at --json id,key,lastAccessedAt,sizeInBytes --limit 1000',
'gh cache list --sort last_accessed_at --json id,key,createdAt,lastAccessedAt,sizeInBytes --limit 1000',
'utf8'
));
let total = 0;
let remove = [];
let cleaned = 0;
// Be aggressive with node cache entries. Ignore entries < 1MB and only keep most
// recently created entry.
const nodeCacheUsage = cacheUsage
.filter(({key, sizeInBytes}) =>
// I've observed some noisy entries, ignore anything that isn't > 1MB
key.startsWith(NODE_CACHE_KEY) && sizeInBytes > 1024 * 1024
)
.sort((a, b) => b.createdAt - a.createdAt);
// Leave the latest entry only
const keeping = nodeCacheUsage.pop();
console.log('TASK: clean up old node_modules cache entries.', keeping ? `\nkeeping ${cacheToString(keeping)}` : ' Skipping, no cache entries.');
for (const entry of nodeCacheUsage) {
console.warn(`removing ${cacheToString(entry)}`);
cleaned += entry.sizeInBytes;
remove.push(entry.id);
}
// Cleanup everything else
console.log('TASK: clean up everything else that takes us over our threshold: ' + mb(CACHE_LIMIT));
for (let i = cacheUsage.length - 1; i > 0; i--) {
const {id, key, msSinceLastAccessed, sizeInBytes} = cacheUsage[i];
total += sizeInBytes;
const cache = cacheUsage[i];
total += cache.sizeInBytes;
// Are we in the danger zone?
if (total > CACHE_LIMIT) {
console.warn(`[${hrs(msSinceLastAccessed).padStart(7)}] ${mb(sizeInBytes).padStart(7)} -> ${key}`);
cleaned += sizeInBytes;
remove.push(id);
console.warn(cacheToString(cacheUsage[i]));
cleaned += cache.sizeInBytes;
remove.push(cache.id);
} else {
console.warn(`skip ${cacheUsage.length - i} ${mb(sizeInBytes)} ${hrs(msSinceLastAccessed)}`);
console.warn(`skip ${cacheUsage.length - i} ${mb(cache.sizeInBytes)} ${hrs(cache.msSinceLastAccessed)}`);
}
}
console.warn(`Identifed ${remove.length} cache keys for removal, reducing cache from ${mb(total)} -> ${mb(total - cleaned)}`);
@ -52,9 +81,9 @@ function main() {
const cleanup = remove.map(id => `gh cache delete ${id} --repo facebook/react-native`);
for (const cmd of cleanup) {
if (dryRun) {
console.warn(`DRY: ${cmd}`);
console.warn(`Skip: ${cmd}`);
} else {
console.warn(`${cmd} -> ${execSync(cmd, 'utf8').toString()}`);
console.warn(`${cmd} 🪓 ${execSync(cmd, 'utf8').toString()}`);
}
}
}