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