Index: lnt/server/reporting/dailyreport.py =================================================================== --- lnt/server/reporting/dailyreport.py +++ lnt/server/reporting/dailyreport.py @@ -1,7 +1,7 @@ from collections import namedtuple from lnt.server.reporting.analysis import REGRESSED, UNCHANGED_FAIL +from lnt.server.reporting.report import * from lnt.util import multidict -import colorsys import datetime import lnt.server.reporting.analysis import lnt.server.ui.app @@ -9,118 +9,6 @@ import sqlalchemy.sql import urllib -OrderAndHistory = namedtuple('OrderAndHistory', ['max_order', 'recent_orders']) - - -def _pairs(list): - return zip(list[:-1], list[1:]) - - -# The hash color palette avoids green and red as these colours are already used -# in quite a few places to indicate "good" or "bad". -_hash_color_palette = ( - colorsys.hsv_to_rgb(h=45. / 360, s=0.3, v=0.9999), # warm yellow - colorsys.hsv_to_rgb(h=210. / 360, s=0.3, v=0.9999), # blue cyan - colorsys.hsv_to_rgb(h=300. / 360, s=0.3, v=0.9999), # mid magenta - colorsys.hsv_to_rgb(h=150. / 360, s=0.3, v=0.9999), # green cyan - colorsys.hsv_to_rgb(h=225. / 360, s=0.3, v=0.9999), # cool blue - colorsys.hsv_to_rgb(h=180. / 360, s=0.3, v=0.9999), # mid cyan -) - - -def _clamp(v, minVal, maxVal): - return min(max(v, minVal), maxVal) - - -def _toColorString(col): - r, g, b = [_clamp(int(v * 255), 0, 255) - for v in col] - return "#%02x%02x%02x" % (r, g, b) - - -def _get_rgb_colors_for_hashes(hash_strings): - hash2color = {} - unique_hash_counter = 0 - for hash_string in hash_strings: - if hash_string is not None: - if hash_string in hash2color: - continue - hash2color[hash_string] = _hash_color_palette[unique_hash_counter] - unique_hash_counter += 1 - if unique_hash_counter >= len(_hash_color_palette): - break - result = [] - for hash_string in hash_strings: - if hash_string is None: - result.append(None) - else: - # If not one of the first N hashes, return rgb value 0,0,0 which is - # white. - rgb = hash2color.get(hash_string, (0.999, 0.999, 0.999)) - result.append(_toColorString(rgb)) - return result - - -# Helper classes to make the sparkline chart construction easier in the jinja -# template. -class DayResult: - def __init__(self, comparisonResult): - self.cr = comparisonResult - self.hash = self.cr.cur_hash - self.samples = self.cr.samples - if self.samples is None: - self.samples = [] - - -class DayResults: - """ - DayResults contains pre-processed data to easily construct the HTML for - a single row in the results table, showing how one test on one board - evolved over a number of runs/days. - """ - def __init__(self): - self.day_results = [] - self._complete = False - self.min_sample = None - self.max_sample = None - - def __getitem__(self, i): - return self.day_results[i] - - def __len__(self): - return len(self.day_results) - - def append(self, day_result): - assert not self._complete - self.day_results.append(day_result) - - def complete(self): - """ - complete() needs to be called after all appends to this object, but - before the data is used the jinja template. - """ - self._complete = True - all_samples = [] - for dr in self.day_results: - if dr is None: - continue - if dr.cr.samples is not None and not dr.cr.failed: - all_samples.extend(dr.cr.samples) - if len(all_samples) > 0: - self.min_sample = min(all_samples) - self.max_sample = max(all_samples) - hashes = [] - for dr in self.day_results: - if dr is None: - hashes.append(None) - else: - hashes.append(dr.hash) - rgb_colors = _get_rgb_colors_for_hashes(hashes) - for i, dr in enumerate(self.day_results): - if dr is not None: - dr.hash_rgb_color = rgb_colors[i] - - class DailyReport(object): def __init__(self, ts, year, month, day, num_prior_days_to_include=3, day_start_offset_hours=16, for_mail=False, @@ -212,7 +100,7 @@ prior_runs = [session.query(ts.Run). filter(ts.Run.start_time > prior_day). filter(ts.Run.start_time <= day).all() - for day, prior_day in _pairs(self.prior_days)] + for day, prior_day in pairs(self.prior_days)] if self.filter_machine_re is not None: prior_runs = [[run for run in runs @@ -380,8 +268,8 @@ continue # Otherwise, compute the results for all the days. - day_results = DayResults() - day_results.append(DayResult(cr)) + day_results = RunResults() + day_results.append(RunResult(cr)) for i in range(1, self.num_prior_days_to_include): day_runs = machine_runs.get((machine.id, i), ()) if len(day_runs) == 0: @@ -394,7 +282,7 @@ cr = sri.get_comparison_result( day_runs, prev_runs, test.id, field, self.hash_of_binary_field) - day_results.append(DayResult(cr)) + day_results.append(RunResult(cr)) day_results.complete() @@ -431,23 +319,6 @@ env = lnt.server.ui.app.create_jinja_environment() template = env.get_template('reporting/daily_report.html') - # Compute static CSS styles for elements. We use the style directly on - # elements instead of via a stylesheet to support major email clients - # (like Gmail) which can't deal with embedded style sheets. - # - # These are derived from the static style.css file we use elsewhere. - styles = { - "body": ("color:#000000; background-color:#ffffff; " - "font-family: Helvetica, sans-serif; font-size:9pt"), - "table": ("font-size:9pt; border-spacing: 0px; " - "border: 1px solid black"), - "th": ( - "background-color:#eee; color:#666666; font-weight: bold; " - "cursor: default; text-align:center; font-weight: bold; " - "font-family: Verdana; padding:5px; padding-left:8px"), - "td": "padding:5px; padding-left:8px", - } - return template.render( - report=self, styles=styles, analysis=lnt.server.reporting.analysis, + report=self, styles=report_css_styles, analysis=lnt.server.reporting.analysis, ts_url=ts_url, only_html_body=only_html_body) Index: lnt/server/reporting/latestrunsreport.py =================================================================== --- /dev/null +++ lnt/server/reporting/latestrunsreport.py @@ -0,0 +1,114 @@ +from collections import namedtuple +from lnt.server.reporting.analysis import REGRESSED, UNCHANGED_FAIL +from lnt.server.reporting.report import * +from lnt.util import multidict +import lnt.server.reporting.analysis +import lnt.server.ui.app +import sqlalchemy.sql +import urllib + +class LatestRunsReport(object): + def __init__(self, ts, run_count): + self.ts = ts + self.run_count = run_count + self.hash_of_binary_field = self.ts.Sample.get_hash_of_binary_field() + self.fields = list(ts.Sample.get_metric_fields()) + + # Computed values. + self.result_table = None + + def build(self, session): + ts = self.ts + + machines = session.query(ts.Machine).all() + + self.result_table = [] + for field in self.fields: + field_results = [] + for machine in machines: + machine_results = [] + machine_runs = list(reversed(session.query(ts.Run) + .filter(ts.Run.machine_id == machine.id) + .order_by(ts.Run.start_time.desc()) + .limit(self.run_count) + .all())) + + if len(machine_runs) < 2: + continue + + machine_runs_ids = [r.id for r in machine_runs] + + # take all tests from latest run and do a comparison + oldest_run = machine_runs[0] + + run_tests = session.query(ts.Test) \ + .join(ts.Sample) \ + .join(ts.Run) \ + .filter(ts.Sample.run_id == oldest_run.id) \ + .filter(ts.Sample.test_id == ts.Test.id) \ + .all() + + # Create a run info object. + sri = lnt.server.reporting.analysis.RunInfo(session, ts, machine_runs_ids) + + # Build the result table of tests with interesting results. + def compute_visible_results_priority(visible_results): + # We just use an ad hoc priority that favors showing tests with + # failures and large changes. We do this by computing the priority + # as tuple of whether or not there are any failures, and then sum + # of the mean percentage changes. + test, results = visible_results + had_failures = False + sum_abs_deltas = 0. + for result in results: + test_status = result.cr.get_test_status() + + if (test_status == REGRESSED or test_status == UNCHANGED_FAIL): + had_failures = True + elif result.cr.pct_delta is not None: + sum_abs_deltas += abs(result.cr.pct_delta) + return (field.name, -int(had_failures), -sum_abs_deltas, test.name) + + for test in run_tests: + cr = sri.get_comparison_result( + [machine_runs[-1]], [oldest_run], test.id, field, + self.hash_of_binary_field) + + # If the result is not "interesting", ignore it. + if not cr.is_result_interesting(): + continue + + # For all previous runs, analyze comparison results + test_results = RunResults() + + for run in reversed(machine_runs): + cr = sri.get_comparison_result( + [run], [oldest_run], test.id, field, + self.hash_of_binary_field) + test_results.append(RunResult(cr)) + + test_results.complete() + + machine_results.append((test, test_results)) + + machine_results.sort(key=compute_visible_results_priority) + + # If there are visible results for this test, append it to the + # view. + if machine_results: + field_results.append((machine, len(machine_runs), machine_results)) + + field_results.sort(key = lambda x: x[0].name) + self.result_table.append((field, field_results)) + + def render(self, ts_url, only_html_body=True): + # Strip any trailing slash on the testsuite URL. + if ts_url.endswith('/'): + ts_url = ts_url[:-1] + + env = lnt.server.ui.app.create_jinja_environment() + template = env.get_template('reporting/latest_runs_report.html') + + return template.render( + report=self, styles=report_css_styles, analysis=lnt.server.reporting.analysis, + ts_url=ts_url, only_html_body=only_html_body) Index: lnt/server/ui/templates/layout.html =================================================================== --- lnt/server/ui/templates/layout.html +++ lnt/server/ui/templates/layout.html @@ -161,6 +161,7 @@
  • Recent Activity
  • Global Status
  • Daily Report
  • +
  • Latest Runs Report
  • All Machines
  • Changes
  • Index: lnt/server/ui/templates/reporting/daily_report.html =================================================================== --- lnt/server/ui/templates/reporting/daily_report.html +++ lnt/server/ui/templates/reporting/daily_report.html @@ -1,5 +1,6 @@ {% import "utils.html" as utils %} {% import "local.html" as local %} +{% import "reportutils.html" as reportutils %} {% if not only_html_body %} @@ -111,39 +112,6 @@ -{% macro get_initial_cell_value(day_result) %} -{%- set cr = day_result.cr -%} -{%- set test_status = cr.get_test_status() -%} - -{%- if (test_status == analysis.REGRESSED or - test_status == analysis.UNCHANGED_FAIL) %} - FAIL -{%- else %} - {#- -#} - {{ ("%.4f" % cr.current) if cr.current != none else "N/A" }} -{%- endif -%} - -{% endmacro %} - -{% macro get_cell_value(day_result) %} -{%- set cr = day_result.cr -%} -{%- set test_status = cr.get_test_status() -%} -{%- set value_status = cr.get_value_status() -%} -{%- if (test_status == analysis.REGRESSED or - test_status == analysis.UNCHANGED_FAIL) %} - FAIL -{%- elif test_status == analysis.IMPROVED %} - PASS -{%- else -%} -{%- if (value_status == analysis.REGRESSED or - value_status == analysis.IMPROVED) %} - {{ cr.pct_delta|aspctcell(reverse=cr.bigger_is_better)|safe }} -{%- else %} - - -{%- endif -%} -{%- endif -%} -{% endmacro %} - {# Generate the table showing the raw sample data. #} {# If the report is for mail, we put the table header on each test. This is @@ -165,73 +133,6 @@ {% endmacro %} -{% macro spark_plot(day_results) %} -{%- set x_border_size = 5 %} -{%- set y_border_size = 2 %} -{%- set height = 18 %} -{%- set full_height = height + 2*y_border_size %} -{%- set x_day_spacing = 10 %} -{%- set sample_fuzzing = 0.5 %} -{%- set nr_days = day_results|length %} -{%- set width = x_day_spacing * nr_days + 2*x_border_size %} -{%- if day_results.max_sample != day_results.min_sample %} - {%- set scaling_factor = (1.0*height) - / (day_results.max_sample-day_results.min_sample) -%} -{%- else %} - {%- set scaling_factor = 1.0 -%} -{%- endif %} -{%- macro spark_y_coord(day_nr, value) -%} - {{ (value-day_results.min_sample) * scaling_factor + y_border_size }} -{%- endmacro -%} -{%- macro spark_x_coord(day_nr) -%} - {{ (nr_days - day_nr) * x_day_spacing + x_border_size }} -{%- endmacro -%} -{%- macro spark_hash_background(day_nr, dr) -%} - {%- if dr.cr.cur_hash is not none -%} - {%- set style = "fill: "+dr.hash_rgb_color+";" -%} - {%- else -%} - {%- set style = "fill: none;" -%} - {%- endif -%} - -{%- endmacro -%} - - - -{#- Make y-axis go upwards instead of downwards: #} - -{%- for dr in day_results -%} - {%- if dr is not none and not dr.cr.failed -%} - {%- set day_nr = loop.index %} - {%- set nr_samples_for_day = dr.samples|length %} - {{ spark_hash_background(day_nr, dr) }} - {%- for sample in dr.samples -%} - {# fuzz the x-coordinate slightly so that multiple samples with the same - value can be noticed #} - {%- set sample_fuzz = (-sample_fuzzing*1.25) + - (2.0*sample_fuzzing/nr_samples_for_day) * loop.index %} - - {%- endfor -%} - {%- endif -%} -{%- endfor %} - - - - -{%- endmacro %} - {% for field,field_results in report.result_table|reverse %} {%- if field_results -%}

    Result Table ({{ field.display_name }})

    @@ -256,14 +157,14 @@ - {%- else -%} {%- if first_result_shown -%} - {{ get_cell_value(day_result) }} + {{ reportutils.get_cell_value(day_result, analysis, styles) }} {%- else -%} {%- set first_result_shown = true -%} - {{ get_initial_cell_value(day_result) }} + {{ reportutils.get_initial_cell_value(day_result, analysis, styles) }} {%- endif -%} {%- endif -%} {%- endfor %} - {{ spark_plot(day_results) }} + {{ reportutils.spark_plot(day_results) }} {%- endfor %} {{ "

    " if report.for_mail }} Index: lnt/server/ui/templates/reporting/latest_runs_report.html =================================================================== --- /dev/null +++ lnt/server/ui/templates/reporting/latest_runs_report.html @@ -0,0 +1,61 @@ +{% import "utils.html" as utils %} +{% import "local.html" as local %} +{% import "reportutils.html" as reportutils %} +{% if not only_html_body %} + + + Latest ({{ report.run_count }}) Runs Report + + +{% endif %} + +

    Latest ({{ report.run_count }}) Runs Report

    + +{{ utils.regex_filter_box(input_id='machine-filter', + selector='.searchable-machine', + placeholder="Machine name regex...", + selector_part_to_search=".machine-name") +}} +{{ utils.regex_filter_box(input_id='test-filter', + selector='.searchable', + placeholder="Test name regex...", + selector_part_to_search=".test-name") +}} + +{% for field, field_results in report.result_table %} + {%- if field_results -%} +

    {{ field.display_name }}

    + {% for machine, machine_runs, machine_results in field_results %} +
    +
    {{machine.name}}
    + + + + + {% for i in range(machine_runs)|reverse %} + + {% endfor %} + + + + + {% for test, test_results in machine_results %} + + + {{ reportutils.get_initial_cell_value(test_results[0], analysis, styles) }} + {% for result in test_results[:-1]|reverse %} + {{ reportutils.get_cell_value(result, analysis, styles) }} + {% endfor %} + + + {% endfor %} + +
    Test NameRun #{{i + 1}}Sparkline
    {{test.name}}{{ reportutils.spark_plot(test_results) }}
    +
    + {% endfor %} + {% else %} +

    No significant {{ field.display_name }} changes found.

    + {%- endif -%} +{% endfor %} Index: lnt/server/ui/templates/reportutils.html =================================================================== --- /dev/null +++ lnt/server/ui/templates/reportutils.html @@ -0,0 +1,99 @@ +{% macro get_initial_cell_value(result, analysis, styles) %} +{%- set cr = result.cr -%} +{%- set test_status = cr.get_test_status() -%} +{%- if (test_status == analysis.REGRESSED or + test_status == analysis.UNCHANGED_FAIL) %} + FAIL +{%- else %} + {#- -#} + {{ ("%.4f" % cr.previous) if cr.previous != none else "N/A" }} +{%- endif -%} +{% endmacro %} + +{% macro get_cell_value(result, analysis, styles) %} +{%- set cr = result.cr -%} +{%- set test_status = cr.get_test_status() -%} +{%- set value_status = cr.get_value_status() -%} +{%- if (test_status == analysis.REGRESSED or + test_status == analysis.UNCHANGED_FAIL) %} + FAIL +{%- elif test_status == analysis.IMPROVED %} + PASS +{%- else -%} +{%- if (value_status == analysis.REGRESSED or + value_status == analysis.IMPROVED) %} + {{ cr.pct_delta|aspctcell(reverse=cr.bigger_is_better)|safe }} +{%- else %} + - +{%- endif -%} +{%- endif -%} +{% endmacro %} + +{% macro spark_plot(results) %} +{%- set x_border_size = 5 %} +{%- set x_border_size = 5 %} +{%- set y_border_size = 2 %} +{%- set height = 18 %} +{%- set full_height = height + 2*y_border_size %} +{%- set x_day_spacing = 10 %} +{%- set sample_fuzzing = 0.5 %} +{%- set nr_days = results|length %} +{%- set width = x_day_spacing * nr_days + 2*x_border_size %} +{%- if results.max_sample != results.min_sample %} + {%- set scaling_factor = (1.0*height) + / (results.max_sample-results.min_sample) -%} +{%- else %} + {%- set scaling_factor = 1.0 -%} +{%- endif %} +{%- macro spark_y_coord(day_nr, value) -%} + {{ (value-results.min_sample) * scaling_factor + y_border_size }} +{%- endmacro -%} +{%- macro spark_x_coord(day_nr) -%} + {{ (nr_days - day_nr) * x_day_spacing + x_border_size }} +{%- endmacro -%} +{%- macro spark_hash_background(day_nr, dr) -%} + {%- if dr.cr.cur_hash is not none -%} + {%- set style = "fill: "+dr.hash_rgb_color+";" -%} + {%- else -%} + {%- set style = "fill: none;" -%} + {%- endif -%} + +{%- endmacro -%} + + + +{#- Make y-axis go upwards instead of downwards: #} + +{%- for dr in results -%} + {%- if dr is not none and not dr.cr.failed -%} + {%- set day_nr = loop.index %} + {%- set nr_samples_for_day = dr.samples|length %} + {{ spark_hash_background(day_nr, dr) }} + {%- for sample in dr.samples -%} + {# fuzz the x-coordinate slightly so that multiple samples with the same + value can be noticed #} + {%- set sample_fuzz = (-sample_fuzzing*1.25) + + (2.0*sample_fuzzing/nr_samples_for_day) * loop.index %} + + {%- endfor -%} + {%- endif -%} +{%- endfor %} + + + + +{%- endmacro %} + Index: lnt/server/ui/templates/v4_latest_runs_report.html =================================================================== --- /dev/null +++ lnt/server/ui/templates/v4_latest_runs_report.html @@ -0,0 +1,10 @@ +{% set db = request.get_db() %} + +{% extends "layout.html" %} +{% set components = [(ts.name, v4_url_for(".v4_recent_activity"))] %} +{% block title %}Latest Runs Report{% endblock %} +{% block body %} + +{{ report.render(v4_url_for('.v4_overview'))|safe }} + +{% endblock %} Index: lnt/server/ui/views.py =================================================================== --- lnt/server/ui/views.py +++ lnt/server/ui/views.py @@ -9,6 +9,7 @@ import sqlalchemy.sql from flask import abort from flask import current_app +from flask import json, jsonify from flask import flash from flask import g from flask import make_response @@ -26,6 +27,7 @@ import lnt.server.db.search import lnt.server.reporting.analysis import lnt.server.reporting.dailyreport +import lnt.server.reporting.latestrunsreport import lnt.server.reporting.runs import lnt.server.reporting.summaryreport import lnt.server.ui.util @@ -1514,6 +1516,25 @@ config=config, all_machines=all_machines, all_orders=all_orders, **ts_data(ts)) +@v4_route("/latest_runs_report") +def v4_latest_runs_report(): + # Redirect to the report for the most recent submitted run's date. + + session = request.session + ts = request.get_testsuite() + + # Create the report object. + report = lnt.server.reporting.latestrunsreport.LatestRunsReport(ts, 10) + + # Build the report. + try: + report.build(request.session) + except ValueError: + return abort(400) + + return render_template("v4_latest_runs_report.html", report=report, + analysis=lnt.server.reporting.analysis, + **ts_data(ts)) @db_route("/summary_report") def v4_summary_report():