Test stand load benchmark¶
The Load benchmark dashboard is a destructive, operator-triggered scenario for measuring how confirmed trade-operation latency changes across increasing account and position counts while the quote source publishes at a configured rate. It is a separate stand workflow from the deterministic regression suites. It restores the selected snapshot before preparing its fixtures; accounts and positions created by the run remain until a later snapshot restore. Use a dedicated test server and snapshot.
The profile allows up to 500,000 accounts and 500,000 positions. These are input limits and target values, not a verified capacity claim. Preparing a large profile can take hours. Choose a time limit appropriate to the environment and stop the run if the test server or load generator approaches resource limits. There is one active test run at a time.
Dashboard workflow¶
- Sign in to the test stand and select a snapshot.
- Enter a sequence of cumulative stages. Each stage raises or retains account and position counts and sets the quote publication target and measurement hold duration.
- Set preparation and measurement concurrency, fixture symbols, probe pairs per measurement, active-account count, quote batch interval, and total time limit.
- Set the measurement interval and start the benchmark. The stand inserts actual measurement barriers at configured targets and intermediate account/position milestones; the dashboard polls its live state every two seconds and displays completed measurements.
- Open Load history in the test-stand sidebar to browse paginated load runs, inspect incidents, open reports or journals, compare saved results, and manage retained or purged run files. The benchmark dashboard itself contains the live benchmark and result comparison.
- Stop from the load dashboard. Load runs do not appear in regression history or its live log stream.
A run first restores the selected snapshot, clones the configured deterministic USD Forex fixture into the requested synthetic symbols, creates a test group, publishes and confirms initial fixture quotes, then funds and creates accounts and opens positions. The benchmark deadline starts when this scenario begins, after the normal snapshot restore and base fixture setup; it includes load-fixture setup and measurements. Configured stage account and position values are cumulative targets. Positions are distributed round-robin over the first min(activeAccounts, prepared accounts) accounts; activeAccounts: 0 uses all prepared accounts. For each measurement point, the stand holds the source target duration, performs the configured number of open/close probe pairs, and times the closure of all seeded positions belonging to the first login. It then reopens those positions so the next point starts at its requested position count. Cancellation or an error can leave a partially prepared fixture until the next snapshot restore.
Before execution, the stand expands configured targets into real measurement barriers. For each increasing accounts or positions axis, it inserts milestones at absolute counts 1,000 and 5,000 and at multiples of measurementEvery between the previous target and the next. If both axes increase, their milestones are combined by proportional progress between the configured targets; each configured target remains an exact measurement point. A configured stage that changes only the quote rate or duration still produces its target measurement. Every inserted point inherits the quote rate and stabilization duration of its source target stage. Setup workers drain before the stabilization pause and probes at each point; the stand does not fetch a full trade snapshot per point. The stage result field is the one-based measurement ordinal, while sourceStage identifies the configured target. For example, one symmetric target of 500,000 accounts and 500,000 positions with the default interval of 10,000 produces 52 measurements, including 1,000 and 5,000 and preserving the 500,000 target. The concurrency setting is unchanged and applies to worker operations as described above.
Profile fields and limits¶
POST /api/load/run requires a profile object and a nonempty snapshotId. Unknown fields inside profile or a stage are rejected. Numeric fields are integers within these limits:
| Field | Range | Meaning |
|---|---|---|
stages |
1–12 entries | Ordered cumulative load stages. accounts and positions may stay level or increase from one stage to the next. |
stages[].accounts |
1–500,000 | Total fixture accounts to prepare by this stage. |
stages[].positions |
0–500,000 | Total seeded open positions to hold at this stage. |
stages[].ticksPerSecond |
0–100,000 | Target aggregate quote-source publication rate during this stage. |
stages[].seconds |
1–3,600 | Hold duration before that stage's latency probes. |
concurrency |
1–64 | Maximum concurrent stand workers for fixture account/position preparation and restoring the measured login. |
probeConcurrency |
1–64 | Maximum concurrent open/close probe and close-login operations. Defaults to 1 when omitted. |
distribution |
uniform or hot80 |
Deterministic symbol distribution for seeded positions, probe commands, and quotes. Defaults to uniform. hot80 requires at least two symbols. |
symbols |
1–100 | Number of synthetic symbols cloned from the configured deterministic fixture. |
activeAccounts |
0–500,000 | Accounts used to distribute positions and probe traffic; 0 means all prepared accounts. |
samples |
1–1,000 | Open/close probe pairs per measurement point. |
measurementEvery |
1,000–500,000 | Account/position milestone spacing. Defaults to 10,000 when omitted. The plan also inserts absolute 1,000 and 5,000 milestones. |
quoteBatchMs |
10–1,000 | Quote-source timer interval for publishing due ticks in bounded batches. |
maxMinutes |
1–720 | Overall scenario deadline, including fixture setup and measurements. |
The dashboard starts with preparation concurrency 16, probe concurrency 1, 200 probe pairs, and uniform distribution across 100 symbols. Its load presets are One hot symbol (one symbol with uniform placement), Uniform · 100 symbols, and 80% load on 20% of 100 symbols. The starter target sweep is 1,000, 10,000, 50,000 and 100,000 accounts/positions; targets remain editable up to 500,000. These are UI starting values, not API defaults or capacity claims. The API allows up to 500,000 accounts and positions; symbols remain capped at 100. The API validates that the profile is well-formed; it does not promise that the requested fixture fits the available server, generator, license, storage or runtime capacity. A run that reaches its deadline or encounters a failed trade operation ends with a failed run and retains its available evidence.
Start request¶
POST /api/load/run¶
Requires a valid standalone test-stand session and the same-origin request checks described in authenticated controls. It also requires that no other run is active. Load and regression scenarios share one serial runner and exclusive trading-server/snapshot state; this endpoint cannot run alongside a regression. The snapshotId is required for this endpoint.
Complete request example:
{
"snapshotId": "clean",
"profile": {
"stages": [
{ "accounts": 1000, "positions": 1000, "ticksPerSecond": 1000, "seconds": 10 },
{ "accounts": 10000, "positions": 10000, "ticksPerSecond": 5000, "seconds": 30 }
],
"concurrency": 16,
"probeConcurrency": 1,
"distribution": "uniform",
"symbols": 2,
"activeAccounts": 0,
"samples": 20,
"measurementEvery": 10000,
"quoteBatchMs": 50,
"maxMinutes": 60
}
}
A successful enqueue returns HTTP 202:
{ "runId": "2026-10-15T10-00-00-000Z-load-benchmark-12345678" }
The run ID identifies the ordinary test-stand run and is used by status, cancellation, history and artifact endpoints. Errors include:
| HTTP | Response | Meaning |
|---|---|---|
| 400 | { "error": "INVALID_LOAD_PROFILE", "issues": [...] } |
Missing/invalid profile, empty or missing snapshotId, or profile validation failed. issues is the Zod validation issue array; it is empty when the profile itself is valid but snapshotId is invalid. |
| 401 | { "error": "AUTHENTICATION_REQUIRED" } |
No valid stand session. |
| 403 | { "error": "INVALID_ORIGIN" } |
Origin or fetch-site check failed. |
| 409 | { "error": "STAND_BUSY" } |
A run is active or another load start is being admitted. |
Other runner or snapshot errors use the stand's existing error handling.
Live state¶
GET /api/load¶
Read-only and available without a stand session, like other stand monitoring endpoints. It returns the latest in-process load state and, when runId resolves to a run summary, an optional run object in the standard RunSummary format documented in authenticated controls. After a test-stand process restart, the in-memory load state starts as idle; completed results remain available as run artifacts.
The response includes these fields:
| Field | Type | Description |
|---|---|---|
runId |
string, optional | Current or most recently started load-run identifier. |
phase |
string | idle, queued, fixture, stage progress text, completed, failed, or cancelled. Stage progress names identify account creation, position creation, measurement, or restoring the closed login. |
accounts |
integer | Number of accounts prepared so far. |
positions |
integer | Number of currently seeded positions; this temporarily decreases while the measured login is closed. |
profile |
object, optional | Validated input profile, with fields listed above. |
points |
array | Completed per-stage measurements. Each item is a LoadPoint described below. |
quotesPublished, quotesSkipped |
integers | Cumulative source publications and due quote ticks skipped by the bounded generator. |
elapsedSeconds |
number, optional | Elapsed wall time for the current run. |
error |
string, optional | Run error text when failed or cancelled. |
generator |
object, optional | Latest generator resource observation: rssMB, cpuPercent, and loopP99Ms. |
environment |
object, optional | Run build, generator host/runtime and measurement-boundary metadata. |
run |
object, optional | Ordinary test-stand RunSummary, when available. |
Complete example with one completed stage; identifiers and measurements are illustrative:
{
"runId": "2026-10-15T10-00-00-000Z-load-benchmark-12345678",
"phase": "completed",
"accounts": 1000,
"positions": 1000,
"profile": {
"stages": [{ "accounts": 1000, "positions": 1000, "ticksPerSecond": 1000, "seconds": 10 }],
"concurrency": 16, "probeConcurrency": 1, "distribution": "uniform", "symbols": 2, "activeAccounts": 0, "samples": 200, "measurementEvery": 10000, "quoteBatchMs": 50, "maxMinutes": 60
},
"points": [{
"stage": 1, "sourceStage": 1, "accounts": 1000, "positions": 1000,
"targetTicksPerSecond": 1000, "actualTicksPerSecond": 997.4,
"quoteSkipped": 26, "quotePublished": 22144, "quoteWindowSeconds": 22.2, "generatorLimited": true,
"open": { "count": 200, "p50": 41.2, "p95": 62.1, "p99": 63.0, "max": 63.0 },
"close": { "count": 200, "p50": 38.0, "p95": 55.2, "p99": 56.1, "max": 56.1 },
"openAccepted": { "count": 200, "p50": 8.2, "p95": 12.1, "p99": 14.0, "max": 15.0 },
"closeAccepted": { "count": 200, "p50": 7.8, "p95": 11.2, "p99": 13.1, "max": 14.0 },
"symbolLoad": [
{ "symbol": "TEST_LOAD_12345678_0", "positions": 500, "targetTicksPerSecond": 500, "published": 11072, "actualTicksPerSecond": 498.7 },
{ "symbol": "TEST_LOAD_12345678_1", "positions": 500, "targetTicksPerSecond": 500, "published": 11072, "actualTicksPerSecond": 498.7 }
],
"closeLoginMs": 1240.5, "closeLoginPositions": 1,
"mainLoop": {
"status": "observed", "source": "local-systemd-journal", "unit": "server.service",
"since": "2026-10-15T10:00:11.000Z", "until": "2026-10-15T10:00:33.200Z",
"thresholdMs": 30, "coverage": "slow-cycles-only-debug-dependent",
"count": 2, "invalidRecords": 0, "maxTotalMs": 45,
"phaseMaxMs": { "total_ms": 45, "intake_ms": 2, "affected_ms": 1, "schedule_ms": 6, "wait_ms": 30, "margins_apply_ms": 2, "trades_collect_ms": 3, "trades_enqueue_ms": 1, "cleanup_ms": 0 },
"slowest": [
{ "timestamp": "2026-10-15T10:00:22.500Z", "values": { "total_ms": 45, "intake_ms": 2, "affected_ms": 1, "schedule_ms": 6, "wait_ms": 30, "margins_apply_ms": 2, "trades_collect_ms": 3, "trades_enqueue_ms": 1, "cleanup_ms": 0, "logins": 12, "batches": 2, "margin_updates": 3, "trade_updates": 4, "deletes": 0, "dirty_trades": 4, "dirty_margins": 3, "dirty_groups": 0, "dirty_symbols": 0, "dirty_symbol_indexes": 0, "dirty_quotes": 1 } },
{ "timestamp": "2026-10-15T10:00:18.000Z", "values": { "total_ms": 38, "intake_ms": 2, "affected_ms": 1, "schedule_ms": 5, "wait_ms": 26, "margins_apply_ms": 1, "trades_collect_ms": 2, "trades_enqueue_ms": 1, "cleanup_ms": 0, "logins": 8, "batches": 1, "margin_updates": 2, "trade_updates": 3, "deletes": 0, "dirty_trades": 3, "dirty_margins": 2, "dirty_groups": 0, "dirty_symbols": 0, "dirty_symbol_indexes": 0, "dirty_quotes": 1 } }
]
},
"generator": { "rssMB": 512.4, "cpuPercent": 38.2, "loopP99Ms": 23.1 }, "elapsedSeconds": 91.5
}],
"quotesPublished": 22144,
"quotesSkipped": 26,
"elapsedSeconds": 91.5,
"generator": { "rssMB": 512.4, "cpuPercent": 38.2, "loopP99Ms": 23.1 },
"environment": {
"server": { "version": "1.25.284", "buildDate": "Oct 14 2026" },
"generatorHost": "stand-host",
"generatorNode": "v22.23.3",
"generatorCpuCount": 8,
"serverResources": "Not collected in MVP",
"transport": "one persistent manager TCP session; closed-loop bounded concurrency",
"closeLoginMode": "bounded sequence of MngCloseTrade requests, not the customer CloseGroupTrade endpoint",
"latency": "accepted: dispatch to successful admission response; confirmed: dispatch through TCP trade event; both exclude worker-slot waiting",
"quoteMetric": "source writes, not engine-applied ticks"
},
"run": {
"id": "2026-10-15T10-00-00-000Z-load-benchmark-12345678",
"scenarioId": "load-benchmark",
"scenarioName": "Load benchmark",
"status": "passed",
"requestedAt": "2026-10-15T10:00:00.000Z",
"startedAt": "2026-10-15T10:00:01.000Z",
"finishedAt": "2026-10-15T10:01:32.500Z",
"snapshotId": "clean",
"snapshotName": "Clean fixture",
"testedBuild": { "version": "1.25.284", "buildDate": "Oct 14 2026" },
"revision": 90, "logCount": 30, "assertionCount": 1, "passed": 1, "failed": 0,
"levels": { "info": 28, "success": 1, "warning": 1, "error": 0 },
"sections": { "Load benchmark": { "passed": 1, "failed": 0 } }, "reportReady": true
}
}
run is omitted if no summary is available. profile, elapsedSeconds, error, generator, environment, and runId are omitted when not present in the current state. The initial idle response is:
{ "phase": "idle", "accounts": 0, "positions": 0, "points": [], "quotesPublished": 0, "quotesSkipped": 0 }
Separate load history and diagnostics¶
The test stand places Load history in its own sidebar section, separate from the Load benchmark screen. The history list is paginated and contains only runs whose scenarioId is load-benchmark; the regression history and regression WebSocket stream omit these runs. From a load-history row, operators can open incidents, reports or journals, compare saved result artifacts, hide a run from the visible list, or permanently delete its files. These actions use existing run-management endpoints; no load-specific delete endpoint is added.
GET /api/load/runs¶
Read-only, no stand session required. Query fields:
| Field | Type | Required | Description |
|---|---|---|---|
limit |
integer | No | Page size 1–100; default 25. |
cursor |
string | No | Opaque exclusive keyset cursor returned in nextCursor; maximum 1,024 characters. |
Runs are ordered by requestedAt descending, then id descending. The server filters to load benchmarks in the indexed query before applying the cursor and page limit. Thus regression rows do not consume a load page or alter its hasMore/nextCursor, and load rows do not consume pages from GET /api/runs.
Success returns items (RunSummary array), hasMore (boolean), nextCursor (opaque string or null), and storage (the common storage/recovery status documented in Test Stand History API). Each item has the complete RunSummary shape documented there. Complete illustrative response:
{
"items": [{
"id": "2026-10-15T10-00-00-000Z-load-benchmark-12345678",
"scenarioId": "load-benchmark",
"scenarioName": "Load benchmark",
"status": "failed",
"requestedAt": "2026-10-15T10:00:00.000Z",
"startedAt": "2026-10-15T10:00:01.000Z",
"finishedAt": "2026-10-15T10:05:00.000Z",
"snapshotId": "clean",
"snapshotName": "Clean fixture",
"testedBuild": { "version": "1.25.284", "buildDate": "Oct 14 2026" },
"progress": { "completed": 2, "total": 5, "current": "stage 3: measuring" },
"error": "Load benchmark time limit reached",
"revision": 47,
"logCount": 12,
"assertionCount": 1,
"passed": 0,
"failed": 1,
"levels": { "info": 10, "success": 0, "warning": 1, "error": 1 },
"sections": { "Load benchmark": { "passed": 0, "failed": 1 } },
"reportReady": true
}],
"hasMore": false,
"nextCursor": null,
"storage": {
"mode": "postgres-with-local-journal",
"pendingRuns": 0,
"pendingRecords": 0,
"pendingWrites": 0,
"indexReady": true,
"error": null,
"recovery": {
"state": "ready", "totalRuns": 23, "loadedRuns": 23,
"currentDirectory": null, "phase": "done",
"processedEntries": 0, "totalEntries": 0, "error": null
}
}
}
A malformed nonempty cursor returns HTTP 400 { "error": "INVALID_CURSOR" }. An invalid limit returns HTTP 400 with the stand framework error envelope and message INVALID_PAGINATION. If the index is unavailable, the endpoint returns HTTP 503 with error: "RUN_INDEX_UNAVAILABLE" and the current storage status. List results are committed indexed summaries; during index catch-up, a newly started run may not yet appear. Follow its run ID through GET /api/load while active.
GET /api/load/runs/hidden¶
Requires a valid stand session; GET does not require an Origin header. This returns archived load runs hidden from normal load history while retaining their artifact evidence. It does not include purged runs. Query fields: limit (integer 1–100, default 25) and after (exclusive run-ID cursor, default empty; IDs are sorted lexicographically ascending).
Complete response:
{
"items": [{
"id": "2026-10-15T10-00-00-000Z-load-benchmark-12345678",
"username": "admin",
"deletedAt": "2026-10-15T11:00:00.000Z"
}],
"hasMore": false,
"nextAfter": null
}
An item contains id, username and deletedAt strings. hasMore is boolean; nextAfter is the last item ID when another page exists, otherwise null. The matching regression-only archive list is GET /api/runs/hidden. Missing authentication returns 401 AUTHENTICATION_REQUIRED.
Hide or permanently delete a load run¶
Load-history row actions are icon buttons. They are disabled while signed out, while a run is active or its report is not ready, and while a row action is pending. Both buttons lock during an action and show a progress indicator, preventing repeat clicks. In the hidden-load list, the permanent-delete action remains available.
Hide archives the run from visible load history while retaining its evidence files. The authenticated request is DELETE /api/runs/:id and returns:
{ "deleted": true, "id": "2026-10-15T10-00-00-000Z-load-benchmark-12345678", "evidenceRetained": true }
The archived run appears in GET /api/load/runs/hidden and can still be reopened. Delete files permanently uses the authenticated request POST /api/runs/:id/purge with an explicit run-ID confirmation:
{ "confirmRunId": "2026-10-15T10-00-00-000Z-load-benchmark-12345678" }
On success, its response is:
{ "deleted": true, "id": "2026-10-15T10-00-00-000Z-load-benchmark-12345678", "evidenceRetained": false, "auditMarkerRetained": true }
Permanent deletion removes run files and artifacts; the deletion audit marker remains. Both operations return 401 AUTHENTICATION_REQUIRED without a valid stand session, 404 RUN_NOT_FOUND for an unknown ID, or 409 RUN_BUSY while the run is queued/preparing/running, its report is not ready, or report generation is in progress. Purge also returns 400 RUN_CONFIRMATION_REQUIRED if confirmRunId does not match the path ID. Storage failures return 503 RUN_DELETE_FAILED or RUN_PURGE_FAILED. These are existing protected controls shared with other run families.
POST /api/load/runs/:id/cancel¶
Requires a stand session and the matching Origin, as described in authenticated controls. It only accepts a run whose scenarioId is load-benchmark; a different or unknown run ID returns 404 RUN_NOT_FOUND. +A successful HTTP 202 response is:
{ "cancelled": true }
This acknowledges the cancellation request; the run may take time to reach a cancellation check and persist its final state. A terminal or otherwise noncancellable run returns 409 RUN_NOT_CANCELLABLE. Missing authentication returns 401 AUTHENTICATION_REQUIRED; an Origin rejection returns 403 INVALID_ORIGIN.
Run-family status and live stream¶
GET /api/status retains activeRunIds, the list of all queued/preparing/running runs used to enforce the global one-run-at-a-time rule. It also returns activeLoadRunIds and activeRegressionRunIds, both arrays of run IDs, so clients can display or stop the correct workload. activeRegressionRunIds includes every non-load scenario. Load runs are omitted from the normal /api/runs list and from /ws run summaries and selected-run live logs; the load dashboard polls /api/load and /api/load/runs itself.
The timeout failure recorded for run 03ea2527 is a load-run outcome, not a regression result or check. It belongs in load history and load diagnostics, not in regression history or regression totals. This presentation and history separation does not change timeout limits, timeout detection, cancellation semantics, or execution behavior.
Per-measurement result (LoadPoint)¶
Each item in points contains:
| Field | Type | Description |
|---|---|---|
stage |
integer | One-based measurement ordinal across the expanded plan; it can exceed the number of configured target stages. |
sourceStage |
integer | One-based configured target stage whose quote rate and stabilization duration this measurement inherits. |
accounts, positions |
integer | Prepared account and requested seeded-position counts for the stage. |
targetTicksPerSecond, actualTicksPerSecond |
number | Target and measured quote-source publication rates. |
quoteSkipped, quotePublished |
integer | Skipped due ticks and publications during the measurement window. |
quoteWindowSeconds |
number | Measurement window used to calculate actualTicksPerSecond; includes probes and measured login closure. |
generatorLimited |
boolean | True when any quote tick was skipped in the stage window or actual publication rate is below 90% of a nonzero target. |
open, close |
Latency |
Confirmed open/close probe latency summaries, measured from dispatch through the correlated TCP trade event. |
openAccepted, closeAccepted |
Latency |
Admission-response latency summaries, measured from dispatch through the successful accepted response. These acknowledge admission, not completed execution. |
symbolLoad |
array | Per-symbol seeded-position and quote-rate breakdown for this measurement window. |
mainLoop |
object, optional | Existing [CORE][LOOP] slow_cycle records collected from the local systemd journal for this measurement. Missing from older artifacts; a collection failure is represented by status: unavailable. |
closeLoginMs |
number | Total elapsed time for the measured login's individual close requests. |
closeLoginPositions |
integer | Number of seeded positions closed for the measured login. |
symbolLoad[] |
object | Contains symbol (string), positions (seeded-position count before the measured login is closed), targetTicksPerSecond (aggregate target rate allocated to that symbol), published (source quote publications during the measurement window), and actualTicksPerSecond (publications divided by the measurement window). |
generator |
object | Generator sample with rssMB, cpuPercent, loopP99Ms. |
elapsedSeconds |
number | Run elapsed time at the end of this measurement. |
The optional mainLoop object contains status (observed, no-records, truncated, or unavailable), source (local-systemd-journal), optional unit (configured server systemd service name), ISO since and until timestamps, thresholdMs (30), and coverage (slow-cycles-only-debug-dependent). Its count is the number of valid parsed records; invalidRecords counts journal lines that failed JSON parsing and matching slow-cycle records with invalid or missing required fields. A nonzero invalid count can mean the displayed diagnostics are incomplete. When records were observed, maxTotalMs gives the maximum total_ms; it may be absent otherwise. phaseMaxMs contains independent maxima for whichever of these timing fields occurred: total_ms, intake_ms, affected_ms, schedule_ms, wait_ms, margins_apply_ms, trades_collect_ms, trades_enqueue_ms, cleanup_ms. These maxima can come from different cycles and must not be summed. slowest contains up to five records sorted by total_ms; each record has an ISO timestamp and allowlisted values containing any available timing fields plus counters logins, batches, margin_updates, trade_updates, deletes, dirty_trades, dirty_margins, dirty_groups, dirty_symbols, dirty_symbol_indexes, and dirty_quotes. Missing fields are omitted, including phase fields absent from zero-batch records. Optional error text is limited to 500 characters.
Latency contains count, p50, p95, p99, and max, all numeric values in milliseconds except count, which is the number of completed observations. Confirmed open/close timing starts immediately before dispatching a manager TCP request and ends after the matching confirmed TCP trade event is observed. Accepted timing ends when the successful admission response is received, before waiting for that event. Both omit time waiting for a probe worker slot; confirmed timing also includes response handling and event delivery, and is not an engine service-clock measurement. Failed or unconfirmed operations fail the run rather than being counted as successful confirmed-latency samples. Older saved results without openAccepted or closeAccepted do not contain those measurements and the dashboard reports them as unavailable.
What this benchmark measures¶
- Open/close samples use manager trade commands over one persistent TCP session. They are bounded closed-loop probes: the stand dispatches work as probe workers become available.
probeConcurrencycontrols this parallelism;concurrencycontrols fixture preparation/restoration. These measurements reflect the offered workload, not an independently paced open-loop arrival rate. - Each measurement reports p50/p95/p99/max for confirmed and accepted open/close latency, plus one duration for closing the first account's seeded positions.
- With
uniform, positions, probe commands and quote publications are distributed round-robin across all configured symbols. Withhot80, deterministic placement assigns eight of every ten positions, probes and quotes to the firstceil(20% × symbols)symbols (capped so at least one symbol remains outside the hot set); the remaining two of ten go to the other symbols. The per-symbol target quote rate allocates 80% of the aggregate target evenly across hot symbols and 20% evenly across the remainder.hot80is valid only with at least two symbols. For each symbol, the quote generator alternates bid and ask together between 1.10000 and 1.10001 on successive publications. Quote rates count calls written to the stand's quote source.actualTicksPerSecondis source publications per measured window, not confirmation that the trading engine applied every tick. Batches are bounded at 1,000 due ticks per timer pulse; pending quote socket data is bounded at 1 MiB. Excess due ticks are skipped and reported. AgeneratorLimitedstage cannot be interpreted as the server's maximum quote throughput. - Generator RSS, CPU usage and event-loop p99 are sampled by the stand process. Trading-server CPU/RAM, internal queue depths, and engine-applied tick rate are not collected in this MVP.
- “Close login” closes each selected seeded position with its own
MngCloseTradeoperation, limited byprobeConcurrency. It does not call the customer bulk-close endpoint and does not measure one atomic account-close operation.
Main-loop slow-cycle observations¶
After the measurement hold, probes and measured-login close finish, and after that login is restored, the stand queries the configured server service's local systemd journal. The since/until timestamps define a half-open wall-clock window [since, until) using journal timestamps. It covers the stabilization hold and timed probe/close operations, but excludes login restoration. This is timestamp correlation at the measurement-window level, not per-request correlation, and adds no engine metric or API endpoint.
The existing engine emits [CORE][LOOP] slow_cycle at debug level only for cycles where total_ms > 30; zero-batch cycles may contain fewer phase fields. The dashboard shows source/unit and window, observed count, maximum total duration and available phase maxima. These sparse records do not provide all-cycle p50/p99 or a complete main-loop health assessment: no-records does not prove that the loop was healthy. Journal delivery lag or wall-clock changes can leave records outside the queried window, so the observer does not guarantee full coverage. The stand does not enable debug logging automatically; debug configuration or journal routing may hide these records.
Journal permission, service routing, command timeout or read errors produce unavailable with bounded error text; diagnostic collection never fails the trade benchmark. The reader requests up to 1,001 entries and parses at most the latest 1,000. truncated means the returned window exceeded that parsing limit. Reads time out after five seconds and use a four MiB output buffer. Existing artifacts without mainLoop have no observer data and the UI displays it as unavailable.
The dashboard lets the operator select confirmed-event or admission-response latency, and plots open/close p99 against the selected actual account or position count using a linear x-axis, with no smoothing; its table identifies each measurement ordinal and source target stage. A comparison is useful only when snapshot, server build, host resources, profile and operating conditions are comparable. One stage and one run do not establish a stable capacity limit.
Results and artifacts¶
The run writes versioned checkpoint artifacts and latest-name aliases through the normal test-stand artifact system:
load-profile.json: validated profile, expandedmeasurementPlanand environment metadata. The plan lists every real measurement barrier, its account/position counts and its one-basedsourceStage.load-results.json: latest load state, completed measurement points and any available failure diagnostics. In each point,stageis the measurement ordinal andsourceStageidentifies the configured target stage. Older saved points withoutsourceStageare displayed asstagefor compatibility.
Use the existing by-ID artifacts endpoints (the same routes used by regression runs) to list and download them:
GET /api/runs/:id/artifacts
GET /api/runs/:id/artifacts/load-profile.json
GET /api/runs/:id/artifacts/load-results.json
A run can be reopened from the load-only history and its result JSON loaded into the dashboard's comparison panel. The live /api/load state is in memory and resets to idle when the test-stand process restarts; saved run artifacts remain the durable record. Artifact, report, journal, incident and check detail URLs remain the existing /api/runs/:id/... by-ID routes for compatibility; their use does not put the run into the regression history list.
Cancellation and cleanup¶
The dashboard's Stop benchmark action uses POST /api/runs/:id/cancel. It returns the usual cancellation acknowledgement; the run may need time to reach a cancellation check and save final evidence. The load scenario does not automatically delete its created accounts, positions, symbols or group. Restore a clean snapshot before another large run. A snapshot restore changes test-server state and should only target the dedicated benchmark environment.