Nick zhu
Senior Program Manager, Developer Division at Microsoft
大家好,欢迎来到最新的 Visual Studio Code Java 更新!在这篇文章中,我们将深入解析最近代码补全的性能优化。
性能提升 – 更快的代码补全
随着 Java Language Server 最近 1.0 版本的发布,我们对代码补全的性能进行了重大的改进。下图比较了最近几个版本之间的代码补全响应时间。对于补全类型、构造函数名等常见的场景,代码补全性能较之前版本(v0.80、v0.81、v.0.82)有显著的提升。
性能改进总览
代码补全引擎由三个阶段组成:
- 阶段一 (P1) – 搜索索引器以查找建议
- 阶段二 (P2) – 转换建议为补全信息
- 阶段三 (P3) – 计算代码片段建议
根据我们的分析,我们发现所有三个阶段都有改进的空间。下表显示了我们在过去版本中所做的改进。我们将在下一节中更多地讨论这些变化的细节。
关键改动细节
版本 0.81.0 – 减少Windows I/O操作. #1831
在过去的性能测试中,我们发现很大一部分时间成本花在了计算文件 URI上面。这个发现佐证了我们之前的观察:由于JVM 中特定于平台的文件系统相关实现,Windows 平台上的代码补全性能相对较差。通过删除不必要的 URI 计算,我们提高了性能,尤其是在 Windows 平台上。
版本 0.81.0 – 对常量/默认值进行优化. #1835
当我们完成一个常量字段(例如 Constants.*)时,完成弹出窗口将在选择列表中显示建议的字段名称及其常量值(例如 Bit1 : int = 1)。我们的分析发现,当类包含大量常量字段成员时,这会让补全非常慢。这是因为我们从 AST Tree 计算字段值,这在操作大文件时开销很大。
为了优化它,我们决定推迟解析常量值。代码补全会简化建议标签并仅显示字段名称(例如 Bit1 :int)。当您将鼠标悬停在 Javadoc 的完成项上时,才会在Javadoc 部分显示它的常量值。
以下是一个拥有1400 多行和 150 多个常量字段的类的字段完成的性能比较。
版本 0.82.0 – 延迟解析通用代码片段Delay resolving generic snippets. #1838
有两种类型的代码段:
- 通用片段(例如 foreach、fori、ifelse 等)
- 类型定义片段(例如类、接口等)
对于通用代码段,它会在构建完成项的“TextEdit”之前评估具有给定上下文的模板,此类评估可能会很昂贵。现在我们将这类评估推迟到解决阶段。当代码补全完成项被建立完成之后,模板模式将作为占位符填充。实际值在解决阶段进行评估,这不会阻止完成项目的显示。这也是一项关于“延迟解析 TextEdit”可以在多大程度上提高性能的实验,并且在大多数情况下,它应该运行良好。
版本 0.82.0 – 优化匿名构造函数Optimize for anonymous constructors. #1836
当我们想完成一个新的Runnable时, 期望的结果是这样的:
Runnable() {}
它由两部分组成
- Runnable 名称
- 空白的body片段 body () {\n\t\n}
通过性能分析,我们发现 CodeFormatUtil.format 花费了大量时间。
为了有一个正确的缩进和行分隔符,它们被格式化为当前的首选项。格式化是昂贵的,并且为所有项目(有时多达数千)重复格式化相同的内容(空体)。为了改进它,我们将空体格式化进行了一次性的操作并在所有项目中重用它。
版本 1.0.0 – 提升代码补全搜索速度
为了优化索引搜索性能,我们做了两个关键改动。
我们的性能分析显示,索引查询任务97% 的 CPU 时间用于从磁盘加载索引内容的 I/O操作。这是因为我们使用的索引机制倾向于节省内存并且在搜索引擎中使用很少的缓存。几乎每个查询都必须从磁盘重新加载索引内容。一种直接的优化是降低 I/O 的频率。
Java 索引器由多个哈希表组成,每个哈希表用于记录某种类型的代码部分,例如类型声明、方法声明、引用、方法引用等。典型的查询作业从索引中读取一个或多个哈希表,然后连接这些将条目索引到目标结果中。
当我们完成类型/构造函数名称(例如 Str或者new Str)时,索引查询作业读取两个哈希表,一个是用于查找匹配类型名称的 typeDecl 表,另一个是用于查找类文件路径的 documentName 表声明相应的类型。由于我们的目的只是完成类型名称并自动导入对应的package,因此 typeDecl 表足以满足我们的要求,并且不需要类文件路径。我们的优化是只读取 typeDecl 索引表,结果证明少读取一张索引表可以节省大量 I/O 成本。
- 优化索引阅读操作. #574464
这个改动来自于社区开发者对上游 JDT 项目的贡献。Java 索引使用 UTF-8 对索引字符进行编码。加载索引时,我们会将它们解码回来。由于大多数索引字符只是ASCII字符,我们优化了解码方法,使其读取ASCII更快。
未来计划
我们上面列出的改进使自动完成速度更快,但我们还没有完成。未来,性能仍然是我们的首要任务,我们将继续优化自动完成性能。以下是我们在未来几个月内计划的一些项目:
延迟文本解析 (Lazy Resolve TextEdit)
由于大多数语言客户端不支持完成项的延迟解析文本编辑,Java 语言服务器必须计算完成响应中所有完成项的文本编辑。这是最昂贵的计算的原因。我们正在与作者合作探索对延迟解析文本编辑的支持。
更高效的索引架构 More Efficient Indexer
当前索引数据对于构造函数等一些代码完成场景来说是不够的。例如,构造函数完成需要知道该类是否具有泛型类型参数,并决定是否在构造函数引用中添加菱形<>。构造函数索引表没有包含这样的类型参数信息,我们必须从 Java 模型中解析它们,此类的解析操作成本很大。我们正在考虑优化索引架构以包含更多信息。
反馈与建议
请积极使用我们的产品!您的反馈和建议对我们非常重要,并将帮助我们做得更好。有几种方法可以给我们留下反馈: