Skip to main content

mz-console-scalability

Run the Console scalability suite against a replayed captured workload and measure what Console load costs mz_catalog_server: a replay-only baseline, the Playwright suite, the k6 ramp, and an optional sweep of N headless viewers fitted to CPU per viewer. Trigger: "console scalability test", "run the console against a captured workload", "what does this page cost catalog_server", "cost per viewer", "how many users fit on M.1-nano", "catalog server CPU under console load", or comparing two console versions on catalog_server cost. For one query's plan and latency use mz-query-perf. For SQL throughput benchmarks under test/scalability use mz-benchmark.

Source facts

Repository
MaterializeInc/materialize
Last source activity
October 28, 2026 at 15:01
Detected SKILL.md language
English
Stars
6,391
Forks
516

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
mz-console-scalability
description
Run the Console scalability suite against a replayed captured workload and measure what Console load costs mz_catalog_server: a replay-only baseline, the Playwright suite, the k6 ramp, and an optional sweep of N headless viewers fitted to CPU per viewer. Trigger: "console scalability test", "run the console against a captured workload", "what does this page cost catalog_server", "cost per viewer", "how many users fit on M.1-nano", "catalog server CPU under console load", or comparing two console versions on catalog_server cost. For one query's plan and latency use mz-query-perf. For SQL throughput benchmarks under test/scalability use mz-benchmark.
argument-hint
["workload-file"]
allowed-tools
["Bash","Read","Grep","Glob"]
# Console scalability and catalog_server cost The `console-scalability` workflow in `test/workload-replay/mzcompose.py` boots a captured workload (objects, initial data, hydration), keeps its ingestions and queries running, and drives the console bundled in the materialized image against it. Every console query runs on `mz_catalog_server`, so its CPU is what limits how many viewers an environment can hold. ## What a run produces Per workload, in order: | Phase | Load on top of the replay | Other output | |---|---|---| | `baseline` | none | | | `playwright` | the `console/e2e-tests/scalability` specs, 1 to 6 tabs | `console/test-results/<workload>/`, query tables in the log | | `k6` | the suite's captured queries, ramped to 100 virtual users over the SQL HTTP API | `console/e2e-tests/k6/{summary,queries}-<workload>.json` | | `baseline-before-viewers`, `viewers-<N>`, `baseline-after-viewers` | with `--viewers`, N headless viewers holding one page | | `console/test-results/catalog-server-<workload>.json` holds, per phase: * `cores.catalog_server`, `cores.environmentd`: process CPU from /proc in cores. Absolute, not above baseline. `null` when the process restarted during the phase. * `scheduled_cores`: time inside dataflow operators on catalog_server, split into `index`, `subscribe`, `introspection_subscribe` and `other`. * `subscribes`: open subscribes at the end of the phase, and the console subscribe dataflows' count and average operator count. * `top_dataflows`: the ten non-subscribe dataflows with the most scheduled time, with operator counts. * `succeeded`: `false` when the suite or k6 exited with an error. The phase still holds what ran before the failure, so check `seconds` against a normal run before reading it. With `--viewers`, `viewer_fit.<process>.mcpu_per_viewer` is a least-squares slope through the origin of each step's cores above the mean of the two viewer baselines, and `above_baseline` lists the points it was fitted to. The log ends with the same numbers as a table. The nightly `console-scalability` step runs with `--skip-large` and no `--viewers`. Its artifacts carry the first three phases for every workload, and it uploads one row per phase to the `console_scalability_result` table of test analytics. ## Run it locally ### Before you start * **Run from a commit whose images CI has built.** mzcompose pulls images by fingerprint and builds the ones ghcr lacks, and a local materialized build is a full Rust cross-compile (budget 100+ GiB of `target-xcompile` in a fresh worktree). The materialized image bundles the console, so any file under `console/` changes its fingerprint, untracked files included. Git-ignored files do not count. CI's build step pushes every image it builds, so a pushed branch becomes runnable once its build step for your architecture has finished. Check before starting: ```bash bin/mzcompose --find workload-replay config | sed -n 's/.*image: \(ghcr[^ ]*\)/\1/p' | sort -u | while read -r image; do docker image inspect "$image" > /dev/null 2>&1 || docker manifest inspect "$image" > /dev/null 2>&1 \ && echo "ok $image" || echo "MISSING $image" done ``` * **Python 3.13** must be the `python3` on PATH, since `bin/pyactivate` runs it. When `VIRTUAL_ENV` points at another venv, as VS Code terminals set it, `bin/pyactivate` installs into that venv. Set `VIRTUAL_ENV=$PWD/misc/python/venv`. * **Captured workloads** come from a private repository the workflow clones into `test/workload-replay/captured-workloads`. In a worktree, symlink the main checkout's copy. Workloads tagged `settings.large: true` take far longer and more memory. * **Keep the host awake and the run detached.** Laptop sleep was observed to halt environmentd (exit 166, a persist `Listen ... received a batch` halt after the timestamp jumped), and nothing restarts it, so the run is lost. A command an agent runs in the background is killed at the tool's time limit, which stops the replay. Start the run with `nohup caffeinate -dis ... &` on AC power and follow the log. `caffeinate` cannot stop a lid-close sleep on battery. * **One run per compose project.** A run starts by killing and removing every container of its project. Pass `--project-name <name>` to run beside another replay. ### Command ```bash PYTHONUNBUFFERED=1 nohup caffeinate -dis bin/mzcompose --find workload-replay \ run console-scalability --viewers 10,20,30 --cost-window 120 workload_ddnet.yml \ > console-scalability.log 2>&1 & grep -E '^\+\+\+|^-- catalog_server cost|Traceback' console-scalability.log ``` On a laptop this takes about 55 minutes: about 10 to boot and hydrate, 20 for the Playwright suite, 6 for k6, and the rest for the viewer sweep. | Flag | Default | Notes | |---|---|---| | `--catalog-server-size` | the bootstrap size | Same worker count and CPU limit as `M.1-nano`. Pin another size to compare. | | `--console-bundle` | the image's console | A `dist` directory to serve instead, copied in after hydration. See "Compare two console versions". | | `--viewers` | none | Comma-separated viewer counts. Use three or more to see whether cost grows linearly. | | `--viewer-path` | the `mz_catalog_server` cluster page | Console path the viewers hold, such as `/regions/local-flexible-deployment/clusters/u2/compute_cluster`. The page matters: on `workload_ddnet`, `main` measured 9.5 mCPU per viewer on the `mz_catalog_server` page and 7.3 on a user cluster's page. User cluster ids follow the order of `clusters` in the workload file, starting at `u2`. | | `--cost-window` | 90 | Seconds per measured window: the baselines and each viewer step. | | `--cost-settle` | 30 | Seconds of settling before each window. | | `--seed` | time | Seeds the replay's generated data and query choices. Fix it when comparing runs. | The replay's own `--factor-initial-data`, `--factor-ingestions`, `--factor-queries` and `--max-concurrent-queries` apply too. ## Read the numbers * **Subtract the baseline.** Every core figure is absolute and includes the replay's own queries. What the console costs is a phase minus `baseline`. * **Check the fit before quoting it.** The slope assumes cost grows in proportion to viewers. On `workload_ddnet` the cost per viewer was lower at 10 viewers than at 20 or 30, so treat `mcpu_per_viewer` as an average over the measured range and quote it with that range. A single step far off the others usually means its pages did not finish loading: look for `page-layout not found` in the log. * **Process CPU minus scheduled time** is work outside dataflow operators: session handling, peeks served from indexes, networking, and dataflows that were dropped before the phase ended. A gap that grows with polling load points at per-request cost. Each new SQL session was measured at about 35 ms of catalog_server CPU, and every HTTP `/api/sql` request opens one, where a WebSocket keeps one session. * **Operator counts explain most dataflow cost.** Measured on a one-worker catalog_server: about 3 microcores per operator for a maintained index over static input, and 3.5 to 5 microcores per operator per viewer for a console subscribe. A subscribe with hundreds of operators per viewer is the slope to cut, see `subscribes.avg_operators`. * **Local readings are not capped.** Observed: the process orchestrator does not enforce the replica's CPU limit, so catalog_server can read above the 0.5 cores an `M.1-nano` gets in production, up to about one core per worker. A reading above 0.5 means production would saturate. * **Do not read `mz_cluster_replica_metrics_history` locally.** It was observed reading 2.7 to 3.2 times the /proc figure. * **k6 over-polls queries it does not know.** `console/e2e-tests/k6/cluster-detail.ts` replays every label in the captured `queries.json` and polls a label missing from its `POLL_INTERVAL_MS` table every 5 seconds per virtual user. A console change that adds queries therefore reads costlier in the `k6` phase than it is. Diff the two runs' `queries-<workload>.json` labels, and add the new ones to `POLL_INTERVAL_MS` with the console's real cadence before comparing `k6`. The viewer sweep runs the real console and has no such gap. * **Browsers share the host.** The viewers and the suite run on the same machine as materialized. Compare runs on the same host, and repeat a run before trusting a difference of a few percent. ## Compare two console versions Keep the CI-built images and swap only the console. Build each version in its own worktree the way the image does, then run once per build with the same host, workload, `--seed` and flags: ```bash (cd console && CONSOLE_DEPLOYMENT_MODE=flexible-deployment yarn build) bin/mzcompose --find workload-replay run console-scalability --seed 1234 \ --viewers 10,20,30 --console-bundle /path/to/worktree/console/dist workload_ddnet.yml ``` Run the baseline side without `--console-bundle`, or with a build of the base commit. A feature behind a LaunchDarkly flag is on in this mode unless `disabledFlexibleDeploymentFlags` in `console/src/config/flexibleDeploymentFlags.ts` turns it off, so force it on in the build's worktree, never in a commit. To check the swap took, compare `curl http://127.0.0.1:<6874 port>/index.html` with the build's `dist/index.html`, and diff the two runs' `queries-<workload>.json` labels: the new page's queries should appear. Compare each phase minus its own run's baseline, and `mcpu_per_viewer`, not absolute cores.
View on GitHub