Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(33)

Side by Side Diff: experimental/telemetry_mini/telemetry_mini.py

Issue 2996133002: [experimental] Add Flipkart/Instagram story (Closed)
Patch Set: complete work Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « experimental/telemetry_mini/android_go_stories.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2017 The Chromium Authors. All rights reserved. 2 # Copyright 2017 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 # This is intended to be a very trimmed down, single-file, hackable, and easy 6 # This is intended to be a very trimmed down, single-file, hackable, and easy
7 # to understand version of Telemetry. It's able to run simple user stories on 7 # to understand version of Telemetry. It's able to run simple user stories on
8 # Android, grab traces, and extract metrics from them. May be useful to 8 # Android, grab traces, and extract metrics from them. May be useful to
9 # diagnose issues with Chrome, reproduce regressions or prototype new user 9 # diagnose issues with Chrome, reproduce regressions or prototype new user
10 # stories. 10 # stories.
(...skipping 12 matching lines...) Expand all
23 import posixpath 23 import posixpath
24 import re 24 import re
25 import socket 25 import socket
26 import subprocess 26 import subprocess
27 import tempfile 27 import tempfile
28 import time 28 import time
29 import websocket # pylint: disable=import-error 29 import websocket # pylint: disable=import-error
30 from xml.etree import ElementTree as element_tree 30 from xml.etree import ElementTree as element_tree
31 31
32 32
33 KEYCODE_HOME = 3
33 KEYCODE_BACK = 4 34 KEYCODE_BACK = 4
35 KEYCODE_APP_SWITCH = 187
34 36
35 # Parse rectangle bounds given as: '[left,top][right,bottom]'. 37 # Parse rectangle bounds given as: '[left,top][right,bottom]'.
36 RE_BOUNDS = re.compile( 38 RE_BOUNDS = re.compile(
37 r'\[(?P<left>\d+),(?P<top>\d+)\]\[(?P<right>\d+),(?P<bottom>\d+)\]') 39 r'\[(?P<left>\d+),(?P<top>\d+)\]\[(?P<right>\d+),(?P<bottom>\d+)\]')
38 40
39 # TODO: Maybe replace with a true on-device temp file. 41 # TODO: Maybe replace with a true on-device temp file.
40 UI_DUMP_TEMP = '/data/local/tmp/tm_ui_dump.xml' 42 UI_DUMP_TEMP = '/data/local/tmp/tm_ui_dump.xml'
41 43
42 44
43 def RetryOn(exc_type=(), returns_falsy=False, retries=5): 45 def RetryOn(exc_type=(), returns_falsy=False, retries=5):
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after
152 if output.startswith('ERROR:'): 154 if output.startswith('ERROR:'):
153 # uiautomator may fail if device is not in idle state, e.g. animations 155 # uiautomator may fail if device is not in idle state, e.g. animations
154 # or video playing. Retry if that's the case. 156 # or video playing. Retry if that's the case.
155 raise AdbCommandError(output) 157 raise AdbCommandError(output)
156 158
157 with tempfile.NamedTemporaryFile(suffix='.xml') as f: 159 with tempfile.NamedTemporaryFile(suffix='.xml') as f:
158 f.close() 160 f.close()
159 self.RunCommand('pull', UI_DUMP_TEMP, f.name) 161 self.RunCommand('pull', UI_DUMP_TEMP, f.name)
160 return element_tree.parse(f.name) 162 return element_tree.parse(f.name)
161 163
162 @RetryOn(LookupError) 164 def HasUiElement(self, attr_values):
163 def FindUiElement(self, attr_values): 165 """Check whether a UI element is visible on the screen."""
164 """Find a UI element on screen capture, retrying if not yet visible."""
165 root = self.GetUiScreenDump() 166 root = self.GetUiScreenDump()
166 for node in root.iter(): 167 for node in root.iter():
167 if all(node.get(k) == v for k, v in attr_values): 168 if all(node.get(k) == v for k, v in attr_values):
168 return node 169 return node
169 raise LookupError('Specified UI element not found') 170 return None
171
172 @RetryOn(LookupError)
173 def FindUiElement(self, *args, **kwargs):
174 """Find a UI element on the screen, retrying if not yet visible."""
175 node = self.HasUiElement(*args, **kwargs)
176 if node is None:
177 raise LookupError('Specified UI element not found')
178 return node
170 179
171 def TapUiElement(self, *args, **kwargs): 180 def TapUiElement(self, *args, **kwargs):
181 """Tap on a UI element found on screen."""
172 node = self.FindUiElement(*args, **kwargs) 182 node = self.FindUiElement(*args, **kwargs)
173 m = RE_BOUNDS.match(node.get('bounds')) 183 m = RE_BOUNDS.match(node.get('bounds'))
174 left, top, right, bottom = (int(v) for v in m.groups()) 184 left, top, right, bottom = (int(v) for v in m.groups())
175 x, y = (left + right) / 2, (top + bottom) / 2 185 x, y = (left + right) / 2, (top + bottom) / 2
176 self.RunShellCommand('input', 'tap', str(x), str(y)) 186 self.RunShellCommand('input', 'tap', str(x), str(y))
177 187
178 188
179 def _UserAction(f): 189 def _UserAction(f):
180 """Decorator to add repeat, and action_delay options to user action methods. 190 """Decorator to add repeat, and action_delay options to user action methods.
181 191
182 Note: The values (or their defaults) supplied for these extra options will 192 Note: The values (or their defaults) supplied for these extra options will
183 also be passed down to the decorated method. It's thus advisable to collect 193 also be passed down to the decorated method. It's thus advisable to collect
184 them in a catch-all **kwargs, even if just to discard them immediately. 194 them in a catch-all **kwargs, even if just to discard them immediately.
185 195
186 This is a workaround for https://github.com/PyCQA/pylint/issues/258 in which 196 This is a workaround for https://github.com/PyCQA/pylint/issues/258 in which
187 decorators confuse pylint and trigger spurious 'unexpected-keyword-arg' 197 decorators confuse pylint and trigger spurious 'unexpected-keyword-arg'
188 warnings on method calls that use the extra options. 198 warnings on method calls that use the extra options.
189 """ 199 """
190 @functools.wraps(f) 200 @functools.wraps(f)
191 def Wrapper(self, *args, **kwargs): 201 def Wrapper(self, *args, **kwargs):
192 repeat = kwargs.setdefault('repeat', 1) 202 repeat = kwargs.setdefault('repeat', 1)
193 action_delay = kwargs.setdefault('action_delay', None) 203 action_delay = kwargs.setdefault('action_delay', None)
194 for _ in xrange(repeat): 204 for _ in xrange(repeat):
195 f(self, *args, **kwargs) 205 f(self, *args, **kwargs)
196 self.Idle(action_delay) 206 self.Idle(action_delay)
197 return Wrapper 207 return Wrapper
198 208
199 209
200 class AndroidActions(object): 210 class AndroidActions(object):
211 APP_SWITCHER_CLEAR_ALL = [
212 ('resource-id', 'com.android.systemui:id/button'),
213 ('text', 'CLEAR ALL')]
214 APP_SWITCHER_NO_RECENT = [
215 ('package', 'com.android.systemui'),
216 ('text', 'No recent items')]
217
201 def __init__(self, device, user_action_delay=1): 218 def __init__(self, device, user_action_delay=1):
202 self.device = device 219 self.device = device
203 self.user_action_delay = user_action_delay 220 self.user_action_delay = user_action_delay
204 221
205 def Idle(self, duration=None): 222 def Idle(self, duration=None):
206 if duration is None: 223 if duration is None:
207 duration = self.user_action_delay 224 duration = self.user_action_delay
208 if duration: 225 if duration:
209 time.sleep(duration) 226 time.sleep(duration)
210 227
211 @_UserAction 228 @_UserAction
229 def GoHome(self, **kwargs):
230 del kwargs
231 self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_HOME))
232
233 @_UserAction
212 def GoBack(self, **kwargs): 234 def GoBack(self, **kwargs):
213 del kwargs 235 del kwargs
214 self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_BACK)) 236 self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_BACK))
215 237
216 @_UserAction 238 @_UserAction
239 def GoAppSwitcher(self, **kwargs):
240 del kwargs
241 self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_APP_SWITCH))
242
243 @_UserAction
217 def StartActivity( 244 def StartActivity(
218 self, data_uri, action='android.intent.action.VIEW', **kwargs): 245 self, data_uri, action='android.intent.action.VIEW', **kwargs):
219 del kwargs 246 del kwargs
220 self.device.RunShellCommand('am', 'start', '-a', action, '-d', data_uri) 247 self.device.RunShellCommand('am', 'start', '-a', action, '-d', data_uri)
221 248
222 @_UserAction 249 @_UserAction
223 def TapUiElement(self, attr_values, **kwargs): 250 def TapUiElement(self, attr_values, **kwargs):
224 del kwargs 251 del kwargs
225 self.device.TapUiElement(attr_values) 252 self.device.TapUiElement(attr_values)
226 253
254 def TapHomeScreenShortcut(self, description, **kwargs):
255 self.TapUiElement([
256 ('package', 'com.android.launcher3'),
257 ('class', 'android.widget.TextView'),
258 ('content-desc', description)
259 ], **kwargs)
260
261 def TapAppSwitcherTitle(self, text, **kwargs):
262 self.TapUiElement([
263 ('resource-id', 'com.android.systemui:id/title'),
264 ('text', text)
265 ], **kwargs)
266
267 def TapAppSwitcherClearAll(self, **kwargs):
268 self.TapUiElement(self.APP_SWITCHER_CLEAR_ALL, **kwargs)
269
227 @_UserAction 270 @_UserAction
228 def SwipeUp(self, **kwargs): 271 def SwipeUp(self, **kwargs):
229 del kwargs 272 del kwargs
230 # Hardcoded values for 480x854 screen size; should work reasonably on 273 # Hardcoded values for 480x854 screen size; should work reasonably on
231 # other screen sizes. 274 # other screen sizes.
232 # Command args: swipe <x1> <y1> <x2> <y2> [duration(ms)] 275 # Command args: swipe <x1> <y1> <x2> <y2> [duration(ms)]
233 self.device.RunShellCommand( 276 self.device.RunShellCommand(
234 'input', 'swipe', '240', '568', '240', '284', '400') 277 'input', 'swipe', '240', '568', '240', '284', '400')
235 278
279 @_UserAction
280 def SwipeDown(self, **kwargs):
281 del kwargs
282 # Hardcoded values for 480x854 screen size; should work reasonably on
283 # other screen sizes.
284 # Command args: swipe <x1> <y1> <x2> <y2> [duration(ms)]
285 self.device.RunShellCommand(
286 'input', 'swipe', '240', '284', '240', '568', '400')
287
288 def ClearRecentApps(self):
289 self.GoAppSwitcher()
290 if self.device.HasUiElement(self.APP_SWITCHER_NO_RECENT):
291 self.GoHome()
292 else:
293 self.SwipeDown()
294 self.TapAppSwitcherClearAll()
295
236 296
237 class DevToolsWebSocket(object): 297 class DevToolsWebSocket(object):
238 def __init__(self, url): 298 def __init__(self, url):
239 self._url = url 299 self._url = url
240 self._socket = None 300 self._socket = None
241 self._cmdid = 0 301 self._cmdid = 0
242 302
243 def __enter__(self): 303 def __enter__(self):
244 self.Open() 304 self.Open()
245 return self 305 return self
(...skipping 138 matching lines...) Expand 10 before | Expand all | Expand 10 after
384 Ensures that command line flags and port forwarding are ready, the browser 444 Ensures that command line flags and port forwarding are ready, the browser
385 is not alive before starting, it has a clear profile to begin with, and is 445 is not alive before starting, it has a clear profile to begin with, and is
386 finally closed when done. 446 finally closed when done.
387 447
388 It does not, however, launch the browser itself. This must be done by the 448 It does not, however, launch the browser itself. This must be done by the
389 context managed code. 449 context managed code.
390 450
391 To the extent possible, measurements from browsers launched within 451 To the extent possible, measurements from browsers launched within
392 different sessions are meant to be independent of each other. 452 different sessions are meant to be independent of each other.
393 """ 453 """
394 self.RemoveProfile() 454 # Removing the profile breaks Chrome Shortcuts on the Home Screen.
455 # TODO: Figure out a way to automatically create the shortcuts before
456 # running the story.
457 # self.RemoveProfile()
395 with self.CommandLineFlags(flags): 458 with self.CommandLineFlags(flags):
396 with self.StartupTracing(trace_config): 459 with self.StartupTracing(trace_config):
397 # Ensure browser is closed after setting command line flags and 460 # Ensure browser is closed after setting command line flags and
398 # trace config to ensure they are read on startup. 461 # trace config to ensure they are read on startup.
399 self.ForceStop() 462 self.ForceStop()
400 with self.PortForwarding(): 463 with self.PortForwarding():
401 try: 464 try:
402 yield 465 yield
403 finally: 466 finally:
404 self.ForceStop() 467 self.ForceStop()
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
449 logging.warning('Disabling %s', self.PACKAGE_NAME) 512 logging.warning('Disabling %s', self.PACKAGE_NAME)
450 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME) 513 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME)
451 514
452 515
453 class UserStory(object): 516 class UserStory(object):
454 def __init__(self, browser): 517 def __init__(self, browser):
455 self.device = browser.device 518 self.device = browser.device
456 self.browser = browser 519 self.browser = browser
457 self.actions = AndroidActions(self.device) 520 self.actions = AndroidActions(self.device)
458 521
459 def GetExtraStoryApps(self):
460 """Sequence of AndroidApp's, other than the browser, used in the story."""
461 return ()
462
463 def EnsureExtraStoryAppsClosed(self):
464 running_processes = self.device.ProcessStatus()
465 for app in self.GetExtraStoryApps():
466 if app.PACKAGE_NAME in running_processes:
467 app.ForceStop()
468
469 def Run(self, browser_flags, trace_config, trace_file): 522 def Run(self, browser_flags, trace_config, trace_file):
470 with self.browser.Session(browser_flags, trace_config): 523 with self.browser.Session(browser_flags, trace_config):
471 self.EnsureExtraStoryAppsClosed() 524 self.RunPrepareSteps()
472 try: 525 try:
473 self.RunStorySteps() 526 self.RunStorySteps()
474 self.browser.CollectTrace(trace_file) 527 self.browser.CollectTrace(trace_file)
475 except Exception as exc: 528 except Exception as exc:
476 # Helps to pin point in the logs the moment where the story failed, 529 # Helps to pin point in the logs the moment where the story failed,
477 # before any of the finally blocks get to be executed. 530 # before any of the finally blocks get to be executed.
478 logging.error('Aborting story due to %s.', type(exc).__name__) 531 logging.error('Aborting story due to %s.', type(exc).__name__)
479 raise 532 raise
480 finally: 533 finally:
481 self.EnsureExtraStoryAppsClosed() 534 self.RunCleanupSteps()
535
536 def RunPrepareSteps(self):
537 """Subclasses may override to perform actions before running the story."""
538 pass
482 539
483 def RunStorySteps(self): 540 def RunStorySteps(self):
484 """Subclasses should override this method to implement the story. 541 """Subclasses should override this method to implement the story.
485 542
486 The steps must: 543 The steps must:
487 - at some point cause the browser to be launched, and 544 - at some point cause the browser to be launched, and
488 - make sure the browser remains alive when done (even if backgrounded). 545 - make sure the browser remains alive when done (even if backgrounded).
489 """ 546 """
490 raise NotImplementedError 547 raise NotImplementedError
491 548
549 def RunCleanupSteps(self):
550 """Subclasses may override to perform actions after running the story.
551
552 Note: This will be called even if an exception was raised during the
553 execution of RunStorySteps (but not for errors in RunPrepareSteps).
554 """
555 pass
556
492 557
493 def ReadProcessMetrics(trace_file): 558 def ReadProcessMetrics(trace_file):
494 """Return a list of {"name": process_name, metric: value} dicts.""" 559 """Return a list of {"name": process_name, metric: value} dicts."""
495 with open(trace_file) as f: 560 with open(trace_file) as f:
496 trace = json.load(f) 561 trace = json.load(f)
497 562
498 processes = collections.defaultdict(dict) 563 processes = collections.defaultdict(dict)
499 for event in trace['traceEvents']: 564 for event in trace['traceEvents']:
500 if event['ph'] == 'v': 565 if event['ph'] == 'v':
501 # Extract any metrics you may need from the trace. 566 # Extract any metrics you may need from the trace.
502 value = event['args']['dumps']['allocators'][ 567 value = event['args']['dumps']['allocators'][
503 'java_heap/allocated_objects']['attrs']['size'] 568 'java_heap/allocated_objects']['attrs']['size']
504 assert value['units'] == 'bytes' 569 assert value['units'] == 'bytes'
505 processes[event['pid']]['java_heap'] = int(value['value'], 16) 570 processes[event['pid']]['java_heap'] = int(value['value'], 16)
506 elif event['ph'] == 'M' and event['name'] == 'process_name': 571 elif event['ph'] == 'M' and event['name'] == 'process_name':
507 processes[event['pid']]['name'] = event['args']['name'] 572 processes[event['pid']]['name'] = event['args']['name']
508 573
509 return processes.values() 574 return processes.values()
OLDNEW
« no previous file with comments | « experimental/telemetry_mini/android_go_stories.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698