Skip to content

buckaroo_state_change requests are not superseded; rapid typing wedges the server #794

Description

@paddymul

Summary

The server processes incoming buckaroo_state_change messages strictly in arrival order with no supersede / debounce. Typing into the search box (one state-change per keystroke) therefore triggers N sequential full pipeline runs even though the client only needs the response to the last one — N-1 of them are dead work.

Empirically on the Boston restaurant data (883K rows, xorq backend):

Test Total time Per-keystroke
5 rapid state_changes typing "PIZZA" 48 s ~9.5 s each
Last state_change response time ~9.5 s (after 5×9.5 = 47.5 s of serialized compute)

Server log confirms 5 sequential entries, each ~9s:

[tperf] state_change: total=9694.9ms
[tperf] state_change: total=9536.2ms
[tperf] state_change: total=9360.5ms
[tperf] state_change: total=8902.4ms
[tperf] state_change: total=8899.0ms

(pandas is faster per-state-change but has the same serialization problem: 5 keystrokes = 5 × ~1.5s = 7.4 s on the test driver.)

Reproduction

ws = await tornado.websocket.websocket_connect("ws://127.0.0.1:8700/ws/boston")
await ws.read_message()  # initial
for term in ("P", "PI", "PIZ", "PIZZ", "PIZZA"):
    ws.write_message(json.dumps({"type":"buckaroo_state_change",
        "new_state":{"quick_command_args":{"search":[term]},
            "post_processing":"","cleaning_method":"","df_display":"main",
            "show_commands":False,"sampled":False,"search_string":term}}))
# 5 frames each block ~9s on xorq — total 45-50s

Why it matters

The grid is unresponsive for the full duration. From the user's perspective, "search" looks like it doesn't work — they typed PIZZA and got stale "P"-filter results for nearly a minute. The rows-first spike (#787) makes this worse, not better — each state_change now produces two frames and the spike's 10ms inter-phase delay is meaningless when the actual compute is 9 seconds.

The scope-merged-SD cache from #785/#789 helps slightly (the raw + clean scope SDs are cached after the first state_change), but the filt-scope SD always misses because the filter changed.

Suggested fix

Pick the simplest cancel-previous strategy:

Drop pending in-flight state-changes when a new one arrives for the same session. Track a pending_state_change_token per session. The handler bumps the token on every incoming state_change; the in-flight compute checks the token after each phase boundary (_compute_processed_result, _get_summary_sd, _populate_sd_cache) and short-circuits if its token is stale.

Mechanically:

def _handle_buckaroo_state_change(self, new_state):
    session = ...
    token = session.state_change_token = session.state_change_token + 1

    # ... existing propagate work ...

    if session.state_change_token != token:
        log.info("state_change superseded; aborting compute session=%s", self.session_id)
        return
    # ... extract + broadcast ...

Coupled with: don't apply the new state to session.buckaroo_state until the response is being broadcast, so an aborted state-change doesn't leave the session in a partially-applied state.

Test plan

  • Failing-test commit: WS test that fires 5 state_changes back-to-back, waits 15 s total, asserts the final response time is under 12 s. Should fail today on xorq backend (would be ~48 s total) and on pandas (~7 s sequential).
  • Fix commit: token + abort check at phase boundaries.
  • Regression coverage: existing test_state_change_* in test_load_expr.py continue to pass; add one new test for "5 state_changes resolve in the time of 1" with the xorq backend.

Related

  • Hard upper bound on per-state-change compute is being addressed by other work (spike(rows-first): two-message state_change protocol behind env gate #787 spike, the per-column query loop in XorqStatPipeline). Supersede is orthogonal — it stops unnecessary work, doesn't speed up the necessary compute.
  • Discovered during a cross-backend stress test (pandas / lazy-polars / xorq).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions