VFS、索引、Gists 与性能实战¶
VFS 与索引 介绍了基础概念。本章关注实战:什么时候刷新 VFS、怎么监听文件变化、什么时候用 File-Based Index、Stub Index 或 Gist,以及如何避免索引和 Dumb Mode 问题拖垮用户体验。
VFS 快照不是磁盘本身¶
VFS 维护的是应用级快照,不是项目级快照。只有被访问过的文件或目录才会进入快照;目录内容也只有在读取子项后才会被加载。这意味着:
- UI 看到的是 VFS 快照,不一定立刻等于磁盘状态。
- 外部工具修改文件后,需要刷新才会被 IDE 看到。
- 已经加载过的文件会留在快照中,除非磁盘删除且父目录刷新。
- ignored/excluded 文件不会被 VFS 自动跳过;上层代码必须自己过滤。
- 同一个磁盘文件在 IDE 生命周期中可能对应多个
VirtualFile实例,但它们相等、hashCode 相同并共享 user data。
刷新策略¶
优先异步刷新:
virtualFile.refresh(false, true) {
// runs after refresh
}
需要批量刷新时使用 RefreshQueue.createSession()。同步刷新会阻塞调用线程;如果后台线程持有 read lock 时发起同步刷新,可能死锁。
实践规则:
- 外部进程生成文件后,对输出目录发起异步刷新。
- 不要在 Read Action 内调用同步刷新。
LocalFileSystem.refreshAndFindFileByPath()找不到文件时可能触发局部刷新,也要遵守锁规则。- 文件内容变化但时间戳不变,IDE 可能无法检测到。
- native file watcher 存在时,刷新只处理报告过变化的路径;没有 watcher 时会遍历刷新范围。
调试 watcher 覆盖范围可用内部动作:
Tools | Internal Actions | VFS | Show Watched VFS Roots
VFS 事件¶
VFS 事件在 EDT 中、write action 下发送。最常用的是 BulkFileListener:
project.messageBus.connect(disposable).subscribe(
VirtualFileManager.VFS_CHANGES,
object : BulkFileListener {
override fun after(events: List<VFileEvent>) {
val index = ProjectFileIndex.getInstance(project)
val relevant = events.filter { event ->
event.file?.let(index::isInContent) == true
}
// handle relevant events
}
}
)
如果需要非阻塞预处理,考虑 AsyncFileListener。
事件注意点:
- VFS listener 是应用级,会收到所有打开项目的事件。
- 必须用
ProjectFileIndex、路径、文件类型等过滤。 - refresh 触发的事件发生时,磁盘变化已经发生。
- before deletion 中,磁盘文件可能已经删除,但 VFS 快照仍能读到最后内容。
- 未加载进快照的文件变化不会产生事件。
File-Based Index、Stub Index、Gist 怎么选¶
| 需求 | 推荐 |
|---|---|
| 按文件内容或轻量元数据查找文件 | File-Based Index |
| 查找声明、函数、类、可见符号等 PSI 元素 | Stub Index |
| 只对少量文件按需计算并缓存结果 | Gist |
| 只需要当前文件临时结果 | 普通缓存或 CachedValue |
File-Based Index 查询结果是文件集合。Stub Index 查询结果是 PSI 元素。自定义语言如果需要跨文件 resolve、补全、导航,通常应投资 Stub Tree 和 Stub Index。
Gists¶
Gist 适合满足这些条件的场景:
- 不需要全项目聚合。
- 不想在 indexing 阶段预计算全量数据。
- 数据能在请求时从单个文件快速计算。
- 结果可以持久缓存到磁盘。
两类常用 Gist:
VirtualFileGist:基于文件内容或元数据计算。PsiFileGist:基于 PSI 文件计算。
示例场景:
- 图片尺寸、位深等 UI 展示信息。
- Java 文件中某些简单属性。
- 单文件配置摘要。
不要把 Gist 当成通用数据库。需要跨文件聚合、反向查找或按 key 检索时,仍然用 Index。
Dumb Mode 实战¶
索引期间,任何访问索引的功能都可能抛 IndexNotReadyException。处理方式:
DumbService.getInstance(project).runWhenSmart {
// index-dependent work
}
可在 Dumb Mode 运行的扩展实现 DumbAware。Action 要继承 DumbAwareAction,不要只覆写 isDumbAware()。
2025.1+ Plugin DevKit 提供 Can be DumbAware 检查,帮助识别可以安全标记的实现。
测试:
Tools | Internal Actions | Enter/Exit Dumb Mode
索引性能¶
索引代码的成本会乘以项目文件数量,必须保守:
- 能用 Lexer 信息就不要构建 AST。
- 必须看结构时,优先 Light AST。
- 从
PsiDependentFileContent.getLighterAST()获取轻量树。 - 遍历 Light AST 时只访问必要节点。
- Stub Index 实现
LightStubBuilder。 - lazy-parseable 元素如果通常不含 stub,可实现
StubBuilder.skipChildProcessingWhenBuildingStubs()。 - XML 索引可评估
NanoXmlUtil。
开发实例会在 logs 目录生成 indexing performance metrics,2021.1+ 还有 HTML 格式。性能问题不要凭感觉猜,先看指标。
Shared Indexes¶
大项目可以考虑 shared project indexes。它能减少用户本地首次索引成本,但会带来生成、存储、版本匹配和 CI 维护成本。适合:
- 企业内大型单仓。
- IDE 和项目版本较稳定。
- 有 CI 资源生成预构建索引。
检查清单¶
- 外部文件生成后有异步刷新。
- VFS 事件按项目、内容根和文件类型过滤。
- 不在 read lock 内同步刷新。
- 不全项目遍历 PSI 替代索引。
- Dumb Mode 下不访问索引,或显式延迟到 Smart Mode。
- 索引构建不加载完整 AST,除非确实必要。
- 单文件懒计算用 Gist,而不是滥建全项目索引。
- 大项目用指标验证索引耗时。