-
Notifications
You must be signed in to change notification settings - Fork 10.8k
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
perf(core): Introduce backend performance benchmarking #9199
Closed
Closed
Changes from all commits
Commits
Show all changes
91 commits
Select commit
Hold shift + click to select a range
772dd09
Initial setup
ivov 762754b
Add CI action
ivov c6d07e8
Refactor into `packages/cli/src/benchmark`
ivov 1a6045a
Add start cmd
ivov b0832cb
Fix script ref
ivov a1e19fb
Init and run start cmd
ivov c974ec2
Update lockfile
ivov f8127b8
Limit CI action to BE PRs
ivov 357b539
Merge branch 'master' into codspeed-tinybench
ivov 168ccb0
Cleanup
ivov 24042a1
Move benchmarks inside `cli/test`
ivov cd99c6e
Add `ts-ignore` to allow `test` to compile
ivov 7e35897
Cleanup
ivov 4414eee
Set up test n8n dir
ivov 98df748
Reload constants
ivov 155ff50
Suite class
ivov 1d1a6ec
Comment out constants reload
ivov ff2d09c
Create BenchmarkSetup
ivov f4e76d6
Remove unused `init.ts`
ivov 2382e65
Restructuring
ivov 43c02be
Merge branch 'master' into codspeed-tinybench
ivov 648d5b8
refactor(core): Fix type errors in BE tests (no-changelog)
ivov 3f38f88
Simplify
ivov 9e7911a
Back to cli/src
ivov 0c39b4f
Remove logging noise
ivov a193bd3
Make naming consistent
ivov 42b67ec
Implement `beforeEach``
ivov 6dcb2e5
Move to suites.ts
ivov 8775968
Cleanup
ivov a6bd9ea
More cleanup
ivov 8282963
Organize
ivov d24f412
Prevent duplicate `beforeEach``
ivov 8a0f64b
Implement `afterEach`
ivov fe4ce3c
Cleanup
ivov e2bc37b
Merge branch 'master' into codspeed-tinybench
ivov 86562de
Reduce diff
ivov 116b92d
Fixtures setup
ivov 7bb25ca
Pick up fixtures
ivov c42016d
First benchmark
ivov 093e75c
Phrasing
ivov 3c36304
Cleanup
ivov 169c627
Typo
ivov ae6ff9b
Add comment
ivov d776bbe
Add question
ivov 403b466
Add second benchmark
ivov b8c6b62
Naming
ivov 497966f
Add TODO
ivov 20069b5
Add clarification
ivov facdac5
Third benchmark
ivov 4157807
Simplify paths
ivov 82b3f78
Introduce `describe()`
ivov 421f23b
Prevent duplicate suites
ivov b12d254
`describe` -> `suite`
ivov 57724d5
Script to document suites
ivov d6d7efb
Cleanup
ivov d0be040
Cleanup
ivov 8694d62
Account for no workflow referred to
ivov 2bb4f63
Docs
ivov dcbd2b7
Disambiguate
ivov fa9f59f
Clarification
ivov 156a054
Stop benchmarks on error
ivov 133a2b1
Try 127.0.0.1
ivov ff53c57
`client` -> `agent`
ivov ea2cc83
Deduplicate repository logic
ivov 26c8e81
Simplify setup and teardown
ivov 54f5463
`registration` -> `api`
ivov 7a4be56
Make bench values configurable
ivov 02f4809
Remove commented out start code
ivov 280caae
Remove outdated comment
ivov c8bf11b
Typo
ivov b61f8c1
Better solution for `InstanceSettings` user home dir
ivov def86d2
Comment out unneeded steps
ivov d94d312
Remove outdated comment
ivov 732b358
Restore steps
ivov 3bef94c
Add TODO
ivov dfbab14
Cleanup
ivov f8cbdd8
More cleanup
ivov d607d70
Add display
ivov 471470a
Simplify logging
ivov 526bb83
Document
ivov e443f6e
Merge branch 'master' into codspeed-tinybench
ivov 57bcc87
perf(core): Support Postgres in benchmarks (#9246)
ivov 4ef5a2a
Improve docs
ivov 1a91102
Handle connection error gracefully
ivov 00ff0b3
Remove outdated comment
ivov eaa8790
Remove another TODO
ivov c1d69e8
Reduce diff
ivov f60caf4
Cleanup
ivov b40652d
Patch @codspeed/[email protected]
ivov 05ed548
Cleanup
ivov bf14a70
Merge branch 'master' into codspeed-tinybench
ivov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
name: Benchmark | ||
|
||
on: | ||
pull_request: | ||
paths: | ||
- 'packages/cli/**' | ||
- 'packages/core/**' | ||
- 'packages/workflow/**' | ||
workflow_dispatch: | ||
|
||
jobs: | ||
benchmark: | ||
name: Benchmark | ||
runs-on: ubuntu-latest | ||
timeout-minutes: 20 | ||
env: | ||
DB_POSTGRESDB_PASSWORD: password | ||
steps: | ||
- uses: actions/[email protected] | ||
|
||
- name: Start Postgres | ||
uses: isbang/[email protected] | ||
with: | ||
compose-file: ./.github/docker-compose.yml | ||
services: postgres | ||
|
||
- run: corepack enable | ||
|
||
- uses: actions/[email protected] | ||
with: | ||
node-version: 18.x | ||
cache: pnpm | ||
|
||
- run: pnpm install --frozen-lockfile | ||
|
||
- name: Build | ||
if: ${{ inputs.cacheKey == '' }} | ||
run: pnpm build:backend | ||
|
||
- name: Restore cached build artifacts | ||
if: ${{ inputs.cacheKey != '' }} | ||
uses: actions/cache/[email protected] | ||
with: | ||
path: ./packages/**/dist | ||
key: ${{ inputs.cacheKey }} | ||
|
||
- run: pnpm build:benchmark | ||
working-directory: packages/cli | ||
|
||
- name: Benchmark | ||
uses: CodSpeedHQ/action@v2 | ||
with: | ||
working-directory: packages/cli | ||
run: | | ||
pnpm benchmark:sqlite | ||
pnpm benchmark:postgres |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
"packageManager": "[email protected]", | ||
"scripts": { | ||
"preinstall": "node scripts/block-npm-install.js", | ||
"benchmark": "pnpm --filter=n8n benchmark", | ||
"build": "turbo run build", | ||
"build:backend": "pnpm --filter=!@n8n/chat --filter=!n8n-design-system --filter=!n8n-editor-ui build", | ||
"build:frontend": "pnpm --filter=@n8n/chat --filter=n8n-design-system --filter=n8n-editor-ui build", | ||
|
@@ -95,7 +96,8 @@ | |
"[email protected]": "patches/[email protected]", | ||
"@types/[email protected]": "patches/@[email protected]", | ||
"@types/[email protected]": "patches/@[email protected]", | ||
"[email protected]": "patches/[email protected]" | ||
"[email protected]": "patches/[email protected]", | ||
"@codspeed/[email protected]": "patches/@[email protected]" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Benchmark | ||
|
||
This package contains benchmarks to measure the execution time of n8n backend operations in sqlite and Postgres. | ||
|
||
Benchmarks are organized into **suites** for the scenario to benchmark, **tasks** for operations in that scenario, and **hooks** for setup and teardown, implemented on top of [`tinybench`](https://github.com/tinylibs/tinybench). Execution in CI is delegated to [Codspeed](https://codspeed.io/) to keep measurements consistent and to monitor improvements and regressions. | ||
|
||
## Running benchmarks | ||
|
||
To run benchmarks: | ||
|
||
```sh | ||
pnpm build:benchmark | ||
pnpm benchmark:sqlite # or | ||
pnpm benchmark:postgres | ||
``` | ||
|
||
Locally, the benchmarking run can be configured via [environment variables](https://docs.n8n.io/hosting/configuration/environment-variables/benchmarking). In CI, the configuration is set by Codspeed. | ||
|
||
## Creating benchmarks | ||
|
||
To create benchmarks: | ||
|
||
1. Create a file at `suites/**/{suiteId}-{suiteTitle}.ts`. | ||
2. Include a `suite()` call for the scenario to benchmark. | ||
3. Inside the suite, include one or more `task()` calls for operations in that scenario. `task()` must contain only the specific operation whose execution time to measure. Move any per-task setup and teardown to `beforeEachTask()` and `afterEachTask()` in the suite. | ||
4. Include workflows at `suites/workflows/{suiteId}-{ordinalNumber}`. During setup, workflows at this dir are saved in the temp DB and activated in memory. | ||
5. Run `pnpm build:benchmark` to add the suite and its tasks to the index below. | ||
|
||
## Index of benchmarking suites | ||
|
||
> **Note**: All workflows with default settings unless otherwise specified, e.g. `EXECUTIONS_MODE` is `regular` unless `queue` is specified. | ||
|
||
<!-- BENCHMARK_SUITES_LIST --> | ||
|
||
### 001 - Production workflow with authless webhook node | ||
|
||
- [using "Respond immediately" mode](./suites/workflows/001-1.json) | ||
- [using "When last node finishes" mode](./suites/workflows/001-2.json) | ||
- [using "Respond to Webhook node" mode](./suites/workflows/001-3.json) | ||
|
||
<!-- /BENCHMARK_SUITES_LIST --> | ||
|
||
## Reading benchmarks | ||
|
||
In a benchmarking run, a task is repeatedly executed for a duration and for a number of iterations - the run will continue until the number of iterations is reached, even if this exceeds the duration. | ||
|
||
``` | ||
BENCHMARK suites/001-production-webhook-with-authless-webhook-node.suite.ts [sqlite] | ||
|
||
• using "Respond immediately" mode | ||
· Ran 27 iterations in 509.992 ms at a rate of 52.941 op/s | ||
· p75 20.251 ms ··· p99 64.570 ms ··· p999 64.570 ms | ||
· min 8.363 ms ···· max 64.570 ms ··· mean 18.888 ms | ||
· MoE ±4.1% ··· std err 02.037 ms ··· std dev 10.586 ms | ||
``` | ||
|
||
`p{n}` is the percentile, i.e. the percentage of data points in a distribution that are less than or equal to a value. For example, `p75` being 20.251 ms means that 75% of the 27 iterations for the task `using "Respond immediately" mode` took 20.251 ms or less. `p75` is the execution time that the majority of users experience, `p99` captures worst-case scenarios for all but 1% of users, and `p999` includes performance at extreme cases for the slowest 0.1% of users. | ||
|
||
`min` is the shortest execution time recorded across all iterations of the task, `max` is the longest, and `mean` is the average. | ||
|
||
`MoE` (margin of error) reflects how much the sample mean is expected to differ from the true population mean. For example, a margin of error of ±4.1% in the task `using "Respond immediately" mode` suggests that, if the benchmarking run were repeated multiple times, the sample mean would fall within 4.1% of the true population mean in 95% of those runs, assuming a standard confidence level. This range indicates the variability we might see due to the randomness of selecting a sample. | ||
|
||
`std err` (standard error) reflects how closely a sample mean is expected to approximate the true population mean. A smaller standard error indicates that the sample mean is likely to be a more accurate estimate of the population mean because the variation among sample means is less. For example, in the task `using "Respond immediately" mode`, the standard error is 2.037 ms, which suggests that the sample mean is expected to differ from the true population mean by 2.037 ms on average. | ||
|
||
`std dev` (standard deviation) is the amount of dispersion across samples. When low, it indicates that the samples tend to be close to the mean; when high, it indicates that the samples are spread out over a wider range. For example, in the task `using "Respond immediately" mode`, the standard deviation is 10.586 ms, which suggests that the execution times varied significantly across iterations. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import axios from 'axios'; | ||
import { BACKEND_BASE_URL, INSTANCE_ONWER } from './constants'; | ||
import { ApplicationError } from 'n8n-workflow'; | ||
|
||
export const agent = axios.create({ baseURL: BACKEND_BASE_URL }); | ||
|
||
export async function authenticateAgent() { | ||
const response = await agent.post('/rest/login', { | ||
email: INSTANCE_ONWER.EMAIL, | ||
password: INSTANCE_ONWER.PASSWORD, | ||
}); | ||
|
||
const cookies = response.headers['set-cookie']; | ||
|
||
if (!cookies || cookies.length !== 1) { | ||
throw new ApplicationError('Expected cookie', { level: 'warning' }); | ||
} | ||
|
||
const [cookie] = cookies; | ||
|
||
agent.defaults.headers.Cookie = cookie; | ||
agent.defaults.headers['x-n8n-api-key'] = INSTANCE_ONWER.API_KEY; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import 'reflect-metadata'; | ||
import path from 'node:path'; | ||
import type Bench from 'tinybench'; | ||
import { assert } from 'n8n-workflow'; | ||
import glob from 'fast-glob'; | ||
import callsites from 'callsites'; | ||
import type { Suites, Task, Callback } from './types'; | ||
import { DuplicateHookError } from './errors/duplicate-hook.error'; | ||
import { DuplicateSuiteError } from './errors/duplicate-suite.error'; | ||
|
||
const suites: Suites = {}; | ||
|
||
export async function collectSuites() { | ||
const files = await glob('**/*.suite.js', { | ||
cwd: path.join('dist', 'benchmark'), | ||
absolute: true, | ||
}); | ||
|
||
for (const f of files) { | ||
await import(f); | ||
} | ||
|
||
return suites; | ||
} | ||
|
||
export function registerSuites(bench: Bench) { | ||
for (const { name: suiteName, hooks, tasks } of Object.values(suites)) { | ||
/** | ||
* In tinybench, `beforeAll` and `afterAll` refer to all _iterations_ of | ||
* a single task, while `beforeEach` and `afterEach` refer to each _iteration_. | ||
* | ||
* In jest and vitest, `beforeAll` and `afterAll` refer to all _tests_, | ||
* while `beforeEach` and `afterEach` refer to each _test_. | ||
* | ||
* This API renames tinybench's hooks to prevent confusion from familiarity with jest. | ||
*/ | ||
const options: Record<string, Callback> = {}; | ||
|
||
if (hooks.beforeEachTask) options.beforeAll = hooks.beforeEachTask; | ||
if (hooks.afterEachTask) options.afterAll = hooks.afterEachTask; | ||
|
||
for (const t of tasks) { | ||
const taskName = process.env.CI === 'true' ? [suiteName, t.name].join('::') : t.name; | ||
|
||
bench.add(taskName, t.operation, options); | ||
} | ||
} | ||
} | ||
|
||
function suiteKey() { | ||
const key = callsites() | ||
.map((site) => site.getFileName()) | ||
.filter((site): site is string => site !== null) | ||
.find((site) => site.endsWith('.suite.js')); | ||
|
||
assert(key !== undefined); | ||
|
||
return key.replace(/^.*benchmark\//, '').replace(/\.js$/, '.ts'); | ||
} | ||
|
||
export function suite(suiteName: string, suiteFn: () => void) { | ||
const key = suiteKey(); | ||
|
||
if (suites[key]) throw new DuplicateSuiteError(key); | ||
|
||
suites[key] = { name: suiteName, hooks: {}, tasks: [] }; | ||
|
||
suiteFn(); | ||
} | ||
|
||
export function task(taskName: string, operation: Task['operation']) { | ||
const key = suiteKey(); | ||
|
||
suites[key].tasks.push({ | ||
name: taskName, | ||
operation, | ||
}); | ||
} | ||
|
||
export function beforeEachTask(fn: Callback) { | ||
const key = suiteKey(); | ||
|
||
if (suites[key]?.hooks.beforeEachTask) { | ||
throw new DuplicateHookError('beforeEachTask', key); | ||
} | ||
|
||
suites[key].hooks.beforeEachTask = fn; | ||
} | ||
|
||
export function afterEachTask(fn: Callback) { | ||
const key = suiteKey(); | ||
|
||
if (suites[key]?.hooks.afterEachTask) { | ||
throw new DuplicateHookError('afterEachTask', key); | ||
} | ||
|
||
suites[key].hooks.afterEachTask = fn; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const BACKEND_BASE_URL = 'http://127.0.0.1:5678'; // localhost on GitHub Actions runners refuses connections | ||
|
||
export const INSTANCE_ONWER = { | ||
EMAIL: '[email protected]', | ||
PASSWORD: 'password', | ||
FIRST_NAME: 'Instance', | ||
LAST_NAME: 'Owner', | ||
API_KEY: 'n8n_api_123', | ||
}; |
10 changes: 10 additions & 0 deletions
10
packages/cli/src/benchmark/lib/errors/duplicate-hook.error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { ApplicationError } from 'n8n-workflow'; | ||
|
||
export class DuplicateHookError extends ApplicationError { | ||
constructor(hookName: 'beforeEachTask' | 'afterEachTask', key: string) { | ||
super( | ||
`Duplicate \`${hookName}\` hook found at \`${key}\`. Please define a single \`${hookName}\` hook for this file.`, | ||
{ level: 'warning' }, | ||
); | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
packages/cli/src/benchmark/lib/errors/duplicate-suite.error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { ApplicationError } from 'n8n-workflow'; | ||
|
||
export class DuplicateSuiteError extends ApplicationError { | ||
constructor(key: string) { | ||
super(`Duplicate suite found at \`${key}\`. Please define a single suite for this file.`, { | ||
level: 'warning', | ||
}); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
packages/cli/src/benchmark/lib/errors/postgres-connection.error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { ApplicationError } from 'n8n-workflow'; | ||
import type { DataSourceOptions } from '@n8n/typeorm'; | ||
|
||
export class PostgresConnectionError extends ApplicationError { | ||
constructor(error: unknown, pgOptions: DataSourceOptions) { | ||
super('Failed to connect to Postgres - check your Postgres configuration', { | ||
level: 'warning', | ||
cause: error, | ||
extra: { postgresConfig: { pgOptions } }, | ||
}); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging changes like these ensure
N8N_LOG_LEVEL=silent
mutes this logging noise during benchmarking runs.