blob: aab26a5bbe07781e1574dd07c1f2683c0f58efee [file] [log] [blame]
showardce12f552008-09-19 00:48:59 +00001import base64, os, tempfile, operator, pickle, datetime, django.db
showard90e27262009-06-08 23:25:19 +00002import os.path, getpass
showardce12f552008-09-19 00:48:59 +00003from math import sqrt
4
showarde5ae1652009-02-11 23:37:20 +00005# When you import matplotlib, it tries to write some temp files for better
showard90e27262009-06-08 23:25:19 +00006# performance, and it does that to the directory in MPLCONFIGDIR, or, if that
7# doesn't exist, the home directory. Problem is, the home directory is not
8# writable when running under Apache, and matplotlib's not smart enough to
9# handle that. It does appear smart enough to handle the files going
showarde5ae1652009-02-11 23:37:20 +000010# away after they are written, though.
showard90e27262009-06-08 23:25:19 +000011
12temp_dir = os.path.join(tempfile.gettempdir(),
13 '.matplotlib-%s' % getpass.getuser())
14if not os.path.exists(temp_dir):
15 os.mkdir(temp_dir)
16os.environ['MPLCONFIGDIR'] = temp_dir
showardce12f552008-09-19 00:48:59 +000017
18import matplotlib
19matplotlib.use('Agg')
20
21import matplotlib.figure, matplotlib.backends.backend_agg
22import StringIO, colorsys, PIL.Image, PIL.ImageChops
23from autotest_lib.frontend.afe import readonly_connection
24from autotest_lib.frontend.afe.model_logic import ValidationError
showard250d84d2010-01-12 21:59:48 +000025from autotest_lib.frontend.afe.simplejson import encoder
showardce12f552008-09-19 00:48:59 +000026from autotest_lib.client.common_lib import global_config
showard250d84d2010-01-12 21:59:48 +000027from autotest_lib.frontend.tko import models, tko_rpc_utils
showardce12f552008-09-19 00:48:59 +000028
showarde5ae1652009-02-11 23:37:20 +000029_FIGURE_DPI = 100
30_FIGURE_WIDTH_IN = 10
31_FIGURE_BOTTOM_PADDING_IN = 2 # for x-axis labels
32
33_SINGLE_PLOT_HEIGHT = 6
34_MULTIPLE_PLOT_HEIGHT_PER_PLOT = 4
35
36_MULTIPLE_PLOT_MARKER_TYPE = 'o'
37_MULTIPLE_PLOT_MARKER_SIZE = 4
38_SINGLE_PLOT_STYLE = 'bs-' # blue squares with lines connecting
39_SINGLE_PLOT_ERROR_BAR_COLOR = 'r'
40
41_LEGEND_FONT_SIZE = 'xx-small'
42_LEGEND_HANDLE_LENGTH = 0.03
43_LEGEND_NUM_POINTS = 3
44_LEGEND_MARKER_TYPE = 'o'
45
46_LINE_XTICK_LABELS_SIZE = 'x-small'
47_BAR_XTICK_LABELS_SIZE = 8
showardce12f552008-09-19 00:48:59 +000048
showard3b2b9302009-04-15 21:53:47 +000049_json_encoder = encoder.JSONEncoder()
50
showardce12f552008-09-19 00:48:59 +000051class NoDataError(Exception):
52 """\
53 Exception to raise if the graphing query returned an empty resultset.
54 """
55
56
57def _colors(n):
58 """\
showarde5ae1652009-02-11 23:37:20 +000059 Generator function for creating n colors. The return value is a tuple
60 representing the RGB of the color.
showardce12f552008-09-19 00:48:59 +000061 """
showardce12f552008-09-19 00:48:59 +000062 for i in xrange(n):
showarde5ae1652009-02-11 23:37:20 +000063 yield colorsys.hsv_to_rgb(float(i) / n, 1.0, 1.0)
showardce12f552008-09-19 00:48:59 +000064
65
66def _resort(kernel_labels, list_to_sort):
67 """\
68 Resorts a list, using a list of kernel strings as the keys. Returns the
69 resorted list.
70 """
71
72 labels = [tko_rpc_utils.KernelString(label) for label in kernel_labels]
73 resorted_pairs = sorted(zip(labels, list_to_sort))
74
75 # We only want the resorted list; we are not interested in the kernel
76 # strings.
showarde5ae1652009-02-11 23:37:20 +000077 return [pair[1] for pair in resorted_pairs]
showardce12f552008-09-19 00:48:59 +000078
79
showarde5ae1652009-02-11 23:37:20 +000080def _quote(string):
81 return "%s%s%s" % ("'", string.replace("'", r"\'"), "'")
82
83
84_HTML_TEMPLATE = """\
showardce12f552008-09-19 00:48:59 +000085<html><head></head><body>
86<img src="data:image/png;base64,%s" usemap="#%s"
87 border="0" alt="graph">
88<map name="%s">%s</map>
89</body></html>"""
90
showarde5ae1652009-02-11 23:37:20 +000091_AREA_TEMPLATE = """\
showardce12f552008-09-19 00:48:59 +000092<area shape="rect" coords="%i,%i,%i,%i" title="%s"
93href="#"
94onclick="%s(%s); return false;">"""
95
96
showardfbdab0b2009-04-29 19:49:50 +000097class MetricsPlot(object):
98 def __init__(self, query_dict, plot_type, inverted_series, normalize_to,
99 drilldown_callback):
100 """
101 query_dict: dictionary containing the main query and the drilldown
102 queries. The main query returns a row for each x value. The first
103 column contains the x-axis label. Subsequent columns contain data
104 for each series, named by the column names. A column named
105 'errors-<x>' will be interpreted as errors for the series named <x>.
106
107 plot_type: 'Line' or 'Bar', depending on the plot type the user wants
108
109 inverted_series: list of series that should be plotted on an inverted
110 y-axis
111
112 normalize_to:
113 None - do not normalize
114 'first' - normalize against the first data point
115 'x__%s' - normalize against the x-axis value %s
116 'series__%s' - normalize against the series %s
117
118 drilldown_callback: name of drilldown callback method.
119 """
120 self.query_dict = query_dict
121 if plot_type == 'Line':
122 self.is_line = True
123 elif plot_type == 'Bar':
124 self.is_line = False
125 else:
126 raise ValidationError({'plot' : 'Plot must be either Line or Bar'})
127 self.plot_type = plot_type
128 self.inverted_series = inverted_series
129 self.normalize_to = normalize_to
130 if self.normalize_to is None:
131 self.normalize_to = ''
132 self.drilldown_callback = drilldown_callback
133
134
135class QualificationHistogram(object):
136 def __init__(self, query, filter_string, interval, drilldown_callback):
137 """
138 query: the main query to retrieve the pass rate information. The first
139 column contains the hostnames of all the machines that satisfied the
140 global filter. The second column (titled 'total') contains the total
141 number of tests that ran on that machine and satisfied the global
142 filter. The third column (titled 'good') contains the number of
143 those tests that passed on that machine.
144
145 filter_string: filter to apply to the common global filter to show the
146 Table View drilldown of a histogram bucket
147
148 interval: interval for each bucket. E.g., 10 means that buckets should
149 be 0-10%, 10%-20%, ...
150
151 """
152 self.query = query
153 self.filter_string = filter_string
154 self.interval = interval
155 self.drilldown_callback = drilldown_callback
156
157
showardce12f552008-09-19 00:48:59 +0000158def _create_figure(height_inches):
159 """\
160 Creates an instance of matplotlib.figure.Figure, given the height in inches.
161 Returns the figure and the height in pixels.
162 """
163
showarde5ae1652009-02-11 23:37:20 +0000164 fig = matplotlib.figure.Figure(
165 figsize=(_FIGURE_WIDTH_IN, height_inches + _FIGURE_BOTTOM_PADDING_IN),
166 dpi=_FIGURE_DPI, facecolor='white')
167 fig.subplots_adjust(bottom=float(_FIGURE_BOTTOM_PADDING_IN) / height_inches)
168 return (fig, fig.get_figheight() * _FIGURE_DPI)
showardce12f552008-09-19 00:48:59 +0000169
170
showardfbdab0b2009-04-29 19:49:50 +0000171def _create_line(plots, labels, plot_info):
showardce12f552008-09-19 00:48:59 +0000172 """\
173 Given all the data for the metrics, create a line plot.
174
showarde5ae1652009-02-11 23:37:20 +0000175 plots: list of dicts containing the plot data. Each dict contains:
showardce12f552008-09-19 00:48:59 +0000176 x: list of x-values for the plot
177 y: list of corresponding y-values
178 errors: errors for each data point, or None if no error information
179 available
180 label: plot title
showarde5ae1652009-02-11 23:37:20 +0000181 labels: list of x-tick labels
showardfbdab0b2009-04-29 19:49:50 +0000182 plot_info: a MetricsPlot
showardce12f552008-09-19 00:48:59 +0000183 """
showardfbdab0b2009-04-29 19:49:50 +0000184 # when we're doing any kind of normalization, all series get put into a
185 # single plot
186 single = bool(plot_info.normalize_to)
showardce12f552008-09-19 00:48:59 +0000187
188 area_data = []
189 lines = []
190 if single:
showarde5ae1652009-02-11 23:37:20 +0000191 plot_height = _SINGLE_PLOT_HEIGHT
showardce12f552008-09-19 00:48:59 +0000192 else:
showarde5ae1652009-02-11 23:37:20 +0000193 plot_height = _MULTIPLE_PLOT_HEIGHT_PER_PLOT * len(plots)
194 figure, height = _create_figure(plot_height)
showardce12f552008-09-19 00:48:59 +0000195
196 if single:
showarde5ae1652009-02-11 23:37:20 +0000197 subplot = figure.add_subplot(1, 1, 1)
showardce12f552008-09-19 00:48:59 +0000198
199 # Plot all the data
showarde5ae1652009-02-11 23:37:20 +0000200 for plot_index, (plot, color) in enumerate(zip(plots, _colors(len(plots)))):
showardfbdab0b2009-04-29 19:49:50 +0000201 needs_invert = (plot['label'] in plot_info.inverted_series)
showardce12f552008-09-19 00:48:59 +0000202
203 # Add a new subplot, if user wants multiple subplots
204 # Also handle axis inversion for subplots here
205 if not single:
showarde5ae1652009-02-11 23:37:20 +0000206 subplot = figure.add_subplot(len(plots), 1, plot_index + 1)
207 subplot.set_title(plot['label'])
showardce12f552008-09-19 00:48:59 +0000208 if needs_invert:
showarde5ae1652009-02-11 23:37:20 +0000209 # for separate plots, just invert the y-axis
210 subplot.set_ylim(1, 0)
showardce12f552008-09-19 00:48:59 +0000211 elif needs_invert:
showarde5ae1652009-02-11 23:37:20 +0000212 # for a shared plot (normalized data), need to invert the y values
213 # manually, since all plots share a y-axis
showardce12f552008-09-19 00:48:59 +0000214 plot['y'] = [-y for y in plot['y']]
215
216 # Plot the series
showarde5ae1652009-02-11 23:37:20 +0000217 subplot.set_xticks(range(0, len(labels)))
218 subplot.set_xlim(-1, len(labels))
showardce12f552008-09-19 00:48:59 +0000219 if single:
showarde5ae1652009-02-11 23:37:20 +0000220 lines += subplot.plot(plot['x'], plot['y'], label=plot['label'],
221 marker=_MULTIPLE_PLOT_MARKER_TYPE,
222 markersize=_MULTIPLE_PLOT_MARKER_SIZE)
223 error_bar_color = lines[-1].get_color()
showardce12f552008-09-19 00:48:59 +0000224 else:
showarde5ae1652009-02-11 23:37:20 +0000225 lines += subplot.plot(plot['x'], plot['y'], _SINGLE_PLOT_STYLE,
226 label=plot['label'])
227 error_bar_color = _SINGLE_PLOT_ERROR_BAR_COLOR
showardce12f552008-09-19 00:48:59 +0000228 if plot['errors']:
showarde5ae1652009-02-11 23:37:20 +0000229 subplot.errorbar(plot['x'], plot['y'], linestyle='None',
230 yerr=plot['errors'], color=error_bar_color)
231 subplot.set_xticklabels([])
showardce12f552008-09-19 00:48:59 +0000232
showarde5ae1652009-02-11 23:37:20 +0000233 # Construct the information for the drilldowns.
234 # We need to do this in a separate loop so that all the data is in
235 # matplotlib before we start calling transform(); otherwise, it will return
236 # incorrect data because it hasn't finished adjusting axis limits.
showardce12f552008-09-19 00:48:59 +0000237 for line in lines:
238
239 # Get the pixel coordinates of each point on the figure
240 x = line.get_xdata()
241 y = line.get_ydata()
242 label = line.get_label()
243 icoords = line.get_transform().transform(zip(x,y))
244
245 # Get the appropriate drilldown query
showardfbdab0b2009-04-29 19:49:50 +0000246 drill = plot_info.query_dict['__' + label + '__']
showardce12f552008-09-19 00:48:59 +0000247
248 # Set the title attributes (hover-over tool-tips)
249 x_labels = [labels[x_val] for x_val in x]
250 titles = ['%s - %s: %f' % (label, x_label, y_val)
251 for x_label, y_val in zip(x_labels, y)]
252
253 # Get the appropriate parameters for the drilldown query
showard3b2b9302009-04-15 21:53:47 +0000254 params = [dict(query=drill, series=line.get_label(), param=x_label)
showardce12f552008-09-19 00:48:59 +0000255 for x_label in x_labels]
256
showarde5ae1652009-02-11 23:37:20 +0000257 area_data += [dict(left=ix - 5, top=height - iy - 5,
258 right=ix + 5, bottom=height - iy + 5,
259 title= title,
showardfbdab0b2009-04-29 19:49:50 +0000260 callback=plot_info.drilldown_callback,
showard3b2b9302009-04-15 21:53:47 +0000261 callback_arguments=param_dict)
262 for (ix, iy), title, param_dict
showardce12f552008-09-19 00:48:59 +0000263 in zip(icoords, titles, params)]
264
showarde5ae1652009-02-11 23:37:20 +0000265 subplot.set_xticklabels(labels, rotation=90, size=_LINE_XTICK_LABELS_SIZE)
showardce12f552008-09-19 00:48:59 +0000266
267 # Show the legend if there are not multiple subplots
268 if single:
showarde5ae1652009-02-11 23:37:20 +0000269 font_properties = matplotlib.font_manager.FontProperties(
270 size=_LEGEND_FONT_SIZE)
271 legend = figure.legend(lines, [plot['label'] for plot in plots],
272 prop=font_properties,
273 handlelen=_LEGEND_HANDLE_LENGTH,
274 numpoints=_LEGEND_NUM_POINTS)
275 # Workaround for matplotlib not keeping all line markers in the legend -
276 # it seems if we don't do this, matplotlib won't keep all the line
277 # markers in the legend.
278 for line in legend.get_lines():
279 line.set_marker(_LEGEND_MARKER_TYPE)
showardce12f552008-09-19 00:48:59 +0000280
showarde5ae1652009-02-11 23:37:20 +0000281 return (figure, area_data)
showardce12f552008-09-19 00:48:59 +0000282
283
showarde5ae1652009-02-11 23:37:20 +0000284def _get_adjusted_bar(x, bar_width, series_index, num_plots):
showardce12f552008-09-19 00:48:59 +0000285 """\
286 Adjust the list 'x' to take the multiple series into account. Each series
showarde5ae1652009-02-11 23:37:20 +0000287 should be shifted such that the middle series lies at the appropriate x-axis
288 tick with the other bars around it. For example, if we had four series
289 (i.e. four bars per x value), we want to shift the left edges of the bars as
290 such:
291 Bar 1: -2 * width
292 Bar 2: -width
293 Bar 3: none
294 Bar 4: width
showardce12f552008-09-19 00:48:59 +0000295 """
showarde5ae1652009-02-11 23:37:20 +0000296 adjust = (-0.5 * num_plots - 1 + series_index) * bar_width
showardce12f552008-09-19 00:48:59 +0000297 return [x_val + adjust for x_val in x]
298
299
showarde5ae1652009-02-11 23:37:20 +0000300# TODO(showard): merge much of this function with _create_line by extracting and
301# parameterizing methods
showardfbdab0b2009-04-29 19:49:50 +0000302def _create_bar(plots, labels, plot_info):
showardce12f552008-09-19 00:48:59 +0000303 """\
304 Given all the data for the metrics, create a line plot.
305
showarde5ae1652009-02-11 23:37:20 +0000306 plots: list of dicts containing the plot data.
showardce12f552008-09-19 00:48:59 +0000307 x: list of x-values for the plot
308 y: list of corresponding y-values
309 errors: errors for each data point, or None if no error information
310 available
311 label: plot title
showarde5ae1652009-02-11 23:37:20 +0000312 labels: list of x-tick labels
showardfbdab0b2009-04-29 19:49:50 +0000313 plot_info: a MetricsPlot
showardce12f552008-09-19 00:48:59 +0000314 """
315
316 area_data = []
317 bars = []
showarde5ae1652009-02-11 23:37:20 +0000318 figure, height = _create_figure(_SINGLE_PLOT_HEIGHT)
showardce12f552008-09-19 00:48:59 +0000319
320 # Set up the plot
showarde5ae1652009-02-11 23:37:20 +0000321 subplot = figure.add_subplot(1, 1, 1)
322 subplot.set_xticks(range(0, len(labels)))
323 subplot.set_xlim(-1, len(labels))
324 subplot.set_xticklabels(labels, rotation=90, size=_BAR_XTICK_LABELS_SIZE)
325 # draw a bold line at y=0, making it easier to tell if bars are dipping
326 # below the axis or not.
327 subplot.axhline(linewidth=2, color='black')
showardce12f552008-09-19 00:48:59 +0000328
329 # width here is the width for each bar in the plot. Matplotlib default is
330 # 0.8.
331 width = 0.8 / len(plots)
showardce12f552008-09-19 00:48:59 +0000332
333 # Plot the data
showarde5ae1652009-02-11 23:37:20 +0000334 for plot_index, (plot, color) in enumerate(zip(plots, _colors(len(plots)))):
showardce12f552008-09-19 00:48:59 +0000335 # Invert the y-axis if needed
showardfbdab0b2009-04-29 19:49:50 +0000336 if plot['label'] in plot_info.inverted_series:
showardce12f552008-09-19 00:48:59 +0000337 plot['y'] = [-y for y in plot['y']]
338
showarde5ae1652009-02-11 23:37:20 +0000339 adjusted_x = _get_adjusted_bar(plot['x'], width, plot_index + 1,
340 len(plots))
341 bar_data = subplot.bar(adjusted_x, plot['y'],
342 width=width, yerr=plot['errors'],
343 facecolor=color,
344 label=plot['label'])
345 bars.append(bar_data[0])
showardce12f552008-09-19 00:48:59 +0000346
showarde5ae1652009-02-11 23:37:20 +0000347 # Construct the information for the drilldowns.
348 # See comment in _create_line for why we need a separate loop to do this.
349 for plot_index, plot in enumerate(plots):
350 adjusted_x = _get_adjusted_bar(plot['x'], width, plot_index + 1,
351 len(plots))
showardce12f552008-09-19 00:48:59 +0000352
353 # Let matplotlib plot the data, so that we can get the data-to-image
354 # coordinate transforms
showarde5ae1652009-02-11 23:37:20 +0000355 line = subplot.plot(adjusted_x, plot['y'], linestyle='None')[0]
356 label = plot['label']
357 upper_left_coords = line.get_transform().transform(zip(adjusted_x,
358 plot['y']))
359 bottom_right_coords = line.get_transform().transform(
showardce12f552008-09-19 00:48:59 +0000360 [(x + width, 0) for x in adjusted_x])
361
362 # Get the drilldown query
showardfbdab0b2009-04-29 19:49:50 +0000363 drill = plot_info.query_dict['__' + label + '__']
showardce12f552008-09-19 00:48:59 +0000364
365 # Set the title attributes
366 x_labels = [labels[x] for x in plot['x']]
367 titles = ['%s - %s: %f' % (plot['label'], label, y)
368 for label, y in zip(x_labels, plot['y'])]
showard3b2b9302009-04-15 21:53:47 +0000369 params = [dict(query=drill, series=plot['label'], param=x_label)
showardce12f552008-09-19 00:48:59 +0000370 for x_label in x_labels]
showarde5ae1652009-02-11 23:37:20 +0000371 area_data += [dict(left=ulx, top=height - uly,
372 right=brx, bottom=height - bry,
373 title=title,
showardfbdab0b2009-04-29 19:49:50 +0000374 callback=plot_info.drilldown_callback,
showard3b2b9302009-04-15 21:53:47 +0000375 callback_arguments=param_dict)
376 for (ulx, uly), (brx, bry), title, param_dict
showarde5ae1652009-02-11 23:37:20 +0000377 in zip(upper_left_coords, bottom_right_coords, titles,
378 params)]
showardce12f552008-09-19 00:48:59 +0000379
showarde5ae1652009-02-11 23:37:20 +0000380 figure.legend(bars, [plot['label'] for plot in plots])
381 return (figure, area_data)
showardce12f552008-09-19 00:48:59 +0000382
383
384def _normalize(data_values, data_errors, base_values, base_errors):
385 """\
386 Normalize the data against a baseline.
387
388 data_values: y-values for the to-be-normalized data
389 data_errors: standard deviations for the to-be-normalized data
390 base_values: list of values normalize against
391 base_errors: list of standard deviations for those base values
392 """
showardd1784472009-06-08 23:29:22 +0000393 values = []
394 for value, base in zip(data_values, base_values):
395 try:
396 values.append(100 * (value - base) / base)
397 except ZeroDivisionError:
398 # Base is 0.0 so just simplify:
399 # If value < base: append -100.0;
400 # If value == base: append 0.0 (obvious); and
mbligh1ef218d2009-08-03 16:57:56 +0000401 # If value > base: append 100.0.
showardd1784472009-06-08 23:29:22 +0000402 values.append(100 * float(cmp(value, base)))
showardce12f552008-09-19 00:48:59 +0000403
404 # Based on error for f(x,y) = 100 * (x - y) / y
405 if data_errors:
406 if not base_errors:
407 base_errors = [0] * len(data_errors)
showardd1784472009-06-08 23:29:22 +0000408 errors = []
409 for data, error, base_value, base_error in zip(
410 data_values, data_errors, base_values, base_errors):
411 try:
412 errors.append(sqrt(error**2 * (100 / base_value)**2
413 + base_error**2 * (100 * data / base_value**2)**2
414 + error * base_error * (100 / base_value**2)**2))
415 except ZeroDivisionError:
416 # Again, base is 0.0 so do the simple thing.
417 errors.append(100 * abs(error))
showardce12f552008-09-19 00:48:59 +0000418 else:
419 errors = None
420
421 return (values, errors)
422
423
showarde5ae1652009-02-11 23:37:20 +0000424def _create_png(figure):
showardce12f552008-09-19 00:48:59 +0000425 """\
426 Given the matplotlib figure, generate the PNG data for it.
427 """
428
429 # Draw the image
showarde5ae1652009-02-11 23:37:20 +0000430 canvas = matplotlib.backends.backend_agg.FigureCanvasAgg(figure)
showardce12f552008-09-19 00:48:59 +0000431 canvas.draw()
432 size = canvas.get_renderer().get_canvas_width_height()
showarde5ae1652009-02-11 23:37:20 +0000433 image_as_string = canvas.tostring_rgb()
434 image = PIL.Image.fromstring('RGB', size, image_as_string, 'raw', 'RGB', 0,
435 1)
436 image_background = PIL.Image.new(image.mode, image.size,
437 figure.get_facecolor())
showardce12f552008-09-19 00:48:59 +0000438
439 # Crop the image to remove surrounding whitespace
showarde5ae1652009-02-11 23:37:20 +0000440 non_whitespace = PIL.ImageChops.difference(image, image_background)
441 bounding_box = non_whitespace.getbbox()
442 image = image.crop(bounding_box)
showardce12f552008-09-19 00:48:59 +0000443
showarde5ae1652009-02-11 23:37:20 +0000444 image_data = StringIO.StringIO()
445 image.save(image_data, format='PNG')
showardce12f552008-09-19 00:48:59 +0000446
showarde5ae1652009-02-11 23:37:20 +0000447 return image_data.getvalue(), bounding_box
showardce12f552008-09-19 00:48:59 +0000448
449
showardfbdab0b2009-04-29 19:49:50 +0000450def _create_image_html(figure, area_data, plot_info):
showardce12f552008-09-19 00:48:59 +0000451 """\
452 Given the figure and drilldown data, construct the HTML that will render the
453 graph as a PNG image, and attach the image map to that image.
454
showarde5ae1652009-02-11 23:37:20 +0000455 figure: figure containing the drawn plot(s)
showardce12f552008-09-19 00:48:59 +0000456 area_data: list of parameters for each area of the image map. See the
showarde5ae1652009-02-11 23:37:20 +0000457 definition of the template string '_AREA_TEMPLATE'
showardfbdab0b2009-04-29 19:49:50 +0000458 plot_info: a MetricsPlot or QualHistogram
showardce12f552008-09-19 00:48:59 +0000459 """
460
showarde5ae1652009-02-11 23:37:20 +0000461 png, bbox = _create_png(figure)
showardce12f552008-09-19 00:48:59 +0000462
463 # Construct the list of image map areas
showard3b2b9302009-04-15 21:53:47 +0000464 areas = [_AREA_TEMPLATE %
465 (data['left'] - bbox[0], data['top'] - bbox[1],
466 data['right'] - bbox[0], data['bottom'] - bbox[1],
467 data['title'], data['callback'],
showardfbdab0b2009-04-29 19:49:50 +0000468 _json_encoder.encode(data['callback_arguments'])
469 .replace('"', '&quot;'))
showardce12f552008-09-19 00:48:59 +0000470 for data in area_data]
471
showardfbdab0b2009-04-29 19:49:50 +0000472 map_name = plot_info.drilldown_callback + '_map'
473 return _HTML_TEMPLATE % (base64.b64encode(png), map_name, map_name,
showarde5ae1652009-02-11 23:37:20 +0000474 '\n'.join(areas))
475
476
477def _find_plot_by_label(plots, label):
478 for index, plot in enumerate(plots):
mbligh1ef218d2009-08-03 16:57:56 +0000479 if plot['label'] == label:
480 return index
showarde5ae1652009-02-11 23:37:20 +0000481 raise ValueError('no plot labeled "%s" found' % label)
showardce12f552008-09-19 00:48:59 +0000482
483
showard84f37322009-03-03 02:16:59 +0000484def _normalize_to_series(plots, base_series):
485 base_series_index = _find_plot_by_label(plots, base_series)
486 base_plot = plots[base_series_index]
487 base_xs = base_plot['x']
488 base_values = base_plot['y']
489 base_errors = base_plot['errors']
490 del plots[base_series_index]
491
492 for plot in plots:
493 old_xs, old_values, old_errors = plot['x'], plot['y'], plot['errors']
494 new_xs, new_values, new_errors = [], [], []
495 new_base_values, new_base_errors = [], []
496 # Select only points in the to-be-normalized data that have a
497 # corresponding baseline value
498 for index, x_value in enumerate(old_xs):
499 try:
500 base_index = base_xs.index(x_value)
501 except ValueError:
502 continue
503
504 new_xs.append(x_value)
505 new_values.append(old_values[index])
506 new_base_values.append(base_values[base_index])
507 if old_errors:
508 new_errors.append(old_errors[index])
509 new_base_errors.append(base_errors[base_index])
510
511 if not new_xs:
512 raise NoDataError('No normalizable data for series ' +
513 plot['label'])
514 plot['x'] = new_xs
515 plot['y'] = new_values
516 if old_errors:
517 plot['errors'] = new_errors
518
519 plot['y'], plot['errors'] = _normalize(plot['y'], plot['errors'],
520 new_base_values,
521 new_base_errors)
522
523
showardfbdab0b2009-04-29 19:49:50 +0000524def _create_metrics_plot_helper(plot_info, extra_text=None):
showardce12f552008-09-19 00:48:59 +0000525 """
showardfbdab0b2009-04-29 19:49:50 +0000526 Create a metrics plot of the given plot data.
527 plot_info: a MetricsPlot object.
528 extra_text: text to show at the uppper-left of the graph
showardce12f552008-09-19 00:48:59 +0000529
showardfbdab0b2009-04-29 19:49:50 +0000530 TODO(showard): move some/all of this logic into methods on MetricsPlot
531 """
532 query = plot_info.query_dict['__main__']
showard56e93772008-10-06 10:06:22 +0000533 cursor = readonly_connection.connection().cursor()
showardce12f552008-09-19 00:48:59 +0000534 cursor.execute(query)
535
536 if not cursor.rowcount:
537 raise NoDataError('query did not return any data')
showard7fc993c2008-09-22 16:21:23 +0000538 rows = cursor.fetchall()
539 # "transpose" rows, so columns[0] is all the values from the first column,
540 # etc.
541 columns = zip(*rows)
showardce12f552008-09-19 00:48:59 +0000542
showardce12f552008-09-19 00:48:59 +0000543 plots = []
showard7fc993c2008-09-22 16:21:23 +0000544 labels = [str(label) for label in columns[0]]
showardce12f552008-09-19 00:48:59 +0000545 needs_resort = (cursor.description[0][0] == 'kernel')
546
547 # Collect all the data for the plot
548 col = 1
549 while col < len(cursor.description):
showard7fc993c2008-09-22 16:21:23 +0000550 y = columns[col]
showardce12f552008-09-19 00:48:59 +0000551 label = cursor.description[col][0]
552 col += 1
553 if (col < len(cursor.description) and
554 'errors-' + label == cursor.description[col][0]):
showard7fc993c2008-09-22 16:21:23 +0000555 errors = columns[col]
showardce12f552008-09-19 00:48:59 +0000556 col += 1
557 else:
558 errors = None
559 if needs_resort:
560 y = _resort(labels, y)
561 if errors:
562 errors = _resort(labels, errors)
563
showarde5ae1652009-02-11 23:37:20 +0000564 x = [index for index, value in enumerate(y) if value is not None]
565 if not x:
566 raise NoDataError('No data for series ' + label)
showardce12f552008-09-19 00:48:59 +0000567 y = [y[i] for i in x]
568 if errors:
showarde5ae1652009-02-11 23:37:20 +0000569 errors = [errors[i] for i in x]
showardce12f552008-09-19 00:48:59 +0000570 plots.append({
571 'label': label,
572 'x': x,
573 'y': y,
574 'errors': errors
575 })
576
577 if needs_resort:
578 labels = _resort(labels, labels)
579
580 # Normalize the data if necessary
showardfbdab0b2009-04-29 19:49:50 +0000581 normalize_to = plot_info.normalize_to
582 if normalize_to == 'first' or normalize_to.startswith('x__'):
583 if normalize_to != 'first':
584 baseline = normalize_to[3:]
showardce12f552008-09-19 00:48:59 +0000585 try:
586 baseline_index = labels.index(baseline)
587 except ValueError:
588 raise ValidationError({
589 'Normalize' : 'Invalid baseline %s' % baseline
590 })
591 for plot in plots:
showardfbdab0b2009-04-29 19:49:50 +0000592 if normalize_to == 'first':
showardce12f552008-09-19 00:48:59 +0000593 plot_index = 0
594 else:
595 try:
596 plot_index = plot['x'].index(baseline_index)
597 # if the value is not found, then we cannot normalize
598 except ValueError:
599 raise ValidationError({
600 'Normalize' : ('%s does not have a value for %s'
showardfbdab0b2009-04-29 19:49:50 +0000601 % (plot['label'], normalize_to[3:]))
showardce12f552008-09-19 00:48:59 +0000602 })
603 base_values = [plot['y'][plot_index]] * len(plot['y'])
604 if plot['errors']:
605 base_errors = [plot['errors'][plot_index]] * len(plot['errors'])
606 plot['y'], plot['errors'] = _normalize(plot['y'], plot['errors'],
607 base_values,
608 None or base_errors)
609
showardfbdab0b2009-04-29 19:49:50 +0000610 elif normalize_to.startswith('series__'):
611 base_series = normalize_to[8:]
showard84f37322009-03-03 02:16:59 +0000612 _normalize_to_series(plots, base_series)
showardce12f552008-09-19 00:48:59 +0000613
614 # Call the appropriate function to draw the line or bar plot
showardfbdab0b2009-04-29 19:49:50 +0000615 if plot_info.is_line:
616 figure, area_data = _create_line(plots, labels, plot_info)
showardce12f552008-09-19 00:48:59 +0000617 else:
showardfbdab0b2009-04-29 19:49:50 +0000618 figure, area_data = _create_bar(plots, labels, plot_info)
showardce12f552008-09-19 00:48:59 +0000619
showarde5ae1652009-02-11 23:37:20 +0000620 # TODO(showard): extract these magic numbers to named constants
showardce12f552008-09-19 00:48:59 +0000621 if extra_text:
622 text_y = .95 - .0075 * len(plots)
showarde5ae1652009-02-11 23:37:20 +0000623 figure.text(.1, text_y, extra_text, size='xx-small')
showardce12f552008-09-19 00:48:59 +0000624
showardfbdab0b2009-04-29 19:49:50 +0000625 return (figure, area_data)
showardce12f552008-09-19 00:48:59 +0000626
showardce12f552008-09-19 00:48:59 +0000627
showardde01b762009-05-01 00:09:11 +0000628def create_metrics_plot(query_dict, plot_type, inverted_series, normalize_to,
showardfbdab0b2009-04-29 19:49:50 +0000629 drilldown_callback, extra_text=None):
showardde01b762009-05-01 00:09:11 +0000630 plot_info = MetricsPlot(query_dict, plot_type, inverted_series,
showardfbdab0b2009-04-29 19:49:50 +0000631 normalize_to, drilldown_callback)
632 figure, area_data = _create_metrics_plot_helper(plot_info, extra_text)
633 return _create_image_html(figure, area_data, plot_info)
showardce12f552008-09-19 00:48:59 +0000634
635
636def _get_hostnames_in_bucket(hist_data, bucket):
637 """\
638 Get all the hostnames that constitute a particular bucket in the histogram.
639
640 hist_data: list containing tuples of (hostname, pass_rate)
641 bucket: tuple containing the (low, high) values of the target bucket
642 """
643
showarde5ae1652009-02-11 23:37:20 +0000644 return [hostname for hostname, pass_rate in hist_data
645 if bucket[0] <= pass_rate < bucket[1]]
showardce12f552008-09-19 00:48:59 +0000646
647
showardfbdab0b2009-04-29 19:49:50 +0000648def _create_qual_histogram_helper(plot_info, extra_text=None):
showardce12f552008-09-19 00:48:59 +0000649 """\
650 Create a machine qualification histogram of the given data.
651
showardfbdab0b2009-04-29 19:49:50 +0000652 plot_info: a QualificationHistogram
showardce12f552008-09-19 00:48:59 +0000653 extra_text: text to show at the upper-left of the graph
showardfbdab0b2009-04-29 19:49:50 +0000654
655 TODO(showard): move much or all of this into methods on
656 QualificationHistogram
showardce12f552008-09-19 00:48:59 +0000657 """
showard56e93772008-10-06 10:06:22 +0000658 cursor = readonly_connection.connection().cursor()
showardfbdab0b2009-04-29 19:49:50 +0000659 cursor.execute(plot_info.query)
showardce12f552008-09-19 00:48:59 +0000660
661 if not cursor.rowcount:
662 raise NoDataError('query did not return any data')
663
664 # Lists to store the plot data.
665 # hist_data store tuples of (hostname, pass_rate) for machines that have
666 # pass rates between 0 and 100%, exclusive.
667 # no_tests is a list of machines that have run none of the selected tests
668 # no_pass is a list of machines with 0% pass rate
669 # perfect is a list of machines with a 100% pass rate
670 hist_data = []
671 no_tests = []
672 no_pass = []
673 perfect = []
674
675 # Construct the lists of data to plot
676 for hostname, total, good in cursor.fetchall():
showarde5ae1652009-02-11 23:37:20 +0000677 if total == 0:
showardce12f552008-09-19 00:48:59 +0000678 no_tests.append(hostname)
showarde5ae1652009-02-11 23:37:20 +0000679 continue
680
681 if good == 0:
682 no_pass.append(hostname)
683 elif good == total:
684 perfect.append(hostname)
685 else:
686 percentage = 100.0 * good / total
687 hist_data.append((hostname, percentage))
showardce12f552008-09-19 00:48:59 +0000688
showardfbdab0b2009-04-29 19:49:50 +0000689 interval = plot_info.interval
showardce12f552008-09-19 00:48:59 +0000690 bins = range(0, 100, interval)
691 if bins[-1] != 100:
692 bins.append(bins[-1] + interval)
693
showarde5ae1652009-02-11 23:37:20 +0000694 figure, height = _create_figure(_SINGLE_PLOT_HEIGHT)
695 subplot = figure.add_subplot(1, 1, 1)
showardce12f552008-09-19 00:48:59 +0000696
697 # Plot the data and get all the bars plotted
showarde5ae1652009-02-11 23:37:20 +0000698 _,_, bars = subplot.hist([data[1] for data in hist_data],
showardce12f552008-09-19 00:48:59 +0000699 bins=bins, align='left')
showarde5ae1652009-02-11 23:37:20 +0000700 bars += subplot.bar([-interval], len(no_pass),
showardce12f552008-09-19 00:48:59 +0000701 width=interval, align='center')
showarde5ae1652009-02-11 23:37:20 +0000702 bars += subplot.bar([bins[-1]], len(perfect),
showardce12f552008-09-19 00:48:59 +0000703 width=interval, align='center')
showarde5ae1652009-02-11 23:37:20 +0000704 bars += subplot.bar([-3 * interval], len(no_tests),
showardce12f552008-09-19 00:48:59 +0000705 width=interval, align='center')
706
707 buckets = [(bin, min(bin + interval, 100)) for bin in bins[:-1]]
showarde5ae1652009-02-11 23:37:20 +0000708 # set the x-axis range to cover all the normal bins plus the three "special"
709 # ones - N/A (3 intervals left), 0% (1 interval left) ,and 100% (far right)
710 subplot.set_xlim(-4 * interval, bins[-1] + interval)
711 subplot.set_xticks([-3 * interval, -interval] + bins + [100 + interval])
712 subplot.set_xticklabels(['N/A', '0%'] +
showardce12f552008-09-19 00:48:59 +0000713 ['%d%% - <%d%%' % bucket for bucket in buckets] +
714 ['100%'], rotation=90, size='small')
715
716 # Find the coordinates on the image for each bar
717 x = []
718 y = []
719 for bar in bars:
720 x.append(bar.get_x())
721 y.append(bar.get_height())
showarde5ae1652009-02-11 23:37:20 +0000722 f = subplot.plot(x, y, linestyle='None')[0]
723 upper_left_coords = f.get_transform().transform(zip(x, y))
724 bottom_right_coords = f.get_transform().transform(
showardce12f552008-09-19 00:48:59 +0000725 [(x_val + interval, 0) for x_val in x])
726
showardce12f552008-09-19 00:48:59 +0000727 # Set the title attributes
728 titles = ['%d%% - <%d%%: %d machines' % (bucket[0], bucket[1], y_val)
729 for bucket, y_val in zip(buckets, y)]
730 titles.append('0%%: %d machines' % len(no_pass))
731 titles.append('100%%: %d machines' % len(perfect))
732 titles.append('N/A: %d machines' % len(no_tests))
733
734 # Get the hostnames for each bucket in the histogram
735 names_list = [_get_hostnames_in_bucket(hist_data, bucket)
736 for bucket in buckets]
737 names_list += [no_pass, perfect]
showarde5ae1652009-02-11 23:37:20 +0000738
showardfbdab0b2009-04-29 19:49:50 +0000739 if plot_info.filter_string:
740 plot_info.filter_string += ' AND '
showardce12f552008-09-19 00:48:59 +0000741
showard3b2b9302009-04-15 21:53:47 +0000742 # Construct the list of drilldown parameters to be passed when the user
showardce12f552008-09-19 00:48:59 +0000743 # clicks on the bar.
showardce12f552008-09-19 00:48:59 +0000744 params = []
745 for names in names_list:
746 if names:
showarde5ae1652009-02-11 23:37:20 +0000747 hostnames = ','.join(_quote(hostname) for hostname in names)
748 hostname_filter = 'hostname IN (%s)' % hostnames
showardfbdab0b2009-04-29 19:49:50 +0000749 full_filter = plot_info.filter_string + hostname_filter
showard3b2b9302009-04-15 21:53:47 +0000750 params.append({'type': 'normal',
751 'filterString': full_filter})
showardce12f552008-09-19 00:48:59 +0000752 else:
showard3b2b9302009-04-15 21:53:47 +0000753 params.append({'type': 'empty'})
showarde5ae1652009-02-11 23:37:20 +0000754
showard3b2b9302009-04-15 21:53:47 +0000755 params.append({'type': 'not_applicable',
756 'hosts': '<br />'.join(no_tests)})
showardce12f552008-09-19 00:48:59 +0000757
showarde5ae1652009-02-11 23:37:20 +0000758 area_data = [dict(left=ulx, top=height - uly,
759 right=brx, bottom=height - bry,
showardfbdab0b2009-04-29 19:49:50 +0000760 title=title, callback=plot_info.drilldown_callback,
showard3b2b9302009-04-15 21:53:47 +0000761 callback_arguments=param_dict)
762 for (ulx, uly), (brx, bry), title, param_dict
763 in zip(upper_left_coords, bottom_right_coords, titles, params)]
showardce12f552008-09-19 00:48:59 +0000764
showarde5ae1652009-02-11 23:37:20 +0000765 # TODO(showard): extract these magic numbers to named constants
showardce12f552008-09-19 00:48:59 +0000766 if extra_text:
showarde5ae1652009-02-11 23:37:20 +0000767 figure.text(.1, .95, extra_text, size='xx-small')
showardce12f552008-09-19 00:48:59 +0000768
showardfbdab0b2009-04-29 19:49:50 +0000769 return (figure, area_data)
showardce12f552008-09-19 00:48:59 +0000770
771
showardfbdab0b2009-04-29 19:49:50 +0000772def create_qual_histogram(query, filter_string, interval, drilldown_callback,
773 extra_text=None):
774 plot_info = QualificationHistogram(query, filter_string, interval,
775 drilldown_callback)
776 figure, area_data = _create_qual_histogram_helper(plot_info, extra_text)
777 return _create_image_html(figure, area_data, plot_info)
showardce12f552008-09-19 00:48:59 +0000778
779
780def create_embedded_plot(model, update_time):
781 """\
782 Given an EmbeddedGraphingQuery object, generate the PNG image for it.
783
784 model: EmbeddedGraphingQuery object
785 update_time: 'Last updated' time
786 """
787
showardce12f552008-09-19 00:48:59 +0000788 params = pickle.loads(model.params)
showard66b510e2009-05-02 00:45:10 +0000789 extra_text = 'Last updated: %s' % update_time
showardce12f552008-09-19 00:48:59 +0000790
showarde5ae1652009-02-11 23:37:20 +0000791 if model.graph_type == 'metrics':
showard66b510e2009-05-02 00:45:10 +0000792 plot_info = MetricsPlot(query_dict=params['queries'],
793 plot_type=params['plot'],
794 inverted_series=params['invert'],
795 normalize_to=None,
796 drilldown_callback='')
797 figure, areas_unused = _create_metrics_plot_helper(plot_info,
798 extra_text)
showarde5ae1652009-02-11 23:37:20 +0000799 elif model.graph_type == 'qual':
showard66b510e2009-05-02 00:45:10 +0000800 plot_info = QualificationHistogram(
801 query=params['query'], filter_string=params['filter_string'],
802 interval=params['interval'], drilldown_callback='')
803 figure, areas_unused = _create_qual_histogram_helper(plot_info,
804 extra_text)
showarde5ae1652009-02-11 23:37:20 +0000805 else:
806 raise ValueError('Invalid graph_type %s' % model.graph_type)
807
808 image, bounding_box_unused = _create_png(figure)
809 return image
showardce12f552008-09-19 00:48:59 +0000810
811
812_cache_timeout = global_config.global_config.get_config_value(
showard250d84d2010-01-12 21:59:48 +0000813 'AUTOTEST_WEB', 'graph_cache_creation_timeout_minutes')
showardce12f552008-09-19 00:48:59 +0000814
815
816def handle_plot_request(id, max_age):
817 """\
818 Given the embedding id of a graph, generate a PNG of the embedded graph
819 associated with that id.
820
821 id: id of the embedded graph
822 max_age: maximum age, in minutes, that a cached version should be held
823 """
824 model = models.EmbeddedGraphingQuery.objects.get(id=id)
825
826 # Check if the cached image needs to be updated
827 now = datetime.datetime.now()
828 update_time = model.last_updated + datetime.timedelta(minutes=int(max_age))
829 if now > update_time:
830 cursor = django.db.connection.cursor()
831
832 # We want this query to update the refresh_time only once, even if
833 # multiple threads are running it at the same time. That is, only the
834 # first thread will win the race, and it will be the one to update the
835 # cached image; all other threads will show that they updated 0 rows
836 query = """
837 UPDATE embedded_graphing_queries
838 SET refresh_time = NOW()
839 WHERE id = %s AND (
840 refresh_time IS NULL OR
841 refresh_time + INTERVAL %s MINUTE < NOW()
842 )
843 """
844 cursor.execute(query, (id, _cache_timeout))
845
846 # Only refresh the cached image if we were successful in updating the
847 # refresh time
848 if cursor.rowcount:
849 model.cached_png = create_embedded_plot(model, now.ctime())
850 model.last_updated = now
851 model.refresh_time = None
852 model.save()
853
854 return model.cached_png