projenrc/benchmark-test.ts (315 lines of code) (raw):
import { spawn } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { github, typescript } from 'projen';
import { JobPermission } from 'projen/lib/github/workflows-model';
import * as tar from 'tar';
import * as ts from 'typescript';
import * as yargs from 'yargs';
import { ACTIONS_SETUP_NODE, YARN_INSTALL } from './common';
export class BenchmarkTest {
public constructor(
project: typescript.TypeScriptProject,
wf: github.GithubWorkflow,
{ needs, artifactName }: { readonly artifactName: string; readonly needs: string[] },
) {
project.addTask('test:benchmark', {
description: 'Executes the benchmark test',
exec: 'ts-node ./projenrc/benchmark-test.ts',
receiveArgs: true,
});
const iterations = 20;
const indices = Array.from({ length: iterations }, (_, idx) => idx);
wf.addJobs({
benchmark: {
env: { CI: 'true' },
name: 'Benchmark (${{ matrix.compiler }}, run ${{ matrix.index }})',
needs,
outputs: Object.fromEntries(
indices.flatMap((idx) => [
[`jsii-${idx}`, { stepId: 'run', outputName: `jsii-${idx}` }],
[`tsc-${idx}`, { stepId: 'run', outputName: `tsc-${idx}` }],
]),
),
permissions: { contents: github.workflows.JobPermission.READ },
runsOn: ['ubuntu-latest'],
strategy: {
matrix: {
domain: {
compiler: ['jsii', 'tsc'],
// Run each 20 times to average out disparities in timing...
index: indices,
},
},
},
steps: [
ACTIONS_SETUP_NODE(undefined, false),
{
name: 'Download artifact',
uses: 'actions/download-artifact@v4',
with: { name: artifactName },
},
YARN_INSTALL('--check-files'),
{
id: 'run',
name: 'Benchmark',
run: [
'set -x',
'RESULT=$(yarn --silent projen test:benchmark --compiler=${{ matrix.compiler }})',
'echo "${{ matrix.compiler }}-${{ matrix.index }}=${RESULT}" >> $GITHUB_OUTPUT',
].join('\n'),
},
],
},
benchmark_summary: {
env: { CI: 'true' },
name: 'Benchmark',
needs: ['benchmark', 'build'],
permissions: {
idToken: JobPermission.WRITE,
},
runsOn: ['ubuntu-latest'],
steps: [
{
name: 'Output Summary',
id: 'output_summary',
run: [
'node <<"EOF"',
'const fs = require("node:fs");',
'',
`const outputFilePath = process.env.GITHUB_OUTPUT;`,
'',
'const results = ${{ toJSON(needs.benchmark.outputs) }};',
'console.debug(results);',
'',
'const stats = {};',
'for (const [key, value] of Object.entries(results)) {',
' const [compiler,] = key.split("-");',
' stats[compiler] ??= [];',
' stats[compiler].push(JSON.parse(value).time);',
'}',
'',
'for (const [compiler, values] of Object.entries(stats)) {',
' const avg = values.reduce((a, b) => a + b, 0) / values.length;',
' const variance = values.reduce((vari, elt) => ((elt - avg) ** 2) + vari, 0) / values.length;',
' stats[compiler] = {',
' min: Math.min(...values),',
' max: Math.max(...values),',
' avg,',
' stddev: Math.sqrt(variance),',
' };',
'}',
'const fastest = Object.values(stats).reduce((fast, { avg }) => Math.min(fast, avg), Infinity);',
'',
'const summary = [',
' "## Benchmark Results",',
' "",',
' "Compiler | Fastest | Avergae | Slowest | StdDev | Slowdown",',
' "---------|--------:|--------:|--------:|-------:|--------:",',
'];',
'const ms = new Intl.NumberFormat("en-US", { style: "unit", unit: "millisecond", maximumFractionDigits: 1, minimumFractionDigits: 1 });',
'const dec = new Intl.NumberFormat("en-US", { style: "decimal", maximumFractionDigits: 1, minimumFractionDigits: 1 });',
'const pre = (s) => `\\`${s}\\``;',
'for (const [compiler, { min, max, avg, stddev }] of Object.entries(stats).sort(([, l], [, r]) => l.avg - r.avg)) {',
' summary.push([compiler, pre(ms.format(min)), pre(ms.format(avg)), pre(ms.format(max)), pre(dec.format(stddev)), pre(`${dec.format(avg / fastest)}x`)].join(" | "));',
` const key = 'duration-' + compiler;`,
` const value = avg;`,
` const slowdownKey = compiler + '-slowdown';`,
` const slowdownValue = dec.format(avg / fastest);`,
'',
' fs.appendFileSync(outputFilePath, `${key}=${value}\n`)',
' fs.appendFileSync(outputFilePath, `${slowdownKey}=${slowdownValue}\n`)',
'}',
'summary.push("");',
'',
'fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary.join("\\n"), "utf-8");',
'EOF',
].join('\n'),
},
{
name: 'Authenticate Via OIDC Role',
if: `github.event.repository.fork == false && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/maintenance/v'))`,
uses: 'aws-actions/configure-aws-credentials@v4',
with: {
'aws-region': 'us-east-1',
'role-duration-seconds': 900,
'role-to-assume': 'arn:aws:iam::590183883712:role/Ops-jsiiTeamOIDC-Role1ABCC5F0-jL37v7e7I15P',
'role-session-name': 'github-diff-action@cdk-ops',
'output-credentials': true,
},
},
{
name: 'Publish Metrics',
if: `github.event.repository.fork == false && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/maintenance/v'))`,
run: `aws cloudwatch put-metric-data --namespace JsiiPerformance --metric-data '[
{
"MetricName": "TSC-average",
"Value": \${{steps.output_summary.outputs.duration-tsc}}
},
{
"MetricName": "JSII-average",
"Value": \${{steps.output_summary.outputs.duration-jsii}}
},
{
"MetricName": "JSII-slowdown",
"Value": \${{steps.output_summary.outputs.jsii-slowdown}}
},
{
"MetricName": "TSC-average",
"Value": \${{steps.output_summary.outputs.duration-tsc}},
"Dimensions": [
{
"Name": "TscVersion",
"Value": "\${{ needs.build.outputs.release-line }}"
}
]
},
{
"MetricName": "JSII-average",
"Value": \${{steps.output_summary.outputs.duration-jsii}},
"Dimensions": [
{
"Name": "JsiiVersion",
"Value": "\${{ needs.build.outputs.release-line }}"
}
]
},
{
"MetricName": "JSII-slowdown",
"Value": \${{steps.output_summary.outputs.jsii-slowdown}},
"Dimensions": [
{
"Name": "JsiiVersion",
"Value": "\${{ needs.build.outputs.release-line }}"
}
]
}
]'`,
},
],
},
});
}
}
if (require.main === module) {
(async function () {
const { compiler, silent } = await yargs
.scriptName('yarn projen test:benchmark')
.option('silent', {
default: false,
desc: 'Run silently, hiding sub-command outputs',
type: 'boolean',
})
.option('compiler', {
choices: ['tsc', 'jsii'],
desc: 'The compiler to be used for benchmarking',
demandOption: true,
coerce: (value: string) => {
switch (value) {
case 'jsii':
case 'tsc':
return value;
default:
throw new Error(`Invalid compiler name: ${JSON.stringify(value)}`);
}
},
})
.help()
.parseAsync();
const workDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-benchmark-'));
try {
// Extract the fixture tarball into the work directory
await tar.x({
file: join(__dirname, '..', 'fixtures', '.tarballs', 'aws-cdk-lib.tgz'),
cwd: workDir,
});
await new Promise<void>((ok, ko) => {
const child = spawn('yarn', ['install', '--frozen-lockfile'], {
stdio: silent ? 'ignore' : ['ignore', process.stderr, process.stderr],
cwd: workDir,
});
child.once('exit', (code, signal) => {
if (code === 0) {
return ok();
}
const reason = code != null ? `exit code ${code}` : `signal ${signal}`;
ko(new Error(`jsii exited with ${reason}`));
});
});
// Build with the selected compiler
const time = await new Promise((ok, ko) => {
const command = (function () {
switch (compiler) {
case 'jsii':
return [require.resolve('../lib/main.js'), workDir, '--silence-warnings=reserved-word'];
case 'tsc':
const tsconfig = join(workDir, 'tsconfig.tsc.json');
writeFileSync(
tsconfig,
JSON.stringify(
{
compilerOptions: {
alwaysStrict: true,
composite: false,
declaration: true,
declarationMap: false,
experimentalDecorators: true,
incremental: true,
inlineSourceMap: true,
inlineSources: true,
lib: ['es2020'],
module: 'CommonJS',
noEmitOnError: true,
noFallthroughCasesInSwitch: true,
noImplicitAny: true,
noImplicitReturns: true,
noImplicitThis: true,
noUnusedLocals: true,
noUnusedParameters: true,
resolveJsonModule: true,
skipLibCheck: true,
strictNullChecks: true,
strictPropertyInitialization: true,
stripInternal: false,
target: 'ES2020',
tsBuildInfoFile: 'tsconfig.tsbuildinfo',
} satisfies Serialized<ts.CompilerOptions>,
include: ['**/*.ts'],
exclude: ['node_modules', '.types-compat', 'build-tools/*'],
},
null,
2,
),
);
return [require.resolve('typescript/bin/tsc'), '--build', tsconfig];
}
})();
const now = Date.now();
const child = spawn(process.execPath, command, {
env: {
NODE_OPTIONS: '--max_old_space_size=4096',
},
stdio: silent ? 'ignore' : ['ignore', process.stderr, process.stderr],
});
child.once('exit', (code, signal) => {
if (code === 0) {
return ok(Date.now() - now);
}
const reason = code != null ? `exit code ${code}` : `signal ${signal}`;
ko(new Error(`${compiler} exited with ${reason}`));
});
});
console.log(JSON.stringify({ time }));
} finally {
rmSync(workDir, { force: true, recursive: true });
}
})().catch((cause) => {
console.error(cause);
process.exitCode = -1;
});
}
type Serialized<T> = {
[P in keyof T]: T[P] extends ts.ModuleKind | undefined
? undefined | keyof typeof ts.ModuleKind
: T[P] extends ts.ScriptTarget | undefined
? undefined | keyof typeof ts.ScriptTarget
: T[P];
};