mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
ci: upload object change stats to Datadog (#50390)
* ci: upload object change stats to Datadog Assisted-by: Claude Opus 4.6 * ci: bump actions/upload-artifact version * chore: only output new object count if non-zero * chore: skip object change tracking on ASan builds * chore: handle pull requests as well * chore: always set chromium-version-changed * chore: remove npx usage
This commit is contained in:
@@ -1,22 +1,143 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
import { getChromiumVersionFromDEPS } from './lib/utils.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ELECTRON_DIR = resolve(__dirname, '..');
|
||||
|
||||
function getCommonTags () {
|
||||
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) {
|
||||
// Will be set in pull requests
|
||||
tags.push(`branch:${process.env.GITHUB_HEAD_REF}`);
|
||||
} else if (process.env.GITHUB_REF_NAME) {
|
||||
// Will be set for release branches
|
||||
tags.push(`branch:${process.env.GITHUB_REF_NAME}`);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
async function uploadSeriesToDatadog (series) {
|
||||
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 })
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadCacheHitRateStats (hitRate, stats) {
|
||||
const timestamp = Math.round(new Date().getTime() / 1000);
|
||||
const tags = getCommonTags();
|
||||
|
||||
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 uploadSeriesToDatadog(series);
|
||||
}
|
||||
|
||||
async function uploadObjectChangeStats (stats) {
|
||||
const timestamp = Math.round(new Date().getTime() / 1000);
|
||||
const tags = getCommonTags();
|
||||
|
||||
if (stats['previous-chromium-version']) tags.push(`previous-chromium-version:${stats['previous-chromium-version']}`);
|
||||
if (stats['chromium-version']) tags.push(`chromium-version:${stats['chromium-version']}`);
|
||||
|
||||
if (stats['previous-chromium-version'] && stats['chromium-version']) {
|
||||
tags.push(`chromium-version-changed:${stats['previous-chromium-version'] !== stats['chromium-version']}`);
|
||||
}
|
||||
|
||||
const series = [
|
||||
{
|
||||
metric: 'electron.build.object-change-rate',
|
||||
points: [{ timestamp, value: (stats['change-rate'] * 100).toFixed(2) }],
|
||||
type: 3, // GAUGE
|
||||
unit: 'percent',
|
||||
tags
|
||||
},
|
||||
{
|
||||
metric: 'electron.build.object-change-size',
|
||||
points: [{ timestamp, value: stats['change-size'] }],
|
||||
type: 1, // COUNT
|
||||
unit: 'byte',
|
||||
tags
|
||||
},
|
||||
{
|
||||
metric: 'electron.build.new-object-count',
|
||||
points: [{ timestamp, value: stats['new-object-count'] }],
|
||||
type: 1, // COUNT
|
||||
unit: 'count',
|
||||
tags
|
||||
}
|
||||
];
|
||||
|
||||
await uploadSeriesToDatadog(series);
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const { positionals: [filename], values: { 'upload-stats': uploadStats } } = parseArgs({
|
||||
const { positionals: [filename], values } = parseArgs({
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
'upload-stats': {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'out-dir': {
|
||||
type: 'string'
|
||||
},
|
||||
'input-object-checksums': {
|
||||
type: 'string'
|
||||
},
|
||||
'output-object-checksums': {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
'upload-stats': uploadStats,
|
||||
'out-dir': outDir,
|
||||
'input-object-checksums': inputObjectChecksums,
|
||||
'output-object-checksums': outputObjectChecksums
|
||||
} = values;
|
||||
|
||||
if (!filename) {
|
||||
throw new Error('filename is required (should be a siso.INFO file)');
|
||||
}
|
||||
|
||||
if ((inputObjectChecksums || outputObjectChecksums) && !outDir) {
|
||||
throw new Error('--out-dir is required when using --input-object-checksums or --output-object-checksums');
|
||||
} else if (outDir && (!inputObjectChecksums && !outputObjectChecksums)) {
|
||||
throw new Error('--out-dir only makes sense with --input-object-checksums or --output-object-checksums');
|
||||
}
|
||||
|
||||
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, ...}
|
||||
@@ -33,55 +154,83 @@ async function main () {
|
||||
const hitRate = stats.CacheHit / (stats.Remote + stats.CacheHit + stats.LocalFallback);
|
||||
|
||||
const messagePrefix = process.env.GITHUB_ACTIONS ? '::notice title=Build Stats::' : '';
|
||||
|
||||
console.log(`${messagePrefix}Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
|
||||
|
||||
const objectChangeStats = {};
|
||||
|
||||
if (inputObjectChecksums || outputObjectChecksums) {
|
||||
const depsContent = await fs.readFile(resolve(ELECTRON_DIR, 'DEPS'), 'utf8');
|
||||
const currentVersion = getChromiumVersionFromDEPS(depsContent);
|
||||
|
||||
// Calculate the SHA256 for each object file under `outDir`
|
||||
const objectFiles = await fs.readdir(outDir, { encoding: 'utf8', recursive: true });
|
||||
const checksums = {};
|
||||
for (const file of objectFiles.filter(f => f.endsWith('.o'))) {
|
||||
const content = await fs.readFile(resolve(outDir, file));
|
||||
checksums[file] = createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
if (outputObjectChecksums) {
|
||||
const outputData = {
|
||||
chromiumVersion: currentVersion,
|
||||
checksums
|
||||
};
|
||||
|
||||
await fs.writeFile(outputObjectChecksums, JSON.stringify(outputData, null, 2));
|
||||
}
|
||||
|
||||
if (inputObjectChecksums) {
|
||||
const inputData = JSON.parse(await fs.readFile(inputObjectChecksums, 'utf8'));
|
||||
const inputFiles = Object.keys(inputData.checksums);
|
||||
let changedCount = 0;
|
||||
let newObjectCount = 0;
|
||||
let changedSize = 0;
|
||||
|
||||
// Count changed files (only those present in both input and current)
|
||||
for (const file of inputFiles) {
|
||||
if (!(file in checksums)) continue; // Skip deleted files
|
||||
if (inputData.checksums[file] !== checksums[file]) {
|
||||
changedCount++;
|
||||
const stat = await fs.stat(resolve(outDir, file));
|
||||
changedSize += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Count new files (in current but not in input)
|
||||
for (const file of Object.keys(checksums)) {
|
||||
if (!(file in inputData.checksums)) {
|
||||
newObjectCount++;
|
||||
const stat = await fs.stat(resolve(outDir, file));
|
||||
changedSize += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
const changeRate = inputFiles.length > 0 ? changedCount / inputFiles.length : 0;
|
||||
console.log(`${messagePrefix}Object change rate: ${(changeRate * 100).toFixed(2)}%`);
|
||||
if (newObjectCount > 0) {
|
||||
console.log(`${messagePrefix}New object count: ${newObjectCount}`);
|
||||
}
|
||||
console.log(`${messagePrefix}Cumulative changed object sizes: ${changedSize.toLocaleString()} bytes`);
|
||||
|
||||
objectChangeStats['change-rate'] = changeRate;
|
||||
objectChangeStats['change-size'] = changedSize;
|
||||
objectChangeStats['new-object-count'] = newObjectCount;
|
||||
objectChangeStats['previous-chromium-version'] = inputData.chromiumVersion;
|
||||
objectChangeStats['chromium-version'] = currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
await uploadCacheHitRateStats(hitRate, stats);
|
||||
|
||||
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) {
|
||||
// Will be set in pull requests
|
||||
tags.push(`branch:${process.env.GITHUB_HEAD_REF}`);
|
||||
} else if (process.env.GITHUB_REF_NAME) {
|
||||
// Will be set for release branches
|
||||
tags.push(`branch:${process.env.GITHUB_REF_NAME}`);
|
||||
if (Object.keys(objectChangeStats).length > 0) {
|
||||
await uploadObjectChangeStats(objectChangeStats);
|
||||
}
|
||||
|
||||
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 })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ const path = require('node:path');
|
||||
const ELECTRON_DIR = path.resolve(__dirname, '..', '..');
|
||||
const SRC_DIR = path.resolve(ELECTRON_DIR, '..');
|
||||
|
||||
const CHROMIUM_VERSION_DEPS_REGEX = /chromium_version':\n +'(.+?)',/m;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const pass = chalk.green('✓');
|
||||
const fail = chalk.red('✗');
|
||||
@@ -162,10 +164,15 @@ function compareVersions (v1, v2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getChromiumVersionFromDEPS (depsContent) {
|
||||
return CHROMIUM_VERSION_DEPS_REGEX.exec(depsContent)?.[1] ?? null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chunkFilenames,
|
||||
compareVersions,
|
||||
findMatchingFiles,
|
||||
getChromiumVersionFromDEPS,
|
||||
getCurrentBranch,
|
||||
getElectronExec,
|
||||
getOutDir,
|
||||
|
||||
@@ -5,12 +5,11 @@ import * as fs from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { compareVersions } from './lib/utils.js';
|
||||
import { compareVersions, getChromiumVersionFromDEPS } from './lib/utils.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ELECTRON_DIR = resolve(__dirname, '..');
|
||||
|
||||
const DEPS_REGEX = /chromium_version':\n +'(.+?)',/m;
|
||||
const CL_REGEX = /https:\/\/chromium-review\.googlesource\.com\/c\/(?:chromium\/src|v8\/v8)\/\+\/(\d+)(#\S+)?/g;
|
||||
const ROLLER_BRANCH_PATTERN = /^roller\/chromium\/(.+)$/;
|
||||
|
||||
@@ -140,12 +139,12 @@ async function main () {
|
||||
cwd: ELECTRON_DIR,
|
||||
encoding: 'utf8'
|
||||
});
|
||||
baseVersion = DEPS_REGEX.exec(baseDepsContent)?.[1] ?? null;
|
||||
baseVersion = getChromiumVersionFromDEPS(baseDepsContent);
|
||||
} catch {
|
||||
// baseVersion remains null
|
||||
}
|
||||
const depsContent = await fs.readFile(resolve(ELECTRON_DIR, 'DEPS'), 'utf8');
|
||||
const newVersion = DEPS_REGEX.exec(depsContent)?.[1] ?? null;
|
||||
const newVersion = getChromiumVersionFromDEPS(depsContent);
|
||||
|
||||
if (!baseVersion || !newVersion) {
|
||||
console.error('Could not determine Chromium version range');
|
||||
|
||||
Reference in New Issue
Block a user