前言 當(dāng)我研發(fā)Finda時,我非常希望它能夠做到快速,最好能在16毫秒內(nèi)相應(yīng)所有用戶輸入。 經(jīng)過認(rèn)真研究,我們驚訝地發(fā)現(xiàn)Finda是用Electron構(gòu)建的,該框架經(jīng)常被批評速度緩慢。 在本文中,我將重點(diǎn)說明如何在充分利用Electron易于打包、可以訪問復(fù)雜操作系統(tǒng)指定API、針對瀏覽器的視覺功能等優(yōu)點(diǎn)的同時,借助Rust來最大限度地減少不可預(yù)知的延遲現(xiàn)象和解決內(nèi)存使用過多問題。
關(guān)于設(shè)計(jì)的注意事項(xiàng) 在深入了解技術(shù)細(xì)節(jié)之前,我們首先要了解Finda自身的設(shè)計(jì)目標(biāo)。 Finda支持單一交互:用戶輸入內(nèi)容,它就能找到相應(yīng)的事物,包括瀏覽器標(biāo)簽、文字編輯器緩沖區(qū)、本地文件、瀏覽器歷史記錄、打開的窗口等。 演示視頻請參考:https://d189ym6tlc5mr2./video/2018_02_16_finda_demo.mp4。 我們最后的目標(biāo)是,想要讓Finda感覺不像是應(yīng)用程序,更像是Command-Tab(macOS默認(rèn)應(yīng)用程序切換工具),只作為操作系統(tǒng)的一部分,在需要時立即出現(xiàn),在完成相應(yīng)功能后就可以消失。 過程中無需菜單、窗口、按鈕或任何類型的本地用戶界面。針對于Finda的互動,我們只需要以下幾點(diǎn): 1、不管在哪個應(yīng)用程序的界面上,全局快捷方式都可以直接將Finda全屏顯示; 2、捕獲輸入的按鍵; 3、呈現(xiàn)搜索結(jié)果。 在不使用的情況下,F(xiàn)inda應(yīng)該隱藏在后臺。
不使用Electron的替代方案 鑒于上述要求,我重新考慮了我的選項(xiàng)。 Native OS X:我很早就想到了這一方案,其原因有兩個: 1、我想要將Finda移植到Windows和Linux上,因?yàn)閎eta測試者在問他們是否可以為他們現(xiàn)有平臺購買一個版本。 2、為了使用XCode進(jìn)行本地開發(fā),我必須升級macOS,這一升級過程幾乎肯定會在一定程度上破壞我電腦的環(huán)境。 Game-like:我之前曾經(jīng)基于此方案寫過一個像素著色器,經(jīng)過實(shí)際使用,游戲的速度非???,也許這一方案能夠有效。經(jīng)過研究,我決定嘗試使用ggez(https://github.com/ggez/ggez),這是一個基于SDL的Rust游戲庫,非常棒。 對于我這樣圖形方面的新手來說,我發(fā)現(xiàn)這個API是非常友好的。然而我很快就意識到,恐怕要制作一個完整的應(yīng)用程序,還是需要相當(dāng)多的基礎(chǔ)工作的。 例如,可以給定文本字符串、字體大小和字體。但是,當(dāng)用戶鍵入時,F(xiàn)inda將突出顯示匹配項(xiàng): https:///blog/building-a-fast-electron-app-with-rust/highlighting.mp4 這就意味著我需要處理多個字體和顏色,并跟蹤每個繪制的子字符串的邊界框,以設(shè)置好所有內(nèi)容。 除了渲染之外,我發(fā)現(xiàn)操作系統(tǒng)集成方面也存在著一些困難點(diǎn): 1、建立一個沒有標(biāo)題欄、最小化、最大化、關(guān)閉按鈕的無邊框窗口; 2、后臺運(yùn)行應(yīng)用程序,不在Dock中顯示; 3、通過Quartz Event Services(https://developer.apple.com/documentation/coregraphics/quartz_event_services?language=objc)獲得一個“全局熱鍵”。 關(guān)于第三個困難點(diǎn),在4小時之后,我設(shè)法獲得了關(guān)鍵代碼,但我發(fā)現(xiàn)我需要通過單獨(dú)的一組循環(huán)來查找活動鍵盤映射,于是就放棄了這一想法。 上述都不是真正的“游戲問題”,并且這看起來并不像切換到另一個框架,例如GLUT(OpenGL,https://www./resources/libraries/glut/)會比ggez(SDL)要好。 Electron:之前我已經(jīng)使用Electron構(gòu)建過應(yīng)用程序,而且我知道它會符合Finda的要求。瀏覽器最初是為了布局文本而設(shè)計(jì)的,Electron提供了廣泛的窗口選項(xiàng)(https://github.com/electron/electron/blob/master/docs/api/browser-window.md)和全局快捷方式的一行API(https://github.com/electron/electron/blob/master/docs/api/global-shortcut.md)。
結(jié)構(gòu) Electron用語用戶界面層,Rust作為二進(jìn)制執(zhí)行并處理所有其他內(nèi)容 當(dāng)Finda打開,并按下一個鍵時: 1) 瀏覽器調(diào)用一個文檔onKeyDown監(jiān)聽器,該監(jiān)聽器將JavaScript keydown事件翻譯為表示事件的普通JavaScript對象,就像是: 2) 這個JavaScript對象被傳遞給Rust(之后會傳遞更多),Rust返回另一個表示整個應(yīng)用程序狀態(tài)的普通JavaScript對象: { query: 'search terms', results: [{label: 'foo', icon: 'bar.png'}, ...], selected_idx: 2, show_overlay: false, ... } 3) 然后將這個JavaScript對象傳遞給React.js,它使用 在這個架構(gòu)中,有兩點(diǎn)需要注意: 首先,Electron沒有維護(hù)任何一種狀態(tài)。從它的角度來看,整個應(yīng)用程序都是最近事件的函數(shù)。這一點(diǎn)是可能的,因?yàn)镽ust始終維持Finda的內(nèi)部狀態(tài)。 其次,這些步驟發(fā)生在每個用戶交互(keyup和keydown)過程中。因此,為了滿足性能要求,所有三個步驟必須在16ms內(nèi)完成。 INTEROP 其中比較有趣的是第二個步驟,如果從JavaScript調(diào)用Rust,那會是什么樣子? 我們使用了Neon庫,與Rust共同構(gòu)建一個Node.js模塊。 從Electron角度來看,這就像調(diào)用任何其他類型的包裝一樣: Rust中這個函數(shù)有一些復(fù)雜,我們來具體分析一下: JavaScript有幾種語義不能完美映射到Rust的語言語義(例如,參數(shù)對象和動態(tài)變量)。 因此,Neon不會試圖將JS調(diào)用映射到Rust函數(shù)簽名,而是將函數(shù)傳遞給一個Call對象,從中可以提取細(xì)節(jié)。 由于我已經(jīng)編寫了這個函數(shù)的調(diào)用(JS)端,我知道第一個參數(shù)是這里唯一的參數(shù),它是一個JavaScript對象,并且始終有一個與字符串值關(guān)聯(lián)的名稱鍵。 然后,可以使用此event_type字符串將JavaScript對象的“翻譯”的其余部分引導(dǎo)至適當(dāng)?shù)腇inda :: Event枚舉變量: 這些分支還會調(diào)用finda :: step函數(shù),它將實(shí)際更新應(yīng)用程序狀態(tài)以響應(yīng)事件,例如:更改查詢并返回相關(guān)結(jié)果、打開選定結(jié)果、隱藏Finda等等。 (我會在以后的博客文章中詳細(xì)講解Rust,希望大家繼續(xù)關(guān)注我的博客,或者關(guān)注@lynaghk) 在應(yīng)用程序狀態(tài)更新之后,它需要返回到Electron端進(jìn)行渲染。這個過程看起來與其他方案都很相似,但實(shí)際是在另一個方向上,它是將Rust數(shù)據(jù)結(jié)構(gòu)翻譯成JavaScript數(shù)據(jù)結(jié)構(gòu): 在這里,我們首先創(chuàng)建JavaScript對象,該對象將返回到Electron并將一些鍵與某些基本類型相關(guān)聯(lián)。 返回結(jié)果(一個對象類型數(shù)組)需要更多的限制:數(shù)組大小需要事先聲明、Rust結(jié)構(gòu)必須明確列舉出來。但整體來說,還不算太糟糕: 最后,在該函數(shù)結(jié)束時返回JavaScript對象: Neon處理所有的細(xì)節(jié),并將其傳遞給JavaScript端的調(diào)用者。 性能驗(yàn)證 那么,在實(shí)踐中它們的性能表現(xiàn)得如何呢? 在Chrome DevTools的“性能”選項(xiàng)卡(內(nèi)置于Electron中)中,我們可以看到,這是一個單一keypress的典型曲線: 其中的每個步驟都被標(biāo)記:1)將按鍵轉(zhuǎn)換為事件,2)在Rust中處理事件,3)使用React渲染結(jié)果。 首選需要注意的是頂部的綠色條,這表明所有這些都在14毫秒之內(nèi)完成。 其次注意的是Rust的Interop,在其中高亮顯示的Native.step()調(diào)用僅在不到1毫秒之內(nèi)就進(jìn)行完成。 我嘗試在查詢中添加一個字母,那么這一特殊的keydown事件會導(dǎo)致在Finda中進(jìn)行如下步驟,而這些步驟都是在1毫秒內(nèi)完成的: 1、對所有我打開的窗口、Emacs緩沖區(qū)、瀏覽器約20000頁標(biāo)題及URL、~/work/、~/Downloads/和~/Dropbox/文件夾進(jìn)行正則表達(dá)式搜索。 2、根據(jù)質(zhì)量啟發(fā)式(匹配數(shù)量、是否出現(xiàn)在詞語邊界等)對所有這些結(jié)果進(jìn)行排序。 3、將前50個結(jié)果轉(zhuǎn)換為JavaScript并返回。 如果你不相信能有這么快的速度,可以自己下載并嘗試。針對不同的事件,其性能數(shù)據(jù)也有所不同,但這種追蹤是非常典型的:Rust需要幾毫秒來完成實(shí)際工作,大部分時間都是在進(jìn)行渲染,并且整個JavaScript執(zhí)行都會在16毫秒內(nèi)完成。 對性能的繼續(xù)研究 考慮到這些性能指標(biāo),我們可以通過刪除React(也可能是整個DOM)來縮短響應(yīng)時間,而不是使用 |
|