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 376