Android系統(tǒng)優(yōu)化Ninja加快編譯
背景
Android系統(tǒng)模塊代碼的編譯實在是太耗時了,即使寥寥幾行代碼的修改,也能讓一臺具有足夠性能的編譯服務(wù)器工作十幾分鐘以上(模塊單編),只為編出一些幾兆大小的jar和dex。
這里探究的是系統(tǒng)完成過一次整編后進行的模塊單編,即m、mm、mmm等命令。
除此之外,一些不會更新源碼、編譯配置等文件的內(nèi)容的操作,如touch、git操作等,會被Android系統(tǒng)編譯工具識別為有差異,從而在編譯時重新生成編譯配置,重新編譯并沒有更新的源碼、重新生成沒有差異的中間文件等一系列嚴(yán)重耗時操作。
本文介紹關(guān)于編譯過程中的幾個階段,以及這些階段的耗時點/耗時原因,并最后給出一個覆蓋一定應(yīng)用場景的基于ninja的加快編譯的方法(實際上是裁剪掉冗余的編譯工作)。
環(huán)境
編譯服務(wù)器硬件及Android信息:
- Ubuntu 18.04.4 LTS
- Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz (28核56超線程)
- MemTotal: 65856428 kB (62.8GiB)
- AOSP Android 10.0
- 僅修改某個Java文件內(nèi)部的boolean初始化值(true改false)
- 不修改其他任何內(nèi)容,包括源碼、mk、bp的情況下,使用m單編模塊(在清理后,使用對比的ninja進行單編)
- 使用time計時
- 此前整個系統(tǒng)已經(jīng)整編過一次
- 編譯時不修改任何編譯配置文件如Android.mk
之所以做一個代碼修改量微乎其微的case,是因為要分析編譯性能瓶頸,代碼變更量越小的情況下,瓶頸就越明顯,越有利于分析。
關(guān)鍵編譯階段和耗時分析
由于Makefile
結(jié)構(gòu)復(fù)雜、不易調(diào)試、難以擴展,因此Android決定將它替換掉。Android在7.0時引入了Soong
,它將Android從Makefile
的編譯架構(gòu)帶入到了ninja
的時代。
Soong
包含兩大模塊,其中Kati
負責(zé)解析Makefile
并轉(zhuǎn)換為.ninja
,第二個模塊Ninja
則基于生成的.ninja
完成編譯。
Kati
是對GNU Make
的clone,并將編譯后端實現(xiàn)切換到ninja。Kati
本身不進行編譯,僅生成.ninja
文件提供給Ninja
進行編譯。
Makefile/Android.mk -> Kati -> Ninja Android.bp -> Blueprint -> Soong -> Ninja
因此在執(zhí)行編譯之前(即Ninja
真正開動時),還有一些生成.ninja
的步驟。關(guān)鍵編譯階段如下:
Soong的自舉(Bootstrap),將Soong本身編譯出來
系統(tǒng)代碼首次編譯會比較耗時,其中一個原因是Soong要全新編譯它自己
遍歷源碼樹,收集所有編譯配置文件(Makefile/Android.mk/Android.bp)
- 遍歷、驗證非常耗時,多么強勁配置的機器都將受限于單線程效率和磁盤IO效率
- 由于Android系統(tǒng)各模塊之間的依賴、引入,因此即使是單編模塊,Soong(Kati)也不得不確認目標(biāo)模塊以外的路徑是否需要重新跟隨編譯。
驗證編譯配置文件的合法性、有效性、時效性、是否應(yīng)該加入編譯,生成.ninja
- 如果沒有任何更改,.ninja不需要重新生成
- 最終生成的.ninja文件很大(In my case,1GB以上),有很明顯的IO性能效率問題,顯然在查詢效率方面也很低下
最后一步,真正執(zhí)行編譯,調(diào)用ninja進入多線程編譯
- 由于Android加入了大量的代碼編譯期工作,如API權(quán)限控制檢查、API列表生成等工作(比如,生成系統(tǒng)API保護名單、插樁工作等等),因此編譯過程實際上不是完全投入到編譯中
- 編譯過程穿插“泛打包工作”,如生成odex、art、res資源打包。雖然不同的“泛打包”可以多線程并行進行,但是每個打包本身只能單線程進行
下面將基于模塊單編(因開發(fā)環(huán)境系統(tǒng)全新編譯場景頻率較低,不予考慮),對這四個關(guān)鍵階段進行性能分析。
階段一:Soong bootstrap
在系統(tǒng)已經(jīng)整編過一次的情況下,Soong已經(jīng)完成了編譯,因此其預(yù)熱過程占整個編譯時間的比例會比較小。
在“環(huán)境”下,修改一行Framework代碼觸發(fā)差異進行編譯。并且使用下面的命令進行編譯。
time m services framework -j57
編譯實際耗時22m37s:
build completed successfully (22:37 (mm:ss)) ####
real 22m37.504s
user 110m25.656s
sys 12m28.056s
對應(yīng)的分階段耗時如下圖。
- 可以看到,包括Soong bootstrap流程在內(nèi)的預(yù)熱耗時占比非常低,耗時約為11.6s,總耗時約為1357s,預(yù)熱耗時占比為
0.8%
。
- Kati和ninja,也就是上述編譯關(guān)鍵流程的第2步和第3步,分別占了接近60%(820秒,13分鐘半)和約35%(521秒,8分鐘半)的耗時,合計占比接近95%的耗時。
注:這個耗時是僅小幅度修改Java代碼后測試的耗時。如果修改編譯配置文件如Android.mk,會有更大的耗時。
小結(jié):看來在完成一次整編后的模塊單編,包括Soong bootstrap、執(zhí)行編譯準(zhǔn)備腳本、vendorsetup腳本的耗時占比很低,可以完全排除存在性能瓶頸的可能。
階段二:Kati遍歷、mk搜集與ninja生成
從上圖可以看到,Kati
耗時占比很大,它的任務(wù)是遍歷源碼樹,收集所有的編譯配置文件,經(jīng)過驗證和篩選后,將它們解析并轉(zhuǎn)化為.ninja
。
從性能角度來看,它的主要特點如下:
- 它要遍歷源碼樹,收集所有mk文件(In my case,有983個mk文件)
- 解析mk文件(In my case,framework/base/Android.mk耗費了~6800ms)
- 生成并寫入對應(yīng)的.ninja
- 單線程
直觀展示如下,它是一個單線程的、IO速度敏感、CPU不敏感的過程:
Kati串行地處理文件,此時對CPU利用率很低,對IO的壓力也不高。
小結(jié):可以確定它的性能瓶頸來源于IO速度,單純?yōu)榫幾g實例分配更多的CPU資源也無益于提升Kati的速度。
階段三:Ninja編譯
Soong
Clone了一份GNU Make
,并將其改造為Kati
。即使我們沒有修改任何mk文件,前面Kati
仍然會花費數(shù)分鐘到數(shù)十分鐘的工作耗時,只為了生成一份能夠被Ninja
或.ninja
的生成工具能夠識別的文件。接下來是調(diào)用Ninja
真正開始編譯工作。
從性能角度來看,它的主要特點如下:
- 根據(jù)目標(biāo)target及依賴,讀取前面生成的.ninja配置,進行編譯
- 比較獨立,不與前面的組件,如blueprint、kati等耦合,只要
.ninja
文件中能找到target和build rule就能完成編譯 - 多線程
直觀展示如下,Ninja
將會根據(jù)傳入的并行任務(wù)數(shù)參數(shù)啟動對應(yīng)數(shù)量的線程進行編譯。Ninja
編譯階段會真正的啟動多線程。但做不到一直多線程編譯,因為部分階段如部分編譯目標(biāo)(比如生成一個API文檔)、泛打包階段等本身無法多線程并行執(zhí)行。
可以看到此時CPU利用率應(yīng)該是可以明顯上升的。但是耗時較大的階段僅啟用了幾個線程,后面的階段和最后的圖形很細(時間占比很?。┑碾A段才用起來更多的線程。
其中,一些階段(圖中時間占比較長的幾條記錄)沒能跑滿資源的原因是這些編譯目標(biāo)本身不支持并行,且本次編譯命令指定的目標(biāo)已經(jīng)全部“安排”了,不需要調(diào)動更多資源啟動其他編譯目標(biāo)的工作。當(dāng)編譯整個系統(tǒng)時就能夠跑滿了。
最后一個階段(圖中最后的幾列很細的記錄)雖然跑滿了所有線程資源,但是運行時間很短。這是因為本case進行編譯分析的過程中,僅修改了一行代碼來觸發(fā)編譯。因編譯工作量很小,所以這幾列很細。
小結(jié):我們看到,Ninja
編譯啟動比較快,這表明Ninja
對.ninja
文件的讀取解析并不敏感。整個過程也沒有看到顯著的耗時點。且最后面編譯量很小,表明Ninja
能夠確保增量編譯、未更新不編譯。
編譯優(yōu)化
本節(jié)完成點題——Android系統(tǒng)編譯優(yōu)化:使用Ninja加快編譯。
根據(jù)前面分析的小結(jié),可以總結(jié)性能瓶頸:
- Kati遍歷、生成太慢,受限于IO速率
- Kati吞吐量太低,單線程
- 不論有無更新均重新解析Makefile
利用Ninja
進行編譯優(yōu)化的思路是,大多數(shù)場景,可以舍棄Kati的工作,僅執(zhí)行Ninja的工作,以節(jié)省掉60%以上的時間。其核心思路,也是制約條件,即在不影響編譯正確性的前提下,舍棄不必要的Kati編譯工作。
- 使用
Ninja
直接基于.ninja
文件進行編譯來改善耗時:
結(jié)合前面的分析,容易想到,如果目標(biāo)被構(gòu)建前,能夠確保mk文件沒有更新也不需要重新生成一長串的最終編譯目標(biāo)(即.ninja),那么make
命令帶來的Soong bootstrap、Kati等工作完全是重復(fù)的冗余的——這個性質(zhì)Soong和Kati自己識別不出來,它們會重復(fù)工作一次。
既重新生成.ninja是冗余的,那么直接命令編譯系統(tǒng)根據(jù)指定的.ninja進行編譯顯然會節(jié)省大量的工作耗時。ninja命令is the key:
使用源碼中自帶的ninja:
./prebuilts/build-tools/linux-x86/bin/ninja --version 1.8.2.git
對比最上面列出的make
命令的編譯,這里用ninja編譯同樣的目標(biāo):
time ./prebuilts/build-tools/linux-x86/bin/ninja -j 57 -v -f out/combined-full_xxxxxx.ninja services framework
ninja自己識別出來CPU平臺后,默認使用-j58。這里為了對比上面的m命令,使用-j57編譯
-f參數(shù)指定.ninja文件。它是編譯配置文件,在Android中由Kati生成。這里文件名用'x'替換修改
編譯結(jié)果,對比上面的m
,有三倍的提升:
real 7m57.835s
user 97m12.564s
sys 8m31.756s
編譯耗時為8分半,僅make的三分之一。As we can see,當(dāng)能夠確保編譯配置沒有更新,變更僅存在于源碼范圍時,使用Ninja直接編譯,跳過Kati可以取得很顯著的提升。
直接使用ninja:
./prebuilts/build-tools/linux-x86/bin/ninja -j $MAKE_JOBS -v -f out/combined-*.ninja <targets...>
對比匯總
這里找了一個其他項目的編譯Demo,該Demo的特點是本身代碼較簡單,編譯配置也較簡單,整體編譯工作較少,通過make編譯的大部分耗時來自soong、make等工具自身的消耗,而真正執(zhí)行編譯的ninja耗時占比極其低。由于ninja本身跳過了soong,因此可以跳過這一無用的繁瑣的耗時。可以看到下面,ninja編譯iperf僅花費10秒。這個時間如果給soong來編譯,預(yù)熱都不夠。
$ -> f_ninja_msf iperf Run ninja with out/combined-full_xxxxxx.ninja to build iperf. ====== ====== ====== Ninja: ./prebuilts/build-tools/linux-x86/bin/ninja@1.8.2.git Ninja: build with out/combined-full_xxxxxx.ninja Ninja: build targets iperf Ninja: j72 ====== ====== ====== time /usr/bin/time ./prebuilts/build-tools/linux-x86/bin/ninja -j 72 -f out/combined-full_xxxxxx.ninja iperf [24/24] Install: out/target/product/xxxxxx/system/bin/iperf 53.62user 11.09system 0:10.17elapsed 636%CPU (0avgtext+0avgdata 5696772maxresident) 4793472inputs+5992outputs (4713major+897026minor)pagefaults 0swaps real 0m10.174s user 0m53.624s sys 0m11.096s
下面給出soong編譯的恐怖耗時:
$ -> rm out/target/product/xxxxxx/system/bin/iperf $ -> time m iperf -j72 ... [100% 993/993] Install: out/target/product/xxxxxx/system/bin/iperf #### build completed successfully (14:45 (mm:ss)) #### real 14m45.164s user 23m40.616s sys 11m46.248s
As we can see,m和ninja一個是10+ minutes,一個是10+ seconds,比例是88.5倍。
以上就是Android系統(tǒng)優(yōu)化Ninja加快編譯的詳細內(nèi)容,更多關(guān)于Android Ninja加快編譯的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android使用Kotlin和RxJava 2.×實現(xiàn)短信驗證碼倒計時效果
本篇文章主要介紹了Android使用Kotlin和RxJava 2.×實現(xiàn)短信驗證碼倒計時效果,非常具有實用價值,需要的朋友可以參考下2017-12-12android開發(fā)教程之實現(xiàn)toast工具類
這篇文章主要介紹了android開發(fā)中需要的toast工具類,需要的朋友可以參考下2014-05-05Android使用Activity實現(xiàn)簡單的可輸入對話框
大家在做彈出對話框效果的時候最容易想到的是用Dialog顯示,但其實彈出對話框的實現(xiàn)效果有兩種:Dialog和Activity,那么下面這篇文章就來給大家介紹了關(guān)于Android使用Activity如何實現(xiàn)一個簡單的可輸入對話框的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-10-10Android初學(xué)者必須知道的10個技術(shù)
本篇內(nèi)容給大家整理10個作為Android初學(xué)者必須要了解和會用的技術(shù)以及詳細代碼分析,需要的朋友收藏下慢慢學(xué)習(xí)吧。2017-12-12Android動態(tài)更換應(yīng)用圖標(biāo)詳情
這篇文章主要介紹了Android動態(tài)更換應(yīng)用圖標(biāo)詳情,文章圍繞主題展開詳細的介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07Android Studio利用AChartEngine制作餅圖的方法
閑來無事,發(fā)現(xiàn)市面上好多app都有餅圖統(tǒng)計的功能,得空自己實現(xiàn)一下,下面這篇文章主要給大家介紹了關(guān)于Android Studio利用AChartEngine制作餅圖的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧2018-10-10