showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 1 | import base64, os, tempfile, operator, pickle, datetime, django.db |
showard | 90e2726 | 2009-06-08 23:25:19 +0000 | [diff] [blame] | 2 | import os.path, getpass |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 3 | from math import sqrt |
| 4 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 5 | # When you import matplotlib, it tries to write some temp files for better |
showard | 90e2726 | 2009-06-08 23:25:19 +0000 | [diff] [blame] | 6 | # 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 |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 10 | # away after they are written, though. |
showard | 90e2726 | 2009-06-08 23:25:19 +0000 | [diff] [blame] | 11 | |
| 12 | temp_dir = os.path.join(tempfile.gettempdir(), |
| 13 | '.matplotlib-%s' % getpass.getuser()) |
| 14 | if not os.path.exists(temp_dir): |
| 15 | os.mkdir(temp_dir) |
| 16 | os.environ['MPLCONFIGDIR'] = temp_dir |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 17 | |
| 18 | import matplotlib |
| 19 | matplotlib.use('Agg') |
| 20 | |
| 21 | import matplotlib.figure, matplotlib.backends.backend_agg |
| 22 | import StringIO, colorsys, PIL.Image, PIL.ImageChops |
| 23 | from autotest_lib.frontend.afe import readonly_connection |
| 24 | from autotest_lib.frontend.afe.model_logic import ValidationError |
showard | 250d84d | 2010-01-12 21:59:48 +0000 | [diff] [blame^] | 25 | from autotest_lib.frontend.afe.simplejson import encoder |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 26 | from autotest_lib.client.common_lib import global_config |
showard | 250d84d | 2010-01-12 21:59:48 +0000 | [diff] [blame^] | 27 | from autotest_lib.frontend.tko import models, tko_rpc_utils |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 28 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 29 | _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 |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 48 | |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 49 | _json_encoder = encoder.JSONEncoder() |
| 50 | |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 51 | class NoDataError(Exception): |
| 52 | """\ |
| 53 | Exception to raise if the graphing query returned an empty resultset. |
| 54 | """ |
| 55 | |
| 56 | |
| 57 | def _colors(n): |
| 58 | """\ |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 59 | Generator function for creating n colors. The return value is a tuple |
| 60 | representing the RGB of the color. |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 61 | """ |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 62 | for i in xrange(n): |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 63 | yield colorsys.hsv_to_rgb(float(i) / n, 1.0, 1.0) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 64 | |
| 65 | |
| 66 | def _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. |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 77 | return [pair[1] for pair in resorted_pairs] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 78 | |
| 79 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 80 | def _quote(string): |
| 81 | return "%s%s%s" % ("'", string.replace("'", r"\'"), "'") |
| 82 | |
| 83 | |
| 84 | _HTML_TEMPLATE = """\ |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 85 | <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 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 91 | _AREA_TEMPLATE = """\ |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 92 | <area shape="rect" coords="%i,%i,%i,%i" title="%s" |
| 93 | href="#" |
| 94 | onclick="%s(%s); return false;">""" |
| 95 | |
| 96 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 97 | class 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 | |
| 135 | class 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 | |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 158 | def _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 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 164 | 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) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 169 | |
| 170 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 171 | def _create_line(plots, labels, plot_info): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 172 | """\ |
| 173 | Given all the data for the metrics, create a line plot. |
| 174 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 175 | plots: list of dicts containing the plot data. Each dict contains: |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 176 | 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 |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 181 | labels: list of x-tick labels |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 182 | plot_info: a MetricsPlot |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 183 | """ |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 184 | # when we're doing any kind of normalization, all series get put into a |
| 185 | # single plot |
| 186 | single = bool(plot_info.normalize_to) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 187 | |
| 188 | area_data = [] |
| 189 | lines = [] |
| 190 | if single: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 191 | plot_height = _SINGLE_PLOT_HEIGHT |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 192 | else: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 193 | plot_height = _MULTIPLE_PLOT_HEIGHT_PER_PLOT * len(plots) |
| 194 | figure, height = _create_figure(plot_height) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 195 | |
| 196 | if single: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 197 | subplot = figure.add_subplot(1, 1, 1) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 198 | |
| 199 | # Plot all the data |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 200 | for plot_index, (plot, color) in enumerate(zip(plots, _colors(len(plots)))): |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 201 | needs_invert = (plot['label'] in plot_info.inverted_series) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 202 | |
| 203 | # Add a new subplot, if user wants multiple subplots |
| 204 | # Also handle axis inversion for subplots here |
| 205 | if not single: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 206 | subplot = figure.add_subplot(len(plots), 1, plot_index + 1) |
| 207 | subplot.set_title(plot['label']) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 208 | if needs_invert: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 209 | # for separate plots, just invert the y-axis |
| 210 | subplot.set_ylim(1, 0) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 211 | elif needs_invert: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 212 | # for a shared plot (normalized data), need to invert the y values |
| 213 | # manually, since all plots share a y-axis |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 214 | plot['y'] = [-y for y in plot['y']] |
| 215 | |
| 216 | # Plot the series |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 217 | subplot.set_xticks(range(0, len(labels))) |
| 218 | subplot.set_xlim(-1, len(labels)) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 219 | if single: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 220 | 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() |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 224 | else: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 225 | lines += subplot.plot(plot['x'], plot['y'], _SINGLE_PLOT_STYLE, |
| 226 | label=plot['label']) |
| 227 | error_bar_color = _SINGLE_PLOT_ERROR_BAR_COLOR |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 228 | if plot['errors']: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 229 | subplot.errorbar(plot['x'], plot['y'], linestyle='None', |
| 230 | yerr=plot['errors'], color=error_bar_color) |
| 231 | subplot.set_xticklabels([]) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 232 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 233 | # 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. |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 237 | 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 |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 246 | drill = plot_info.query_dict['__' + label + '__'] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 247 | |
| 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 |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 254 | params = [dict(query=drill, series=line.get_label(), param=x_label) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 255 | for x_label in x_labels] |
| 256 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 257 | area_data += [dict(left=ix - 5, top=height - iy - 5, |
| 258 | right=ix + 5, bottom=height - iy + 5, |
| 259 | title= title, |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 260 | callback=plot_info.drilldown_callback, |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 261 | callback_arguments=param_dict) |
| 262 | for (ix, iy), title, param_dict |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 263 | in zip(icoords, titles, params)] |
| 264 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 265 | subplot.set_xticklabels(labels, rotation=90, size=_LINE_XTICK_LABELS_SIZE) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 266 | |
| 267 | # Show the legend if there are not multiple subplots |
| 268 | if single: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 269 | 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) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 280 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 281 | return (figure, area_data) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 282 | |
| 283 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 284 | def _get_adjusted_bar(x, bar_width, series_index, num_plots): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 285 | """\ |
| 286 | Adjust the list 'x' to take the multiple series into account. Each series |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 287 | 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 |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 295 | """ |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 296 | adjust = (-0.5 * num_plots - 1 + series_index) * bar_width |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 297 | return [x_val + adjust for x_val in x] |
| 298 | |
| 299 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 300 | # TODO(showard): merge much of this function with _create_line by extracting and |
| 301 | # parameterizing methods |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 302 | def _create_bar(plots, labels, plot_info): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 303 | """\ |
| 304 | Given all the data for the metrics, create a line plot. |
| 305 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 306 | plots: list of dicts containing the plot data. |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 307 | 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 |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 312 | labels: list of x-tick labels |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 313 | plot_info: a MetricsPlot |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 314 | """ |
| 315 | |
| 316 | area_data = [] |
| 317 | bars = [] |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 318 | figure, height = _create_figure(_SINGLE_PLOT_HEIGHT) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 319 | |
| 320 | # Set up the plot |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 321 | 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') |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 328 | |
| 329 | # width here is the width for each bar in the plot. Matplotlib default is |
| 330 | # 0.8. |
| 331 | width = 0.8 / len(plots) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 332 | |
| 333 | # Plot the data |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 334 | for plot_index, (plot, color) in enumerate(zip(plots, _colors(len(plots)))): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 335 | # Invert the y-axis if needed |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 336 | if plot['label'] in plot_info.inverted_series: |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 337 | plot['y'] = [-y for y in plot['y']] |
| 338 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 339 | 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]) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 346 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 347 | # 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)) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 352 | |
| 353 | # Let matplotlib plot the data, so that we can get the data-to-image |
| 354 | # coordinate transforms |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 355 | 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( |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 360 | [(x + width, 0) for x in adjusted_x]) |
| 361 | |
| 362 | # Get the drilldown query |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 363 | drill = plot_info.query_dict['__' + label + '__'] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 364 | |
| 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'])] |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 369 | params = [dict(query=drill, series=plot['label'], param=x_label) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 370 | for x_label in x_labels] |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 371 | area_data += [dict(left=ulx, top=height - uly, |
| 372 | right=brx, bottom=height - bry, |
| 373 | title=title, |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 374 | callback=plot_info.drilldown_callback, |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 375 | callback_arguments=param_dict) |
| 376 | for (ulx, uly), (brx, bry), title, param_dict |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 377 | in zip(upper_left_coords, bottom_right_coords, titles, |
| 378 | params)] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 379 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 380 | figure.legend(bars, [plot['label'] for plot in plots]) |
| 381 | return (figure, area_data) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 382 | |
| 383 | |
| 384 | def _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 | """ |
showard | d178447 | 2009-06-08 23:29:22 +0000 | [diff] [blame] | 393 | 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 |
mbligh | 1ef218d | 2009-08-03 16:57:56 +0000 | [diff] [blame] | 401 | # If value > base: append 100.0. |
showard | d178447 | 2009-06-08 23:29:22 +0000 | [diff] [blame] | 402 | values.append(100 * float(cmp(value, base))) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 403 | |
| 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) |
showard | d178447 | 2009-06-08 23:29:22 +0000 | [diff] [blame] | 408 | 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)) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 418 | else: |
| 419 | errors = None |
| 420 | |
| 421 | return (values, errors) |
| 422 | |
| 423 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 424 | def _create_png(figure): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 425 | """\ |
| 426 | Given the matplotlib figure, generate the PNG data for it. |
| 427 | """ |
| 428 | |
| 429 | # Draw the image |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 430 | canvas = matplotlib.backends.backend_agg.FigureCanvasAgg(figure) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 431 | canvas.draw() |
| 432 | size = canvas.get_renderer().get_canvas_width_height() |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 433 | 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()) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 438 | |
| 439 | # Crop the image to remove surrounding whitespace |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 440 | non_whitespace = PIL.ImageChops.difference(image, image_background) |
| 441 | bounding_box = non_whitespace.getbbox() |
| 442 | image = image.crop(bounding_box) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 443 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 444 | image_data = StringIO.StringIO() |
| 445 | image.save(image_data, format='PNG') |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 446 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 447 | return image_data.getvalue(), bounding_box |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 448 | |
| 449 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 450 | def _create_image_html(figure, area_data, plot_info): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 451 | """\ |
| 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 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 455 | figure: figure containing the drawn plot(s) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 456 | area_data: list of parameters for each area of the image map. See the |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 457 | definition of the template string '_AREA_TEMPLATE' |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 458 | plot_info: a MetricsPlot or QualHistogram |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 459 | """ |
| 460 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 461 | png, bbox = _create_png(figure) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 462 | |
| 463 | # Construct the list of image map areas |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 464 | 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'], |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 468 | _json_encoder.encode(data['callback_arguments']) |
| 469 | .replace('"', '"')) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 470 | for data in area_data] |
| 471 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 472 | map_name = plot_info.drilldown_callback + '_map' |
| 473 | return _HTML_TEMPLATE % (base64.b64encode(png), map_name, map_name, |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 474 | '\n'.join(areas)) |
| 475 | |
| 476 | |
| 477 | def _find_plot_by_label(plots, label): |
| 478 | for index, plot in enumerate(plots): |
mbligh | 1ef218d | 2009-08-03 16:57:56 +0000 | [diff] [blame] | 479 | if plot['label'] == label: |
| 480 | return index |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 481 | raise ValueError('no plot labeled "%s" found' % label) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 482 | |
| 483 | |
showard | 84f3732 | 2009-03-03 02:16:59 +0000 | [diff] [blame] | 484 | def _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 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 524 | def _create_metrics_plot_helper(plot_info, extra_text=None): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 525 | """ |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 526 | 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 |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 529 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 530 | TODO(showard): move some/all of this logic into methods on MetricsPlot |
| 531 | """ |
| 532 | query = plot_info.query_dict['__main__'] |
showard | 56e9377 | 2008-10-06 10:06:22 +0000 | [diff] [blame] | 533 | cursor = readonly_connection.connection().cursor() |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 534 | cursor.execute(query) |
| 535 | |
| 536 | if not cursor.rowcount: |
| 537 | raise NoDataError('query did not return any data') |
showard | 7fc993c | 2008-09-22 16:21:23 +0000 | [diff] [blame] | 538 | rows = cursor.fetchall() |
| 539 | # "transpose" rows, so columns[0] is all the values from the first column, |
| 540 | # etc. |
| 541 | columns = zip(*rows) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 542 | |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 543 | plots = [] |
showard | 7fc993c | 2008-09-22 16:21:23 +0000 | [diff] [blame] | 544 | labels = [str(label) for label in columns[0]] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 545 | 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): |
showard | 7fc993c | 2008-09-22 16:21:23 +0000 | [diff] [blame] | 550 | y = columns[col] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 551 | label = cursor.description[col][0] |
| 552 | col += 1 |
| 553 | if (col < len(cursor.description) and |
| 554 | 'errors-' + label == cursor.description[col][0]): |
showard | 7fc993c | 2008-09-22 16:21:23 +0000 | [diff] [blame] | 555 | errors = columns[col] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 556 | 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 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 564 | 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) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 567 | y = [y[i] for i in x] |
| 568 | if errors: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 569 | errors = [errors[i] for i in x] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 570 | 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 |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 581 | 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:] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 585 | 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: |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 592 | if normalize_to == 'first': |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 593 | 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' |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 601 | % (plot['label'], normalize_to[3:])) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 602 | }) |
| 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 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 610 | elif normalize_to.startswith('series__'): |
| 611 | base_series = normalize_to[8:] |
showard | 84f3732 | 2009-03-03 02:16:59 +0000 | [diff] [blame] | 612 | _normalize_to_series(plots, base_series) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 613 | |
| 614 | # Call the appropriate function to draw the line or bar plot |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 615 | if plot_info.is_line: |
| 616 | figure, area_data = _create_line(plots, labels, plot_info) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 617 | else: |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 618 | figure, area_data = _create_bar(plots, labels, plot_info) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 619 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 620 | # TODO(showard): extract these magic numbers to named constants |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 621 | if extra_text: |
| 622 | text_y = .95 - .0075 * len(plots) |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 623 | figure.text(.1, text_y, extra_text, size='xx-small') |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 624 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 625 | return (figure, area_data) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 626 | |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 627 | |
showard | de01b76 | 2009-05-01 00:09:11 +0000 | [diff] [blame] | 628 | def create_metrics_plot(query_dict, plot_type, inverted_series, normalize_to, |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 629 | drilldown_callback, extra_text=None): |
showard | de01b76 | 2009-05-01 00:09:11 +0000 | [diff] [blame] | 630 | plot_info = MetricsPlot(query_dict, plot_type, inverted_series, |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 631 | 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) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 634 | |
| 635 | |
| 636 | def _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 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 644 | return [hostname for hostname, pass_rate in hist_data |
| 645 | if bucket[0] <= pass_rate < bucket[1]] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 646 | |
| 647 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 648 | def _create_qual_histogram_helper(plot_info, extra_text=None): |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 649 | """\ |
| 650 | Create a machine qualification histogram of the given data. |
| 651 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 652 | plot_info: a QualificationHistogram |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 653 | extra_text: text to show at the upper-left of the graph |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 654 | |
| 655 | TODO(showard): move much or all of this into methods on |
| 656 | QualificationHistogram |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 657 | """ |
showard | 56e9377 | 2008-10-06 10:06:22 +0000 | [diff] [blame] | 658 | cursor = readonly_connection.connection().cursor() |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 659 | cursor.execute(plot_info.query) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 660 | |
| 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(): |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 677 | if total == 0: |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 678 | no_tests.append(hostname) |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 679 | 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)) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 688 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 689 | interval = plot_info.interval |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 690 | bins = range(0, 100, interval) |
| 691 | if bins[-1] != 100: |
| 692 | bins.append(bins[-1] + interval) |
| 693 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 694 | figure, height = _create_figure(_SINGLE_PLOT_HEIGHT) |
| 695 | subplot = figure.add_subplot(1, 1, 1) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 696 | |
| 697 | # Plot the data and get all the bars plotted |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 698 | _,_, bars = subplot.hist([data[1] for data in hist_data], |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 699 | bins=bins, align='left') |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 700 | bars += subplot.bar([-interval], len(no_pass), |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 701 | width=interval, align='center') |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 702 | bars += subplot.bar([bins[-1]], len(perfect), |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 703 | width=interval, align='center') |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 704 | bars += subplot.bar([-3 * interval], len(no_tests), |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 705 | width=interval, align='center') |
| 706 | |
| 707 | buckets = [(bin, min(bin + interval, 100)) for bin in bins[:-1]] |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 708 | # 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%'] + |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 713 | ['%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()) |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 722 | 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( |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 725 | [(x_val + interval, 0) for x_val in x]) |
| 726 | |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 727 | # 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] |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 738 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 739 | if plot_info.filter_string: |
| 740 | plot_info.filter_string += ' AND ' |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 741 | |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 742 | # Construct the list of drilldown parameters to be passed when the user |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 743 | # clicks on the bar. |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 744 | params = [] |
| 745 | for names in names_list: |
| 746 | if names: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 747 | hostnames = ','.join(_quote(hostname) for hostname in names) |
| 748 | hostname_filter = 'hostname IN (%s)' % hostnames |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 749 | full_filter = plot_info.filter_string + hostname_filter |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 750 | params.append({'type': 'normal', |
| 751 | 'filterString': full_filter}) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 752 | else: |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 753 | params.append({'type': 'empty'}) |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 754 | |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 755 | params.append({'type': 'not_applicable', |
| 756 | 'hosts': '<br />'.join(no_tests)}) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 757 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 758 | area_data = [dict(left=ulx, top=height - uly, |
| 759 | right=brx, bottom=height - bry, |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 760 | title=title, callback=plot_info.drilldown_callback, |
showard | 3b2b930 | 2009-04-15 21:53:47 +0000 | [diff] [blame] | 761 | callback_arguments=param_dict) |
| 762 | for (ulx, uly), (brx, bry), title, param_dict |
| 763 | in zip(upper_left_coords, bottom_right_coords, titles, params)] |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 764 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 765 | # TODO(showard): extract these magic numbers to named constants |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 766 | if extra_text: |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 767 | figure.text(.1, .95, extra_text, size='xx-small') |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 768 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 769 | return (figure, area_data) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 770 | |
| 771 | |
showard | fbdab0b | 2009-04-29 19:49:50 +0000 | [diff] [blame] | 772 | def 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) |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 778 | |
| 779 | |
| 780 | def 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 | |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 788 | params = pickle.loads(model.params) |
showard | 66b510e | 2009-05-02 00:45:10 +0000 | [diff] [blame] | 789 | extra_text = 'Last updated: %s' % update_time |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 790 | |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 791 | if model.graph_type == 'metrics': |
showard | 66b510e | 2009-05-02 00:45:10 +0000 | [diff] [blame] | 792 | 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) |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 799 | elif model.graph_type == 'qual': |
showard | 66b510e | 2009-05-02 00:45:10 +0000 | [diff] [blame] | 800 | 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) |
showard | e5ae165 | 2009-02-11 23:37:20 +0000 | [diff] [blame] | 805 | else: |
| 806 | raise ValueError('Invalid graph_type %s' % model.graph_type) |
| 807 | |
| 808 | image, bounding_box_unused = _create_png(figure) |
| 809 | return image |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 810 | |
| 811 | |
| 812 | _cache_timeout = global_config.global_config.get_config_value( |
showard | 250d84d | 2010-01-12 21:59:48 +0000 | [diff] [blame^] | 813 | 'AUTOTEST_WEB', 'graph_cache_creation_timeout_minutes') |
showard | ce12f55 | 2008-09-19 00:48:59 +0000 | [diff] [blame] | 814 | |
| 815 | |
| 816 | def 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 |