1#!/usr/local/bin/python3.8
2
3import re
4from optparse import OptionParser
5import matplotlib.pyplot as plt
6
7p = OptionParser(usage='%prog [OPTION] FILE...',
8                 description='plot timings from gpaw parallel timer.  '
9                 'The timer dumps a lot of files called "timings.<...>.txt".  '
10                 'This programme plots the contents of those files.  '
11                 'Typically one would run "%prog timings.*.txt" to plot '
12                 'timings on all cores.')
13p.add_option('--interval', metavar='TIME1:TIME2',
14             help='plot only timings within TIME1 and TIME2 '
15             'after start of calculation.')
16p.add_option('--align', metavar='NAME', default='SCF-cycle',
17             help='horizontally align timings of different ranks at first '
18             'call of NAME [default=%default]')
19p.add_option('--nolegend', action='store_true',
20             help='do not plot the separate legend figure')
21p.add_option('--nointeractive', action='store_true',
22             help='disable interactive legend')
23
24
25opts, fnames = p.parse_args()
26
27
28if opts.interval:
29    plotstarttime, plotendtime = map(float, opts.interval.split(':'))
30else:
31    plotstarttime = 0
32    plotendtime = None
33
34
35# We will read/store absolute timings T1 and T2, which are probably 1e9.
36# For the plot we want timings relative to some starting point.
37class Call:
38    def __init__(self, name, T1, level, rankno):
39        self.name = name
40        self.level = level  # nesting level
41        self.T1 = T1
42        self.T2 = None
43        self.rankno = rankno
44
45    @property
46    def t1(self):
47        return self.T1 - alignments[self.rankno]
48
49    @property
50    def t2(self):
51        return self.T2 - alignments[self.rankno]
52
53
54fig = plt.figure()
55ax = fig.add_subplot(111)
56fig.subplots_adjust(left=0.08, right=.95, bottom=0.07, top=.95)
57patch_name_p = []
58
59
60class Function:
61    thecolors = ['blue', 'green', 'red', 'cyan', 'magenta', 'yellow',
62                 'darkred', 'indigo', 'springgreen', 'purple']
63    thehatches = ['', '//', 'O', '*', 'o', r'\\', '.', '|']
64
65    def __init__(self, name, num):
66        self.name = name
67        self.num = num
68        self.color = self.thecolors[num % len(self.thecolors)]
69        self.hatch = self.thehatches[num // len(self.thecolors)]
70        self.bar = None
71        self.calls = []
72
73
74# Example: 'T42  >>  12.34  name  (5.51s)    started'
75pattern = r'T\d+ \S+ (\S+) (.+?) \(.*?\) (started|stopped)'
76functions = {}
77functionslist = []
78maxlevel = 0
79alignments = []
80firstcallsbyrank = []
81lastcallsbyrank = []
82
83
84for rankno, fname in enumerate(fnames):
85    alignment = None
86    ongoing = []
87    with open(fname) as fd:
88        for lineno, line in enumerate(fd):
89            m = re.match(pattern, line)
90            if m is None:
91                failing_line = line
92                break  # Bad syntax
93
94            T, name, action = m.group(1, 2, 3)
95            T = float(T)
96
97            if action == 'started':
98                level = len(ongoing)
99                maxlevel = max(level, maxlevel)
100                call = Call(name, T1=T, level=level, rankno=rankno)
101                if lineno == 0:
102                    assert len(firstcallsbyrank) == rankno
103                    firstcallsbyrank.append(call)
104                if alignment is None and name == opts.align:
105                    alignment = call.T1
106                if name not in functions:
107                    function = Function(name, len(functions))
108                    functions[name] = function
109                    functionslist.append(function)
110                ongoing.append(call)
111            elif action == 'stopped':
112                assert action == 'stopped', action
113                call = ongoing.pop()
114                assert name == call.name
115                call.T2 = T
116                functions[name].calls.append(call)
117
118        # If we failed there may still be lines left.  If there are no
119        # lines left, only last line was mangled (file was incomplete) and
120        # that is okay.  Otherwise it's an error:
121        for line in fd:
122            p.error('Bad syntax: {}'.format(failing_line))
123
124    assert alignment is not None, 'Cannot align to "{}"'.format(opts.align)
125    alignments.append(alignment)
126
127    # End any remaining ongoing calls:
128    for call in ongoing:
129        call.T2 = T
130        functions[call.name].calls.append(call)
131    lastcallsbyrank.append(call)
132
133tmp_tmin = min([call.t1 for call in firstcallsbyrank])
134alignments = [a + tmp_tmin for a in alignments]  # Now timings start at 0
135tmax = max([call.t2 for call in lastcallsbyrank])
136
137
138if plotendtime is None:
139    plotendtime = tmax
140
141for name in sorted(functions):
142    function = functions[name]
143    plotcalls = []
144    for call in function.calls:
145        # Skip timings that fall out of the viewed interval:
146        if call.t2 < plotstarttime:
147            continue
148        if call.t1 > plotendtime:
149            continue
150        plotcalls.append(call)
151
152    # Shift: maxlevel, rank
153    bar = ax.bar([call.t1 for call in plotcalls],
154                 height=[1.0] * len(plotcalls),
155                 width=[call.T2 - call.T1 for call in plotcalls],
156                 bottom=[call.level + call.rankno * (maxlevel + 1)
157                         for call in plotcalls],
158                 color=function.color,
159                 hatch=function.hatch,
160                 edgecolor='black',
161                 align='edge',
162                 label=function.name)
163    for child in bar.get_children():
164        patch_name_p.append((child, name))
165    ax.axis(xmin=plotstarttime, xmax=plotendtime)
166
167
168if not opts.nolegend:
169    ncolumns = 1 + len(functionslist) // 32
170    namefig = plt.figure()
171    nameax = namefig.add_subplot(111)
172
173    for function in functionslist:
174        nameax.bar([0], [0], [0], [0],
175                   color=function.color, hatch=function.hatch,
176                   label=function.name[:20])
177
178    nameax.legend(handlelength=2.5,
179                  labelspacing=0.0,
180                  fontsize='large',
181                  ncol=ncolumns,
182                  mode='expand',
183                  frameon=True,
184                  loc='best')
185
186
187if not opts.nointeractive:
188    default_text = 'Click on a patch to get the name'
189    p = fig.subplotpars
190
191    # Create interactive axes
192    box = (p.left, p.top, p.right - p.left, 1 - p.top)
193    fc = fig.get_facecolor()
194    try:
195        iax = fig.add_axes(box, facecolor=fc)  # matplotlib 2.x
196    except AttributeError:
197        iax = fig.add_axes(box, axisbg=fc)  # matplotlib 1.x
198
199    ibg = iax.patch
200    for s in iax.get_children():
201        if s != ibg:
202            s.set_visible(False)
203    itext = iax.text(0.5, 0.5, default_text, va='center', ha='center',
204                     transform=iax.transAxes)
205
206    def print_name_event(event):
207        text = default_text
208        # Do action based on the chosen tool
209        # tb = fig.canvas.manager.toolbar
210        # if tb.mode != '':
211        #     return
212        for patch, name in patch_name_p:
213            if patch.contains(event)[0]:
214                text = name
215                break
216        itext.set_text(text)
217        # The next lines can be replace with
218        # fig.canvas.draw()
219        # but it'll be very slow
220        iax.draw_artist(ibg)
221        iax.draw_artist(itext)
222
223        canvas = fig.canvas
224        if hasattr(canvas, 'update'):
225            canvas.update()  # matplotlib 0.x
226        else:
227            canvas.blit()  # matplotlib 2.x
228        canvas.flush_events()
229
230    fig.canvas.mpl_connect('button_press_event', print_name_event)
231
232
233plt.show()
234