VS Code、ATOM這些開源文本編輯器的代碼實現中有哪些奇技淫巧?

這篇是我在知乎的回答,原文在這裏:justjavac: VS Code、ATOM這些開源文本編輯器的代碼實現中有哪些奇技淫巧?html

研究 V8 比較多,也關注了一下 vscode 和 atom 的性能,每次 vscode、atom 的 change log 我都會看一遍。印象最深的是 vscode 1.14 的一次更新日誌,doApplyEdits Lines inserted using splice · Issue #351 · Microsoft/monaco-editor不要在循環中使用 splicejava

下圖是我一年前跑的測試結果:Inserting an array within an arraynode

300+倍的差距。git

在以前 vscode 還有一次很大的性能提高,在版本 1.9 的時候,改進了語法高亮的算法。程序員

語法高亮的過程一般分爲 2 個階段(tokenization 和 render):先將源碼分割爲 token,而後使用不一樣的主題對分割後的 token 進行着色。github

tokenization 的過程是:從上到下逐行運行。tokenizer 在行的末尾存儲一些狀態,在 tokenize 下一行時會用到這些狀態。這樣,在用戶進行編輯時僅須要從新 tokenize 行的一小部分,而不須要掃描整個文件內容。算法

還有一種狀況是當前行的輸入會影響到後面(甚至是前面)的行,這時會用到結束狀態:sql

在 1.9 以前的版本,vscode 如何 tokenization 呢?數據庫

好比上面的代碼:數組

在 vscode 種這樣存儲:

tokens = [
    { startIndex:  0, type: 'keyword.js' },
    { startIndex:  8, type: '' },
    { startIndex:  9, type: 'identifier.js' },
    { startIndex: 11, type: 'delimiter.paren.js' },
    { startIndex: 12, type: 'delimiter.paren.js' },
    { startIndex: 13, type: '' },
    { startIndex: 14, type: 'delimiter.curly.js' },
]

{ startIndex: 0, type: 'keyword.js' } 表示從 0 開始的 token 是一個 keyword。

VSCode 團隊在博客種指出這在 Chrome 中佔據 648 個字節,所以存儲這樣的對象在內存方面的代價很是高(每一個對象實例必須保留指向其原型的空間,以及其屬性列表等)。爲 15 個字符存儲 648 字節是不可接受的。

因此,vscode 使用二進制來存儲token:

//     0        1               2                  3                      4
map = ['', 'keyword.js', 'identifier.js', 'delimiter.paren.js', 'delimiter.curly.js'];
tokens = [
    { startIndex:  0, type: 1 },
    { startIndex:  8, type: 0 },
    { startIndex:  9, type: 2 },
    { startIndex: 11, type: 3 },
    { startIndex: 12, type: 3 },
    { startIndex: 13, type: 0 },
    { startIndex: 14, type: 4 },
]

和上面的表示法相比,只是把 type 由字符串變成了數字,本質上並無節約太多的內存。可是彆着急,vscode 還有黑科技。

咱們都知道 JavaScript 使用 IEEE-754 標準存儲雙精度浮點數,尾數爲 53bit。可以在不丟失精度的狀況下處理的最大整數爲 2^53-1。所以 vscode 使用其中的 48big 進行編碼:使用 32bit 來存儲 startIndex,16bit 來存儲type。 因而上面的對象在 vscode 種被存儲爲:

tokens = [
                 //       type                 startIndex
     4294967296, // 0000000000000001 00000000000000000000000000000000
              8, // 0000000000000000 00000000000000000000000000001000
     8589934601, // 0000000000000010 00000000000000000000000000001001
    12884901899, // 0000000000000011 00000000000000000000000000001011
    12884901900, // 0000000000000011 00000000000000000000000000001100
             13, // 0000000000000000 00000000000000000000000000001101
    17179869198, // 0000000000000100 00000000000000000000000000001110
]

每一個數字是 64bit(8字節),一共是 7 個數字,存儲這些元素一共須要 7*8 = 56 字節,再加上數組的額外開銷共須要 104 個字節,只有以前的 648 字節的 1/6。

而主題的渲染則用到了 Trie 數據結構。

這個學過《數據結構》的都懂,算不上奇技淫巧,就不展開了。

這一切都是 2017 年 3 月發佈的 vscode 1.9。

而今年 3 月,vscode 又重寫了 Text Buffer。用戶使用編輯器,大部分時間就是寫新代碼,改舊代碼,說到底仍是對 text 進行編輯。

