Code Review效率低?来试试智能语法服务
一 前言
代碼文本不是簡單的二維平面結(jié)構(gòu),看懂一段代碼需要反復(fù)地通過定義與引用的跳轉(zhuǎn),才能將代碼深層次的邏輯和片段影響范圍理解透徹。純文本形式的代碼瀏覽是網(wǎng)頁端代碼評審的最大痛點(diǎn)之一,朱熹老先生常說“心不在此,則眼不看仔細(xì),心眼既不專一,卻只漫浪誦讀,決不能記,記亦不能久也。”代碼文本扁平式地漫浪誦讀只能達(dá)到眼到、口到的境界,如果你是一個認(rèn)真負(fù)責(zé)的代碼評審者,阿里云云效代碼智能語法服務(wù)一定是幫助你充分理解代碼變更,超越眼口,到達(dá)“心到”境界的功能。心既到矣,眼口豈不到乎?
那么什么是代碼智能語法服務(wù)呢?語法服務(wù)提供了基于云端備份的快速代碼導(dǎo)航服務(wù),無須本地克隆即可在頁面體驗(yàn)熟悉的定義引用快速查看跳轉(zhuǎn)功能,大幅提升代碼評審的效率和質(zhì)量。
二 技術(shù)基礎(chǔ)
阿里云云效代碼智能語法服務(wù)的底層技術(shù)是LSIF(Language Server Index Format),它是一種持久化語言的索引的圖存儲格式,通過圖的格式,表示了“代碼文檔”-> “語法智能結(jié)果”之間的事件關(guān)系。
在LSIF之前,LSP(Language Server Protocol)定義了編碼語言與各類終端代碼編輯器之前的交互協(xié)議。原先開發(fā)者需要為每一款編輯器都定義適配一種語法分析服務(wù)應(yīng)用,那么M個語言要在M個代碼編輯器中使用的話需要MxN個應(yīng)用。而有了LSP的出現(xiàn),開發(fā)者在解析代碼語法時只需要遵循LSP協(xié)議格式,實(shí)現(xiàn)代碼補(bǔ)全、定義展示、代碼診斷等接口,就只需要開發(fā)M+N個應(yīng)用。
然而代碼分析往往需要耗費(fèi)大量的時間和資源,當(dāng)用戶請求某個語法服務(wù)(如查看定義),后端需要克隆代碼,下載依賴包,解析語法,構(gòu)建索引(類比一下IntelliJ Idea初始化工程的場景),編輯器場景用戶已經(jīng)習(xí)慣于這樣的方式,等待幾分鐘或許問題不大。但CR場景或者輕量級的代碼瀏覽場景,這種方式就顯得時效性比較低了,幾分鐘后或許用戶已經(jīng)完成了代碼瀏覽,而且缺少持久化的存儲會導(dǎo)致資源過度消耗。于是,LSIF就在這樣的背景下應(yīng)運(yùn)而生,秉承用空間換時間的思想,提前計(jì)算好語法分析結(jié)果以特定的索引格式存儲在云上,從而快速響應(yīng)不同用戶的多次請求。
援引官方示例來簡單介紹下LSIF,如下方代碼:
// this is a sample class public class Sample { }假定只有一種交互,當(dāng)鼠標(biāo)移動到Sample的類名上,就會出現(xiàn)“this is a sample class”的注釋信息。用LSIF的圖就可以如下描述。
一個sample文件,包含了一個range信息,這個range關(guān)聯(lián)了一個hoverResult。含義是該文件的某個位置范圍內(nèi),觸發(fā)hover事件的話,就給出hoverResult存儲的結(jié)果。
如果用Json文件描述這張圖的存儲,就可以得到如下結(jié)果:
{ id: 1, type: "vertex", label: "document", uri: "file:///abc/sample.java", languageId: "java" } { id: 2, type: "vertex", label: "range", start: { line: 0, character: 13}, end: { line: 0, character: 18 } } { id: 3, type: "edge", label: "contains", outV: 1, inVs: [2] } { id: 4, type: "vertex", label: "hoverResult", result: {["this is a sample class"]} } { id: 5, type: "edge", label: "textDocument/hover", outV: 2, inV: 4 }實(shí)際一個工程的LSIF圖會非常復(fù)雜,經(jīng)常會包含幾十萬個節(jié)點(diǎn)。感興趣的同學(xué)可以參考LSIF具體描述[1]。
三 實(shí)現(xiàn)方式
阿里云云效的語法服務(wù)架構(gòu)圖主要分為兩部分:
- 基于事件觸發(fā)的索引構(gòu)建過程
- 基于用戶請求的語法服務(wù)響應(yīng)
1 索引構(gòu)建
用戶對代碼的瀏覽場景主要集中在代碼評審和主干分支的代碼瀏覽,所以我們目前主要支持兩種場景的語法服務(wù)。語法服務(wù)接收來自代碼平臺的事件消息,如代碼推送事件,評審的創(chuàng)建、更新、合并、關(guān)閉、重新開啟事件,來觸發(fā)語法服務(wù)構(gòu)建。
我們的構(gòu)建工作流調(diào)度主要基于阿里巴巴開源的分布式調(diào)度框架tbschedule,該系統(tǒng)會通過zookeeper維護(hù)一個任務(wù)集群,通過zookeeper做節(jié)點(diǎn)管理和任務(wù)分發(fā),不重復(fù)不遺漏地快速處理調(diào)度任務(wù)。
針對不同語言,我們只需要實(shí)現(xiàn)一次從源代碼到LSIF格式的轉(zhuǎn)換,就能將其應(yīng)用在多種場景。多種代碼語言代碼語言都會被解析成統(tǒng)一的LSIF格式文件。
針對阿里巴巴內(nèi)部主要的Java語言,我們利用開源Java代碼解析工具Spoon[2]將Java源代碼分析為AST(抽象語法樹),然后捕捉定義和引用、定義與注釋之間的關(guān)聯(lián),將坐標(biāo)信息、注釋內(nèi)容,文本類型,所屬文件等信息聚合,輸出為統(tǒng)一的LSIF的Json格式。
開發(fā)期間修復(fù)并適配了一些lsif-java的問題,如位置范圍信息錯亂,召回多種遺漏的高亮詞類型,適配非Maven倉庫的索引構(gòu)建。同時還修復(fù)了Spoon關(guān)于無法正確解析注釋中的部分注解的問題,PR已被Spoon社區(qū)接受合并[3]。
生成lsif.json文件后,由于這個Json文件較大,直接由前端加載并響應(yīng)請求不太合理,后期增量生成與維護(hù)難度也很大,所以我們還需要一步:將lsif.json轉(zhuǎn)化為結(jié)構(gòu)化數(shù)據(jù),從而按需響應(yīng)用戶查詢請求。lsif的圖存儲格式讓人自然地聯(lián)想到用圖數(shù)據(jù)結(jié)構(gòu)存儲,圖查詢的速度也比較快,然而由于索引變化迭代較快,頻繁更換的ID導(dǎo)致圖存儲難以適配增量方案,不同代碼庫不同語言的索引數(shù)據(jù)很難在一張圖中結(jié)構(gòu)化,參考了社區(qū)的相關(guān)實(shí)踐,考慮到成本和性能,因?yàn)镋S天然地適合大規(guī)模的數(shù)據(jù)存儲和索引,我們最后選擇了用ES(Elasticsearch)做結(jié)構(gòu)化數(shù)據(jù)存儲。
我們將這種結(jié)構(gòu)化的數(shù)據(jù)上傳到ES,然后語法服務(wù)后端服務(wù)器會基于用戶的語法請求,構(gòu)造ES請求Query,查詢定義、引用或注釋信息,將其拼裝返回。
對于分支,我們會持續(xù)更新和保留最新版本的索引數(shù)據(jù);對于代碼評審,我們會構(gòu)建源分支的每次Push版本和源目標(biāo)分支的merge-base版本的索引。
索引構(gòu)建的另外一個難點(diǎn)是增量計(jì)算。如上文所述,語法服務(wù)索引構(gòu)建對資源的要求非常高,而現(xiàn)實(shí)中代碼庫不可避免地會存在頻繁提交的現(xiàn)象。如此引申出了兩個優(yōu)化點(diǎn):
增量方案
每次分支索引構(gòu)建成功,我們都會在數(shù)據(jù)庫中記錄分支對應(yīng)的版本號,當(dāng)該分支有了一次新的提交后,在生成lsif.json后,系統(tǒng)會比較兩個分支的Diff,獲取到變更文件和變更類型,通過變更文件來進(jìn)一步提取索引受到影響的文件(引用或定義的坐標(biāo)信息變更),分析出所有受影響的文件和對應(yīng)的ES增刪操作后,完成增量索引上傳。這個增量的過程平均能減少45%的分支構(gòu)建時間。
時序鎖管理
根據(jù)庫大小的區(qū)別,LSIF的索引構(gòu)建時間為10秒至數(shù)分鐘不等,而用戶對同一個代碼倉庫的提交操作峰值可能會達(dá)到每分鐘近百次,即使我們采用了增量技術(shù)也很難滿足高頻的構(gòu)建請求,并且提交事件觸達(dá)和調(diào)度任務(wù)執(zhí)行無法保證精準(zhǔn)的時序性。綜上所述,我們需要一個分布式時序鎖來保證任務(wù)調(diào)度的順序和盡量減少重復(fù)調(diào)度。
當(dāng)同一代碼庫的不同推動消息紛涌而至,Redis維護(hù)的分布式鎖會做如下判斷:若該庫當(dāng)前沒有正在運(yùn)行的任務(wù),將任務(wù)置于隊(duì)首,立即運(yùn)行;若已有一個正在執(zhí)行的任務(wù),比較新來的Push消息是否是最新的,若是,則加入隊(duì)尾;當(dāng)隊(duì)伍已有兩個成員時,則將任務(wù)丟棄,因?yàn)槊看螆?zhí)行任務(wù)時,系統(tǒng)都會克隆分支代碼,基于最新的版本構(gòu)建索引,如此就避免了多少次Push就需要執(zhí)行多少次索引構(gòu)建的可能性。考慮到線程意外退出的情況,隊(duì)首會每隔5秒鐘全局發(fā)送心跳,當(dāng)隊(duì)尾或新來的任務(wù)監(jiān)聽到心跳超時,則會將隊(duì)首的任務(wù)放棄并執(zhí)行新的任務(wù)。
2 語法服務(wù)響應(yīng)
如前言的示例,用戶在使用語法服務(wù)時,主要有以下三個請求:
針對第一個請求,系統(tǒng)會構(gòu)造基于文件路徑的過濾條件構(gòu)造ES的Query請求,將當(dāng)前文件的所有高亮詞坐標(biāo)信息發(fā)送給前端,避免了前端做語法分詞,沒有構(gòu)建好的文件自然也不會在頁面上被高亮出來。另外,為了避免超大文件對ES的壓力,前端會做分批動態(tài)加載。
針對第二個請求,我們在獲取定義與引用列表的過程中,不光要得到文件名和位置信息,還需要將對應(yīng)的代碼內(nèi)容展示出來,方便用戶理解。為了實(shí)現(xiàn)這個效果,我們新增了批量獲取文件片段的接口。
對于第三個請求,同一個文件內(nèi)的跳轉(zhuǎn)會自動高亮到對應(yīng)的代碼行,不同文件間的跳轉(zhuǎn)會新開頁面并跳轉(zhuǎn)。
語法服務(wù)響應(yīng)和語法索引構(gòu)建是完全異步的,互不影響,支持獨(dú)立的資源擴(kuò)縮。
3 索引清理
語法服務(wù)的索引大小約是代碼文件內(nèi)容的數(shù)倍,比較消耗存儲資源。所以針對用戶通常的使用習(xí)慣和場景,制定了一系列索引清理任務(wù)來避免資源過度的損耗。
當(dāng)代碼評審合并或刪除時,當(dāng)分支刪除時,系統(tǒng)會開始執(zhí)行索引清理任務(wù),釋放索引資源。
四 語法服務(wù)展望
缺少符號跳轉(zhuǎn)長久以來一直是頁面上代碼閱讀的痛點(diǎn)之一,各種語法協(xié)議和技術(shù)層出不窮,如LSIF、Kythe、SARIF、UAST、Tree-sitter,ctags,全球技術(shù)人都在為更智能的代碼分析,更好的代碼體驗(yàn),更高的代碼質(zhì)量做努力。云效語法服務(wù)后續(xù)也會逐漸加快語法構(gòu)建速度,支持更多的代碼語言,滿足更多的語法場景,提升用戶的代碼瀏覽體驗(yàn)。
相關(guān)鏈接
[1]https://microsoft.github.io/language-server-protocol/specifications/lsif/0.4.0/specification/
[2]http://spoon.gforge.inria.fr/
[3]https://github.com/INRIA/spoon/pull/3513
原文鏈接:https://developer.aliyun.com/article/779951?
版權(quán)聲明:本文內(nèi)容由阿里云實(shí)名注冊用戶自發(fā)貢獻(xiàn),版權(quán)歸原作者所有,阿里云開發(fā)者社區(qū)不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。具體規(guī)則請查看《阿里云開發(fā)者社區(qū)用戶服務(wù)協(xié)議》和《阿里云開發(fā)者社區(qū)知識產(chǎn)權(quán)保護(hù)指引》。如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,填寫侵權(quán)投訴表單進(jìn)行舉報,一經(jīng)查實(shí),本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。總結(jié)
以上是生活随笔為你收集整理的Code Review效率低?来试试智能语法服务的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 双12来了!揭秘秒杀剁手背后的云数据库P
- 下一篇: AliExpress智能营销引擎大揭秘-