Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add n8n-benchmark cli (no-changelog) #10410

Merged
merged 19 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/docker-images-benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Benchmark Docker Image CI

on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'packages/benchmark/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/docker-images-benchmark.yml'

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4.1.1

- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0

- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./packages/benchmark/Dockerfile
platforms: linux/amd64
provenance: false
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest
62 changes: 62 additions & 0 deletions packages/@n8n/benchmark/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# syntax=docker/dockerfile:1
FROM node:20.16.0 AS base

# Install required dependencies
RUN apt-get update && apt-get install -y gnupg2 curl

# Add k6 GPG key and repository
RUN mkdir -p /etc/apt/keyrings && \
curl -sS https://dl.k6.io/key.gpg | gpg --dearmor --yes -o /etc/apt/keyrings/k6.gpg && \
chmod a+x /etc/apt/keyrings/k6.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/k6.gpg] https://dl.k6.io/deb stable main" | tee /etc/apt/sources.list.d/k6.list

# Update and install k6
RUN apt-get update && \
apt-get install -y k6 tini && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

#
# Builder
FROM base AS builder

WORKDIR /app

COPY --chown=node:node ./pnpm-lock.yaml /app/pnpm-lock.yaml
COPY --chown=node:node ./pnpm-workspace.yaml /app/pnpm-workspace.yaml
COPY --chown=node:node ./package.json /app/package.json
COPY --chown=node:node ./packages/@n8n/benchmark/package.json /app/packages/@n8n/benchmark/package.json
COPY --chown=node:node ./patches /app/patches
COPY --chown=node:node ./scripts /app/scripts

RUN pnpm install --frozen-lockfile

# TS config files
COPY --chown=node:node ./tsconfig.json /app/tsconfig.json
COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json
COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.json /app/packages/@n8n/benchmark/tsconfig.json
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json

# Source files
COPY --chown=node:node ./packages/@n8n/benchmark/src /app/packages/@n8n/benchmark/src
COPY --chown=node:node ./packages/@n8n/benchmark/bin /app/packages/@n8n/benchmark/bin
COPY --chown=node:node ./packages/@n8n/benchmark/scenarios /app/packages/@n8n/benchmark/scenarios

WORKDIR /app/packages/@n8n/benchmark
RUN pnpm build

#
# Runner
FROM base AS runner

COPY --from=builder /app /app

WORKDIR /app/packages/@n8n/benchmark
USER node

ENTRYPOINT [ "/app/packages/@n8n/benchmark/bin/n8n-benchmark" ]
55 changes: 55 additions & 0 deletions packages/@n8n/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# n8n benchmarking tool

Tool for executing benchmarks against an n8n instance.

## Running locally with Docker

Build the Docker image:

```sh
# Must be run in the repository root
# k6 doesn't have an arm64 build available for linux, we need to build against amd64
docker build --platform linux/amd64 -t n8n-benchmark -f packages/@n8n/benchmark/Dockerfile .
```

Run the image

```sh
docker run \
-e N8N_USER_EMAIL=user@n8n.io \
-e N8N_USER_PASSWORD=password \
# For macos, n8n running outside docker
-e N8N_BASE_URL=http://host.docker.internal:5678 \
n8n-benchmark
```

## Running locally without Docker

Requirements:

- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/)
- Node.js v20 or higher

```sh
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
```

## 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:

- Manifest file which describes and configures the scenario
- Any test data that is imported before the scenario is run
- 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/).
13 changes: 13 additions & 0 deletions packages/@n8n/benchmark/bin/n8n-benchmark
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node

// Check if version should be displayed
const versionFlags = ['-v', '-V', '--version'];
if (versionFlags.includes(process.argv.slice(-1)[0])) {
console.log(require('../package').version);
process.exit(0);
}

