1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import os
7import subprocess
8import threading
9
10from telemetry.core import platform
11from telemetry.core import util
12from telemetry.internal.backends.chrome import android_browser_finder
13from telemetry.internal.platform import profiler
14from telemetry.internal.util import binary_manager
15
16try:
17  from devil.android import device_errors  # pylint: disable=import-error
18except ImportError:
19  device_errors = None
20
21
22class JavaHeapProfiler(profiler.Profiler):
23  """Android-specific, trigger and fetch java heap dumps."""
24
25  _DEFAULT_DEVICE_DIR = '/data/local/tmp/javaheap'
26  # TODO(bulach): expose this as a command line option somehow.
27  _DEFAULT_INTERVAL = 20
28  def __init__(self, browser_backend, platform_backend, output_path, state):
29    super(JavaHeapProfiler, self).__init__(
30        browser_backend, platform_backend, output_path, state)
31    self._run_count = 1
32
33    self._DumpJavaHeap(False)
34
35    self._timer = threading.Timer(self._DEFAULT_INTERVAL, self._OnTimer)
36    self._timer.start()
37
38  @classmethod
39  def name(cls):
40    return 'java-heap'
41
42  @classmethod
43  def is_supported(cls, browser_type):
44    if browser_type == 'any':
45      return android_browser_finder.CanFindAvailableBrowsers()
46    return browser_type.startswith('android')
47
48  def CollectProfile(self):
49    self._timer.cancel()
50    self._DumpJavaHeap(True)
51    try:
52      self._browser_backend.device.PullFile(
53          self._DEFAULT_DEVICE_DIR, self._output_path)
54    except:
55      logging.exception('New exception caused by DeviceUtils conversion')
56      raise
57    self._browser_backend.device.RunShellCommand(
58        'rm ' + os.path.join(self._DEFAULT_DEVICE_DIR, '*'))
59    output_files = []
60    for f in os.listdir(self._output_path):
61      if os.path.splitext(f)[1] == '.aprof':
62        input_file = os.path.join(self._output_path, f)
63        output_file = input_file.replace('.aprof', '.hprof')
64        hprof_conv = binary_manager.FetchPath(
65            'hprof-conv',
66            platform.GetHostPlatform().GetArchName(),
67            platform.GetHostPlatform().GetOSName())
68        subprocess.call([hprof_conv, input_file, output_file])
69        output_files.append(output_file)
70    return output_files
71
72  def _OnTimer(self):
73    self._DumpJavaHeap(False)
74
75  def _DumpJavaHeap(self, wait_for_completion):
76    if not self._browser_backend.device.FileExists(
77        self._DEFAULT_DEVICE_DIR):
78      self._browser_backend.device.RunShellCommand(
79          'mkdir -p ' + self._DEFAULT_DEVICE_DIR)
80      self._browser_backend.device.RunShellCommand(
81          'chmod 777 ' + self._DEFAULT_DEVICE_DIR)
82
83    device_dump_file = None
84    for pid in self._GetProcessOutputFileMap().iterkeys():
85      device_dump_file = '%s/%s.%s.aprof' % (self._DEFAULT_DEVICE_DIR, pid,
86                                             self._run_count)
87      self._browser_backend.device.RunShellCommand('am dumpheap %s %s' %
88                                                   (pid, device_dump_file))
89    if device_dump_file and wait_for_completion:
90      util.WaitFor(lambda: self._FileSize(device_dump_file) > 0, timeout=2)
91    self._run_count += 1
92
93  def _FileSize(self, file_name):
94    try:
95      return self._browser_backend.device.Stat(file_name).st_size
96    except device_errors.CommandFailedError:
97      return 0
98