[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
232
electron/bridges/terminalFlowAck.cjs
Normal file
232
electron/bridges/terminalFlowAck.cjs
Normal file
@@ -0,0 +1,232 @@
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
FLOW_HIGH_WATER_MARK,
|
||||
FLOW_LOW_WATER_MARK,
|
||||
} = require("../../infrastructure/config/terminalFlowConstants.cjs");
|
||||
const { logTerminalOutputPerf } = require("./terminalPerformanceDiagnostics.cjs");
|
||||
|
||||
function getFlowTarget(session) {
|
||||
return session?.stream || session?.proc || session?.socket || session?.serialPort || null;
|
||||
}
|
||||
|
||||
function ensureFlowState(session) {
|
||||
if (!session.flowState) {
|
||||
session.flowState = {
|
||||
rendererPaused: false,
|
||||
unackedBytes: 0,
|
||||
bufferedBytes: 0,
|
||||
appliedPause: false,
|
||||
outputPaused: false,
|
||||
firstUnackedAt: 0,
|
||||
lastEmittedAt: 0,
|
||||
lastAckAt: 0,
|
||||
lastPerfAckLogAt: 0,
|
||||
sessionId: null,
|
||||
};
|
||||
}
|
||||
const state = session.flowState;
|
||||
state.rendererPaused = Boolean(state.rendererPaused);
|
||||
state.unackedBytes = Number.isFinite(state.unackedBytes) ? Math.max(0, state.unackedBytes) : 0;
|
||||
state.bufferedBytes = Number.isFinite(state.bufferedBytes) ? Math.max(0, state.bufferedBytes) : 0;
|
||||
state.appliedPause = Boolean(state.appliedPause);
|
||||
state.firstUnackedAt = Number.isFinite(state.firstUnackedAt) ? Math.max(0, state.firstUnackedAt) : 0;
|
||||
state.lastEmittedAt = Number.isFinite(state.lastEmittedAt) ? Math.max(0, state.lastEmittedAt) : 0;
|
||||
state.lastAckAt = Number.isFinite(state.lastAckAt) ? Math.max(0, state.lastAckAt) : 0;
|
||||
state.lastPerfAckLogAt = Number.isFinite(state.lastPerfAckLogAt) ? Math.max(0, state.lastPerfAckLogAt) : 0;
|
||||
state.sessionId = typeof state.sessionId === "string" && state.sessionId ? state.sessionId : null;
|
||||
if (typeof state.outputPaused !== "boolean") {
|
||||
state.outputPaused = state.appliedPause && (state.rendererPaused || state.unackedBytes >= FLOW_HIGH_WATER_MARK);
|
||||
}
|
||||
return session.flowState;
|
||||
}
|
||||
|
||||
function applyPause(session, target) {
|
||||
try {
|
||||
target.pause?.();
|
||||
} catch (err) {
|
||||
if (err?.code !== "EPIPE" && err?.code !== "ERR_STREAM_DESTROYED") {
|
||||
console.warn("Flow control pause failed", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyResume(session, target) {
|
||||
try {
|
||||
target.resume?.();
|
||||
} catch (err) {
|
||||
if (err?.code !== "EPIPE" && err?.code !== "ERR_STREAM_DESTROYED") {
|
||||
console.warn("Flow control resume failed", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFlowPerfDetails(session, extra = {}) {
|
||||
const state = ensureFlowState(session);
|
||||
return {
|
||||
sessionId: state.sessionId || session?.id || session?.sessionId || null,
|
||||
protocol: session?.protocol || session?.type || null,
|
||||
rendererPaused: state.rendererPaused,
|
||||
outputPaused: state.outputPaused,
|
||||
appliedPause: state.appliedPause,
|
||||
unackedBytes: state.unackedBytes,
|
||||
bufferedBytes: state.bufferedBytes,
|
||||
highWaterMark: FLOW_HIGH_WATER_MARK,
|
||||
lowWaterMark: FLOW_LOW_WATER_MARK,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function rememberSessionId(state, sessionId) {
|
||||
if (typeof sessionId === "string" && sessionId) {
|
||||
state.sessionId = sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE on FLOW_HIGH_WATER_MARK size (issue #1961): for SSH sessions the flow
|
||||
// target is the ssh2 channel, so pause() stops the remote until resume() plus a
|
||||
// full round-trip. The watermark is kept near ssh2's own 2MB channel window
|
||||
// (WINDOW_THRESHOLD 1MB); a small watermark pauses/resumes dozens of times
|
||||
// during a multi-MB dump (e.g. `tail -2000f big.log`) and adds ~1 RTT per cycle
|
||||
// on WAN links, which is what made those dumps crawl.
|
||||
function reconcileSessionFlow(session) {
|
||||
if (!session) return;
|
||||
const state = ensureFlowState(session);
|
||||
const target = getFlowTarget(session);
|
||||
if (!target) return;
|
||||
|
||||
if (!state.outputPaused && (state.rendererPaused || state.unackedBytes >= FLOW_HIGH_WATER_MARK)) {
|
||||
state.outputPaused = true;
|
||||
} else if (state.outputPaused && !state.rendererPaused && state.unackedBytes <= FLOW_LOW_WATER_MARK) {
|
||||
state.outputPaused = false;
|
||||
}
|
||||
|
||||
const pendingBytes = state.unackedBytes + state.bufferedBytes;
|
||||
const shouldPause = state.outputPaused || pendingBytes >= FLOW_HIGH_WATER_MARK;
|
||||
const shouldResume = !state.outputPaused && pendingBytes <= FLOW_LOW_WATER_MARK;
|
||||
|
||||
if (!state.appliedPause && shouldPause) {
|
||||
logTerminalOutputPerf("backend-flow-pause", getFlowPerfDetails(session, { pendingBytes }));
|
||||
applyPause(session, target);
|
||||
state.appliedPause = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.appliedPause && shouldResume) {
|
||||
logTerminalOutputPerf("backend-flow-resume", getFlowPerfDetails(session, { pendingBytes }));
|
||||
applyResume(session, target);
|
||||
state.appliedPause = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setRendererFlowPaused(session, paused) {
|
||||
if (!session) return;
|
||||
const state = ensureFlowState(session);
|
||||
state.rendererPaused = Boolean(paused);
|
||||
reconcileSessionFlow(session);
|
||||
}
|
||||
|
||||
function trackEmitted(session, bytes, sessionId) {
|
||||
if (!session || !Number.isFinite(bytes) || bytes <= 0) return;
|
||||
const state = ensureFlowState(session);
|
||||
rememberSessionId(state, sessionId);
|
||||
const now = Date.now();
|
||||
if (state.unackedBytes === 0) {
|
||||
state.firstUnackedAt = now;
|
||||
}
|
||||
state.lastEmittedAt = now;
|
||||
state.unackedBytes += bytes;
|
||||
reconcileSessionFlow(session);
|
||||
}
|
||||
|
||||
function trackAck(session, bytes, sessionId) {
|
||||
if (!session || !Number.isFinite(bytes) || bytes <= 0) return;
|
||||
const state = ensureFlowState(session);
|
||||
rememberSessionId(state, sessionId);
|
||||
const now = Date.now();
|
||||
const unackedBefore = state.unackedBytes;
|
||||
state.unackedBytes = Math.max(0, state.unackedBytes - bytes);
|
||||
state.lastAckAt = now;
|
||||
const ackAgeMs = state.firstUnackedAt ? now - state.firstUnackedAt : 0;
|
||||
if (
|
||||
unackedBefore > 0
|
||||
&& (
|
||||
now - state.lastPerfAckLogAt >= 1000
|
||||
|| (
|
||||
state.unackedBytes === 0
|
||||
&& (ackAgeMs >= 250 || unackedBefore >= 64 * 1024)
|
||||
)
|
||||
)
|
||||
) {
|
||||
state.lastPerfAckLogAt = now;
|
||||
logTerminalOutputPerf("backend-flow-ack", getFlowPerfDetails(session, {
|
||||
bytes,
|
||||
unackedBefore,
|
||||
unackedAfter: state.unackedBytes,
|
||||
ackAgeMs,
|
||||
sinceLastEmitMs: state.lastEmittedAt ? now - state.lastEmittedAt : null,
|
||||
}));
|
||||
}
|
||||
if (state.unackedBytes === 0) {
|
||||
state.firstUnackedAt = 0;
|
||||
}
|
||||
reconcileSessionFlow(session);
|
||||
}
|
||||
|
||||
function setBufferedOutputBytes(session, bytes) {
|
||||
if (!session) return;
|
||||
const state = ensureFlowState(session);
|
||||
state.bufferedBytes = Number.isFinite(bytes) ? Math.max(0, bytes) : 0;
|
||||
reconcileSessionFlow(session);
|
||||
}
|
||||
|
||||
function shouldAcceptSessionOutput(session) {
|
||||
if (!session) return true;
|
||||
const state = ensureFlowState(session);
|
||||
return !state.outputPaused;
|
||||
}
|
||||
|
||||
function isTransferSentryActive(transferSentry) {
|
||||
try {
|
||||
return Boolean(transferSentry?.isActive?.());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldProcessSessionOutput(session, transferSentry) {
|
||||
return Boolean(session) || isTransferSentryActive(transferSentry);
|
||||
}
|
||||
|
||||
function clearSessionFlowState(session, options = {}) {
|
||||
if (!session?.flowState) return;
|
||||
const target = getFlowTarget(session);
|
||||
if (session.flowState.appliedPause && target && options.resume !== false) {
|
||||
applyResume(session, target);
|
||||
}
|
||||
session.flowState = {
|
||||
rendererPaused: false,
|
||||
unackedBytes: 0,
|
||||
bufferedBytes: 0,
|
||||
appliedPause: false,
|
||||
outputPaused: false,
|
||||
firstUnackedAt: 0,
|
||||
lastEmittedAt: 0,
|
||||
lastAckAt: 0,
|
||||
lastPerfAckLogAt: 0,
|
||||
sessionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FLOW_HIGH_WATER_MARK,
|
||||
FLOW_LOW_WATER_MARK,
|
||||
setRendererFlowPaused,
|
||||
setBufferedOutputBytes,
|
||||
trackEmitted,
|
||||
trackAck,
|
||||
shouldAcceptSessionOutput,
|
||||
shouldProcessSessionOutput,
|
||||
clearSessionFlowState,
|
||||
reconcileSessionFlow,
|
||||
};
|
||||
Reference in New Issue
Block a user