Repository navigation
perf: keep many-deal paths at milliseconds - #1141
Merged
Merged
Conversation
A heavy install (ten cores, thousands of deals in the 72-hour window) raised the question whether the Telegram notification read, the per-picture day sums and the report totals stay cheap. Add ignored probes that build that load on synthetic data and time each path, so the numbers can be re-measured after any change to them. Measured within budget: the 72-hour read over 5000 closes and ten cores takes about 29 ms every 2 s, twenty pictures over 2000 closes about 0.5 ms, and the Telegram reports over 20000 deals 21-63 ms. No production code changes.
A Report read sorted by close date (newest first) over a large replica scanned and sorted every matching row with all its wide columns before LIMIT cut it to the displayed page: 31 ms at 20000 deals and 82 ms at 100000. Select the ordered, per-source-limited row keys first, then fetch the wide rows by rowid and sort only that bounded set. The inner key pass keeps the old WHERE, ORDER BY with its primary-key tie-break and LIMIT, so the same row set and order come back; ascending, computed and coin-seek reads keep their old SQL. Now 7.7 ms at 20000 and 40 ms at 100000. Add an ignored scale probe of the Report reads, a tied-row parity test of the old and new statements, and run the coin-index seek test that the current IN predicate already satisfies.
Every feed wake drains all cores' order batches on the UI thread in one
pass. With ten cores holding 5000 retained closed orders and 200 open
orders each, that pass took 3.4 ms for order lines and 3.1 ms for table
messages: a fifth of a 60 Hz frame spent rebuilding state that mostly
did not change.
- Cache each market's open-order draw membership and rebuild it only
when an order is inserted, revived or closed, or the retained set
changes size, instead of rescanning every retained order per batch.
- Skip the auto-fit range rebuild for batches that move no line price.
- Drop consumed order batches on one named background thread
("order-retirement", bounded queue) instead of on the UI thread.
The ten-core drain is now 1.5 ms for lines and 1.4 ms for tables, p95
under 2 ms. Rows, order, revisions and auto-fit ranges are unchanged;
heavy-core scale probes and parity tests cover the paths.
Valuation ran one statement per report change: a burst of N changed deals produced N outbox inserts, N single-row report reads and repeated per-row rate and value statements, and reading archived order traces issued one query per deal plus one per answer (1802 statements for 1000 traces). Unresolved deals waiting for a rate sat in an unbounded in-memory map that was rescanned against every blocked rate minute. - Stage report page changes into the outbox with bounded multi-row inserts inside the same writer transaction. - Load rows, cached rates and values in bounded batches; every batch stays well under SQLite's variable limit. - Keep deferred deals in a TEMP table on the worker connection, indexed by their retry time, and take bounded due batches from it. - Read archived trace answers and lines in bounded batches (1802 statements down to 4). Stored values, rates, provenance, modes and the reconciliation order are unchanged; scalar-versus-batched parity tests cover them.
normalized_quote_breakdown_matches_unfenced_sql failed in full parallel runs, also on the base commit, and passed alone. It pins current rates but did not hold the valuation cache health guard its neighbours hold, so a concurrent corruption test could flip cache health between its two reads and one side came back without valuation. Take the existing test_health_guard() before pinning rates.
…hart A chart with a long retained tape spent tens of milliseconds on the UI thread whenever trade bubbles refolded: 77 ms per refold at 100000 prints. The debug build that is run day to day compiled moon-chart at opt-level 0, because the dev profile optimizes only non-workspace dependencies and moon-core. - Compile moon-chart at opt-level 2 in the dev profile, as moon-core already is. The bubble pipeline drops from 55 to 11 ms and ten-pane order geometry from 14 to 8 ms. - Share one price lookup between both sides, reuse expired slots without a second hash, cache packed price/time sort keys and pick percentile ranks by selection instead of a full sort. - Prepare bubble folds, native markers and the release of superseded buffers on a two-worker background executor. The previous fold stays on screen; a result whose settings, layout, epoch, history generation or visible range no longer match is dropped. The UI thread now spends about 0.6 ms per refold. - Use the same lateness-bounded tick lookup on wgpu as on DX11/Metal. - Box Orders rows so sorting traced orders moves less memory. Bubbles, markers, Orders rows and their order match the synchronous path; parity tests compare against frozen copies of the old code.
Opening a Trade window, or clicking another trade in an open one, stable-sorted the whole period's history on the UI thread to pick the twenty nearest neighbours: up to 39 ms on open and 26 ms on a re-click at 50000 trades. Select the neighbours on a background executor and hand the UI the prepared rows; the whole period stays retained, as before. A refresh keeps the window's current focus. The view holds one cancellable selection task and a generation counter, so an older selection that finishes after a newer click is dropped instead of replacing it. Neighbour order, ties and rows without a uid are unchanged; tests compare against a frozen copy of the old selector and cover out-of-order completions.
A core's history could stay without dollar values until the app was restarted. When a core's clock offset is adopted, its valuation partition is wiped (RescanCore) and only a reconciliation walk can revalue the rows, because no per-row events follow. The walk was re-armed only when it was idle; at startup it is usually running over the whole history while cores connect, so it carried on from its cursor and never came back to the wiped rows it had already passed. Restart a running walk from the first source on RescanCore and PurgeLegacy, as an idle one already was; Row and Delete events still leave the walk alone. A regression test wipes a core behind the cursor and checks its rows end up valued. Also attempt RELEASE after a failed ROLLBACK TO of the ready-values savepoint, returning the rollback error, so a rollback failure no longer leaves the savepoint open as the connection's transaction.
The Analytics window is one GPUI entity with no shell throttle, so every repaint re-ran its render-time passes. The Entry/Exit tab scanned the whole deal table three times for its KPI line plus once for the delta summary on each render, a changed reload hashed and cloned all judged rows on the UI thread, and the Screener rebuilt and re-sorted every market row (9.2 ms at 8000 rows) on the UI thread. - Cache the tuner's render population and delta summaries keyed on the row revision, locale and the time window in which a verdict can change by itself; KPI and delta reads now cost under 1 us. - Prepare the reload scope and judged-row snapshots on a background executor; stale results are dropped by sequence and skip key. - Sort Screener rows by index and reuse unfiltered market counts; build unfiltered market snapshots in the background from shared rows and publish them atomically. Source, scope, text, volume and sort apply at once to the cached rows and again at publication. The UI side now takes 0.3-1.4 ms. Figures, labels, rows and their order are unchanged; parity tests and a cold-locale regression cover the caches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
With many deals (ten cores, 20k-100k report rows, 5000 retained closed and 200 open orders per core) several paths spent tens of milliseconds on the UI thread or issued one SQL statement per row. Each candidate was measured first on synthetic data at that scale; only paths over a 1-2 ms UI budget (or doing O(N) statements where a bounded batch does) were changed. Output stays identical: parity tests compare against frozen copies of the old code.
Notable:
moon-chartat opt-level 0; it now gets opt-level 2 likemoon-core.RescanCore/PurgeLegacy.normalized_quote_breakdown_matches_unfenced_sqlraced the cache-health tests in parallel runs; it now holds the existing health guard.How to verify
cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings,make test(nextest workspace + doc tests) - green locally.#[ignore]: e.g.cargo test -p moon-core --lib scale_probe -- --ignored --nocapture.