aboutsummaryrefslogtreecommitdiff
path: root/heatmaps
diff options
context:
space:
mode:
Diffstat (limited to 'heatmaps')
-rwxr-xr-xheatmaps/heat_map.py341
-rwxr-xr-xheatmaps/heat_map_test.py281
-rw-r--r--heatmaps/heatmap_generator.py929
-rwxr-xr-xheatmaps/heatmap_generator_test.py572
-rwxr-xr-xheatmaps/perf-to-inst-page.sh2
5 files changed, 1117 insertions, 1008 deletions
diff --git a/heatmaps/heat_map.py b/heatmaps/heat_map.py
index a989ab70..a3c52369 100755
--- a/heatmaps/heat_map.py
+++ b/heatmaps/heat_map.py
@@ -1,12 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Copyright 2015 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wrapper to generate heat maps for chrome."""
-from __future__ import print_function
import argparse
import os
@@ -19,167 +18,189 @@ from heatmaps import heatmap_generator
def IsARepoRoot(directory):
- """Returns True if directory is the root of a repo checkout."""
- return os.path.exists(
- os.path.join(os.path.realpath(os.path.expanduser(directory)), '.repo'))
+ """Returns True if directory is the root of a repo checkout."""
+ return os.path.exists(
+ os.path.join(os.path.realpath(os.path.expanduser(directory)), ".repo")
+ )
class HeatMapProducer(object):
- """Class to produce heat map."""
-
- def __init__(self,
- chromeos_root,
- perf_data,
- hugepage,
- binary,
- title,
- logger=None):
- self.chromeos_root = os.path.realpath(os.path.expanduser(chromeos_root))
- self.perf_data = os.path.realpath(os.path.expanduser(perf_data))
- self.hugepage = hugepage
- self.dir = os.path.dirname(os.path.realpath(__file__))
- self.binary = binary
- self.ce = command_executer.GetCommandExecuter()
- self.temp_dir = ''
- self.temp_perf_inchroot = ''
- self.temp_dir_created = False
- self.perf_report = ''
- self.title = title
- self.logger = logger
-
- def _EnsureFileInChroot(self):
- chroot_prefix = os.path.join(self.chromeos_root, 'chroot')
- if self.perf_data.startswith(chroot_prefix):
- # If the path to perf_data starts with the same chromeos_root, assume
- # it's in the chromeos_root so no need for temporary directory and copy.
- self.temp_dir = self.perf_data.replace('perf.data', '')
- self.temp_perf_inchroot = self.temp_dir.replace(chroot_prefix, '')
-
- else:
- # Otherwise, create a temporary directory and copy perf.data into chroot.
- self.temp_dir = tempfile.mkdtemp(
- prefix=os.path.join(self.chromeos_root, 'src/'))
- temp_perf = os.path.join(self.temp_dir, 'perf.data')
- shutil.copy2(self.perf_data, temp_perf)
- self.temp_perf_inchroot = os.path.join('~/trunk/src',
- os.path.basename(self.temp_dir))
- self.temp_dir_created = True
-
- def _GeneratePerfReport(self):
- cmd = ('cd %s && perf report -D -i perf.data > perf_report.txt' %
- self.temp_perf_inchroot)
- retval = self.ce.ChrootRunCommand(self.chromeos_root, cmd)
- if retval:
- raise RuntimeError('Failed to generate perf report')
- self.perf_report = os.path.join(self.temp_dir, 'perf_report.txt')
-
- def _GetHeatMap(self, top_n_pages):
- generator = heatmap_generator.HeatmapGenerator(
- perf_report=self.perf_report,
- page_size=4096,
- hugepage=self.hugepage,
- title=self.title)
- generator.draw()
- # Analyze top N hottest symbols with the binary, if provided
- if self.binary:
- generator.analyze(self.binary, top_n_pages)
-
- def _RemoveFiles(self):
- files = [
- 'out.txt', 'inst-histo.txt', 'inst-histo-hp.txt', 'inst-histo-sp.txt'
- ]
- for f in files:
- if os.path.exists(f):
- os.remove(f)
-
- def Run(self, top_n_pages):
- try:
- self._EnsureFileInChroot()
- self._GeneratePerfReport()
- self._GetHeatMap(top_n_pages)
- finally:
- self._RemoveFiles()
- msg = ('heat map and time histogram genereated in the current '
- 'directory with name heat_map.png and timeline.png '
- 'accordingly.')
- if self.binary:
- msg += ('\nThe hottest %d pages inside and outside hugepage '
- 'is symbolized and saved to addr2symbol.txt' % top_n_pages)
- if self.logger:
- self.logger.LogOutput(msg)
- else:
- print(msg)
+ """Class to produce heat map."""
+
+ def __init__(
+ self, chromeos_root, perf_data, hugepage, binary, title, logger=None
+ ):
+ self.chromeos_root = os.path.realpath(os.path.expanduser(chromeos_root))
+ self.perf_data = os.path.realpath(os.path.expanduser(perf_data))
+ self.hugepage = hugepage
+ self.dir = os.path.dirname(os.path.realpath(__file__))
+ self.binary = binary
+ self.ce = command_executer.GetCommandExecuter()
+ self.temp_dir = ""
+ self.temp_perf_inchroot = ""
+ self.temp_dir_created = False
+ self.perf_report = ""
+ self.title = title
+ self.logger = logger
+
+ def _EnsureFileInChroot(self):
+ chroot_prefix = os.path.join(self.chromeos_root, "chroot")
+ if self.perf_data.startswith(chroot_prefix):
+ # If the path to perf_data starts with the same chromeos_root, assume
+ # it's in the chromeos_root so no need for temporary directory and copy.
+ self.temp_dir = self.perf_data.replace("perf.data", "")
+ self.temp_perf_inchroot = self.temp_dir.replace(chroot_prefix, "")
+
+ else:
+ # Otherwise, create a temporary directory and copy perf.data into chroot.
+ self.temp_dir = tempfile.mkdtemp(
+ prefix=os.path.join(self.chromeos_root, "src/")
+ )
+ temp_perf = os.path.join(self.temp_dir, "perf.data")
+ shutil.copy2(self.perf_data, temp_perf)
+ self.temp_perf_inchroot = os.path.join(
+ "~/trunk/src", os.path.basename(self.temp_dir)
+ )
+ self.temp_dir_created = True
+
+ def _GeneratePerfReport(self):
+ cmd = (
+ "cd %s && perf report -D -i perf.data > perf_report.txt"
+ % self.temp_perf_inchroot
+ )
+ retval = self.ce.ChrootRunCommand(self.chromeos_root, cmd)
+ if retval:
+ raise RuntimeError("Failed to generate perf report")
+ self.perf_report = os.path.join(self.temp_dir, "perf_report.txt")
+
+ def _GetHeatMap(self, top_n_pages):
+ generator = heatmap_generator.HeatmapGenerator(
+ perf_report=self.perf_report,
+ page_size=4096,
+ hugepage=self.hugepage,
+ title=self.title,
+ )
+ generator.draw()
+ # Analyze top N hottest symbols with the binary, if provided
+ if self.binary:
+ generator.analyze(self.binary, top_n_pages)
+
+ def _RemoveFiles(self):
+ files = [
+ "out.txt",
+ "inst-histo.txt",
+ "inst-histo-hp.txt",
+ "inst-histo-sp.txt",
+ ]
+ for f in files:
+ if os.path.exists(f):
+ os.remove(f)
+
+ def Run(self, top_n_pages):
+ try:
+ self._EnsureFileInChroot()
+ self._GeneratePerfReport()
+ self._GetHeatMap(top_n_pages)
+ finally:
+ self._RemoveFiles()
+ msg = (
+ "heat map and time histogram genereated in the current "
+ "directory with name heat_map.png and timeline.png "
+ "accordingly."
+ )
+ if self.binary:
+ msg += (
+ "\nThe hottest %d pages inside and outside hugepage "
+ "is symbolized and saved to addr2symbol.txt" % top_n_pages
+ )
+ if self.logger:
+ self.logger.LogOutput(msg)
+ else:
+ print(msg)
def main(argv):
- """Parse the options.
-
- Args:
- argv: The options with which this script was invoked.
-
- Returns:
- 0 unless an exception is raised.
- """
- parser = argparse.ArgumentParser()
-
- parser.add_argument(
- '--chromeos_root',
- dest='chromeos_root',
- required=True,
- help='ChromeOS root to use for generate heatmaps.')
- parser.add_argument(
- '--perf_data',
- dest='perf_data',
- required=True,
- help='The raw perf data. Must be collected with -e instructions while '
- 'disabling ASLR.')
- parser.add_argument(
- '--binary',
- dest='binary',
- help='The path to the Chrome binary. Only useful if want to print '
- 'symbols on hottest pages',
- default=None)
- parser.add_argument(
- '--top_n',
- dest='top_n',
- type=int,
- default=10,
- help='Print out top N hottest pages within/outside huge page range. '
- 'Must be used with --hugepage and --binary. (Default: %(default)s)')
- parser.add_argument(
- '--title', dest='title', help='Title of the heatmap', default='')
- parser.add_argument(
- '--hugepage',
- dest='hugepage',
- help='A range of addresses (start,end) where huge page starts and ends'
- ' in text section, separated by a comma.'
- ' Used to differentiate regions in heatmap.'
- ' Example: --hugepage=0,4096'
- ' If not specified, no effect on the heatmap.',
- default=None)
-
- options = parser.parse_args(argv)
-
- if not IsARepoRoot(options.chromeos_root):
- parser.error('%s does not contain .repo dir.' % options.chromeos_root)
-
- if not os.path.isfile(options.perf_data):
- parser.error('Cannot find perf_data: %s.' % options.perf_data)
-
- hugepage_range = None
- if options.hugepage:
- hugepage_range = options.hugepage.split(',')
- if len(hugepage_range) != 2 or \
- int(hugepage_range[0]) > int(hugepage_range[1]):
- parser.error('Wrong format of hugepage range: %s' % options.hugepage)
- hugepage_range = [int(x) for x in hugepage_range]
-
- heatmap_producer = HeatMapProducer(options.chromeos_root, options.perf_data,
- hugepage_range, options.binary,
- options.title)
-
- heatmap_producer.Run(options.top_n)
-
-
-if __name__ == '__main__':
- sys.exit(main(sys.argv[1:]))
+ """Parse the options.
+
+ Args:
+ argv: The options with which this script was invoked.
+
+ Returns:
+ 0 unless an exception is raised.
+ """
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ "--chromeos_root",
+ dest="chromeos_root",
+ required=True,
+ help="ChromeOS root to use for generate heatmaps.",
+ )
+ parser.add_argument(
+ "--perf_data",
+ dest="perf_data",
+ required=True,
+ help="The raw perf data. Must be collected with -e instructions while "
+ "disabling ASLR.",
+ )
+ parser.add_argument(
+ "--binary",
+ dest="binary",
+ help="The path to the Chrome binary. Only useful if want to print "
+ "symbols on hottest pages",
+ default=None,
+ )
+ parser.add_argument(
+ "--top_n",
+ dest="top_n",
+ type=int,
+ default=10,
+ help="Print out top N hottest pages within/outside huge page range. "
+ "Must be used with --hugepage and --binary. (Default: %(default)s)",
+ )
+ parser.add_argument(
+ "--title", dest="title", help="Title of the heatmap", default=""
+ )
+ parser.add_argument(
+ "--hugepage",
+ dest="hugepage",
+ help="A range of addresses (start,end) where huge page starts and ends"
+ " in text section, separated by a comma."
+ " Used to differentiate regions in heatmap."
+ " Example: --hugepage=0,4096"
+ " If not specified, no effect on the heatmap.",
+ default=None,
+ )
+
+ options = parser.parse_args(argv)
+
+ if not IsARepoRoot(options.chromeos_root):
+ parser.error("%s does not contain .repo dir." % options.chromeos_root)
+
+ if not os.path.isfile(options.perf_data):
+ parser.error("Cannot find perf_data: %s." % options.perf_data)
+
+ hugepage_range = None
+ if options.hugepage:
+ hugepage_range = options.hugepage.split(",")
+ if len(hugepage_range) != 2 or int(hugepage_range[0]) > int(
+ hugepage_range[1]
+ ):
+ parser.error(
+ "Wrong format of hugepage range: %s" % options.hugepage
+ )
+ hugepage_range = [int(x) for x in hugepage_range]
+
+ heatmap_producer = HeatMapProducer(
+ options.chromeos_root,
+ options.perf_data,
+ hugepage_range,
+ options.binary,
+ options.title,
+ )
+
+ heatmap_producer.Run(options.top_n)
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/heatmaps/heat_map_test.py b/heatmaps/heat_map_test.py
index ad62cd91..96300bb4 100755
--- a/heatmaps/heat_map_test.py
+++ b/heatmaps/heat_map_test.py
@@ -1,158 +1,179 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
-# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for heat_map.py."""
-from __future__ import print_function
-
-import unittest.mock as mock
-import unittest
import os
+import unittest
+import unittest.mock as mock
from cros_utils import command_executer
-
from heatmaps import heat_map
from heatmaps import heatmap_generator
-def make_heatmap(chromeos_root='/path/to/fake/chromeos_root/',
- perf_data='/any_path/perf.data'):
- return heat_map.HeatMapProducer(chromeos_root, perf_data, None, None, '')
+def make_heatmap(
+ chromeos_root="/path/to/fake/chromeos_root/",
+ perf_data="/any_path/perf.data",
+):
+ return heat_map.HeatMapProducer(chromeos_root, perf_data, None, None, "")
def fake_mkdtemp(prefix):
- """Mock tempfile.mkdtemp() by just create a pathname."""
- return prefix + 'random_dir'
+ """Mock tempfile.mkdtemp() by just create a pathname."""
+ return prefix + "random_dir"
def fake_parser_error(_, msg):
- """Redirect parser.error() to exception."""
- raise Exception(msg)
+ """Redirect parser.error() to exception."""
+ raise Exception(msg)
def fake_generate_perf_report_exception(_):
- raise Exception
+ raise Exception
class HeatmapTest(unittest.TestCase):
- """All of our tests for heat_map."""
-
- # pylint: disable=protected-access
- @mock.patch('shutil.copy2')
- @mock.patch('tempfile.mkdtemp')
- def test_EnsureFileInChrootAlreadyInside(self, mock_mkdtemp, mock_copy):
- perf_data_inchroot = (
- '/path/to/fake/chromeos_root/chroot/inchroot_path/perf.data')
- heatmap = make_heatmap(perf_data=perf_data_inchroot)
- heatmap._EnsureFileInChroot()
- self.assertFalse(heatmap.temp_dir_created)
- self.assertEqual(heatmap.temp_dir,
- '/path/to/fake/chromeos_root/chroot/inchroot_path/')
- self.assertEqual(heatmap.temp_perf_inchroot, '/inchroot_path/')
- mock_mkdtemp.assert_not_called()
- mock_copy.assert_not_called()
-
- @mock.patch('shutil.copy2')
- @mock.patch('tempfile.mkdtemp', fake_mkdtemp)
- def test_EnsureFileInChrootOutsideNeedCopy(self, mock_copy):
- heatmap = make_heatmap()
- heatmap._EnsureFileInChroot()
- self.assertTrue(heatmap.temp_dir_created)
- self.assertEqual(mock_copy.call_count, 1)
- self.assertEqual(heatmap.temp_dir,
- '/path/to/fake/chromeos_root/src/random_dir')
- self.assertEqual(heatmap.temp_perf_inchroot, '~/trunk/src/random_dir')
-
- @mock.patch.object(command_executer.CommandExecuter, 'ChrootRunCommand')
- def test_GeneratePerfReport(self, mock_ChrootRunCommand):
- heatmap = make_heatmap()
- heatmap.temp_dir = '/fake/chroot/inchroot_path/'
- heatmap.temp_perf_inchroot = '/inchroot_path/'
- mock_ChrootRunCommand.return_value = 0
- heatmap._GeneratePerfReport()
- cmd = ('cd %s && perf report -D -i perf.data > perf_report.txt' %
- heatmap.temp_perf_inchroot)
- mock_ChrootRunCommand.assert_called_with(heatmap.chromeos_root, cmd)
- self.assertEqual(mock_ChrootRunCommand.call_count, 1)
- self.assertEqual(heatmap.perf_report,
- '/fake/chroot/inchroot_path/perf_report.txt')
-
- @mock.patch.object(heatmap_generator, 'HeatmapGenerator')
- def test_GetHeatMap(self, mock_heatmap_generator):
- heatmap = make_heatmap()
- heatmap._GetHeatMap(10)
- self.assertTrue(mock_heatmap_generator.called)
-
- @mock.patch.object(heat_map.HeatMapProducer, '_EnsureFileInChroot')
- @mock.patch.object(heat_map.HeatMapProducer, '_GeneratePerfReport')
- @mock.patch.object(heat_map.HeatMapProducer, '_GetHeatMap')
- @mock.patch.object(heat_map.HeatMapProducer, '_RemoveFiles')
- def test_Run(self, mock_remove_files, mock_get_heatmap,
- mock_generate_perf_report, mock_ensure_file_in_chroot):
- heatmap = make_heatmap()
- heatmap.Run(10)
- mock_ensure_file_in_chroot.assert_called_once_with()
- mock_generate_perf_report.assert_called_once_with()
- mock_get_heatmap.assert_called_once_with(10)
- mock_remove_files.assert_called_once_with()
-
- @mock.patch.object(heat_map.HeatMapProducer, '_EnsureFileInChroot')
- @mock.patch.object(
- heat_map.HeatMapProducer,
- '_GeneratePerfReport',
- new=fake_generate_perf_report_exception)
- @mock.patch.object(heat_map.HeatMapProducer, '_GetHeatMap')
- @mock.patch.object(heat_map.HeatMapProducer, '_RemoveFiles')
- @mock.patch('builtins.print')
- def test_Run_with_exception(self, mock_print, mock_remove_files,
- mock_get_heatmap, mock_ensure_file_in_chroot):
- heatmap = make_heatmap()
- with self.assertRaises(Exception):
- heatmap.Run(10)
- mock_ensure_file_in_chroot.assert_called_once_with()
- mock_get_heatmap.assert_not_called()
- mock_remove_files.assert_called_once_with()
- mock_print.assert_not_called()
-
- @mock.patch('argparse.ArgumentParser.error', fake_parser_error)
- @mock.patch.object(os.path, 'isfile')
- @mock.patch.object(heat_map, 'IsARepoRoot')
- def test_main_arg_format(self, mock_IsARepoRoot, mock_isfile):
- """Test wrong arg format are detected."""
- args = ['--chromeos_root=/fake/chroot/', '--perf_data=/path/to/perf.data']
-
- # Test --chromeos_root format
- mock_IsARepoRoot.return_value = False
- with self.assertRaises(Exception) as msg:
- heat_map.main(args)
- self.assertIn('does not contain .repo dir.', str(msg.exception))
-
- # Test --perf_data format
- mock_IsARepoRoot.return_value = True
- mock_isfile.return_value = False
- with self.assertRaises(Exception) as msg:
- heat_map.main(args)
- self.assertIn('Cannot find perf_data', str(msg.exception))
-
- # Test --hugepage format
- mock_isfile.return_value = True
- args.append('--hugepage=0')
- with self.assertRaises(Exception) as msg:
- heat_map.main(args)
- self.assertIn('Wrong format of hugepage range', str(msg.exception))
-
- # Test --hugepage parse
- args[-1] = '--hugepage=0,4096'
- heat_map.HeatMapProducer = mock.MagicMock()
- heat_map.main(args)
- heat_map.HeatMapProducer.assert_called_with(
- '/fake/chroot/', '/path/to/perf.data', [0, 4096], None, '')
-
-
-if __name__ == '__main__':
- unittest.main()
+ """All of our tests for heat_map."""
+
+ # pylint: disable=protected-access
+ @mock.patch("shutil.copy2")
+ @mock.patch("tempfile.mkdtemp")
+ def test_EnsureFileInChrootAlreadyInside(self, mock_mkdtemp, mock_copy):
+ perf_data_inchroot = (
+ "/path/to/fake/chromeos_root/chroot/inchroot_path/perf.data"
+ )
+ heatmap = make_heatmap(perf_data=perf_data_inchroot)
+ heatmap._EnsureFileInChroot()
+ self.assertFalse(heatmap.temp_dir_created)
+ self.assertEqual(
+ heatmap.temp_dir,
+ "/path/to/fake/chromeos_root/chroot/inchroot_path/",
+ )
+ self.assertEqual(heatmap.temp_perf_inchroot, "/inchroot_path/")
+ mock_mkdtemp.assert_not_called()
+ mock_copy.assert_not_called()
+
+ @mock.patch("shutil.copy2")
+ @mock.patch("tempfile.mkdtemp", fake_mkdtemp)
+ def test_EnsureFileInChrootOutsideNeedCopy(self, mock_copy):
+ heatmap = make_heatmap()
+ heatmap._EnsureFileInChroot()
+ self.assertTrue(heatmap.temp_dir_created)
+ self.assertEqual(mock_copy.call_count, 1)
+ self.assertEqual(
+ heatmap.temp_dir, "/path/to/fake/chromeos_root/src/random_dir"
+ )
+ self.assertEqual(heatmap.temp_perf_inchroot, "~/trunk/src/random_dir")
+
+ @mock.patch.object(command_executer.CommandExecuter, "ChrootRunCommand")
+ def test_GeneratePerfReport(self, mock_ChrootRunCommand):
+ heatmap = make_heatmap()
+ heatmap.temp_dir = "/fake/chroot/inchroot_path/"
+ heatmap.temp_perf_inchroot = "/inchroot_path/"
+ mock_ChrootRunCommand.return_value = 0
+ heatmap._GeneratePerfReport()
+ cmd = (
+ "cd %s && perf report -D -i perf.data > perf_report.txt"
+ % heatmap.temp_perf_inchroot
+ )
+ mock_ChrootRunCommand.assert_called_with(heatmap.chromeos_root, cmd)
+ self.assertEqual(mock_ChrootRunCommand.call_count, 1)
+ self.assertEqual(
+ heatmap.perf_report, "/fake/chroot/inchroot_path/perf_report.txt"
+ )
+
+ @mock.patch.object(heatmap_generator, "HeatmapGenerator")
+ def test_GetHeatMap(self, mock_heatmap_generator):
+ heatmap = make_heatmap()
+ heatmap._GetHeatMap(10)
+ self.assertTrue(mock_heatmap_generator.called)
+
+ @mock.patch.object(heat_map.HeatMapProducer, "_EnsureFileInChroot")
+ @mock.patch.object(heat_map.HeatMapProducer, "_GeneratePerfReport")
+ @mock.patch.object(heat_map.HeatMapProducer, "_GetHeatMap")
+ @mock.patch.object(heat_map.HeatMapProducer, "_RemoveFiles")
+ def test_Run(
+ self,
+ mock_remove_files,
+ mock_get_heatmap,
+ mock_generate_perf_report,
+ mock_ensure_file_in_chroot,
+ ):
+ heatmap = make_heatmap()
+ heatmap.Run(10)
+ mock_ensure_file_in_chroot.assert_called_once_with()
+ mock_generate_perf_report.assert_called_once_with()
+ mock_get_heatmap.assert_called_once_with(10)
+ mock_remove_files.assert_called_once_with()
+
+ @mock.patch.object(heat_map.HeatMapProducer, "_EnsureFileInChroot")
+ @mock.patch.object(
+ heat_map.HeatMapProducer,
+ "_GeneratePerfReport",
+ new=fake_generate_perf_report_exception,
+ )
+ @mock.patch.object(heat_map.HeatMapProducer, "_GetHeatMap")
+ @mock.patch.object(heat_map.HeatMapProducer, "_RemoveFiles")
+ @mock.patch("builtins.print")
+ def test_Run_with_exception(
+ self,
+ mock_print,
+ mock_remove_files,
+ mock_get_heatmap,
+ mock_ensure_file_in_chroot,
+ ):
+ heatmap = make_heatmap()
+ with self.assertRaises(Exception):
+ heatmap.Run(10)
+ mock_ensure_file_in_chroot.assert_called_once_with()
+ mock_get_heatmap.assert_not_called()
+ mock_remove_files.assert_called_once_with()
+ mock_print.assert_not_called()
+
+ @mock.patch("argparse.ArgumentParser.error", fake_parser_error)
+ @mock.patch.object(os.path, "isfile")
+ @mock.patch.object(heat_map, "IsARepoRoot")
+ def test_main_arg_format(self, mock_IsARepoRoot, mock_isfile):
+ """Test wrong arg format are detected."""
+ args = [
+ "--chromeos_root=/fake/chroot/",
+ "--perf_data=/path/to/perf.data",
+ ]
+
+ # Test --chromeos_root format
+ mock_IsARepoRoot.return_value = False
+ with self.assertRaises(Exception) as msg:
+ heat_map.main(args)
+ self.assertIn("does not contain .repo dir.", str(msg.exception))
+
+ # Test --perf_data format
+ mock_IsARepoRoot.return_value = True
+ mock_isfile.return_value = False
+ with self.assertRaises(Exception) as msg:
+ heat_map.main(args)
+ self.assertIn("Cannot find perf_data", str(msg.exception))
+
+ # Test --hugepage format
+ mock_isfile.return_value = True
+ args.append("--hugepage=0")
+ with self.assertRaises(Exception) as msg:
+ heat_map.main(args)
+ self.assertIn("Wrong format of hugepage range", str(msg.exception))
+
+ # Test --hugepage parse
+ args[-1] = "--hugepage=0,4096"
+ heat_map.HeatMapProducer = mock.MagicMock()
+ heat_map.main(args)
+ heat_map.HeatMapProducer.assert_called_with(
+ "/fake/chroot/", "/path/to/perf.data", [0, 4096], None, ""
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/heatmaps/heatmap_generator.py b/heatmaps/heatmap_generator.py
index 0dd6ad28..703c37d4 100644
--- a/heatmaps/heatmap_generator.py
+++ b/heatmaps/heatmap_generator.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
@@ -13,7 +13,6 @@ performed by another script perf-to-inst-page.sh). It can also analyze
the symbol names in hot pages.
"""
-from __future__ import division, print_function
import bisect
import collections
@@ -23,445 +22,503 @@ import subprocess
from cros_utils import command_executer
-HugepageRange = collections.namedtuple('HugepageRange', ['start', 'end'])
+HugepageRange = collections.namedtuple("HugepageRange", ["start", "end"])
-class MMap(object):
- """Class to store mmap information in perf report.
-
- We assume ASLR is disabled, so MMap for all Chrome is assumed to be
- the same. This class deals with the case hugepage creates several
- mmaps for Chrome but should be merged together. In these case, we
- assume the first MMAP is not affected by the bug and use the MMAP.
- """
-
- def __init__(self, addr, size, offset):
- self.start_address = addr
- self.size = size
- self.offset = offset
-
- def __str__(self):
- return '(%x, %x, %x)' % (self.start_address, self.size, self.offset)
-
- def merge(self, mmap):
- # This function should not be needed, since we should only have
- # one MMAP on Chrome of each process. This function only deals with
- # images that is affected by http://crbug.com/931465.
- # This function is only checking a few conditions to make sure
- # the bug is within our expectation.
-
- if self.start_address == mmap.start_address:
- assert self.size >= mmap.size, \
- 'Original MMAP size(%x) is smaller than the forked process(%x).' % (
- self.size, mmap.size)
- # The case that the MMAP is forked from the previous process
- # No need to do anything, OR
- # The case where hugepage causes a small Chrome mmap.
- # In this case, we use the prior MMAP for the whole Chrome
- return
-
- assert self.start_address < mmap.start_address, \
- 'Original MMAP starting address(%x) is larger than the forked' \
- 'process(%x).' % (self.start_address, mmap.start_address)
-
- assert self.start_address + self.size >= mmap.start_address + mmap.size, \
- 'MMAP of the forked process exceeds the end of original MMAP.'
+class MMap(object):
+ """Class to store mmap information in perf report.
+
+ We assume ASLR is disabled, so MMap for all Chrome is assumed to be
+ the same. This class deals with the case hugepage creates several
+ mmaps for Chrome but should be merged together. In these case, we
+ assume the first MMAP is not affected by the bug and use the MMAP.
+ """
+
+ def __init__(self, addr, size, offset):
+ self.start_address = addr
+ self.size = size
+ self.offset = offset
+
+ def __str__(self):
+ return "(%x, %x, %x)" % (self.start_address, self.size, self.offset)
+
+ def merge(self, mmap):
+ # This function should not be needed, since we should only have
+ # one MMAP on Chrome of each process. This function only deals with
+ # images that is affected by http://crbug.com/931465.
+
+ # This function is only checking a few conditions to make sure
+ # the bug is within our expectation.
+
+ if self.start_address == mmap.start_address:
+ assert (
+ self.size >= mmap.size
+ ), "Original MMAP size(%x) is smaller than the forked process(%x)." % (
+ self.size,
+ mmap.size,
+ )
+ # The case that the MMAP is forked from the previous process
+ # No need to do anything, OR
+ # The case where hugepage causes a small Chrome mmap.
+ # In this case, we use the prior MMAP for the whole Chrome
+ return
+
+ assert self.start_address < mmap.start_address, (
+ "Original MMAP starting address(%x) is larger than the forked"
+ "process(%x)." % (self.start_address, mmap.start_address)
+ )
+
+ assert (
+ self.start_address + self.size >= mmap.start_address + mmap.size
+ ), "MMAP of the forked process exceeds the end of original MMAP."
class HeatmapGenerator(object):
- """Class to generate heat map with a perf report, containing mmaps and
-
- samples. This class contains two interfaces with other modules:
- draw() and analyze().
-
- draw() draws a heatmap with the sample information given in the perf report
- analyze() prints out the symbol names in hottest pages with the given
- chrome binary
- """
-
- def __init__(self,
- perf_report,
- page_size,
- hugepage,
- title,
- log_level='verbose'):
- self.perf_report = perf_report
- # Pick 1G as a relatively large number. All addresses less than it will
- # be recorded. The actual heatmap will show up to a boundary of the
- # largest address in text segment.
- self.max_addr = 1024 * 1024 * 1024
- self.ce = command_executer.GetCommandExecuter(log_level=log_level)
- self.dir = os.path.dirname(os.path.realpath(__file__))
- with open(perf_report, 'r', encoding='utf-8') as f:
- self.perf_report_contents = f.readlines()
- # Write histogram results to a text file, in order to use gnu plot to draw
- self.hist_temp_output = open('out.txt', 'w', encoding='utf-8')
- self.processes = {}
- self.deleted_processes = {}
- self.count = 0
- if hugepage:
- self.hugepage = HugepageRange(start=hugepage[0], end=hugepage[1])
- else:
- self.hugepage = None
- self.title = title
- self.symbol_addresses = []
- self.symbol_names = []
- self.page_size = page_size
-
- def _parse_perf_sample(self, line):
- # In a perf report, generated with -D, a PERF_RECORD_SAMPLE command should
- # look like this: TODO: some arguments are unknown
- #
- # cpuid cycle unknown [unknown]: PERF_RECORD_SAMPLE(IP, 0x2): pid/tid:
- # 0xaddr period: period addr: addr
- # ... thread: threadname:tid
- # ...... dso: process
- #
- # This is an example:
- # 1 136712833349 0x6a558 [0x30]: PERF_RECORD_SAMPLE(IP, 0x2): 5227/5227:
- # 0x55555683b810 period: 372151 addr: 0
- # ... thread: chrome:5227
- # ...... dso: /opt/google/chrome/chrome
- #
- # For this function, the 7th argument (args[6]) after spltting with spaces
- # is pid/tid. We use the combination of the two as the pid.
- # Also, we add an assertion here to check the tid in the 7th argument(
- # args[6]) and the 15th argument(arg[14]) are the same
- #
- # The function returns the ((pid,tid), address) pair if the sampling
- # is on Chrome. Otherwise, return (None, None) pair.
-
- if 'thread: chrome' not in line or \
- 'dso: /opt/google/chrome/chrome' not in line:
- return None, None
- args = line.split(' ')
- pid_raw = args[6].split('/')
- assert pid_raw[1][:-1] == args[14].split(':')[1][:-1], \
- 'TID in %s of sample is not the same: %s/%s' % (
- line[:-1], pid_raw[1][:-1], args[14].split(':')[1][:-1])
- key = (int(pid_raw[0]), int(pid_raw[1][:-1]))
- address = int(args[7], base=16)
- return key, address
-
- def _parse_perf_record(self, line):
- # In a perf report, generated with -D, a PERF_RECORD_MMAP2 command should
- # look like this: TODO: some arguments are unknown
- #
- # cpuid cycle unknown [unknown]: PERF_RECORD_MMAP2 pid/tid:
- # [0xaddr(0xlength) @ pageoffset maj:min ino ino_generation]:
- # permission process
- #
- # This is an example.
- # 2 136690556823 0xa6898 [0x80]: PERF_RECORD_MMAP2 5227/5227:
- # [0x555556496000(0x8d1b000) @ 0xf42000 b3:03 92844 1892514370]:
- # r-xp /opt/google/chrome/chrome
- #
- # For this function, the 6th argument (args[5]) after spltting with spaces
- # is pid/tid. We use the combination of the two as the pid.
- # The 7th argument (args[6]) is the [0xaddr(0xlength). We can peel the
- # string to get the address and size of the mmap.
- # The 9th argument (args[8]) is the page offset.
- # The function returns the ((pid,tid), mmap) pair if the mmap is for Chrome
- # is on Chrome. Otherwise, return (None, None) pair.
-
- if 'chrome/chrome' not in line:
- return None, None
- args = line.split(' ')
- pid_raw = args[5].split('/')
- assert pid_raw[0] == pid_raw[1][:-1], \
- 'PID in %s of mmap is not the same: %s/%s' % (
- line[:-1], pid_raw[0], pid_raw[1])
- pid = (int(pid_raw[0]), int(pid_raw[1][:-1]))
- address_raw = args[6].split('(')
- start_address = int(address_raw[0][1:], base=16)
- size = int(address_raw[1][:-1], base=16)
- offset = int(args[8], base=16)
- # Return an mmap object instead of only starting address,
- # in case there are many mmaps for the sample PID
- return pid, MMap(start_address, size, offset)
-
- def _parse_pair_event(self, arg):
- # This function is called by the _parse_* functions that has a pattern of
- # pids like: (pid:tid):(pid:tid), i.e.
- # PERF_RECORD_FORK and PERF_RECORD_COMM
- _, remain = arg.split('(', 1)
- pid1, remain = remain.split(':', 1)
- pid2, remain = remain.split(')', 1)
- _, remain = remain.split('(', 1)
- pid3, remain = remain.split(':', 1)
- pid4, remain = remain.split(')', 1)
- return (int(pid1), int(pid2)), (int(pid3), int(pid4))
-
- def _process_perf_record(self, line):
- # This function calls _parse_perf_record() to get information from
- # PERF_RECORD_MMAP2. It records the mmap object for each pid (a pair of
- # pid,tid), into a dictionary.
- pid, mmap = self._parse_perf_record(line)
- if pid is None:
- # PID = None meaning the mmap is not for chrome
- return
- if pid in self.processes:
- # This should never happen for a correct profiling result, as we
- # should only have one MMAP for Chrome for each process.
- # If it happens, see http://crbug.com/931465
- self.processes[pid].merge(mmap)
- else:
- self.processes[pid] = mmap
-
- def _process_perf_fork(self, line):
- # In a perf report, generated with -D, a PERF_RECORD_FORK command should
- # look like this:
- #
- # cpuid cycle unknown [unknown]:
- # PERF_RECORD_FORK(pid_to:tid_to):(pid_from:tid_from)
- #
- # This is an example.
- # 0 0 0x22a8 [0x38]: PERF_RECORD_FORK(1:1):(0:0)
- #
- # In this function, we need to peel the information of pid:tid pairs
- # So we get the last argument and send it to function _parse_pair_event()
- # for analysis.
- # We use (pid, tid) as the pid.
- args = line.split(' ')
- pid_to, pid_from = self._parse_pair_event(args[-1])
- if pid_from in self.processes:
- assert pid_to not in self.processes
- self.processes[pid_to] = MMap(self.processes[pid_from].start_address,
- self.processes[pid_from].size,
- self.processes[pid_from].offset)
-
- def _process_perf_exit(self, line):
- # In a perf report, generated with -D, a PERF_RECORD_EXIT command should
- # look like this:
- #
- # cpuid cycle unknown [unknown]:
- # PERF_RECORD_EXIT(pid1:tid1):(pid2:tid2)
- #
- # This is an example.
- # 1 136082505621 0x30810 [0x38]: PERF_RECORD_EXIT(3851:3851):(3851:3851)
- #
- # In this function, we need to peel the information of pid:tid pairs
- # So we get the last argument and send it to function _parse_pair_event()
- # for analysis.
- # We use (pid, tid) as the pid.
- args = line.split(' ')
- pid_to, pid_from = self._parse_pair_event(args[-1])
- assert pid_to == pid_from, '(%d, %d) (%d, %d)' % (pid_to[0], pid_to[1],
- pid_from[0], pid_from[1])
- if pid_to in self.processes:
- # Don't delete the process yet
- self.deleted_processes[pid_from] = self.processes[pid_from]
-
- def _process_perf_sample(self, line):
- # This function calls _parse_perf_sample() to get information from
- # the perf report.
- # It needs to check the starting address of allocated mmap from
- # the dictionary (self.processes) to calculate the offset within
- # the text section of the sampling.
- # The offset is calculated into pages (4KB or 2MB) and writes into
- # out.txt together with the total counts, which will be used to
- # calculate histogram.
- pid, addr = self._parse_perf_sample(line)
- if pid is None:
- return
-
- assert pid in self.processes and pid not in self.deleted_processes, \
- 'PID %d not found mmap and not forked from another process'
-
- start_address = self.processes[pid].start_address
- address = addr - start_address
- assert address >= 0 and \
- 'addresses accessed in PERF_RECORD_SAMPLE should be larger than' \
- ' the starting address of Chrome'
- if address < self.max_addr:
- self.count += 1
- line = '%d/%d: %d %d' % (pid[0], pid[1], self.count,
- address // self.page_size * self.page_size)
- if self.hugepage:
- if self.hugepage.start <= address < self.hugepage.end:
- line += ' hugepage'
+ """Class to generate heat map with a perf report, containing mmaps and
+
+ samples. This class contains two interfaces with other modules:
+ draw() and analyze().
+
+ draw() draws a heatmap with the sample information given in the perf report
+ analyze() prints out the symbol names in hottest pages with the given
+ chrome binary
+ """
+
+ def __init__(
+ self, perf_report, page_size, hugepage, title, log_level="verbose"
+ ):
+ self.perf_report = perf_report
+ # Pick 1G as a relatively large number. All addresses less than it will
+ # be recorded. The actual heatmap will show up to a boundary of the
+ # largest address in text segment.
+ self.max_addr = 1024 * 1024 * 1024
+ self.ce = command_executer.GetCommandExecuter(log_level=log_level)
+ self.dir = os.path.dirname(os.path.realpath(__file__))
+ with open(perf_report, "r", encoding="utf-8") as f:
+ self.perf_report_contents = f.readlines()
+ # Write histogram results to a text file, in order to use gnu plot to draw
+ self.hist_temp_output = open("out.txt", "w", encoding="utf-8")
+ self.processes = {}
+ self.deleted_processes = {}
+ self.count = 0
+ if hugepage:
+ self.hugepage = HugepageRange(start=hugepage[0], end=hugepage[1])
+ else:
+ self.hugepage = None
+ self.title = title
+ self.symbol_addresses = []
+ self.symbol_names = []
+ self.page_size = page_size
+
+ def _parse_perf_sample(self, line):
+ # In a perf report, generated with -D, a PERF_RECORD_SAMPLE command should
+ # look like this: TODO: some arguments are unknown
+ #
+ # cpuid cycle unknown [unknown]: PERF_RECORD_SAMPLE(IP, 0x2): pid/tid:
+ # 0xaddr period: period addr: addr
+ # ... thread: threadname:tid
+ # ...... dso: process
+ #
+ # This is an example:
+ # 1 136712833349 0x6a558 [0x30]: PERF_RECORD_SAMPLE(IP, 0x2): 5227/5227:
+ # 0x55555683b810 period: 372151 addr: 0
+ # ... thread: chrome:5227
+ # ...... dso: /opt/google/chrome/chrome
+ #
+ # For this function, the 7th argument (args[6]) after spltting with spaces
+ # is pid/tid. We use the combination of the two as the pid.
+ # Also, we add an assertion here to check the tid in the 7th argument(
+ # args[6]) and the 15th argument(arg[14]) are the same
+ #
+ # The function returns the ((pid,tid), address) pair if the sampling
+ # is on Chrome. Otherwise, return (None, None) pair.
+
+ if (
+ "thread: chrome" not in line
+ or "dso: /opt/google/chrome/chrome" not in line
+ ):
+ return None, None
+ args = line.split(" ")
+ pid_raw = args[6].split("/")
+ assert (
+ pid_raw[1][:-1] == args[14].split(":")[1][:-1]
+ ), "TID in %s of sample is not the same: %s/%s" % (
+ line[:-1],
+ pid_raw[1][:-1],
+ args[14].split(":")[1][:-1],
+ )
+ key = (int(pid_raw[0]), int(pid_raw[1][:-1]))
+ address = int(args[7], base=16)
+ return key, address
+
+ def _parse_perf_record(self, line):
+ # In a perf report, generated with -D, a PERF_RECORD_MMAP2 command should
+ # look like this: TODO: some arguments are unknown
+ #
+ # cpuid cycle unknown [unknown]: PERF_RECORD_MMAP2 pid/tid:
+ # [0xaddr(0xlength) @ pageoffset maj:min ino ino_generation]:
+ # permission process
+ #
+ # This is an example.
+ # 2 136690556823 0xa6898 [0x80]: PERF_RECORD_MMAP2 5227/5227:
+ # [0x555556496000(0x8d1b000) @ 0xf42000 b3:03 92844 1892514370]:
+ # r-xp /opt/google/chrome/chrome
+ #
+ # For this function, the 6th argument (args[5]) after spltting with spaces
+ # is pid/tid. We use the combination of the two as the pid.
+ # The 7th argument (args[6]) is the [0xaddr(0xlength). We can peel the
+ # string to get the address and size of the mmap.
+ # The 9th argument (args[8]) is the page offset.
+ # The function returns the ((pid,tid), mmap) pair if the mmap is for Chrome
+ # is on Chrome. Otherwise, return (None, None) pair.
+
+ if "chrome/chrome" not in line:
+ return None, None
+ args = line.split(" ")
+ pid_raw = args[5].split("/")
+ assert (
+ pid_raw[0] == pid_raw[1][:-1]
+ ), "PID in %s of mmap is not the same: %s/%s" % (
+ line[:-1],
+ pid_raw[0],
+ pid_raw[1],
+ )
+ pid = (int(pid_raw[0]), int(pid_raw[1][:-1]))
+ address_raw = args[6].split("(")
+ start_address = int(address_raw[0][1:], base=16)
+ size = int(address_raw[1][:-1], base=16)
+ offset = int(args[8], base=16)
+ # Return an mmap object instead of only starting address,
+ # in case there are many mmaps for the sample PID
+ return pid, MMap(start_address, size, offset)
+
+ def _parse_pair_event(self, arg):
+ # This function is called by the _parse_* functions that has a pattern of
+ # pids like: (pid:tid):(pid:tid), i.e.
+ # PERF_RECORD_FORK and PERF_RECORD_COMM
+ _, remain = arg.split("(", 1)
+ pid1, remain = remain.split(":", 1)
+ pid2, remain = remain.split(")", 1)
+ _, remain = remain.split("(", 1)
+ pid3, remain = remain.split(":", 1)
+ pid4, remain = remain.split(")", 1)
+ return (int(pid1), int(pid2)), (int(pid3), int(pid4))
+
+ def _process_perf_record(self, line):
+ # This function calls _parse_perf_record() to get information from
+ # PERF_RECORD_MMAP2. It records the mmap object for each pid (a pair of
+ # pid,tid), into a dictionary.
+ pid, mmap = self._parse_perf_record(line)
+ if pid is None:
+ # PID = None meaning the mmap is not for chrome
+ return
+ if pid in self.processes:
+ # This should never happen for a correct profiling result, as we
+ # should only have one MMAP for Chrome for each process.
+ # If it happens, see http://crbug.com/931465
+ self.processes[pid].merge(mmap)
else:
- line += ' smallpage'
- print(line, file=self.hist_temp_output)
-
- def _read_perf_report(self):
- # Serve as main function to read perf report, generated by -D
- lines = iter(self.perf_report_contents)
- for line in lines:
- if 'PERF_RECORD_MMAP' in line:
- self._process_perf_record(line)
- elif 'PERF_RECORD_FORK' in line:
- self._process_perf_fork(line)
- elif 'PERF_RECORD_EXIT' in line:
- self._process_perf_exit(line)
- elif 'PERF_RECORD_SAMPLE' in line:
- # Perf sample is multi-line
- self._process_perf_sample(line + next(lines) + next(lines))
- self.hist_temp_output.close()
-
- def _draw_heat_map(self):
- # Calls a script (perf-to-inst-page.sh) to calculate histogram
- # of results written in out.txt and also generate pngs for
- # heat maps.
- heatmap_script = os.path.join(self.dir, 'perf-to-inst-page.sh')
- if self.hugepage:
- hp_arg = 'hugepage'
- else:
- hp_arg = 'none'
-
- cmd = '{0} {1} {2}'.format(heatmap_script, pipes.quote(self.title), hp_arg)
- retval = self.ce.RunCommand(cmd)
- if retval:
- raise RuntimeError('Failed to run script to generate heatmap')
-
- def _restore_histogram(self):
- # When hugepage is used, there are two files inst-histo-{hp,sp}.txt
- # So we need to read in all the files.
- names = [x for x in os.listdir('.') if 'inst-histo' in x and '.txt' in x]
- hist = {}
- for n in names:
- with open(n, encoding='utf-8') as f:
- for l in f.readlines():
- num, addr = l.strip().split(' ')
- assert int(addr) not in hist
- hist[int(addr)] = int(num)
- return hist
-
- def _read_symbols_from_binary(self, binary):
- # FIXME: We are using nm to read symbol names from Chrome binary
- # for now. Can we get perf to hand us symbol names, instead of
- # using nm in the future?
- #
- # Get all the symbols (and their starting addresses) that fall into
- # the page. Will be used to print out information of hot pages
- # Each line shows the information of a symbol:
- # [symbol value (0xaddr)] [symbol type] [symbol name]
- # For some symbols, the [symbol name] field might be missing.
- # e.g.
- # 0000000001129da0 t Builtins_LdaNamedPropertyHandler
-
- # Generate a list of symbols from nm tool and check each line
- # to extract symbols names
- text_section_start = 0
- for l in subprocess.check_output(['nm', '-n', binary]).split('\n'):
- args = l.strip().split(' ')
- if len(args) < 3:
- # No name field
- continue
- addr_raw, symbol_type, name = args
- addr = int(addr_raw, base=16)
- if 't' not in symbol_type and 'T' not in symbol_type:
- # Filter out symbols not in text sections
- continue
- if not self.symbol_addresses:
- # The first symbol in text sections
- text_section_start = addr
- self.symbol_addresses.append(0)
- self.symbol_names.append(name)
- else:
- assert text_section_start != 0, \
- 'The starting address of text section has not been found'
- if addr == self.symbol_addresses[-1]:
- # if the same address has multiple symbols, put them together
- # and separate symbol names with '/'
- self.symbol_names[-1] += '/' + name
+ self.processes[pid] = mmap
+
+ def _process_perf_fork(self, line):
+ # In a perf report, generated with -D, a PERF_RECORD_FORK command should
+ # look like this:
+ #
+ # cpuid cycle unknown [unknown]:
+ # PERF_RECORD_FORK(pid_to:tid_to):(pid_from:tid_from)
+ #
+ # This is an example.
+ # 0 0 0x22a8 [0x38]: PERF_RECORD_FORK(1:1):(0:0)
+ #
+ # In this function, we need to peel the information of pid:tid pairs
+ # So we get the last argument and send it to function _parse_pair_event()
+ # for analysis.
+ # We use (pid, tid) as the pid.
+ args = line.split(" ")
+ pid_to, pid_from = self._parse_pair_event(args[-1])
+ if pid_from in self.processes:
+ assert pid_to not in self.processes
+ self.processes[pid_to] = MMap(
+ self.processes[pid_from].start_address,
+ self.processes[pid_from].size,
+ self.processes[pid_from].offset,
+ )
+
+ def _process_perf_exit(self, line):
+ # In a perf report, generated with -D, a PERF_RECORD_EXIT command should
+ # look like this:
+ #
+ # cpuid cycle unknown [unknown]:
+ # PERF_RECORD_EXIT(pid1:tid1):(pid2:tid2)
+ #
+ # This is an example.
+ # 1 136082505621 0x30810 [0x38]: PERF_RECORD_EXIT(3851:3851):(3851:3851)
+ #
+ # In this function, we need to peel the information of pid:tid pairs
+ # So we get the last argument and send it to function _parse_pair_event()
+ # for analysis.
+ # We use (pid, tid) as the pid.
+ args = line.split(" ")
+ pid_to, pid_from = self._parse_pair_event(args[-1])
+ assert pid_to == pid_from, "(%d, %d) (%d, %d)" % (
+ pid_to[0],
+ pid_to[1],
+ pid_from[0],
+ pid_from[1],
+ )
+ if pid_to in self.processes:
+ # Don't delete the process yet
+ self.deleted_processes[pid_from] = self.processes[pid_from]
+
+ def _process_perf_sample(self, line):
+ # This function calls _parse_perf_sample() to get information from
+ # the perf report.
+ # It needs to check the starting address of allocated mmap from
+ # the dictionary (self.processes) to calculate the offset within
+ # the text section of the sampling.
+ # The offset is calculated into pages (4KB or 2MB) and writes into
+ # out.txt together with the total counts, which will be used to
+ # calculate histogram.
+ pid, addr = self._parse_perf_sample(line)
+ if pid is None:
+ return
+
+ assert (
+ pid in self.processes and pid not in self.deleted_processes
+ ), "PID %d not found mmap and not forked from another process"
+
+ start_address = self.processes[pid].start_address
+ address = addr - start_address
+ assert (
+ address >= 0
+ and "addresses accessed in PERF_RECORD_SAMPLE should be larger than"
+ " the starting address of Chrome"
+ )
+ if address < self.max_addr:
+ self.count += 1
+ line = "%d/%d: %d %d" % (
+ pid[0],
+ pid[1],
+ self.count,
+ address // self.page_size * self.page_size,
+ )
+ if self.hugepage:
+ if self.hugepage.start <= address < self.hugepage.end:
+ line += " hugepage"
+ else:
+ line += " smallpage"
+ print(line, file=self.hist_temp_output)
+
+ def _read_perf_report(self):
+ # Serve as main function to read perf report, generated by -D
+ lines = iter(self.perf_report_contents)
+ for line in lines:
+ if "PERF_RECORD_MMAP" in line:
+ self._process_perf_record(line)
+ elif "PERF_RECORD_FORK" in line:
+ self._process_perf_fork(line)
+ elif "PERF_RECORD_EXIT" in line:
+ self._process_perf_exit(line)
+ elif "PERF_RECORD_SAMPLE" in line:
+ # Perf sample is multi-line
+ self._process_perf_sample(line + next(lines) + next(lines))
+ self.hist_temp_output.close()
+
+ def _draw_heat_map(self):
+ # Calls a script (perf-to-inst-page.sh) to calculate histogram
+ # of results written in out.txt and also generate pngs for
+ # heat maps.
+ heatmap_script = os.path.join(self.dir, "perf-to-inst-page.sh")
+ if self.hugepage:
+ hp_arg = "hugepage"
else:
- # The output of nm -n command is already sorted by address
- # Insert to the end will result in a sorted array for bisect
- self.symbol_addresses.append(addr - text_section_start)
- self.symbol_names.append(name)
-
- def _map_addr_to_symbol(self, addr):
- # Find out the symbol name
- assert self.symbol_addresses
- index = bisect.bisect(self.symbol_addresses, addr)
- assert 0 < index <= len(self.symbol_names), \
- 'Failed to find an index (%d) in the list (len=%d)' % (
- index, len(self.symbol_names))
- return self.symbol_names[index - 1]
-
- def _print_symbols_in_hot_pages(self, fp, pages_to_show):
- # Print symbols in all the pages of interest
- for page_num, sample_num in pages_to_show:
- print(
- '----------------------------------------------------------', file=fp)
- print(
- 'Page Offset: %d MB, Count: %d' % (page_num // 1024 // 1024,
- sample_num),
- file=fp)
-
- symbol_counts = collections.Counter()
- # Read Sample File and find out the occurance of symbols in the page
- lines = iter(self.perf_report_contents)
- for line in lines:
- if 'PERF_RECORD_SAMPLE' in line:
- pid, addr = self._parse_perf_sample(line + next(lines) + next(lines))
- if pid is None:
- # The sampling is not on Chrome
- continue
- if addr // self.page_size != (
- self.processes[pid].start_address + page_num) // self.page_size:
- # Sampling not in the current page
- continue
-
- name = self._map_addr_to_symbol(addr -
- self.processes[pid].start_address)
- assert name, 'Failed to find symbol name of addr %x' % addr
- symbol_counts[name] += 1
-
- assert sum(symbol_counts.values()) == sample_num, \
- 'Symbol name matching missing for some addresses: %d vs %d' % (
- sum(symbol_counts.values()), sample_num)
-
- # Print out the symbol names sorted by the number of samples in
- # the page
- for name, count in sorted(
- symbol_counts.items(), key=lambda kv: kv[1], reverse=True):
- if count == 0:
- break
- print('> %s : %d' % (name, count), file=fp)
- print('\n\n', file=fp)
-
- def draw(self):
- # First read perf report to process information and save histogram
- # into a text file
- self._read_perf_report()
- # Then use gnu plot to draw heat map
- self._draw_heat_map()
-
- def analyze(self, binary, top_n):
- # Read histogram from histo.txt
- hist = self._restore_histogram()
- # Sort the pages in histogram
- sorted_hist = sorted(hist.items(), key=lambda value: value[1], reverse=True)
-
- # Generate symbolizations
- self._read_symbols_from_binary(binary)
-
- # Write hottest pages
- with open('addr2symbol.txt', 'w', encoding='utf-8') as fp:
- if self.hugepage:
- # Print hugepage region first
- print(
- 'Hugepage top %d hot pages (%d MB - %d MB):' %
- (top_n, self.hugepage.start // 1024 // 1024,
- self.hugepage.end // 1024 // 1024),
- file=fp)
- pages_to_print = [(k, v)
- for k, v in sorted_hist
- if self.hugepage.start <= k < self.hugepage.end
- ][:top_n]
- self._print_symbols_in_hot_pages(fp, pages_to_print)
- print('==========================================', file=fp)
- print('Top %d hot pages landed outside of hugepage:' % top_n, file=fp)
- # Then print outside pages
- pages_to_print = [(k, v)
- for k, v in sorted_hist
- if k < self.hugepage.start or k >= self.hugepage.end
- ][:top_n]
- self._print_symbols_in_hot_pages(fp, pages_to_print)
- else:
- # Print top_n hottest pages.
- pages_to_print = sorted_hist[:top_n]
- self._print_symbols_in_hot_pages(fp, pages_to_print)
+ hp_arg = "none"
+
+ cmd = "{0} {1} {2}".format(
+ heatmap_script, pipes.quote(self.title), hp_arg
+ )
+ retval = self.ce.RunCommand(cmd)
+ if retval:
+ raise RuntimeError("Failed to run script to generate heatmap")
+
+ def _restore_histogram(self):
+ # When hugepage is used, there are two files inst-histo-{hp,sp}.txt
+ # So we need to read in all the files.
+ names = [
+ x for x in os.listdir(".") if "inst-histo" in x and ".txt" in x
+ ]
+ hist = {}
+ for n in names:
+ with open(n, encoding="utf-8") as f:
+ for l in f.readlines():
+ num, addr = l.strip().split(" ")
+ assert int(addr) not in hist
+ hist[int(addr)] = int(num)
+ return hist
+
+ def _read_symbols_from_binary(self, binary):
+ # FIXME: We are using nm to read symbol names from Chrome binary
+ # for now. Can we get perf to hand us symbol names, instead of
+ # using nm in the future?
+ #
+ # Get all the symbols (and their starting addresses) that fall into
+ # the page. Will be used to print out information of hot pages
+ # Each line shows the information of a symbol:
+ # [symbol value (0xaddr)] [symbol type] [symbol name]
+ # For some symbols, the [symbol name] field might be missing.
+ # e.g.
+ # 0000000001129da0 t Builtins_LdaNamedPropertyHandler
+
+ # Generate a list of symbols from nm tool and check each line
+ # to extract symbols names
+ text_section_start = 0
+ for l in subprocess.check_output(["nm", "-n", binary]).split("\n"):
+ args = l.strip().split(" ")
+ if len(args) < 3:
+ # No name field
+ continue
+ addr_raw, symbol_type, name = args
+ addr = int(addr_raw, base=16)
+ if "t" not in symbol_type and "T" not in symbol_type:
+ # Filter out symbols not in text sections
+ continue
+ if not self.symbol_addresses:
+ # The first symbol in text sections
+ text_section_start = addr
+ self.symbol_addresses.append(0)
+ self.symbol_names.append(name)
+ else:
+ assert (
+ text_section_start != 0
+ ), "The starting address of text section has not been found"
+ if addr == self.symbol_addresses[-1]:
+ # if the same address has multiple symbols, put them together
+ # and separate symbol names with '/'
+ self.symbol_names[-1] += "/" + name
+ else:
+ # The output of nm -n command is already sorted by address
+ # Insert to the end will result in a sorted array for bisect
+ self.symbol_addresses.append(addr - text_section_start)
+ self.symbol_names.append(name)
+
+ def _map_addr_to_symbol(self, addr):
+ # Find out the symbol name
+ assert self.symbol_addresses
+ index = bisect.bisect(self.symbol_addresses, addr)
+ assert (
+ 0 < index <= len(self.symbol_names)
+ ), "Failed to find an index (%d) in the list (len=%d)" % (
+ index,
+ len(self.symbol_names),
+ )
+ return self.symbol_names[index - 1]
+
+ def _print_symbols_in_hot_pages(self, fp, pages_to_show):
+ # Print symbols in all the pages of interest
+ for page_num, sample_num in pages_to_show:
+ print(
+ "----------------------------------------------------------",
+ file=fp,
+ )
+ print(
+ "Page Offset: %d MB, Count: %d"
+ % (page_num // 1024 // 1024, sample_num),
+ file=fp,
+ )
+
+ symbol_counts = collections.Counter()
+ # Read Sample File and find out the occurance of symbols in the page
+ lines = iter(self.perf_report_contents)
+ for line in lines:
+ if "PERF_RECORD_SAMPLE" in line:
+ pid, addr = self._parse_perf_sample(
+ line + next(lines) + next(lines)
+ )
+ if pid is None:
+ # The sampling is not on Chrome
+ continue
+ if (
+ addr // self.page_size
+ != (self.processes[pid].start_address + page_num)
+ // self.page_size
+ ):
+ # Sampling not in the current page
+ continue
+
+ name = self._map_addr_to_symbol(
+ addr - self.processes[pid].start_address
+ )
+ assert name, "Failed to find symbol name of addr %x" % addr
+ symbol_counts[name] += 1
+
+ assert (
+ sum(symbol_counts.values()) == sample_num
+ ), "Symbol name matching missing for some addresses: %d vs %d" % (
+ sum(symbol_counts.values()),
+ sample_num,
+ )
+
+ # Print out the symbol names sorted by the number of samples in
+ # the page
+ for name, count in sorted(
+ symbol_counts.items(), key=lambda kv: kv[1], reverse=True
+ ):
+ if count == 0:
+ break
+ print("> %s : %d" % (name, count), file=fp)
+ print("\n\n", file=fp)
+
+ def draw(self):
+ # First read perf report to process information and save histogram
+ # into a text file
+ self._read_perf_report()
+ # Then use gnu plot to draw heat map
+ self._draw_heat_map()
+
+ def analyze(self, binary, top_n):
+ # Read histogram from histo.txt
+ hist = self._restore_histogram()
+ # Sort the pages in histogram
+ sorted_hist = sorted(
+ hist.items(), key=lambda value: value[1], reverse=True
+ )
+
+ # Generate symbolizations
+ self._read_symbols_from_binary(binary)
+
+ # Write hottest pages
+ with open("addr2symbol.txt", "w", encoding="utf-8") as fp:
+ if self.hugepage:
+ # Print hugepage region first
+ print(
+ "Hugepage top %d hot pages (%d MB - %d MB):"
+ % (
+ top_n,
+ self.hugepage.start // 1024 // 1024,
+ self.hugepage.end // 1024 // 1024,
+ ),
+ file=fp,
+ )
+ pages_to_print = [
+ (k, v)
+ for k, v in sorted_hist
+ if self.hugepage.start <= k < self.hugepage.end
+ ][:top_n]
+ self._print_symbols_in_hot_pages(fp, pages_to_print)
+ print("==========================================", file=fp)
+ print(
+ "Top %d hot pages landed outside of hugepage:" % top_n,
+ file=fp,
+ )
+ # Then print outside pages
+ pages_to_print = [
+ (k, v)
+ for k, v in sorted_hist
+ if k < self.hugepage.start or k >= self.hugepage.end
+ ][:top_n]
+ self._print_symbols_in_hot_pages(fp, pages_to_print)
+ else:
+ # Print top_n hottest pages.
+ pages_to_print = sorted_hist[:top_n]
+ self._print_symbols_in_hot_pages(fp, pages_to_print)
diff --git a/heatmaps/heatmap_generator_test.py b/heatmaps/heatmap_generator_test.py
index 5008c653..898c7370 100755
--- a/heatmaps/heatmap_generator_test.py
+++ b/heatmaps/heatmap_generator_test.py
@@ -1,316 +1,326 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for heatmap_generator.py."""
-from __future__ import division, print_function
-
-import unittest.mock as mock
-import unittest
import os
+import unittest
+import unittest.mock as mock
from heatmaps import heatmap_generator
def _write_perf_mmap(pid, tid, addr, size, fp):
- print(
- '0 0 0 0 PERF_RECORD_MMAP2 %d/%d: '
- '[%x(%x) @ 0x0 0:0 0 0] '
- 'r-xp /opt/google/chrome/chrome\n' % (pid, tid, addr, size),
- file=fp)
+ print(
+ "0 0 0 0 PERF_RECORD_MMAP2 %d/%d: "
+ "[%x(%x) @ 0x0 0:0 0 0] "
+ "r-xp /opt/google/chrome/chrome\n" % (pid, tid, addr, size),
+ file=fp,
+ )
def _write_perf_fork(pid_from, tid_from, pid_to, tid_to, fp):
- print(
- '0 0 0 0 PERF_RECORD_FORK(%d:%d):(%d:%d)\n' % (pid_to, tid_to, pid_from,
- tid_from),
- file=fp)
+ print(
+ "0 0 0 0 PERF_RECORD_FORK(%d:%d):(%d:%d)\n"
+ % (pid_to, tid_to, pid_from, tid_from),
+ file=fp,
+ )
def _write_perf_exit(pid_from, tid_from, pid_to, tid_to, fp):
- print(
- '0 0 0 0 PERF_RECORD_EXIT(%d:%d):(%d:%d)\n' % (pid_to, tid_to, pid_from,
- tid_from),
- file=fp)
+ print(
+ "0 0 0 0 PERF_RECORD_EXIT(%d:%d):(%d:%d)\n"
+ % (pid_to, tid_to, pid_from, tid_from),
+ file=fp,
+ )
def _write_perf_sample(pid, tid, addr, fp):
- print(
- '0 0 0 0 PERF_RECORD_SAMPLE(IP, 0x2): '
- '%d/%d: %x period: 100000 addr: 0' % (pid, tid, addr),
- file=fp)
- print(' ... thread: chrome:%d' % tid, file=fp)
- print(' ...... dso: /opt/google/chrome/chrome\n', file=fp)
+ print(
+ "0 0 0 0 PERF_RECORD_SAMPLE(IP, 0x2): "
+ "%d/%d: %x period: 100000 addr: 0" % (pid, tid, addr),
+ file=fp,
+ )
+ print(" ... thread: chrome:%d" % tid, file=fp)
+ print(" ...... dso: /opt/google/chrome/chrome\n", file=fp)
def _heatmap(file_name, page_size=4096, hugepage=None, analyze=False, top_n=10):
- generator = heatmap_generator.HeatmapGenerator(
- file_name, page_size, hugepage, '',
- log_level='none') # Don't log to stdout
- generator.draw()
- if analyze:
- generator.analyze('/path/to/chrome', top_n)
+ generator = heatmap_generator.HeatmapGenerator(
+ file_name, page_size, hugepage, "", log_level="none"
+ ) # Don't log to stdout
+ generator.draw()
+ if analyze:
+ generator.analyze("/path/to/chrome", top_n)
def _cleanup(file_name):
- files = [
- file_name, 'out.txt', 'inst-histo.txt', 'inst-histo-hp.txt',
- 'inst-histo-sp.txt', 'heat_map.png', 'timeline.png', 'addr2symbol.txt'
- ]
- for f in files:
- if os.path.exists(f):
- os.remove(f)
+ files = [
+ file_name,
+ "out.txt",
+ "inst-histo.txt",
+ "inst-histo-hp.txt",
+ "inst-histo-sp.txt",
+ "heat_map.png",
+ "timeline.png",
+ "addr2symbol.txt",
+ ]
+ for f in files:
+ if os.path.exists(f):
+ os.remove(f)
class HeatmapGeneratorDrawTests(unittest.TestCase):
- """All of our tests for heatmap_generator.draw() and related."""
-
- def test_with_one_mmap_one_sample(self):
- """Tests one perf record and one sample."""
- fname = 'test.txt'
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
- _write_perf_sample(101, 101, 0xABCD101, f)
- self.addCleanup(_cleanup, fname)
- _heatmap(fname)
- self.assertIn('out.txt', os.listdir('.'))
- with open('out.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 1)
- self.assertIn('101/101: 1 0', lines[0])
-
- def test_with_one_mmap_multiple_samples(self):
- """Tests one perf record and three samples."""
- fname = 'test.txt'
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
- _write_perf_sample(101, 101, 0xABCD101, f)
- _write_perf_sample(101, 101, 0xABCD102, f)
- _write_perf_sample(101, 101, 0xABCE102, f)
- self.addCleanup(_cleanup, fname)
- _heatmap(fname)
- self.assertIn('out.txt', os.listdir('.'))
- with open('out.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 3)
- self.assertIn('101/101: 1 0', lines[0])
- self.assertIn('101/101: 2 0', lines[1])
- self.assertIn('101/101: 3 4096', lines[2])
-
- def test_with_fork_and_exit(self):
- """Tests perf fork and perf exit."""
- fname = 'test_fork.txt'
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
- _write_perf_fork(101, 101, 202, 202, f)
- _write_perf_sample(101, 101, 0xABCD101, f)
- _write_perf_sample(202, 202, 0xABCE101, f)
- _write_perf_exit(202, 202, 202, 202, f)
- self.addCleanup(_cleanup, fname)
- _heatmap(fname)
- self.assertIn('out.txt', os.listdir('.'))
- with open('out.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 2)
- self.assertIn('101/101: 1 0', lines[0])
- self.assertIn('202/202: 2 4096', lines[1])
-
- def test_hugepage_creates_two_chrome_mmaps(self):
- """Test two chrome mmaps for the same process."""
- fname = 'test_hugepage.txt'
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
- _write_perf_fork(101, 101, 202, 202, f)
- _write_perf_mmap(202, 202, 0xABCD000, 0x100, f)
- _write_perf_mmap(202, 202, 0xABCD300, 0xD00, f)
- _write_perf_sample(101, 101, 0xABCD102, f)
- _write_perf_sample(202, 202, 0xABCD102, f)
- self.addCleanup(_cleanup, fname)
- _heatmap(fname)
- self.assertIn('out.txt', os.listdir('.'))
- with open('out.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 2)
- self.assertIn('101/101: 1 0', lines[0])
- self.assertIn('202/202: 2 0', lines[1])
-
- def test_hugepage_creates_two_chrome_mmaps_fail(self):
- """Test two chrome mmaps for the same process."""
- fname = 'test_hugepage.txt'
- # Cases where first_mmap.size < second_mmap.size
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
- _write_perf_fork(101, 101, 202, 202, f)
- _write_perf_mmap(202, 202, 0xABCD000, 0x10000, f)
- self.addCleanup(_cleanup, fname)
- with self.assertRaises(AssertionError) as msg:
- _heatmap(fname)
- self.assertIn('Original MMAP size', str(msg.exception))
-
- # Cases where first_mmap.address > second_mmap.address
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
- _write_perf_fork(101, 101, 202, 202, f)
- _write_perf_mmap(202, 202, 0xABCC000, 0x10000, f)
- with self.assertRaises(AssertionError) as msg:
- _heatmap(fname)
- self.assertIn('Original MMAP starting address', str(msg.exception))
-
- # Cases where first_mmap.address + size <
- # second_mmap.address + second_mmap.size
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
- _write_perf_fork(101, 101, 202, 202, f)
- _write_perf_mmap(202, 202, 0xABCD100, 0x10000, f)
- with self.assertRaises(AssertionError) as msg:
- _heatmap(fname)
- self.assertIn('exceeds the end of original MMAP', str(msg.exception))
-
- def test_histogram(self):
- """Tests if the tool can generate correct histogram.
-
- In the tool, histogram is generated from statistics
- of perf samples (saved to out.txt). The histogram is
- generated by perf-to-inst-page.sh and saved to
- inst-histo.txt. It will be used to draw heat maps.
- """
- fname = 'test_histo.txt'
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
- for i in range(100):
- _write_perf_sample(101, 101, 0xABCD000 + i, f)
- _write_perf_sample(101, 101, 0xABCE000 + i, f)
- _write_perf_sample(101, 101, 0xABFD000 + i, f)
- _write_perf_sample(101, 101, 0xAFCD000 + i, f)
- self.addCleanup(_cleanup, fname)
- _heatmap(fname)
- self.assertIn('inst-histo.txt', os.listdir('.'))
- with open('inst-histo.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 4)
- self.assertIn('100 0', lines[0])
- self.assertIn('100 4096', lines[1])
- self.assertIn('100 196608', lines[2])
- self.assertIn('100 4194304', lines[3])
-
- def test_histogram_two_mb_page(self):
- """Tests handling of 2MB page."""
- fname = 'test_histo.txt'
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
- for i in range(100):
- _write_perf_sample(101, 101, 0xABCD000 + i, f)
- _write_perf_sample(101, 101, 0xABCE000 + i, f)
- _write_perf_sample(101, 101, 0xABFD000 + i, f)
- _write_perf_sample(101, 101, 0xAFCD000 + i, f)
- self.addCleanup(_cleanup, fname)
- _heatmap(fname, page_size=2 * 1024 * 1024)
- self.assertIn('inst-histo.txt', os.listdir('.'))
- with open('inst-histo.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 2)
- self.assertIn('300 0', lines[0])
- self.assertIn('100 4194304', lines[1])
-
- def test_histogram_in_and_out_hugepage(self):
- """Tests handling the case of separating samples in and out huge page."""
- fname = 'test_histo.txt'
- with open(fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
- for i in range(100):
- _write_perf_sample(101, 101, 0xABCD000 + i, f)
- _write_perf_sample(101, 101, 0xABCE000 + i, f)
- _write_perf_sample(101, 101, 0xABFD000 + i, f)
- _write_perf_sample(101, 101, 0xAFCD000 + i, f)
- self.addCleanup(_cleanup, fname)
- _heatmap(fname, hugepage=[0, 8192])
- file_list = os.listdir('.')
- self.assertNotIn('inst-histo.txt', file_list)
- self.assertIn('inst-histo-hp.txt', file_list)
- self.assertIn('inst-histo-sp.txt', file_list)
- with open('inst-histo-hp.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 2)
- self.assertIn('100 0', lines[0])
- self.assertIn('100 4096', lines[1])
- with open('inst-histo-sp.txt') as f:
- lines = f.readlines()
- self.assertEqual(len(lines), 2)
- self.assertIn('100 196608', lines[0])
- self.assertIn('100 4194304', lines[1])
+ """All of our tests for heatmap_generator.draw() and related."""
+
+ def test_with_one_mmap_one_sample(self):
+ """Tests one perf record and one sample."""
+ fname = "test.txt"
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
+ _write_perf_sample(101, 101, 0xABCD101, f)
+ self.addCleanup(_cleanup, fname)
+ _heatmap(fname)
+ self.assertIn("out.txt", os.listdir("."))
+ with open("out.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 1)
+ self.assertIn("101/101: 1 0", lines[0])
+
+ def test_with_one_mmap_multiple_samples(self):
+ """Tests one perf record and three samples."""
+ fname = "test.txt"
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
+ _write_perf_sample(101, 101, 0xABCD101, f)
+ _write_perf_sample(101, 101, 0xABCD102, f)
+ _write_perf_sample(101, 101, 0xABCE102, f)
+ self.addCleanup(_cleanup, fname)
+ _heatmap(fname)
+ self.assertIn("out.txt", os.listdir("."))
+ with open("out.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 3)
+ self.assertIn("101/101: 1 0", lines[0])
+ self.assertIn("101/101: 2 0", lines[1])
+ self.assertIn("101/101: 3 4096", lines[2])
+
+ def test_with_fork_and_exit(self):
+ """Tests perf fork and perf exit."""
+ fname = "test_fork.txt"
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
+ _write_perf_fork(101, 101, 202, 202, f)
+ _write_perf_sample(101, 101, 0xABCD101, f)
+ _write_perf_sample(202, 202, 0xABCE101, f)
+ _write_perf_exit(202, 202, 202, 202, f)
+ self.addCleanup(_cleanup, fname)
+ _heatmap(fname)
+ self.assertIn("out.txt", os.listdir("."))
+ with open("out.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 2)
+ self.assertIn("101/101: 1 0", lines[0])
+ self.assertIn("202/202: 2 4096", lines[1])
+
+ def test_hugepage_creates_two_chrome_mmaps(self):
+ """Test two chrome mmaps for the same process."""
+ fname = "test_hugepage.txt"
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
+ _write_perf_fork(101, 101, 202, 202, f)
+ _write_perf_mmap(202, 202, 0xABCD000, 0x100, f)
+ _write_perf_mmap(202, 202, 0xABCD300, 0xD00, f)
+ _write_perf_sample(101, 101, 0xABCD102, f)
+ _write_perf_sample(202, 202, 0xABCD102, f)
+ self.addCleanup(_cleanup, fname)
+ _heatmap(fname)
+ self.assertIn("out.txt", os.listdir("."))
+ with open("out.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 2)
+ self.assertIn("101/101: 1 0", lines[0])
+ self.assertIn("202/202: 2 0", lines[1])
+
+ def test_hugepage_creates_two_chrome_mmaps_fail(self):
+ """Test two chrome mmaps for the same process."""
+ fname = "test_hugepage.txt"
+ # Cases where first_mmap.size < second_mmap.size
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
+ _write_perf_fork(101, 101, 202, 202, f)
+ _write_perf_mmap(202, 202, 0xABCD000, 0x10000, f)
+ self.addCleanup(_cleanup, fname)
+ with self.assertRaises(AssertionError) as msg:
+ _heatmap(fname)
+ self.assertIn("Original MMAP size", str(msg.exception))
+
+ # Cases where first_mmap.address > second_mmap.address
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
+ _write_perf_fork(101, 101, 202, 202, f)
+ _write_perf_mmap(202, 202, 0xABCC000, 0x10000, f)
+ with self.assertRaises(AssertionError) as msg:
+ _heatmap(fname)
+ self.assertIn("Original MMAP starting address", str(msg.exception))
+
+ # Cases where first_mmap.address + size <
+ # second_mmap.address + second_mmap.size
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x1000, f)
+ _write_perf_fork(101, 101, 202, 202, f)
+ _write_perf_mmap(202, 202, 0xABCD100, 0x10000, f)
+ with self.assertRaises(AssertionError) as msg:
+ _heatmap(fname)
+ self.assertIn("exceeds the end of original MMAP", str(msg.exception))
+
+ def test_histogram(self):
+ """Tests if the tool can generate correct histogram.
+
+ In the tool, histogram is generated from statistics
+ of perf samples (saved to out.txt). The histogram is
+ generated by perf-to-inst-page.sh and saved to
+ inst-histo.txt. It will be used to draw heat maps.
+ """
+ fname = "test_histo.txt"
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
+ for i in range(100):
+ _write_perf_sample(101, 101, 0xABCD000 + i, f)
+ _write_perf_sample(101, 101, 0xABCE000 + i, f)
+ _write_perf_sample(101, 101, 0xABFD000 + i, f)
+ _write_perf_sample(101, 101, 0xAFCD000 + i, f)
+ self.addCleanup(_cleanup, fname)
+ _heatmap(fname)
+ self.assertIn("inst-histo.txt", os.listdir("."))
+ with open("inst-histo.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 4)
+ self.assertIn("100 0", lines[0])
+ self.assertIn("100 4096", lines[1])
+ self.assertIn("100 196608", lines[2])
+ self.assertIn("100 4194304", lines[3])
+
+ def test_histogram_two_mb_page(self):
+ """Tests handling of 2MB page."""
+ fname = "test_histo.txt"
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
+ for i in range(100):
+ _write_perf_sample(101, 101, 0xABCD000 + i, f)
+ _write_perf_sample(101, 101, 0xABCE000 + i, f)
+ _write_perf_sample(101, 101, 0xABFD000 + i, f)
+ _write_perf_sample(101, 101, 0xAFCD000 + i, f)
+ self.addCleanup(_cleanup, fname)
+ _heatmap(fname, page_size=2 * 1024 * 1024)
+ self.assertIn("inst-histo.txt", os.listdir("."))
+ with open("inst-histo.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 2)
+ self.assertIn("300 0", lines[0])
+ self.assertIn("100 4194304", lines[1])
+
+ def test_histogram_in_and_out_hugepage(self):
+ """Tests handling the case of separating samples in and out huge page."""
+ fname = "test_histo.txt"
+ with open(fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
+ for i in range(100):
+ _write_perf_sample(101, 101, 0xABCD000 + i, f)
+ _write_perf_sample(101, 101, 0xABCE000 + i, f)
+ _write_perf_sample(101, 101, 0xABFD000 + i, f)
+ _write_perf_sample(101, 101, 0xAFCD000 + i, f)
+ self.addCleanup(_cleanup, fname)
+ _heatmap(fname, hugepage=[0, 8192])
+ file_list = os.listdir(".")
+ self.assertNotIn("inst-histo.txt", file_list)
+ self.assertIn("inst-histo-hp.txt", file_list)
+ self.assertIn("inst-histo-sp.txt", file_list)
+ with open("inst-histo-hp.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 2)
+ self.assertIn("100 0", lines[0])
+ self.assertIn("100 4096", lines[1])
+ with open("inst-histo-sp.txt") as f:
+ lines = f.readlines()
+ self.assertEqual(len(lines), 2)
+ self.assertIn("100 196608", lines[0])
+ self.assertIn("100 4194304", lines[1])
class HeatmapGeneratorAnalyzeTests(unittest.TestCase):
- """All of our tests for heatmap_generator.analyze() and related."""
-
- def setUp(self):
- # Use the same perf report for testing
- self.fname = 'test_histo.txt'
- with open(self.fname, 'w') as f:
- _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
- for i in range(10):
- _write_perf_sample(101, 101, 0xABCD000 + i, f)
- _write_perf_sample(101, 101, 0xABCE000 + i, f)
- _write_perf_sample(101, 101, 0xABFD000 + i, f)
- self.nm = ('000000000abcd000 t Func1@Page1\n'
- '000000000abcd001 t Func2@Page1\n'
- '000000000abcd0a0 t Func3@Page1andFunc1@Page2\n'
- '000000000abce010 t Func2@Page2\n'
- '000000000abfd000 t Func1@Page3\n')
-
- def tearDown(self):
- _cleanup(self.fname)
-
- @mock.patch('subprocess.check_output')
- def test_analyze_hot_pages_with_hp_top(self, mock_nm):
- """Test if the analyze() can print the top page with hugepage."""
- mock_nm.return_value = self.nm
- _heatmap(self.fname, hugepage=[0, 8192], analyze=True, top_n=1)
- file_list = os.listdir('.')
- self.assertIn('addr2symbol.txt', file_list)
- with open('addr2symbol.txt') as f:
- contents = f.read()
- self.assertIn('Func2@Page1 : 9', contents)
- self.assertIn('Func1@Page1 : 1', contents)
- self.assertIn('Func1@Page3 : 10', contents)
- # Only displaying one page in hugepage
- self.assertNotIn('Func3@Page1andFunc1@Page2 : 10', contents)
-
- @mock.patch('subprocess.check_output')
- def test_analyze_hot_pages_without_hp_top(self, mock_nm):
- """Test if the analyze() can print the top page without hugepage."""
- mock_nm.return_value = self.nm
- _heatmap(self.fname, analyze=True, top_n=1)
- file_list = os.listdir('.')
- self.assertIn('addr2symbol.txt', file_list)
- with open('addr2symbol.txt') as f:
- contents = f.read()
- self.assertIn('Func2@Page1 : 9', contents)
- self.assertIn('Func1@Page1 : 1', contents)
- # Only displaying one page
- self.assertNotIn('Func3@Page1andFunc1@Page2 : 10', contents)
- self.assertNotIn('Func1@Page3 : 10', contents)
-
- @mock.patch('subprocess.check_output')
- def test_analyze_hot_pages_with_hp_top10(self, mock_nm):
- """Test if the analyze() can print with default top 10."""
- mock_nm.return_value = self.nm
- _heatmap(self.fname, analyze=True)
- # Make sure nm command is called correctly.
- mock_nm.assert_called_with(['nm', '-n', '/path/to/chrome'])
- file_list = os.listdir('.')
- self.assertIn('addr2symbol.txt', file_list)
- with open('addr2symbol.txt') as f:
- contents = f.read()
- self.assertIn('Func2@Page1 : 9', contents)
- self.assertIn('Func1@Page1 : 1', contents)
- self.assertIn('Func3@Page1andFunc1@Page2 : 10', contents)
- self.assertIn('Func1@Page3 : 10', contents)
-
-
-if __name__ == '__main__':
- unittest.main()
+ """All of our tests for heatmap_generator.analyze() and related."""
+
+ def setUp(self):
+ # Use the same perf report for testing
+ self.fname = "test_histo.txt"
+ with open(self.fname, "w") as f:
+ _write_perf_mmap(101, 101, 0xABCD000, 0x100, f)
+ for i in range(10):
+ _write_perf_sample(101, 101, 0xABCD000 + i, f)
+ _write_perf_sample(101, 101, 0xABCE000 + i, f)
+ _write_perf_sample(101, 101, 0xABFD000 + i, f)
+ self.nm = (
+ "000000000abcd000 t Func1@Page1\n"
+ "000000000abcd001 t Func2@Page1\n"
+ "000000000abcd0a0 t Func3@Page1andFunc1@Page2\n"
+ "000000000abce010 t Func2@Page2\n"
+ "000000000abfd000 t Func1@Page3\n"
+ )
+
+ def tearDown(self):
+ _cleanup(self.fname)
+
+ @mock.patch("subprocess.check_output")
+ def test_analyze_hot_pages_with_hp_top(self, mock_nm):
+ """Test if the analyze() can print the top page with hugepage."""
+ mock_nm.return_value = self.nm
+ _heatmap(self.fname, hugepage=[0, 8192], analyze=True, top_n=1)
+ file_list = os.listdir(".")
+ self.assertIn("addr2symbol.txt", file_list)
+ with open("addr2symbol.txt") as f:
+ contents = f.read()
+ self.assertIn("Func2@Page1 : 9", contents)
+ self.assertIn("Func1@Page1 : 1", contents)
+ self.assertIn("Func1@Page3 : 10", contents)
+ # Only displaying one page in hugepage
+ self.assertNotIn("Func3@Page1andFunc1@Page2 : 10", contents)
+
+ @mock.patch("subprocess.check_output")
+ def test_analyze_hot_pages_without_hp_top(self, mock_nm):
+ """Test if the analyze() can print the top page without hugepage."""
+ mock_nm.return_value = self.nm
+ _heatmap(self.fname, analyze=True, top_n=1)
+ file_list = os.listdir(".")
+ self.assertIn("addr2symbol.txt", file_list)
+ with open("addr2symbol.txt") as f:
+ contents = f.read()
+ self.assertIn("Func2@Page1 : 9", contents)
+ self.assertIn("Func1@Page1 : 1", contents)
+ # Only displaying one page
+ self.assertNotIn("Func3@Page1andFunc1@Page2 : 10", contents)
+ self.assertNotIn("Func1@Page3 : 10", contents)
+
+ @mock.patch("subprocess.check_output")
+ def test_analyze_hot_pages_with_hp_top10(self, mock_nm):
+ """Test if the analyze() can print with default top 10."""
+ mock_nm.return_value = self.nm
+ _heatmap(self.fname, analyze=True)
+ # Make sure nm command is called correctly.
+ mock_nm.assert_called_with(["nm", "-n", "/path/to/chrome"])
+ file_list = os.listdir(".")
+ self.assertIn("addr2symbol.txt", file_list)
+ with open("addr2symbol.txt") as f:
+ contents = f.read()
+ self.assertIn("Func2@Page1 : 9", contents)
+ self.assertIn("Func1@Page1 : 1", contents)
+ self.assertIn("Func3@Page1andFunc1@Page2 : 10", contents)
+ self.assertIn("Func1@Page3 : 10", contents)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/heatmaps/perf-to-inst-page.sh b/heatmaps/perf-to-inst-page.sh
index d6acd5ed..5be1a2b9 100755
--- a/heatmaps/perf-to-inst-page.sh
+++ b/heatmaps/perf-to-inst-page.sh
@@ -1,5 +1,5 @@
#! /bin/bash -u
-# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Copyright 2015 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.