first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,118 @@
# Performance Testing Helper
A simple toolkit for measuring and asserting performance in Playwright tests.
## Quick Start
### "I just want to measure how long something takes"
```typescript
const duration = await measurePerformance(page, 'open-node', async () => {
await n8n.canvas.openNode('Code');
});
console.log(`Opening node took ${duration.toFixed(1)}ms`);
```
### "I want to ensure an action completes within a time limit"
```typescript
const openNodeDuration = await measurePerformance(page, 'open-node', async () => {
await n8n.canvas.openNode('Code');
});
expect(openNodeDuration).toBeLessThan(2000); // Must complete in under 2 seconds
```
### "I want to measure the same action multiple times"
```typescript
const stats = [];
for (let i = 0; i < 20; i++) {
const duration = await measurePerformance(page, `open-node-${i}`, async () => {
await n8n.canvas.openNode('Code');
});
await n8n.ndv.clickBackToCanvasButton();
stats.push(duration);
}
const average = stats.reduce((a, b) => a + b, 0) / stats.length;
console.log(`Average: ${average.toFixed(1)}ms`);
expect(average).toBeLessThan(2000);
```
### "I want to set performance budgets for different actions"
```typescript
const budgets = {
triggerWorkflow: 8000, // 8 seconds
openLargeNode: 2500, // 2.5 seconds
};
// Measure workflow execution
const triggerDuration = await measurePerformance(page, 'trigger-workflow', async () => {
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
});
expect(triggerDuration).toBeLessThan(budgets.triggerWorkflow);
// Measure node opening
const openDuration = await measurePerformance(page, 'open-large-node', async () => {
await n8n.canvas.openNode('Code');
});
expect(openDuration).toBeLessThan(budgets.openLargeNode);
```
### "I want to test performance with different data sizes"
```typescript
const testData = [
{ size: 30000, budgets: { triggerWorkflow: 8000, openLargeNode: 2500 } },
{ size: 60000, budgets: { triggerWorkflow: 15000, openLargeNode: 6000 } },
];
testData.forEach(({ size, budgets }) => {
test(`performance - ${size.toLocaleString()} items`, async ({ page }) => {
// Setup test with specific data size
await setupTest(size);
// Measure against size-specific budgets
const duration = await measurePerformance(page, 'trigger-workflow', async () => {
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful')
});
expect(duration).toBeLessThan(budgets.triggerWorkflow);
});
});
```
### "I want to see all performance metrics from my test"
```typescript
// After running various performance measurements...
const allMetrics = await getAllPerformanceMetrics(page);
console.log('All performance metrics:', allMetrics);
// Output: { 'open-node': 1234.5, 'save-workflow': 567.8, ... }
```
### "I want to attach performance results to my test report"
```typescript
const allMetrics = await getAllPerformanceMetrics(page);
await test.info().attach('performance-metrics', {
body: JSON.stringify({
dataSize: 30000,
metrics: allMetrics,
budgets: { triggerWorkflow: 8000, openLargeNode: 2500 },
passed: {
triggerWorkflow: allMetrics['trigger-workflow'] < 8000,
openNode: allMetrics['open-large-node'] < 2500,
}
}, null, 2),
contentType: 'application/json',
});
```
## API Reference
### `measurePerformance(page, actionName, actionFn)`
Measures the duration of an async action using the Performance API.
- **Returns:** `Promise<number>` - Duration in milliseconds
### `getAllPerformanceMetrics(page)`
Retrieves all performance measurements from the current page.
- **Returns:** `Promise<Record<string, number>>` - Map of action names to durations
## Tips
- Use unique names for measurements in loops (e.g., `open-node-${i}`) to avoid conflicts
- Set realistic budgets - add some buffer to account for variance
- Consider different budgets for different data sizes or environments
@@ -0,0 +1,62 @@
import { test, expect } from '../../fixtures/base';
import type { n8nPage } from '../../pages/n8nPage';
import { measurePerformance, attachMetric } from '../../utils/performance-helper';
async function setupPerformanceTest(n8n: n8nPage, size: number) {
await n8n.start.fromImportedWorkflow('large.json');
await n8n.notifications.closeNotificationByText('Successful');
await n8n.canvas.openNode('Edit Fields');
await n8n.ndv.fillParameterInputByName('value', size.toString());
await n8n.ndv.clickBackToCanvasButton();
}
test.use({
capability: {
resourceQuota: {
memory: 0.75,
cpu: 0.5,
},
},
});
test.describe('Large Data Size Performance - Cloud Resources', {
annotation: [
{ type: 'owner', description: 'Catalysts' },
],
}, () => {
test('Code Node with 30000 items', async ({ n8n }, testInfo) => {
const itemCount = 30000;
await setupPerformanceTest(n8n, itemCount);
const workflowExecuteTimeout = 65_000;
const loopSize = 30;
const stats = [];
const triggerDuration = await measurePerformance(n8n.page, 'trigger-workflow', async () => {
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
'Workflow executed successfully',
{
timeout: workflowExecuteTimeout,
},
);
});
for (let i = 0; i < loopSize; i++) {
const openNodeDuration = await measurePerformance(n8n.page, `open-node-${i}`, async () => {
await n8n.canvas.openNode('Code');
});
stats.push(openNodeDuration);
await n8n.ndv.clickBackToCanvasButton();
}
const average = stats.reduce((a, b) => a + b, 0) / stats.length;
await attachMetric(testInfo, `open-node-${itemCount}`, average, 'ms');
await attachMetric(testInfo, `trigger-workflow-${itemCount}`, triggerDuration, 'ms');
expect(average).toBeGreaterThan(0);
expect(triggerDuration).toBeGreaterThan(0);
console.log(
`[PERF] Open node avg: ${average.toFixed(2)} ms | Workflow trigger: ${triggerDuration.toFixed(2)} ms`,
);
});
});
@@ -0,0 +1,41 @@
import { test, expect } from '../../fixtures/base';
import { attachMetric, getStableHeap } from '../../utils/performance-helper';
test.use({
capability: {
resourceQuota: { memory: 0.75, cpu: 0.5 },
services: ['victoriaLogs', 'victoriaMetrics', 'vector'],
},
});
test.describe(
'Memory Consumption @capability:observability',
{
annotation: [{ type: 'owner', description: 'Catalysts' }],
},
() => {
test('Memory consumption baseline with starter plan resources', async ({
n8nContainer,
services,
}, testInfo) => {
const obs = services.observability;
const result = await getStableHeap(n8nContainer.baseUrl, obs.metrics);
await attachMetric(testInfo, 'memory-heap-used-baseline', result.heapUsedMB, 'MB');
await attachMetric(testInfo, 'memory-heap-total-baseline', result.heapTotalMB, 'MB');
await attachMetric(testInfo, 'memory-rss-baseline', result.rssMB, 'MB');
await attachMetric(testInfo, 'memory-pss-baseline', result.pssMB ?? 0, 'MB');
await attachMetric(
testInfo,
'memory-non-heap-overhead-baseline',
result.nonHeapOverheadMB,
'MB',
);
expect(result.heapUsedMB).toBeGreaterThan(0);
expect(result.heapTotalMB).toBeGreaterThan(0);
expect(result.rssMB).toBeGreaterThan(0);
});
},
);
@@ -0,0 +1,51 @@
import { test, expect } from '../../fixtures/base';
import type { n8nPage } from '../../pages/n8nPage';
import { attachMetric, getStableHeap } from '../../utils/performance-helper';
test.use({
capability: {
resourceQuota: { memory: 0.75, cpu: 0.5 },
services: ['victoriaLogs', 'victoriaMetrics', 'vector'],
},
});
test.describe('Memory Leak Detection @capability:observability', {
annotation: [
{ type: 'owner', description: 'Catalysts' },
],
}, () => {
async function performMemoryAction(n8n: n8nPage) {
await n8n.start.fromBlankCanvas();
await n8n.navigate.toWorkflows();
}
test('Memory should be released after actions', async ({
n8nContainer,
n8n,
services,
}, testInfo) => {
const obs = services.observability;
const baseline = await getStableHeap(n8nContainer.baseUrl, obs.metrics);
await performMemoryAction(n8n);
await n8n.page.goto('/home/workflows');
const final = await getStableHeap(n8nContainer.baseUrl, obs.metrics);
const retainedMB = final.heapUsedMB - baseline.heapUsedMB;
const retentionPercent = (retainedMB / baseline.heapUsedMB) * 100;
await attachMetric(testInfo, 'memory-heap-retention-percent', retentionPercent, '%');
await attachMetric(testInfo, 'memory-heap-used-pre-action', baseline.heapUsedMB, 'MB');
await attachMetric(testInfo, 'memory-heap-used-post-action', final.heapUsedMB, 'MB');
await attachMetric(testInfo, 'memory-heap-retained', retainedMB, 'MB');
expect(baseline.heapUsedMB).toBeGreaterThan(0);
expect(final.heapUsedMB).toBeGreaterThan(0);
console.log(
`[MEMORY RETENTION] Baseline: ${baseline.heapUsedMB.toFixed(1)} MB | ` +
`Final: ${final.heapUsedMB.toFixed(1)} MB | ` +
`Retained: ${retainedMB.toFixed(1)} MB (${retentionPercent.toFixed(1)}%)`,
);
});
});