Repository: electron/electron
Pull request: ci: upload build effective cache hit rate stats to Datadog

#### Description of Change

<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md
-->

We want to track this over time, as historically it was lower than we would have expected.

It looks like this currently doesn't work on the Windows build jobs as `siso.INFO` isn't present - I'll look into this separately and follow-up as needed.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd

#### Release Notes

Notes: none <!-- Please add a one-line description for app developers to read in the release notes, or 'none' if no notes relevant to app developers. Examples and help on special cases: https://github.com/electron/clerk/blob/main/README.md#examples -->


Changed files:
filename: .github/actions/build-electron/action.yml
patch: @@ -63,6 +63,13 @@ runs:
         NINJA_SUMMARIZE_BUILD=1 e build
         cp out/Default/.ninja_log out/electron_ninja_log
         node electron/script/check-symlinks.js
+
+        # Upload build stats to Datadog
+        if ! [ -z $DD_API_KEY ]; then
+          npx node electron/script/build-stats.mjs out/Default/siso.INFO --upload-stats || true
+        else
+          echo "Skipping build-stats.mjs upload because DD_API_KEY is not set"
+        fi
     - name: Build Electron dist.zip ${{ inputs.step-suffix }}
       shell: bash
       run: |

filename: .github/workflows/pipeline-segment-electron-build.yml
patch: @@ -66,6 +66,7 @@ concurrency:
 env:
   CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }}
   CHROMIUM_GIT_COOKIE_WINDOWS_STRING: ${{ secrets.CHROMIUM_GIT_COOKIE_WINDOWS_STRING }}
+  DD_API_KEY: ${{ secrets.DD_API_KEY }}
   ELECTRON_ARTIFACTS_BLOB_STORAGE: ${{ secrets.ELECTRON_ARTIFACTS_BLOB_STORAGE }}
   ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
   SUDOWOODO_EXCHANGE_URL: ${{ secrets.SUDOWOODO_EXCHANGE_URL }}
@@ -84,6 +85,7 @@ jobs:
     environment: ${{ inputs.environment }}
     env:
       TARGET_ARCH: ${{ inputs.target-arch }}
+      TARGET_PLATFORM: ${{ inputs.target-platform }}
     steps:
     - name: Create src dir
       run: |

filename: script/build-stats.mjs
patch: @@ -0,0 +1,90 @@
+import * as fs from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import { parseArgs } from 'node:util';
+
+async function main () {
+  const { positionals: [filename], values: { 'upload-stats': uploadStats } } = parseArgs({
+    allowPositionals: true,
+    options: {
+      'upload-stats': {
+        type: 'boolean',
+        default: false
+      }
+    }
+  });
+
+  if (!filename) {
+    throw new Error('filename is required (should be a siso.INFO file)');
+  }
+
+  const log = await fs.readFile(filename, 'utf-8');
+
+  // We expect to find a line which looks like stats=build.Stats{..., CacheHit:39008, Local:4778, Remote:0, LocalFallback:0, ...}
+  const match = log.match(/stats=build\.Stats{(.*)}/);
+
+  if (!match) {
+    throw new Error('could not find stats=build.Stats in log');
+  }
+
+  const stats = Object.fromEntries(match[1].split(',').map(part => {
+    const [key, value] = part.trim().split(':');
+    return [key, parseInt(value)];
+  }));
+  const hitRate = stats.CacheHit / (stats.Remote + stats.CacheHit + stats.LocalFallback);
+
+  console.log(`Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
+
+  if (uploadStats) {
+    if (!process.env.DD_API_KEY) {
+      throw new Error('DD_API_KEY is not set');
+    }
+
+    const timestamp = Math.round(new Date().getTime() / 1000);
+
+    const tags = [];
+
+    if (process.env.TARGET_ARCH) tags.push(`target-arch:${process.env.TARGET_ARCH}`);
+    if (process.env.TARGET_PLATFORM) tags.push(`target-platform:${process.env.TARGET_PLATFORM}`);
+    if (process.env.GITHUB_HEAD_REF) tags.push(`branch:${process.env.GITHUB_HEAD_REF}`);
+
+    const series = [
+      {
+        metric: 'electron.build.effective-cache-hit-rate',
+        points: [{ timestamp, value: (hitRate * 100).toFixed(2) }],
+        type: 3, // GAUGE
+        unit: 'percent',
+        tags
+      }
+    ];
+
+    // Add all raw stats as individual metrics
+    for (const [key, value] of Object.entries(stats)) {
+      series.push({
+        metric: `electron.build.stats.${key.toLowerCase()}`,
+        points: [{ timestamp, value }],
+        type: 1, // COUNT
+        tags
+      });
+    }
+
+    await fetch('https://api.datadoghq.com/api/v2/series', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'DD-API-KEY': process.env.DD_API_KEY
+      },
+      body: JSON.stringify({ series })
+    });
+  }
+}
+
+if ((await fs.realpath(process.argv[1])) === fileURLToPath(import.meta.url)) {
+  main()
+    .then(() => {
+      process.exit(0);
+    })
+    .catch((err) => {
+      console.error(`ERROR: ${err.message}`);
+      process.exit(1);
+    });
+}