mirror of
				https://github.com/ARM-software/workload-automation.git
				synced 2025-10-29 22:24:51 +00:00 
			
		
		
		
	instrumentation: add FPS instrument.
Add an instrument to collect FPS (frames per second) and associated rendering statics using corresponding devlib functionality.
This commit is contained in:
		
							
								
								
									
										169
									
								
								wa/instrumentation/fps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								wa/instrumentation/fps.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| import os | ||||
| import shutil | ||||
|  | ||||
| from devlib import SurfaceFlingerFramesInstrument, GfxInfoFramesInstrument | ||||
| from devlib import DerivedSurfaceFlingerStats, DerivedGfxInfoStats | ||||
|  | ||||
| from wa import Instrument, Parameter, WorkloadError | ||||
| from wa.utils.types import numeric | ||||
|  | ||||
|  | ||||
| class FpsInstrument(Instrument): | ||||
|  | ||||
|     name = 'fps' | ||||
|     description = """ | ||||
|     Measures Frames Per Second (FPS) and associated metrics for a workload. | ||||
|  | ||||
|     .. note:: This instrument depends on pandas Python library (which is not part of standard | ||||
|               WA dependencies), so you will need to install that first, before you can use it. | ||||
|  | ||||
|     Android L and below use SurfaceFlinger to calculate the FPS data. | ||||
|     Android M and above use gfxinfo to calculate the FPS data. | ||||
|  | ||||
|     SurfaceFlinger: | ||||
|     The view is specified by the workload as ``view`` attribute. This defaults | ||||
|     to ``'SurfaceView'`` for game workloads, and ``None`` for non-game | ||||
|     workloads (as for them FPS mesurement usually doesn't make sense). | ||||
|     Individual workloads may override this. | ||||
|  | ||||
|     gfxinfo: | ||||
|     The view is specified by the workload as ``package`` attribute. | ||||
|     This is because gfxinfo already processes for all views in a package. | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     parameters = [ | ||||
|         Parameter('drop_threshold', kind=numeric, default=5, | ||||
|                   description=""" | ||||
|                   Data points below this FPS will be dropped as they do not | ||||
|                   constitute "real" gameplay. The assumption being that while | ||||
|                   actually running, the FPS in the game will not drop below X | ||||
|                   frames per second, except on loading screens, menus, etc, | ||||
|                   which should not contribute to FPS calculation. | ||||
|                   """), | ||||
|         Parameter('keep_raw', kind=bool, default=False, | ||||
|                   description=""" | ||||
|                   If set to ``True``, this will keep the raw dumpsys output in | ||||
|                   the results directory (this is maily used for debugging) | ||||
|                   Note: frames.csv with collected frames data will always be | ||||
|                   generated regardless of this setting. | ||||
|                    """), | ||||
|         Parameter('crash_threshold', kind=float, default=0.7, | ||||
|                   description=""" | ||||
|                   Specifies the threshold used to decided whether a | ||||
|                   measured/expected frames ration indicates a content crash. | ||||
|                   E.g. a value of ``0.75`` means the number of actual frames | ||||
|                   counted is a quarter lower than expected, it will treated as | ||||
|                   a content crash. | ||||
|  | ||||
|                   If set to zero, no crash check will be performed. | ||||
|                   """), | ||||
|         Parameter('period', kind=float, default=2, constraint=lambda x: x > 0, | ||||
|                   description=""" | ||||
|                   Specifies the time period between polling frame data in | ||||
|                   seconds when collecting frame data. Using a lower value | ||||
|                   improves the granularity of timings when recording actions | ||||
|                   that take a short time to complete.  Note, this will produce | ||||
|                   duplicate frame data in the raw dumpsys output, however, this | ||||
|                   is filtered out in frames.csv.  It may also affect the | ||||
|                   overall load on the system. | ||||
|  | ||||
|                   The default value of 2 seconds corresponds with the | ||||
|                   NUM_FRAME_RECORDS in | ||||
|                   android/services/surfaceflinger/FrameTracker.h (as of the | ||||
|                   time of writing currently 128) and a frame rate of 60 fps | ||||
|                   that is applicable to most devices. | ||||
|                   """), | ||||
|         Parameter('force_surfaceflinger', kind=bool, default=False, | ||||
|                   description=""" | ||||
|                   By default, the method to capture fps data is based on | ||||
|                   Android version.  If this is set to true, force the | ||||
|                   instrument to use the SurfaceFlinger method regardless of its | ||||
|                   Android version. | ||||
|                   """), | ||||
|     ] | ||||
|  | ||||
|     def __init__(self, target, **kwargs): | ||||
|         super(FpsInstrument, self).__init__(target, **kwargs) | ||||
|         self.collector = None | ||||
|         self.processor = None | ||||
|         self._is_enabled = None | ||||
|  | ||||
|     def setup(self, context): | ||||
|         use_gfxinfo = self.target.get_sdk_version() >= 23 and not self.force_surfaceflinger | ||||
|         if use_gfxinfo: | ||||
|             collector_target_attr = 'package' | ||||
|         else: | ||||
|             collector_target_attr = 'view' | ||||
|         collector_target = getattr(context.workload, collector_target_attr, None) | ||||
|  | ||||
|         if not collector_target: | ||||
|             self._is_enabled = False | ||||
|             msg = 'Workload {} does not define a {}; disabling frame collection and FPS evaluation.' | ||||
|             self.logger.info(msg.format(context.workload.name, collector_target_attr)) | ||||
|             return | ||||
|  | ||||
|         self._is_enabled = True | ||||
|         if use_gfxinfo: | ||||
|             self.collector = GfxInfoFramesInstrument(self.target, collector_target, self.period) | ||||
|             self.processor = DerivedGfxInfoStats(self.drop_threshold, filename='fps.csv') | ||||
|         else: | ||||
|             self.collector = SurfaceFlingerFramesInstrument(self.target, collector_target, self.period) | ||||
|             self.processor = DerivedSurfaceFlingerStats(self.drop_threshold, filename='fps.csv') | ||||
|         self.collector.reset() | ||||
|  | ||||
|     def start(self, context): | ||||
|         if not self._is_enabled: | ||||
|             return | ||||
|         self.collector.start() | ||||
|  | ||||
|     def stop(self, context): | ||||
|         if not self._is_enabled: | ||||
|             return | ||||
|         self.collector.stop() | ||||
|  | ||||
|     def update_result(self, context): | ||||
|         if not self._is_enabled: | ||||
|             return | ||||
|         outpath = os.path.join(context.output_directory, 'frames.csv') | ||||
|         frames_csv = self.collector.get_data(outpath) | ||||
|         raw_output = self.collector.get_raw() | ||||
|  | ||||
|         processed = self.processor.process(frames_csv) | ||||
|         processed.extend(self.processor.process_raw(*raw_output)) | ||||
|         fps, frame_count, fps_csv = processed[:3] | ||||
|         rest = processed[3:] | ||||
|  | ||||
|         context.add_metric(fps.name, fps.value, fps.units) | ||||
|         context.add_metric(frame_count.name, frame_count.value, frame_count.units) | ||||
|         context.add_artifact('frames', frames_csv.path, kind='raw') | ||||
|         context.add_artifact('fps', fps_csv.path, kind='data') | ||||
|         for metric in rest: | ||||
|             context.add_metric(metric.name, metric.value, metric.units, lower_is_better=True) | ||||
|  | ||||
|         if not self.keep_raw: | ||||
|             for entry in raw_output: | ||||
|                 if os.path.isdir(entry): | ||||
|                     shutil.rmtree(entry) | ||||
|                 elif os.path.isfile(entry): | ||||
|                     os.remove(entry) | ||||
|  | ||||
|         if not frame_count.value: | ||||
|             context.add_event('Could not frind frames data in gfxinfo output') | ||||
|             context.set_status('PARTIAL') | ||||
|  | ||||
|         self.check_for_crash(context, fps.value, frame_count.value, | ||||
|                              context.current_job.run_time.total_seconds()) | ||||
|  | ||||
|     def check_for_crash(self, context, fps, frames, exec_time): | ||||
|         if not self.crash_threshold: | ||||
|             return | ||||
|         self.logger.debug('Checking for crashed content.') | ||||
|         if all([exec_time, fps, frames]): | ||||
|             expected_frames = fps * exec_time | ||||
|             ratio = frames / expected_frames | ||||
|             self.logger.debug('actual/expected frames: {:.2}'.format(ratio)) | ||||
|             if ratio < self.crash_threshold: | ||||
|                 msg = 'Content for {} appears to have crashed.\n'.format(context.current_job.spec.label) | ||||
|                 msg += 'Content crash detected (actual/expected frames: {:.2}).'.format(ratio) | ||||
|                 raise WorkloadError(msg) | ||||
		Reference in New Issue
	
	Block a user