开始用beagrep查代码定义

之前我一直是在用GNU Global + Ctags-exuberant来查代码的定义,后来发现Global有两个缺点让我不能忍,所以最近一咬牙切换到了用我最喜欢的beagrep来查代码定义,居然也挺爽的。

Global哪两个地方让我不爽呢?

  1. 基本不能正则表达式查询定义。

    比如在读Kernel代码的时候,你想查所有slub_.*的定义,会变得非常慢,因为Global的实现原理是这样的,一旦发现你想用正则表达式查询,就会把每个tag都用这个正则表达式匹配一遍,其算法从LogN变成了线性的N,一旦代码库变得像Android这么庞大的时候,查找就会变得相当慢了。

    这里还包括基本不能用不区分大小写的查询模式,因为这个也是用线性的算法实现的。

  2. 代码发生较大改动后重建索引会变得相当慢,甚至可能segfault。不知道最新的版本里修正了这个bug了没有,我用的好像是5.9.3,最新的已经6.4了。

    这个问题在我的Android版本发生升级、切换的时候尤为严重,比如从4.4升到5.0,或者从5.0又切换回到4.4,然后重新一建索引,基本上就进入死循环➡崩溃的节奏了,如果Global不崩溃,那就我崩溃😅。

所以我现在换到了用beagrep查定义。以前我是用beagrep查全文搜索,这个需要建一个全文的索引;现在只想查定义,所以拿到一个源代码文件,我需要把里面所有的定义抽出来——这个用ctags-exuberant就可以实现了——然后把这些定义当成此源代码文件的文本,对它建立索引,以后我想查readlink的定义,我就先用beagrep查询一下“哪些源代码文件包含readlink的定义?”,然后在结果文件里挨个地再用ctags-exuberant精确地查出readlink定义的行号。

一开始是想只建一个索引数据库,然后同时处理全文搜索和定义搜索的两个需求,后来发现这样做太复杂了,有点得不偿失。所以最后采取的方案很简单粗暴,建两个索引数据库!第一个就是老的beagrep数据库,第二个则是新的叫beatags的数据库。

由此带来的一些好处包括上面说的那些Global的缺点,现在都可以解决了。比如正则表达式查询,因为Tokenizer的关系, slub_.* 这样的查询,对beagrep来说都不是事儿了。另外即使不用Tokenizer的帮助,beagrep本身其实是支持“不完整单词”(partial words)查询的,但现在还没有用上。至于大小写不区分的支持,beagrep本来就没有大小写的概念(作为一个搜索引擎,本来就要把大小写的差异给抹除的)。

当然也有一些缺点,就是在查一些常用单词,比如File、String的定义的时候,速度会比Global慢很多,这是因为当前的分词实现里,read_a_file,this_is_a_string类似这样的定义,都会在查File或String的时候作为潜在的匹配文件出现,然后需要用ctags+grep精确查行号的时候把它们过滤掉。但这个可以用很多优化的方案去解决。其一是大规模地用cache,比如现在我即使是在用beagrep查全文搜索readlink的时候,会把所有含readlink的文件给cache住,下回再查的时候就直接cat这个cache。另外搜索readlink定义的时候,我会直接把所有readlink的tag也给cache住,下回再查的时候直接cat它。

接下来还要再做一个优化,就是修改beatags的分词规则,slub_cache、read_a_file,this_is_a_string这样的定义,应该被认为是一个Token,而不是依下划线把它们分开。这样就不会影响查File、String等定义时的性能了。

最后带来一个影响,就是我的Ajoke项目,它是重度依赖于之前的Global的。经过我的努力,现在Ajoke还可以用,但是以前.jar文件也可以用gtags来建索引,从而为Ajoke使用,现在就不行了😁。上面说的许多cache的优化,就是为了让Ajoke能赶上之前的“慢”的程度而做的,结果加上cache之后,发现一不小心Ajoke的性能变得好多了🈶🈚🈶!

整个代码也清晰了很多,值得我喝一杯🍹。