用python第三方库(surface_stats_collector.py) 获取 Android FPS 帧数 过程分析_python 获取手机指定应用程序的fps-程序员宅基地

技术标签: 性能测试  Android  

(https://github.com/ChromiumWebApps/chromium/blob/master/build/android/pylib/perf/surface_stats_collector.py)
这段时间在做Android的性能测试,发现这种获取fps的方法比较方便,对其实现方式很好奇,但是在网上搜了下,相关的资料较少,大多都没讲清楚或,so记录下自己的分析过程,给大家个参考。

def Start(self):
    assert not self._collector_thread

    if self._ClearSurfaceFlingerLatencyData():
      self._get_data_event = threading.Event()
      self._stop_event = threading.Event()
      self._data_queue = Queue.Queue()
      self._collector_thread = threading.Thread(target=self._CollectorThread)
      self._collector_thread.start()
    else:
      self._use_legacy_method = True
      self._surface_before = self._GetSurfaceStatsLegacy()

首先从Start开始,先assert没有搜集线程在跑,在判断self._ClearSurfaceFlingerLatencyData():,看名字是清除surface数据的,跟进去看下:

def _ClearSurfaceFlingerLatencyData(self):
    """Clears the SurfaceFlinger latency data.
    Returns:
      True if SurfaceFlinger latency is supported by the device, otherwise
      False.
    """
    # The command returns nothing if it is supported, otherwise returns many
    # lines of result just like 'dumpsys SurfaceFlinger'.
    results = self._adb.RunShellCommand(
        'dumpsys SurfaceFlinger --latency-clear SurfaceView')
    return not len(results)

执行了adb shell dumpsys SurfaceFlinger --latency-clear SurfaceView命令,如果该命令返回空,说明硬件支持SurfaceFlinger latency,如果返回很多行结果(像adb shell dumpsys SurfaceFlinger 一样),说明硬件不支持SurfaceFlinger latency,好吧,我的手机返回的是支持SurfaceFlinger latency,我们先看支持的情况,回到Start函数:

def Start(self):
    assert not self._collector_thread

    if self._ClearSurfaceFlingerLatencyData():
      self._get_data_event = threading.Event()  #获取线程event对象
      self._stop_event = threading.Event()  #再来一个线程event对象
      self._data_queue = Queue.Queue() #申请个数据队列
      self._collector_thread = threading.Thread(target=self._CollectorThread) #线程,指向self._CollectorThread函数
      self._collector_thread.start() #线程启动
    else:
      self._use_legacy_method = True
      self._surface_before = self._GetSurfaceStatsLegacy()

去看看线程在干什么,跟到self._CollectorThread函数:

def _CollectorThread(self):
    last_timestamp = 0 #最后时间戳设置成0
    timestamps = [] #初始化一个时间戳数组
    retries = 0 #重试次数初始为0

    while not self._stop_event.is_set(): #self._stop_event第一次用,is_set()返回false,所以就进去了。
      self._get_data_event.wait(1) #阻塞线程1秒
      try:
        refresh_period, new_timestamps = self._GetSurfaceFlingerFrameData() #看下面分析,返回(刷新周期,[B列时间戳数组])
        if refresh_period is None or timestamps is None: #如果出错,重试3次
          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] #把循环得来的new_timestamps 放入总的timestamps
        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

self._GetSurfaceFlingerFrameData() 函数分析:

