Repository navigation
feat(stats): stats_request runs units for a time budget, with per-client cursors and the final assignment (rows-first s5) - #1028
Closed
paddymul wants to merge 18 commits into
Conversation
…the _handle_widget_change split (rows-first s1) A default-tier XorqBuckarooWidget and BuckarooWidget publish df_data_dict, then df_display_args, then the rest of the widget_args_tuple observers on a search change, and merged_sd carries the full stat set. These pass on main and pin the behaviour the split must keep. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-handler fields (rows-first s1) A schema-tier XorqServerDataflow should match full stats on pinned_rows, data_key, summary_stats_key and (except the stats-derived minWidth) column_config, issue no data query besides the cached count, and keep init_sd hints and sorted windows working. A pending state must write nothing under a full-tier cache key, and a later full assignment must reach merged_sd for the raw, clean and filt scopes. assemble_merged_sd must equal merged_sd, and _handle_widget_change must be built from separately callable all_stats and display-args builders. /load_expr and /reload_expr accept stats_tier and stats_delivery, replay them on reload, and keep them out of the warm short-circuit's has_config tuple. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… s1)
Add a dataflow-level stats_tier ("full" default, "schema"). The xorq schema
tier builds identity and typing for every column from the expression's
schema, with no data query beyond the cached row count, so a dataflow
constructs in milliseconds rather than the stats' hundreds.
The tier is part of _scope_cache_key, so a schema entry is never read as a
full one, and _populate_sd_cache stores summary_sd under the filt key only
if it was computed for the current frame, klass list and tier. add_analysis
no longer builds DFStatsClass outside the hook when the tier is not full.
The merged_sd observer body is extracted as the pure assemble_merged_sd,
and _handle_widget_change is split into _build_df_data_dict and
_build_df_display_args.
/load_expr and /reload_expr accept stats_tier and stats_delivery, stored on
the session beside dataflow_kwargs and replayed on reload. They stay out of
the has_config tuple; the warm short-circuit compares the stored pair.
stats_delivery="deferred" builds the schema-tier dataflow and publishes it.
Defaults (full, inline) leave behaviour unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ation test (rows-first s1) The Max Versions jobs resolve pandas 3, which reports a string column's dtype as 'str' where pandas 2 says 'object'. The characterization test asserted 'object'. Verified in a Max Versions environment (pandas 3.0.6, polars 1.44.2, xorq 0.4.5): the unit suite passes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g on a skipped column (rows-first s1) A column in skip_stat_columns gets only name, dtype and length from the full-tier pipeline, so its _type comes from init_sd. The schema tier layers the schema-derived _type and is_* keys over it, so an int64 column that init_sd types as float merges as integer and renders with zero fraction digits instead of the float displayer init_sd asked for. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ows-first s1) _get_schema_sd never read skip_stat_columns, so a skipped column's schema-derived _type and is_* keys overrode init_sd's _type once merged. The full tier gives a skipped column only name, dtype and length. The schema tier now does the same, so init_sd's _type decides the displayer at both tiers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er (rows-first s2) ServerDataflow and PolarsServerDataflow with stats_tier="schema" should publish the display state full stats give (column_config without stats-derived keys, pinned_rows including a host-supplied one, data_key, summary_stats_key) with no stat computed on the data, still apply init_sd, serve sorted windows, take a later full assignment into merged_sd for every scope, and assemble to the same sd as merged_sd. Both backends run through the same parametrized class, plus a pandas test that pins how an object column is typed from its dtype. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ServerDataflow and PolarsServerDataflow now implement the _get_schema_sd hook,
so stats_tier="schema" builds a dataflow that types every column from its dtype
and runs no stat on the data. schema_sd (stat_pipeline.py) builds the sd the way
process_df shapes it: an empty frame gives {}, a skipped column keeps only its
names. pandas applies the existing typing_stats to a zero-row slice and derives
_type through the _type stat; polars factors pl_dtype_typing out of
pl_typing_stats and feeds it the dtype.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tats_request (rows-first s3)
On a deferred /load_expr session a stats_request {stats_gen, scope} should
return a stats_update with the matching stats_gen whose inline wide payload
equals the all_stats an inline session sends, a stale stats_gen should get
stats_aborted and run no query, and /load_expr and /reload_expr should bump the
generation. df_meta.stats should be injected on every frame and survive a
dataflow-field change, which returns the session to the schema tier. With a
caps client and a legacy client on one session, the legacy client should keep
getting complete messages through the websocket broadcast, the /load_expr,
/reload_expr, /load and /load_compare pushes and the highlight overlay, while
the caps client gets a stats-free frame and then pulls a stats_update. A spy
telemetry sink should see a stats.request span.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eration edge cases (rows-first s3) Four more cases for the stats wire format, kept in their own commit so each is seen failing on CI before the implementation lands. Returning to a state whose stats were completed once is answered from summary_stats_cache with no query. Completing the stats keeps the session's component_config on the refreshed display config. A warm /load_expr, which rebuilds nothing, leaves stats_gen alone. A stats_request on a session with no data is answered with stats_aborted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed sessions (rows-first s3)
A client that advertises ?caps=stats_update gets a stats-free initial_state on a
deferred /load_expr session (df_meta.stats.status "pending") and pulls the stats
with stats_request {stats_gen, scope}. The reply is a stats_update carrying the
dataflow's all_stats as an inline wide envelope, or stats_aborted when the
generation is stale. The request is the whole run: one synchronous call that
computes the full stats, writes the full-tier summary_stats_cache entry, assigns
summary_sd and refreshes the session snapshot through one helper.
stats_gen is a server-owned counter bumped by every load handler and by a state
change that touches a dataflow field, which also returns a deferred session to
the schema tier. df_meta.stats is injected by build_state_message from the
session, since the dataflow rebuilds df_meta wholesale. Every send site goes
through build_state_message_for, so a client without the capability still gets
complete messages (its missing stats run synchronously first); broadcast_state
replaces the five copies of the send loop and sends to capable clients first.
The stats_request branch binds the session's telemetry sink and emits a
stats.request span.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s (rows-first s4) Both pass on the current code. process_df is written out as the process_column loop it is, and each xorq histogram query's snapshot-cache key is compared with the key of a query built in the test, so the refactor into resumable units that follows can be checked against something that does not go through the units. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tore (rows-first s4) Each stats class gets plan(state) and run(unit, acc): the fragments of the planned units, assembled through assemble_merged_sd, must equal the full-stats merged_sd on pandas, polars and xorq with init_sd, cleaning and overrides; a column group returns only its columns; skip_stat_columns columns get no unit; the xorq batch can be split by column chunk behind a default-off flag and is refused for anything but a plain parquet scan; the histogram cache keys stay put. A StatRun held on the session, keyed by (stats_gen, scope), keeps an append-only fragment list and accumulator, runs a unit only when asked, is dropped when stats_gen changes, and each connection keeps its own cursor. test_the_batch_is_one_query_by_default passes on the current code: it pins the default the flag must not change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
StatPipeline and XorqStatPipeline get plan(state), new_accumulator(state) and run(unit, acc); process_df and process_table are those three in order, so the inline path and a caller running units one at a time compute the same stats. pandas and polars plan one unit per non-skipped column. xorq plans the scalar batch (which also yields histogram_bins), then one histogram query per column, visible columns first. The batch can be cut into column chunks sized by cells when a host sets stat_chunk_cells, and only for a plain parquet scan: any other source falls back to the single batch and logs why. A StatRun, kept on the session by (stats_gen, scope), holds the planned units, an append-only fragment list and the accumulator. It has no thread, timer or callback, begin_stats_generation drops it, and each WebSocket connection keeps its own StatCursor into the list. CustomizableDataflow.build_stats is the one place a stats class is built, with run=False for plan/run callers. Two of the new tests are corrected here: the default-path test now ignores the PERVERSE_DF self-check units, and the xorq comparisons round floats to nine digits because a parallel aggregate adds its partial sums in varying order. The new-module imports in the tests move to module level. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… once (rows-first s4)
A column group, the priority columns and StatRun's prefer hint accept
original or rewritten (a, b, c) names and read each name in both. When an
original name equals another column's rewritten name, a group returns the
columns of both, priority ranks both first, and prefer picks the unit of
whichever comes first in the frame.
On a frame whose columns are c, b, a (rewritten a, b, c):
- StatState(df, columns=('c',)) plans the units of c and a.
- priority=('a',) plans c before a.
- StatRun.next_unit(prefer=('a',)) picks the unit of c.
The new tests also pin the namespace argument the fix adds, so a client that
holds the rewritten names can ask for exactly those: StatState(namespace=)
and StatRun.next_unit/run_next(namespace=), with "any" (the default),
"original" and "rewritten".
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rst s4) A column group, StatState.priority and StatRun's prefer hint matched a name against both the original and the rewritten (a, b, c) column names. When an original name equals another column's rewritten name, one name picked two columns: a group returned columns that were not asked for, and prefer ran the unit of whichever column came first in the frame. resolve_names reads each name once. In "any", the default and what the code did for names that do not collide, a name that is an original column name picks that column and only a name that is not one is read as a rewritten name. StatState(namespace=) and StatRun.next_unit/run_next(namespace=) also take "original" and "rewritten", so a caller that holds the client's rewritten names says so and gets exactly those columns. Priority and prefer names are resolved against every column of the frame, not only the columns in the group, so a name picks the same column whatever group is asked for. prioritized now takes the state, since it needs the frame's columns and the namespace to read the priority names. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t cursors and the final assignment (rows-first s5) Covers the request branch that runs units for a time budget (at least one, one cold unit per budget), the per-handler cursors over a shared StatRun, the final assignment that frees the run and carries the rebuilt df_display_args, a legacy client finishing a run in progress, a concurrent infinite_request between units, the end-to-end order of first initial_state, rows and stats, /reload_expr mid-run, and the stats.unit and firstpull.stats_total spans. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contributor
📦 TestPyPI package publishedpip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37188764309or with uv: uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37188764309MCP server for Claude Codeclaude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.9.dev37188764309" --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo-table📖 Docs preview🎨 Storybook preview |
…nt cursors and the final assignment (rows-first s5) A stats_request with incremental: true runs the units of the session's StatRun for about STATS_BUDGET_S (at least one) and answers with the fragments the asking client has not seen, read through the handler's own StatCursor. The request that runs the last unit does the final assignment in complete_stats: the full-tier summary_stats_cache entry, summary_sd, the session snapshot and the status in one step, the run freed, and the final reply carries the rebuilt df_display_args when its digest differs from the one the client holds. Without the field a request is still the whole run, now finishing the run's remaining units when a client has made progress. A legacy client's complete frame does the same. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Oct 4, 2026
Closed
Draft
paddymul
changed the base branch from
main
to
adr-002-rows-first-stats-delivery
October 6, 2026 14:40
Collaborator
Author
|
Closing. This is the client-pull loop (incremental One piece carries over: attaching the rebuilt |
paddymul
added a commit
that referenced
this pull request
Oct 6, 2026
…d the compare reset under a stats policy (rows-first p33) Cases found untested after the first tests commit: a scalar tier named with inline delivery builds a schema dataflow, and /load_compare clears the policy a session held. Rebased off the closed unit PRs (#1026, #1028): the assertions on unit tiers and on df_display_args in the final reply are dropped with the code they tested. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
paddymul
added a commit
that referenced
this pull request
Oct 6, 2026
…irst p33) /load_expr and /reload_expr accept stats_tier auto, full, scalar or schema, stored with the pair and kept out of has_config, so the warm short-circuit holds. When the dataflow is built at the schema tier the handler resolves the policy (resolve_stats_policy) from the count it already has, stores it on the session and starts the stats generation from it: a target below full is not_computed with the policy's reason. df_meta.stats reports the policy as tier_target and estimate, plus auto_request, requestable, omitted_keys, approx_keys and demand_columns where they differ from their documented defaults. It is applied at WebSocket open only for a client that sends ?caps=stats_update,stats_ondemand. A client with stats_update only is told the session is pending and pulls the stats, and a client with no caps gets them at connect, as for any deferred session. A tier the host named (scalar, schema) reaches every client as before. A session on an explicit stats_tier full within the ceiling sends the message it always has. /load keeps resolving to full: it does not read the field and clears a policy left on the session. Rebased onto #1024 without the unit PRs (#1026, #1028): stats_request is the whole run of #1024, so handle_stats_request now takes the client to decide stats_to_pull, and the incremental and display-config parts are gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This branch was successfully deployed
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.
Stacked on #1026 (
feat/rowsfirst-s4-stat-units), which is stacked on #1024, #1022 and #1021. This PR is based onmainso the repo's Checks workflow runs on it (itspull_requesttrigger usesbranches: "*", which does not match a base branch containing a slash). The diff therefore includes the commits of #1021, #1022, #1024 and #1026 until they merge. The commits of this phase are2a73c8ed(failing tests) andad26fdf4(implementation); read only those two (git diff origin/feat/rowsfirst-s4-stat-units...HEAD).Problem
stats_request(#1024) is a whole run: one synchronous call that computes every stat before it replies. On a long xorq batch it holds the loop for as long as that takes, and no client sees a stat before the last one is done. #1026 cut a run into units that can be run one at a time, and added the per-sessionStatRunand the per-connectionStatCursor, but nothing in the server calls them. The request branch has to run units for a bounded time per request, answer each client with the fragments it has not seen, and do the final assignment when the last unit completes.Phase and plan references
Rows-first s5, from
buckaroo2-reports/plans/: plan 2 (02-rows-first-xorq-and-lazy-polars.md) section 7 "Phase 3" with sections 4.1 (Request, Final assignment) and 4.3 (reload, telemetry), and plan 1 (01-rows-first-stats-separate-plumbing.md) section 8 (telemetry).Approach
stats_request.{stats_gen, scope, columns?}is still the whole run and answers as in feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024.{stats_gen, scope, columns?, incremental: true}is a time-boxed step. A value other than a boolean forincrementalisstats_abortedwith reasonbad_request, and is not run as a whole run.stats_wire._serve_units). It takes the session'sStatRunfor the current generation (start_stat_run, planning only). If this client's cursor is behind the run, the reply carries the fragments it has not seen and no unit runs. Otherwiserun_unitsruns units untilSTATS_BUDGET_S(75 ms) is spent, at least one, so a unit that is a query is the only one of its request and units that are snapshot-cache hits share a request.columnsare the grid's column names (a, b, c) and put the units that cover them first. The reply isstats_update {stats_gen, scope, tier, final: false, remaining, payload, elapsed_ms}.payloadis an inline wideDFEnvelopeof the columns the unseen fragments cover, assembled the waymerged_sdis (CustomizableDataflow._assemble_merged_sd(running_sd), the body of themerged_sdobserver with the run's accumulating sd standing in for the scopes that share the filt chain), soinit_sd, a processing step's sd and thecleaned_*andfiltered_*layers apply. A client merges it key by key.complete_stats, shared by every path). When the last unit has run, in whichever request ran it: write the sd intosummary_stats_cacheunder the full-tier key, then assignsummary_sd; refresh the session snapshot throughrefresh_session_snapshotand set the status tocompletein the same step; free the run. A state whose full sd is already in the cache runs no unit. The reply that follows isfinal: true,remaining: 0, carries the completeall_stats, and carries the rebuiltdf_display_argswhen its digest differs from the one the client holds (a float column'sminWidthis 114 with full stats and 78 without in the test fixture). The digest of the config a stats-free frame carried is kept on the handler (display_args_hash, set bybuild_state_message_for). The client's own search highlight is applied to the config it gets, since the config replaces the one that carries it.StatRunfor the generation,complete_statsis one_get_summary_sdcall, as before: it runs the same units in the same order, and it is the hook the xorq dataflow customizes. With a run that a client has made progress on, it runs the units left and assigns the run's results, so no unit runs twice. A legacy client's complete frame does that too.stats_statuserror, reasonstats_failed, runs dropped). It is not retried by the next request, and the next generation starts clean. A run that cannot be planned (a post-processor that fails leaves an error frame, which the xorq stats class cannot plan) falls back to the whole-run path, with a warning.DataStreamHandler._handle_stats_requestalready bindssession.tele_sink. New: astats.unitspan per unit (session,stats_gen,unit,phase,cost,columns);firstpull.stats_totalonce per completed run, now withunits,run_secsand, on xorq, thecache_status,cache_hits,cache_misses,cache_secs,cache_snapshots,cache_bytesandcache_write_errorsthatfirstpull.summary_statscarries for a whole run; thestats.requestspan gainsfinal,remainingandunits.StatRunsumselapsed_sover its units./reload_exprbumpsstats_genand drops the run (begin_stats_generation, from feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024 and feat(stats): resumable stat units and the StatRun store (rows-first s4) #1026); a request for the old generation getsstats_abortedwith no query, and the new generation starts from its first unit.What changes
buckaroo/server/stats_wire.py:STATS_BUDGET_S,run_units,partial_payload,display_args_hash,highlighted_display_args(the overlay's loop, moved here so the final reply can use it),assign_full_stats(dataflow, computed=None),complete_statsfinishing a run,_serve_units,handle_stats_request(session, msg, client=None), thebad_requestabort, and the digest kept inbuild_state_message_for.buckaroo/server/stat_run.py:StatRun.elapsed_sandStatRun.errs().buckaroo/dataflow/dataflow.py:_assemble_merged_sd(running_sd=None); the_merged_sdobserver calls it.buckaroo/server/websocket_handler.py: passes the handler tohandle_stats_request, initializesdisplay_args_hash, and_send_highlight_overlayuseshighlighted_display_args.Tests
Two commits:
2a73c8ed(failing tests) andad26fdf4(implementation).test_load_expr.py,TestStatsWire(xorq, over WebSocket): one unit per request and the complete state at the end; the last unit assigns the stats and frees the run; a long budget runs every unit in one request; thecolumnshint orders units; a stale generation mid-run; client B opening after client A finished; two interleaved clients end with full stats and run no unit twice; the final reply carries the rebuiltdf_display_argswhen the digest differs, carries none when it does not (an int and string table), and keeps the client's highlight; a filtered state ends as the inline session's does; a legacy client connecting mid-run finishes only the units left; a request without the flag finishes a run in progress; a concurrentinfinite_requestis served between units, with every request under plan 1's 250 ms ceiling (a fake unit that sleeps 40 ms); the firstinitial_stateand the first rows precede every data stat query and rows are served before stats complete;/reload_exprmid-run; thestats.unit,firstpull.stats_totalandstats.requestspans on the session sink; the completion span's cache outcome (a miss, then a hit after a reload on acache_storage_path); a unit that raises; a non-boolean flag.test_data_loading_polars.py(pandas and polars):TestRunUnits(units that cost little run together, a request stops once the budget is spent, a unit over the budget is alone in its request, a cold unit after warm ones ends its request, one unit runs whatever the budget, no budget runs every unit, the hint in rewritten names, a raising unit, the spans,elapsed_s,errs()), run on real units with a fake clock and a fake cost;TestPartialPayloadandTestAssembledMergedSd(the payload of every fragment equals the dataflow'sall_stats, only the covered columns,filtered_*keys for a search,init_sdwins);TestHighlightedDisplayArgsandTestDisplayArgsHash.columnshint that is not a list of names is only a hint.next_unittakes plan order among the preferred columns, not the order ofprefer(test_the_columns_hint_is_read_in_the_client_s_rewritten_namesassumed the latter), and a decodedall_statsrow has alevel_0key that the column sets must skip, asStatsChannel's merge does. One test also dropped a legacy-client check that could not hold (every send to a legacy client completes the session, so with one connected the session is never pending after a push).On
2a73c8ed(tests only) all ninePython / Testjobs failed (3.11 to 3.14, Max Versions 3.11 to 3.14, Windows), all 28 checks completed. Locally 63 of the new tests fail on the earlier code: 43 intest_data_loading_polars.py, each on a missing attribute, and 20 inTestStatsWire, each on an assertion about the reply (the earlier code ignoresincrementaland answers with the whole run) or a missing key. CI logs were not read (the REST log API is rate-limited for this account), so the reasons rest on the local run of the same commit. The 174 tests that were in the two files before pass unchanged.CI on
ad26fdf4: all 28 checks completed, 27 succeeded (one of them the Read the Docs status) anddeploywas skipped. That includes all ninePython / Testjobs (3.11 to 3.14, Max Versions 3.11 to 3.14, Windows),Python / Lint,Python / Typecheck, the JS job, the wheel build and the Playwright jobs.Locally, on
ad26fdf4, the full unit suite (pytest ./tests/unit -m "not slow") gives 1459 passed and 5 skipped, the 1393 of #1026 plus 66 new tests, and the same 1459 in an environment resolved with the newest versions and pandas 3 (pandas 3.0.6, polars 1.44.2, xorq 0.4.5, numpy 2.5.3).basedpyrighton the scoped files reports 0 errors and 0 warnings. Nineteen deliberate regressions of the implementation each make at least one test fail: the budget ignored, the budget checked before the first unit, no catch-up guard,complete_statsignoring the run, the display config never attached, always attached, the run not freed, the partial payload not restricted to the covered columns, nostats.unitspan, a failure that keeps the runs, no flag validation, no cache check before planning, the running sd not used for the raw scope, the final assignment recomputing, the digest not recorded, thecolumnshint ignored,elapsed_snot summed, a missingunitsattribute onstats.request, and the wrong tier on a partial reply.Measurements
Each incremental request timed through
handle_stats_request(no WebSocket), from a schema-tier session to the final reply, on the 27-column synthetic parquet fixture of #1026 (4 ints, 6 floats with nulls, 2 low-cardinality ints, 8 strings of 5 to 5,000 distinct values, 3 booleans, 4 timestamps), the default 75 ms budget, an Apple M4 Pro with other agents running. The final request includes its own units, the final assignment and the snapshot refresh.stat_chunk_cells=1Mstat_chunk_cells=12M(6-column chunks)A request is as long as its longest unit plus the partial payload, so the bound a waiting
infinite_requestsees is the unit. With the batch whole at 2M rows that is 0.86 s; with 6-column chunks it is 0.25 s, at plan 1's proposed 250 ms ceiling and a few milliseconds over it. The final request's own work (the assignment and the cascade it fires) is small here, 30 to 80 ms with its units; a state with a cleaning op or a search computes more in it (see "Not in this PR").Why default behaviour is unchanged
?caps=stats_updateto a session loaded withstats_delivery="deferred"can send astats_requestthat matters, and onlyincremental: trueruns units for a budget. An inline session is never pending, so none of this runs for it. Splitting themerged_sdobserver into_assemble_merged_sdchanges no value: the observer calls it with no running sd, and every existing test ofmerged_sdpasses unchanged.stats_update {type, stats_gen, scope, tier, final: true, payload, elapsed_ms}, with two additive fields,remaining: 0and, for a client whose pending frame carried a different display config,df_display_args. A client that does not read them ignores them._send_highlight_overlaybuilds the same message from the same loop, which now lives inhighlighted_display_args.Deviations from the plan
stats_requestrun units for 50 to 100 ms. Here onlyincremental: truedoes, because the whole-run request exists in feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024 with its own tests and clients, and the maintainer's rule is that default behaviour does not change. The unflagged request is the whole run, as the phase asks ("the whole-run request becomes run all units"): it runs every unit still to run.StatRunthe whole run is one_get_summary_sdcall (the same units in the same order, shown by feat(stats): resumable stat units and the StatRun store (rows-first s4) #1026) instead of a loop over a run's units._get_summary_sdis the hook the xorq dataflow overrides (the pandas error-frame result, the debug-mode raise), and the feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024 tests patch it. With a run that has progress it runs the units left.all_stats, not only what lies beyond the client's cursor. The final assignment also fills layers that no fragment carries (the other scopes'cleaned_*andfiltered_*keys, aninit_sdor processing sd override), and a client that missed a frame then still ends complete.STATS_BUDGET_S = 75 ms). The plan's 250 ms ceiling is a bound on the longest request, and a request cannot be cut short inside a unit, so the ceiling is what a test asserts, not something the server enforces.columnsare the grid's rewritten names, and the first unit that covers any of them runs first (plan order among them, asStatRun.next_unitdoes), not in the order the client lists them.stats_abortedgains the reasonbad_request.firstpull.stats_totalis not emitted asfirstpull.summary_stats. That span's duration is the time spent computing stats, which a run spread over several requests has no single value for. The attributes are the same and sit onfirstpull.stats_total, whose duration is the request that completed the run, withrun_secsfor the sum of the units./load_exprsnapshot copy (the third site the plan names) is not replaced byrefresh_session_snapshot. The helper applies the session's storedcomponent_config, and that copy applies only the one in the POST, so replacing it would change what a re-POST that omits the field sends. feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024 left it for the same reason.outcomeattribute ofstats.request, as in feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024.Not in this PR
summary_sdfills the other scopes' cache entries through the cascade. For xorq that is a full stats pass per scope in the request that finishes the run. Units for those scopes belong with plan 3's scoped runs.Stack
#1021, #1022, #1024 and #1026 are underneath; their commits are in this diff until they merge. Commits of this phase:
2a73c8ed(failing tests) andad26fdf4(implementation).🤖 Generated with Claude Code