|
| 1 | +#!/usr/bin/env python |
| 2 | +# utils/coverage-build-db - Build sqlite3 database from profdata |
| 3 | +# |
| 4 | +# This source file is part of the Swift.org open source project |
| 5 | +# |
| 6 | +# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors |
| 7 | +# Licensed under Apache License v2.0 with Runtime Library Exception |
| 8 | +# |
| 9 | +# See http://swift.org/LICENSE.txt for license information |
| 10 | +# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 11 | + |
| 12 | +import Queue |
| 13 | +import argparse |
| 14 | +import logging |
| 15 | +import multiprocessing |
| 16 | +import os |
| 17 | +import re |
| 18 | +import sqlite3 |
| 19 | +import sys |
| 20 | + |
| 21 | +logging_format = '%(asctime)s %(levelname)s %(message)s' |
| 22 | +logging.basicConfig(level=logging.DEBUG, |
| 23 | + format=logging_format, |
| 24 | + filename='/tmp/%s.log' % os.path.basename(__file__), |
| 25 | + filemode='w') |
| 26 | +console = logging.StreamHandler() |
| 27 | +console.setLevel(logging.INFO) |
| 28 | +formatter = logging.Formatter(logging_format) |
| 29 | +console.setFormatter(formatter) |
| 30 | +logging.getLogger().addHandler(console) |
| 31 | + |
| 32 | + |
| 33 | +def parse_coverage_log(filepath): |
| 34 | + """Return parsed coverage intervals from `llvm-profdata show` output at |
| 35 | + `filepath`""" |
| 36 | + logging.info('Parsing: %s', filepath) |
| 37 | + test = os.path.basename(os.path.dirname(filepath)).rsplit('.', 1)[0] |
| 38 | + with open(filepath) as f: |
| 39 | + # Initial parse |
| 40 | + parsed = [] |
| 41 | + for section in [section.split("\n") |
| 42 | + for section in f.read().split("\n\n")]: |
| 43 | + if ".cpp:" in section[0] or ".h:" in section[0]: |
| 44 | + parsed_section = [section[0].split(":", 1) + [test]] |
| 45 | + for line in section[1:]: |
| 46 | + linedata = linedata_re.match(line).group(1, 2) |
| 47 | + parsed_section.append(linedata) |
| 48 | + parsed.append(parsed_section) |
| 49 | + # Build intervals |
| 50 | + parsed_interval = [] |
| 51 | + for section in parsed: |
| 52 | + new_section = [section[0]] |
| 53 | + i = 1 |
| 54 | + if len(section) > 2: |
| 55 | + while i < len(section)-1: |
| 56 | + while i < len(section)-1 and section[i][0] in ['0', None]: |
| 57 | + i += 1 |
| 58 | + if i == len(section)-1: |
| 59 | + if section[i][0] not in ['0', None]: |
| 60 | + istart = int(section[i][1]) |
| 61 | + iend = istart |
| 62 | + new_section.append((istart, iend)) |
| 63 | + break |
| 64 | + istart = int(section[i][1]) |
| 65 | + iend = istart # single line interval starting |
| 66 | + while i < len(section)-1 and \ |
| 67 | + int(section[i+1][1]) == iend + 1 and \ |
| 68 | + section[i+1][0] not in ['0', None]: |
| 69 | + iend += 1 |
| 70 | + i += 1 |
| 71 | + new_section.append((istart, iend)) |
| 72 | + i += 1 |
| 73 | + else: |
| 74 | + if section[i][0] not in ['0', None]: |
| 75 | + istart = int(section[i][1]) |
| 76 | + iend = istart |
| 77 | + new_section.append((istart, iend)) |
| 78 | + parsed_interval.append(new_section) |
| 79 | + return parsed_interval |
| 80 | + |
| 81 | + |
| 82 | +def worker(args): |
| 83 | + """Parse coverage log at path `args[0]` and place in queue `args[1]`""" |
| 84 | + args[1].put(parse_coverage_log(args[0])) |
| 85 | + |
| 86 | +manager = multiprocessing.Manager() |
| 87 | +result_queue = manager.Queue() |
| 88 | +worker_queue = [] |
| 89 | + |
| 90 | +linedata_re = re.compile(r"^\s*([^\s]+?)?\|\s*(\d+)\|") |
| 91 | +strings = dict() |
| 92 | + |
| 93 | +pool = multiprocessing.Pool(3) |
| 94 | + |
| 95 | + |
| 96 | +def insert_coverage_section_into_db(db_cursor, section): |
| 97 | + """Insert parsed intervals in `section` into database at `db_cursor`""" |
| 98 | + filename = section[0][0] |
| 99 | + function = section[0][1] |
| 100 | + test = section[0][2] |
| 101 | + logging.debug('Inserting section into database for file: %s', filename) |
| 102 | + if filename not in strings: |
| 103 | + db_cursor.execute("INSERT INTO strings (string) VALUES (?)", |
| 104 | + (filename,)) |
| 105 | + strings[filename] = db_cursor.lastrowid |
| 106 | + if test not in strings: |
| 107 | + db_cursor.execute("INSERT INTO strings (string) VALUES (?)", (test,)) |
| 108 | + strings[test] = db_cursor.lastrowid |
| 109 | + if function not in strings: |
| 110 | + db_cursor.execute("INSERT INTO strings (string) VALUES (?)", |
| 111 | + (function,)) |
| 112 | + strings[function] = db_cursor.lastrowid |
| 113 | + for istart, iend in section[1:]: |
| 114 | + logging.debug('%s, %s, %s, %s, %s', function, filename, test, istart, |
| 115 | + iend) |
| 116 | + db_cursor.execute("INSERT INTO coverage VALUES (?,?,?,?,?)", |
| 117 | + (strings[function], strings[filename], |
| 118 | + strings[test], istart, iend)) |
| 119 | + |
| 120 | + |
| 121 | +def main(): |
| 122 | + parser = argparse.ArgumentParser( |
| 123 | + description='Build sqlite3 database from profdata') |
| 124 | + parser.add_argument('root_directory', |
| 125 | + metavar='root-directory', |
| 126 | + help='a root directory to recursively search for ' |
| 127 | + 'coverage logs') |
| 128 | + parser.add_argument('--coverage-filename', |
| 129 | + help='a coverage log file name to search for ' |
| 130 | + '(default: coverage.log.demangled)', |
| 131 | + metavar='NAME', |
| 132 | + default='coverage.log.demangled') |
| 133 | + parser.add_argument('--db-filepath', |
| 134 | + help='the filepath of the coverage db to build ' |
| 135 | + '(default: coverage.db)', |
| 136 | + metavar='PATH', |
| 137 | + default='coverage.db') |
| 138 | + parser.add_argument('--log', |
| 139 | + help='the level of information to log (default: info)', |
| 140 | + metavar='LEVEL', |
| 141 | + default='info', |
| 142 | + choices=['info', 'debug', 'warning', 'error', |
| 143 | + 'critical']) |
| 144 | + args = parser.parse_args() |
| 145 | + |
| 146 | + console.setLevel(level=args.log.upper()) |
| 147 | + logging.debug(args) |
| 148 | + |
| 149 | + if not os.path.exists(args.root_directory): |
| 150 | + logging.critical('Directory not found: %s', args.root_directory) |
| 151 | + return 1 |
| 152 | + |
| 153 | + if os.path.exists(args.db_filepath): |
| 154 | + logging.info('Deleting existing database: %s', args.db_filepath) |
| 155 | + os.unlink(args.db_filepath) |
| 156 | + |
| 157 | + logging.info('Creating database: %s', args.db_filepath) |
| 158 | + conn = sqlite3.connect(args.db_filepath) |
| 159 | + |
| 160 | + try: |
| 161 | + logging.debug('Creating coverage table') |
| 162 | + c = conn.cursor() |
| 163 | + c.execute('''CREATE TABLE strings |
| 164 | + (id integer primary key autoincrement, string text)''') |
| 165 | + c.execute('''CREATE TABLE coverage |
| 166 | + (function references strings(id), |
| 167 | + src references strings(id), |
| 168 | + test references strings(id), |
| 169 | + istart integer, iend integer)''') |
| 170 | + conn.commit() |
| 171 | + |
| 172 | + for root, folders, files in os.walk(args.root_directory): |
| 173 | + for f in files: |
| 174 | + if f == args.coverage_filename: |
| 175 | + filepath = os.path.join(root, f) |
| 176 | + logging.debug('Adding file to queue: %s', filepath) |
| 177 | + worker_queue.append((filepath, result_queue)) |
| 178 | + |
| 179 | + logging.info('Finished adding %s paths to queue', len(worker_queue)) |
| 180 | + pool.map_async(worker, worker_queue) |
| 181 | + |
| 182 | + while True: |
| 183 | + parsed = result_queue.get(timeout=60) |
| 184 | + logging.info('Inserting coverage data into db for %s function(s)', |
| 185 | + len(parsed)) |
| 186 | + for section in parsed: |
| 187 | + insert_coverage_section_into_db(c, section) |
| 188 | + conn.commit() |
| 189 | + result_queue.task_done() |
| 190 | + except Queue.Empty: |
| 191 | + logging.info('Queue exhausted. Shutting down...') |
| 192 | + except KeyboardInterrupt: |
| 193 | + logging.debug('Caught KeyboardInterrupt. Shutting down...') |
| 194 | + finally: |
| 195 | + logging.debug('Closing database') |
| 196 | + conn.commit() |
| 197 | + conn.close() |
| 198 | + logging.debug('Terminating pool') |
| 199 | + pool.terminate() |
| 200 | + |
| 201 | + return 0 |
| 202 | + |
| 203 | +if __name__ == '__main__': |
| 204 | + sys.exit(main()) |
0 commit comments