OLD | NEW |
(Empty) | |
| 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 |
| 5 import datetime |
| 6 import json |
| 7 import logging |
| 8 import sys |
| 9 |
| 10 from lib.bucket import BUCKET_ID, COMMITTED |
| 11 from lib.pageframe import PFNCounts |
| 12 from lib.policy import PolicySet |
| 13 from lib.subcommand import SubCommand |
| 14 |
| 15 |
| 16 LOGGER = logging.getLogger('dmprof') |
| 17 |
| 18 |
| 19 class PolicyCommands(SubCommand): |
| 20 def __init__(self, command): |
| 21 super(PolicyCommands, self).__init__( |
| 22 'Usage: %%prog %s [-p POLICY] <first-dump> [shared-first-dumps...]' % |
| 23 command) |
| 24 self._parser.add_option('-p', '--policy', type='string', dest='policy', |
| 25 help='profile with POLICY', metavar='POLICY') |
| 26 self._parser.add_option('--alternative-dirs', dest='alternative_dirs', |
| 27 metavar='/path/on/target@/path/on/host[:...]', |
| 28 help='Read files in /path/on/host/ instead of ' |
| 29 'files in /path/on/target/.') |
| 30 |
| 31 def _set_up(self, sys_argv): |
| 32 options, args = self._parse_args(sys_argv, 1) |
| 33 dump_path = args[1] |
| 34 shared_first_dump_paths = args[2:] |
| 35 alternative_dirs_dict = {} |
| 36 if options.alternative_dirs: |
| 37 for alternative_dir_pair in options.alternative_dirs.split(':'): |
| 38 target_path, host_path = alternative_dir_pair.split('@', 1) |
| 39 alternative_dirs_dict[target_path] = host_path |
| 40 (bucket_set, dumps) = SubCommand.load_basic_files( |
| 41 dump_path, True, alternative_dirs=alternative_dirs_dict) |
| 42 |
| 43 pfn_counts_dict = {} |
| 44 for shared_first_dump_path in shared_first_dump_paths: |
| 45 shared_dumps = SubCommand._find_all_dumps(shared_first_dump_path) |
| 46 for shared_dump in shared_dumps: |
| 47 pfn_counts = PFNCounts.load(shared_dump) |
| 48 if pfn_counts.pid not in pfn_counts_dict: |
| 49 pfn_counts_dict[pfn_counts.pid] = [] |
| 50 pfn_counts_dict[pfn_counts.pid].append(pfn_counts) |
| 51 |
| 52 policy_set = PolicySet.load(SubCommand._parse_policy_list(options.policy)) |
| 53 return policy_set, dumps, pfn_counts_dict, bucket_set |
| 54 |
| 55 @staticmethod |
| 56 def _apply_policy(dump, pfn_counts_dict, policy, bucket_set, first_dump_time): |
| 57 """Aggregates the total memory size of each component. |
| 58 |
| 59 Iterate through all stacktraces and attribute them to one of the components |
| 60 based on the policy. It is important to apply policy in right order. |
| 61 |
| 62 Args: |
| 63 dump: A Dump object. |
| 64 pfn_counts_dict: A dict mapping a pid to a list of PFNCounts. |
| 65 policy: A Policy object. |
| 66 bucket_set: A BucketSet object. |
| 67 first_dump_time: An integer representing time when the first dump is |
| 68 dumped. |
| 69 |
| 70 Returns: |
| 71 A dict mapping components and their corresponding sizes. |
| 72 """ |
| 73 LOGGER.info(' %s' % dump.path) |
| 74 all_pfn_dict = {} |
| 75 if pfn_counts_dict: |
| 76 LOGGER.info(' shared with...') |
| 77 for pid, pfnset_list in pfn_counts_dict.iteritems(): |
| 78 closest_pfnset_index = None |
| 79 closest_pfnset_difference = 1024.0 |
| 80 for index, pfnset in enumerate(pfnset_list): |
| 81 time_difference = pfnset.time - dump.time |
| 82 if time_difference >= 3.0: |
| 83 break |
| 84 elif ((time_difference < 0.0 and pfnset.reason != 'Exiting') or |
| 85 (0.0 <= time_difference and time_difference < 3.0)): |
| 86 closest_pfnset_index = index |
| 87 closest_pfnset_difference = time_difference |
| 88 elif time_difference < 0.0 and pfnset.reason == 'Exiting': |
| 89 closest_pfnset_index = None |
| 90 break |
| 91 if closest_pfnset_index: |
| 92 for pfn, count in pfnset_list[closest_pfnset_index].iter_pfn: |
| 93 all_pfn_dict[pfn] = all_pfn_dict.get(pfn, 0) + count |
| 94 LOGGER.info(' %s (time difference = %f)' % |
| 95 (pfnset_list[closest_pfnset_index].path, |
| 96 closest_pfnset_difference)) |
| 97 else: |
| 98 LOGGER.info(' (no match with pid:%d)' % pid) |
| 99 |
| 100 sizes = dict((c, 0) for c in policy.components) |
| 101 |
| 102 PolicyCommands._accumulate_malloc(dump, policy, bucket_set, sizes) |
| 103 verify_global_stats = PolicyCommands._accumulate_maps( |
| 104 dump, all_pfn_dict, policy, bucket_set, sizes) |
| 105 |
| 106 # TODO(dmikurube): Remove the verifying code when GLOBAL_STATS is removed. |
| 107 # http://crbug.com/245603. |
| 108 for verify_key, verify_value in verify_global_stats.iteritems(): |
| 109 dump_value = dump.global_stat('%s_committed' % verify_key) |
| 110 if dump_value != verify_value: |
| 111 LOGGER.warn('%25s: %12d != %d (%d)' % ( |
| 112 verify_key, dump_value, verify_value, dump_value - verify_value)) |
| 113 |
| 114 sizes['mmap-no-log'] = ( |
| 115 dump.global_stat('profiled-mmap_committed') - |
| 116 sizes['mmap-total-log']) |
| 117 sizes['mmap-total-record'] = dump.global_stat('profiled-mmap_committed') |
| 118 sizes['mmap-total-record-vm'] = dump.global_stat('profiled-mmap_virtual') |
| 119 |
| 120 sizes['tc-no-log'] = ( |
| 121 dump.global_stat('profiled-malloc_committed') - |
| 122 sizes['tc-total-log']) |
| 123 sizes['tc-total-record'] = dump.global_stat('profiled-malloc_committed') |
| 124 sizes['tc-unused'] = ( |
| 125 sizes['mmap-tcmalloc'] - |
| 126 dump.global_stat('profiled-malloc_committed')) |
| 127 if sizes['tc-unused'] < 0: |
| 128 LOGGER.warn(' Assuming tc-unused=0 as it is negative: %d (bytes)' % |
| 129 sizes['tc-unused']) |
| 130 sizes['tc-unused'] = 0 |
| 131 sizes['tc-total'] = sizes['mmap-tcmalloc'] |
| 132 |
| 133 # TODO(dmikurube): global_stat will be deprecated. |
| 134 # See http://crbug.com/245603. |
| 135 for key, value in { |
| 136 'total': 'total_committed', |
| 137 'filemapped': 'file_committed', |
| 138 'absent': 'absent_committed', |
| 139 'file-exec': 'file-exec_committed', |
| 140 'file-nonexec': 'file-nonexec_committed', |
| 141 'anonymous': 'anonymous_committed', |
| 142 'stack': 'stack_committed', |
| 143 'other': 'other_committed', |
| 144 'unhooked-absent': 'nonprofiled-absent_committed', |
| 145 'total-vm': 'total_virtual', |
| 146 'filemapped-vm': 'file_virtual', |
| 147 'anonymous-vm': 'anonymous_virtual', |
| 148 'other-vm': 'other_virtual' }.iteritems(): |
| 149 if key in sizes: |
| 150 sizes[key] = dump.global_stat(value) |
| 151 |
| 152 if 'mustbezero' in sizes: |
| 153 removed_list = ( |
| 154 'profiled-mmap_committed', |
| 155 'nonprofiled-absent_committed', |
| 156 'nonprofiled-anonymous_committed', |
| 157 'nonprofiled-file-exec_committed', |
| 158 'nonprofiled-file-nonexec_committed', |
| 159 'nonprofiled-stack_committed', |
| 160 'nonprofiled-other_committed') |
| 161 sizes['mustbezero'] = ( |
| 162 dump.global_stat('total_committed') - |
| 163 sum(dump.global_stat(removed) for removed in removed_list)) |
| 164 if 'total-exclude-profiler' in sizes: |
| 165 sizes['total-exclude-profiler'] = ( |
| 166 dump.global_stat('total_committed') - |
| 167 (sizes['mmap-profiler'] + sizes['mmap-type-profiler'])) |
| 168 if 'hour' in sizes: |
| 169 sizes['hour'] = (dump.time - first_dump_time) / 60.0 / 60.0 |
| 170 if 'minute' in sizes: |
| 171 sizes['minute'] = (dump.time - first_dump_time) / 60.0 |
| 172 if 'second' in sizes: |
| 173 sizes['second'] = dump.time - first_dump_time |
| 174 |
| 175 return sizes |
| 176 |
| 177 @staticmethod |
| 178 def _accumulate_malloc(dump, policy, bucket_set, sizes): |
| 179 for line in dump.iter_stacktrace: |
| 180 words = line.split() |
| 181 bucket = bucket_set.get(int(words[BUCKET_ID])) |
| 182 if not bucket or bucket.allocator_type == 'malloc': |
| 183 component_match = policy.find_malloc(bucket) |
| 184 elif bucket.allocator_type == 'mmap': |
| 185 continue |
| 186 else: |
| 187 assert False |
| 188 sizes[component_match] += int(words[COMMITTED]) |
| 189 |
| 190 assert not component_match.startswith('mmap-') |
| 191 if component_match.startswith('tc-'): |
| 192 sizes['tc-total-log'] += int(words[COMMITTED]) |
| 193 else: |
| 194 sizes['other-total-log'] += int(words[COMMITTED]) |
| 195 |
| 196 @staticmethod |
| 197 def _accumulate_maps(dump, pfn_dict, policy, bucket_set, sizes): |
| 198 # TODO(dmikurube): Remove the dict when GLOBAL_STATS is removed. |
| 199 # http://crbug.com/245603. |
| 200 global_stats = { |
| 201 'total': 0, |
| 202 'file-exec': 0, |
| 203 'file-nonexec': 0, |
| 204 'anonymous': 0, |
| 205 'stack': 0, |
| 206 'other': 0, |
| 207 'nonprofiled-file-exec': 0, |
| 208 'nonprofiled-file-nonexec': 0, |
| 209 'nonprofiled-anonymous': 0, |
| 210 'nonprofiled-stack': 0, |
| 211 'nonprofiled-other': 0, |
| 212 'profiled-mmap': 0, |
| 213 } |
| 214 |
| 215 for key, value in dump.iter_map: |
| 216 # TODO(dmikurube): Remove the subtotal code when GLOBAL_STATS is removed. |
| 217 # It's temporary verification code for transition described in |
| 218 # http://crbug.com/245603. |
| 219 committed = 0 |
| 220 if 'committed' in value[1]: |
| 221 committed = value[1]['committed'] |
| 222 global_stats['total'] += committed |
| 223 key = 'other' |
| 224 name = value[1]['vma']['name'] |
| 225 if name.startswith('/'): |
| 226 if value[1]['vma']['executable'] == 'x': |
| 227 key = 'file-exec' |
| 228 else: |
| 229 key = 'file-nonexec' |
| 230 elif name == '[stack]': |
| 231 key = 'stack' |
| 232 elif name == '': |
| 233 key = 'anonymous' |
| 234 global_stats[key] += committed |
| 235 if value[0] == 'unhooked': |
| 236 global_stats['nonprofiled-' + key] += committed |
| 237 if value[0] == 'hooked': |
| 238 global_stats['profiled-mmap'] += committed |
| 239 |
| 240 if value[0] == 'unhooked': |
| 241 if pfn_dict and dump.pageframe_length: |
| 242 for pageframe in value[1]['pageframe']: |
| 243 component_match = policy.find_unhooked(value, pageframe, pfn_dict) |
| 244 sizes[component_match] += pageframe.size |
| 245 else: |
| 246 component_match = policy.find_unhooked(value) |
| 247 sizes[component_match] += int(value[1]['committed']) |
| 248 elif value[0] == 'hooked': |
| 249 if pfn_dict and dump.pageframe_length: |
| 250 for pageframe in value[1]['pageframe']: |
| 251 component_match, _ = policy.find_mmap( |
| 252 value, bucket_set, pageframe, pfn_dict) |
| 253 sizes[component_match] += pageframe.size |
| 254 assert not component_match.startswith('tc-') |
| 255 if component_match.startswith('mmap-'): |
| 256 sizes['mmap-total-log'] += pageframe.size |
| 257 else: |
| 258 sizes['other-total-log'] += pageframe.size |
| 259 else: |
| 260 component_match, _ = policy.find_mmap(value, bucket_set) |
| 261 sizes[component_match] += int(value[1]['committed']) |
| 262 if component_match.startswith('mmap-'): |
| 263 sizes['mmap-total-log'] += int(value[1]['committed']) |
| 264 else: |
| 265 sizes['other-total-log'] += int(value[1]['committed']) |
| 266 else: |
| 267 LOGGER.error('Unrecognized mapping status: %s' % value[0]) |
| 268 |
| 269 return global_stats |
| 270 |
| 271 |
| 272 class CSVCommand(PolicyCommands): |
| 273 def __init__(self): |
| 274 super(CSVCommand, self).__init__('csv') |
| 275 |
| 276 def do(self, sys_argv): |
| 277 policy_set, dumps, pfn_counts_dict, bucket_set = self._set_up(sys_argv) |
| 278 return CSVCommand._output( |
| 279 policy_set, dumps, pfn_counts_dict, bucket_set, sys.stdout) |
| 280 |
| 281 @staticmethod |
| 282 def _output(policy_set, dumps, pfn_counts_dict, bucket_set, out): |
| 283 max_components = 0 |
| 284 for label in policy_set: |
| 285 max_components = max(max_components, len(policy_set[label].components)) |
| 286 |
| 287 for label in sorted(policy_set): |
| 288 components = policy_set[label].components |
| 289 if len(policy_set) > 1: |
| 290 out.write('%s%s\n' % (label, ',' * (max_components - 1))) |
| 291 out.write('%s%s\n' % ( |
| 292 ','.join(components), ',' * (max_components - len(components)))) |
| 293 |
| 294 LOGGER.info('Applying a policy %s to...' % label) |
| 295 for dump in dumps: |
| 296 component_sizes = PolicyCommands._apply_policy( |
| 297 dump, pfn_counts_dict, policy_set[label], bucket_set, dumps[0].time) |
| 298 s = [] |
| 299 for c in components: |
| 300 if c in ('hour', 'minute', 'second'): |
| 301 s.append('%05.5f' % (component_sizes[c])) |
| 302 else: |
| 303 s.append('%05.5f' % (component_sizes[c] / 1024.0 / 1024.0)) |
| 304 out.write('%s%s\n' % ( |
| 305 ','.join(s), ',' * (max_components - len(components)))) |
| 306 |
| 307 bucket_set.clear_component_cache() |
| 308 |
| 309 return 0 |
| 310 |
| 311 |
| 312 class JSONCommand(PolicyCommands): |
| 313 def __init__(self): |
| 314 super(JSONCommand, self).__init__('json') |
| 315 |
| 316 def do(self, sys_argv): |
| 317 policy_set, dumps, pfn_counts_dict, bucket_set = self._set_up(sys_argv) |
| 318 return JSONCommand._output( |
| 319 policy_set, dumps, pfn_counts_dict, bucket_set, sys.stdout) |
| 320 |
| 321 @staticmethod |
| 322 def _output(policy_set, dumps, pfn_counts_dict, bucket_set, out): |
| 323 json_base = { |
| 324 'version': 'JSON_DEEP_2', |
| 325 'policies': {}, |
| 326 } |
| 327 |
| 328 for label in sorted(policy_set): |
| 329 json_base['policies'][label] = { |
| 330 'legends': policy_set[label].components, |
| 331 'snapshots': [], |
| 332 } |
| 333 |
| 334 LOGGER.info('Applying a policy %s to...' % label) |
| 335 for dump in dumps: |
| 336 component_sizes = PolicyCommands._apply_policy( |
| 337 dump, pfn_counts_dict, policy_set[label], bucket_set, dumps[0].time) |
| 338 component_sizes['dump_path'] = dump.path |
| 339 component_sizes['dump_time'] = datetime.datetime.fromtimestamp( |
| 340 dump.time).strftime('%Y-%m-%d %H:%M:%S') |
| 341 json_base['policies'][label]['snapshots'].append(component_sizes) |
| 342 |
| 343 bucket_set.clear_component_cache() |
| 344 |
| 345 json.dump(json_base, out, indent=2, sort_keys=True) |
| 346 |
| 347 return 0 |
| 348 |
| 349 |
| 350 class ListCommand(PolicyCommands): |
| 351 def __init__(self): |
| 352 super(ListCommand, self).__init__('list') |
| 353 |
| 354 def do(self, sys_argv): |
| 355 policy_set, dumps, pfn_counts_dict, bucket_set = self._set_up(sys_argv) |
| 356 return ListCommand._output( |
| 357 policy_set, dumps, pfn_counts_dict, bucket_set, sys.stdout) |
| 358 |
| 359 @staticmethod |
| 360 def _output(policy_set, dumps, pfn_counts_dict, bucket_set, out): |
| 361 for label in sorted(policy_set): |
| 362 LOGGER.info('Applying a policy %s to...' % label) |
| 363 for dump in dumps: |
| 364 component_sizes = PolicyCommands._apply_policy( |
| 365 dump, pfn_counts_dict, policy_set[label], bucket_set, dump.time) |
| 366 out.write('%s for %s:\n' % (label, dump.path)) |
| 367 for c in policy_set[label].components: |
| 368 if c in ['hour', 'minute', 'second']: |
| 369 out.write('%40s %12.3f\n' % (c, component_sizes[c])) |
| 370 else: |
| 371 out.write('%40s %12d\n' % (c, component_sizes[c])) |
| 372 |
| 373 bucket_set.clear_component_cache() |
| 374 |
| 375 return 0 |
OLD | NEW |