authorgravatar for cjnwob@gmail.comChukwudi Nwobodo <cjnwob@gmail.com> 2026-03-08 06:22:14+00:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-09 14:02:29-07:00
log296eb82c312b3c36237b51870b9a87249f5c6d83
treed6c9a363f14b74031329ee46c179658c6e76c0c4
parent7a377fc5e4d607a475a7d36957689562d5dcccb9

perf: throttle domain event processing with 100ms batch window

During active sessions, every domain event triggered a full syncSnapshot (IPC fetch + state rebuild + React re-render cascade) and sometimes a provider query invalidation. Events fire in rapid bursts during AI turns. Replace per-event processing with a throttle-first pattern: schedule a flush on the first event, absorb subsequent events within a 100ms window, then sync once. Provider query invalidation is batched via a flag. Since syncSnapshot fetches the complete snapshot, no events are lost by skipping intermediate syncs.

1 files changed, 24 insertions(+), 2 deletions(-)

apps/web/src/routes/__root.tsx+24-2
......@@ -187,15 +187,34 @@ function EventRouter() {
187187
188188 void syncSnapshot().catch(() => undefined);
189189
190 // Batch domain events: collect over a short window then sync once.
191 const DOMAIN_EVENT_BATCH_MS = 100;
192 let batchTimer: ReturnType<typeof setTimeout> | null = null;
193 let needsProviderInvalidation = false;
194
195 const flushBatch = () => {
196 batchTimer = null;
197 if (needsProviderInvalidation) {
198 needsProviderInvalidation = false;
199 void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all });
200 }
201 void syncSnapshot();
202 };
203
190204 const unsubDomainEvent = api.orchestration.onDomainEvent((event) => {
191205 if (event.sequence <= latestSequence) {
192206 return;
193207 }
194208 latestSequence = event.sequence;
195209 if (event.type === "thread.turn-diff-completed" || event.type === "thread.reverted") {
196 void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all });
210 needsProviderInvalidation = true;
211 }
212 // Throttle (not debounce): schedule a flush if none is pending.
213 // This guarantees at most one syncSnapshot per batch window
214 // without starving the UI during sustained event bursts.
215 if (batchTimer === null) {
216 batchTimer = setTimeout(flushBatch, DOMAIN_EVENT_BATCH_MS);
197217 }
198 void syncSnapshot();
199218 });
200219 const unsubTerminalEvent = api.terminal.onEvent((event) => {
201220 const hasRunningSubprocess = terminalRunningSubprocessFromEvent(event);
......@@ -280,6 +299,9 @@ function EventRouter() {
280299 });
281300 return () => {
282301 disposed = true;
302 if (batchTimer !== null) {
303 clearTimeout(batchTimer);
304 }
283305 unsubDomainEvent();
284306 unsubTerminalEvent();
285307 unsubWelcome();