def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--add-note", nargs=3, help="Development only. --add-note N commit seed \ Adds N fake metrics to the given commit using the random seed." ) parser.add_argument("--ci", action='store_true', help="Use ci results. You must fetch these with:\n " \ + "$ git fetch https://gitlab.haskell.org/ghc/ghc-performance-notes.git refs/notes/perf:refs/notes/ci/perf") group = parser.add_argument_group( title='Filtering', description="Select which subset of performance metrics to dump") group.add_argument( "--test-env", help= "The given test environment to be compared. Use 'local' for locally run results. If using --ci, see .gitlab-ci file for TEST_ENV settings." ) group.add_argument( "--test-name", help="Filters for tests matching the given regular expression.") group.add_argument("--metric", help="Test metric (one of " + str(testing_metrics()) + ").") group.add_argument("--way", help="Test way (one of " + str(testing_metrics()) + ").") group = parser.add_argument_group( title='Plotting', description="Plot historical performance metrics") group.add_argument( "--chart", nargs='?', default=None, action='store', const='./PerformanceChart.html', help= 'Create a chart of the results an save it to the given file. Default to "./PerformanceChart.html".' ) group.add_argument("--zero-y", action='store_true', help='When charting, include 0 in y axis') parser.add_argument( "commits", nargs='+', help= "Either a list of commits or a single commit range (e.g. HEAD~10..HEAD)." ) args = parser.parse_args() env = 'local' name = re.compile('.*') CommitAndStat = NamedTuple('CommitAndStat', [('commit', GitHash), ('stat', PerfStat)]) metrics = [] # type: List[CommitAndStat] singleton_commit = len(args.commits) == 1 # # Main logic of the program when called from the command-line. # ref = NoteNamespace('perf') if args.ci: ref = NoteNamespace('ci/perf') commits = args.commits if args.commits: # Commit range if len(commits) == 1 and ".." in commits[0]: commits = list(reversed(commit_log(commits[0]))) for c in commits: metrics += [ CommitAndStat(c, stat) for stat in get_perf_stats(c, ref) ] if args.metric: metrics = [test for test in metrics if test.stat.metric == args.metric] if args.way: metrics = [test for test in metrics if test.stat.way == args.way] if args.test_env: if '"' in args.test_env: raise Exception('test_env should not contain quotation marks') metrics = [ test for test in metrics if test.stat.test_env == args.test_env ] if args.test_name: nameRe = re.compile(args.test_name) metrics = [test for test in metrics if nameRe.search(test.stat.test)] if args.add_note: def note_gen(n, commit, delta=''): note = [] # Generates simple fake data. Likely not comprehensive enough to catch all edge cases. if not delta: note.extend([ PerfStat('local', 'T' + str(i * 100), 'some_way', 'some_field', str(i * 1000)) for i in range(1, int(int(n) / 2) + 1) ]) note.extend([ PerfStat('non-local', 'W' + str(i * 100), 'other_way', 'other_field', str(i * 100)) for i in range(int(int(n) / 2) + 1, int(n) + 1) ]) if delta: hu = abs(hash(delta)) hv = abs(hash(hu)) u = int(hu % 100) v = int(hv % 10) note.extend([ PerfStat('local', 'T' + str(i * 100), 'some_way', 'some_field', str(i * u)) for i in range(1, int(int(n) / 2) + 1) ]) note.extend([ PerfStat('non-local', 'W' + str(i * 100), 'other_way', 'other_field', str(i * v)) for i in range(int(int(n) / 2) + 1, int(n) + 1) ]) append_perf_stat(note, commit) note_gen(args.add_note[0], args.add_note[1], args.add_note[2]) # # Chart # def metricAt(commit, testName, testMetric): values2 = [float(t.stat.value) for t in metrics if t.commit == commit \ and t.stat.test == testName \ and t.stat.metric == testMetric] if values2 == []: return None else: return (sum(values2) / len(values2)) testSeries = list( set([(test.stat.test_env, test.stat.test, test.stat.metric, test.stat.way) for test in metrics])) # # Use Chart.js to visualize the data. # if args.chart: commitMsgs = dict([(h, get_commit_message(h)) for h in commits]) chartData = { 'type': 'line', 'data': { 'labels': [commitMsgs[h].split("\n")[0] + " (" + \ (h[:8] if is_commit_hash(h) else h) + \ ")" for h in commits], 'datasets': [{ 'label': name + "(" + way + ") " + metric + " - " + env, 'data': [get_commit_metric_value_str_or_none(ref, commit, env, name, metric, way) \ for commit in commits], 'fill': 'false', 'spanGaps': 'true', 'lineTension': 0, 'backgroundColor': hash_rgb_str((env, name, metric, way)), 'borderColor': hash_rgb_str((env, name, metric, way)) } for (env, name, metric, way) in testSeries] }, 'options': { 'scales': { 'yAxes': [{ 'ticks': { 'beginAtZero': True } }] } } } # Try use local Chart.js file else use online version. tooltipjsFilePath = sys.path[0] + "/js/tooltip.js" chartjsFilePath = sys.path[0] + "/js/Chart-2.8.0.min.js" tooltipjsTag = None try: tooltipjsFile = open(tooltipjsFilePath, "r") tooltipjsTag = '<script>' + tooltipjsFile.read() + '</script>' tooltipjsFile.close() except: print("Failed to load custom tooltip: " + chartjsFilePath + ".") tooltipjsTag = None try: chartjsFile = open(chartjsFilePath, "r") chartjsTag = '<script>' + chartjsFile.read() + '</script>' chartjsFile.close() except: print("Failed to load " + chartjsFilePath + ", reverting to online Chart.js.") chartjsTag = '<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>' file = open(args.chart, "w+t") print(\ "<html>" + \ '<head>\n' + \ (tooltipjsTag if tooltipjsTag is not None else '') + \ chartjsTag + \ '</head>' + \ '<body style="padding: 20px"><canvas id="myChart"></canvas><script>' + \ "var ctx = document.getElementById('myChart').getContext('2d');" + \ "var commitMsgs = " + json.dumps(commitMsgs, indent=2) + ";" + \ "var chartData = " + json.dumps(chartData, indent=2) + ";" + \ (("var chart = new Chart(ctx, setCustomTooltip(chartData, commitMsgs));") \ if tooltipjsTag is not None else \ ("var chart = new Chart(ctx, chartData);")) + \ '</script></body>' + \ "</html>"\ , file=file) file.close() exit(0) # # Print the data in tablular format # # T1234 T1234 # max_bytes max_bytes # normal normal # commit x86_64-darwin i386-linux-deb9 # -------------------------------------------- # HEAD 9123 9123 # HEAD~1 10023 10023 # HEAD~2 21234 21234 # HEAD~3 20000 20000 def strMetric(x): return '{:.2f}'.format(x.value) if x != None else "" # Data is in colum major format, so transpose and pass to print_table. T = TypeVar('T') def transpose(xss: List[List[T]]) -> List[List[T]]: return list(map(list, zip(*xss))) headerCols = [ ["","","","Commit"] ] \ + [ [name, metric, way, env] for (env, name, metric, way) in testSeries ] dataCols = [ commits ] \ + [ [strMetric(get_commit_metric(ref, commit, env, name, metric, way)) \ for commit in commits ] \ for (env, name, metric, way) in testSeries ] print_table(transpose(headerCols), transpose(dataCols))
help= 'Create a chart of the results an save it to the given file. Default to "./PerformanceChart.html".' ) parser.add_argument("--ci", action='store_true', help="Use ci results. You must fetch these with:\n " \ + "$ git fetch https://gitlab.haskell.org/ghc/ghc-performance-notes.git refs/notes/perf:refs/notes/ci/perf") parser.add_argument( "--test-env", help= "The given test environment to be compared. Use 'local' for localy run results. If using --ci, see .gitlab-ci file for TEST_ENV settings." ) parser.add_argument( "--test-name", help="Filters for tests matching the given regular expression.") parser.add_argument("--metric", help="Test metric (one of " + str(testing_metrics()) + ").") parser.add_argument("--way", help="Test way (one of " + str(testing_metrics()) + ").") parser.add_argument( "commits", nargs=argparse.REMAINDER, help= "Either a list of commits or a single commit range (e.g. HEAD~10..HEAD)." ) args = parser.parse_args() env = 'local' name = re.compile('.*') # metrics is a tuple (str commit, PerfStat stat)