OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """The deep heap profiler script for Chrome.""" | 6 """The deep heap profiler script for Chrome.""" |
7 | 7 |
8 from datetime import datetime | 8 from datetime import datetime |
9 import json | 9 import json |
10 import os | 10 import os |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
59 # POLICY_DEEP_2 DOES include allocation_type columns. | 59 # POLICY_DEEP_2 DOES include allocation_type columns. |
60 # mmap regions are distincted w/ the allocation_type column. | 60 # mmap regions are distincted w/ the allocation_type column. |
61 POLICY_DEEP_2 = 'POLICY_DEEP_2' | 61 POLICY_DEEP_2 = 'POLICY_DEEP_2' |
62 | 62 |
63 # TODO(dmikurube): Avoid global variables. | 63 # TODO(dmikurube): Avoid global variables. |
64 address_symbol_dict = {} | 64 address_symbol_dict = {} |
65 appeared_addresses = set() | 65 appeared_addresses = set() |
66 components = [] | 66 components = [] |
67 | 67 |
68 | 68 |
| 69 class ParsingException(Exception): |
| 70 def __init__(self, value): |
| 71 self.value = value |
| 72 def __str__(self): |
| 73 return repr(self.value) |
| 74 |
| 75 |
69 class Policy(object): | 76 class Policy(object): |
70 | 77 |
71 def __init__(self, name, mmap, pattern): | 78 def __init__(self, name, mmap, pattern): |
72 self.name = name | 79 self.name = name |
73 self.mmap = mmap | 80 self.mmap = mmap |
74 self.condition = re.compile(pattern + r'\Z') | 81 self.condition = re.compile(pattern + r'\Z') |
75 | 82 |
76 | 83 |
77 def get_component(policy_list, bucket, mmap): | 84 def get_component(policy_list, bucket, mmap): |
78 """Returns a component name which a given bucket belongs to. | 85 """Returns a component name which a given bucket belongs to. |
(...skipping 26 matching lines...) Expand all Loading... |
105 class Bucket(object): | 112 class Bucket(object): |
106 | 113 |
107 def __init__(self, stacktrace): | 114 def __init__(self, stacktrace): |
108 self.stacktrace = stacktrace | 115 self.stacktrace = stacktrace |
109 self.component = '' | 116 self.component = '' |
110 | 117 |
111 | 118 |
112 class Log(object): | 119 class Log(object): |
113 | 120 |
114 """A class representing one dumped log data.""" | 121 """A class representing one dumped log data.""" |
115 def __init__(self, log_path, buckets): | 122 def __init__(self, log_path): |
116 self.log_path = log_path | 123 self.log_path = log_path |
117 self.log_lines = [ | 124 self.log_lines = [ |
118 l for l in open(self.log_path, 'r') if l and not l.startswith('#')] | 125 l for l in open(self.log_path, 'r') if l and not l.startswith('#')] |
119 self.log_version = '' | 126 self.log_version = '' |
120 sys.stderr.write('parsing a log file:%s\n' % log_path) | 127 sys.stderr.write('Loading a dump: %s\n' % log_path) |
121 self.mmap_stacktrace_lines = [] | 128 self.mmap_stacktrace_lines = [] |
122 self.malloc_stacktrace_lines = [] | 129 self.malloc_stacktrace_lines = [] |
123 self.counters = {} | 130 self.counters = {} |
124 self.log_time = os.stat(self.log_path).st_mtime | 131 self.log_time = os.stat(self.log_path).st_mtime |
125 self.parse_log(buckets) | |
126 | 132 |
127 @staticmethod | 133 @staticmethod |
128 def dump_stacktrace_lines(stacktrace_lines, buckets): | 134 def dump_stacktrace_lines(stacktrace_lines, buckets): |
129 """Prints a given stacktrace. | 135 """Prints a given stacktrace. |
130 | 136 |
131 Args: | 137 Args: |
132 stacktrace_lines: A list of strings which are valid as stacktraces. | 138 stacktrace_lines: A list of strings which are valid as stacktraces. |
133 buckets: A dict mapping bucket ids and their corresponding Bucket | 139 buckets: A dict mapping bucket ids and their corresponding Bucket |
134 objects. | 140 objects. |
135 """ | 141 """ |
(...skipping 136 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
272 return False | 278 return False |
273 bucket = buckets.get(int(words[BUCKET_ID])) | 279 bucket = buckets.get(int(words[BUCKET_ID])) |
274 if bucket: | 280 if bucket: |
275 for address in bucket.stacktrace: | 281 for address in bucket.stacktrace: |
276 appeared_addresses.add(address) | 282 appeared_addresses.add(address) |
277 return True | 283 return True |
278 | 284 |
279 @staticmethod | 285 @staticmethod |
280 def skip_lines_while(line_number, max_line_number, skipping_condition): | 286 def skip_lines_while(line_number, max_line_number, skipping_condition): |
281 """Increments line_number until skipping_condition(line_number) is false. | 287 """Increments line_number until skipping_condition(line_number) is false. |
| 288 |
| 289 Returns: |
| 290 A pair of an integer indicating a line number after skipped, and a |
| 291 boolean value which is True if found a line which skipping_condition |
| 292 is False for. |
282 """ | 293 """ |
283 while skipping_condition(line_number): | 294 while skipping_condition(line_number): |
284 line_number += 1 | 295 line_number += 1 |
285 if line_number >= max_line_number: | 296 if line_number >= max_line_number: |
286 sys.stderr.write('invalid heap profile dump.') | 297 return line_number, False |
287 return line_number | 298 return line_number, True |
288 return line_number | |
289 | 299 |
290 def parse_stacktraces_while_valid(self, buckets, log_lines, line_number): | 300 def parse_stacktraces_while_valid(self, buckets, log_lines, line_number): |
291 """Parses stacktrace lines while the lines are valid. | 301 """Parses stacktrace lines while the lines are valid. |
292 | 302 |
293 Args: | 303 Args: |
294 buckets: A dict mapping bucket ids and their corresponding Bucket | 304 buckets: A dict mapping bucket ids and their corresponding Bucket |
295 objects. | 305 objects. |
296 log_lines: A list of lines to be parsed. | 306 log_lines: A list of lines to be parsed. |
297 line_number: An integer representing the starting line number in | 307 line_number: An integer representing the starting line number in |
298 log_lines. | 308 log_lines. |
299 | 309 |
300 Returns: | 310 Returns: |
301 A pair of a list of valid lines and an integer representing the last | 311 A pair of a list of valid lines and an integer representing the last |
302 line number in log_lines. | 312 line number in log_lines. |
303 """ | 313 """ |
304 line_number = self.skip_lines_while( | 314 (line_number, _) = self.skip_lines_while( |
305 line_number, len(log_lines), | 315 line_number, len(log_lines), |
306 lambda n: not log_lines[n].split()[0].isdigit()) | 316 lambda n: not log_lines[n].split()[0].isdigit()) |
307 stacktrace_lines_start = line_number | 317 stacktrace_lines_start = line_number |
308 line_number = self.skip_lines_while( | 318 (line_number, _) = self.skip_lines_while( |
309 line_number, len(log_lines), | 319 line_number, len(log_lines), |
310 lambda n: self.check_stacktrace_line(log_lines[n], buckets)) | 320 lambda n: self.check_stacktrace_line(log_lines[n], buckets)) |
311 return (log_lines[stacktrace_lines_start:line_number], line_number) | 321 return (log_lines[stacktrace_lines_start:line_number], line_number) |
312 | 322 |
313 def parse_stacktraces(self, buckets, line_number): | 323 def parse_stacktraces(self, buckets, line_number): |
314 """Parses lines in self.log_lines as stacktrace. | 324 """Parses lines in self.log_lines as stacktrace. |
315 | 325 |
316 Valid stacktrace lines are stored into self.mmap_stacktrace_lines and | 326 Valid stacktrace lines are stored into self.mmap_stacktrace_lines and |
317 self.malloc_stacktrace_lines. | 327 self.malloc_stacktrace_lines. |
318 | 328 |
319 Args: | 329 Args: |
320 buckets: A dict mapping bucket ids and their corresponding Bucket | 330 buckets: A dict mapping bucket ids and their corresponding Bucket |
321 objects. | 331 objects. |
322 line_number: An integer representing the starting line number in | 332 line_number: An integer representing the starting line number in |
323 log_lines. | 333 log_lines. |
324 | 334 |
325 Raises: | 335 Raises: |
326 RuntimeException for invalid dump versions. | 336 ParsingException for invalid dump versions. |
327 """ | 337 """ |
328 sys.stderr.write(' heap profile dump version: %s\n' % self.log_version) | 338 sys.stderr.write(' Version: %s\n' % self.log_version) |
329 | 339 |
330 if self.log_version in (DUMP_DEEP_3, DUMP_DEEP_4): | 340 if self.log_version in (DUMP_DEEP_3, DUMP_DEEP_4): |
331 (self.mmap_stacktrace_lines, line_number) = ( | 341 (self.mmap_stacktrace_lines, line_number) = ( |
332 self.parse_stacktraces_while_valid( | 342 self.parse_stacktraces_while_valid( |
333 buckets, self.log_lines, line_number)) | 343 buckets, self.log_lines, line_number)) |
334 line_number = self.skip_lines_while( | 344 (line_number, _) = self.skip_lines_while( |
335 line_number, len(self.log_lines), | 345 line_number, len(self.log_lines), |
336 lambda n: self.log_lines[n] != 'MALLOC_STACKTRACES:\n') | 346 lambda n: self.log_lines[n] != 'MALLOC_STACKTRACES:\n') |
337 (self.malloc_stacktrace_lines, line_number) = ( | 347 (self.malloc_stacktrace_lines, line_number) = ( |
338 self.parse_stacktraces_while_valid( | 348 self.parse_stacktraces_while_valid( |
339 buckets, self.log_lines, line_number)) | 349 buckets, self.log_lines, line_number)) |
340 | 350 |
341 elif self.log_version == DUMP_DEEP_2: | 351 elif self.log_version == DUMP_DEEP_2: |
342 (self.mmap_stacktrace_lines, line_number) = ( | 352 (self.mmap_stacktrace_lines, line_number) = ( |
343 self.parse_stacktraces_while_valid( | 353 self.parse_stacktraces_while_valid( |
344 buckets, self.log_lines, line_number)) | 354 buckets, self.log_lines, line_number)) |
345 line_number = self.skip_lines_while( | 355 (line_number, _) = self.skip_lines_while( |
346 line_number, len(self.log_lines), | 356 line_number, len(self.log_lines), |
347 lambda n: self.log_lines[n] != 'MALLOC_STACKTRACES:\n') | 357 lambda n: self.log_lines[n] != 'MALLOC_STACKTRACES:\n') |
348 (self.malloc_stacktrace_lines, line_number) = ( | 358 (self.malloc_stacktrace_lines, line_number) = ( |
349 self.parse_stacktraces_while_valid( | 359 self.parse_stacktraces_while_valid( |
350 buckets, self.log_lines, line_number)) | 360 buckets, self.log_lines, line_number)) |
351 self.malloc_stacktrace_lines.extend(self.mmap_stacktrace_lines) | 361 self.malloc_stacktrace_lines.extend(self.mmap_stacktrace_lines) |
352 self.mmap_stacktrace_lines = [] | 362 self.mmap_stacktrace_lines = [] |
353 | 363 |
354 elif self.log_version == DUMP_DEEP_1: | 364 elif self.log_version == DUMP_DEEP_1: |
355 (self.malloc_stacktrace_lines, line_number) = ( | 365 (self.malloc_stacktrace_lines, line_number) = ( |
356 self.parse_stacktraces_while_valid( | 366 self.parse_stacktraces_while_valid( |
357 buckets, self.log_lines, line_number)) | 367 buckets, self.log_lines, line_number)) |
358 | 368 |
359 else: | 369 else: |
360 raise RuntimeError('invalid heap profile dump version: %s' % ( | 370 raise ParsingException('invalid heap profile dump version: %s' % ( |
361 self.log_version)) | 371 self.log_version)) |
362 | 372 |
363 def parse_global_stats(self): | 373 def parse_global_stats(self): |
364 """Parses lines in self.log_lines as global stats.""" | 374 """Parses lines in self.log_lines as global stats.""" |
365 ln = self.skip_lines_while( | 375 (ln, _) = self.skip_lines_while( |
366 0, len(self.log_lines), | 376 0, len(self.log_lines), |
367 lambda n: self.log_lines[n] != 'GLOBAL_STATS:\n') | 377 lambda n: self.log_lines[n] != 'GLOBAL_STATS:\n') |
368 | 378 |
369 if self.log_version == DUMP_DEEP_4: | 379 if self.log_version == DUMP_DEEP_4: |
370 global_stat_names = [ | 380 global_stat_names = [ |
371 'total', 'file-exec', 'file-nonexec', 'anonymous', 'stack', 'other', | 381 'total', 'file-exec', 'file-nonexec', 'anonymous', 'stack', 'other', |
372 'nonprofiled-absent', 'nonprofiled-anonymous', | 382 'nonprofiled-absent', 'nonprofiled-anonymous', |
373 'nonprofiled-file-exec', 'nonprofiled-file-nonexec', | 383 'nonprofiled-file-exec', 'nonprofiled-file-nonexec', |
374 'nonprofiled-stack', 'nonprofiled-other', | 384 'nonprofiled-stack', 'nonprofiled-other', |
375 'profiled-mmap', 'profiled-malloc'] | 385 'profiled-mmap', 'profiled-malloc'] |
376 else: | 386 else: |
377 global_stat_names = [ | 387 global_stat_names = [ |
378 'total', 'file', 'anonymous', 'other', 'mmap', 'tcmalloc'] | 388 'total', 'file', 'anonymous', 'other', 'mmap', 'tcmalloc'] |
379 | 389 |
380 for prefix in global_stat_names: | 390 for prefix in global_stat_names: |
381 ln = self.skip_lines_while( | 391 (ln, _) = self.skip_lines_while( |
382 ln, len(self.log_lines), | 392 ln, len(self.log_lines), |
383 lambda n: self.log_lines[n].split()[0] != prefix) | 393 lambda n: self.log_lines[n].split()[0] != prefix) |
384 words = self.log_lines[ln].split() | 394 words = self.log_lines[ln].split() |
385 self.counters[prefix + '_virtual'] = int(words[-2]) | 395 self.counters[prefix + '_virtual'] = int(words[-2]) |
386 self.counters[prefix + '_committed'] = int(words[-1]) | 396 self.counters[prefix + '_committed'] = int(words[-1]) |
387 | 397 |
388 def parse_version(self): | 398 def parse_version(self): |
389 """Parses a version string in self.log_lines. | 399 """Parses a version string in self.log_lines. |
390 | 400 |
391 Returns: | 401 Returns: |
392 A pair of (a string representing a version of the stacktrace dump, | 402 A pair of (a string representing a version of the stacktrace dump, |
393 and an integer indicating a line number next to the version string). | 403 and an integer indicating a line number next to the version string). |
394 | 404 |
395 Raises: | 405 Raises: |
396 RuntimeException for invalid dump versions. | 406 ParsingException for invalid dump versions. |
397 """ | 407 """ |
398 version = '' | 408 version = '' |
399 | 409 |
400 # Skip until an identifiable line. | 410 # Skip until an identifiable line. |
401 headers = ('STACKTRACES:\n', 'MMAP_STACKTRACES:\n', 'heap profile: ') | 411 headers = ('STACKTRACES:\n', 'MMAP_STACKTRACES:\n', 'heap profile: ') |
402 ln = self.skip_lines_while( | 412 if not self.log_lines: |
| 413 raise ParsingException('Empty heap dump file.') |
| 414 (ln, found) = self.skip_lines_while( |
403 0, len(self.log_lines), | 415 0, len(self.log_lines), |
404 lambda n: not self.log_lines[n].startswith(headers)) | 416 lambda n: not self.log_lines[n].startswith(headers)) |
| 417 if not found: |
| 418 raise ParsingException('Invalid heap dump file (no version header).') |
405 | 419 |
406 # Identify a version. | 420 # Identify a version. |
407 if self.log_lines[ln].startswith('heap profile: '): | 421 if self.log_lines[ln].startswith('heap profile: '): |
408 version = self.log_lines[ln][13:].strip() | 422 version = self.log_lines[ln][13:].strip() |
409 if (version == DUMP_DEEP_2 or version == DUMP_DEEP_3 or | 423 if (version == DUMP_DEEP_2 or version == DUMP_DEEP_3 or |
410 version == DUMP_DEEP_4): | 424 version == DUMP_DEEP_4): |
411 ln = self.skip_lines_while( | 425 (ln, _) = self.skip_lines_while( |
412 ln, len(self.log_lines), | 426 ln, len(self.log_lines), |
413 lambda n: self.log_lines[n] != 'MMAP_STACKTRACES:\n') | 427 lambda n: self.log_lines[n] != 'MMAP_STACKTRACES:\n') |
414 else: | 428 else: |
415 raise RuntimeError('invalid heap profile dump version: %s' % version) | 429 raise ParsingException('invalid heap profile dump version: %s' |
| 430 % version) |
416 elif self.log_lines[ln] == 'STACKTRACES:\n': | 431 elif self.log_lines[ln] == 'STACKTRACES:\n': |
417 version = DUMP_DEEP_1 | 432 version = DUMP_DEEP_1 |
418 elif self.log_lines[ln] == 'MMAP_STACKTRACES:\n': | 433 elif self.log_lines[ln] == 'MMAP_STACKTRACES:\n': |
419 version = DUMP_DEEP_2 | 434 version = DUMP_DEEP_2 |
420 | 435 |
421 return (version, ln) | 436 return (version, ln) |
422 | 437 |
423 def parse_log(self, buckets): | 438 def parse_log(self, buckets): |
424 self.log_version, ln = self.parse_version() | 439 self.log_version, ln = self.parse_version() |
425 self.parse_global_stats() | 440 self.parse_global_stats() |
(...skipping 313 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
739 break | 754 break |
740 n += 1 | 755 n += 1 |
741 continue | 756 continue |
742 sys.stderr.write('reading buckets from %s\n' % (buckets_path)) | 757 sys.stderr.write('reading buckets from %s\n' % (buckets_path)) |
743 with open(buckets_path, 'r') as buckets_f: | 758 with open(buckets_path, 'r') as buckets_f: |
744 for l in buckets_f: | 759 for l in buckets_f: |
745 words = l.split() | 760 words = l.split() |
746 buckets[int(words[0])] = Bucket(words[1:]) | 761 buckets[int(words[0])] = Bucket(words[1:]) |
747 n += 1 | 762 n += 1 |
748 | 763 |
749 sys.stderr.write('the number buckets: %d\n' % (bucket_count)) | |
750 | |
751 log_path_list = [log_path] | 764 log_path_list = [log_path] |
752 | 765 |
753 if action in ('--csv', '--json'): | 766 if action in ('--csv', '--json'): |
754 # search for the sequence of files | 767 # search for the sequence of files |
755 n = int(log_path[len(log_path) - 9 : len(log_path) - 5]) | 768 n = int(log_path[len(log_path) - 9 : len(log_path) - 5]) |
756 n += 1 # skip current file | 769 n += 1 # skip current file |
757 while True: | 770 while True: |
758 p = '%s.%04d.heap' % (prefix, n) | 771 p = '%s.%04d.heap' % (prefix, n) |
759 if os.path.exists(p): | 772 if os.path.exists(p): |
760 log_path_list.append(p) | 773 log_path_list.append(p) |
761 else: | 774 else: |
762 break | 775 break |
763 n += 1 | 776 n += 1 |
764 | 777 |
765 logs = [Log(path, buckets) for path in log_path_list] | 778 logs = [] |
| 779 for path in log_path_list: |
| 780 new_log = Log(path) |
| 781 sys.stderr.write('Parsing a dump: %s\n' % path) |
| 782 try: |
| 783 new_log.parse_log(buckets) |
| 784 except ParsingException: |
| 785 sys.stderr.write(' Ignored an invalid dump: %s\n' % path) |
| 786 else: |
| 787 logs.append(new_log) |
766 | 788 |
767 sys.stderr.write('getting symbols\n') | 789 sys.stderr.write('getting symbols\n') |
768 update_symbols(symbol_path, maps_lines, chrome_path) | 790 update_symbols(symbol_path, maps_lines, chrome_path) |
769 | 791 |
770 # TODO(dmikurube): Many modes now. Split them into separete functions. | 792 # TODO(dmikurube): Many modes now. Split them into separete functions. |
771 if action == '--stacktrace': | 793 if action == '--stacktrace': |
772 logs[0].dump_stacktrace(buckets) | 794 logs[0].dump_stacktrace(buckets) |
773 | 795 |
774 elif action == '--csv': | 796 elif action == '--csv': |
775 sys.stdout.write(','.join(components)) | 797 sys.stdout.write(','.join(components)) |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
817 | 839 |
818 elif action == '--pprof': | 840 elif action == '--pprof': |
819 if len(sys.argv) > 5: | 841 if len(sys.argv) > 5: |
820 logs[0].dump_for_pprof(policy_list, buckets, maps_lines, sys.argv[5]) | 842 logs[0].dump_for_pprof(policy_list, buckets, maps_lines, sys.argv[5]) |
821 else: | 843 else: |
822 logs[0].dump_for_pprof(policy_list, buckets, maps_lines, None) | 844 logs[0].dump_for_pprof(policy_list, buckets, maps_lines, None) |
823 | 845 |
824 | 846 |
825 if __name__ == '__main__': | 847 if __name__ == '__main__': |
826 sys.exit(main()) | 848 sys.exit(main()) |
OLD | NEW |