diff --git a/ACT/LibMCData.xml b/ACT/LibMCData.xml index dc5583b37..c0a417087 100644 --- a/ACT/LibMCData.xml +++ b/ACT/LibMCData.xml @@ -1415,6 +1415,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DevTools/executionlist_perf/README.md b/DevTools/executionlist_perf/README.md new file mode 100644 index 000000000..67f2908d7 --- /dev/null +++ b/DevTools/executionlist_perf/README.md @@ -0,0 +1,72 @@ +# Execution list performance + +`executionlist_perf.py` seeds a large build history into a dev `amc_server` and measures the +Build History list, for before/after comparisons of changes to it. It needs only +Python 3 (standard library) on Linux. + +## Usage + +```bash +python3 DevTools/executionlist_perf/executionlist_perf.py run \ + --output build_linux64/Output --workdir /tmp/elperf --fresh [-n 2000] [--json result.json] +``` + +`run` does every step and then stops the server it started: + +1. **prepare**: creates `--workdir` from the build's `Output/`. The binaries are symlinked. The + MinimalSystem config is copied with a `buildhistory` page added (an `executionlist`, as in + `Examples/LPBFSystem/config.xml`). The server binds to 127.0.0.1:18869 (`--port`) with + `telemetry="off"`. +2. Starts the server once so that it creates and migrates `data/storage.db`. This step is + skipped when the database already exists. +3. **seed**: writes N jobs and executions straight into SQLite. The seed is deterministic, and + rerunning it replaces its own rows. About 85% of executions are finished, 10% failed and 5% + in process. Every 10th job is `deleted`, and every 3rd job has a thumbnail UUID. Every seeded + row gets an `incremental_id` (one step per row, above any existing rows), so the list head + is the newest seeded execution and `?since=` returns exactly the last k rows. +4. Starts the server and **measures** each mode in `--modes` (default + `full,concurrent,delta,frontend`). It logs in as `test`/`test` with the same + `/api/auth` session-key handshake the client uses, and sends the token as a Bearer header. + +The steps can also run one at a time, against a server you start yourself: + +```bash +executionlist_perf.py prepare --output build_linux64/Output --workdir W +executionlist_perf.py seed W/data/storage.db -n 2000 # server stopped +executionlist_perf.py measure --pid [--modes full,delta --single-count 20] +``` + +## Modes + +The client (`Client/core/modules/AMCModule_ExecutionList.js` with `AMCListDeltaSync`) fetches +the full list once when the page loads, then only a `?since=` delta whenever the +`executionlistheadid` attribute changes. It does not poll the list. + +| mode | what it simulates | +|--------------|-------------------| +| `full` | Sequential `GET /api/executions` (`--single-count`): one page load. | +| `concurrent` | `--clients` (default 6) full fetches started at once: several tabs or devices loading together. | +| `delta` | Sequential `GET /api/executions?since=` for each k in `--delta-sizes` (default `1,10,100`): the fetch after k executions changed. | +| `frontend` | Sequential `GET /api/frontend?pages=buildhistory`: the scoped v2 snapshot the client polls when it has no SSE stream. The list item contributes only its head id, so this should not grow with N. | + +## Output + +- `latency_*_s`: per-request latency. +- `rows`, `response_bytes`: list entries and JSON size of one response. +- `wall_s` (concurrent): time until the last of the simultaneous fetches finished. +- `errors` / `first_error`: failed requests and the server's message for the first. +- `cpu.cpu_mean_pct` / `cpu.cpu_peak_pct`: `amc_server` CPU from `/proc//stat`, sampled + every 1 s over the mode. 100% means one core. Short modes finish before the first sample, so + only the mean is meaningful there. + +Measure an optimised build when you want device-like numbers. An empty `CMAKE_BUILD_TYPE` +compiles at -O0, while device builds use `-DCMAKE_BUILD_TYPE=Release`. Compare before/after +only between builds of the same type. + +## Damaged databases + +`--damaged K` (on `seed` and `run`) appends K executions whose job row does not exist, as in a +damaged database. They carry the newest `incremental_id`s, so they are in the full list and in +every delta. Before the per-entry degrade fix in LibMCData, one such row makes the whole +`/api/executions` request fail (`errors` > 0), and the client's list stays frozen. + diff --git a/DevTools/executionlist_perf/executionlist_perf.py b/DevTools/executionlist_perf/executionlist_perf.py new file mode 100755 index 000000000..ddcfb4eb0 --- /dev/null +++ b/DevTools/executionlist_perf/executionlist_perf.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +"""Seed a large build history into a dev amc_server and measure the Build History list. + +Before/after tooling for the execution list (N+1 queries per list request). Standard +library only. See README.md next to this file. + +Subcommands + prepare Build a scratch server directory from a build Output dir (MinimalSystem plus a + "buildhistory" page with an executionlist), bound to 127.0.0.1, telemetry off. + seed Insert N build jobs + executions straight into storage.db (server stopped). + measure Fetch the execution list of a running server the way the client does and report + latency, response size and amc_server CPU. + run prepare + start/stop once to migrate the DB + seed + start + measure all modes + stop. + +Measure modes (measure --modes, run --modes) + full Sequential GET /api/executions: the full list a client fetches once per page load + (Client/core/modules/AMCModule_ExecutionList.js via AMCListDeltaSync). + concurrent --clients full fetches started at once: several tabs/devices loading together. + delta Sequential GET /api/executions?since= for each k in --delta-sizes: the + incremental fetch after k executions changed (the client sends one per head change). + frontend Sequential GET /api/frontend?pages=buildhistory: the scoped v2 snapshot the + client polls when it has no SSE stream (the list item only adds its head id). +""" +import argparse, hashlib, json, os, random, re, shutil, signal, socket, sqlite3, subprocess +import sys, threading, time, urllib.error, urllib.parse, urllib.request, uuid +from datetime import datetime, timedelta, timezone + +DEFAULT_PORT = 18869 +PAGE = "buildhistory" + +# -------------------------------------------------------------------------------------------- +# prepare + +BUILDHISTORY_PAGE = """ + + + +