OLD | NEW |
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 389 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
400 | 400 |
401 class ChromiumApp(AndroidApp): | 401 class ChromiumApp(AndroidApp): |
402 PACKAGE_NAME = 'org.chromium.chrome' | 402 PACKAGE_NAME = 'org.chromium.chrome' |
403 APK_FILENAME = 'ChromePublic.apk' | 403 APK_FILENAME = 'ChromePublic.apk' |
404 COMMAND_LINE_FILE = '/data/local/tmp/chrome-command-line' | 404 COMMAND_LINE_FILE = '/data/local/tmp/chrome-command-line' |
405 TRACE_CONFIG_FILE = '/data/local/chrome-trace-config.json' | 405 TRACE_CONFIG_FILE = '/data/local/chrome-trace-config.json' |
406 | 406 |
407 def __init__(self, *args, **kwargs): | 407 def __init__(self, *args, **kwargs): |
408 super(ChromiumApp, self).__init__(*args, **kwargs) | 408 super(ChromiumApp, self).__init__(*args, **kwargs) |
409 self._devtools_local_port = None | 409 self._devtools_local_port = None |
| 410 self._browser_flags = None |
| 411 self._trace_config = None |
410 self.startup_time = None | 412 self.startup_time = None |
411 | 413 |
412 def RemoveProfile(self): | 414 def RemoveProfile(self): |
413 # TODO: Path to profile may need to be updated on newer Android versions. | 415 # TODO: Path to profile may need to be updated on newer Android versions. |
414 profile_dir = posixpath.join('/data/data', self.PACKAGE_NAME) | 416 profile_dir = posixpath.join('/data/data', self.PACKAGE_NAME) |
415 filenames = self.device.ListPath(profile_dir) | 417 filenames = self.device.ListPath(profile_dir) |
416 args = ['rm', '-r'] | 418 args = ['rm', '-r'] |
417 args.extend( | 419 args.extend( |
418 posixpath.join(profile_dir, f) | 420 posixpath.join(profile_dir, f) |
419 for f in filenames if f not in ['.', '..', 'lib']) | 421 for f in filenames if f not in ['.', '..', 'lib']) |
420 self.device.RunShellCommand(*args) | 422 self.device.RunShellCommand(*args) |
421 | 423 |
422 @contextlib.contextmanager | 424 @contextlib.contextmanager |
423 def CommandLineFlags(self, flags): | 425 def CommandLineFlags(self): |
424 self.device.WriteText(' '.join(['_'] + flags), self.COMMAND_LINE_FILE) | 426 command_line = ' '.join(['_'] + self._browser_flags) |
| 427 self.device.WriteText(command_line, self.COMMAND_LINE_FILE) |
425 try: | 428 try: |
426 yield | 429 yield |
427 finally: | 430 finally: |
428 self.device.RunShellCommand('rm', '-f', self.COMMAND_LINE_FILE) | 431 self.device.RunShellCommand('rm', '-f', self.COMMAND_LINE_FILE) |
429 | 432 |
| 433 def SetBrowserFlags(self, browser_flags): |
| 434 self._browser_flags = browser_flags |
| 435 |
| 436 def SetTraceConfig(self, trace_config): |
| 437 self._trace_config = trace_config |
| 438 |
430 def SetDevToolsLocalPort(self, port): | 439 def SetDevToolsLocalPort(self, port): |
431 self._devtools_local_port = port | 440 self._devtools_local_port = port |
432 | 441 |
433 def GetDevToolsLocalAddr(self, host='localhost'): | 442 def GetDevToolsLocalAddr(self, host='localhost'): |
434 assert self._devtools_local_port is not None | 443 assert self._devtools_local_port is not None |
435 return '%s:%d' % (host, self._devtools_local_port) | 444 return '%s:%d' % (host, self._devtools_local_port) |
436 | 445 |
437 def GetDevToolsRemoteAddr(self): | 446 def GetDevToolsRemoteAddr(self): |
438 return 'localabstract:chrome_devtools_remote' | 447 return 'localabstract:chrome_devtools_remote' |
439 | 448 |
440 @contextlib.contextmanager | 449 @contextlib.contextmanager |
441 def PortForwarding(self): | 450 def PortForwarding(self): |
442 """Setup port forwarding to connect with DevTools on remote device.""" | 451 """Setup port forwarding to connect with DevTools on remote device.""" |
443 local = self.GetDevToolsLocalAddr('tcp') | 452 local = self.GetDevToolsLocalAddr('tcp') |
444 remote = self.GetDevToolsRemoteAddr() | 453 remote = self.GetDevToolsRemoteAddr() |
445 self.device.RunCommand('forward', '--no-rebind', local, remote) | 454 self.device.RunCommand('forward', '--no-rebind', local, remote) |
446 try: | 455 try: |
447 yield | 456 yield |
448 finally: | 457 finally: |
449 self.device.RunCommand('forward', '--remove', local) | 458 self.device.RunCommand('forward', '--remove', local) |
450 | 459 |
451 @contextlib.contextmanager | 460 @contextlib.contextmanager |
452 def StartupTracing(self, trace_config): | 461 def StartupTracing(self): |
453 self.device.WriteText( | 462 self.device.WriteText( |
454 json.dumps({'trace_config': trace_config}), self.TRACE_CONFIG_FILE) | 463 json.dumps({'trace_config': self._trace_config}), |
| 464 self.TRACE_CONFIG_FILE) |
455 try: | 465 try: |
456 yield | 466 yield |
457 finally: | 467 finally: |
458 self.device.RunShellCommand('rm', '-f', self.TRACE_CONFIG_FILE) | 468 self.device.RunShellCommand('rm', '-f', self.TRACE_CONFIG_FILE) |
459 | 469 |
460 @contextlib.contextmanager | 470 @contextlib.contextmanager |
461 def Session(self, flags, trace_config): | 471 def Session(self): |
462 """A context manager to guard the lifetime of a browser process. | 472 """A context manager to guard the lifetime of a browser process. |
463 | 473 |
464 Ensures that command line flags and port forwarding are ready, the browser | 474 Ensures that command line flags and port forwarding are ready, the browser |
465 is not alive before starting, it has a clear profile to begin with, and is | 475 is not alive before starting, it has a clear profile to begin with, and is |
466 finally closed when done. | 476 finally closed when done. |
467 | 477 |
468 It does not, however, launch the browser itself. This must be done by the | 478 It does not, however, launch the browser itself. This must be done by the |
469 context managed code. | 479 context managed code. |
470 | 480 |
471 To the extent possible, measurements from browsers launched within | 481 To the extent possible, measurements from browsers launched within |
472 different sessions are meant to be independent of each other. | 482 different sessions are meant to be independent of each other. |
473 """ | 483 """ |
474 # Removing the profile breaks Chrome Shortcuts on the Home Screen. | 484 # Removing the profile breaks Chrome Shortcuts on the Home Screen. |
475 # TODO: Figure out a way to automatically create the shortcuts before | 485 # TODO: Figure out a way to automatically create the shortcuts before |
476 # running the story. | 486 # running the story. |
477 # self.RemoveProfile() | 487 # self.RemoveProfile() |
478 with self.CommandLineFlags(flags): | 488 with self.CommandLineFlags(): |
479 with self.StartupTracing(trace_config): | 489 with self.StartupTracing(): |
480 # Ensure browser is closed after setting command line flags and | 490 # Ensure browser is closed after setting command line flags and |
481 # trace config to ensure they are read on startup. | 491 # trace config to ensure they are read on startup. |
482 self.ForceStop() | 492 self.ForceStop() |
483 with self.PortForwarding(): | 493 with self.PortForwarding(): |
484 try: | 494 try: |
485 yield | 495 yield |
486 finally: | 496 finally: |
487 self.ForceStop() | 497 self.ForceStop() |
488 | 498 |
489 def WaitForCurrentPageReady(self): | 499 def WaitForCurrentPageReady(self): |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
550 logging.warning('Disabling %s', self.PACKAGE_NAME) | 560 logging.warning('Disabling %s', self.PACKAGE_NAME) |
551 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME) | 561 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME) |
552 | 562 |
553 | 563 |
554 class UserStory(object): | 564 class UserStory(object): |
555 def __init__(self, browser): | 565 def __init__(self, browser): |
556 self.device = browser.device | 566 self.device = browser.device |
557 self.browser = browser | 567 self.browser = browser |
558 self.actions = AndroidActions(self.device) | 568 self.actions = AndroidActions(self.device) |
559 | 569 |
560 def Run(self, browser_flags, trace_config, trace_file): | 570 def Run(self, trace_file): |
561 with self.browser.Session(browser_flags, trace_config): | 571 with self.browser.Session(): |
562 self.RunPrepareSteps() | 572 self.RunPrepareSteps() |
563 try: | 573 try: |
564 self.RunStorySteps() | 574 self.RunStorySteps() |
565 self.browser.CollectTrace(trace_file) | 575 self.browser.CollectTrace(trace_file) |
566 except Exception as exc: | 576 except Exception as exc: |
567 # Helps to pin point in the logs the moment where the story failed, | 577 # Helps to pin point in the logs the moment where the story failed, |
568 # before any of the finally blocks get to be executed. | 578 # before any of the finally blocks get to be executed. |
569 logging.error('Aborting story due to %s.', type(exc).__name__) | 579 logging.error('Aborting story due to %s.', type(exc).__name__) |
570 raise | 580 raise |
571 finally: | 581 finally: |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
603 if event['ph'] == 'v': | 613 if event['ph'] == 'v': |
604 # Extract any metrics you may need from the trace. | 614 # Extract any metrics you may need from the trace. |
605 value = event['args']['dumps']['allocators'][ | 615 value = event['args']['dumps']['allocators'][ |
606 'java_heap/allocated_objects']['attrs']['size'] | 616 'java_heap/allocated_objects']['attrs']['size'] |
607 assert value['units'] == 'bytes' | 617 assert value['units'] == 'bytes' |
608 processes[event['pid']]['java_heap'] = int(value['value'], 16) | 618 processes[event['pid']]['java_heap'] = int(value['value'], 16) |
609 elif event['ph'] == 'M' and event['name'] == 'process_name': | 619 elif event['ph'] == 'M' and event['name'] == 'process_name': |
610 processes[event['pid']]['name'] = event['args']['name'] | 620 processes[event['pid']]['name'] = event['args']['name'] |
611 | 621 |
612 return processes.values() | 622 return processes.values() |
| 623 |
| 624 |
| 625 def RunStories(browser, stories, repeat, output_dir): |
| 626 for repeat_idx in range(1, repeat + 1): |
| 627 for story_cls in stories: |
| 628 trace_file = os.path.join( |
| 629 output_dir, 'trace_%s_%d.json' % (story_cls.NAME, repeat_idx)) |
| 630 print '[ RUN ]', story_cls.NAME |
| 631 status = '[ OK ]' |
| 632 start = time.time() |
| 633 try: |
| 634 story_cls(browser).Run(trace_file) |
| 635 except Exception: # pylint: disable=broad-except |
| 636 logging.exception('Exception raised while running story') |
| 637 status = '[ FAILED ]' |
| 638 finally: |
| 639 elapsed = '(%.1f secs)' % (time.time() - start) |
| 640 print status, story_cls.NAME, elapsed |
OLD | NEW |