first commit
This commit is contained in:
233
apps/miniprogram/pages/terminal/terminalStdoutRenderPolicy.js
Normal file
233
apps/miniprogram/pages/terminal/terminalStdoutRenderPolicy.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/* 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
|
||||
};
|
||||
Reference in New Issue
Block a user