blob: 39fc32ff7a2d5a18b625d701df1a57fe0288614a [file] [log] [blame]
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Django chart model implementation.
Produce the data behind a google visualisation data table that can
be rendered into a chart.
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.
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
from autotest_lib.frontend.croschart.charterrors import ChartDBError
from autotest_lib.frontend.croschart.charterrors import ChartInputError
import gviz_api
FIELD_SEPARATOR = ','
BUILD_PATTERN = re.compile(
'([\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.
# e.g. GetBuildRangedChartQuery() depends on
# GetBaseQuery() for efficiency.
COMMON_QUERY_TEMPLATE = """
SELECT %(select_keys)s
FROM tko_perf_view_2
WHERE job_name REGEXP %(job_name)s
AND platform = '%(platform)s'
AND job_owner = 'chromeos-test'
AND NOT ISNULL(iteration_value)
AND iteration_value >= 0.0
AND NOT ISNULL(test_started_time)
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 GetBaseQueryParts(request):
"""Fully populates and returns a base query string."""
query = COMMON_QUERY_TEMPLATE + CHART_QUERY_KEYS
boards = '|'.join(request.GET.getlist('board'))
platform = 'netbook_%s' % request.GET.get('system').upper()
test_name, test_key = request.GET.get('testkey').split(FIELD_SEPARATOR)
query_parameters = {}
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
return query, query_parameters
def GetBaseQuery(request):
"""Produce the assembled query."""
query, parameters = GetBaseQueryParts(request)
return query % parameters
def GetBuildRangedChartQuery(request):
"""Apply a build range against the BaseQuery."""
query = RANGE_QUERY_TEMPLATE
boards = request.GET.getlist('board')
from_build = request.GET.get('from_build')
to_build = request.GET.get('to_build')
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))')
min_parameters['job_name'] = (COMMON_REGEXP % '|'.join(
'%s-%s' % (b, from_build.replace('.', '\.')) for b in boards))
max_parameters = base_query_parameters
max_parameters['select_keys'] = (
'IFNULL(MAX(test_started_time), NOW())')
max_parameters['job_name'] = (COMMON_REGEXP % '|'.join(
'%s-%s' % (b, to_build.replace('.', '\.')) for b in boards))
query_parameters = {}
query_parameters['min_query'] = (base_query % min_parameters)
query_parameters['max_query'] = (base_query % max_parameters)
"""Fully populates and returns a filter query string."""
return query % query_parameters
def GetDateRangedChartQuery(request):
"""Apply a date range against the BaseQuery."""
query = RANGE_QUERY_TEMPLATE
from_date = request.GET.get('from_date')
to_date = request.GET.get('to_date')
query_parameters = {}
query_parameters['min_query'] = "SELECT '%s'" % from_date
query_parameters['max_query'] = "SELECT '%s'" % to_date
"""Fully populates and returns a filter query string."""
return query % query_parameters
def GetIntervalRangedChartQuery(request):
"""Apply an interval range against the BaseQuery."""
query = RANGE_QUERY_TEMPLATE
interval = request.GET.get('interval')
interval = interval.replace(FIELD_SEPARATOR, ' ')
query_parameters = {}
query_parameters['min_query'] = (
'SELECT DATE_SUB(NOW(), INTERVAL %s)' % interval)
query_parameters['max_query'] = 'SELECT NOW()'
"""Fully populates and returns a filter query string."""
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, with_board=False):
"""Condense full build string for x-axis representation."""
m = re.match(BUILD_PATTERN, build)
if not m or m.lastindex < 4:
logging.warning('Skipping poorly formatted build: %s.', build)
return build
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):
"""Prepare and run the db query and massage the results."""
def AggregateBuilds(test_key, data_list):
"""Groups and averages data by build and extracts job_tags."""
raw_dict = {} # unsummarized data
builds_inorder = [] # summarized data
job_tags = [] # for click-through to data
# Organize the returned data by build.
# Keep the builds in date order and check build name format.
for build, tag, test_value in data_list:
build = AbbreviateBuild(build)
if not build in raw_dict:
builds_inorder.append({'build': build})
job_tags.append(tag)
value_list = raw_dict.setdefault(build, [])
value_list.append(test_value)
if not builds_inorder:
raise ChartDBError('No data returned')
# Now avg data by build and key. This is the format used by gviz.
for build_dict in builds_inorder:
value_list = raw_dict[build_dict['build']]
build_dict[test_key] = round(sum(value_list, 0.0) / len(value_list), 2)
return job_tags, builds_inorder
def ToGVizJsonTable(test_key, builds_inorder):
"""Massage data into gviz data table in proper order."""
# Now format for gviz table.
gviz_data_table = gviz_api.DataTable({'build': ('string', 'Build'),
test_key: ('number', test_key)})
gviz_data_table.LoadData(builds_inorder)
gviz_data_table = gviz_data_table.ToJSon(['build', test_key])
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))
job_tags, build_data = AggregateBuilds(test_key, cursor.fetchall())
gviz_data_table = ToGVizJsonTable(test_key, build_data)
return {'gviz_data_table': gviz_data_table, 'job_tags': job_tags}
def GetRangedOneKeyByBuildLinechartData(request):
"""Assemble the proper query and order."""
ranged_queries = {'from_build': GetBuildRangedChartQuery,
'from_date': GetDateRangedChartQuery,
'interval': GetIntervalRangedChartQuery}
query_list = [GetBaseQuery(request)]
for range_key in ['from_build', 'from_date', 'interval', None]:
if request.GET.get(range_key, None):
break
if not range_key:
raise ChartInputError('One interval-type parameter must be supplied.')
query_list.append(ranged_queries[range_key](request))
test_name, test_key = request.GET.get('testkey').split(FIELD_SEPARATOR)
data_dict = GetOneKeyByBuildLinechartData(test_key, ' '.join(query_list))
# 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