調試性能的Adobe AIR應用程序
性能相關問題往往表明作者無法理解問題鏈中最薄弱的環節。以下是我最喜歡提到的一些有關性能和 AIR 應用程序的糟糕提問:
- 我的 AIR 應用程序會運行如飛嗎?
- AIR 是否可以達到執行 X 的速度?
- AIR 執行 Y 是否太慢?
(以下內容還證明了一點,無論幼稚園老師教了您什么,總會出現糟糕提問這類事物。)
AIR 幾乎總可以通過應用程序實現良好性能。而另一方面,AIR 無法為您這樣做。正如我所說的,這是問題的本性。
幸運的是,標準調試技術像適用于編寫桌面軟件一樣適用于 AIR。
合適的提問
實現良好性能的***步,就如同大多數設計問題,在于理解您嘗試解決的問題。以下是一些針對您的應用程序的合適的提問:
- 我的應用程序中哪些操作對于性能比較敏感?
- 我可以使用什么衡量標準來測量這一敏感度?
- 如何優化應用程序以達到這一衡量標準?
大多數應用程序包含大量可以穩定運行的代碼。不要在這方面浪費時間,尤其是當益處低于用戶可以察覺的閾值時。務必將注意力集中在重要方面。
值得優化的常見操作示例包括:
- 圖像、聲音和視頻處理
- 渲染大型數據集或 3D 模型
- 搜索
- 響應用戶輸入
定義衡量標準
人們往往將性能和速度劃上等號,但千萬不要誤以為這是唯一重要的衡量標準。您可能發現需要針對內存使用或電池壽命進行調試。將這些降至***的應用程序也可以被認為比那些不降低的應用程序性能更高。有時優化其他衡量標準也可以提高速度,但其他時候需要做出折衷。
無論測量什么,您必須測量一些對象。如果不測量任何對象,您就無法得知更改是有利于還是有害于性能。良好的衡量標準有以下三個特性:
- 它們可以量化。可以測量它們并記錄為數字。
- 它們是一致的。您可以反復測量它們,并有效地比較測量結果。
- 它們有意義。測量值中的變化對應于您正在優化的對象。
為了使它更形象,假設您正在編寫一個應用程序,它將對一個大型圖像集執行一些圖像處理任務。在處理過程中,應用程序需要向用戶顯示進度反饋。它還必須允許用戶能取消操作,而不是等待操作完成。這是一個十分簡單的應用程序,但即便如此,它至少仍有三個重要的衡量標準可供審視。
示例:吞吐量
***個、最顯而易見的衡量標準是吞吐量。它在這個示例中是有意義的,因為我們知道自己必須處理大量圖像。吞吐量越高,處理完成得越快。
吞吐量可以輕松量化為每單位時間的處理量。盡管可以測量已處理圖像的數量,但當圖像大小不一時,測量字節數可以產生一致性更高的值。在這個示例中,直接測量每毫秒字節數作為吞吐量。
示例:內存使用
對于這個應用程序,一個不太顯眼的衡量標準是內存使用。對于最終用戶,內存使用不像吞吐量那樣顯而易見。為了監視內存使用,用戶必須運行另一個應用程序,如 Activity Monitor。但內存使用可能成為一個限制因素:內存不足,此時應用程序將無法正常運行。
內存使用對于我們的圖像處理示例是重要的,因為這些圖像本身很大。我們希望能處理大型圖像-即便是那些超出可用 RAM 的圖像-前提是不出現內存不足的情況。內存使用按字節測量很簡單。
示例:響應時間
我們的范例應用程序的***一個衡量標準往往被忽略:用戶輸入的響應時間。這個衡量標準對于您的所有用戶而言都是顯而易見的,雖然他們很少停下來測量它。它也十分普遍。用戶希望所有操作都能得到快速響應-無論是調整窗口大小、取消某個操作還是鍵入文本。
用戶認為某些衡量標準是線性的,而響應時間卻有一個重要的閾值。輸入響應只要超過 100 毫秒左右,用戶就會有慢的感覺。如果您的應用程序響應速度始終低于這個閾值,就沒有進一步優化的必要了。顯然,這個衡量標準可以按毫秒輕松量化。
響應時間對于圖像處理應用程序是一個重要挑戰,因為處理任何一張圖像的時間都遠遠超出 100 毫秒。在某些編程環境中,通過在連續計算線程以外的線程上處理用戶輸入來解決這個問題。而在內部,這種解決方法需要操作系統快速切換線程環境,確保用戶輸入線程可以及時響應。但 AIR 不提供明確的線程模型,所以必須直接完成這一切換操作。下一部分將說明這一操作。以下范例說明了設置圖像處理的三種不同方式,它們針對不同的衡量標準而優化:
- <?xml version="1.0" encoding="utf-8"?>
- <mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="horizontal" frameRate='45'>
- <mx:Script>
- <![CDATA[
- private static const DATASET_SIZE_MB:int = 100;
- private function doThroughput():void {
- var start:Number = new Date().time;
- var data:ByteArray = new ByteArray();
- data.length = DATASET_SIZE_MB * 1024 * 1024;
- filter( data );
- var end:Number = new Date().time;
- _throughputLabel.text = ( data.length / ( end - start )) + " bytes/msec";
- }
- private function doMemory():void {
- var start:Number = new Date().time;
- var data:ByteArray = new ByteArray();
- data.length = 1024 * 1024;
- for( var chunk:int = 0; chunk < DATASET_SIZE_MB; chunk++ ) {
- filter( data );
- }
- var end:Number = new Date().time;
- _memoryLabel.text = ( DATASET_SIZE_MB * data.length / ( end - start )) + " bytes/msec";
- }
- private function doResponse():void {
- _chunkStart = new Date().time;
- _chunkData = new ByteArray();
- _chunkData.length = 100 * 1024;
- _chunksRemaining = DATASET_SIZE_MB * 1024 / 100;
- _chunkTimer = new Timer( 1, 1 );
- _chunkTimer.addEventListener( TimerEvent.TIMER_COMPLETE, doChunk );
- _chunkTimer.start();
- }
- private function doChunk( event:TimerEvent ):void {
- var iterStart:Number = new Date().time;
- while( _chunksRemaining > 0 ) {
- filter( _chunkData );
- _chunksRemaining--;
- var now:Number = new Date().time;
- if( now - iterStart > 90 ) break;
- }
- if( _chunksRemaining > 0 ) {
- _chunkTimer.start();
- } else {
- var end:Number = new Date().time;
- _responseLabel.text = ( DATASET_SIZE_MB * 1024 * 1024 / ( end - _chunkStart )) + " bytes/msec";
- }
- }
- private var _chunkStart:Number;
- private var _chunkData:ByteArray;
- private var _chunksRemaining:int;
- private var _chunkTimer:Timer;
- private function filter( data:ByteArray ):void {
- for( var i:int = 0; i < data.length; i++ ) {
- data[i] = data[i] * data[i] + 2;
- }
- }
- private function onMouseMove( event:MouseEvent ):void {
- var global:Point = new Point( event.stageX, event.stageY );
- var local:Point = _canvas.globalToLocal( global );
- _button.x = local.x;
- _button.y = local.y;
- }
- ]]>
- </mx:Script>
- <mx:HBox width='100%' height='100%'>
- <mx:VBox width='50%' height='100%'>
- <mx:Button label='Measure throughput' click='doThroughput();' />
- <mx:Label id='_throughputLabel' />
- <mx:Button label='Reduce memory use' click='doMemory();' />
- <mx:Label id='_memoryLabel' />
- <mx:Button label='Maintain responsiveness' click='doResponse();' />
- <mx:Label id='_responseLabel' />
- </mx:VBox>
- <mx:Canvas
- width='50%' height='100%'
- id="_canvas"
- horizontalScrollPolicy="off"
- verticalScrollPolicy="off"
- backgroundColor="white"
- mouseMove='onMouseMove( event );'
- >
- <mx:Label text="Move Me" id="_button" />
- </mx:Canvas>
- </mx:HBox>
- </mx:WindowedApplication>
進行測量
當確定并定義衡量標準后,您首先必須能測量它們,隨后才能處理它們。只有通過前后兩次的測量和跟蹤,您才能確定那些變化的影響。盡可能同時跟蹤所有衡量標準,這樣可以了解為優化一個衡量標準所做的更改對其他衡量標準產生的影響。
測量吞吐量
可以通過程序輕松測量吞吐量。測量吞吐量的基本模式為:
- start_msec = new Date().time
- do_work()
- end_msec = new Date().time
- rate = bytes_processed / ( end_msec - start_msec )
測量內存
內存更復雜一些。包括 AIR 在內的大多數運行時環境不提供可以確定應用程序內存使用的適當 API。***使用外部工具監視內存使用,如 Activity Monitor (Mac OS X)、任務管理器 (Windows)、BigTop (Mac OS X) 等。選擇一個監視工具后,您需要決定要跟蹤哪個內存衡量標準。
虛擬內存是跟蹤工具的頭號報告對象。 人如其名,它不會測量進程使用的物理 RAM 量??梢詫⑺胂鬄檫M程使用的內存地址空間量。在某個時刻,分配給進程的一部分內存通常會存儲在磁盤而不是 RAM 中。人們通常認為 RAM 量以及占用的磁盤空間之和就是進程的虛擬內存,但地址空間的某些部分可能不在這兩個地方。具體情況取決于操作系統以及它根據不同目的分配虛擬內存部分的方式。
根據虛擬內存包含的內容,應用程序虛擬內存的絕對大小可能不是一個重要的衡量標準。您的應用程序相對于其他類似應用程序的虛擬內存可能是重要的,但依然很難進行有效比較。虛擬內存最重要的一個方面是它隨著時間流逝產生的行為:無限增長表明存在內存泄漏。其他內存衡量標準中可能不顯示內存泄漏,因為如果未引用泄漏的內存,它們會調入磁盤并駐留在那里。
可供監視的***內存衡量標準是專用字節,它測量進程單獨使用的 RAM 量。這個衡量標準直接表明您的應用程序對整個系統產生的影響,它使用的是共享資源。
專用字節會隨著應用程序分配和取消分配內存而波動。它也會隨著應用程序活動或空閑而波動,空閑時部分頁面會調入磁盤。要跟蹤專用字節,我建議在您優化的操作過程中使用監視工具進行定期采樣,即每秒一次。
監視工具包含的其他內存衡量標準包括駐留大小和共享字節。駐留大小是您的進程所使用的 RAM 總量,它由專用字節和共享字節組成。共享字節是與其他進程共享的 RAM 部分。這些部分通常包含只讀資源,如共享庫或系統框架中的代碼。雖然您可以跟蹤這些衡量指標,應用程序目前對專用字節值的控制度***,問題也最多。
響應時間
響應時間***用秒表測量。當用戶執行操作時開始計時,如單擊按鈕時。當應用程序響應時停止計時,通常更改顯示的用戶界面即可。將兩個計時相減就可以得出測量值。
優化流程
有了目標和衡量標準,就可以進行優化了。流程本身很簡單,并且應當很常見。重復以下三個步驟,直至完成:
- 測量
- 分析
- 修改
大致而言,分析可能產生兩種更改中的一種:設計或代碼。
設計更改
設計更改通常影響***。但是,在游戲后期進行設計更改難度可能更大,所以定義并測量性能目標之前不要耽擱太久。
例如,我們回到圖像處理應用程序。一種單純的實施方法是:將每個圖像完整加載到內存中,處理它,然后將結果寫回磁盤。這個應用程序的內存使用峰值(專用字節)主要就是已加載圖像大小的函數。如果圖像超出可用 RAM,應用程序將失敗。
圖像處理操作很少是全局的;大多數操作每次可以在圖像的某個部分上執行。通過將圖像分為固定大小的多個塊并且逐個處理這些塊,您可以將應用程序的內存使用峰值限制為選定的數值。這樣,處理超出可用 RAM 的圖像也成為可能。
修改設計后,請務必重新評估所有衡量標準。它們之間始終會出現一些相互作用,因為設計發生了變化。那些更改有時可能會出乎想象。當我構建這個范例應用程序的原型時,按固定大小的塊處理圖像并未大幅改變應用程序的吞吐量,我預計它可能變慢。
代碼更改
當不再需要增強設計時,可轉向代碼調試。這個方面可以嘗試許多技術。其中有些是 ActionScript 特有的;有些則不然。
切記不要過早應用代碼更改。它們可能會犧牲可讀性和性能結構。雖然不一定每次都會很糟,但是如果過早應用代碼更改,它們會降低您改進和維護應用程序的能力。正如 Donald Knuth 所說,“過早的優化是一切罪惡的源頭。”
特制的測試應用程序
真實的應用程序往往較大、較復雜并且滿是快速運行的代碼。為了幫助您既愛那個優化精力集中在主要操作上,可考慮創建一個專用測試應用程序。
除了其他優勢,測試應用程序提供了一個包含測試的空間(即,用于測量吞吐量),您無需將這個代碼包含在最終的應用程序中。
當然,將您的改進移回應用程序時,您需要驗證優化結果是否依然有效。
分塊
如前所述,AIR 運行時不提供在后臺線程上執行應用程序代碼的機制。在計算密集型任務過程中嘗試保持響應度時,這個問題尤為棘手。
與空間分塊可用于優化內存使用一樣,時間分塊可以將計算分為多個短時運行部分。通過響應各部分之間的用戶輸入,您可以保持應用程序的響應度。
以下偽代碼每次可以執行約 90 毫秒的工作,然后把控制權交給主事件循環。主事件循環確保已處理鼠標單擊等操作。根據這一時間安排,可以在 100 毫秒內處理大多數用戶輸入,從用戶角度而言,應用程序的響應速度已足夠。
- var timer:Timer = new Timer( 1, 1 )
- timer.addEventListener( TimerEvent.TIMER, doChunk )
- function doChunk( event:Event ):void {
- var start:Number = new Date().time
- while( workRemaining ) {
- doWork()
- var now:Number = new Date().time
- if( now - start > 90 ) {
- // reschedule more work to occur after input
- if( workRemaining )
- timer.start()
- break
- }
- }
- }
在此例中,為了保持響應度,doWork() 的運行時間必須遠遠小于塊持續時間。為了保持在 100 毫秒的最糟情況下,運行時間不能超出 10 毫秒。
再次強調,采用這類方法后,請重新測量所有衡量標準。在我的圖像處理應用程序中,采用這種分塊方法后吞吐量下降了約 10%。另一方面,我的應用程序在所有用戶輸入的 100 毫秒內可以做出響應,而不僅僅是在圖像之間。我認為這是一個合理的折衷。
包裝
創建高性能的應用程序并非易事,但要回應嚴格的測量、分析和不斷改進會遇到問題。AIR 應用程序在這個問題上沒有很大區別。
性能同時也是一個不斷變化的目標。不僅每一組改進可能影響到其他衡量標準,底層硬件、操作系統和其他更改也會改變快與慢之間的平衡。即便是您在優化的對象也可能隨時間發生變化。
憑借充分的實戰經驗,您可以創建出高性能的 AIR 應用程序并使它們持之以恒。切記不要放松警惕。只要有一個功能變慢,用戶就會發問,“您的應用程序是否可以達到執行 X 的速度?”
關于作者
自 Adobe AIR 誕生并發展出新穎的安裝技術以來,Oliver 一直致力于這個領域。在轉向 AIR 之前,他致力于 Adobe LiveCycle,而在加入 Adobe 之前,他從事的領域很廣,包括金融服務、數字信號處理和視頻游戲。他有時為 Dr. Dobb's Journal 撰稿,并且是 Kidos Computer 技術咨詢委員會的成員。他獲得了斯坦福大學的計算機本科及碩士學位。