Skip to content

fix(manage): 依赖健康检查不再随依赖路径数爆炸(修「打开管理页就未响应」) - #93

Open
std-external wants to merge 1 commit into
std-microblock:masterfrom
std-external:fix/manage-health-walk
Open

std-external wants to merge 1 commit into
std-microblock:masterfrom
std-external:fix/manage-health-walk

Conversation

@std-external

Copy link
Copy Markdown
Contributor

问题

打开「管理 Mod」页时整个窗口卡死(Windows 未响应)。有玩家在小 Mod 列表上没感觉,Mod 装得多、helper 之间互相依赖时一进管理页就假死。

原因

getDependencyHealth()(src/celemod-ui/src/stores/manage.ts:413)做依赖健康检查时用的是一个「当前路径」集合,递归返回后就把节点从集合里删掉。于是同一个 Mod 会被它经过的每一条依赖路径各走一遍,代价随路径数增长 —— helper 层共用得越多,路径数越爆炸。

而它在这两个地方每次渲染都会被调用:

  • selectVisibleRootNames() 对每一个 Mod 都调用一次,而且不管健康筛选是不是 all 都算;
  • ManageTreeNode(src/celemod-ui/src/routes/Manage.tsx:840)每渲染一行都调用一次(只为了错误徽标)。

所以打开管理页 = 一次渲染里跑几百次指数级的依赖遍历,主线程被 JS 占住 → 窗口假死。

本机实测(300 个 Mod + 20 个 helper,每个 helper 依赖后面两个):

场景 修复前 修复后
管理页默认筛选(health=all),单次 selectVisibleRootNames 2.22 s 0.10 ms
同上,额外带可选依赖 3.33 s 0.10 ms
health 筛选实际要用时 2.22 s 7.5 ms
每个 helper 依赖 3 个(同样 300 Mod) 4 分钟以上没跑完 17 ms

管理页每次渲染要跑两遍 selectVisibleRootNames,所以默认视图以前就是 4.4–6.7 s 的白屏/假死,重一点的依赖图直接无限等下去。

修复

  • getDependencyHealth():一次检查里每个 Mod 只访问一次(visited 集合同时继续用来挡循环依赖)。同一个 Mod 无论从哪条路径到达,它自己的依赖和状态都一样,所以状态结论不变,只是不再重复走路径(顺带不会再把同一条问题重复塞进徽标提示)。
  • selectVisibleRootNames():健康筛选是 all(默认)时不再计算依赖健康,先跑其它便宜的筛选条件。

验证

  • 新增回归测试(src/celemod-ui/src/manageDependencies.test.ts,用 Proxy 统计节点访问次数):旧代码这两个测试 90 秒都跑不完,新代码 0.8 ms / 6.6 ms。
  • node --import tsx --test src/manageDependencies.test.ts src/manageDisplayNames.test.ts 20 个全过。
  • npx tsc --noEmit 通过;test:mod-updates / test:crash-trace / test:deep-link / test:catalog-download 通过。

另外发现(未改,供参考)

管理页调用的 apply_mod_profiles、resolve_duplicate_mod_files、switch_mod_profile_mods、update_blacklist_mod_file、get_current_profiles 等都是同步 tauri 命令(在主线程执行),而 blacklist.rs 里它们又会调用 get_installed_mods_sync() → 整目录扫描 + get_mod_cached_new()(目录缓存不新鲜时最多阻塞 8 秒)。这条路径也可能是「未响应」的来源,但那需要具体环境才能定论,这次没动。

来源:Celeste 玩家交流群反馈(一开管理页就未响应)。

The dependency health check walked every dependency path.  A Mod that
several Mods depend on (a shared helper layer) is reachable through all of
them, so the walk grew with the number of paths: opening the 管理 Mod page
ran it for every Mod several times per render, which freezes the window for
seconds or minutes on a large Mods folder.

- Visit each Mod at most once per check (the visited set still stops cycles).
- Only walk when the health filter actually asks for it; the default view
  does not need the result.

Measured on a Mods folder of 300 Mods with a 20-Mod helper layer (each helper
depending on the next two): 2.2-3.3s per render before, 0.1-12ms after;
with three dependencies per helper the old code did not finish in 4 minutes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant