Android效能測試之fps獲取
關鍵點
在testerhome看到一個好的帖子,說的是fps的獲取方式,值得好好研究一下。
獲取的方式是通過下面的命令獲取
adb shell dumpsys SurfaceFlinger --latency <window_activity>
命令意義
上面的命令是做什麼的?
可以看看老羅的關於SurfaceFlinger的詳細講解,那我這裡只是簡單的描述一下:
SurfaceFlinger是一個系統服務,管理Android幀緩衝區,瞭解這些就足夠啦,因為我們要獲得的FPS值(Frames Per Second)中文翻譯過來是每秒鐘填充影象的幀數。
ok,那我們知道了這個服務的作用。
命令的結果
我們來看一下這個命令的結果,取android系統的主介面的幀資料
qianhuis-Mac-mini:app qianhui$ adb shell dumpsys SurfaceFlinger --latency com.android.launcher/com.android.launcher2.Launcher 16666666 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 53476438728 53483331194 53476438728 53774334579 53783331182 53774334579 53804473320 53833331180 53804473320 53821433876 53849997846 53821433876 54482172942 54499997820 54482172942 62828275267 62849997486 62828275267 77744212604 77749996890 77744212604 137676463526 137683327826 137676463526 197665365491 197683325426 197665365491 257656215141 257666656360 257656215141 317667889815 317666653960 317667889815 377658368227 377666651560 377658368227 437659404105 437666649160 437659404105 497680028798 497683313426 497680028798 557661828695 557666644360 557661828695 617669142813 617683308626 617669142813 677664261743 677683306226 677664261743 737664607859 737683303826 737664607859 797520577853 797549968098 797520577853 797663672552 797683301426 797663672552 813703318654 813749967450 813703318654 857696035562 857716632358 857696035562 917697480455 917716629958 917697480455 977667175775 977666627560 977667175775 1037666198546 1037666625160 1037666198546 1097679780732 1097699956092 1097679780732 1157680091691 1157699953692 1157680091691 1217681064721 1217699951292 1217681064721 1277681725100 1277699948892 1277681725100 1337665343758 1337683279826 1337665343758 1397664084052 1397683277426 1397664084052 1457665440087 1457683275026 1457665440087 1517669937332 1517666605960 1517669937332 1524620120922 1524649939014 1524620120922 1549500333658 1549533271352 1549500333658 1577676396539 1577699936892 1577676396539 1637681916267 1637699934492 1637681916267 1697678605745 1697699932092 1697678605745 1757681427632 1757699929692 1757681427632 1817680212373 1817699927292 1817680212373 1877681586834 1877699924892 1877681586834 1937702217412 1937733255824 1937702217412 1997665879256 1997683253426 1997665879256 2057664998469 2057683251026 2057664998469 2117667796048 2117683248626 2117667796048 2177667609969 2177683246226 2177667609969 2237666557333 2237666577160 2237666557333 2260655440820 2260683242906 2260655440820 2295708486334 2295733241504 2295708486334 2297667588171 2297666574760 2297667588171 2357668458964 2357666572360 2357668458964 2417677948705 2417699903292 2417677948705 2477680415203 2477699900892 2477680415203 2537681084888 2537699898492 2537681084888 2597682623610 2597699896092 2597682623610 2657662892357 2657683227026 2657662892357 2717663341559 2717683224626 2717663341559 2777684593156 2777683222226 2777684593156 2837677400623 2837699886492 2837677400623 2897718308856 2897733217424 2897718308856 2957669662475 2957683215026 2957669662475 3007072891033 3007099879716 3007072891033 3017678196794 3017699879292 3017678196794 3077679633292 3077683210226 3077679633292 3137681037968 3137699874492 3137681037968 3169623894137 3169666539880 3169623894137 3197683176766 3197699872092 3197683176766 3257684223564 3257699869692 3257684223564 3317680588767 3317733200624 3317680588767 3377665920385 3377683198226 3377665920385 3437676819013 3437683195826 3437676819013 3497666530549 3497683193426 3497666530549 3557665435190 3557666524360 3557665435190 3617697519980 3617716521958 3617697519980 3677680073314 3677699852892 3677680073314 3737679371848 3737699850492 3737679371848 3797700730719 3797733181424 3797700730719 3857682152646 3857699845692 3857682152646 3881320403971 3881349844746 3881320403971 3917683549768 3917699843292 3917683549768
是不是有點暈,這些數字都是啥跟啥啊。其實testerhome上面的那篇文章提供了獲取fps方法具體解釋
用到了一個第三方的庫:pylib
# adb shell dumpsys SurfaceFlinger --latency <window name> # prints some information about the last 128 frames displayed in # that window.只打印128行的幀資料 # The data returned looks like this: # 16954612 # 7657467895508 7657482691352 7657493499756 # 7657484466553 7657499645964 7657511077881 # 7657500793457 7657516600576 7657527404785 # (...) # # The first line is the refresh period (here 16.95 ms), it is followed # by 128 lines w/ 3 timestamps in nanosecond each: # A) when the app started to draw # B) the vsync immediately preceding SF submitting the frame to the h/w # C) timestamp immediately after SF submitted that frame to the h/w # # The difference between the 1st and 3rd timestamp is the frame-latency. # An interesting data is when the frame latency crosses a refresh period # boundary, this can be calculated this way: # # ceil((C - A) / refresh-period) # # (each time the number above changes, we have a "jank"). # If this happens a lot during an animation, the animation appears # janky, even if it runs at 60 fps in average. # # We use the special "SurfaceView" window name because the statistics for # the activity's main window are not updated when the main web content is # composited into a SurfaceView.
上面的解釋資訊有如下主要資訊:
資料的單位是納秒,時間是以開機時間為起始點。
每一次的命令都會得到128行的幀相關的資料。
第一行資料,表示重新整理的時間間隔refresh_period,我的機器打印出來的間隔期是:
16666666/1000/1000 = 16.67ms(毫秒)
那麼剩下來127行的資料分為3部分,每一行的資料的每一列都代表一部分。
第一部分
這一部分的資料表示應用程式繪製圖像的時間點。
第二部分
在SF(軟體)將幀提交給H/W(硬體)繪製之前的垂直同步時間。
第三部分
在SF將幀提交給H/W的時間點,算是H/W接受完SF發來資料的時間點,繪製完成的時間點。
那麼可以看出第一部分和第三部分類似,那麼差異在於哪裡?差異在於幀有延遲時間,從準備好繪製完成繪製的時間間隔就是幀延遲。
何為jank,即掉幀 。每一行都可以通過下面的公式得到一個值,該值是一個標準,我們稱為jankflag,如果當前行的jankflag與上一行的jankflag發生改變,那麼就叫掉幀。
ceil((C - A) / refresh-period)
所以掉幀是一個狀態。
FPS計算
那麼我們要用到上面的哪些資料了?那麼我們去庫裡面一步一步去按照方法來找到最後的答案,首先它是呼叫了下面方法:
collector = surface_stats_collector.SurfaceStatsCollector(adb, activityName,0.5)
results = collector.SampleResults()
那麼我們首先找到SurfaceStatsCollector這個類,在pylib庫中的perl包下
SampleResults方法:
def SampleResults(self):
self._StorePerfResults()
results = self.GetResults()
self._results = []
return results
首先呼叫了_StorePerfResults方法
從上面看出計算方法有兩種方式,一個是支援legacy方法,一個不支援legacy方法。
第一種情況
支援legacy方法。判斷是否支援legacy方法,可以通過執行dumpsys SurfaceFlinger --latency-clear SurfaceView來判斷。
然後我們進入
def _GetSurfaceStatsLegacy(self):
"""Legacy method (before JellyBean), returns the current Surface index
and timestamp.
Calculate FPS by measuring the difference of Surface index returned by
SurfaceFlinger in a period of time.
Returns:
Dict of {page_flip_count (or 0 if there was an error), timestamp}.
"""
results = self._adb.RunShellCommand('service call SurfaceFlinger 1013')
assert len(results) == 1
match = re.search('^Result: Parcel\((\w+)', results[0])
cur_surface = 0
if match:
try:
cur_surface = int(match.group(1), 16)
except Exception:
logging.error('Failed to parse current surface from ' + match.group(1))
else:
logging.warning('Failed to call SurfaceFlinger surface ' + results[0])
return {
'page_flip_count': cur_surface,
'timestamp': datetime.datetime.now(),
}
這個_GetSurfaceStatsLegacy方法會呼叫"service call SurfaceFlinger 1013"這個命令,結果是如下形式:
10|[email protected]_x86:/ # service call SurfaceFlinger 1013
Result: Parcel(00000b5e '^...')
然後我們會提取裡面的00000b5e這個16進位制的數,然後通過int('str',16)方法將16進位制數轉化為10進位制的數,這個值就是當前surface的索引值。可以通過2個不同索引值的surface之間的間隔時間獲得。然後返回一個字典,裡面包含了2個內容:當前surface索引,以及當前時間戳。這個方法就結束了。該字典將賦值給surface_after。我們就回到了_StorePerfResults方法中,看下一行要執行的程式碼:
td = surface_after['timestamp'] - self._surface_before['timestamp']
seconds =td.seconds +td.microseconds/1e6
frame_count = (surface_after['page_flip_count'] -self._surface_before['page_flip_count']
上面的程式碼,是將這次的時間-上次的時間,得到的值就是2次獲取surface索引的時間間隔,賦值給td,然後取得td的秒數,但是精確到微妙級別。賦值給seconds。
然後計算2次surface索引值的差值,得到的值就是在seconds時間內產生surface的個數。賦值給frame_count。
然後用frame_count/seconds公式計算,做4舍5入,最後轉化為整形,追加到results陣列中,該方法就返回了。就到了SampleResults方法中:
results = self.GetResults()
self._results = []
由於上面的第二行可以看出來,每次的results的陣列都會重新設定為空,那麼當前results的值就只有一個值,就是我們這次所獲得值,所以返回的results數組裡就只有一個數值。就是我們的fps的值,然後至於你獲得多少次這樣的值,以及間隔時間,那是你自己決定的事啦。
what?
有人看完上面的內容,有蒙的感覺麼?其實我也蒙了,我們說了那麼多的dumpsys SurfaceFlinger --latency,但是經過我們一分析,居然真正的計算是沒有用到這個裡面的資料的,是不是很奔潰。但是要想到的一點是,在呼叫collector.SampleResults()方法前,是需要啟動_CollectorThread(self)的,嘗試執行dumpsys命令3次獲得需要的值。該執行緒中的就是呼叫了
_GetSurfaceFlingerFrameData()裡的方法,具體實現細節如下程式碼所示。
def _CollectorThread(self):
last_timestamp = 0
timestamps = []
retries = 0
while not self._stop_event.is_set():
self._get_data_event.wait(1)
try:
refresh_period, new_timestamps = self._GetSurfaceFlingerFrameData()
if refresh_period is None or timestamps is None:
retries += 1
if retries < 3:
continue
if last_timestamp:
# Some data has already been collected, but either the app
# was closed or there's no new data. Signal the main thread and
# wait.
self._data_queue.put((None, None))
self._stop_event.wait()
break
raise Exception('Unable to get surface flinger latency data')
timestamps += [timestamp for timestamp in new_timestamps
if timestamp > last_timestamp]
if len(timestamps):
last_timestamp = timestamps[-1]
if self._get_data_event.is_set():
self._get_data_event.clear()
self._data_queue.put((refresh_period, timestamps))
timestamps = []
except Exception as e:
# On any error, before aborting, put the exception into _data_queue to
# prevent the main thread from waiting at _data_queue.get() infinitely.
self._data_queue.put(e)
raise
_GetSurfaceFlingerFrameData方法實現細節如下:
def _GetSurfaceFlingerFrameData(self):
results = self._adb.RunShellCommand(
'dumpsys SurfaceFlinger --latency SurfaceView',
log_result=logging.getLogger().isEnabledFor(logging.DEBUG))
if not len(results):
return (None, None)
timestamps = []
nanoseconds_per_second = 1e9
refresh_period = long(results[0]) / nanoseconds_per_second
pending_fence_timestamp = (1 << 63) - 1
for line in results[1:]:
fields = line.split()
if len(fields) != 3:
continue
timestamp = long(fields[1])
if timestamp == pending_fence_timestamp:
continue
timestamp /= nanoseconds_per_second
timestamps.append(timestamp)
return (refresh_period, timestamps)
上面的具體執行流程,是先執行dumpsys SurfaceFlinger --latency 命令,從得到的128行資料中,提取第一行的重新整理時間,然後得到每一行的中第二部分資料,獲取其中秒數,儲存到timestamps陣列中。然後方法返回包含重新整理時間refresh_period和timestamps的陣列。然後回到執行緒的主方法中。
1.判斷獲取到的資料為NONE
如果嘗試的次數沒到3次,再執行一遍dumpsys命令。
如果超過了3次,我們要判斷上一次獲取的timestamps陣列中最後一個值是否為0。因為last_timestamp的賦值語句如下:
last_timestamp = timestamps[-1]
如果不為0,說明我們已經收集過了,直接在佇列中加上NONE值,讓執行緒等待,跳出迴圈了。否則丟擲異常
2.不為NONE
如果為0,說明我們fps的資料還沒有開始收集,我們會將_GetSurfaceFlingerFrameData方法返回的陣列中元素一個一個賦值到我們本地陣列中。然後給last_timestamp賦值。
然後我們會向_data_queue佇列中新增refresh_period, timestamps資料。
總結
從上面的分析可知,要想獲得獲取fps值,需要3步:
1.adb shell dumpsys SurfaceFlinger --latency命令產生fps資料
2.通過service call SurfaceFlinger 1013 來得到當前幀的索引以及時間戳,設定為A = {indexA,timeA}
3.公式:
設上一次的資料為B = {indexB,timeB}
FPS = (indexA-indexB)/(timeA-timeB)
第二種情況
當--latency-clear不能使用,也就是`service call SurfaceFlinger 1013
`命令不能使用,那自然上面的方法就不起作用了。這個時候我們就要從下面的程式碼進行分析了:
# Non-legacy method.
assert self._collector_thread
(refresh_period, timestamps) = self._GetDataFromThread()
if not refresh_period or not len(timestamps) >= 3:
if self._warn_about_empty_data:
logging.warning('Surface stat data is empty')
return
self._results.append(SurfaceStatsCollector.Result(
'refresh_period', refresh_period, 'seconds'))
self._results += self._CalculateResults(refresh_period, timestamps, '')
self._results += self._CalculateBuckets(refresh_period, timestamps)
首先我們從執行緒中獲得dumpsys命令得到的值,然後建立Result物件,該物件中含有屬性名和屬性值,以及單位。
然後我們要進入_CalculateResults和_CalculateBuckets方法。
_CalculateResults
上面的方法中,首先得到幀的數量frame_count,然後得到產生frame_count所用的時間seconds。然後呼叫_GetNormalizedDeltas方法
上面巧妙的使用zip來計算各個資料之間的差值。由於_MIN_NORMALIZED_FRAME_LENGTH =0.5,所以要執行後續的語句,filter函式中,從deltas陣列中取出元素除以refresh_period,判斷杯除後的值是否大於0.5,這個函式作用過濾掉被除後小於0.5的值。那麼我們最終返回的值就是這個陣列deltas,以及陣列中每個元素除以refresh_period後的生成的新的陣列。然後回到_CalculateResults方法中,差值陣列賦值給frame_lengths,新的陣列賦值給normalized_frame_lengths。然後對frame_lengths的個數進行判斷。然後我們再呼叫一次_GetNormalizedDeltas,這個時候傳入的min_normalized_delta是空的,所以不會執行filter函式。直接求出frame_lengths陣列中各個元素的差值儲存到陣列deltas中。然後再計算deltas的值與refresh_period比值,這是為了求jank(掉幀)。然後方法返回,將deltas值賦給length_changes,將比值賦給normalized_changes。
為了求出jank,我們需要求出normalized_changes陣列中比0大的數。下面的程式碼就是求出jank_count程式碼塊。
jankiness = [max(0, round(change)) for change in normalized_changes]
pause_threshold = 20
jank_count = sum(1 for change in jankiness
if change > 0 and change < pause_threshold)
jank_count初始值設為,然後遍歷jankiness,求出大於0,小於20的數的個數。這個值就是jank的值,掉幀值。而fps的值是通過下面公式得到的:
int(round((frame_count - 1) / seconds))
這樣我們就得到了fps和jank,然後fps-jank 就是我們要得到的數。
_CalculateBuckets
這個地方是幫助你去頭去尾後的資料。
總結
當無法使用--latency--clear方法的時候,我們需要計算fps和jank的值,
fps的值計算公司變為int(round((frame_count-1)/ seconds)),
而且還可以得到吊幀的個數
感謝
kasi前輩的補充