Unverified 提交 4d7fa151 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

perf: batch and throttle IPC app output to prevent log flooding (#3035)

## Summary - Buffer stdout/stderr messages from child processes and flush them every 100ms as a single batched IPC event (`app:output-batch`), reducing IPC traffic, array allocations, and React re-renders when apps emit high-volume logs - Keep `input-requested` messages on the immediate `app:output` channel for responsive UX - Renderer processes batched events with a single `setConsoleEntries` state update instead of one per message ## Test plan - [ ] Run an app that emits high-volume logs (e.g., `console.log` in a loop) and verify the UI remains responsive - [ ] Verify app console still shows all log output correctly - [ ] Verify interactive prompts (y/n) still appear immediately - [ ] Verify proxy URL detection and preview panel still work - [ ] Verify HMR updates still trigger iframe refresh 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/3035" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
上级 300f0ac2
......@@ -101,6 +101,18 @@ queryClient.invalidateQueries({ queryKey: queryKeys.apps.all });
**Adding new keys:** Add entries to the appropriate domain in `queryKeys.ts`. Follow the existing pattern with `all` for the base key and factory functions using object parameters for parameterized keys.
## High-volume event batching
When an IPC event can fire at very high frequency (e.g., stdout/stderr from child processes), **batch messages and flush on a timer** instead of sending each message individually. This prevents IPC channel saturation, excessive array allocations in the renderer, and unnecessary React re-renders.
**Pattern** (see `app_handlers.ts` `enqueueAppOutput`/`flushAllAppOutputs`):
- Buffer outgoing events in a `Map<WebContents, Payload[]>`.
- Start a `setTimeout` on first enqueue; flush all buffered messages as a single batch event (e.g., `app:output-batch`) when the timer fires (100ms default).
- Flush immediately on process exit so no messages are lost.
- Keep latency-sensitive events (e.g., `input-requested`) on an immediate, unbatched channel.
- On the renderer side, process the entire batch array in a single state update (`setConsoleEntries(prev => [...prev, ...newEntries])`) instead of one update per message.
## Streaming chunk optimizations
The `chat:response:chunk` event supports two modes:
......
......@@ -70,12 +70,22 @@ export function useAppOutputSubscription() {
console.error("Failed to respond to app input:", error);
}
});
return; // Don't add to regular output
return null; // Don't add to regular output
}
// Add to console entries
// Use "server" type for stdout/stderr to match the backend log store
// (app_handlers.ts stores these as type: "server")
// Handle HMR updates
if (
output.message.includes("hmr update") &&
output.message.includes("[vite]")
) {
onHotModuleReload();
}
// Process proxy server output
processProxyServerOutput(output);
// Only send client-error logs to central store
// Server logs (stdout/stderr) are already stored in the main process
const logEntry = {
level:
output.type === "stderr" || output.type === "client-error"
......@@ -87,39 +97,52 @@ export function useAppOutputSubscription() {
timestamp: output.timestamp ?? Date.now(),
};
// Only send client-error logs to central store
// Server logs (stdout/stderr) are already stored in the main process
if (output.type === "client-error") {
ipc.misc.addLog(logEntry);
}
// Also update UI state
setConsoleEntries((prev) => [...prev, logEntry]);
// Process proxy server output
processProxyServerOutput(output);
return logEntry;
},
[setConsoleEntries, processProxyServerOutput],
[processProxyServerOutput, onHotModuleReload],
);
// Subscribe to app output events from main process
// Subscribe to immediate app output events (input-requested)
useEffect(() => {
const unsubscribe = ipc.events.misc.onAppOutput((output) => {
// Only process events for the currently selected app
if (appId !== null && output.appId === appId) {
// Handle HMR updates
if (
output.message.includes("hmr update") &&
output.message.includes("[vite]")
) {
onHotModuleReload();
const entry = processAppOutput(output);
if (entry) {
setConsoleEntries((prev) => [...prev, entry]);
}
}
});
return unsubscribe;
}, [appId, processAppOutput, setConsoleEntries]);
// Subscribe to batched app output events (stdout/stderr)
useEffect(() => {
const unsubscribe = ipc.events.misc.onAppOutputBatch((outputs) => {
const newEntries: ReturnType<typeof processAppOutput>[] = [];
for (const output of outputs) {
if (appId !== null && output.appId === appId) {
const entry = processAppOutput(output);
if (entry) {
newEntries.push(entry);
}
}
processAppOutput(output);
}
if (newEntries.length > 0) {
setConsoleEntries((prev) => [
...prev,
...(newEntries as NonNullable<(typeof newEntries)[number]>[]),
]);
}
});
return unsubscribe;
}, [appId, processAppOutput, onHotModuleReload]);
}, [appId, processAppOutput, setConsoleEntries]);
}
export function useRunApp() {
......
......@@ -50,6 +50,7 @@ import {
gitRenameBranch,
} from "../utils/git_utils";
import { safeSend } from "../utils/safe_sender";
import type { AppOutput } from "../types/misc";
import { normalizePath } from "../../../shared/normalizePath";
import {
isServerFunction,
......@@ -279,6 +280,43 @@ Details: ${details || "n/a"}
});
}
// =============================================================================
// App Output Batcher
// =============================================================================
// Batches stdout/stderr IPC messages to avoid flooding the renderer when apps
// emit high-volume logs. Messages are buffered and flushed every 100ms.
const APP_OUTPUT_FLUSH_INTERVAL_MS = 100;
const pendingOutputs = new Map<Electron.WebContents, AppOutput[]>();
let flushTimer: ReturnType<typeof setTimeout> | null = null;
function enqueueAppOutput(
sender: Electron.WebContents,
output: AppOutput,
): void {
let queue = pendingOutputs.get(sender);
if (!queue) {
queue = [];
pendingOutputs.set(sender, queue);
}
queue.push(output);
if (!flushTimer) {
flushTimer = setTimeout(flushAllAppOutputs, APP_OUTPUT_FLUSH_INTERVAL_MS);
}
}
function flushAllAppOutputs(): void {
flushTimer = null;
for (const [sender, outputs] of pendingOutputs) {
if (outputs.length > 0) {
safeSend(sender, "app:output-batch", outputs);
}
}
pendingOutputs.clear();
}
function listenToProcess({
process: spawnedProcess,
appId,
......@@ -323,15 +361,15 @@ function listenToProcess({
const inputRequestPattern = /\s*›\s*\([yY]\/[nN]\)\s*$/;
const isInputRequest = inputRequestPattern.test(message);
if (isInputRequest) {
// Send special input-requested event for interactive prompts
// Send input-requested immediately (not batched) for responsive UX
safeSend(event.sender, "app:output", {
type: "input-requested",
message,
appId,
});
} else {
// Normal stdout handling
safeSend(event.sender, "app:output", {
// Batch normal stdout for efficient IPC
enqueueAppOutput(event.sender, {
type: "stdout",
message,
appId,
......@@ -351,7 +389,7 @@ function listenToProcess({
appInfo.originalUrl === originalUrl &&
appInfo.proxyUrl
) {
safeSend(event.sender, "app:output", {
enqueueAppOutput(event.sender, {
type: "stdout",
message: `[dyad-proxy-server]started=[${appInfo.proxyUrl}] original=[${originalUrl}]`,
appId,
......@@ -372,7 +410,7 @@ function listenToProcess({
latestAppInfo.proxyUrl = proxyUrl;
latestAppInfo.originalUrl = originalUrl;
}
safeSend(event.sender, "app:output", {
enqueueAppOutput(event.sender, {
type: "stdout",
message: `[dyad-proxy-server]started=[${proxyUrl}] original=[${originalUrl}]`,
appId,
......@@ -406,7 +444,7 @@ function listenToProcess({
appId,
});
safeSend(event.sender, "app:output", {
enqueueAppOutput(event.sender, {
type: "stderr",
message,
appId,
......@@ -418,6 +456,8 @@ function listenToProcess({
logger.log(
`App ${appId} (PID: ${spawnedProcess.pid}) process closed with code ${code}, signal ${signal}.`,
);
// Flush any remaining batched output before signaling process exit
flushAllAppOutputs();
removeAppIfCurrentProcess(appId, spawnedProcess);
});
......
......@@ -383,6 +383,11 @@ export const miscEvents = {
payload: AppOutputSchema,
}),
appOutputBatch: defineEvent({
channel: "app:output-batch",
payload: z.array(AppOutputSchema),
}),
deepLinkReceived: defineEvent({
channel: "deep-link-received",
payload: DeepLinkDataSchema,
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论