Rascal TypePal 语言服务器打包为 VSIX 后启动缓慢,如何优化?
将 .rsc 文件放进 JAR 并打包为 VSIX,并不意味着这些模块已经变成可直接执行的 JVM 字节码。优化启动时,应先定位耗时阶段,再分别处理 runtime 启动、模块加载和首次语义分析。现有信息不足以确认瓶颈在 TypePal。
TypePal 的实际分析可以放在语言服务回调中按需执行,但这不一定会推迟 TypePal 模块本身的加载。至于 .tpl,应先确认它在所用版本中的用途,不能仅凭官方扩展包含该文件就认定它能加速 runtime 初始化。
先把“启动慢”拆成几个阶段
建议记录以下节点的时间:
- VSCode 扩展开始激活。
- 开始创建语言服务器进程。
- Java 入口开始执行。
- Rascal runtime 创建完成。
- 入口模块及依赖加载完成。
- 语言服务注册完成、LSP 初始化完成。
- 首次解析和首次 TypePal 分析完成。
这些节点不一定严格按上述顺序出现,应以实际启动流程为准。记录时要区分“扩展尚未启动服务器”“服务器尚未就绪”和“服务器已就绪但首次诊断尚未完成”。
下面的计时代码可以放在扩展端。需要将标记插入现有启动流程的对应位置,服务器内部还需单独记录:
const startupStart = Date.now();
function markStartup(stage: string): void {
console.error(`[startup +${Date.now() - startupStart}ms] ${stage}`);
}
// 在现有流程的对应位置调用:
// markStartup("extension activation started");
// markStartup("server process launch requested");
// markStartup("LSP initialization completed");
如果服务器通过 stdio 传输 LSP 消息,服务器端调试日志应写到 stderr 或独立日志文件,避免污染 stdout 中的协议消息。
至少比较三种场景:没有目标语言文件打开、打开一个小文件、恢复包含多个目标文件的工作区。如果只有后两种明显变慢,应重点检查首次解析、分析调度和重复任务;如果没有文件打开也很慢,再集中排查进程与模块初始化。
用最小服务判断模块加载是否占主导
做两个临时对照版本:
- 只提供 parsing 的版本,同时移除入口模块对 TypePal 分析实现的直接及间接依赖。
- 保留完整依赖,但暂时不执行实际 TypePal 分析的版本。
第一个版本有助于判断依赖加载的影响,第二个版本有助于判断分析执行的影响。仅注释掉分析函数调用,仍可能保留原有 import 链,因此不能据此排除模块加载成本。
如果 parsing 版本仍然启动缓慢,再检查 runtime 创建、模块搜索路径、解析器构造,以及是否重复创建服务器实例。若日志显示卡在依赖解析或文件访问,再排查相应的磁盘或网络访问,不必提前将这些因素认定为根因。
.tpl 能否替代预编译代码?
需要区分供类型检查使用的接口模型和供 runtime 执行的代码。Rascal 的 Compile Mojo 文档将 .tpl 描述为 TModel,用于表示模块接口、支持模块化类型检查。该页面的注意事项还明确说明,其所述实现尚未启用 JVM 字节码生成。因此,生成 .tpl 本身不能证明语言服务器启动时省去了源码模块加载和解释执行成本。Rascal Compile Mojo 文档
该页面标注的 Rascal 版本为 0.42.2。由于你没有提供实际使用的版本,不能直接将其构建行为套用到当前项目。应先核对 Rascal、rascal-lsp、TypePal 和 rascal-maven-plugin 的具体版本,再查看对应版本的构建配置。
文档中的 Maven goal 是 mvn rascal:compile。原问题提到的 mvn:compile 需要回到实际 pom.xml 核实;某个项目移除了相关配置,也不能单独证明整个编译功能已经被移除。Rascal Compile Mojo 文档
如何让 TypePal 按需执行?
将 TypePal 的分析调用放到文件分析回调中,避免在服务注册函数、模块级变量初始化或扩展激活阶段主动分析整个工作区。
util::LanguageServer 文档区分了两种服务:analysis 面向交互式文件分析,在输入暂停时按需触发;build 在保存时触发,适合更重的检查。可以据此将快速文件检查和较重的全程序检查分开。具体构造函数与行为应以项目依赖版本为准。util::LanguageServer 文档
实现时还要注意:
- 回调应接收并分析当前文件内容,避免每次重新扫描整个项目。
- 同一内容版本的分析结果可以复用;文件内容或依赖变化时需要失效。
- 如果自行增加异步任务,应避免旧任务完成后覆盖新版本的诊断结果。
- 将调用移入回调只能推迟计算;要推迟依赖加载,还需调整模块依赖和服务注册方式。
官方 Pico 示例展示了分阶段提供语言服务的思路,并说明服务可以异步、增量注册。可以参考它先提供基础能力,再增加较重功能,但仍需按实际版本验证注册和更新行为。Pico LanguageServer 示例
预加载和缓存应该缓存什么?
预加载通常会把等待提前到另一时刻。如果在每次启动的同一条关键路径上预加载相同模块,总等待时间未必减少。可以考虑在基础服务可用后预热后续能力,但应同时测量首次编辑延迟,确认收益。
缓存则应区分用途:
| 对象 | 可以考虑的策略 | 需要注意的问题 |
|---|---|---|
| 已加载模块 | 在现有服务生命周期内复用运行环境 | 不应假定进程退出后仍能保留执行状态 |
| 文件分析结果 | 按文件内容、分析配置及依赖状态复用 | 依赖变化也可能使结果失效 |
| 跨文件索引 | 保存可复用的项目分析数据 | 需要明确更新范围和失效规则 |
| .tpl | 按对应版本的类型检查流程生成和分发 | 不能当作通用 runtime 快照 |
如果要将自定义分析结果持久化到磁盘,应让缓存标识包含必要的工具版本和配置,并在不匹配时重新计算。不要未经验证就序列化整个运行环境,也不要假定安装 VSIX 会自动建立跨进程模块缓存。
推荐的实施顺序
先完成分阶段计时,再通过 parsing 最小版本确定依赖加载成本。随后将实际 TypePal 计算移出初始化路径,把交互检查与保存时较重的检查分开,最后根据测量结果引入结果缓存或分阶段注册。
每次调整都要比较服务器就绪时间、首次文件分析时间和后续编辑响应时间。官方扩展启动较快只能作为比较线索;在缺少相同版本、相同依赖和启动日志的情况下,不能把差异直接归因于 .tpl。
备注:内容仅供参考。
