summaryrefslogtreecommitdiff
path: root/chromium/tools/cygprofile/phased_orderfile.py
blob: aee4c42282dc1c58177a6eb28059fccd596f8b17 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env vpython
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Utilities for creating a phased orderfile.

This kind of orderfile is based on cygprofile lightweight instrumentation. The
profile dump format is described in process_profiles.py. These tools assume
profiling has been done with two phases.

The first phase, labeled 0 in the filename, is called "startup" and the second,
labeled 1, is called "interaction". These two phases are used to create an
orderfile with three parts: the code touched only in startup, the code
touched only during interaction, and code common to the two phases. We refer to
these parts as the orderfile phases.
"""

import argparse
import collections
import glob
import itertools
import logging
import os.path

import process_profiles


# Files matched when using this script to analyze directly (see main()).
PROFILE_GLOB = 'cygprofile-*.txt_*'


OrderfilePhaseOffsets = collections.namedtuple(
    'OrderfilePhaseOffsets', ('startup', 'common', 'interaction'))


class PhasedAnalyzer(object):
  """A class which collects analysis around phased orderfiles.

  It maintains common data such as symbol table information to make analysis
  more convenient.
  """
  # These figures are taken from running memory and speedometer telemetry
  # benchmarks, and are still subject to change as of 2018-01-24.
  STARTUP_STABILITY_THRESHOLD = 1.5
  COMMON_STABILITY_THRESHOLD = 1.75
  INTERACTION_STABILITY_THRESHOLD = 2.5

  def __init__(self, profiles, processor):
    """Intialize.

    Args:
      profiles (ProfileManager) Manager of the profile dump files.
      processor (SymbolOffsetProcessor) Symbol table processor for the dumps.
    """
    self._profiles = profiles
    self._processor = processor
    self._phase_offsets = None

  def IsStableProfile(self):
    """Verify that the profiling has been stable.

    See ComputeStability for details.

    Returns:
      True if the profile was stable as described above.
    """
    (startup_stability, common_stability,
     interaction_stability) = self.ComputeStability()

    stable = True
    if startup_stability > self.STARTUP_STABILITY_THRESHOLD:
      logging.error('Startup unstable: %.3f', startup_stability)
      stable = False
    if common_stability > self.COMMON_STABILITY_THRESHOLD:
      logging.error('Common unstable: %.3f', common_stability)
      stable = False
    if interaction_stability > self.INTERACTION_STABILITY_THRESHOLD:
      logging.error('Interaction unstable: %.3f', interaction_stability)
      stable = False

    return stable

  def ComputeStability(self):
    """Compute heuristic phase stability metrics.

    This computes the ratio in size of symbols between the union and
    intersection of all orderfile phases. Intuitively if this ratio is not too
    large it means that the profiling phases are stable with respect to the code
    they cover.

    Returns:
      (float, float, float) A heuristic stability metric for startup, common and
          interaction orderfile phases, respectively.
    """
    phase_offsets = self._GetOrderfilePhaseOffsets()
    assert len(phase_offsets) > 1  # Otherwise the analysis is silly.

    startup_union = set(phase_offsets[0].startup)
    startup_intersection = set(phase_offsets[0].startup)
    common_union = set(phase_offsets[0].common)
    common_intersection = set(phase_offsets[0].common)
    interaction_union = set(phase_offsets[0].interaction)
    interaction_intersection = set(phase_offsets[0].interaction)
    for offsets in phase_offsets[1:]:
      startup_union |= set(offsets.startup)
      startup_intersection &= set(offsets.startup)
      common_union |= set(offsets.common)
      common_intersection &= set(offsets.common)
      interaction_union |= set(offsets.interaction)
      interaction_intersection &= set(offsets.interaction)
    startup_stability = self._SafeDiv(
        self._processor.OffsetsPrimarySize(startup_union),
        self._processor.OffsetsPrimarySize(startup_intersection))
    common_stability = self._SafeDiv(
        self._processor.OffsetsPrimarySize(common_union),
        self._processor.OffsetsPrimarySize(common_intersection))
    interaction_stability = self._SafeDiv(
        self._processor.OffsetsPrimarySize(interaction_union),
        self._processor.OffsetsPrimarySize(interaction_intersection))
    return (startup_stability, common_stability, interaction_stability)

  def _GetOrderfilePhaseOffsets(self):
    """Compute the phase offsets for each run.

    Returns:
      [OrderfilePhaseOffsets] Each run corresponds to an OrderfilePhaseOffsets,
          which groups the symbol offsets discovered in the runs.
    """
    if self._phase_offsets is not None:
      return self._phase_offsets

    assert self._profiles.GetPhases() == set([0, 1]), 'Unexpected phases'
    self._phase_offsets = []
    for first, second in zip(self._profiles.GetRunGroupOffsets(phase=0),
                             self._profiles.GetRunGroupOffsets(phase=1)):
      all_first_offsets = self._processor.GetReachedOffsetsFromDump(first)
      all_second_offsets = self._processor.GetReachedOffsetsFromDump(second)
      first_offsets_set = set(all_first_offsets)
      second_offsets_set = set(all_second_offsets)
      common_offsets_set = first_offsets_set & second_offsets_set
      first_offsets_set -= common_offsets_set
      second_offsets_set -= common_offsets_set

      startup = [x for x in all_first_offsets
                 if x in first_offsets_set]

      interaction = [x for x in all_second_offsets
                     if x in second_offsets_set]

      common_seen = set()
      common = []
      for x in itertools.chain(all_first_offsets, all_second_offsets):
        if x in common_offsets_set and x not in common_seen:
          common_seen.add(x)
          common.append(x)

      self._phase_offsets.append(OrderfilePhaseOffsets(
          startup=startup,
          interaction=interaction,
          common=common))

    return self._phase_offsets

  @classmethod
  def _SafeDiv(cls, a, b):
    if not b:
      return None
    return float(a) / b


def _CreateArgumentParser():
  parser = argparse.ArgumentParser(
      description='Compute statistics on phased orderfiles')
  parser.add_argument('--profile-directory', type=str, required=True,
                      help=('Directory containing profile runs. Files '
                            'matching {} are used.'.format(PROFILE_GLOB)))
  parser.add_argument('--instrumented-build-dir', type=str,
                      help='Path to the instrumented build', required=True)
  parser.add_argument('--library-name', default='libchrome.so',
                      help=('Chrome shared library name (usually libchrome.so '
                            'or libmonochrome.so'))
  return parser


def main():
  logging.basicConfig(level=logging.INFO)
  parser = _CreateArgumentParser()
  args = parser.parse_args()
  profiles = process_profiles.ProfileManager(
      glob.glob(os.path.join(args.profile_directory, PROFILE_GLOB)))
  processor = process_profiles.SymbolOffsetProcessor(os.path.join(
      args.instrumented_build_dir, 'lib.unstripped', args.library_name))
  phaser = PhasedAnalyzer(profiles, processor)
  print 'Stability: {:.2f} {:.2f} {:.2f}'.format(*phaser.ComputeStability())


if __name__ == '__main__':
  main()