1#!/usr/bin/python
2
3import pandas as pd
4import numpy as np
5import re
6import sys
7import os
8import argparse
9import matplotlib
10from matplotlib import pyplot as plt
11from matplotlib.projections.polar import PolarAxes
12from matplotlib.projections import register_projection
13
14"""
15Read the stats file produced by the OpenMP runtime
16and produce a processed summary
17
18The radar_factory original code was taken from
19matplotlib.org/examples/api/radar_chart.html
20We added support to handle negative values for radar charts
21"""
22
23def radar_factory(num_vars, frame='circle'):
24    """Create a radar chart with num_vars axes."""
25    # calculate evenly-spaced axis angles
26    theta = 2*np.pi * np.linspace(0, 1-1./num_vars, num_vars)
27    # rotate theta such that the first axis is at the top
28    #theta += np.pi/2
29
30    def draw_poly_frame(self, x0, y0, r):
31        # TODO: use transforms to convert (x, y) to (r, theta)
32        verts = [(r*np.cos(t) + x0, r*np.sin(t) + y0) for t in theta]
33        return plt.Polygon(verts, closed=True, edgecolor='k')
34
35    def draw_circle_frame(self, x0, y0, r):
36        return plt.Circle((x0, y0), r)
37
38    frame_dict = {'polygon': draw_poly_frame, 'circle': draw_circle_frame}
39    if frame not in frame_dict:
40        raise ValueError, 'unknown value for `frame`: %s' % frame
41
42    class RadarAxes(PolarAxes):
43        """
44        Class for creating a radar chart (a.k.a. a spider or star chart)
45
46        http://en.wikipedia.org/wiki/Radar_chart
47        """
48        name = 'radar'
49        # use 1 line segment to connect specified points
50        RESOLUTION = 1
51        # define draw_frame method
52        draw_frame = frame_dict[frame]
53
54        def fill(self, *args, **kwargs):
55            """Override fill so that line is closed by default"""
56            closed = kwargs.pop('closed', True)
57            return super(RadarAxes, self).fill(closed=closed, *args, **kwargs)
58
59        def plot(self, *args, **kwargs):
60            """Override plot so that line is closed by default"""
61            lines = super(RadarAxes, self).plot(*args, **kwargs)
62            #for line in lines:
63            #    self._close_line(line)
64
65        def set_varlabels(self, labels):
66            self.set_thetagrids(theta * 180/np.pi, labels,fontsize=14)
67
68        def _gen_axes_patch(self):
69            x0, y0 = (0.5, 0.5)
70            r = 0.5
71            return self.draw_frame(x0, y0, r)
72
73    register_projection(RadarAxes)
74    return theta
75
76# Code to read the raw stats
77def extractSI(s):
78    """Convert a measurement with a range suffix into a suitably scaled value"""
79    du     = s.split()
80    num    = float(du[0])
81    units  = du[1] if len(du) == 2 else ' '
82    # http://physics.nist.gov/cuu/Units/prefixes.html
83    factor = {'Y':  1e24,
84              'Z':  1e21,
85              'E':  1e18,
86              'P':  1e15,
87              'T':  1e12,
88              'G':  1e9,
89              'M':  1e6,
90              'k':  1e3,
91              ' ':  1  ,
92              'm': -1e3, # Yes, I do mean that, see below for the explanation.
93              'u': -1e6,
94              'n': -1e9,
95              'p': -1e12,
96              'f': -1e15,
97              'a': -1e18,
98              'z': -1e21,
99              'y': -1e24}[units[0]]
100    # Minor trickery here is an attempt to preserve accuracy by using a single
101    # divide, rather than  multiplying by 1/x, which introduces two roundings
102    # since 1/10 is not representable perfectly in IEEE floating point. (Not
103    # that this really matters, other than for cleanliness, since we're likely
104    # reading numbers with at most five decimal digits of precision).
105    return  num*factor if factor > 0 else num/-factor
106
107def readData(f):
108    line = f.readline()
109    fieldnames = [x.strip() for x in line.split(',')]
110    line = f.readline().strip()
111    data = []
112    while line != "":
113        if line[0] != '#':
114            fields = line.split(',')
115            data.append ((fields[0].strip(), [extractSI(v) for v in fields[1:]]))
116        line = f.readline().strip()
117    # Man, working out this next incantation out was non-trivial!
118    # They really want you to be snarfing data in csv or some other
119    # format they understand!
120    res = pd.DataFrame.from_items(data, columns=fieldnames[1:], orient='index')
121    return res
122
123def readTimers(f):
124    """Skip lines with leading #"""
125    line = f.readline()
126    while line[0] == '#':
127        line = f.readline()
128    line = line.strip()
129    if line == "Statistics on exit\n" or "Aggregate for all threads\n":
130        line = f.readline()
131    return readData(f)
132
133def readCounters(f):
134    """This can be just the same!"""
135    return readData(f)
136
137def readFile(fname):
138    """Read the statistics from the file. Return a dict with keys "timers", "counters" """
139    res = {}
140    try:
141        with open(fname) as f:
142            res["timers"]   = readTimers(f)
143            res["counters"] = readCounters(f)
144            return res
145    except (OSError, IOError):
146        print "Cannot open " + fname
147        return None
148
149def usefulValues(l):
150    """I.e. values which are neither null nor zero"""
151    return [p and q for (p,q) in zip (pd.notnull(l), l != 0.0)]
152
153def uselessValues(l):
154    """I.e. values which are null or zero"""
155    return [not p for p in usefulValues(l)]
156
157interestingStats = ("counters", "timers")
158statProperties   = {"counters" : ("Count", "Counter Statistics"),
159                    "timers"   : ("Time (ticks)", "Timer Statistics")
160                   }
161
162def drawChart(data, kind, filebase):
163    """Draw a summary bar chart for the requested data frame into the specified file"""
164    data["Mean"].plot(kind="bar", logy=True, grid=True, colormap="GnBu",
165                      yerr=data["SD"], ecolor="black")
166    plt.xlabel("OMP Constructs")
167    plt.ylabel(statProperties[kind][0])
168    plt.title (statProperties[kind][1])
169    plt.tight_layout()
170    plt.savefig(filebase+"_"+kind)
171
172def normalizeValues(data, countField, factor):
173    """Normalize values into a rate by dividing them all by the given factor"""
174    data[[k for k in data.keys() if k != countField]] /= factor
175
176
177def setRadarFigure(titles):
178    """Set the attributes for the radar plots"""
179    fig = plt.figure(figsize=(9,9))
180    rect = [0.1, 0.1, 0.8, 0.8]
181    labels = [0.2, 0.4, 0.6, 0.8, 1, 2, 3, 4, 5, 10]
182    matplotlib.rcParams.update({'font.size':13})
183    theta = radar_factory(len(titles))
184    ax = fig.add_axes(rect, projection='radar')
185    ax.set_rgrids(labels)
186    ax.set_varlabels(titles)
187    ax.text(theta[2], 1, "Linear->Log", horizontalalignment='center', color='green', fontsize=18)
188    return {'ax':ax, 'theta':theta}
189
190
191def drawRadarChart(data, kind, filebase, params, color):
192    """Draw the radar plots"""
193    tmp_lin = data * 0
194    tmp_log = data * 0
195    for key in data.keys():
196        if data[key] >= 1:
197           tmp_log[key] = np.log10(data[key])
198        else:
199           tmp_lin[key] = (data[key])
200    params['ax'].plot(params['theta'], tmp_log, color='b', label=filebase+"_"+kind+"_log")
201    params['ax'].plot(params['theta'], tmp_lin, color='r', label=filebase+"_"+kind+"_linear")
202    params['ax'].legend(loc='best', bbox_to_anchor=(1.4,1.2))
203    params['ax'].set_rlim((0, np.ceil(max(tmp_log))))
204
205def multiAppBarChartSettings(ax, plt, index, width, n, tmp, s):
206    ax.set_yscale('log')
207    ax.legend()
208    ax.set_xticks(index + width * n / 2)
209    ax.set_xticklabels(tmp[s]['Total'].keys(), rotation=50, horizontalalignment='right')
210    plt.xlabel("OMP Constructs")
211    plt.ylabel(statProperties[s][0])
212    plt.title(statProperties[s][1])
213    plt.tight_layout()
214
215def derivedTimerStats(data):
216    stats = {}
217    for key in data.keys():
218        if key == 'OMP_worker_thread_life':
219            totalRuntime = data['OMP_worker_thread_life']
220        elif key in ('FOR_static_iterations', 'OMP_PARALLEL_args',
221                     'OMP_set_numthreads', 'FOR_dynamic_iterations'):
222            break
223        else:
224            stats[key] = 100 * data[key] / totalRuntime
225    return stats
226
227def compPie(data):
228    compKeys = {}
229    nonCompKeys = {}
230    for key in data.keys():
231        if key in ('OMP_critical', 'OMP_single', 'OMP_serial',
232                   'OMP_parallel', 'OMP_master', 'OMP_task_immediate',
233                   'OMP_task_taskwait', 'OMP_task_taskyield', 'OMP_task_taskgroup',
234                   'OMP_task_join_bar', 'OMP_task_plain_bar', 'OMP_task_taskyield'):
235            compKeys[key] = data[key]
236        else:
237            nonCompKeys[key] = data[key]
238    print "comp keys:", compKeys, "\n\n non comp keys:", nonCompKeys
239    return [compKeys, nonCompKeys]
240
241def drawMainPie(data, filebase, colors):
242    sizes = [sum(data[0].values()), sum(data[1].values())]
243    explode = [0,0]
244    labels = ["Compute - " + "%.2f" % sizes[0], "Non Compute - " + "%.2f" % sizes[1]]
245    patches = plt.pie(sizes, explode, colors=colors, startangle=90)
246    plt.title("Time Division")
247    plt.axis('equal')
248    plt.legend(patches[0], labels, loc='best', bbox_to_anchor=(-0.1,1), fontsize=16)
249    plt.savefig(filebase+"_main_pie", bbox_inches='tight')
250
251def drawSubPie(data, tag, filebase, colors):
252    explode = []
253    labels = data.keys()
254    sizes = data.values()
255    total = sum(sizes)
256    percent = []
257    for i in range(len(sizes)):
258        explode.append(0)
259        percent.append(100 * sizes[i] / total)
260        labels[i] = labels[i] + " - %.2f" % percent[i]
261    patches = plt.pie(sizes, explode=explode, colors=colors, startangle=90)
262    plt.title(tag+"(Percentage of Total:"+" %.2f" % (sum(data.values()))+")")
263    plt.tight_layout()
264    plt.axis('equal')
265    plt.legend(patches[0], labels, loc='best', bbox_to_anchor=(-0.1,1), fontsize=16)
266    plt.savefig(filebase+"_"+tag, bbox_inches='tight')
267
268def main():
269    parser = argparse.ArgumentParser(description='''This script takes a list
270        of files containing each of which contain output from a stats-gathering
271        enabled OpenMP runtime library.  Each stats file is read, parsed, and
272        used to produce a summary of the statistics''')
273    parser.add_argument('files', nargs='+',
274        help='files to parse which contain stats-gathering output')
275    command_args = parser.parse_args()
276    colors = ['orange', 'b', 'r', 'yellowgreen', 'lightsage', 'lightpink',
277              'green', 'purple', 'yellow', 'cyan', 'mediumturquoise',
278              'olive']
279    stats = {}
280    matplotlib.rcParams.update({'font.size':22})
281    for s in interestingStats:
282        fig, ax = plt.subplots()
283        width = 0.45
284        n = 0
285        index = 0
286
287        for f in command_args.files:
288            filebase = os.path.splitext(f)[0]
289            tmp = readFile(f)
290            data = tmp[s]['Total']
291            """preventing repetition by removing rows similar to Total_OMP_work
292                as Total_OMP_work['Total'] is same as OMP_work['Total']"""
293            if s == 'counters':
294                elapsedTime = tmp["timers"]["Mean"]["OMP_worker_thread_life"]
295                normalizeValues(tmp["counters"], "SampleCount",
296                    elapsedTime / 1.e9)
297                """Plotting radar charts"""
298                params = setRadarFigure(data.keys())
299                chartType = "radar"
300                drawRadarChart(data, s, filebase, params, colors[n])
301                """radar Charts finish here"""
302                plt.savefig(filebase+"_"+s+"_"+chartType, bbox_inches='tight')
303            elif s == 'timers':
304                print "overheads in "+filebase
305                numThreads = tmp[s]['SampleCount']['Total_OMP_parallel']
306                for key in data.keys():
307                    if key[0:5] == 'Total':
308                        del data[key]
309                stats[filebase] = derivedTimerStats(data)
310                dataSubSet = compPie(stats[filebase])
311                drawMainPie(dataSubSet, filebase, colors)
312                plt.figure(0)
313                drawSubPie(dataSubSet[0], "Computational Time", filebase, colors)
314                plt.figure(1)
315                drawSubPie(dataSubSet[1], "Non Computational Time", filebase, colors)
316                with open('derivedStats_{}.csv'.format(filebase), 'w') as f:
317                    f.write('================={}====================\n'.format(filebase))
318                    f.write(pd.DataFrame(stats[filebase].items()).to_csv()+'\n')
319            n += 1
320    plt.close()
321
322if __name__ == "__main__":
323    main()
324