整合求解器活动监控与步长回归证据

同步远端 PNL0003 诊断和大采样网格能力,语义合并活动感知的 60 秒真停滞判定与旧后端 15 分钟兼容兜底。

纳管热路径优化、15 单元运行证据、浏览器与 API 报告,并补充北京时间更新日志和遗留问题。
This commit is contained in:
lujingze committed 2026-08-19 16:24:31 +00:00
1 parent c19cf77aee
commit e18399c022
46 files changed
+181589 -170

No files matched your search

+223 -22
View File
@@ -109,8 +109,12 @@ import {
lmechn1RightPortCount,
} from "./componentSymbols/mechanical";
import {
SIMULATION_ACTIVITY_STALL_TIMEOUT_MS,
SIMULATION_ACTIVITY_STALL_TIMEOUT_SECONDS,
SIMULATION_SOLVER_STALL_TIMEOUT_MINUTES,
SIMULATION_SOLVER_STALL_TIMEOUT_MS,
solverActivityStallTimeoutMessage,
solverStallTimeoutMessage,
solverStallTimeoutReached,
} from "./simulationTimeout";
import {
CONTACT_AWARE_EDGE_TYPE,
@@ -1053,6 +1057,37 @@ type SimulationStreamIssue = {
message?: string;
};
type SimulationActivityTelemetry = {
activitySequence?: number;
activityKind?: string;
currentTrialTime?: number | null;
rhsCallCount?: number;
acceptedStepSequence?: number;
acceptedTime?: number | null;
};
export type SimulationActivityWatchdogStatus =
| "progressing"
| "slow-active"
| "slow-telemetry-unavailable"
| "stalled";
export type SimulationActivityWatchdogState = {
lastAcceptedProgressAt: number;
lastActivityChangeAt: number | null;
lastActivitySequence: number | null;
lastAcceptedStepSequence: number | null;
lastAcceptedTime: number | null;
integratingPhaseActive: boolean;
};
export type SimulationActivityWatchdogObservation = {
status: SimulationActivityWatchdogStatus;
acceptedProgressIdleMs: number;
activityIdleMs: number | null;
activityTelemetryAvailable: boolean;
};
type SimulationCancelReason = "user" | "stalled";
type SimulationTaskResponse = {
@@ -1064,7 +1099,7 @@ type SimulationTaskResponse = {
};
type SimulationStreamEvent =
| {
| ({
event: "progress";
progress: number;
phase: string;
@@ -1072,7 +1107,7 @@ type SimulationStreamEvent =
heartbeat?: boolean;
simulatedTime?: number;
totalTime?: number;
}
} & SimulationActivityTelemetry)
| {
event: "result";
progress: number;
@@ -1092,6 +1127,137 @@ type SimulationStreamEvent =
simulatedTime?: number;
totalTime?: number;
};
export function createSimulationActivityWatchdog(
now = Date.now(),
): SimulationActivityWatchdogState {
return {
lastAcceptedProgressAt: now,
lastActivityChangeAt: null,
lastActivitySequence: null,
lastAcceptedStepSequence: null,
lastAcceptedTime: null,
integratingPhaseActive: false,
};
}
export function isSimulationSolverActivityPhase(phase: string): boolean {
return phase === "integrating";
}
export function observeSimulationActivityWatchdog(
current: SimulationActivityWatchdogState,
event: SimulationActivityTelemetry & { heartbeat?: boolean; phase?: string },
now = Date.now(),
stallTimeoutMs = SIMULATION_ACTIVITY_STALL_TIMEOUT_MS,
legacyStallTimeoutMs = SIMULATION_SOLVER_STALL_TIMEOUT_MS,
): {
state: SimulationActivityWatchdogState;
observation: SimulationActivityWatchdogObservation;
} {
const reportsPhase = typeof event.phase === "string";
if (reportsPhase && !isSimulationSolverActivityPhase(event.phase!)) {
return {
state: createSimulationActivityWatchdog(now),
observation: {
status: "progressing",
acceptedProgressIdleMs: 0,
activityIdleMs: null,
activityTelemetryAvailable: false,
},
};
}
const baseline =
reportsPhase && !current.integratingPhaseActive
? {
...createSimulationActivityWatchdog(now),
integratingPhaseActive: true,
}
: current;
let acceptedProgress = event.heartbeat !== true;
let lastAcceptedStepSequence = baseline.lastAcceptedStepSequence;
let lastAcceptedTime = baseline.lastAcceptedTime;
if (
typeof event.acceptedStepSequence === "number" &&
Number.isFinite(event.acceptedStepSequence)
) {
if (
lastAcceptedStepSequence !== null &&
event.acceptedStepSequence !== lastAcceptedStepSequence
) {
acceptedProgress = true;
}
lastAcceptedStepSequence = event.acceptedStepSequence;
}
if (typeof event.acceptedTime === "number" && Number.isFinite(event.acceptedTime)) {
if (lastAcceptedTime !== null && event.acceptedTime !== lastAcceptedTime) {
acceptedProgress = true;
}
lastAcceptedTime = event.acceptedTime;
}
let lastActivityChangeAt = baseline.lastActivityChangeAt;
let lastActivitySequence = baseline.lastActivitySequence;
const reportsActivitySequence =
typeof event.activitySequence === "number" &&
Number.isFinite(event.activitySequence);
if (reportsActivitySequence) {
if (
lastActivitySequence === null ||
event.activitySequence !== lastActivitySequence
) {
lastActivityChangeAt = now;
}
lastActivitySequence = event.activitySequence!;
}
const lastAcceptedProgressAt = acceptedProgress
? now
: baseline.lastAcceptedProgressAt;
const acceptedProgressIdleMs = Math.max(0, now - lastAcceptedProgressAt);
const activityIdleMs =
lastActivityChangeAt === null
? null
: Math.max(0, now - lastActivityChangeAt);
const effectiveStallTimeoutMs = Math.max(0, stallTimeoutMs);
const effectiveLegacyStallTimeoutMs = Math.max(
effectiveStallTimeoutMs,
legacyStallTimeoutMs,
);
let status: SimulationActivityWatchdogStatus = "progressing";
if (acceptedProgressIdleMs >= effectiveStallTimeoutMs) {
if (!reportsActivitySequence || activityIdleMs === null) {
status =
acceptedProgressIdleMs >= effectiveLegacyStallTimeoutMs
? "stalled"
: "slow-telemetry-unavailable";
} else if (activityIdleMs >= effectiveStallTimeoutMs) {
status = "stalled";
} else {
status = "slow-active";
}
}
return {
state: {
lastAcceptedProgressAt,
lastActivityChangeAt,
lastActivitySequence,
lastAcceptedStepSequence,
lastAcceptedTime,
integratingPhaseActive:
reportsPhase || baseline.integratingPhaseActive,
},
observation: {
status,
acceptedProgressIdleMs,
activityIdleMs,
activityTelemetryAvailable: reportsActivitySequence,
},
};
}
const SUPPORTED_SOLVER_METHODS = ["BDF", "Radau", "LSODA", "RK45", "RK23", "DOP853"];
const simulationConfigLabels: Record<SimulationNumericKey, string> = {
t_start: "起始时间",
@@ -7355,10 +7521,11 @@ function FlowWorkbench() {
setSimulationStopPending(false);
let lastLoggedPhase = "";
let lastLoggedBucket = -1;
let lastWatchdogStatus: SimulationActivityWatchdogStatus = "progressing";
try {
const result = await streamSystemSimulation(
xml,
(event) => {
(event, watchdog) => {
if (event.event !== "progress") {
return;
}
@@ -7373,14 +7540,38 @@ function FlowWorkbench() {
"initialization",
"initializing",
].includes(event.phase);
const progressMessage =
watchdog?.status === "slow-active"
? `正在处理耗时积分步,内部计算仍活跃${
event.activityKind ? `(${event.activityKind})` : ""
},可手动停止`
: watchdog?.status === "slow-telemetry-unavailable"
? "积分步耗时较长,后端未提供活动遥测;任务保持运行,可手动停止"
: event.message;
setSimulationProgress((current) => ({
...current,
state: "running",
percent: normalizedProgress.percent,
message: event.message,
message: progressMessage,
simulatedTime: normalizedProgress.simulatedTime,
totalTime: normalizedProgress.totalTime,
}));
if (watchdog && watchdog.status !== lastWatchdogStatus) {
if (watchdog.status === "slow-active") {
appendConsoleEntry(
"warning",
`积分步超过 ${SIMULATION_ACTIVITY_STALL_TIMEOUT_SECONDS} 秒尚未接受,但后端内部活动仍在推进;继续等待,可随时手动停止`,
);
} else if (watchdog.status === "slow-telemetry-unavailable") {
appendConsoleEntry(
"warning",
`积分步超过 ${SIMULATION_ACTIVITY_STALL_TIMEOUT_SECONDS} 秒尚未接受,且后端未提供活动遥测;将按 ${SIMULATION_SOLVER_STALL_TIMEOUT_MINUTES} 分钟保守上限继续等待,可手动停止`,
);
} else if (lastWatchdogStatus !== "progressing") {
appendConsoleEntry("info", "求解器已接受新的积分步,仿真继续推进");
}
lastWatchdogStatus = watchdog.status;
}
const progressBucket = Math.floor(normalizedProgress.percent / 10);
if (event.phase !== lastLoggedPhase || progressBucket !== lastLoggedBucket) {
const progressDetail = reportsSimulationTime
@@ -7404,7 +7595,9 @@ function FlowWorkbench() {
appendConsoleEntry(
"error",
error.code === "SOLVER_STALLED"
? solverStallTimeoutMessage("recovering")
? error.message.includes("内部活动")
? solverActivityStallTimeoutMessage("recovering")
: solverStallTimeoutMessage("recovering")
: "超过 30 秒未收到后端数据,正在终止任务并恢复部分结果",
);
try {
@@ -10882,7 +11075,10 @@ async function readSimulationStreamChunk(
async function streamSystemSimulation(
xml: string,
onEvent: (event: SimulationStreamEvent) => void,
onEvent: (
event: SimulationStreamEvent,
watchdog?: SimulationActivityWatchdogObservation,
) => void,
simulationId: string,
): Promise<SimulationResult> {
const abortController = new AbortController();
@@ -10920,7 +11116,7 @@ async function streamSystemSimulation(
const decoder = new TextDecoder();
let buffer = "";
let result: SimulationResult | null = null;
let lastSolverProgressAt = Date.now();
let activityWatchdog = createSimulationActivityWatchdog();
const consumeLine = (line: string) => {
const trimmed = line.trim();
@@ -10936,23 +11132,28 @@ async function streamSystemSimulation(
throw new SimulationStreamError("仿真服务返回了无法识别的进度消息");
}
const event = parsed as SimulationStreamEvent;
let watchdogObservation: SimulationActivityWatchdogObservation | undefined;
if (event.event === "progress") {
if (
event.heartbeat === true &&
solverStallTimeoutReached(lastSolverProgressAt)
) {
throw new SimulationStreamError(
solverStallTimeoutMessage("detected"),
[],
undefined,
"SOLVER_STALLED",
);
}
if (event.heartbeat !== true) {
lastSolverProgressAt = Date.now();
const watched = observeSimulationActivityWatchdog(
activityWatchdog,
event,
);
activityWatchdog = watched.state;
if (isSimulationSolverActivityPhase(event.phase)) {
watchdogObservation = watched.observation;
if (watchdogObservation.status === "stalled") {
throw new SimulationStreamError(
watchdogObservation.activityTelemetryAvailable
? solverActivityStallTimeoutMessage("detected")
: solverStallTimeoutMessage("detected"),
[],
undefined,
"SOLVER_STALLED",
);
}
}
}
onEvent(event);
onEvent(event, watchdogObservation);
if (event.event === "result") {
result = event.result;
return;
+12
View File
@@ -1,6 +1,9 @@
export const SIMULATION_SOLVER_STALL_TIMEOUT_MINUTES = 15;
export const SIMULATION_SOLVER_STALL_TIMEOUT_MS =
SIMULATION_SOLVER_STALL_TIMEOUT_MINUTES * 60_000;
export const SIMULATION_ACTIVITY_STALL_TIMEOUT_SECONDS = 60;
export const SIMULATION_ACTIVITY_STALL_TIMEOUT_MS =
SIMULATION_ACTIVITY_STALL_TIMEOUT_SECONDS * 1_000;
export function solverStallTimeoutReached(
lastSolverProgressAt: number,
@@ -17,3 +20,12 @@ export function solverStallTimeoutMessage(
? `${prefix},正在终止任务并恢复部分结果`
: `${prefix},任务可能已经卡死`;
}
export function solverActivityStallTimeoutMessage(
context: "detected" | "recovering",
) {
const prefix = `求解器的接受步和内部活动均连续 ${SIMULATION_ACTIVITY_STALL_TIMEOUT_SECONDS} 秒没有变化`;
return context === "recovering"
? `${prefix},正在终止任务并恢复部分结果`
: `${prefix},任务可能已经卡死`;
}