1#!/usr/bin/env python 2 3"""Calculate the costs of compiling headers; by considering how many 4times each header is included compared to the time to compile it. 5 6Requires two files: 7 81. Dependancy information for each target, so we can determine how 9 many times each header is included. For example if using Ninja: 10 11 $ ninja -t deps > deps.txt 12 132. List of costs of compiling each target. For example if using Ninja: 14 15 show_ninja_build_stats < .ninja_log > costs.txt 16 17Output: 18 19Prints a list of all headers which have both cost and dependancy 20information, of the form: 21 22 total_time, header, count, cost_to_compile 23 24total_time: 'count' * 'cost'. This attempts to measure the overall 25 cost of this header across the whole build. 26header: Path to header 27count: Number of targets which depend on this header (i.e. how many 28 times it is included) 29cost_to_compile: time in seconds to compile this header. 30 31 32By sorting by the first column one can identify potential build 33hotspots. Either reduce the number of times they are included; or the 34cost to compile to minimise the overall impact. 35""" 36 37from __future__ import print_function 38import collections 39import os 40import re 41import sys 42 43if len(sys.argv) != 3: 44 print("Usage: <deps_file> <header costs>", file=sys.stderr) 45 sys.exit(1) 46 47headers = collections.defaultdict(dict) 48 49with open(sys.argv[1]) as deps: 50 for line in deps: 51 # File consists of a paragraph for each target. Each paragraph 52 # is of the form: 53 # 54 # relative/path/to/target.cc.o: #deps ... extra details 55 # ../path/to/dependancy1.cc 56 # /path/to/dependancy2.h 57 # <blank line> 58 if '#deps' in line: 59 # Target name 60 (target, _x) = line.split(':', 1) 61 # Ignore targets which we don't want to count dependancies 62 # for - such as the '.h.cc' fake targets. 63 if target.endswith('.h.cc.o'): 64 target = None 65 elif line[0] != '\n': 66 # Dependancy name 67 dep = line.strip() 68 # Remove any '../XXX/' prefix (due to source -> build path 69 # conversion). 70 # Assumes that the build directory is located inside the source. 71 if dep.startswith('../'): 72 dep = dep[3:] 73 if 'count' not in headers[dep]: 74 headers[dep]['count'] = 0 75 headers[dep]['count'] += 1 76 if (dep.startswith('/usr/include/') or 77 dep.startswith('/Applications/Xcode.app')): 78 headers[dep]['system'] = True 79 else: 80 # Paragraph (target) separator. 81 target = None 82 83with open(sys.argv[2]) as costs: 84 for line in costs: 85 (cost, target) = line.split() 86 # Ignore the building of the .h.cc - that's just the cost to 87 # create a symlink. 88 if target.endswith('.h.cc'): 89 continue 90 # Fixup name of the .h.cc.o fake targets -> .h 91 if target.endswith('.h.cc.o'): 92 target = target[:-5] 93 target = re.sub('CMakeFiles/.*_obj.dir/', '', target) 94 headers[target]['cost'] = float(cost) 95 96for k,v in headers.items(): 97 if 'count' in v: 98 if 'cost' in v: 99 total_cost = v['count'] * v['cost'] 100 print(total_cost, k, v['count'], v['cost']) 101 else: 102 # No cost value - print warming if this is a high count 103 # header (and not system) 104 if v['count'] > 100 and 'system' not in v: 105 print(("Warning: No cost value for '{}' but has #include " + 106 "count of {}").format(k, v['count']), 107 file=sys.stderr) 108