diff --git a/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.manifest.json b/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.manifest.json index 8b0165baf70..4b71275069d 100644 --- a/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.manifest.json +++ b/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.manifest.json @@ -1,7 +1,7 @@ { "$schema": "../scenario.schema.json", "name": "CodeNodeJs", - "description": "A JS Code Node that first generates 100 items and then runs once for each item and adds, modifies and removes properties. The data returned with RespondToWebhook Node.", + "description": "A JS Code Node that first generates 100 items and then runs once for each item and adds, modifies and removes properties. The data is returned with RespondToWebhook Node.", "scenarioData": { "workflowFiles": ["js-code-node.json"] }, "scriptPath": "js-code-node.script.js" } diff --git a/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.json b/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.json new file mode 100644 index 00000000000..2599f19ca4d --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.json @@ -0,0 +1,98 @@ +{ + "createdAt": "2024-08-06T12:19:51.268Z", + "updatedAt": "2024-08-06T12:20:45.000Z", + "name": "Python Code Node", + "active": true, + "nodes": [ + { + "parameters": { + "respondWith": "allIncomingItems", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [1280, 460], + "id": "0067e317-09b8-478a-8c50-e19b4c9e294c", + "name": "Respond to Webhook" + }, + { + "parameters": { + "language": "pythonNative", + "mode": "runOnceForEachItem", + "pythonCode": "def pseudo_random(seed_str, max_val):\n return hash(seed_str) % max_val\n\n# Add new field\n_item['json']['age'] = 10 + pseudo_random(str(_item['json']['email']), 30)\n\n# Mutate existing field\n_item['json']['password'] = '*' * len(_item['json']['password'])\n\n# Remove field\nif 'lastname' in _item['json']:\n del _item['json']['lastname']\n\n# New object field\nemail_parts = _item['json']['email'].split('@')\n_item['json']['emailData'] = {\n 'user': email_parts[0],\n 'domain': email_parts[1]\n}\n\nreturn _item" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1040, 460], + "id": "56d751c0-0d30-43c3-89fa-bebf3a9d436f", + "name": "OnceForEachItemPythonCode" + }, + { + "parameters": { + "httpMethod": "POST", + "path": "py-code-node-benchmark", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [580, 460], + "id": "417d749d-156c-4ffe-86ea-336f702dc5da", + "name": "Webhook", + "webhookId": "34ca1895-ccf4-4a4a-8bb8-a042f5edb567" + }, + { + "parameters": { + "language": "pythonNative", + "pythonCode": "def pseudo_random_string(length):\n characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n result = ''\n seed = hash(str(length)) % 1000\n for i in range(length):\n index = (hash(str(seed + i)) % len(characters))\n result += characters[index]\n seed = (seed * 31 + i) % 1000\n return result\n\ndef random_uid():\n lengths = [8, 4, 4, 4, 8]\n parts = [pseudo_random_string(length) for length in lengths]\n return '-'.join(parts)\n\ndef random_email():\n return f\"{pseudo_random_string(8)}@{pseudo_random_string(10)}.com\"\n\ndef random_person():\n return {\n 'uid': random_uid(),\n 'email': random_email(),\n 'firstname': pseudo_random_string(5),\n 'lastname': pseudo_random_string(12),\n 'password': pseudo_random_string(10)\n }\n\nreturn [{'json': random_person()} for _ in range(100)]" + }, + "id": "c30db155-73ca-48b9-8860-c3fe7a0926fb", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [820, 460] + } + ], + "connections": { + "OnceForEachItemPythonCode": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "OnceForEachItemPythonCode", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { "executionOrder": "v1" }, + "staticData": null, + "meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} }, + "pinData": {}, + "versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b", + "triggerCount": 1, + "tags": [] +} diff --git a/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.manifest.json b/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.manifest.json new file mode 100644 index 00000000000..a8d706ab772 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.manifest.json @@ -0,0 +1,7 @@ +{ + "$schema": "../scenario.schema.json", + "name": "CodeNodePython", + "description": "A Python Code Node that first generates 100 items and then runs once for each item and adds, modifies and removes properties. The data is returned with RespondToWebhook Node.", + "scenarioData": { "workflowFiles": ["py-code-node.json"] }, + "scriptPath": "py-code-node.script.js" +} diff --git a/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.script.js b/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.script.js new file mode 100644 index 00000000000..aee846aec14 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/py-code-node/py-code-node.script.js @@ -0,0 +1,29 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const apiBaseUrl = __ENV.API_BASE_URL; + +export default function () { + const res = http.post(`${apiBaseUrl}/webhook/py-code-node-benchmark`, {}); + + if (res.status !== 200) { + console.error( + `Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`, + ); + } + + check(res, { + 'is status 200': (r) => r.status === 200, + 'has items in response': (r) => { + if (r.status !== 200) return false; + + try { + const body = JSON.parse(r.body); + return Array.isArray(body) ? body.length === 100 : false; + } catch (error) { + console.error('Error parsing response body: ', error); + return false; + } + }, + }); +} diff --git a/packages/@n8n/benchmark/scripts/bootstrap.sh b/packages/@n8n/benchmark/scripts/bootstrap.sh index 02858094c8d..5cd5063fe6c 100644 --- a/packages/@n8n/benchmark/scripts/bootstrap.sh +++ b/packages/@n8n/benchmark/scripts/bootstrap.sh @@ -52,12 +52,14 @@ sudo systemctl disable cron.service curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh sudo -E bash nodesource_setup.sh -# Install docker, docker compose and nodejs +# Install docker, docker compose, nodejs, python3, python3-pip sudo DEBIAN_FRONTEND=noninteractive apt-get update -yq -sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq docker.io docker-compose nodejs +sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq docker.io docker-compose nodejs python3 python3-pip # Add the current user to the docker group sudo usermod -aG docker "$CURRENT_USER" -# Install zx +# Install zx and websockets npm install zx +pip3 install 'websockets==15.0.1' + diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml index 52419713257..bc129a8b22a 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml @@ -35,6 +35,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true ports: - 5678:5678 volumes: diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml index 723c10f00e2..33a498cfb88 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml @@ -53,6 +53,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true command: worker volumes: - ${RUN_DIR}/n8n-worker1:/n8n @@ -88,6 +89,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true command: worker volumes: - ${RUN_DIR}/n8n-worker2:/n8n @@ -126,6 +128,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true volumes: - ${RUN_DIR}/n8n-main2:/n8n depends_on: @@ -166,6 +169,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true volumes: - ${RUN_DIR}/n8n-main1:/n8n depends_on: diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml index ecefddf5b0e..d0fb3b1d97c 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml @@ -51,6 +51,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true command: worker volumes: - ${RUN_DIR}/n8n-worker1:/n8n @@ -84,6 +85,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true command: worker volumes: - ${RUN_DIR}/n8n-worker2:/n8n @@ -118,6 +120,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true ports: - 5678:5678 volumes: diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml index 37e1424cfda..02f263f0a36 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml @@ -17,6 +17,7 @@ services: # Task Runner config - N8N_RUNNERS_ENABLED=true - N8N_RUNNERS_MODE=internal + - N8N_NATIVE_PYTHON_RUNNER=true ports: - 5678:5678 volumes: