
Mole 的缓存失效、数值一致性、进度反馈与采样新鲜度macOS 清理工具的状态核算工程实践【免费下载链接】Mole Clean, uninstall, analyze, optimize, and monitor your Mac. Free open-source CLI, plus a native Mac app.项目地址: https://gitcode.com/GitHub_Trending/mole15/Mole本文以 Mole 仓库中 state-accounting-and-progress.md 这份缺陷模式参考文档为主体系统讲解 Mole 在持久化派生数据、双路径数值一致性、终端进度反馈、异步采样新鲜度四个方面的工程约束。该文档是 Mole 缺陷模式目录bugs/SKILL.md中编号 8、9、10、16 四类复发性缺陷的修复准则。读完后你可以掌握如何为每个缓存派生值确定 schema 版本、TTL 与失效条件如何避免 dry-run 预览与最终汇总对同一数字给出不同答案如何让慢速扫描在终端里看起来活着以及如何用代次标签与原子采样契约管理异步指标的过期数据。一、背景为什么需要一份状态与核算缺陷清单Mole 是一个 macOS 清理与分析工具CLI 加原生 App其核心动作——扫描目录、预估可清理空间、执行删除、展示系统状态——全部依赖计算出来的派生值目录大小快照、清理预览总额、指标采样、异步探针结果。这些值一旦出错用户看到的可能不是数据不准而是删错了东西或界面卡死了。在 SKILL.md 的路由表中这四类问题各有明确的第一探针编号复发性形态第一探针8持久化派生数据比其算法活得更久追踪 schema、TTL、证据指纹与变更点9两条路径用不同算法计算同一个数字找出所有生产者选定唯一定义10慢速工作看起来像冻结找出所有约超过一秒且无反馈的操作16异步或缓存数据没有代次或新鲜度契约把结果绑定到请求纪元保持采集时间、stale、完整性字段整体一致原文档开头给出了适用条件当一次修复涉及缓存派生值、展示合计、预览核算或终端反馈时序时应阅读此参考。以下逐条展开并结合 Mole 仓库中的真实实现与测试佐证每一条准则的落点。二、模式 8持久化派生数据比其算法活得更久2.1 核心命题改变了一个计算却不清空它的缓存就会让旧结果在源码修复之后继续被使用。文档给出的代表形态是提交7a996aa5硬链接去重hardlink-dedup的语义变化要求同时提升缓存 schema 版本并把依赖去重的子树标记为不可缓存。Analyze 命令历史上还需要过期机制、变更驱动的失效以及一条绕过嵌套缓存的手动刷新路径。2.2 Mole 源码中的落点schema 版本 TTL 失效在 cmd/analyze/cache.go 中可以看到准则的第一条schema version如何落地// v2: analyze deduplicates hardlinked files to match du. // v3: ordinary Parallels VM storage is included instead of skipped by name. const cacheSchemaVersion 3注释直接记录了 v2 就是硬链接去重这次语义变化——与文档中7a996aa5的叙述相互印证。读取端 loadRawCacheFromDisk 对 schema 不匹配的条目直接删除并报错而不是静默复用if entry.SchemaVersion ! cacheSchemaVersion { _ os.Remove(cachePath) return nil, fmt.Errorf(cache schema mismatch: got %d, want %d, entry.SchemaVersion, cacheSchemaVersion) }文档要求为每个持久化派生值识别的五项清单在 Mole 的 analyze 缓存里逐一有对应实现Schema 版本cacheSchemaVersion 3不匹配即作废见上。TTLconstants.go 定义了analyzerCacheTTL 7*24h、overviewCacheTTL 7*24h、staleCacheTTL 3*24h首次绘制窗口、cacheModTimeGrace 30m目录 mtime 噪声宽限、cacheReuseWindow 24h。loadCacheFromDisk 同时校验扫描年龄、目录 ModTime 与宽限窗口loadStaleCacheFromDisk 则提供宽松加载通道用于先画第一帧再后台刷新。对每个输入变更的失效invalidateCache 与 invalidateCacheTree后者因 issue #812 增加失效目标目录及其全部直接子目录防止重扫时复用过期子目录大小。调用者是否把该值当作不存在的证明这正是 2.3 节要单独讨论的完整性问题。验证跑是否读到了上一个 release 的数据schema 版本检查 启动时的 pruneAnalyzerCache 清扫保证旧版本写入的条目不会进入新版本会话。此外缓存写入本身也是原子且可裁剪的条目通过临时文件 os.Rename落盘避免杀进程留下截断文件pruneAnalyzerCacheDirWithLimits 用最小堆按修改时间逐批淘汰维持 constants.go 中analyzerCacheMaxEntries 5000/analyzerCacheMaxBytes 50MB的预算。注释里记录了一个真实案例无准入控制时曾有用户的缓存达到 188 万个文件 / 7.82GB。2.3 TTL 只证明不够旧从不证明完整这是文档中最重要的一个概念区分原文A TTL proves only that an entry is not too old. It never proves completeness.Mole 中曾有一个真实缺陷pkg_receipt_nonstandard_app_paths --require-complete一度接受一小时前的 pkgutil 回执缓存作为不存在同级安装sibling install的证据。问题在于复查缓存中的路径可以删掉失效条目却无法发现新安装的属主。删除操作把缓存里没有读成系统里不存在于是误删。修复提交b4f00651把完整性绑定到pkgutil --pkgs输出的指纹上——新证据出现即作废旧条目。这条修复在 lib/core/pkg_receipts.sh 中可以完整看到指纹生成L49-L52pkgutil --pkgs输出经cksum归一化为纯数字与连字符组成的指纹。注释解释了为什么选它作缓存键——安装包必然新增回执回执变化即指纹变化从而强制重扫。缓存命中条件L59-L81文件头必须是#receipts:指纹且通过 TTL默认 3600 秒可用MOLE_PKG_RECEIPT_CACHE_TTL调整检查才可用且当调用方要求--require-complete时无指纹的缓存一律不可用L60 的[[ -n $receipts_fingerprint || $require_complete ! 1 ]]。超时语义区分L90-L97普通模式下扫描超时只是break提前结束返回部分结果--require-complete模式下超时返回 124把不完整显式暴露给调用方而不是静默当作空结果。写缓存L177-L197无法生成指纹时宁可不写留下一个永远不可能命中的文件比不写更危险。删除路径如何消费这个完整证明可以在 lib/uninstall/batch.sh 看到批量卸载调用pkg_receipt_nonstandard_app_paths --require-complete其空输出即没有其他安装拥有这些残留文件的权威证据同文件 L1393 注释Complete absence proof; the empty fingerprint is authoritative.。文档由此给出的通用规则当调用者要用缓存数据授权删除时要么绕过缓存要么把缓存绑定到其出现会改变结论的所有证据的指纹上。三、模式 9两条路径计算同一个数字算法不同3.1 核心命题任何被渲染两次的值最终都会不一致dry-run 预览对最终汇总、条目数对原始目标数、子树大小对du、十进制单位对二进制单位。文档给出的处置方法找出所有生产者选定唯一定义优先把已测得的值传入汇总/渲染端而不是重新计算然后在回归测试里比较两个渲染面而不是钉死某个无关的字面量。3.2 Mole 的核算规则Accounting Rules文档列出的五条规则直接对应 Mole 清理管线的实现约束被过滤、被拒绝、超时、失败或已消失的候选既不贡献清理条目数也不贡献回收字节dry-run 与真实模式使用同一批合格候选只是动作不同大小超时只能产生显式的未知或部分合计绝不许伪造一个看起来完整的零大候选快速路径可以跳过逐项精确计量前提是输出必须声明合计是部分的或未扫描的硬链接必须按同一条具名策略在子树与汇总两条路径上计数这正与第二章 v2 schema 的硬链接去重以对齐du呼应——两条路径共用同一策略否则预览和汇总必然分歧。3.3 回归测试比较两个渲染面而非钉死字面量tests/clean_core.bats 中的用例mo clean --dry-run keeps container totals and preview paths consistent (#1282)是预览 vs 汇总模式的活样例它先解析预览文件中的# Potential cleanup:/# Items:/# Categories:三个头部再断言最终输出中这三个数字与预览文件一致preview_total$(sed -n s/^# Potential cleanup: //p $preview) preview_items$(sed -n s/^# Items: //p $preview) preview_categories$(sed -n s/^# Categories: //p $preview) ... grep -F Items: $preview_items | grep -F Categories: $preview_categories | grep -qF $preview_total || return 1注意测试的写法它没有写死 Items: 3 这样的字面量而是从预览面取值再与汇总面比对——这正是文档所要求的compare the two rendered surfaces in a regression test rather than pinning an unrelated literal。同文件 L221-L242 还验证了另一条规则guard 拒绝的候选必须在 dry-run 下同样阻止预览登记A guard that refuses must stop the preview the same way it stops the real run即拒绝项不进入预览账本。3.4 单位定义也要唯一定义十进制对二进制单位这一对分歧在 Mole 中被 internal/units/bytes.go 显式管理。包注释说明了两条命令有意采用不同约定analyze格式化磁盘数字用 SI1000 进制以对齐 Finder/diskutilstatus报告内存与实时计数器用二进制1024 进制以对齐活动监视器与 gopsutil。包内提供BytesSI、BytesBin、BytesBinShort、BytesBinCompact四个格式化器连边界语义都是刻意区分的BytesBin用使 1024 仍显示为 1024 BBytesBinShort用使 1024 升格为 1K。把定义集中在一处任何精度/标签调整都只改一个文件——这是选择唯一定义准则在单位层的落地。四、模式 10沉默会被读成冻结4.1 核心命题慢速工作在 spinner 窗口之外即使有界看起来也像挂死。文档记录了代表案例提交8f064707中一个删除循环在做昂贵工作之前就停掉了 spinnerdotdir、登录项、System Data、大文件扫描都出现过同构问题。修复方法是走查完整的渲染区块测量每一个大约超过一秒的操作。4.2 Mole 的输出节奏文档给出了 Mole 的终端区块节奏rhythmsection title loading state content one trailing blank line两条关键时序规则spinner 必须在会覆盖它的输出出现之前立即停止如果之后还有更多静默工作则重新启动 spinner超时警告不能替代健康慢扫描期间的进度反馈。在 cmd/analyze/constants.go 可以看到与可见进度直接相关的参数batchUpdateSize 100每批多少条目刷新一次 UI、uiTickInterval 100msUI 心跳间隔、scanSendTimeout 100ms、scanPathInlineMinWidth 24终端窄于 24 列时扫描路径独占一行避免过度截断。spinnerFrames则定义了| / - \四帧动画。这些常量共同保证扫描期间 UI 以 100ms 级心跳推进进度数字以 100 条为一批更新慢操作不会长时间停留在同一帧。4.3 性能工作的两张凭证与禁区文档要求性能优化必须交出两份凭证有界的微基准或调用次数不变量隔离被测路径同一模式、同一机器条件下的端到端命令计时。同时明确了一条禁区原文不要靠缓存目录大小来优化APFS 不会把子孙文件的 mtime 传播到父目录。这条禁区解释了 constants.go 为什么需要cacheModTimeGrace 30 * time.Minute宽限窗口——macOS 上目录 ModTime 噪声大见 cache.go 的注释Directory mod time is noisy on macOS; reuse recent cache...直接以父目录 mtime 判断内容变化在 APFS 上不可靠。文档建议的优化方向是不存在的目标、重复的属主工具启动、仅用于报告的计量、错误作用域的扫描而对破坏性工作最终属主探测与身份重绑定即使昂贵也必须保留与 SKILL.md Do not trade final-sink rebinding or fail-closed owner checks for speed 一致。五、模式 16异步代次与采样新鲜度是同一份契约5.1 代次generation契约文档的核心命题一个异步结果在它产生时可以是有效的到达时却可能已经过期。处置方法每个请求打上单调变化的代次或探针 ID随结果消息携带结果只应用到匹配的代次上。代次递增发生在刷新或导航过渡产生新请求时而不是视图重绘时。测试要求双向覆盖生命周期旧结果不得覆盖新刷新当用户处于钻取drill-down状态时一个匹配代次的结果到达仍然值得保留返回概览时必须调度新探针而不是把旧结果当新测数据展示。文档举例异步 Time Machine 计数需要覆盖刷新、离开、返回、乱序到达四类用例而不是只测一条快乐路径命令。5.2 缓存指标的原子采样契约缓存指标使用平行契约——把相关字段当作一个原子样本对待value group collected_at stale completeness刷新瞬时失败时返回错误但保留上一成功组的可见性带上其原始采集时间与staletrue。三个禁止项不许把旧值和新刷新时间拼在一起不许只清空组内一半字段不许把未测数据变成测得的零。下一次成功采样整体替换该组并把 stale 复位为 false。这条契约在 Mole 源码中有直接的结构性证据。cmd/status/metrics.go 的MetricsSnapshot中进程相关字段被刻意设计为可空指针并成组出现ProcessCollectedAt *time.Time json:process_collected_at,omitempty ProcessStale *bool json:process_stale,omitempty ZombieCount *int json:zombie_count,omitempty ZombieParents []ZombieParent json:zombie_parents ZombieParentsComplete *bool json:zombie_parents_complete,omitempty文档明确点名状态进程行、僵尸计数、父进程归属与父进程完整性是同一组——对应到上表就是TopProcessesProcessCollectedAtProcessStale与ZombieCountZombieParentsZombieParentsComplete。指针类型区分测得的零ZombieCount 0与从未测得字段缺省collected_at与stale随组整体替换。顶层还有快照级的CollectedAtL63保证每个样本自带采集时间。文档最后一条要求每个序列化器与 fast/full/watch 路径都必须保持新鲜数据 / 过期但最后已知良好 / 测得的零 / 从未测得四种状态的区别一个只测首次采集、不测下一帧或消费它的失败刷新的缓存测试是不完整的。六、可操作的核查清单把四节准则压缩成一条可直接执行的检查流程与原文档一一对应每个持久化派生值schema 版本TTL每个输入变更点是否都触发了失效调用者是否把缓存值当作不存在的证明验证跑会不会读到上一 release 的数据证据cmd/analyze/cache.go、lib/core/pkg_receipts.sh每个被渲染两次的数值列出所有生产者选定唯一定义把测量值传入渲染端回归测试比较两个渲染面。证据tests/clean_core.bats、internal/units/bytes.go每个渲染区块逐操作计时凡约超过一秒且无反馈的纳入 spinner 管理spinner 先停后输出超时警告不替代进度。证据cmd/analyze/constants.go 的uiTickInterval/batchUpdateSize每个异步结果有代次标签、按代次应用、双向生命周期测试每个缓存指标组保持value group collected_at stale completeness原子替换。证据cmd/status/metrics.go 的ProcessStale/ZombieParentsComplete成组字段这四类缺陷的共同根源是把数据曾经被正确计算过误当作数据现在仍然可用。Mole 的实践给出了一致的答案让每个派生值自带版本、时间、完整性与归属字段让任何消费方尤其是授权删除的消费方在消费前先验证这些字段而不是信任数据本身的在场。【免费下载链接】Mole Clean, uninstall, analyze, optimize, and monitor your Mac. Free open-source CLI, plus a native Mac app.项目地址: https://gitcode.com/GitHub_Trending/mole15/Mole创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考