(async () => {
const oclif = require('@oclif/core');
await oclif.execute({ dir: __dirname });
})();
48 changes: 48 additions & 0 deletions packages/@n8n/benchmark/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@n8n/n8n-benchmark",
"version": "1.0.0",
"description": "Cli for running benchmark tests for n8n",
"main": "dist/index",
"scripts": {
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
"start": "./bin/n8n-benchmark",
"test": "echo \"Error: no test specified\" && exit 1",
"typecheck": "tsc --noEmit",
"watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\""
},
"engines": {
"node": ">=20.10"
},
"keywords": [
"automate",
"automation",
"IaaS",
"iPaaS",
"n8n",
"workflow",
"benchmark",
"performance"
],
"dependencies": {
"@oclif/core": "4.0.7",
"axios": "catalog:",
"convict": "6.2.4",
"dotenv": "8.6.0",
"zx": "^8.1.4"
},
"devDependencies": {
"@types/convict": "^6.1.1",
"@types/k6": "^0.52.0",
"@types/node": "^20.14.8",
"tsc-alias": "^1.8.7",
"typescript": "^5.5.2"
},
"bin": {
"n8n-benchmark": "./bin/n8n-benchmark"
},
"oclif": {
"bin": "n8n-benchmark",
"commands": "./dist/commands",
"topicSeparator": " "
}
}
42 changes: 42 additions & 0 deletions packages/@n8n/benchmark/scenarios/scenario.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"definitions": {
"ScenarioData": {
"type": "object",
"properties": {
"workflowFiles": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [],
"additionalProperties": false
}
},
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "The JSON schema to validate this file"
},
"name": {
"type": "string",
"description": "The name of the scenario"
},
"description": {
"type": "string",
"description": "A longer description of the scenario"
},
"scriptPath": {
"type": "string",
"description": "Relative path to the k6 test script"
},
"scenarioData": {
"$ref": "#/definitions/ScenarioData",
"description": "Data to import before running the scenario"
}
},
"required": ["name", "description", "scriptPath", "scenarioData"],
"additionalProperties": false
}
25 changes: 25 additions & 0 deletions packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"createdAt": "2024-08-06T12:19:51.268Z",
"updatedAt": "2024-08-06T12:20:45.000Z",
"name": "Single Webhook",
"active": true,
"nodes": [
{
"parameters": { "path": "single-webhook", "options": {} },
"id": "7587ab0e-cc15-424f-83c0-c887a0eb97fb",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [760, 400],
"webhookId": "fa563fc2-c73f-4631-99a1-39c16f1f858f"
}
],
"connections": {},
"settings": { "executionOrder": "v1" },
"staticData": null,
"meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} },
"pinData": {},
"versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b",
"triggerCount": 1,
"tags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../scenario.schema.json",
"name": "SingleWebhook",
"description": "A single webhook trigger that responds with a 200 status code",
"scenarioData": { "workflowFiles": ["singleWebhook.json"] },
"scriptPath": "singleWebhook.script.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import http from 'k6/http';
import { check } from 'k6';

const apiBaseUrl = __ENV.API_BASE_URL;

export default function () {
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
check(res, {
'is status 200': (r) => r.status === 200,
});
}
21 changes: 21 additions & 0 deletions packages/@n8n/benchmark/src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Command } from '@oclif/core';
import { ScenarioLoader } from '@/scenario/scenarioLoader';
import { loadConfig } from '@/config/config';

export default class ListCommand extends Command {
static description = 'List all available scenarios';

async run() {
const config = loadConfig();
const scenarioLoader = new ScenarioLoader();

const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath'));

console.log('Available test scenarios:');
console.log('');

for (const scenario of allScenarios) {
console.log('\t', scenario.name, ':', scenario.description);
}
}
}
39 changes: 39 additions & 0 deletions packages/@n8n/benchmark/src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Command, Flags } from '@oclif/core';
import { loadConfig } from '@/config/config';
import { ScenarioLoader } from '@/scenario/scenarioLoader';
import { ScenarioRunner } from '@/testExecution/scenarioRunner';
import { N8nApiClient } from '@/n8nApiClient/n8nApiClient';
import { ScenarioDataFileLoader } from '@/scenario/scenarioDataLoader';
import { K6Executor } from '@/testExecution/k6Executor';

export default class RunCommand extends Command {
static description = 'Run all (default) or specified test scenarios';

// TODO: Add support for filtering scenarios
static flags = {
scenarios: Flags.string({
char: 't',
description: 'Comma-separated list of test scenarios to run',
required: false,
}),
};

async run() {
const config = loadConfig();
const scenarioLoader = new ScenarioLoader();

const scenarioRunner = new ScenarioRunner(
new N8nApiClient(config.get('n8n.baseUrl')),
new ScenarioDataFileLoader(),
new K6Executor(config.get('k6ExecutablePath'), config.get('n8n.baseUrl')),
{
email: config.get('n8n.user.email'),
password: config.get('n8n.user.password'),
},
);

const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath'));

await scenarioRunner.runManyScenarios(allScenarios);
}
}
Loading
Loading