對於高性能的文本操做,vscode 最初嘗試使用 C++ 進行編寫,畢竟 C++ 的性能要比 JavaScript 高出很多,可是事實卻不夠理想,使用 C++ 確實節約了內存,可是在使用 C++ 模塊時,須要在 JavaScript 和 C++ 之間往返數次,這大大減慢了 vscode 的性能。

vscode 團隊從 Vyacheslav Egorov 的一篇文章 Maybe you don't need Rust and WASM to speed up your JS 收到了啓發,如何充分壓榨 V8 引擎的性能。mrale.ph 的博客我幾乎每篇都看,很是經典,也很是難懂 。

大多編輯器都是基於行的。程序員逐行編寫代碼,編譯器提供基於行的反饋信息,堆棧跟蹤包含行號,tokenization 引擎逐行運行…… 在 vscode 的早期版本中也是直接把每行代碼做爲字符串存儲在數組中。

可是這種方式存在一些問題:

  • 沒法打開大文件,由於把全部內容讀入數組中可能致使內存不足。
  • 即便文件不大,可是行數太多也沒法打開。例如,一個用戶沒法打開一個 35 MB 的文件。根本緣由是該文件的行數太多,1370 萬行。引擎將爲ModelLine每行和每一個對象使用大約 40-60 個字節,所以整個數組使用大約 600MB 內存來存儲文檔。也就是說打開這個 35M 的文件須要 600M 的內容,20 倍啊!!!
  • 另外一個問題就是速度。爲了構建這個數組,必須經過換行符分割內容,以便每行得到一個字符串對象。

因而 vscode 開始尋找新的數據結果,最終選擇了 Piece table。不知道爲何這麼晚才選擇 piece table,要知道在微軟的 office word 中早就已經使用了 piece table。我也是在一次 Java 讀取 word 的 jar 包源碼中第一次知道的 piece table 數據結構。

推薦幾篇延伸閱讀的文章:

目前主要的三種編輯方式有 gap buffer, rope, piece table。


最近用 Atom 少了。

上一次讓我興奮的地方是:The State of Atom's Performance。在2017年6月 Atom 使用了 piece table 數據結構,使用 C++ 從新實現了 text buffer:Atom's new concurrency-friendly buffer implementation。比 vscode 還要早半年,可是爲何仍是這麼慢呢???

Atom 使用 V8 的自定義快照(snapshot)提高啓動性能,最終刪除了影響性能的 jQuery 和自定義 element。就連 V8 的

Atom 還更新了 DOM 渲染的方式:A new approach to text rendering,而這個新算法包括一個相似 React 的 vdom,從 issue 來看這是一個大工程啊,包含了近 100 個 task

圖片描述

通過一系列優化,官方說道:

we made loading Atom almost 50% faster and snapshots were a crucial tool that enabled some otherwise impossible optimizations.

咱們使 Atom 快了 50%,snapshot 功不可沒。(PS:我必定是使用了假的 Atom)

不過 snapshot 確實是 V8 的神器,Nodejs 也看到了 Atom 的成果,於 2017-11-16 開了 issue :speeding up Node.js startup using V8 snapshot · Issue #17058 · nodejs/node。這在我以前的專欄裏面有介紹:Node.js 新計劃:使用 V8 snapshot 將啓動速度提高 8 倍


最近一次關注 Atom 是 atom/xray。知乎上也有相關的討論,atom 開發的下一代編輯器(莫非已經定義 atom 爲上一代編輯器了嗎)。大概就是一種「大號廢了,開小號重練」的感受。

值得學習的地方是 text 處理使用 copy-on-write CRDT:

若是一直關注 Atom,對於 CRDT 應該不會陌生。Atom 的多人實時共同編輯插件 https://teletype.atom.io/ 就是使用的 CRDT。

CRDT 全稱:Conflict-Free Replicated Data Types,強行翻譯過來就是「無衝突可複製數據類型」。

CAP定理:在分佈式系統中,最多隻能同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance)這三項中的兩項。

不少分佈式系統都捨棄了C(一致性):容許能夠在某些時刻不一致,轉而求其次要求系統知足最終一致性。這也是目前不少 nosql 數據庫追求的方式(另外一種是傳統的符合 ACID 特性的數據庫系統,放棄了A(可用性),這種系統稱爲強一致性)。

而在最終一致性分佈式系統中,一個最基本的問題就是,應該採用什麼樣的數據結構來保證最終一致性? 答案就是 CRDT。


這篇文章只是一個提綱,裏面的每一個知識點均可以展開了講上三天三夜。

相關文章
相關標籤/搜索