1#!/usr/bin/env python 2''' 3 timings 4 ======= 5 6 Plot the timings from building minimal-lexical. 7''' 8 9import argparse 10import json 11import subprocess 12import os 13 14import matplotlib.pyplot as plt 15from matplotlib import patches 16from matplotlib import textpath 17 18plt.style.use('ggplot') 19 20scripts = os.path.dirname(os.path.realpath(__file__)) 21home = os.path.dirname(scripts) 22 23def parse_args(argv=None): 24 '''Create and parse our command line arguments.''' 25 26 parser = argparse.ArgumentParser(description='Time building minimal-lexical.') 27 parser.add_argument( 28 '--features', 29 help='''optional features to add''', 30 default='', 31 ) 32 parser.add_argument( 33 '--no-default-features', 34 help='''disable default features''', 35 action='store_true', 36 ) 37 return parser.parse_args(argv) 38 39def clean(directory=home): 40 '''Clean the project''' 41 42 os.chdir(directory) 43 subprocess.check_call( 44 ['cargo', '+nightly', 'clean'], 45 shell=False, 46 stdout=subprocess.DEVNULL, 47 stderr=subprocess.DEVNULL, 48 ) 49 50def build(args): 51 '''Build the project and get the timings output.''' 52 53 command = 'cargo +nightly build -Z timings=json' 54 if args.no_default_features: 55 command = f'{command} --no-default-features' 56 if args.features: 57 command = f'{command} --features={args.features}' 58 process = subprocess.Popen( 59 # Use shell for faster performance. 60 # Spawning a new process is a **lot** slower, gives misleading info. 61 command, 62 shell=True, 63 stderr=subprocess.DEVNULL, 64 stdout=subprocess.PIPE, 65 ) 66 process.wait() 67 data = {} 68 for line in iter(process.stdout.readline, b''): 69 line = line.decode('utf-8') 70 crate = json.loads(line) 71 name = crate['target']['name'] 72 data[name] = (crate['duration'], crate['rmeta_time']) 73 74 process.stdout.close() 75 76 return data 77 78def filename(basename, args): 79 '''Get a resilient name for the benchmark data.''' 80 81 name = basename 82 if args.no_default_features: 83 name = f'{name}_nodefault' 84 if args.features: 85 name = f'{name}_features={args.features}' 86 return name 87 88def plot_timings(timings, output): 89 '''Plot our timings data.''' 90 91 offset = 0 92 text_length = 0 93 count = len(timings) + 1 94 fig, ax = plt.subplots() 95 bar_height = count * 0.05 96 97 def plot_timing(name): 98 '''Plot the timing of a specific value.''' 99 100 nonlocal count 101 nonlocal text_length 102 103 if name not in timings: 104 return 105 duration, rmeta = timings[name] 106 local_offset = offset 107 ax.add_patch(patches.Rectangle( 108 (offset, count - bar_height / 2), duration, bar_height, 109 alpha=0.6, 110 facecolor='lightskyblue', 111 label=name, 112 )) 113 local_offset += rmeta 114 ax.add_patch(patches.Rectangle( 115 (local_offset, count - bar_height / 2), duration - rmeta, bar_height, 116 alpha=0.6, 117 facecolor='darkorchid', 118 label=f'{name}_rmeta', 119 )) 120 local_offset += duration - rmeta 121 text = f'minimal-lexical {round(duration, 2)}s' 122 text_length = max(len(text), text_length) 123 ax.annotate( 124 text, 125 xy=(local_offset + 0.02, count), 126 xycoords='data', 127 horizontalalignment='left', 128 verticalalignment='center', 129 ) 130 count -= 1 131 132 def max_duration(*keys): 133 '''Get the max duration from a list of keys.''' 134 135 max_time = 0 136 for key in keys: 137 if key not in timings: 138 continue 139 max_time = max(timings[key][0], max_time) 140 return max_time 141 142 # Plot in order of our dependencies. 143 plot_timing('minimal-lexical') 144 offset += max_duration('minimal-lexical') 145 146 title = 'Build Timings' 147 ax.set_title(title) 148 ax.set_xlabel('Time (s)') 149 150 # Hide the y-axis labels. 151 ax.set_yticks(list(range(1, len(timings) + 2))) 152 ax.yaxis.set_tick_params(which='both', length=0) 153 plt.setp(ax.get_yticklabels(), visible=False) 154 155 # Ensure the canvas includes all the annotations. 156 # 0.5 is long enough for the largest label. 157 plt.xlim(0, offset + 0.02 * text_length) 158 plt.ylim(count + 0.5, len(timings) + 1.5) 159 160 # Save the figure. 161 fig.savefig(output, format='svg') 162 fig.clf() 163 164def plot(args): 165 '''Build and plot the timings for the root module.''' 166 167 clean() 168 timings = build(args) 169 path = f'{home}/assets/timings_{filename("timings", args)}_{os.name}.svg' 170 plot_timings(timings, path) 171 172def main(argv=None): 173 '''Entry point.''' 174 175 args = parse_args(argv) 176 plot(args) 177 178if __name__ == '__main__': 179 main() 180