• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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