Skip to content

Commit e4bf37a

Browse files
authored
build: speed up golden testing by parallelizing (#2714)
Speeds up golden testing by parallelizing. This got it down from ~85second of e.g. Material golden testing to 15seconds!t
1 parent 1ab7328 commit e4bf37a

File tree

5 files changed

+223
-18
lines changed

5 files changed

+223
-18
lines changed

bazel/api-golden/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ts_project(
2020
deps = [
2121
"//bazel:node_modules/@microsoft/api-extractor",
2222
"//bazel:node_modules/@types/node",
23+
"//bazel:node_modules/piscina",
2324
"//bazel:node_modules/typescript",
2425
],
2526
)

bazel/api-golden/index_npm_packages.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {normalizePathToPosix} from './path-normalize.js';
1212
import {readFileSync} from 'fs';
1313
import {testApiGolden} from './test_api_report.js';
1414
import * as fs from 'fs';
15+
import {Piscina} from 'piscina';
1516

1617
/** Interface describing contents of a `package.json`. */
1718
export interface PackageJson {
@@ -40,11 +41,11 @@ async function main(
4041
const packageJsonPath = path.join(npmPackageDir, 'package.json');
4142
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson;
4243
const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir, packageJson);
43-
const outdatedGoldens: string[] = [];
44-
45-
let allTestsSucceeding = true;
44+
const worker = new Piscina<Parameters<typeof testApiGolden>, string>({
45+
filename: path.resolve(__dirname, './test_api_report.js'),
46+
});
4647

47-
for (const {subpath, typesEntryPointPath} of entryPoints) {
48+
const processEntryPoint = async (subpath: string, typesEntryPointPath: string) => {
4849
// API extractor generates API reports as markdown files. For each types
4950
// entry-point we maintain a separate golden file. These golden files are
5051
// based on the name of the defining NodeJS exports subpath in the NPM package,
@@ -53,32 +54,56 @@ async function main(
5354
const goldenFilePath = path.join(goldenDir, goldenName);
5455
const moduleName = normalizePathToPosix(path.join(packageJson.name, subpath));
5556

56-
const actual = await testApiGolden(
57+
// Run API extractor in child processes. This is because API extractor is very
58+
// synchronous. This allows us to significantly speed up golden testing.
59+
const actual = await worker.run([
5760
typesEntryPointPath,
5861
stripExportPattern,
5962
typeNames,
6063
packageJsonPath,
6164
moduleName,
62-
);
65+
]);
6366

6467
if (actual === null) {
6568
console.error(`Could not generate API golden for subpath: "${subpath}". See errors above.`);
6669
process.exit(1);
6770
}
6871

6972
if (approveGolden) {
70-
fs.mkdirSync(path.dirname(goldenFilePath), {recursive: true});
71-
fs.writeFileSync(goldenFilePath, actual, 'utf8');
73+
await fs.promises.mkdir(path.dirname(goldenFilePath), {recursive: true});
74+
await fs.promises.writeFile(goldenFilePath, actual, 'utf8');
7275
} else {
73-
const expected = fs.readFileSync(goldenFilePath, 'utf8');
76+
const expected = await fs.promises.readFile(goldenFilePath, 'utf8');
7477
if (actual !== expected) {
7578
// Keep track of outdated goldens for error message.
7679
outdatedGoldens.push(goldenName);
77-
allTestsSucceeding = false;
80+
return false;
7881
}
7982
}
83+
84+
return true;
85+
};
86+
87+
const outdatedGoldens: string[] = [];
88+
const tasks: Promise<boolean>[] = [];
89+
// Process in batches. Otherwise we risk out of memory errors.
90+
const batchSize = 10;
91+
92+
for (let i = 0; i < entryPoints.length; i += batchSize) {
93+
const batchEntryPoints = entryPoints.slice(i, i + batchSize);
94+
95+
for (const {subpath, typesEntryPointPath} of batchEntryPoints) {
96+
tasks.push(processEntryPoint(subpath, typesEntryPointPath));
97+
}
98+
99+
// Wait for new batch.
100+
await Promise.all(tasks);
80101
}
81102

103+
// Wait for final batch/retrieve all results.
104+
const results = await Promise.all(tasks);
105+
const allTestsSucceeding = results.every((r) => r === true);
106+
82107
if (outdatedGoldens.length) {
83108
console.error(chalk.red(`The following goldens are outdated:`));
84109
outdatedGoldens.forEach((name) => console.info(`- ${name}`));

bazel/api-golden/test_api_report.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export async function testApiGolden(
5555
customPackageName: string,
5656
): Promise<string | null> {
5757
const tempDir =
58-
process.env.TEST_TMPDIR ?? fs.mkdtempSync(path.join(os.tmpdir(), 'api-golden-rule'));
58+
process.env.TEST_TMPDIR ??
59+
(await fs.promises.mkdtemp(path.join(os.tmpdir(), 'api-golden-rule')));
5960
const rjsMode = process.env['RJS_MODE'] === 'true';
6061

6162
let resolvedTypePackages: Awaited<ReturnType<typeof resolveTypePackages>> | null = null;
@@ -144,8 +145,8 @@ export async function testApiGolden(
144145
if (!result.succeeded) {
145146
return null;
146147
}
147-
const reportOut = fs.readFileSync(reportTmpOutPath, 'utf8');
148-
fs.rmSync(reportTmpOutPath);
148+
const reportOut = await fs.promises.readFile(reportTmpOutPath, 'utf8');
149+
await fs.promises.rm(reportTmpOutPath);
149150
return reportOut;
150151
}
151152

@@ -167,7 +168,6 @@ async function processExtractorMessage(message: ExtractorMessage) {
167168
}
168169
}
169170

170-
/** Resolves the `package.json` of the workspace executing this action. */
171-
function resolveWorkspacePackageJsonPath(): string {
172-
return path.resolve(`./package.json`);
171+
export default function (args: Parameters<typeof testApiGolden>) {
172+
return testApiGolden(...args);
173173
}

bazel/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"name": "@devinfra/bazel",
33
"dependencies": {
44
"@microsoft/api-extractor": "7.52.2",
5-
"typescript": "5.8.2",
6-
"@types/node": "22.14.0"
5+
"@types/node": "22.14.0",
6+
"piscina": "^4.9.2",
7+
"typescript": "5.8.2"
78
},
89
"pnpm": {
910
"onlyBuiltDependencies": []

bazel/pnpm-lock.yaml

Lines changed: 178 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)