blob: a310fbaa13dd4d6c96dd02e030ae81ec3bbb52d2 [file]
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import datetime
import json
import os
import pickle
import Queue
import re
import sys
import time
from threading import Thread
_APPENGINE_SDK_DIR = os.path.join(
os.path.dirname(__file__), os.path.pardir, os.path.pardir, os.path.pardir,
os.path.pardir, os.path.pardir, 'google_appengine')
sys.path.insert(1, _APPENGINE_SDK_DIR)
from google.appengine.ext import ndb
_FINDIT_DIR = os.path.join(
os.path.dirname(__file__), os.path.pardir, os.path.pardir)
sys.path.insert(1, _FINDIT_DIR)
from local_libs import remote_api
from common.findit_http_client import FinditHttpClient
from libs import time_util
from model.wf_analysis import WfAnalysis
from pipeline_utils.appengine_third_party_pipeline_python_src_pipeline \
import pipeline
_THIRD_PARTY_DIR = os.path.join(
os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'third_party')
sys.path.insert(0, _THIRD_PARTY_DIR)
import google
google.__path__.insert(0, os.path.join(_THIRD_PARTY_DIR, 'google'))
from handlers import handlers_util
from waterfall import swarming_util
NOW = time_util.GetUTCNow()
START_DATE, END_DATE = datetime.datetime(2017, 1, 1), NOW
THREAD_COUNT = 64
# If more than 1199 entities are requested at once, the ndb query.fetch_page()
# operation will stall.
MAX_ENTITIES_PER_REQUEST = 1199
CLIENT_SINGLETON = FinditHttpClient()
def _BigFetch(query, page_size=MAX_ENTITIES_PER_REQUEST):
print 'Please wait. Fetching results at a rate of ~400 records per second...'
cursor = None
is_more = True
big_results = []
while is_more:
results, cursor, is_more = query.fetch_page(page_size, start_cursor=cursor)
big_results.extend(results)
print 'Finished fetching results.'
return big_results
def _FetchAnalyses(start_date, end_date):
analyses_query = WfAnalysis.query(WfAnalysis.build_start_time >= start_date,
WfAnalysis.build_start_time < end_date)
return _BigFetch(analyses_query)
def _GetPickleFilePath():
findit_tmp_dir = os.environ.get('TMP_DIR', os.getcwd())
return (os.path.join(findit_tmp_dir, 'analyses.pickle'), os.path.join(
findit_tmp_dir, 'records.pickle'))
def _SaveAnalyses(analyses, records):
pickled_analyses = pickle.dumps(analyses)
pickled_records = pickle.dumps(records)
analyses_path, records_path = _GetPickleFilePath()
with open(analyses_path, 'w') as f:
f.write(pickled_analyses)
with open(records_path, 'w') as f:
f.write(pickled_records)
def _LoadAnalyses():
analyses_path, records_path = _GetPickleFilePath()
with open(analyses_path, 'r') as f:
pickled_analyses = f.read()
with open(records_path, 'r') as f:
pickled_records = f.read()
print 'Loading pickled analyses...'
analyses = pickle.loads(pickled_analyses)
times = [
x.build_start_time for x in analyses if hasattr(x, 'build_start_time')
]
records = pickle.loads(pickled_records)
return analyses, min(times), max(times), records
def _GetTimesFromPipeline(pid):
result = {}
suffixes = {}
status_tree = pipeline.get_status_tree(pid)
for _, child_status in status_tree.get('pipelines', {}).iteritems():
cls = child_status['classPath'].split('.')[-1]
start = child_status.get('startTimeMs')
end = child_status.get('endTimeMs')
if start and end:
if (cls + '.start' + suffixes.get(cls, '')) in result.keys():
old_suffix = suffixes.get(cls, '.0')
old_index = int(old_suffix[1:])
suffixes[cls] = '.' + str(old_index + 1)
result['pl.' + cls + '.start' + suffixes.get(cls, '')] = (
_UnknownToDatetime(start))
result['pl.'
+ cls + '.end' + suffixes.get(cls, '')] = (_UnknownToDatetime(end))
return result
def _GetTimes(q, r):
"""Obtains times of interest from a WfAnalysis instance.
This is meant to be run by a worker thread."""
while True:
a = q.get()
result = {
'wfa.build_start_time': a.build_start_time,
'wfa.request_time': a.request_time,
'wfa.start_time': a.start_time,
'wfa.end_time': a.end_time,
'wfa.updated_time': a.updated_time,
'wfa.result_status': a.result_status,
'wfa.build_failure_type': a.build_failure_type,
}
try:
tryjobs_times = {}
step_to_tryjobs = handlers_util.GetAllTryJobResults(
a.master_name, a.builder_name, a.build_number, True)
for step in step_to_tryjobs.keys():
this_tryjobs = step_to_tryjobs[step]['try_jobs']
for job in this_tryjobs:
if job.get('try_job_url'):
tryjobs_times.setdefault('try.' + step, {})
times = _GetTimesFromBuildbot(job['try_job_url'])
tryjobs_times['try.' + step].update(times)
if job.get('task_url'):
tryjobs_times.setdefault('swarm.' + step, {})
times = _GetTimesFromSwarming(job['task_url'])
tryjobs_times['swarm.' + step].update(times)
result.update(tryjobs_times)
if a.pipeline_status_path:
pipeline_root = re.search(r'(?<=root\=)[^&]*',
a.pipeline_status_path).group(0)
result.update(_GetTimesFromPipeline(pipeline_root))
r.put((a.key, result))
except Exception, e:
print 'Problem with ', a.key, e
q.task_done()
def _GetTimesFromBuildbot(buildbot_url):
# TODO(robertocn): Use existing code that talks to milo.
# Convert the buildbot url to request to milo.
milo_url = ('https://luci-milo.appspot.com/prpc/'
'milo.Buildbot/GetBuildbotBuildJSON')
# http://build.chromium.org/p/<master>/builders/<builder>/builds/<number>
master, _, builder, _, buildnum = str(buildbot_url).split('/')[4:]
data = json.dumps({
'master': master,
'builder': builder,
'buildNum': buildnum,
'exclude_deprecated': True,
})
_, response = CLIENT_SINGLETON.Post(
milo_url, data, headers={'Content-Type': 'application/json'})
response_json = response[response.index('{'):]
response_data = json.loads(response_json)
result = {}
for step in response_data['steps']:
if 'name' in step and 'times' in step:
# Times from buildbot seem to be in PST, naively converting to UTC
start = _UnknownToDatetime(step['times'][0])
end = _UnknownToDatetime(step['times'][1])
if start:
result[step['name'] + '.start'] = start + datetime.timedelta(hours=8)
if end:
result[step['name'] + '.end'] = end + datetime.timedelta(hours=8)
return result
def _GetTimesFromSwarming(url):
results = {}
task_id = url.split('/')[-1]
task_result, _ = swarming_util.GetSwarmingTaskResultById(
task_id, CLIENT_SINGLETON)
for k in task_result.keys():
if k.endswith('_ts'):
results[k] = _UnknownToDatetime(task_result[k])
return results
# TODO: Instead of guessing, make the right conversions when retrieving data,
# including timezone adjustment (i.e. make everything UTC)
def _UnknownToDatetime(unknown):
if isinstance(unknown, datetime.datetime):
return unknown
if isinstance(unknown, int) and unknown < 1000000000: # Timestamp in 2001
return unknown
if isinstance(unknown, basestring):
for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
try:
return datetime.datetime.strptime(unknown, fmt)
except ValueError:
pass
# This is only used to guess whether the time given is in seconds or ms
epoch = time.mktime(datetime.datetime(year=2008, month=9, day=2).timetuple())
try:
ts = float(unknown)
if ts > epoch:
if ts > epoch * 1000:
ts = float(ts) / 1000
return datetime.datetime.fromtimestamp(ts)
except (TypeError, ValueError):
# Couldn't cast it, return the original value.
pass
return unknown
def _PrependKeys(prefix, d):
new_d = {}
for k in d.keys():
new_d[prefix + k] = d[k]
return new_d
def _Denest(d):
"""Converts {'a': {'b': 'c'}} into {'a.b': 'c'}."""
if isinstance(d, dict):
removals = []
insertions = {}
for k, v in d.iteritems():
if isinstance(v, dict):
insertions.update(_PrependKeys(k + '.', v))
removals.append(k)
for k in removals:
del (d[k])
d.update(insertions)
return d
def main():
# TODO: add options to limit the date range to fetch
# TODO: add options to display summary of fetched info.
remote_api.EnableRemoteApi(app_id='findit-for-me')
try:
all_analyses, stored_start, stored_end, time_records = _LoadAnalyses()
except IOError:
all_analyses = _FetchAnalyses(START_DATE, END_DATE)
time_records = {}
_SaveAnalyses(all_analyses, time_records)
else:
if stored_start > START_DATE:
all_analyses = _FetchAnalyses(START_DATE, stored_start) + all_analyses
if END_DATE > stored_end:
all_analyses += _FetchAnalyses(stored_end, END_DATE)
_SaveAnalyses(all_analyses, time_records)
saved_count = len(time_records)
q = Queue.Queue()
r = Queue.Queue()
for _ in range(THREAD_COUNT):
t = Thread(target=_GetTimes, args=(q, r))
t.daemon = True
t.start()
for a in all_analyses:
if a.key not in time_records.keys():
q.put(a)
while not (q.empty() and r.empty()):
key, record = r.get()
time_records[key] = _Denest(record)
print len(time_records)
if saved_count + THREAD_COUNT < len(time_records):
_SaveAnalyses(all_analyses, time_records)
saved_count = len(time_records)
print 'Saved %d results' % saved_count
q.join()
if __name__ == '__main__':
main()