Skip to content

Commit

Permalink
feat: Add local orchestration of benchmarks (no-changelog) (#10589)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomi authored Aug 30, 2024
1 parent 47eb28d commit 1c5164c
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 214 deletions.
55 changes: 37 additions & 18 deletions packages/@n8n/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,38 @@
# n8n benchmarking tool

Tool for executing benchmarks against an n8n instance.
Tool for executing benchmarks against an n8n instance. The tool consists of these components:

## Running locally with Docker
## Directory structure

```text
packages/@n8n/benchmark
├── scenarios Benchmark scenarios
├── src Source code for the n8n-benchmark cli
├── Dockerfile Dockerfile for the n8n-benchmark cli
├── scripts Orchestration scripts
```

## Running the entire benchmark suite

The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and different [n8n setups](#n8n-setups).

### locally

```sh
pnpm run-locally
```

### In the cloud

```sh
pnpm run-in-cloud
```

## Running the `n8n-benchmark` cli

The `n8n-benchmark` cli is a node.js program that runs one or more scenarios against a single n8n instance.

### Locally with Docker

Build the Docker image:

Expand All @@ -23,7 +53,7 @@ docker run \
n8n-benchmark
```

## Running locally without Docker
### Locally without Docker

Requirements:

Expand All @@ -35,23 +65,8 @@ pnpm build

# Run tests against http://localhost:5678 with specified email and password
N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run

# If you installed k6 using brew, you might have to specify it explicitly
K6_PATH=/opt/homebrew/bin/k6 N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run
```

## Running in the cloud

There's a script to run the performance tests in a cloud environment. The script provisions a cloud environment, sets up n8n in the environment, runs the tests and destroys the environment.

```sh
pnpm run-in-cloud
```

## Configuration

The configuration options the cli accepts can be seen from [config.ts](./src/config/config.ts)

## Benchmark scenarios

A benchmark scenario defines one or multiple steps to execute and measure. It consists of:
Expand All @@ -61,3 +76,7 @@ A benchmark scenario defines one or multiple steps to execute and measure. It co
- A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) script which executes the steps and receives `API_BASE_URL` environment variable in runtime.

Available scenarios are located in [`./scenarios`](./scenarios/).

## n8n setups

A n8n setup defines a single n8n runtime configuration using Docker compose. Different n8n setups are located in [`./scripts/n8nSetups`](./scripts/n8nSetups).
4 changes: 3 additions & 1 deletion packages/@n8n/benchmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"start": "./bin/n8n-benchmark",
"test": "echo \"Error: no test specified\" && exit 1",
"typecheck": "tsc --noEmit",
"run-in-cloud": "zx scripts/runInCloud.mjs",
"benchmark": "zx scripts/run.mjs",
"benchmark-in-cloud": "pnpm benchmark --env cloud",
"benchmark-locally": "pnpm benchmark --env local",
"destroy-cloud-env": "zx scripts/destroyCloudEnv.mjs",
"watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\""
},
Expand Down
45 changes: 45 additions & 0 deletions packages/@n8n/benchmark/scripts/clients/dockerComposeClient.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { which } from 'zx';

