Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
S2-group
GitHub Repository: S2-group/android-runner
Path: blob/master/AndroidRunner/Plugins/frametimes/Frametimes.py
630 views
1
import os.path as op
2
import os
3
import time
4
import timeit
5
import threading
6
import csv
7
8
from AndroidRunner.Plugins.Profiler import Profiler
9
10
11
class ConfigError(Exception):
12
pass
13
14
15
class Frametimes(Profiler):
16
def __init__(self, config, paths):
17
super(Frametimes, self).__init__(config, paths)
18
self.output_dir = ''
19
self.paths = paths
20
self.profile = False
21
self.interval = float(self.is_integer(config.get('sample_interval', 0))) / 1000
22
self.data = set()
23
24
def get_frame_times(self, device, app):
25
result = device.shell(
26
'dumpsys gfxinfo {} framestats | sed -n /--PROFILEDATA---/,/--PROFILEDATA---/p'.format(app))
27
28
if 'No process found' in result:
29
raise Exception('FrameTimes Profiler: {}'.format(result))
30
31
filteredResult = filter(lambda row: not row.startswith('Flags') and row != '---PROFILEDATA---', result.split())
32
33
return map(lambda stats: self.extract_frame_start_end(stats.split(',')), filteredResult)
34
35
def extract_frame_start_end(self, frame_times):
36
return [int(frame_times[1]), int(frame_times[13])]
37
38
def start_profiling(self, device, **kwargs):
39
self.profile = True
40
self.data = set()
41
app = kwargs.get('app', None)
42
self.get_data(device, app)
43
44
def get_data(self, device, app):
45
"""Runs the profiling methods every self.interval seconds in a separate thread"""
46
start = timeit.default_timer()
47
for frame in self.get_frame_times(device, app):
48
self.data.add(tuple(frame))
49
end = timeit.default_timer()
50
# timer results could be negative
51
interval = max(float(0), self.interval - max(0, end - start))
52
if self.profile:
53
threading.Timer(interval, self.get_data, args=(device, app)).start()
54
55
def stop_profiling(self, device, **kwargs):
56
self.profile = False
57
58
def collect_results(self, device, path=None):
59
times_filename = 'frame_times_{}_{}.csv'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S'))
60
delayed_filename = 'delayed_{}_{}.csv'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S'))
61
delayed_count = 0
62
63
with open(op.join(self.output_dir, times_filename), 'w+') as f:
64
writer = csv.writer(f)
65
writer.writerow(['frame_start', 'frame_end', 'frame_time', 'is_delayed'])
66
for row in self.data:
67
frame_time = row[1] - row[0]
68
# https://developer.android.com/topic/performance/vitals/render
69
# TL;DR; A frame is considered as delayed whenever it took more than 16m to render
70
if row[1] - row[0] > 16000000:
71
writer.writerow([row[0], row[1], frame_time, True])
72
delayed_count += 1
73
else:
74
writer.writerow([row[0], row[1], frame_time, False])
75
self.data = set()
76
77
with open(op.join(self.output_dir, delayed_filename), 'w+') as f:
78
writer = csv.writer(f)
79
writer.writerow(['delayed_frames_count'])
80
writer.writerow([delayed_count])
81
82
def set_output(self, output_dir):
83
self.output_dir = output_dir
84
85
def dependencies(self):
86
return []
87
88
def load(self, device):
89
return
90
91
def unload(self, device):
92
return
93
94
def aggregate_subject(self):
95
self.aggregate_delayed_frames()
96
self.aggregate_frame_times()
97
98
def aggregate_delayed_frames(self):
99
with open(op.join(self.output_dir, 'all_delayed_frame_counts.csv'), 'w+') as output:
100
writer = csv.writer(output)
101
writer.writerow(['delayed_frames'])
102
for output_file in os.listdir(self.output_dir):
103
if output_file.startswith("delayed_"):
104
writer.writerow([int(open(op.join(self.output_dir, output_file)).readlines()[1])])
105
106
def aggregate_frame_times(self):
107
with open(op.join(self.output_dir, 'all_frame_times.csv'), 'w+') as output:
108
writer = csv.writer(output)
109
writer.writerow(['frame_time'])
110
for output_file in os.listdir(self.output_dir):
111
if output_file.startswith("frame_times_"):
112
for row in open(op.join(self.output_dir, output_file)).readlines()[1:]:
113
writer.writerow([int(row.split(',')[2])])
114
115
def aggregate_end(self, data_dir, output_file):
116
return
117
118
def aggregate_final(self, data_dir):
119
return
120
121
def is_integer(self, number, minimum=0):
122
if not isinstance(number, int):
123
raise ConfigError('%s is not an integer' % number)
124
if number < minimum:
125
raise ConfigError('%s should be equal or larger than %i' % (number, minimum))
126
return number
127
128