fix(runner): propagate duration_ms through SSE done event

The SSE streaming path hardcoded '耗时: -' because duration_ms was
never threaded through:

- docker.rs: OutputChunk::Done now carries duration_ms, computed from
  start_container to wait completion via Instant::elapsed
- sse.rs: DonePayload gains duration_ms, serialized into the done event
- runner.rs: WASM done callback formats the real duration instead of '-'

The polling fallback path already had correct duration via ExecResult;
only the SSE path was missing it.
This commit is contained in:
xfy 2026-07-13 10:09:15 +08:00
parent aa2d7f3f2d
commit e50941ef24
3 changed files with 30 additions and 5 deletions

View File

@ -31,6 +31,7 @@ struct DonePayload {
exit_code: Option<i64>,
oom_killed: bool,
timed_out: bool,
duration_ms: u64,
}
/// SSE handler从 EXEC_STREAMS 取出 receiver取出即移除防重复连接
@ -52,12 +53,14 @@ pub async fn exec_stream(
exit_code,
oom_killed,
timed_out,
duration_ms,
} => Event::default()
.event("done")
.json_data(DonePayload {
exit_code,
oom_killed,
timed_out,
duration_ms,
})
.unwrap_or_else(|_| Event::default().event("done").data("{}")),
})

View File

@ -569,6 +569,7 @@ mod sse_consumer {
exit_code: Option<i64>,
oom_killed: bool,
timed_out: bool,
duration_ms: u64,
}
/// 启动 EventSource 消费 SSE 流。
@ -630,17 +631,31 @@ mod sse_consumer {
exit_code: None,
oom_killed: false,
timed_out: false,
duration_ms: 0,
});
let (info, err) = if payload.timed_out {
("耗时: - · 状态: 超时".to_string(), "超时".to_string())
(
format!("耗时: {}ms · 状态: 超时", payload.duration_ms),
"超时".to_string(),
)
} else if payload.oom_killed {
("耗时: - · 状态: 内存超限".to_string(), "内存超限".to_string())
(
format!("耗时: {}ms · 状态: 内存超限", payload.duration_ms),
"内存超限".to_string(),
)
} else if payload.exit_code == Some(0) {
("耗时: - · 状态: 成功".to_string(), String::new())
(
format!("耗时: {}ms · 状态: 成功", payload.duration_ms),
String::new(),
)
} else {
(
format!("退出码: {} · 运行错误", payload.exit_code.unwrap_or(-1)),
format!(
"耗时: {}ms · 退出码: {} · 运行错误",
payload.duration_ms,
payload.exit_code.unwrap_or(-1)
),
"运行错误".to_string(),
)
};

View File

@ -231,7 +231,7 @@ pub async fn run_in_container(
/// 流式输出 chunkrun_in_container_stream 边读日志边推送给 SSE handler。
///
/// 序列化后作为 SSE event data`Done` 同时携带终态信息(退出码 / OOM / 超时)。
/// 序列化后作为 SSE event data`Done` 同时携带终态信息(退出码 / OOM / 超时 / 耗时)。
#[derive(Clone, Debug)]
pub enum OutputChunk {
/// stdout 块(容器逐块产出)。
@ -239,10 +239,12 @@ pub enum OutputChunk {
/// stderr 块(容器逐块产出)。
Stderr(String),
/// 终态容器执行结束。exit_code=None 表示拿不到退出码wait 出错)。
/// duration_ms = start_container 到 wait 完成的耗时。
Done {
exit_code: Option<i64>,
oom_killed: bool,
timed_out: bool,
duration_ms: u64,
},
}
@ -320,6 +322,9 @@ pub async fn run_in_container_stream(
.start_container(&container_id, None::<StartContainerOptions>)
.await?;
// 容器开始执行的时刻,用于计算 duration_msstart_container 返回即视为起点)。
let start_time = std::time::Instant::now();
// 写入源码到 stdin 后关闭 writer。
use tokio::io::AsyncWriteExt;
let write_fut = async {
@ -477,12 +482,14 @@ pub async fn run_in_container_stream(
.unwrap_or(false);
// 推送终态 chunk客户端已断开则跳过send 必然失败)。
let duration_ms = start_time.elapsed().as_millis() as u64;
if !client_disconnected {
let _ = tx
.send(OutputChunk::Done {
exit_code,
oom_killed,
timed_out,
duration_ms,
})
.await;
}