perf(launch): M304 —— 点启动/停止不再在 UI 线程扫盘
M302 把 `refresh_instances` 的扫盘搬进了后台线程,但「点 Play」那条路上还留着两次 `game::instances(&self.env)`(`launch_target` 与 `running_pid`):官方根 200 个版本 236 ms、叠加 10 份外部档案 1.35 s,全部落在 UI 线程上。 改法:两条查询都读**已发布快照**(`CoreHost::send` 发 `InstancesChanged` 时留一份), 并在受理那一刻补一道**单实例**复核(`game::Scan::revalidate`),于是 - 调用方线程一次盘都不扫(一次内存查表 + 微秒级复核); - 快照过期不会拉错东西:档案实例查 `profiles.json` 是否还在、版本有没有被改; 自动发现的 `ext-` 实例用「逐个根重算 id」定位 (根, 版本),再让 `adopt::inspect` 只判这一个版本(不扫 `versions/`)。复核不过 ⇒ 拒绝 + 具名原因 + 立刻重扫。 - 同名不猜:名字兜底只在唯一命中时用,两个同名实例直接拒绝(多根之后很常见)。 - 「点下去立刻有反馈」不变:`Busy` 仍在 `dispatch` 返回前发出(`launch::accept`)。 拆分:`launch_target` / `in_flight` / `mark_pending` / `start_launch` / `running_pid` 从 `host/mod.rs`(原 395/400 行)搬进 `host/launch/accept.rs`;解析与拒绝形状在 `host/launch/target.rs`,单实例复核在 `host/game/refresh/live.rs`。 判据(`host/launch/target_tests.rs`,5 条): - 同一拍进 Busy(无 tick、无后台回程),且受理不派扫描(代次不变); - 快照落地后**新增**的实例点启动必须被拒绝(解析改回现场扫盘 ⇒ 红,实测); - 快照落地后**删掉**的实例点启动必须拒绝并给具名原因(去掉复核 ⇒ 红,实测); - 两个同名实例不猜;停止读快照里的 pid。 门禁:`cargo test -p unlucky-presenter -p unlucky-app-iced --all-targets`(423 + 1075 全绿)、 `cargo clippy --workspace --all-targets -- -D warnings`(0)、`cargo fmt --all --check`(0)、 文件行数全 ≤400(`host/mod.rs` 340、`refresh.rs` 382、`launch/` 90/114/242/280)。
This commit is contained in:
1 parent
bdb97f8189
commit
583a6cf24b
7 files changed
+625
-75
No files matched your search
@@ -0,0 +1,109 @@
|
||||
//! 快照条目的**现在**:这一条还在不在盘上(M304)。
|
||||
//!
|
||||
//! ## 为什么需要
|
||||
//!
|
||||
//! M304 把「点启动」的实例解析从「同步扫一遍 [`super::super::instances`]」改成「查已发布快照」。
|
||||
//! 快照是**过去**的一拍:用户可能在别处(文件管理器 / 另一个启动器 / 同一会话的另一条命令)
|
||||
//! 把这个实例删了、或者把它绑的版本改了。照旧解析就会「按快照把它拉起来」——拉的是**已经不
|
||||
//! 存在的东西**,最坏的一种是 `game_for` 反查不到那个根、退回配置根,于是**启动的是另一个根里
|
||||
//! 同名版本**(用户看着 A,起来的却是 B)。
|
||||
//!
|
||||
//! ## 判据(单实例,不扫 `versions/`)
|
||||
//!
|
||||
//! 一次只判**这一条**,所以绝不能再用 `instances()`(那是全量扫盘:官方根 200 个版本 236 ms,
|
||||
//! 叠加外部档案 1.35 s)——那等于把「点了就受理」换成「点了等一扫」。两条判据各用最便宜的
|
||||
//! 那个事实:
|
||||
//!
|
||||
//! | 实例 | 判据 | 成本 |
|
||||
//! |---|---|---|
|
||||
//! | 档案实例(id 不是 `ext-`) | `profiles.json` 里还有这条 id,且 `version_id` 没被改过 | 一次小文件读 |
|
||||
//! | 自动发现的(`ext-`) | 逐个根重算 id(哈希)定位到 (根, 版本),再用 core 的 `adopt::inspect` 判这一个版本 | #根 次哈希 + 一次单版本检查 |
|
||||
//!
|
||||
//! 反查不到根的 `ext-` id(M273 时代的老档案,哈希里没有根)**不在这里判死**:那种 id 的语义
|
||||
//! 本来就是「按配置根解析」,真没版本时 `build_request` 会给出具名阶段失败(`VersionManifest` +
|
||||
//! 路径),一个进程都没起来。改前那条同步扫盘也不会拦它——`version_present: false` 的磁贴在
|
||||
//! 旧实现里照样能点、照样走到同一条具名失败。
|
||||
//!
|
||||
//! ## 为什么挂在 [`super::Scan`] 上
|
||||
//!
|
||||
//! [`super::super::discovered`] / [`super::super::roots`] 是 `game` 的私有子模块,`host` 其它地方
|
||||
//! 只够得着 `game` 转出的那几个名字(`Scan` / `instances` / `game_for` …)。`impl` 块可以写在任何
|
||||
//! 模块里,于是这一层用关联函数挂在 [`super::Scan`] 上——语义也对:被判的正是「这份扫描账里的
|
||||
//! 那一条现在还成不成立」。
|
||||
|
||||
use unlucky_core::game::adopt;
|
||||
use unlucky_core::game::install::instance::read_profiles;
|
||||
|
||||
use crate::dto::InstanceSummary;
|
||||
|
||||
use super::super::super::HostEnv;
|
||||
use super::super::discovered::external_instance_id;
|
||||
use super::super::roots;
|
||||
|
||||
impl super::Scan {
|
||||
/// 这条快照条目**现在**还在不在盘上;不在就给一句给用户看的具名原因。
|
||||
///
|
||||
/// 只判这一条(成本见模块文档的表格),**不重扫 `versions/`**:调用方是「点启动」那条同步
|
||||
/// 路径,它必须便宜到能在同一拍里给出 Busy。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 返回的字符串是**给人读的原因**(presenter 没有 i18n 依赖,文案在这里拼,与 `toast` 同一种
|
||||
/// 形状):实例清单里没了 / 版本被改过 / 版本目录已不可用。调用方负责拒绝受理并重扫一遍。
|
||||
pub(in crate::host) fn revalidate(
|
||||
env: &HostEnv,
|
||||
instance: &InstanceSummary,
|
||||
) -> Result<(), String> {
|
||||
if crate::dto::is_discovered_instance(&instance.id) {
|
||||
return discovered(env, instance);
|
||||
}
|
||||
profile(env, instance)
|
||||
}
|
||||
}
|
||||
|
||||
/// 档案实例:`profiles.json` 里还有这条 id,且绑的版本没被改过。
|
||||
///
|
||||
/// 「版本被改过」也算过期:用户改的是**同一个档案**,但屏幕上那条磁贴写着旧版本、启动链也会按
|
||||
/// 旧版本装配——那不是他刚才看中的那一条。
|
||||
fn profile(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> {
|
||||
let profiles =
|
||||
read_profiles(&env.layout).map_err(|error| format!("读实例清单失败:{error}"))?;
|
||||
let Some(profile) = profiles
|
||||
.profiles
|
||||
.iter()
|
||||
.find(|profile| profile.id.as_str() == instance.id)
|
||||
else {
|
||||
return Err(format!(
|
||||
"实例 `{}` 已不在实例清单里(可能刚在别处被删掉),列表已过期",
|
||||
instance.name
|
||||
));
|
||||
};
|
||||
if profile.version_id != instance.version_id {
|
||||
return Err(format!(
|
||||
"实例 `{}` 绑的版本已被改成 `{}`(列表里还是 `{}`),列表已过期",
|
||||
instance.name, profile.version_id, instance.version_id
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 自动发现的实例:id 是 (根, 版本) 的哈希,逐个根重算一遍定位,再判这一个版本。
|
||||
///
|
||||
/// 反查不到根 ⇒ 放行(M273 老档案,理由见模块文档):不猜、也不在这里改成「拒绝」,
|
||||
/// 否则所有老用户的 `ext-` 磁贴会集体点不动。
|
||||
fn discovered(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> {
|
||||
for root in roots::all(env) {
|
||||
if external_instance_id(root.key(), &instance.version_id) != instance.id {
|
||||
continue;
|
||||
}
|
||||
return adopt::inspect(root.game(), &env.layout, &instance.version_id)
|
||||
.map(|_| ())
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"实例 `{}` 的版本 `{}` 已不可用({error}),列表已过期",
|
||||
instance.name, instance.version_id
|
||||
)
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user