diff --git a/.github/workflows/frontend-pr-workflow.yml b/.github/workflows/frontend-pr-workflow.yml index 7c91477..f08eac8 100644 --- a/.github/workflows/frontend-pr-workflow.yml +++ b/.github/workflows/frontend-pr-workflow.yml @@ -47,6 +47,10 @@ on: description: 'Disable restore-keys to avoid restoring stale caches (forces exact key match)' type: boolean default: false + use-tmpfs: + description: 'Mount tmpfs at cache target paths (PLT-3439 test). RAM-backed, bypasses memcg dirty accounting.' + type: boolean + default: false # Runner configuration runner: @@ -314,12 +318,13 @@ jobs: uses: actions/checkout@v6 - name: Setup Node with Cache - uses: Typeform/.github/shared-actions/setup-node-with-cache@v1 + uses: Typeform/.github/shared-actions/setup-node-with-cache@test-tmpfs-cache-extract with: node-version: ${{ inputs.node-version }} use-asdf: ${{ inputs.use-asdf }} cache-mode: ${{ inputs.cache-mode }} disable-restore-keys: ${{ inputs.disable-restore-keys }} + use-tmpfs: ${{ inputs.use-tmpfs }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: Setup Jarvis @@ -387,12 +392,13 @@ jobs: uses: actions/checkout@v6 - name: Setup Node with Cache - uses: Typeform/.github/shared-actions/setup-node-with-cache@v1 + uses: Typeform/.github/shared-actions/setup-node-with-cache@test-tmpfs-cache-extract with: node-version: ${{ inputs.node-version }} use-asdf: ${{ inputs.use-asdf }} cache-mode: ${{ inputs.cache-mode }} disable-restore-keys: ${{ inputs.disable-restore-keys }} + use-tmpfs: ${{ inputs.use-tmpfs }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: Setup Jarvis @@ -435,12 +441,13 @@ jobs: uses: actions/checkout@v6 - name: Setup Node with Cache - uses: Typeform/.github/shared-actions/setup-node-with-cache@v1 + uses: Typeform/.github/shared-actions/setup-node-with-cache@test-tmpfs-cache-extract with: node-version: ${{ inputs.node-version }} use-asdf: ${{ inputs.use-asdf }} cache-mode: ${{ inputs.cache-mode }} disable-restore-keys: ${{ inputs.disable-restore-keys }} + use-tmpfs: ${{ inputs.use-tmpfs }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: Setup Jarvis @@ -501,12 +508,13 @@ jobs: uses: actions/checkout@v6 - name: Setup Node with Cache - uses: Typeform/.github/shared-actions/setup-node-with-cache@v1 + uses: Typeform/.github/shared-actions/setup-node-with-cache@test-tmpfs-cache-extract with: node-version: ${{ inputs.node-version }} use-asdf: ${{ inputs.use-asdf }} cache-mode: ${{ inputs.cache-mode }} disable-restore-keys: ${{ inputs.disable-restore-keys }} + use-tmpfs: ${{ inputs.use-tmpfs }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download Build Artifacts @@ -574,13 +582,14 @@ jobs: needs: [build, unit-tests] permissions: contents: read - uses: Typeform/.github/.github/workflows/sonarcloud-scan.yml@v1 + uses: Typeform/.github/.github/workflows/sonarcloud-scan.yml@test-tmpfs-cache-extract with: app-name: ${{ inputs.app-name }} node-version: ${{ inputs.node-version }} use-asdf: ${{ inputs.use-asdf }} cache-mode: ${{ inputs.cache-mode }} disable-restore-keys: ${{ inputs.disable-restore-keys }} + use-tmpfs: ${{ inputs.use-tmpfs }} runner: ${{ inputs.runner }} coverage-artifact-name: ${{ inputs.run-unit-tests && format('coverage-{0}', github.run_id) || '' }} timeout: ${{ inputs.sonarcloud-timeout }} diff --git a/.github/workflows/sonarcloud-scan.yml b/.github/workflows/sonarcloud-scan.yml index 6bed514..ee76600 100644 --- a/.github/workflows/sonarcloud-scan.yml +++ b/.github/workflows/sonarcloud-scan.yml @@ -27,7 +27,11 @@ on: description: 'Disable restore-keys to avoid restoring stale caches (forces exact key match)' type: boolean default: false - + use-tmpfs: + description: 'Mount tmpfs at cache target paths (PLT-3439 test).' + type: boolean + default: false + # Runner configuration runner: description: 'Runner for SonarCloud scan' @@ -68,12 +72,13 @@ jobs: fetch-depth: 0 # Required for SonarCloud to analyze git history - name: Setup Node with Cache - uses: Typeform/.github/shared-actions/setup-node-with-cache@v1 + uses: Typeform/.github/shared-actions/setup-node-with-cache@test-tmpfs-cache-extract with: node-version: ${{ inputs.node-version }} use-asdf: ${{ inputs.use-asdf }} cache-mode: ${{ inputs.cache-mode }} disable-restore-keys: ${{ inputs.disable-restore-keys }} + use-tmpfs: ${{ inputs.use-tmpfs }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download coverage artifacts diff --git a/shared-actions/setup-node-with-cache/action.yml b/shared-actions/setup-node-with-cache/action.yml index 03232b0..a39389a 100644 --- a/shared-actions/setup-node-with-cache/action.yml +++ b/shared-actions/setup-node-with-cache/action.yml @@ -24,10 +24,60 @@ inputs: description: 'Disable restore-keys to avoid restoring stale caches (forces exact key match)' required: false default: 'false' + use-tmpfs: + description: 'Mount tmpfs (RAM-backed) at cache target paths to bypass disk I/O and memcg dirty-page accounting (PLT-3439).' + required: false + default: 'false' runs: using: 'composite' steps: + # Mount tmpfs at paths that will receive cached content, before any cache + # restore runs. On ARC scale-set runners the pod's ephemeral storage is + # shared across the k8s node and is IOPS-throttled at the cgroup level; + # extracting hundreds of MB of small files into it triggers writeback + # backpressure that blocks subsequent syscalls for tens of seconds. + # Using tmpfs avoids both the disk and the memcg dirty-page budget. + # Graceful: if mount fails (e.g. no CAP_SYS_ADMIN), we log and continue + # with disk-backed paths. See https://typeform.atlassian.net/browse/PLT-3439. + - name: Mount tmpfs at cache targets + if: ${{ inputs.use-tmpfs == 'true' && !env.ACT }} + shell: bash + run: | + set +e + mount_tmpfs() { + local path="$1" + local size="$2" + sudo mkdir -p "$path" 2>/dev/null + if sudo mount -t tmpfs -o size="$size" tmpfs "$path" 2>/tmp/mount.err; then + sudo chown -R "$(id -u):$(id -g)" "$path" 2>/dev/null + echo "✓ mounted tmpfs size=$size at $path" + else + echo "⚠ tmpfs mount failed at $path: $(cat /tmp/mount.err 2>/dev/null)" + echo " Continuing with disk-backed storage." + fi + } + + if [ "${{ inputs.use-asdf }}" == "true" ]; then + mount_tmpfs "$HOME/.asdf/installs" "2g" + mount_tmpfs "$HOME/.asdf/plugins" "200m" + fi + + case "${{ inputs.cache-mode }}" in + full|node_modules-only|"") + mount_tmpfs "$GITHUB_WORKSPACE/node_modules" "3g" + ;; + esac + + case "${{ inputs.cache-mode }}" in + full|yarn-cache-only) + mount_tmpfs "$HOME/.cache/yarn" "1g" + ;; + esac + + echo "=== Mounted filesystems ===" + mount | grep -E 'tmpfs.*(asdf|node_modules|yarn)' || echo "(no tmpfs mounts present)" + # Cache 1: asdf installations (Node.js and Yarn binaries) # This cache is separate because it only changes when .tool-versions is updated, # which happens infrequently (only when upgrading Node/Yarn versions).