mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 20:32:10 +00:00
This PR shards benchmarks when running on CI. Each benchmark (parser, minifier etc) runs as a separate job, and then a final job combines the results and uploads to Codspeed. A bit of a hacky implementation. Uses a small NodeJS HTTP server to intercept the results from `codspeed-runner`, and then another NodeJS script to combine them all together, and upload to CodSpeed. I will submit PRs on Codspeed's runner + action to do it properly, but as I imagine it'll be a slow process getting that merged upstream, I wanted to see if it worked first. We can replace this once it's supported upstream. Sharding only reduces total time to run the benchmarks by about 70 secs at present, because linter benchmark takes 6 mins alone and holds up the whole process (all the rest are done in ~2 mins). If we can split up the linter benchmark, we can likely get total run time down to around 3 mins. I'll try that in a follow-on PR. I guess the other upside is we can now add as many benchmarks as we like with impunity - they'll run in parallel, and so won't slow things down overall.
135 lines
4.1 KiB
JavaScript
135 lines
4.1 KiB
JavaScript
/*
|
|
* Combine benchmark data from different jobs, and upload to Codspeed.
|
|
*/
|
|
|
|
import {createReadStream} from 'fs';
|
|
import fs from 'fs/promises';
|
|
import {join as pathJoin} from 'path';
|
|
import {createHash} from 'crypto';
|
|
import assert from 'assert';
|
|
import tar from 'tar';
|
|
import axios from 'axios';
|
|
|
|
const METADATA_SUFFIX = 'metadata.json',
|
|
ARCHIVE_SUFFIX = `archive.tar.gz`,
|
|
CODSPEED_UPLOAD_URL = 'https://api.codspeed.io/upload';
|
|
|
|
const dataDir = process.env.DATA_DIR,
|
|
token = process.env.CODSPEED_TOKEN;
|
|
|
|
// Get list of components
|
|
const components = (await fs.readdir(dataDir))
|
|
.filter(filename => filename.endsWith(METADATA_SUFFIX))
|
|
.map(filename => filename.slice(0, -METADATA_SUFFIX.length - 1));
|
|
|
|
// Unzip tarballs
|
|
const unzipDir = pathJoin(dataDir, 'unzip');
|
|
await fs.mkdir(unzipDir);
|
|
|
|
for (const component of components) {
|
|
console.log(`Unzipping profile data: ${component}`);
|
|
const archivePath = pathJoin(dataDir, `${component}_${ARCHIVE_SUFFIX}`);
|
|
const componentUnzipDir = pathJoin(unzipDir, component);
|
|
await fs.mkdir(componentUnzipDir);
|
|
await tar.extract({file: archivePath, cwd: componentUnzipDir});
|
|
await fs.rm(archivePath);
|
|
}
|
|
|
|
// Move all `.out` files to one directory
|
|
console.log('Combining profiles');
|
|
|
|
const outDir = pathJoin(dataDir, 'out');
|
|
await fs.mkdir(outDir);
|
|
|
|
const pids = new Set(),
|
|
duplicates = [];
|
|
let highestPid = -1;
|
|
for (const component of components) {
|
|
const componentDir = pathJoin(unzipDir, component);
|
|
const outFiles = await fs.readdir(componentDir);
|
|
for (const filename of outFiles) {
|
|
if (!filename.endsWith('.out')) continue;
|
|
let pid = filename.slice(0, -4);
|
|
assert(/^\d+$/.test(pid), `Unexpected file: ${component}/${filename}`);
|
|
pid *= 1;
|
|
|
|
const path = pathJoin(componentDir, filename);
|
|
if (pids.has(pid)) {
|
|
// Duplicate PID
|
|
duplicates.push({pid, path});
|
|
} else {
|
|
pids.add(pid);
|
|
if (pid > highestPid) highestPid = pid;
|
|
|
|
await fs.rename(path, pathJoin(outDir, `${pid}.out`));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Alter PIDs for `.out` files with duplicate filenames
|
|
for (let {pid, path} of duplicates) {
|
|
let content = await fs.readFile(path, 'utf8');
|
|
|
|
const pidLine = `\npid: ${pid}\n`;
|
|
const index = content.indexOf(pidLine);
|
|
assert(index !== -1, `Could not locate PID in ${path}`);
|
|
const before = content.slice(0, index);
|
|
assert(before.split('\n').length === 3, `Unexpected formatting in ${path}`);
|
|
|
|
pid = ++highestPid;
|
|
content = `${before}\npid: ${pid}\n${content.slice(index + pidLine.length)}`;
|
|
|
|
await fs.writeFile(pathJoin(outDir, `${pid}.out`), content);
|
|
await fs.rm(path);
|
|
}
|
|
|
|
// Add log files to output dir
|
|
for (const filename of ['runner.log', 'valgrind.log']) {
|
|
await fs.rename(pathJoin(unzipDir, components[0], filename), pathJoin(outDir, filename));
|
|
}
|
|
|
|
// ZIP combined profile directory
|
|
console.log('Zipping combined profile directory');
|
|
const archivePath = pathJoin(dataDir, 'archive.tar.gz');
|
|
await tar.create({file: archivePath, gzip: true, cwd: outDir}, ['./']);
|
|
|
|
// Get size + MD5 hash of archive
|
|
console.log('Hashing ZIP');
|
|
const {size} = await fs.stat(archivePath);
|
|
|
|
const hash = createHash('md5');
|
|
const inputStream = createReadStream(archivePath);
|
|
for await (const chunk of inputStream) {
|
|
hash.update(chunk);
|
|
}
|
|
const md5 = hash.digest('base64');
|
|
|
|
// Alter MD5 hash in metadata object
|
|
const metadata = JSON.parse(
|
|
await fs.readFile(pathJoin(dataDir, `${components[0]}_${METADATA_SUFFIX}`), 'utf8')
|
|
);
|
|
metadata.profileMd5 = md5;
|
|
|
|
// Upload metadata to CodSpeed
|
|
console.log('Uploading metadata to CodSpeed');
|
|
const {data} = await axios({
|
|
method: 'post',
|
|
url: CODSPEED_UPLOAD_URL,
|
|
data: metadata,
|
|
headers: {Authorization: token},
|
|
});
|
|
assert(data?.status === 'success', 'Failed to upload metadata to Codspeed');
|
|
const {uploadUrl} = data;
|
|
|
|
// Upload profile ZIP to Codspeed
|
|
console.log('Uploading profile ZIP to CodSpeed');
|
|
await axios({
|
|
method: 'put',
|
|
url: uploadUrl,
|
|
data: createReadStream(archivePath),
|
|
headers: {
|
|
'Content-Type': 'application/gzip',
|
|
'Content-Length': size,
|
|
'Content-MD5': md5,
|
|
}
|
|
});
|