mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-27 09:33:27 +00:00
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
* perf(web-shell): optimize streaming transcript rendering * test(web-shell): pin streaming fast paths --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
import {
|
|
useCallback,
|
|
useDeferredValue,
|
|
useMemo,
|
|
useSyncExternalStore,
|
|
} from 'react';
|
|
import type {
|
|
DaemonTranscriptBlock,
|
|
DaemonTranscriptBlockChangeSummary,
|
|
} from '@qwen-code/sdk/daemon';
|
|
import {
|
|
useConnection,
|
|
useTranscriptStore,
|
|
} from '@qwen-code/webui/daemon-react-sdk';
|
|
|
|
// Cap transcript re-renders at ~20fps. During streaming every network chunk
|
|
// notifies the store; each render then runs the O(transcript) normalization
|
|
// pass, so rendering at 60fps triples that cost per second while the visible
|
|
// text (itself throttled at 80ms for markdown) cannot change that fast. A
|
|
// 50ms window is still smooth for streaming text.
|
|
const TRANSCRIPT_RENDER_THROTTLE_MS = 50;
|
|
const INPUT_QUIET_WINDOW_MS = 100;
|
|
const MAX_INPUT_DEFERRAL_MS = 250;
|
|
|
|
function hasPendingInput(): boolean {
|
|
const scheduling = (
|
|
navigator as Navigator & {
|
|
scheduling?: { isInputPending?: () => boolean };
|
|
}
|
|
).scheduling;
|
|
return scheduling?.isInputPending?.() === true;
|
|
}
|
|
|
|
interface AnimationFrameTranscriptSnapshot {
|
|
blocks: readonly DaemonTranscriptBlock[];
|
|
blockChangeSummary?: DaemonTranscriptBlockChangeSummary;
|
|
}
|
|
|
|
export function useAnimationFrameTranscriptSnapshot(): AnimationFrameTranscriptSnapshot {
|
|
const store = useTranscriptStore();
|
|
const { sessionId } = useConnection();
|
|
const subscribe = useCallback(
|
|
(notify: () => void) => {
|
|
let frame: number | null = null;
|
|
let lastNotifyTs = Number.NEGATIVE_INFINITY;
|
|
let lastInputTs = Number.NEGATIVE_INFINITY;
|
|
let pendingSinceTs: number | null = null;
|
|
const recordInput = () => {
|
|
lastInputTs = performance.now();
|
|
};
|
|
const dispatchWhenDue = (ts: number) => {
|
|
frame = null;
|
|
if (
|
|
ts - lastNotifyTs >= TRANSCRIPT_RENDER_THROTTLE_MS &&
|
|
((ts - lastInputTs >= INPUT_QUIET_WINDOW_MS && !hasPendingInput()) ||
|
|
(pendingSinceTs !== null &&
|
|
ts - pendingSinceTs >= MAX_INPUT_DEFERRAL_MS))
|
|
) {
|
|
lastNotifyTs = ts;
|
|
pendingSinceTs = null;
|
|
notify();
|
|
} else {
|
|
frame = window.requestAnimationFrame(dispatchWhenDue);
|
|
}
|
|
};
|
|
document.addEventListener('beforeinput', recordInput, true);
|
|
const unsubscribe = store.subscribe(() => {
|
|
if (frame !== null) return;
|
|
pendingSinceTs = performance.now();
|
|
frame = window.requestAnimationFrame(dispatchWhenDue);
|
|
});
|
|
return () => {
|
|
unsubscribe();
|
|
document.removeEventListener('beforeinput', recordInput, true);
|
|
if (frame !== null) {
|
|
window.cancelAnimationFrame(frame);
|
|
}
|
|
};
|
|
},
|
|
[store],
|
|
);
|
|
const getSnapshot = useMemo(() => {
|
|
let cached:
|
|
| {
|
|
blocks: readonly DaemonTranscriptBlock[];
|
|
blockIndexById: Readonly<Record<string, number>>;
|
|
blockChangeSummary: DaemonTranscriptBlockChangeSummary | undefined;
|
|
}
|
|
| undefined;
|
|
return () => {
|
|
const state = store.getSnapshot();
|
|
const blockChangeSummary = store.getBlockChangeSummary?.();
|
|
if (
|
|
!cached ||
|
|
cached.blocks !== state.blocks ||
|
|
cached.blockIndexById !== state.blockIndexById ||
|
|
cached.blockChangeSummary !== blockChangeSummary
|
|
) {
|
|
cached = {
|
|
blocks: state.blocks,
|
|
blockIndexById: state.blockIndexById,
|
|
blockChangeSummary,
|
|
};
|
|
}
|
|
return cached;
|
|
};
|
|
}, [store]);
|
|
const live = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
// Defer transcript re-renders so urgent updates — composer keystrokes,
|
|
// button presses — are never queued behind a streaming frame. The deferred
|
|
// value catches up on the next idle render, so streaming stays smooth while
|
|
// typing stays responsive.
|
|
//
|
|
// Session and block-index identities ride inside the deferred snapshot. The
|
|
// session id rejects a previous session, while the index identity rejects a
|
|
// same-session store reset without blocking ordinary streamed text updates.
|
|
const snapshot = useMemo(() => ({ sessionId, ...live }), [live, sessionId]);
|
|
const deferred = useDeferredValue(snapshot);
|
|
const current =
|
|
deferred.sessionId === sessionId &&
|
|
deferred.blockIndexById === live.blockIndexById
|
|
? deferred
|
|
: live;
|
|
return {
|
|
blocks: current.blocks,
|
|
blockChangeSummary: current.blockChangeSummary,
|
|
};
|
|
}
|