arewefastyet/server/condenser.py

353 строки
10 KiB
Python

# vim: set ts=4 sw=4 tw=99 et:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import re
import os
import sys
import awfy, util
import math
from profiler import Profiler
from datetime import datetime
import glob
SecondsPerDay = 60 * 60 * 24
MaxRecentRuns = 30
class FolderChanger:
def __init__(self, folder):
self.old = os.getcwd()
self.new = folder
def __enter__(self):
os.chdir(self.new)
def __exit__(self, type, value, traceback):
os.chdir(self.old)
def export(name, j):
path = os.path.join(awfy.path, name)
if os.path.exists(path):
os.remove(path)
with open(path, 'w') as fp:
util.json_dump(j, fp)
def find_all_months(cx, prefix, name):
pattern = prefix + 'raw-' + name + '-*-*.json'
re_pattern = prefix + 'raw-' + name + '-(\d\d\d\d)-(\d+)\.json'
with FolderChanger(awfy.path):
files = []
for file in glob.glob(pattern):
m = re.match(re_pattern, file)
if not m:
continue
year = int(m.group(1))
month = int(m.group(2))
files.append(((year, month), file))
files = sorted(files, key=lambda key: key[0][0] * 12 + key[0][1])
return files
def retrieve_graphs(cx, files):
graphs = []
for when, file in files:
graphs.append((when, retrieve_graph(cx, file)))
return graphs
def retrieve_graph(cx, file):
with open(os.path.join(awfy.path, file)) as fp:
cache = util.json_load(fp)
return cache['graph']
# Take a timelist and split it into lists of which times correspond to days.
def split_into_days(timelist):
if not len(timelist):
return []
days = []
first = None
earliest = timelist[0]
for i, t in enumerate(timelist):
if t >= earliest + SecondsPerDay:
days.append((first, i))
first = i
days.append((first, i))
return days
# Aggregate the datapoints in a graph into the supplied regions. Line ordering
# stays the same.
def condense_graph(graph, regions):
# Prefill the new graph.
new_graph = { 'direction': graph['direction'],
'timelist': [],
'lines': []
}
for line in graph['lines']:
points = []
for start, end in regions:
total = 0
count = 0
first = None
last = None
suite_version = None
id = None
for i in range(start, end):
p = line['data'][i]
if not p or not p[0]:
continue
total += p[0]
count += 1
if not first:
first = p[1]
last = p[1]
suite_version = p[3]
id = p[4]
if count == 0:
avg = 0
else:
avg = total/count
points.append([avg, first, last, suite_version, id if count is 1 else None])
newline = { 'modeid': line['modeid'],
'data': points
}
new_graph['lines'].append(newline)
for start, end in regions:
new_graph['timelist'].append(graph['timelist'][start])
return new_graph
def condense_month(cx, graph, prefix, name):
days = split_into_days(graph['timelist'])
new_graph = condense_graph(graph, days)
j = { 'version': awfy.version,
'graph': new_graph
}
export(name + '.json', j)
def combine(graphs):
combined = { 'lines': [],
'timelist': [],
'direction': graphs[0]['direction']
}
# Pre-fill modes.
modes = { }
for graph in graphs:
for line in graph['lines']:
if line['modeid'] in modes:
continue
obj = { 'modeid': line['modeid'],
'data': []
}
modes[line['modeid']] = obj
combined['lines'].append(obj)
for graph in graphs:
updated = { }
for line in graph['lines']:
newline = modes[line['modeid']]
newline['data'].extend(line['data'])
updated[line['modeid']] = True
for mode in modes:
if mode in updated:
continue
empty = [None] * len(graph['timelist'])
modes[mode]['data'].extend(empty)
combined['timelist'].extend(graph['timelist'])
# Sanity check.
for line in combined['lines']:
if len(line['data']) != len(combined['timelist']):
raise Exception('corrupt graph')
return combined
def aggregate(cx, prefix, name):
with Profiler() as p:
sys.stdout.write('Aggregating ' + name + '... ')
sys.stdout.flush()
files = find_all_months(cx, prefix, name)
graphs = retrieve_graphs(cx, files)
graph = combine([graph for when, graph in graphs])
graph['aggregate'] = True
# If we don't have enough points for a historical view, we won't display
# a historical view.
if len(graph['timelist']) <= MaxRecentRuns:
if len(graph['timelist']) == 0:
graph['earliest'] = 0
else:
graph['earliest'] = graph['timelist'][0]
return graph
# Show MacRecentRuns of atleast one line.
recentRuns = 0
runs = []
for i in range(len(graph['lines'])):
runs.append(0)
for i in range(len(graph['timelist'])-1, -1, -1):
for j in range(len(graph['lines'])):
if graph['lines'] and i < len(graph['lines'][j]["data"]) and graph['lines'][j]["data"][i]:
runs[j] += 1
recentRuns += 1
if max(runs) == MaxRecentRuns:
break
# If the number of historical points is <= the number of recent points,
# then the graph is about split so we don't have to do anything.
historical = len(graph['timelist']) - recentRuns
if historical <= MaxRecentRuns:
graph['earliest'] = graph['timelist'][historical]
return graph
# How big should each region be?
region_length = float(historical) / MaxRecentRuns
pos = 0
regions = []
for i in range(0, MaxRecentRuns):
start = int(round(pos))
end = min(int(math.floor(pos + region_length)), historical) - 1
if end < start:
end = start
regions.append((start, end))
pos += region_length
new_graph = condense_graph(graph, regions)
for i, line in enumerate(new_graph['lines']):
oldline = graph['lines'][i]
line['data'].extend(oldline['data'][historical:])
new_graph['timelist'].extend(graph['timelist'][historical:])
new_graph['earliest'] = graph['timelist'][historical]
new_graph['aggregate'] = True
# Sanity check.
for line in new_graph['lines']:
if len(line['data']) != len(new_graph['timelist']):
raise Exception('corrupt graph')
diff = p.time()
print('took ' + diff)
return new_graph
def file_is_newer(file1, file2):
return os.path.getmtime(file1) >= os.path.getmtime(file2)
def condense(cx, prefix, name):
with Profiler() as p:
sys.stdout.write('Importing all datapoints for ' + name + '... ')
sys.stdout.flush()
files = find_all_months(cx, prefix, name)
diff = p.time()
print('took ' + diff)
if not len(files):
return False
change = False
for when, raw_file in files:
condensed_name = prefix + 'condensed-' + name + '-' + str(when[0]) + '-' + str(when[1])
condensed_file = condensed_name + '.json'
# Only update the graph when condensed file is older.
if os.path.exists(os.path.join(awfy.path, condensed_file)) and file_is_newer(os.path.join(awfy.path, condensed_file), os.path.join(awfy.path, raw_file)):
continue
# There was a datapoint added to one of the condensed files.
change = True
with Profiler() as p:
sys.stdout.write('Condensing ' + condensed_name + '... ')
sys.stdout.flush()
graph = retrieve_graph(cx, raw_file)
condense_month(cx, graph, prefix, condensed_name)
diff = p.time()
print(' took ' + diff)
return change
def condense_suite(cx, machine, suite):
name = suite.name + '-' + str(machine.id)
prefix = ""
if suite.visible == 2:
prefix = "auth-"
# Condense suite
change = condense(cx, prefix, name)
# Aggregate suite if needed.
aggregated_file = prefix + 'aggregate-' + name + '.json'
if change:
j = {
'version': awfy.version,
'graph': aggregate(cx, prefix, name)
}
export(aggregated_file, j)
# Note: only run the subtest condenser when suite was changed.
for subtest in suite.tests:
test_path = suite.name + '-' + subtest.name + '-' + str(machine.id)
# Condense test
change = condense(cx, prefix + 'bk-', test_path)
# Aggregate suite if needed.
if change:
j = {
'version': awfy.version,
'graph': aggregate(cx, prefix + 'bk-', test_path)
}
export(prefix + 'bk-aggregate-' + test_path + '.json', j)
if not os.path.exists(os.path.join(awfy.path, aggregated_file)):
return None
return retrieve_graph(cx, aggregated_file)
def condense_all(cx):
for machine in cx.machines:
# If a machine is set to no longer report scores, don't condense it.
if machine.active == 2:
continue
aggregates = { }
for suite in cx.benchmarks:
if suite.name == 'v8':
continue
suite_aggregate = condense_suite(cx, machine, suite)
if suite.name == 'misc':
continue
if suite.visible == 2:
continue
if suite_aggregate == None:
continue
aggregates[suite.name] = suite_aggregate
j = {
'version': awfy.version,
'graphs': aggregates
}
export('aggregate-' + str(machine.id) + '.json', j)