Add benchmarking for dev and build (#240)

* Add benchmarking for the dev server

This adds benchmarking for the dev server.

* Use fs.rm instead

* Only rimraf if the folder exists

* Make uncached match CI

* Change the cached time too

* Don't run benchmark in CI

* Switch back test command

* Make tests be within 10 percent

* Use yarn to run multiple things

* Turn benchmark into uvu tests

* Debugging benchmark

* Print chunk for testing

* Ignore benchmark folder in uvu

* Add build benchmarking

* Update benchmark numbers
This commit is contained in:
Matthew Phillips 2021-05-26 11:31:53 -04:00 committed by GitHub
parent 8a375e66d2
commit da47225efe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 255 additions and 4 deletions

View file

@ -38,3 +38,22 @@ Commit and push these changes, then run an npm publish for each of the packages
cd packages/astro cd packages/astro
npm publish npm publish
``` ```
## Running benchmarks
We have benchmarks to keep performance under control. You can run these by running (from the project root):
```shell
yarn workspace astro run benchmark
```
Which will fail if the performance has regressed by __10%__ or more.
To update the times cd into the `packages/astro` folder and run the following:
```shell
node test/benchmark/build.bench.js --save
node test/benchmark/dev.bench.js --save
```
Which will update the build and dev benchmarks.

View file

@ -28,7 +28,8 @@
"build": "astro-scripts build \"src/*.ts\" \"src/compiler/index.ts\" \"src/frontend/**/*.ts\" && tsc", "build": "astro-scripts build \"src/*.ts\" \"src/compiler/index.ts\" \"src/frontend/**/*.ts\" && tsc",
"postbuild": "astro-scripts copy \"src/**/*.astro\"", "postbuild": "astro-scripts copy \"src/**/*.astro\"",
"dev": "astro-scripts dev \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "uvu test -i fixtures -i test-utils.js" "benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
"test": "uvu test -i fixtures -i benchmark -i test-utils.js"
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.12.13", "@babel/code-frame": "^7.12.13",

View file

@ -20,7 +20,7 @@ import { getDistPath, stopTimer } from './build/util.js';
import { debug, defaultLogDestination, error, info, warn, trapWarn } from './logger.js'; import { debug, defaultLogDestination, error, info, warn, trapWarn } from './logger.js';
import { createRuntime } from './runtime.js'; import { createRuntime } from './runtime.js';
const logging: LogOptions = { const defaultLogging: LogOptions = {
level: 'debug', level: 'debug',
dest: defaultLogDestination, dest: defaultLogDestination,
}; };
@ -39,7 +39,7 @@ function isRemote(url: string) {
} }
/** The primary build action */ /** The primary build action */
export async function build(astroConfig: AstroConfig): Promise<0 | 1> { export async function build(astroConfig: AstroConfig, logging: LogOptions = defaultLogging): Promise<0 | 1> {
const { projectRoot, astroRoot } = astroConfig; const { projectRoot, astroRoot } = astroConfig;
const dist = new URL(astroConfig.dist + '/', projectRoot); const dist = new URL(astroConfig.dist + '/', projectRoot);
const pageRoot = new URL('./pages/', astroRoot); const pageRoot = new URL('./pages/', astroRoot);

View file

@ -0,0 +1,68 @@
import { promises as fsPromises, existsSync } from 'fs';
import { performance } from 'perf_hooks';
const MUST_BE_AT_LEAST_PERC_OF = 90;
const shouldSave = process.argv.includes('--save');
export class Benchmark {
constructor(options) {
this.options = options;
this.setup = options.setup || Function.prototype;
}
async execute() {
const { run } = this.options;
const start = performance.now();
const end = await run(this.options);
const time = Math.floor(end - start);
return time;
}
async run() {
const { file } = this.options;
await this.setup();
const time = await this.execute();
if(existsSync(file)) {
const raw = await fsPromises.readFile(file, 'utf-8');
const data = JSON.parse(raw);
if(Math.floor(data.time / time * 100) > MUST_BE_AT_LEAST_PERC_OF) {
this.withinPreviousRuns = true;
} else {
this.withinPreviousRuns = false;
}
}
this.time = time;
}
report() {
const { name } = this.options;
console.log(name, 'took', this.time, 'ms');
}
check() {
if(this.withinPreviousRuns === false) {
throw new Error(`${this.options.name} ran too slowly`);
}
}
async save() {
const { file, name } = this.options;
const data = JSON.stringify({
name,
time: this.time
}, null, ' ');
await fsPromises.writeFile(file, data, 'utf-8');
}
async test() {
await this.run();
if(shouldSave) {
await this.save();
}
this.report();
this.check();
}
}

View file

@ -0,0 +1,4 @@
{
"name": "Snowpack Example Build Cached",
"time": 10484
}

View file

@ -0,0 +1,4 @@
{
"name": "Snowpack Example Build Uncached",
"time": 19629
}

View file

@ -0,0 +1,86 @@
import { fileURLToPath } from 'url';
import { performance } from 'perf_hooks';
import { build as astroBuild } from '#astro/build';
import { loadConfig } from '#astro/config';
import { Benchmark } from './benchmark.js';
import del from 'del';
import { Writable } from 'stream';
import { format as utilFormat } from 'util';
const snowpackExampleRoot = new URL('../../../../examples/snowpack/', import.meta.url);
export const errorWritable = new Writable({
objectMode: true,
write(event, _, callback) {
let dest = process.stderr;
dest.write(utilFormat(...event.args));
dest.write('\n');
callback();
},
});
let build;
async function setupBuild() {
const astroConfig = await loadConfig(fileURLToPath(snowpackExampleRoot));
const logging = {
level: 'error',
dest: errorWritable,
};
build = () => astroBuild(astroConfig, logging);
}
async function runBuild() {
await build();
return performance.now();
}
const benchmarks = [
new Benchmark({
name: 'Snowpack Example Build Uncached',
root: snowpackExampleRoot,
file: new URL('./build-uncached.json', import.meta.url),
async setup() {
process.chdir(new URL('../../../../', import.meta.url).pathname);
const spcache = new URL('../../node_modules/.cache/', import.meta.url);
await Promise.all([
del(spcache.pathname, { force: true }),
setupBuild()
]);
},
run: runBuild
}),
new Benchmark({
name: 'Snowpack Example Build Cached',
root: snowpackExampleRoot,
file: new URL('./build-cached.json', import.meta.url),
async setup() {
process.chdir(new URL('../../../../', import.meta.url).pathname);
await setupBuild();
await this.execute();
},
run: runBuild
}),
/*new Benchmark({
name: 'Snowpack Example Dev Server Cached',
root: snowpackExampleRoot,
file: new URL('./dev-server-cached.json', import.meta.url),
async setup() {
// Execute once to make sure Snowpack is cached.
await this.execute();
}
})*/
];
async function run() {
for(const b of benchmarks) {
await b.test();
}
}
run().catch(err => {
console.error(err);
process.exit(1);
});

View file

@ -0,0 +1,4 @@
{
"name": "Snowpack Example Dev Server Cached",
"time": 1868
}

View file

@ -0,0 +1,4 @@
{
"name": "Snowpack Example Dev Server Uncached",
"time": 9803
}

View file

@ -0,0 +1,61 @@
import { performance } from 'perf_hooks';
import { Benchmark } from './benchmark.js';
import { runDevServer } from '../helpers.js';
import del from 'del';
const snowpackExampleRoot = new URL('../../../../examples/snowpack/', import.meta.url);
async function runToStarted(root) {
const args = [];
const process = runDevServer(root, args);
let started = null;
process.stdout.setEncoding('utf8');
for await (const chunk of process.stdout) {
if (/Server started/.test(chunk)) {
started = performance.now();
break;
}
}
process.kill();
return started;
}
const benchmarks = [
new Benchmark({
name: 'Snowpack Example Dev Server Uncached',
root: snowpackExampleRoot,
file: new URL('./dev-server-uncached.json', import.meta.url),
async setup() {
const spcache = new URL('../../node_modules/.cache/', import.meta.url);
await del(spcache.pathname);
},
run({ root }) {
return runToStarted(root);
}
}),
new Benchmark({
name: 'Snowpack Example Dev Server Cached',
root: snowpackExampleRoot,
file: new URL('./dev-server-cached.json', import.meta.url),
async setup() {
// Execute once to make sure Snowpack is cached.
await this.execute();
},
run({ root }) {
return runToStarted(root);
}
})
];
async function run() {
for(const b of benchmarks) {
await b.test();
}
}
run().catch(err => {
console.error(err);
process.exit(1);
});

View file

@ -48,7 +48,7 @@ export function setupBuild(Suite, fixturePath) {
dest: process.stderr, dest: process.stderr,
}; };
build = (...args) => astroBuild(astroConfig, ...args); build = () => astroBuild(astroConfig, logging);
context.build = build; context.build = build;
context.readFile = async (path) => { context.readFile = async (path) => {
const resolved = fileURLToPath(new URL(`${fixturePath}/${astroConfig.dist}${path}`, import.meta.url)); const resolved = fileURLToPath(new URL(`${fixturePath}/${astroConfig.dist}${path}`, import.meta.url));