好久没有更新Reqable的项目日志了,今天趁着版本发出去的一点点空闲时间,写一篇文章记录下前几天的一个重要性优化。
背景介绍
Reqable中JSON语法高亮的性能问题由来已久了,早在2023年9月份的时候,就有用户反馈JSON单个字段超过8055后无法正常高亮,详见 Issue #195,当时评估后不太好解决就挂起了。前段时间,又有用户反馈不到2M的JSON文件高亮渲染性能很差,详见 Issue #1421。这次,我决定把这个问题再次好好捋一捋。
语法高亮原理
先说说Reqable中是如何实现文本语法高亮的。
早期我在Github上面找到一个非常不错的开源项目highlight.dart,这个项目是基于highlight.js的原理实现的,一个是版本太低,一个是实现逻辑有些问题,因此存在不少Bug。
放弃之后,我们决定参考highlight.dart的思路,将highlight.js用Dart语言严格地完完整整地翻译了一遍(已经开源re-highlight),才总算实现了语法高亮功能。
接下来,我们说说highlight.js这个项目,实现主要分为三个部分语言模式
、处理器
和样式定义
。语言模式
定义了各种语言的语法,例如关键词、变量定义、函数定义等等语法规则,大量利用正则表达式
进行语法定义和约束。处理器
负责根据指定的语言模式对输入文本进行匹配处理,输出这段文本中哪部分是关键词、哪部分是变量、哪部分是函数等等,打上标签。如果在未提前指定语言的情况下,还可以对所有语言进行处理,并输出每个语言的置信度,自动做一个语言检测。样式定义
则是定义了大量的主题样式,比如字体颜色、背景色、字体粗细等等,根据前面处理器输出结果用相应的样式进行渲染,便可以看到语法高亮的效果了。
相信看到这里,大家都能猜测到性能瓶颈在哪里了。没错,就是正则表达式。但是正则表达式效率低归低,但是JSON单个字段超过8055后无法正常高亮又是怎么回事呢?
Dart StackOverflow
JSON单个字段超过8055后无法正常高亮这个问题,在用户没有提出来之前,在2023年6月份的时候我们就已经发现了。在Release模式运行时,有些数据JSON高亮会失败,但是在Debug模式下同样的数据没有任何问题。执行语法高亮的逻辑是在一个单独的Isolate中执行的,在Release模式下没有任何输出,仿佛没有执行,一度让我感到困扰。
直到我在Dart仓库下发现这个 Dart compile: StackOverflow when running RegExp.matchAsPrefix ,Dart语言为了性能,AoT下Stack设计得比较小,容易触发StackOverflow。这个问题引来了highlight.js和Dart两边维护者关于正则表达式写法性能的相关讨论。重写正则表达式不可能,重新调参编译Dart VM也挺麻烦,当时又想要不自己重写一套JSON语法高亮解析器算了,因为其他事情要处理,就搁置了。
解决问题
现在回过头来,看这个问题,比较可行的方案就是单独给JSON写一套语法高亮解析器。输入字符串从头到尾扫描一遍,也就是O(n)的算法复杂度,肯定是不会有性能瓶颈的,JSON节点树深浅,即使是用递归也不会爆栈。问题在于,如何优雅地接入到现在的项目中?能否利用highlight.js本身的机制做到低成本接入?
再次回过头来看highlight.js的代码,直到看到下面这段,心中大喜:
js
/** @type {BeforeHighlightContext} */
const context = {
code,
language: languageName
};
// the plugin can change the desired language or the code to be highlighted
// just be changing the object it was passed
fire("before:highlight", context);
// a before plugin can usurp the result completely by providing it's own
// in which case we don't even need to call highlight
const result = context.result
? context.result
: _highlight(context.language, context.code, ignoreIllegals);
result.code = context.code;
// the plugin can change anything in result to suite it
fire("after:highlight", result);
原来highlight.js中已经提供了插件API,支持前置处理和后置处理,可以替换掉默认的高亮处理逻辑。所以,我只需要提供一个自定义的JSON解析插件然后注册进去即可,其他任何修改都不需要!
接下来,就是如何在插件里面实现JSON解析逻辑了,像JSON这种严格语法的解析器其实很好写的。不过需要注意的是,和常规的JSON解析不同,我们需要保留全部的空格、换行符、符号等等字符,而不是输出一个Map结构。另外,输出需要按照highlight.js定义的格式,在解析器中还需要做一些特殊处理。终于在Github Copilot的加持下,写完了这个插件,大约300行代码,详见:github.com/reqable/re-...
性能测试
最后,我们测试下性能,用Issue #195中用户提供的JSON数据(大约1.6M)来实测跑下。优化前的版本在我的Apple M2上跑一遍语法高亮需要30s,优化后跑了一遍我惊呆了,只要150ms,差不多200倍
的提升!
欢迎各位阅读!也欢迎大家来下载和体验 Reqable - 新一代API生产力工具,也欢迎与我一起交流更多Flutter的开发经验和心得!