export class DockerComposeClient {
/**
*
* @param {{ $: Shell; verbose?: boolean }} opts
*/
constructor({ $ }) {
this.$$ = $;
}

async $(...args) {
await this.resolveExecutableIfNeeded();

if (this.isCompose) {
return await this.$$`docker-compose ${args}`;
} else {
return await this.$$`docker compose ${args}`;
}
}

async resolveExecutableIfNeeded() {
if (this.isResolved) {
return;
}

// The VM deployment doesn't have `docker compose` available,
// so try to resolve the `docker-compose` first
const compose = await which('docker-compose', { nothrow: true });
if (compose) {
this.isResolved = true;
this.isCompose = true;
return;
}

const docker = await which('docker', { nothrow: true });
if (docker) {
this.isResolved = true;
this.isCompose = false;
return;
}

throw new Error('Could not resolve docker-compose or docker');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ const paths = {
};

export class TerraformClient {
constructor({ privateKeyPath, isVerbose = false }) {
this.privateKeyPath = privateKeyPath;
constructor({ isVerbose = false }) {
this.isVerbose = isVerbose;
this.$$ = $({
cwd: paths.infraCodeDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
ports:
- 5678:5678
volumes:
- /n8n:/n8n
- ${RUN_DIR}:/n8n
benchmark:
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
depends_on:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
ports:
- 5678:5678
volumes:
- /n8n:/n8n
- ${RUN_DIR}:/n8n
benchmark:
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
depends_on:
Expand Down
151 changes: 151 additions & 0 deletions packages/@n8n/benchmark/scripts/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env zx
/**
* Script to run benchmarks either on the cloud benchmark environment or locally.
*
* NOTE: Must be run in the root of the package.
*
* Usage:
* zx scripts/run.mjs
*
*/
// @ts-check
import fs from 'fs';
import minimist from 'minimist';
import path from 'path';
import { runInCloud } from './runInCloud.mjs';
import { runLocally } from './runLocally.mjs';

const paths = {
n8nSetupsDir: path.join(path.resolve('scripts'), 'n8nSetups'),
};

async function main() {
const config = await parseAndValidateConfig();

const n8nSetupsToUse =
config.n8nSetupToUse === 'all' ? readAvailableN8nSetups() : [config.n8nSetupToUse];

console.log('Using n8n tag', config.n8nTag);
console.log('Using benchmark cli tag', config.benchmarkTag);
console.log('Using environment', config.env);
console.log('Using n8n setups', n8nSetupsToUse.join(', '));
console.log('');

if (config.env === 'cloud') {
await runInCloud({
benchmarkTag: config.benchmarkTag,
isVerbose: config.isVerbose,
k6ApiToken: config.k6ApiToken,
n8nTag: config.n8nTag,
n8nSetupsToUse,
});
} else {
await runLocally({
benchmarkTag: config.benchmarkTag,
isVerbose: config.isVerbose,
k6ApiToken: config.k6ApiToken,
n8nTag: config.n8nTag,
runDir: config.runDir,
n8nSetupsToUse,
});
}
}

function readAvailableN8nSetups() {
const setups = fs.readdirSync(paths.n8nSetupsDir);

return setups;
}

/**
* @typedef {Object} Config
* @property {boolean} isVerbose
* @property {'cloud' | 'local'} env
* @property {string} n8nSetupToUse
* @property {string} n8nTag
* @property {string} benchmarkTag
* @property {string} [k6ApiToken]
* @property {string} [runDir]
*
* @returns {Promise<Config>}
*/
async function parseAndValidateConfig() {
const args = minimist(process.argv.slice(3), {
boolean: ['debug', 'help'],
});

if (args.help) {
printUsage();
process.exit(0);
}

const n8nSetupToUse = await getAndValidateN8nSetup(args);
const isVerbose = args.debug || false;
const n8nTag = args.n8nTag || process.env.N8N_DOCKER_TAG || 'latest';
const benchmarkTag = args.benchmarkTag || process.env.BENCHMARK_DOCKER_TAG || 'latest';
const k6ApiToken = args.k6ApiToken || process.env.K6_API_TOKEN || undefined;
const runDir = args.runDir || undefined;
const env = args.env || 'local';

if (!env) {
printUsage();
process.exit(1);
}

return {
isVerbose,
env,
n8nSetupToUse,
n8nTag,
benchmarkTag,
k6ApiToken,
runDir,
};
}

/**
* @param {minimist.ParsedArgs} args
*/
async function getAndValidateN8nSetup(args) {
// Last parameter is the n8n setup to use
const n8nSetupToUse = args._[args._.length - 1];
if (!n8nSetupToUse || n8nSetupToUse === 'all') {
return 'all';
}

const availableSetups = readAvailableN8nSetups();

if (!availableSetups.includes(n8nSetupToUse)) {
printUsage();
process.exit(1);
}

return n8nSetupToUse;
}

function printUsage() {
const availableSetups = readAvailableN8nSetups();

console.log(`Usage: zx scripts/${path.basename(__filename)} [n8n setup name]`);
console.log(` eg: zx scripts/${path.basename(__filename)}`);
console.log('');
console.log('Options:');
console.log(
` [n8n setup name] Against which n8n setup to run the benchmarks. One of: ${['all', ...availableSetups].join(', ')}. Default is all`,
);
console.log(
' --env Env where to run the benchmarks. Either cloud or local. Default is local.',
);
console.log(' --debug Enable verbose output');
console.log(' --n8nTag Docker tag for n8n image. Default is latest');
console.log(' --benchmarkTag Docker tag for benchmark cli image. Default is latest');
console.log(
' --k6ApiToken API token for k6 cloud. Default is read from K6_API_TOKEN env var. If omitted, k6 cloud will not be used',
);
console.log(
' --runDir Directory to share with the n8n container for storing data. Needed only for local runs.',
);
console.log('');
}

main().catch(console.error);
Loading

0 comments on commit 1c5164c

Please sign in to comment.