234 lines
6.9 KiB
JavaScript
234 lines
6.9 KiB
JavaScript
/* global module */
|
||
|
||
const MEDIUM_BACKLOG_MIN_REMAINING_BYTES = 8 * 1024;
|
||
const MEDIUM_BACKLOG_MIN_PENDING_BYTES = 8 * 1024;
|
||
const MEDIUM_BACKLOG_MAX_SLICES = 8;
|
||
const MEDIUM_BACKLOG_RENDER_COOLDOWN_MS = 220;
|
||
|
||
const LARGE_BACKLOG_MIN_REMAINING_BYTES = 64 * 1024;
|
||
const LARGE_BACKLOG_MIN_PENDING_BYTES = 32 * 1024;
|
||
const LARGE_BACKLOG_MAX_SLICES = 32;
|
||
const LARGE_BACKLOG_RENDER_COOLDOWN_MS = 320;
|
||
|
||
const CRITICAL_BACKLOG_MIN_TOTAL_BYTES = 64 * 1024;
|
||
const CRITICAL_BACKLOG_MIN_PENDING_BYTES = 24 * 1024;
|
||
const CRITICAL_BACKLOG_MIN_PENDING_SAMPLES = 128;
|
||
const CRITICAL_BACKLOG_MIN_SCHEDULER_WAIT_MS = 1200;
|
||
const CRITICAL_BACKLOG_MIN_ACTIVE_AGE_MS = 1200;
|
||
const CRITICAL_BACKLOG_FRAME_PENDING_BYTES = 24 * 1024;
|
||
const CRITICAL_BACKLOG_FRAME_MAX_SLICES = 20;
|
||
const CRITICAL_BACKLOG_RENDER_COOLDOWN_MS = 520;
|
||
|
||
const STDOUT_OVERLAY_SYNC_COOLDOWN_MS = 240;
|
||
|
||
function normalizeNonNegativeInteger(value, fallback) {
|
||
const numeric = Number(value);
|
||
if (!Number.isFinite(numeric)) {
|
||
return Math.max(0, Math.round(Number(fallback) || 0));
|
||
}
|
||
return Math.max(0, Math.round(numeric));
|
||
}
|
||
|
||
function shouldUseCriticalBacklogPolicy(options) {
|
||
const source = options && typeof options === "object" ? options : {};
|
||
const totalRawBytes = normalizeNonNegativeInteger(source.totalRawBytes, 0);
|
||
const pendingStdoutBytes = normalizeNonNegativeInteger(source.pendingStdoutBytes, 0);
|
||
const pendingStdoutSamples = normalizeNonNegativeInteger(source.pendingStdoutSamples, 0);
|
||
const schedulerWaitMs = normalizeNonNegativeInteger(source.schedulerWaitMs, 0);
|
||
const activeStdoutAgeMs = normalizeNonNegativeInteger(source.activeStdoutAgeMs, 0);
|
||
return (
|
||
pendingStdoutBytes >= CRITICAL_BACKLOG_MIN_PENDING_BYTES ||
|
||
pendingStdoutSamples >= CRITICAL_BACKLOG_MIN_PENDING_SAMPLES ||
|
||
schedulerWaitMs >= CRITICAL_BACKLOG_MIN_SCHEDULER_WAIT_MS ||
|
||
(totalRawBytes >= CRITICAL_BACKLOG_MIN_TOTAL_BYTES &&
|
||
activeStdoutAgeMs >= CRITICAL_BACKLOG_MIN_ACTIVE_AGE_MS)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* stdout 的真正瓶颈不是 VT 解析,而是每个 slice 都把整份 `outputRenderLines`
|
||
* 重新通过 `setData` 送去视图层。
|
||
*
|
||
* 这里按 backlog 做两档降频:
|
||
* 1. 中等 backlog:累计到 8KB 或 8 个 slice 再提交一次;
|
||
* 2. 大 backlog:累计到 32KB 或 32 个 slice 再提交一次。
|
||
*
|
||
* 若用户刚有输入,或当前 slice 触发了终端响应帧,则立即提交,避免交互被延后。
|
||
*/
|
||
function resolveTerminalStdoutRenderDecision(options) {
|
||
const source = options && typeof options === "object" ? options : {};
|
||
if (source.yieldedToUserInput) {
|
||
return {
|
||
defer: false,
|
||
reason: "user_input",
|
||
policy: "interactive"
|
||
};
|
||
}
|
||
if (normalizeNonNegativeInteger(source.pendingResponseCount, 0) > 0) {
|
||
return {
|
||
defer: false,
|
||
reason: "pending_response",
|
||
policy: "interactive"
|
||
};
|
||
}
|
||
|
||
const remainingBytes = normalizeNonNegativeInteger(source.remainingBytes, 0);
|
||
const pendingReplayBytes = normalizeNonNegativeInteger(source.pendingReplayBytes, 0);
|
||
const nextSlicesSinceLastRender = normalizeNonNegativeInteger(source.nextSlicesSinceLastRender, 0);
|
||
const timeSinceLastRenderMs = normalizeNonNegativeInteger(
|
||
source.timeSinceLastRenderMs,
|
||
Number.MAX_SAFE_INTEGER
|
||
);
|
||
const taskDone = !!source.taskDone;
|
||
const usingCriticalPolicy = shouldUseCriticalBacklogPolicy(source);
|
||
|
||
if (usingCriticalPolicy) {
|
||
if (taskDone) {
|
||
return {
|
||
defer: false,
|
||
reason: "task_complete",
|
||
policy: "critical_backlog"
|
||
};
|
||
}
|
||
if (
|
||
nextSlicesSinceLastRender > 0 &&
|
||
timeSinceLastRenderMs < CRITICAL_BACKLOG_RENDER_COOLDOWN_MS
|
||
) {
|
||
return {
|
||
defer: true,
|
||
reason: "render_cooldown",
|
||
policy: "critical_backlog"
|
||
};
|
||
}
|
||
if (pendingReplayBytes >= CRITICAL_BACKLOG_FRAME_PENDING_BYTES) {
|
||
return {
|
||
defer: false,
|
||
reason: "pending_bytes_threshold",
|
||
policy: "critical_backlog"
|
||
};
|
||
}
|
||
if (nextSlicesSinceLastRender >= CRITICAL_BACKLOG_FRAME_MAX_SLICES) {
|
||
return {
|
||
defer: false,
|
||
reason: "slice_threshold",
|
||
policy: "critical_backlog"
|
||
};
|
||
}
|
||
return {
|
||
defer: true,
|
||
reason: "defer_critical_backlog",
|
||
policy: "critical_backlog"
|
||
};
|
||
}
|
||
|
||
const usingLargePolicy =
|
||
remainingBytes >= LARGE_BACKLOG_MIN_REMAINING_BYTES ||
|
||
pendingReplayBytes >= LARGE_BACKLOG_MIN_PENDING_BYTES;
|
||
const minRemainingBytes = usingLargePolicy
|
||
? LARGE_BACKLOG_MIN_REMAINING_BYTES
|
||
: MEDIUM_BACKLOG_MIN_REMAINING_BYTES;
|
||
const minPendingBytes = usingLargePolicy
|
||
? LARGE_BACKLOG_MIN_PENDING_BYTES
|
||
: MEDIUM_BACKLOG_MIN_PENDING_BYTES;
|
||
const maxSlicesSinceRender = usingLargePolicy ? LARGE_BACKLOG_MAX_SLICES : MEDIUM_BACKLOG_MAX_SLICES;
|
||
const renderCooldownMs = usingLargePolicy
|
||
? LARGE_BACKLOG_RENDER_COOLDOWN_MS
|
||
: MEDIUM_BACKLOG_RENDER_COOLDOWN_MS;
|
||
const policy = usingLargePolicy ? "large_backlog" : "medium_backlog";
|
||
|
||
if (taskDone) {
|
||
return {
|
||
defer: false,
|
||
reason: "task_complete",
|
||
policy
|
||
};
|
||
}
|
||
|
||
if (
|
||
nextSlicesSinceLastRender > 0 &&
|
||
timeSinceLastRenderMs < renderCooldownMs &&
|
||
pendingReplayBytes < minPendingBytes &&
|
||
nextSlicesSinceLastRender < maxSlicesSinceRender
|
||
) {
|
||
return {
|
||
defer: true,
|
||
reason: "render_cooldown",
|
||
policy
|
||
};
|
||
}
|
||
|
||
if (remainingBytes < minRemainingBytes) {
|
||
return {
|
||
defer: false,
|
||
reason: "remaining_below_threshold",
|
||
policy
|
||
};
|
||
}
|
||
if (pendingReplayBytes >= minPendingBytes) {
|
||
return {
|
||
defer: false,
|
||
reason: "pending_bytes_threshold",
|
||
policy
|
||
};
|
||
}
|
||
if (nextSlicesSinceLastRender >= maxSlicesSinceRender) {
|
||
return {
|
||
defer: false,
|
||
reason: "slice_threshold",
|
||
policy
|
||
};
|
||
}
|
||
return {
|
||
defer: true,
|
||
reason: "defer_backlog",
|
||
policy
|
||
};
|
||
}
|
||
|
||
function resolveTerminalStdoutOverlayDecision(options) {
|
||
const source = options && typeof options === "object" ? options : {};
|
||
if (source.isFinalRender) {
|
||
return {
|
||
sync: true,
|
||
reason: "task_complete"
|
||
};
|
||
}
|
||
if (source.yieldedToUserInput) {
|
||
return {
|
||
sync: true,
|
||
reason: "user_input"
|
||
};
|
||
}
|
||
const overlayPassCount = normalizeNonNegativeInteger(source.overlayPassCount, 0);
|
||
if (overlayPassCount <= 0) {
|
||
return {
|
||
sync: true,
|
||
reason: "first_render"
|
||
};
|
||
}
|
||
const timeSinceLastOverlayMs = normalizeNonNegativeInteger(
|
||
source.timeSinceLastOverlayMs,
|
||
Number.MAX_SAFE_INTEGER
|
||
);
|
||
if (timeSinceLastOverlayMs >= STDOUT_OVERLAY_SYNC_COOLDOWN_MS) {
|
||
return {
|
||
sync: true,
|
||
reason: "overlay_cooldown_elapsed"
|
||
};
|
||
}
|
||
return {
|
||
sync: false,
|
||
reason: "overlay_throttled"
|
||
};
|
||
}
|
||
|
||
function shouldDeferTerminalStdoutRender(options) {
|
||
return resolveTerminalStdoutRenderDecision(options).defer;
|
||
}
|
||
|
||
module.exports = {
|
||
resolveTerminalStdoutOverlayDecision,
|
||
resolveTerminalStdoutRenderDecision,
|
||
shouldDeferTerminalStdoutRender
|
||
};
|