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