├── Readme.md └── parse_swift_compile_times.py /Readme.md: -------------------------------------------------------------------------------- 1 | This script parses and aggregates Swift function body compile times from an 2 | Xcode build log. It can either print errors/warnings that Xcode will pick up 3 | from a Run Script build step, or tell you your worst N blocks. 4 | 5 | Tested exclusively with Swift 2.2 and Xcode 7.3.1. 6 | 7 | To use it, you need an Xcode build log generated by a build with a special 8 | setting: `OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies"`. 9 | 10 | You'll probably also want to clean all relevant targets first. 11 | 12 | You can create a target for use by a continuous integration server to report 13 | long compile times as errors by creating an 'aggregate' target and adding a 14 | Run Script phase like this: 15 | 16 | # xcodebuild subprocesses to rm, which prints errors we 17 | # don't care about at all 18 | exec 3>&2 19 | exec 2> /dev/null 20 | 21 | # save log to /tmp/ 22 | LOG_PATH=`mktemp /tmp/build.log.XXXXXX` || exit 1 23 | 24 | # call xcodebuild...from within Xcode. INCEPTION 25 | xcodebuild \ 26 | -workspace MyWorkspace.xcworkspace 27 | -scheme "My scheme" \ 28 | -sdk iphonesimulator9.3 \ # or whatever 29 | OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" \ 30 | > $LOG_PATH 31 | 32 | # now get our stderr back so we can print errors 33 | exec 2>&3 34 | 35 | # report all function body compile times >500ms as an error 36 | /path/to/parse_swift_compile_times.py --max-time=500 $LOG_PATH 37 | 38 | # save script return code 39 | e=$? 40 | 41 | # remove temp file 42 | rm $LOG_PATH 43 | 44 | # exit with script's return code, which will be 1 if there were errors 45 | exit $e 46 | -------------------------------------------------------------------------------- /parse_swift_compile_times.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | """ 4 | This script parses and aggregates Swift function body compile times from an 5 | Xcode build log. It can either print errors/warnings that Xcode will pick up 6 | from a Run Script build step, or tell you your worst N blocks. 7 | 8 | Tested exclusively with Swift 2.2 and Xcode 7.3.1. 9 | 10 | To use it, you need an Xcode build log generated by a build with a special 11 | setting: `OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies"`. 12 | 13 | You'll probably also want to clean all relevant targets first. 14 | 15 | You can create a target for use by a continuous integration server to report 16 | long compile times as errors by creating an 'aggregate' target and adding a 17 | Run Script phase like this: 18 | 19 | # xcodebuild subprocesses to rm, which prints errors we 20 | # don't care about at all 21 | exec 3>&2 22 | exec 2> /dev/null 23 | 24 | # save log to /tmp/ 25 | LOG_PATH=`mktemp /tmp/build.log.XXXXXX` || exit 1 26 | 27 | # call xcodebuild...from within Xcode. INCEPTION 28 | xcodebuild \ 29 | -workspace MyWorkspace.xcworkspace 30 | -scheme "My scheme" \ 31 | -sdk iphonesimulator9.3 \ # or whatever 32 | OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" \ 33 | > $LOG_PATH 34 | 35 | # now get our stderr back so we can print errors 36 | exec 2>&3 37 | 38 | # report all function body compile times >500ms as an error 39 | /path/to/parse_swift_compile_times.py --max-time=500 $LOG_PATH 40 | 41 | # save script return code 42 | e=$? 43 | 44 | # remove temp file 45 | rm $LOG_PATH 46 | 47 | # exit with script's return code, which will be 1 if there were errors 48 | exit $e 49 | """ 50 | 51 | from __future__ import print_function 52 | 53 | import argparse 54 | import logging 55 | import re 56 | import sys 57 | 58 | from collections import namedtuple 59 | 60 | 61 | TIMING_LINE_RE = re.compile(r'(?P\d+\.\d+)ms\s+(?P[^:]+):(?P\d+):(?P\d+)\s+(?P.+)') 62 | Location = namedtuple('Location', ['path', 'line', 'col', 'name']) 63 | 64 | 65 | logging.basicConfig(level=logging.INFO) 66 | log = logging.getLogger('parse_build_times') 67 | 68 | 69 | class Measurement(object): 70 | def __init__(self): 71 | self.blocks = {} 72 | super(Measurement, self).__init__() 73 | 74 | def record(self, ms, path, line, col, name): 75 | location = Location(path, line, col, name) 76 | self.blocks.setdefault(location, 0) 77 | self.blocks[location] += float(ms) 78 | 79 | def print_worst_blocks(self, n=10): 80 | print('Worst blocks:') 81 | sorted_pairs = sorted(self.blocks.items(), key=lambda pair: -pair[1]) 82 | for (location, time) in sorted_pairs[:n]: 83 | print(' {}ms - {}:{}:{}'.format(time, location.path, location.line, location.name)) 84 | 85 | def print_error_for_times_greater_than(self, max_time, severity='error'): 86 | had_error = False 87 | for location, ms in self.blocks.iteritems(): 88 | if ms > max_time: 89 | print("{}:{}:{}: {}: {}ms compile time".format( 90 | location.path, location.line, location.col, severity, ms)) 91 | had_error = True 92 | return had_error 93 | 94 | 95 | def main(): 96 | parser = argparse.ArgumentParser(__doc__) 97 | parser.add_argument('infile', type=argparse.FileType('r'), default=sys.stdin) 98 | parser.add_argument( 99 | '-n', default=None, type=int, 100 | help="If provided, print the worst N blocks") 101 | parser.add_argument( 102 | '-t', '--max-time', default=None, type=int, 103 | help="If provided, print a line that will cause Xcode to add an error pointing to the relevant location") 104 | parser.add_argument( 105 | '--just-warn', 106 | help='(used with --max-time) Signal a warning instead of an error to Xcode') 107 | args = parser.parse_args() 108 | 109 | measurement = Measurement() 110 | for line in args.infile: 111 | m = TIMING_LINE_RE.match(line) 112 | if m: 113 | measurement.record(**m.groupdict()) 114 | 115 | if args.n is not None: 116 | measurement.print_worst_blocks(args.n) 117 | 118 | if args.max_time is not None: 119 | has_error = measurement.print_error_for_times_greater_than( 120 | args.max_time, "warning" if args.just_warn else "error") 121 | if has_error: 122 | return 1 123 | 124 | return 0 125 | 126 | 127 | if __name__ == '__main__': 128 | sys.exit(main() or 0) 129 | --------------------------------------------------------------------------------