def _GetSurfaceFlingerFrameData(self):
    """Returns collected SurfaceFlinger frame timing data.返回手机的SurfaceFlinger 帧数据
    Returns:返回一个元组,(刷新周期,[时间戳数组])
      A tuple containing:
      - The display's nominal refresh period in seconds.
      - A list of timestamps signifying frame presentation times in seconds.
      The return value may be (None, None) if there was no data collected (for
      example, if the app was closed before the collector thread has finished).
    如果线程完成之前,app已经退出了,有可能返回None,None。
    """
    #这段是介绍 dumpsys SurfaceFlinger --latency命令的
    # adb shell dumpsys SurfaceFlinger --latency <window name>
    # prints some information about the last 128 frames displayed in
    # that window.
    # 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 第一行是刷新周期,换算毫秒是16.95毫秒
    # by 128 lines w/ 3 timestamps in nanosecond each: 后面跟着128行,每行3列数据
    #第一列表示app开始绘制的时间
    # A) when the app started to draw 
    #垂直同步软件把帧提交给硬件之前的瞬时时间戳,我理解是frame已经写入到共享内存的时间,后面就是交给硬件层去显示了
    # 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:
    #C-A,除以刷新周期,如果C-A大于刷新周期,就是一次jank,掉帧?
    # ceil((C - A) / refresh-period)
    #每出现一次这种情况,我们有了一次jank
    """
    # (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.
    #下面运行下命令
    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 #刷新周期,就是第一行的数据,转换成秒
    #3d的东西不太懂,大概意思就是说我们查询某帧数据出问题的时候,系统会返回一个64位的int最大值,我们会忽略这列数据
    # If a fence associated with a frame is still pending when we query the
    # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
    # Since we only care about completed frames, we will ignore any timestamps
    # with this value.
    pending_fence_timestamp = (1 << 63) - 1

    for line in results[1:]: #处理后面128行帧数据
      fields = line.split()
      if len(fields) != 3: #确认有3个数据待处理
        continue
      timestamp = long(fields[1]) #取中间的数据
      if timestamp == pending_fence_timestamp:#忽略异常数据
        continue
      timestamp /= nanoseconds_per_second #这个时间转换成秒
      timestamps.append(timestamp) #放入数据

    return (refresh_period, timestamps) #(刷新周期,[时间戳数组])

start函数暂时告一段落,总的来说初始化了一些数据,开启了一个数据收集线程。

下面来看Stop函数,就是收集结束时调用的。

def Stop(self):
    self._StorePerfResults()
    if self._collector_thread:
      self._stop_event.set()
      self._collector_thread.join()
      self._collector_thread = None

调用了self._StorePerfResults():

def _StorePerfResults(self):
    if self._use_legacy_method: #如果用legacy方法,我们上面用的不是这种方法,先不看
      surface_after = self._GetSurfaceStatsLegacy()
      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'])
      self._results.append(SurfaceStatsCollector.Result(
          'avg_surface_fps', int(round(frame_count / seconds)), 'fps'))
      return

    # Non-legacy method.
    assert self._collector_thread #确认收集数据的线程存在
    (refresh_period, timestamps) = self._GetDataFromThread()# _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)

看下数据结果对象的格式:

class Result(object):
   def __init__(self, name, value, unit):  #'refresh_period', refresh_period, 'seconds'
      self.name = name
      self.value = value
      self.unit = unit

几个计算结果的静态方法

  @staticmethod
 def _GetNormalizedDeltas(data, refresh_period, min_normalized_delta=None):
    deltas = [t2 - t1 for t1, t2 in zip(data, data[1:])]
    if min_normalized_delta != None:
      deltas = filter(lambda d: d / refresh_period >= min_normalized_delta,
                      deltas)
    return (deltas, [delta / refresh_period for delta in deltas])

  @staticmethod
  def _CalculateResults(refresh_period, timestamps, result_suffix):
    """Returns a list of SurfaceStatsCollector.Result."""
    frame_count = len(timestamps) #总共多少帧
    seconds = timestamps[-1] - timestamps[0] #算时间戳查,得到总的时间

    frame_lengths, normalized_frame_lengths = \  
        SurfaceStatsCollector._GetNormalizedDeltas(
            timestamps, refresh_period, _MIN_NORMALIZED_FRAME_LENGTH)
   #下面这块不是很明白,开始说要用C-A/刷新周期,这里却只用的第二列B的数据
   # 说下我对这块的理解,不知道对不对
   # B列数据是每帧绘制完成提交到硬件的时间戳,单位是纳秒,从开机算起。
   #_GetNormalizedDeltas返回的是:
    # [B2-B1,B3-B2,B4-B3.....], [B2-B1/周期,B3-B2/周期,B4-B3/周期.....]对应代码变量:
    #frame_lengths, normalized_frame_lengths,就是[每2帧之间的时间间隔]和[这个时间间隔/16.6667] 2个数组
    #然后用[每2帧的时间间隔]数组和刷新周期,又调用了一次_GetNormalizedDeltas函数
    #如果时间间隔差出现大于刷新周期的情况的话,就是一次jank,加起来就是jank_count,对应代码:
    #sum(1 for change in jankiness
    #                 if change > 0 and change < pause_threshold)
    if len(frame_lengths) < frame_count - 1:
      logging.warning('Skipping frame lengths that are too short.')
      frame_count = len(frame_lengths) + 1
    if len(frame_lengths) == 0:
      raise Exception('No valid frames lengths found.')
    length_changes, normalized_changes = \
        SurfaceStatsCollector._GetNormalizedDeltas(  
            frame_lengths, refresh_period)
    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)
    return [        #返回结果,包过平均fps,和 jank的总数
        SurfaceStatsCollector.Result(
            'avg_surface_fps' + result_suffix,
            int(round((frame_count - 1) / seconds)), 'fps'),
        SurfaceStatsCollector.Result(
            'jank_count' + result_suffix, jank_count, 'janks'),
        SurfaceStatsCollector.Result(
            'max_frame_delay' + result_suffix,
            round(max(normalized_frame_lengths)),
            'vsyncs'),
        SurfaceStatsCollector.Result(
            'frame_lengths' + result_suffix, normalized_frame_lengths,
            'vsyncs'),
    ]

  @staticmethod
  def _CalculateBuckets(refresh_period, timestamps):
    results = []
    for pct in [0.99, 0.5]:
      sliced = timestamps[min(int(-pct * len(timestamps)), -3) : ]
      #print sliced
      results += SurfaceStatsCollector._CalculateResults(
          refresh_period, sliced, '_' + str(int(pct * 100)))
    return results
至此,第一种计算fps完成了。

第二种就简单了,开始和结束时,分别用service call SurfaceFlinger 1013取帧数的index和开始结束时间戳,
2个index相减就是帧总数,在除以时间相减,就等到了平均fps,so easy!但是感觉这个方法有点问题,用的时间是
当前电脑返回的时间,可能算出来结果不太准。

  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(),
    }

总的来说,2种方法都不是绝对准确,从实际测试结果来看,还是能体现出效果的,可以当做一个量化的指标。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tianya0035/article/details/44541689

智能推荐

5个超厉害的资源搜索网站,每一款都可以让你的资源满满!_最全资源搜索引擎-程序员宅基地

文章浏览阅读1.6w次,点赞8次,收藏41次。生活中我们无时不刻不都要在网站搜索资源,但就是缺少一个趁手的资源搜索网站,如果有一个比较好的资源搜索网站可以帮助我们节省一大半时间!今天小编在这里为大家分享5款超厉害的资源搜索网站,每一款都可以让你的资源丰富精彩!网盘传奇一款最有效的网盘资源搜索网站你还在为找网站里面的资源而烦恼找不到什么合适的工具而烦恼吗?这款网站传奇网站汇聚了4853w个资源,并且它每一天都会持续更新资源;..._最全资源搜索引擎

Book类的设计(Java)_6-1 book类的设计java-程序员宅基地

文章浏览阅读4.5k次,点赞5次,收藏18次。阅读测试程序,设计一个Book类。函数接口定义:class Book{}该类有 四个私有属性 分别是 书籍名称、 价格、 作者、 出版年份,以及相应的set 与get方法;该类有一个含有四个参数的构造方法,这四个参数依次是 书籍名称、 价格、 作者、 出版年份 。裁判测试程序样例:import java.util.*;public class Main { public static void main(String[] args) { List <Book>_6-1 book类的设计java

基于微信小程序的校园导航小程序设计与实现_校园导航微信小程序系统的设计与实现-程序员宅基地

文章浏览阅读613次,点赞28次,收藏27次。相比于以前的传统手工管理方式,智能化的管理方式可以大幅降低学校的运营人员成本,实现了校园导航的标准化、制度化、程序化的管理,有效地防止了校园导航的随意管理,提高了信息的处理速度和精确度,能够及时、准确地查询和修正建筑速看等信息。课题主要采用微信小程序、SpringBoot架构技术,前端以小程序页面呈现给学生,结合后台java语言使页面更加完善,后台使用MySQL数据库进行数据存储。微信小程序主要包括学生信息、校园简介、建筑速看、系统信息等功能,从而实现智能化的管理方式,提高工作效率。

有状态和无状态登录

传统上用户登陆状态会以 Session 的形式保存在服务器上,而 Session ID 则保存在前端的 Cookie 中;而使用 JWT 以后,用户的认证信息将会以 Token 的形式保存在前端,服务器不需要保存任何的用户状态,这也就是为什么 JWT 被称为无状态登陆的原因,无状态登陆最大的优势就是完美支持分布式部署,可以使用一个 Token 发送给不同的服务器,而所有的服务器都会返回同样的结果。有状态和无状态最大的区别就是服务端会不会保存客户端的信息。

九大角度全方位对比Android、iOS开发_ios 开发角度-程序员宅基地

文章浏览阅读784次。发表于10小时前| 2674次阅读| 来源TechCrunch| 19 条评论| 作者Jon EvansiOSAndroid应用开发产品编程语言JavaObjective-C摘要:即便Android市场份额已经超过80%,对于开发者来说,使用哪一个平台做开发仍然很难选择。本文从开发环境、配置、UX设计、语言、API、网络、分享、碎片化、发布等九个方面把Android和iOS_ios 开发角度

搜索引擎的发展历史

搜索引擎的发展历史可以追溯到20世纪90年代初,随着互联网的快速发展和信息量的急剧增加,人们开始感受到了获取和管理信息的挑战。这些阶段展示了搜索引擎在技术和商业模式上的不断演进,以满足用户对信息获取的不断增长的需求。

随便推点

控制对象的特性_控制对象特性-程序员宅基地

文章浏览阅读990次。对象特性是指控制对象的输出参数和输入参数之间的相互作用规律。放大系数K描述控制对象特性的静态特性参数。它的意义是:输出量的变化量和输入量的变化量之比。时间常数T当输入量发生变化后,所引起输出量变化的快慢。(动态参数) ..._控制对象特性

FRP搭建内网穿透(亲测有效)_locyanfrp-程序员宅基地

文章浏览阅读5.7w次,点赞50次,收藏276次。FRP搭建内网穿透1.概述:frp可以通过有公网IP的的服务器将内网的主机暴露给互联网,从而实现通过外网能直接访问到内网主机;frp有服务端和客户端,服务端需要装在有公网ip的服务器上,客户端装在内网主机上。2.简单的图解:3.准备工作:1.一个域名(www.test.xyz)2.一台有公网IP的服务器(阿里云、腾讯云等都行)3.一台内网主机4.下载frp,选择适合的版本下载解压如下:我这里服务器端和客户端都放在了/usr/local/frp/目录下4.执行命令# 服务器端给执_locyanfrp

UVA 12534 - Binary Matrix 2 (网络流‘最小费用最大流’ZKW)_uva12534-程序员宅基地

文章浏览阅读687次。题目:http://acm.hust.edu.cn/vjudge/contest/view.action?cid=93745#problem/A题意:给出r*c的01矩阵,可以翻转格子使得0表成1,1变成0,求出最小的步数使得每一行中1的个数相等,每一列中1的个数相等。思路:网络流。容量可以保证每一行和每一列的1的个数相等,费用可以算出最小步数。行向列建边,如果该格子是_uva12534

免费SSL证书_csdn alphassl免费申请-程序员宅基地

文章浏览阅读504次。1、Let's Encrypt 90天,支持泛域名2、Buypass:https://www.buypass.com/ssl/resources/go-ssl-technical-specification6个月,单域名3、AlwaysOnSLL:https://alwaysonssl.com/ 1年,单域名 可参考蜗牛(wn789)4、TrustAsia5、Alpha..._csdn alphassl免费申请

测试算法的性能(以选择排序为例)_算法性能测试-程序员宅基地

文章浏览阅读1.6k次。测试算法的性能 很多时候我们需要对算法的性能进行测试,最简单的方式是看算法在特定的数据集上的执行时间,简单的测试算法性能的函数实现见testSort()。【思想】:用clock_t计算某排序算法所需的时间,(endTime - startTime)/ CLOCKS_PER_SEC来表示执行了多少秒。【关于宏CLOCKS_PER_SEC】:以下摘自百度百科,“CLOCKS_PE_算法性能测试

Lane Detection_lanedetectionlite-程序员宅基地

文章浏览阅读1.2k次。fromhttps://towardsdatascience.com/finding-lane-lines-simple-pipeline-for-lane-detection-d02b62e7572bIdentifying lanes of the road is very common task that human driver performs. This is important ..._lanedetectionlite

推荐文章

热门文章

相关标签