Add release report with image status indicators.
BUG=crosbug:14747
TEST=https://spreadsheets.google.com/a/google.com/ccc?key=0Ahn3gnzB-A8mdFVMMnNjNW04LWp0RmFWSzdmZU1xZUE&hl=en&pli=1#gid=0
Change-Id: I40e845cfad3d714c6a57a671eb1cfb88a646d5c1
Reviewed-on: http://gerrit.chromium.org/gerrit/842
Reviewed-by: Eric Li <ericli@chromium.org>
Tested-by: Mike Truty <truty@chromium.org>
diff --git a/apache/conf/tko-directives b/apache/conf/tko-directives
index 6964475..70da03c 100644
--- a/apache/conf/tko-directives
+++ b/apache/conf/tko-directives
@@ -1,3 +1,14 @@
+Alias /images "/usr/local/autotest/images/"
+<Directory /usr/local/autotest/images/>
+ Options Indexes FollowSymLinks MultiViews
+ AllowOverride None
+ Order allow,deny
+ Allow from all
+ <FilesMatch "\.log$">
+ ForceType "text/plain; authoritative=true"
+ </FilesMatch>
+</Directory>
+
Alias /results "/usr/local/autotest/results/"
<Directory /usr/local/autotest/results/>
Options Indexes FollowSymLinks MultiViews
diff --git a/frontend/croschart/DEPLOYMENT_README b/frontend/croschart/DEPLOYMENT_README
index 36f87e6..2b0024e 100644
--- a/frontend/croschart/DEPLOYMENT_README
+++ b/frontend/croschart/DEPLOYMENT_README
@@ -5,9 +5,11 @@
Dynamic Charts deployment notes:
-When running under apache, the common apache user must have access to the
classes and cache files of dynamic chart.
--Create a croschart directory:
- mkdir ./croschart && chmod 755 ./croschart
--Create a writable croschart/.cache directory:
- mkdir ./croschart/.cache && chmod 777 ./croschart/.cache
+-Create an autotest/frontend/croschart directory:
+ mkdir autotest/frontend/croschart && chmod 755 autotest/frontend/croschart
+-Create a writable autotest/frontend/croschart/.cache directory:
+ mkdir croschart/.cache && chmod 777 croschart/.cache
-The files under croschart (including template files) must be world-readable.
find ./croschart -type f -exec chmod 644 {} \;
+-Create an autotest/images directory:
+ mkdir autotest/images && chmod 755 autotest/images
diff --git a/frontend/croschart/chartmodels.py b/frontend/croschart/chartmodels.py
index 8dc1e50..39fc32f 100644
--- a/frontend/croschart/chartmodels.py
+++ b/frontend/croschart/chartmodels.py
@@ -4,26 +4,23 @@
"""Django chart model implementation.
- This class produces the data behind a google visualisation data table
- that can be rendered into a chart.
+ Produce the data behind a google visualisation data table that can
+ be rendered into a chart.
- CLASSES:
+ This file is broken in 3 sections:
+ 1. Data queries wrapped in stateless function wrappers.
+ 2. Common helper functions to massage query results in to data tables.
+ 3. Data retrieval entry points that are called from views.
- BaseChartQuery: building block class.
- |->BuildRangedChartQuery: adds from_build/to_build constraint.
- |->DateRangedChartQuery: adds from_date/to_date constraint.
- |->IntervalRangedChartQuery: adds mysql interval constraint.
-
- OneKeyByBuildLinechart: base chart building model class.
- |->OneKeyRangedChartModel: adds from_build/to_build constraint.
-
- FUNCTIONS:
-
- GetChartData(): The principal entry-point.
+ Data entry points at this time include:
+ -GetRangedOneKeyByBuildLinechartData(): produce a value by builds data table.
+ -GetMultiTestKeyReleaseTableData(): produce a values by 2builds data table.
"""
import logging
+import os
import re
+import simplejson
from autotest_lib.frontend.afe import readonly_connection
@@ -35,9 +32,9 @@
FIELD_SEPARATOR = ','
BUILD_PATTERN = re.compile(
- '[\w]+\-[\w]+\-r[\w]+\-'
- '([\d]+\.[\d]+\.[\d]+\.[\d]+)-(r[\w]{8})-(b[\d]+)')
+ '([\w\-]+-r[c0-9]+)-([\d]+\.[\d]+\.[\d]+\.[\d]+)-(r[\w]{8})-(b[\d]+)')
COMMON_REGEXP = "'(%s).*'"
+NO_DIFF = 'n/a'
###############################################################################
# Queries: These are designed as stateless functions with static relationships.
@@ -55,19 +52,30 @@
AND NOT ISNULL(test_finished_time)
AND NOT ISNULL(job_finished_time)"""
+CHART_SELECT_KEYS = 'job_name, job_tag, iteration_value'
+RELEASE_SELECT_KEYS = """
+job_name, job_tag, test_name, iteration_key, iteration_value"""
+
CHART_QUERY_KEYS = """
AND test_name = '%(test_name)s'
AND iteration_key = '%(test_key)s'"""
+RELEASEREPORT_QUERY_KEYS = """
+ AND test_name in ('%(test_names)s')
+ AND iteration_key in ('%(test_keys)s')"""
+
# Use subqueries to find bracketing dates mapping version to job_names.
RANGE_QUERY_TEMPLATE = """
AND test_started_time >= (%(min_query)s)
AND test_started_time <= (%(max_query)s)"""
+# Can only get date order from the db.
DEFAULT_ORDER = 'ORDER BY test_started_time'
+# Release data sorted here.
+RELEASE_ORDER = ''
-def GetBaseQuery(request, raw=False):
+def GetBaseQueryParts(request):
"""Fully populates and returns a base query string."""
query = COMMON_QUERY_TEMPLATE + CHART_QUERY_KEYS
@@ -76,16 +84,19 @@
test_name, test_key = request.GET.get('testkey').split(FIELD_SEPARATOR)
query_parameters = {}
- query_parameters['select_keys'] = 'job_name, job_tag, iteration_value'
+ query_parameters['select_keys'] = CHART_SELECT_KEYS
query_parameters['job_name'] = (COMMON_REGEXP % boards)
query_parameters['platform'] = platform
query_parameters['test_name'] = test_name
query_parameters['test_key'] = test_key
- if raw:
- return query, query_parameters
- else:
- return query % query_parameters
+ return query, query_parameters
+
+
+def GetBaseQuery(request):
+ """Produce the assembled query."""
+ query, parameters = GetBaseQueryParts(request)
+ return query % parameters
def GetBuildRangedChartQuery(request):
@@ -96,7 +107,7 @@
from_build = request.GET.get('from_build')
to_build = request.GET.get('to_build')
- base_query, base_query_parameters = GetBaseQuery(request, raw=True)
+ base_query, base_query_parameters = GetBaseQueryParts(request)
min_parameters = base_query_parameters.copy()
min_parameters['select_keys'] = (
'IFNULL(MIN(test_started_time), DATE_SUB(NOW(), INTERVAL 1 DAY))')
@@ -148,18 +159,108 @@
return query % query_parameters
+def GetReleaseQueryParts(request):
+ """Fully populates and returns a base query string."""
+ query = COMMON_QUERY_TEMPLATE + RELEASEREPORT_QUERY_KEYS
+
+ boards = request.GET.getlist('board')
+ platform = 'netbook_%s' % request.GET.get('system').upper()
+ test_names = set()
+ test_keys = set()
+ test_key_tuples = {}
+ for t in request.GET.getlist('testkey'):
+ test_key_tuples[t] = ''
+ if not test_key_tuples:
+ test_key_tuples = simplejson.load(open(os.path.join(
+ os.path.abspath(os.path.dirname(__file__)),
+ 'crosrelease_defaults.json')))
+ for t in test_key_tuples:
+ test_name, test_key = t.split(FIELD_SEPARATOR)
+ if not test_key:
+ raise ChartInputError('testkey must be a test,key pair.')
+ test_names.add(test_name)
+ test_keys.add(test_key)
+
+ from_build = request.GET.get('from_build')
+ to_build = request.GET.get('to_build')
+
+ query_parameters = {}
+ query_parameters['select_keys'] = RELEASE_SELECT_KEYS
+ query_parameters['job_name'] = "'(%s)-(%s|%s).*'" % (
+ '|'.join(boards), from_build, to_build)
+ query_parameters['platform'] = platform
+ query_parameters['test_names'] = "','".join(test_names)
+ query_parameters['test_keys'] = "','".join(test_keys)
+
+ # Use the query_parameters to communicate parsed data.
+ query_parameters['lowhigh'] = test_key_tuples
+
+ return query, query_parameters
+
+
+def GetReleaseQuery(request):
+ """Produce the assembled query."""
+ query, parameters = GetReleaseQueryParts(request)
+ return query % parameters
+
+
###############################################################################
# Helpers
-def AbbreviateBuild(build):
+def AbbreviateBuild(build, with_board=False):
"""Condense full build string for x-axis representation."""
m = re.match(BUILD_PATTERN, build)
- if not m:
+ if not m or m.lastindex < 4:
logging.warning('Skipping poorly formatted build: %s.', build)
return build
- new_build = '%s-%s' % (m.group(1), m.group(3))
+ if with_board:
+ new_build = '%s-%s-%s' % (m.group(1), m.group(2), m.group(4))
+ else:
+ new_build = '%s-%s' % (m.group(2), m.group(4))
return new_build
+def BuildNumberCmp(build_number1, build_number2):
+ """Compare build numbers and return in ascending order."""
+ # 3 different build formats:
+ #1. xxx-yyy-r13-0.12.133.0-b1
+ #2. ttt_sss-rc-0.12.133.0-b1
+ #3. 0.12.133.0-b1
+ build1_split = build_number1.split('-')
+ build2_split = build_number2.split('-')
+ if len(build1_split) > 5:
+ return cmp(build_number1, build_number2)
+ if len(build1_split) > 3:
+ if len(build1_split) == 4:
+ board1, release1, build1, b1 = build1_split
+ board2, release2, build2, b2 = build2_split
+ platform1 = platform2 = ''
+ else:
+ platform1, board1, release1, build1, b1 = build1_split
+ platform2, board2, release2, build2, b2 = build2_split
+
+ if (platform1, board1, release1) != (platform2, board2, release2):
+ if platform1 != platform2:
+ return cmp(platform1, platform2)
+ if board1 != board2:
+ return cmp(board1, board2)
+ if release1 != release2:
+ return cmp(int(release1[1:]), int(release2[1:]))
+ else:
+ build1, b1 = build1_split
+ build2, b2 = build2_split
+
+ if build1 != build2:
+ major1 = build1.split('.')
+ major2 = build2.split('.')
+ major_len = min([len(major1), len(major2)])
+ for i in xrange(major_len):
+ if major1[i] != major2[i]:
+ return cmp(int(major1[i]), int(major2[i]))
+ return cmp(build1, build2)
+ else:
+ return cmp(int(b1[1:]), int(b2[1:]))
+
+
###############################################################################
# Models
def GetOneKeyByBuildLinechartData(test_key, query, query_order=DEFAULT_ORDER):
@@ -223,3 +324,129 @@
# Added for chart labeling.
data_dict.update({'test_name': test_name, 'test_key': test_key})
return data_dict
+
+
+def GetMultiTestKeyReleaseTableData(query, query_order=RELEASE_ORDER,
+ extra=None):
+ """Prepare and run the db query and massage the results."""
+
+ def GetHighlights(test_name, test_key, lowhigh, diff):
+ """Select the background color based on a setting and the diff value."""
+ black_fg = '#000000'
+ green_fg = '#009900'
+ red_fg = '#cc0000'
+ highlights = {'test': test_name, 'metric': test_key, 'diff': diff}
+
+ if not lowhigh:
+ # Cannot decide which indicators to show.
+ return highlights
+
+ # Lookup if this key is driven up or down.
+ image_template = '<img src="/images/%s" />'
+ lowhigh_indicator = {'lowisgood': image_template % 'downisgoodmetric.png',
+ 'highisgood': image_template % 'upisgoodmetric.png'}
+ lookup = lowhigh.get('%s,%s' % (test_name, test_key), None)
+ if not lookup or not lookup in lowhigh_indicator:
+ # Cannot get a key indicator or diff indicator.
+ return highlights
+
+ highlights['metric'] = '%s%s' % (test_key, lowhigh_indicator[lookup])
+ if diff == NO_DIFF:
+ # Cannot pick a diff indicator.
+ return highlights
+
+ image_vector = [(red_fg, image_template % 'unhappymetric.png'),
+ (black_fg, ''),
+ (green_fg, image_template % 'happymetric.png')]
+ media_lookup = {'lowisgood': image_vector,
+ 'highisgood': image_vector[::-1]}
+ cmp_diff = float(diff.split(' ')[0])
+ fg_color, diff_indicator = media_lookup[lookup][cmp(cmp_diff, 0.0)+1]
+ diff_template = '<span style="color:%s">%s%s</span>'
+ highlights['diff'] = diff_template % (fg_color, diff, diff_indicator)
+ return highlights
+
+
+ def CalculateDiff(diff_list):
+ """Produce a diff string."""
+ if len(diff_list) < 2:
+ return NO_DIFF
+ return '%s (%s%%)' % (
+ diff_list[0] - diff_list[1],
+ round((diff_list[0] - diff_list[1]) / diff_list[0] * 100))
+
+ def AggregateBuilds(lowhigh, data_list):
+ """Groups and averages data by build and extracts job_tags."""
+ raw_dict = {} # unsummarized data
+ builds = set()
+ # Aggregate all the data values by test_name, test_key, build.
+ for build, tag, test_name, test_key, test_value in data_list:
+ key_dict = raw_dict.setdefault(test_name, {})
+ build_dict = key_dict.setdefault(test_key, {})
+ build = AbbreviateBuild(build=build, with_board=True)
+ job_dict = build_dict.setdefault(build, {})
+ job_dict.setdefault('tag', tag)
+ value_list = job_dict.setdefault('values', [])
+ value_list.append(test_value)
+ builds.add(build)
+ if not raw_dict:
+ raise ChartDBError('No data returned')
+ if len(builds) < 2:
+ raise ChartDBError(
+ 'Release report expected 2 builds and found %s builds.' % len(builds))
+ # Now append summary dict entries of the data for gviz.
+ builds = sorted(builds, cmp=BuildNumberCmp)
+ build_data = []
+ for test_name, key_dict in raw_dict.iteritems():
+ for test_key, build_dict in key_dict.iteritems():
+ data_dict = {}
+ diff_stats = []
+ for build in builds:
+ job_dict = build_dict.get(build, None)
+ # Need to make sure there is a value for every build.
+ if job_dict:
+ value_list = job_dict['values']
+ avg = round(sum(value_list, 0.0) / len(value_list), 2)
+ diff_stats.append(avg)
+ data_dict[build] = (
+ '<a href="http://cautotest/results/%s/%s/results/keyval" '
+ 'target="_blank">%s</a>' % (job_dict['tag'], test_name, avg))
+ else:
+ data_dict[build] = 0.0
+ diff = CalculateDiff(diff_stats)
+ data_dict.update(GetHighlights(test_name, test_key, lowhigh, diff))
+ build_data.append(data_dict)
+ return builds, build_data
+
+ def ToGVizJsonTable(builds, table_data):
+ """Massage data into gviz data table in proper order."""
+ # Now format for gviz table.
+ description = {'test': ('string', 'Test'),
+ 'metric': ('string', 'Metric'),
+ 'diff': ('string', 'Diff')}
+ keys_in_order = ['test', 'metric']
+ for build in builds:
+ description[build] = ('string', build)
+ keys_in_order.append(build)
+ keys_in_order.append('diff')
+ gviz_data_table = gviz_api.DataTable(description)
+ gviz_data_table.LoadData(table_data)
+ gviz_data_table = gviz_data_table.ToJSon(keys_in_order)
+ return gviz_data_table
+
+ # Now massage the returned data into a gviz data table.
+ cursor = readonly_connection.connection().cursor()
+ cursor.execute('%s %s' % (query, query_order))
+ builds, build_data = AggregateBuilds(lowhigh=extra.get('lowhigh', None),
+ data_list=cursor.fetchall())
+ gviz_data_table = ToGVizJsonTable(builds, build_data)
+ return {'gviz_data_table': gviz_data_table}
+
+
+def GetReleaseReportData(request):
+ """Prepare and run the db query and massage the results."""
+
+ query, parameters = GetReleaseQueryParts(request)
+ data_dict = GetMultiTestKeyReleaseTableData(
+ query=query % parameters, extra=parameters)
+ return data_dict
diff --git a/frontend/croschart/crosrelease_defaults.json b/frontend/croschart/crosrelease_defaults.json
new file mode 100644
index 0000000..ba3f4f9
--- /dev/null
+++ b/frontend/croschart/crosrelease_defaults.json
@@ -0,0 +1,15 @@
+{
+ "platform_BootPerfServer,seconds_kernel_to_startup": "lowisgood",
+ "platform_BootPerfServer,seconds_power_on_to_login": "lowisgood",
+ "platform_BootPerfServer,seconds_reboot_time": "lowisgood",
+ "platform_BootPerfServer,seconds_shutdown_time": "lowisgood",
+ "platform_BootPerfServer,rdbytes_kernel_to_login": "lowisgood",
+ "platform_BootPerfServer,seconds_firmware_boot": "lowisgood",
+ "desktopui_ChromeFirstRender,seconds_chrome_first_tab": "lowisgood",
+ "build_RootFilesystemSize,bytes_rootfs_prod": "lowisgood",
+ "power_LoadTest.WIFI,minutes_battery_life": "highisgood",
+ "power_Resume,seconds_system_resume": "lowisgood",
+ "power_Resume,seconds_system_suspend": "lowisgood",
+ "desktopui_PageCyclerTests,PageCyclerTest.Alexa_usFile": "lowisgood",
+ "desktopui_V8Bench,score_total": "highisgood"
+}
diff --git a/frontend/croschart/templates/plot_releasereport.html b/frontend/croschart/templates/plot_releasereport.html
new file mode 100644
index 0000000..6863253
--- /dev/null
+++ b/frontend/croschart/templates/plot_releasereport.html
@@ -0,0 +1,48 @@
+{% extends "base.html" %}
+{% comment %}
+
+Copyright 2010 Google Inc. All Rights Reserved.
+
+This template builds a report comparing a new build to an older build.
+{% endcomment %}
+{% block title %}ChromeOS Release Report{% endblock %}
+{% block html_block %}
+<!--Load the AJAX API-->
+<script type="text/javascript" src="https://www.google.com/jsapi"></script>
+<script type="text/javascript">
+ // Load the Visualization API and the piechart package.
+ google.load('visualization', '1', {'packages':['table']});
+
+ // Set a callback to run when the Google Visualization API is loaded.
+ google.setOnLoadCallback(drawCharts);
+
+ // Callback that creates and populates a data table,
+ // instantiates the charts, passes in the data and
+ // draws them.
+ function drawCharts() {
+ var table = new google.visualization.Table(
+ document.getElementById('releasetable'));
+ var json_data = new google.visualization.DataTable(
+ {% autoescape off %}{{ tpl_chart.gviz_data_table }}{% endautoescape %}, 0.5);
+ diff_url = 'http://chromeos-images/diff/report' +
+ '?from=' + json_data.getColumnLabel(2).split('-')[3] +
+ '&to=' + json_data.getColumnLabel(3).split('-')[3];
+ document.getElementById('diff_iframe').src=diff_url;
+ table.draw(json_data,
+ {allowHtml: true, showRowNumber: true, sortColumn: 0});
+ }
+</script>
+<center>
+<h2>Chrome OS Release Report</h2>
+<span id="releasetable"></span>
+</center>
+<hr>
+Links:
+[<a href="http://goto/chromeos-dash" target="_blank">BVT Stats</a>]
+
+[<a href="http://chromeos-images/diff" target="_blank">Diff Stats</a>]
+
+[<a href="http://chromeos-images" target="_blank">Image Stats</a>]
+<hr>
+<iframe id="diff_iframe" width="100%" height="50%" frameborder="0"></iframe>
+{% endblock %}
diff --git a/frontend/croschart/urls.py b/frontend/croschart/urls.py
index 9e8c5f2..1bcb19c 100644
--- a/frontend/croschart/urls.py
+++ b/frontend/croschart/urls.py
@@ -13,4 +13,5 @@
'frontend.croschart.views',
(r'^chartdiff?%s$' % COMMON_URL, 'PlotChartDiff'),
(r'^chartreport?%s$' % COMMON_URL, 'PlotChartReport'),
- (r'^chart?%s$' % COMMON_URL, 'PlotChart'))
+ (r'^chart?%s$' % COMMON_URL, 'PlotChart'),
+ (r'^releasereport?%s$' % COMMON_URL, 'PlotReleaseReport'))
diff --git a/frontend/croschart/views.py b/frontend/croschart/views.py
index 26b44b6..ba4c64a 100644
--- a/frontend/croschart/views.py
+++ b/frontend/croschart/views.py
@@ -31,7 +31,10 @@
'from_date': [validators.CrosReportValidator,
validators.DateRangeValidator],
'interval': [validators.CrosReportValidator,
- validators.IntervalRangeValidator]}}
+ validators.IntervalRangeValidator]},
+ 'releasereport': {
+ 'from_build': [validators.CrosReportValidator,
+ validators.BuildRangeValidator]}}
def ValidateParameters(request, vlist):
@@ -82,3 +85,15 @@
except ChartInputError as e:
tpl_hostname = request.get_host()
return render_to_response('plot_syntax.html', locals())
+
+
+def PlotReleaseReport(request):
+ """Plot the requested report from /releasereport?..."""
+ try:
+ salt = ValidateParameters(request, VLISTS['releasereport'])
+ return chartviews.PlotChart(
+ request, 'plot_releasereport.html',
+ chartmodels.GetReleaseReportData, salt)
+ except ChartInputError as e:
+ tpl_hostname = request.get_host()
+ return render_to_response('plot_syntax.html', locals())
diff --git a/images/downisgoodmetric.png b/images/downisgoodmetric.png
new file mode 100644
index 0000000..d331226
--- /dev/null
+++ b/images/downisgoodmetric.png
Binary files differ
diff --git a/images/happymetric.png b/images/happymetric.png
new file mode 100644
index 0000000..4de7925
--- /dev/null
+++ b/images/happymetric.png
Binary files differ
diff --git a/images/unhappymetric.png b/images/unhappymetric.png
new file mode 100644
index 0000000..10bc938
--- /dev/null
+++ b/images/unhappymetric.png
Binary files differ
diff --git a/images/upisgoodmetric.png b/images/upisgoodmetric.png
new file mode 100644
index 0000000..00a6aae
--- /dev/null
+++ b/images/upisgoodmetric.png
Binary files differ