feat(host): M302 实例列表刷新搬出 UI 线程(扫盘进后台 + 代次丢弃过期结果)
`refresh_instances()` 以前把「扫盘」与「盖回运行时态」写在一起,而 `dispatch(RefreshAll)`
那一支是唯一没走 `job::spawn` 的(邻座 ListVersions / ListLoaderVersions 都走了)⇒ 同步跑在
UI 线程:官方根 200 版本约 236 ms、叠加 10 份外部档案 1.35 s(仓库内旧实测 M297)。
两半分家:
- `game::instances(&env)` 是纯 env 函数 ⇒ 搬进 `job::spawn("instances", …)`,回程是新载荷
`HostTask::Instances { epoch, instances, elapsed_ms }`;
- 「按 `launches/pending/failed` 覆写 Running/Busy/Failed」要读 host 自己的表 ⇒ 留在 host 线程,
挪到结果回来那一刻(`handle::apply_instances`),结论与之前逐字一致。
顺序:每次刷新领一个只增不减的代次(`game::Scan`),只采纳当代次那份。并发两次刷新 / 扫描途中
删实例 / 收尾触发第三次这三种交错下,先发后到的扫描一律丢(不发事件:清空再重填只是把卡顿换成
白屏)。不做线程取消 —— 扫描是纯读,没有值得取消的东西,丢结果比掐线程安全。
可观测:每次采纳打一行 `[instances] N 个实例 · 扫盘 X ms · epoch E · 丢旧代次 D 份`,
丢弃也各打一行;账本 `game::Scan` 可直接断言(实例数 / 耗时 / 丢包数)。
判据(都真构造:真临时根 + 真 profiles.json + 真线程,只有交付顺序是摆的):
- `a_refresh_returns_before_the_scan_is_done`(改回 inline 即红)
- `two_refreshes_in_flight_publish_only_the_newest`
- `an_older_scan_cannot_resurrect_what_a_newer_one_saw_deleted`
- `a_launch_end_refresh_beats_the_two_that_were_in_flight`
- app 侧 `a_refresh_in_flight_keeps_the_list_and_does_not_pin_the_clock`(不清空 + M298 的
view_dirty 不永久置真)
- `launch_state_tests` 的 5 处 `refresh_instances()` 后补一行 `pump_instances()`(断言一字未动)
This commit is contained in:
1 parent
38fbd2a778
commit
a0413a5f94
8 files changed
+753
-28
No files matched your search
@@ -2,7 +2,10 @@
|
||||
//!
|
||||
//! 父模块的私有字段对子模块可见,所以这里直接读写 `self.tx` / `self.launches` 等。
|
||||
|
||||
use crate::dto::{AppEvent, CoreEvent, InstanceRuntime, LaunchFailureCause, LogOutcome, PackEvent};
|
||||
use crate::dto::{
|
||||
AppEvent, CoreEvent, InstanceRuntime, InstanceSummary, LaunchFailureCause, LogOutcome,
|
||||
PackEvent,
|
||||
};
|
||||
use crate::toast::ToastKind;
|
||||
|
||||
use super::{addon, logs, mods, CoreHost, HostTask, LiveLaunch};
|
||||
@@ -15,6 +18,12 @@ impl CoreHost {
|
||||
HostTask::Versions(Err(reason)) => {
|
||||
self.toast(ToastKind::Warn, format!("拉取版本清单失败:{reason}"));
|
||||
}
|
||||
// M302:实例扫描回程。**代次不符一律丢**(先发后到的扫描可能带着已被删掉的实例)。
|
||||
HostTask::Instances {
|
||||
epoch,
|
||||
instances,
|
||||
elapsed_ms,
|
||||
} => self.apply_instances(epoch, instances, elapsed_ms),
|
||||
HostTask::LoaderVersions {
|
||||
kind,
|
||||
mc_version,
|
||||
@@ -243,6 +252,66 @@ impl CoreHost {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把一份**当代次**的扫盘结果落地:丢旧代次 → 盖运行时态 → 记账 → 发快照。
|
||||
///
|
||||
/// ## 运行时态为什么在这里盖(M302)
|
||||
///
|
||||
/// `game::instances` 只读盘,每条都是 `Idle`;在跑的实例必须按 host 自己的启动表覆写
|
||||
/// `Running{pid}`/`Busy`/`Failed`,否则任意一次刷新(装版本成功、加删账户、另一实例收尾)
|
||||
/// 都会把磁贴打回 Idle:停止键与「运行中」条消失,进程还在跑却再也停不下来。
|
||||
///
|
||||
/// 盖的时机是**结果回来那一刻**而不是「发请求那一刻」:扫描期间发生的启动/收尾已经落在
|
||||
/// host 上了,这一读就是最新的(老实现同步扫,两件事本来就是同一刻)。所以异步化没有改变
|
||||
/// 这条结论——`launch_state_tests` 的 Running/Busy/Failed 三条照旧成立。
|
||||
///
|
||||
/// ## 代次
|
||||
///
|
||||
/// 不符就整份丢掉并计数:那份结果的盘面已经过期,采纳它会让「删掉的实例又回来」。
|
||||
/// 丢完就 return,**不发事件**——界面上的旧快照原样留着(清空再重填只是把卡顿换成白屏)。
|
||||
fn apply_instances(
|
||||
&mut self,
|
||||
epoch: u64,
|
||||
mut instances: Vec<InstanceSummary>,
|
||||
elapsed_ms: u64,
|
||||
) {
|
||||
if !self.instances_scan.admit(epoch) {
|
||||
// 丢掉的那一份也记一行:交错真的发生时,光看「采纳」那一行是看不出来的
|
||||
eprintln!(
|
||||
"[instances] 丢掉过期扫描(epoch {epoch} ≠ 当代次 {},累计丢 {} 份)",
|
||||
self.instances_scan.epoch(),
|
||||
self.instances_scan.dropped()
|
||||
);
|
||||
return;
|
||||
}
|
||||
for instance in &mut instances {
|
||||
if let Some(live) = self
|
||||
.launches
|
||||
.iter()
|
||||
.find(|live| live.instance == instance.id)
|
||||
{
|
||||
instance.runtime = InstanceRuntime::Running { pid: live.pid };
|
||||
} else if self.pending.contains(&instance.id) {
|
||||
instance.runtime = InstanceRuntime::Busy {
|
||||
stage: crate::dto::LaunchStage::Launch,
|
||||
progress: crate::dto::Progress { done: 0, total: 1 },
|
||||
};
|
||||
} else if let Some(cause) = self.failed.get(&instance.id) {
|
||||
instance.runtime = InstanceRuntime::Failed {
|
||||
cause: cause.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
let count = instances.len();
|
||||
self.instances_scan.adopt(epoch, count, elapsed_ms);
|
||||
// M302 的可观测口径:一次刷新一行(前缀与 `[mods]` / `[auth]` 同一种形状,好 grep)。
|
||||
// 「扫了几条、扫了多久、丢过几份」三样都在这一行里——回归时它比"感觉快了"硬。
|
||||
eprintln!(
|
||||
"[instances] {count} 个实例 · 扫盘 {elapsed_ms} ms · epoch {epoch} · 丢旧代次 {} 份",
|
||||
self.instances_scan.dropped()
|
||||
);
|
||||
self.send(CoreEvent::InstancesChanged(instances));
|
||||
}
|
||||
|
||||
/// 尾随运行中会话(每 `TAIL_POLL_TICKS` 拍一次)。
|
||||
pub(super) fn poll_tail(&mut self) {
|
||||
let Some((session, lines)) = self.tail.poll() else {
|
||||
|
||||
Reference in new issue
Block a user