| # -*- coding: utf-8 -*- |
| # Copyright 2016 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. |
| |
| """Script that shows build timing for a build, and it's stages. |
| |
| This script shows how long a build took, how long each stage took, and when each |
| stage started relative to the start of the build. |
| """ |
| |
| from __future__ import print_function |
| from sets import Set |
| |
| import collections |
| import datetime |
| |
| from chromite.lib import constants |
| from chromite.lib import cros_collections |
| |
| |
| BUILD_TYPE_MAP = { |
| 'cq': constants.CQ_MASTER, |
| 'canary': constants.CANARY_MASTER, |
| 'chrome-pfq': constants.PFQ_MASTER, |
| } |
| |
| |
| BuildTiming = collections.namedtuple( |
| 'BuildTiming', ['id', 'build_config', 'success', |
| 'start', 'finish', 'duration', |
| 'stages']) |
| |
| |
| # Sometimes used with TimeDeltas, and sometimes with TimeDeltaStats. |
| StageTiming = collections.namedtuple( |
| 'StageTiming', ['name', 'start', 'finish', 'duration']) |
| |
| |
| def GetPerBuildStageStats(build_statuses): |
| """Creates a map of builders to their individual stages. |
| |
| Args: |
| build_statuses: List of build_status dictionaries as returned by various |
| CIDB methods. |
| |
| Returns: |
| final_map: A map of builders to a dict of the individual stages for that |
| builder. |
| """ |
| all_builds = {} |
| final_map = {} |
| |
| # Group all builds by build config. |
| for status in build_statuses: |
| all_builds.setdefault(status['build_config'], []).extend(status['stages']) |
| |
| # Group the individual stages now. |
| for b in all_builds: |
| stage_map = {} |
| stages = all_builds[b] |
| |
| for s in stages: |
| stage_map.setdefault(s['name'], []).append(s) |
| |
| final_map[b] = stage_map |
| |
| return final_map |
| |
| |
| class TimeDeltaStats(collections.namedtuple( |
| 'TimeDeltaStats', ['median', 'mean', 'min', 'max'])): |
| """Collection a stats about a set of time.timedelta values.""" |
| __slotes__ = () |
| def __str__(self): |
| return 'median %s mean %s min %s max %s' % self |
| |
| |
| def FillInBuildStatusesWithStages(db, build_statuses): |
| """Fill in a 'stages' value for a list of build_statuses. |
| |
| Modifies the build_status objects in-place. |
| |
| Args: |
| db: cidb.CIDBConnection object. |
| build_statuses: List of build_status dictionaries as returned by various |
| CIDB methods. |
| """ |
| ids = [status['id'] for status in build_statuses] |
| all_stages = db.GetBuildsStages(ids) |
| |
| stages_by_build_id = cros_collections.GroupByKey(all_stages, 'build_id') |
| |
| for status in build_statuses: |
| status['stages'] = stages_by_build_id.get(status['id'], []) |
| |
| |
| def BuildIdToBuildStatus(db, build_id): |
| """Fetch a BuildStatus (with stages) from cidb from a build_id. |
| |
| Args: |
| db: cidb.CIDBConnection object. |
| build_id: build id as an integer. |
| |
| Returns: |
| build status dictionary from CIDB with 'stages' field populated. |
| """ |
| build_status = db.GetBuildStatus(build_id) |
| build_status['stages'] = db.GetBuildStages(build_id) |
| return build_status |
| |
| |
| def FilterBuildStatuses(build_statuses): |
| """We only want to process passing 'normal' builds for stats. |
| |
| Args: |
| build_statuses: List of Cidb result dictionary. 'stages' are not needed. |
| |
| Returns: |
| List of all build statuses that weren't removed. |
| """ |
| # Ignore release branches, branch builders, chrome waterfall, etc. |
| WATERFALLS = ('chromeos', 'chromiumos', 'chromiumos.tryserver') |
| |
| return [status for status in build_statuses |
| if status['waterfall'] in WATERFALLS] |
| |
| |
| def BuildConfigToStatuses(db, build_config, start_date, end_date): |
| """Find a list of BuildStatus dictionaries with stages populated. |
| |
| Args: |
| db: cidb.CIDBConnection object. |
| build_config: Name of build config to find builds for. |
| start_date: datetime.datetime object for start of search range. |
| end_date: datetime.datetime object for end of search range. |
| |
| Returns: |
| A list of cidb style BuildStatus dictionaries with 'stages' populated. |
| """ |
| # Find builds. |
| build_statuses = db.GetBuildHistory( |
| build_config, db.NUM_RESULTS_NO_LIMIT, |
| start_date=start_date, end_date=end_date) |
| |
| build_statuses = FilterBuildStatuses(build_statuses) |
| |
| # Fill in stage information. |
| FillInBuildStatusesWithStages(db, build_statuses) |
| return build_statuses |
| |
| |
| def MasterConfigToStatuses(db, build_config, start_date, end_date): |
| """Find a list of BuildStatuses for all master/slave builds. |
| |
| Args: |
| db: cidb.CIDBConnection object. |
| build_config: Name of build config of master builder. |
| start_date: datetime.datetime object for start of search range. |
| end_date: datetime.datetime object for end of search range. |
| |
| Returns: |
| A list of cidb style BuildStatus dictionaries with 'stages' populated. |
| """ |
| # Find masters. |
| master_statuses = db.GetBuildHistory( |
| build_config, db.NUM_RESULTS_NO_LIMIT, |
| start_date=start_date, end_date=end_date) |
| |
| # Find slaves. |
| slave_statuses = [] |
| for status in master_statuses: |
| slave_statuses += db.GetSlaveStatuses(status['id']) |
| |
| build_statuses = FilterBuildStatuses(master_statuses + slave_statuses) |
| |
| # Fill in stage information. |
| FillInBuildStatusesWithStages(db, build_statuses) |
| return build_statuses |
| |
| |
| def GetBuildTimings(build_status): |
| """Convert a build_status with stages into BuildTimings. |
| |
| After filling in a build_status dictionary with stage information |
| (FillInBuildStatusesWithStages), convert to a BuildTimings tuple with only the |
| data we care about. |
| |
| Args: |
| build_status: Cidb result dictionary with 'stages' added. |
| |
| Returns: |
| BuildTimings tuple with all time values populated as timedeltas or None. |
| """ |
| start = build_status['start_time'] |
| |
| def safeDuration(start, finish): |
| # Do time math, but don't raise on a missing value. |
| if start is None or finish is None: |
| return None |
| return finish - start |
| |
| stage_times = [] |
| for stage in build_status['stages']: |
| stage_times.append( |
| StageTiming(stage['name'], |
| safeDuration(start, stage['start_time']), |
| safeDuration(start, stage['finish_time']), |
| safeDuration(stage['start_time'], stage['finish_time']))) |
| |
| return BuildTiming(build_status['id'], |
| build_status['build_config'], |
| build_status['status'] == 'pass', |
| build_status['start_time'], |
| build_status['finish_time'], |
| safeDuration(start, build_status['finish_time']), |
| stage_times) |
| |
| |
| def CalculateTimeStats(durations): |
| """Use a set of durations to populate a TimeDelaStats. |
| |
| Args: |
| durations: A list of timedate.timedelta objects. May contain None values. |
| |
| Returns: |
| A TimeDeltaStats object or None (if no valid deltas). |
| """ |
| durations = [d for d in durations if d is not None] |
| |
| if not durations: |
| return None |
| |
| durations.sort() |
| median = durations[len(durations) / 2] |
| |
| # Convert to seconds so we can round to nearest second. |
| summation = sum(d.total_seconds() for d in durations) |
| average = datetime.timedelta(seconds=int(summation / len(durations))) |
| |
| minimum = durations[0] |
| maximum = durations[-1] |
| |
| return TimeDeltaStats(median, average, minimum, maximum) |
| |
| |
| def CalculateBuildStats(builds_timings): |
| """Find total build time stats for a set of BuildTiming objects. |
| |
| Args: |
| builds_timings: List of BuildTiming objects. |
| |
| Returns: |
| Successful build count as int. |
| Timeout build count as int. |
| Total build count as int. |
| TimeDeltaStats object,or None if no valid builds. |
| """ |
| timeout_count = sum(1 for b in builds_timings if b.finish is None) |
| successful = [b.duration for b in builds_timings if b.success] |
| time_stats = CalculateTimeStats(successful) |
| |
| return len(successful), timeout_count, len(builds_timings), time_stats |
| |
| |
| def CalculateStageStats(builds_timings): |
| """Find time stats for all stages in a set of BuildTiming objects. |
| |
| Given a set of builds, find all unique stage names, and calculate average |
| stats across all instances of that stage name. |
| |
| Given a list of 20 builds, if a stage is only in 2 of them, it's stats are |
| only computed across the two instances. |
| |
| Args: |
| builds_timings: List of BuildTiming objects. |
| |
| Returns: |
| List of StageTiming objects with all time values populated with |
| TimeDeltaStats values. |
| """ |
| stage_map = {} |
| for b in builds_timings: |
| if b.success: |
| for s in b.stages: |
| stage_map.setdefault(s.name, []).append(s) |
| |
| stage_stats = [] |
| for name in stage_map.iterkeys(): |
| named_stages = stage_map[name] |
| stage_stats.append( |
| StageTiming( |
| name=name, |
| start=CalculateTimeStats([s.start for s in named_stages]), |
| finish=CalculateTimeStats([s.finish for s in named_stages]), |
| duration=CalculateTimeStats([s.duration for s in named_stages]))) |
| |
| return stage_stats |
| |
| |
| def GetNumDistinctChromeVersions(db, build_config, start_date, end_date): |
| """Get the number of distinct chrome versions. |
| |
| This represents the number of successful chrome uprevs. |
| |
| Args: |
| db: cidb.CIDBConnection object. |
| build_config: Name of build config of master builder. |
| start_date: datetime.datetime object for start of search range. |
| end_date: datetime.datetime object for end of search range. |
| |
| Returns: |
| A list of cidb style BuildStatus dictionaries with 'stages' populated. |
| """ |
| # Find masters. |
| master_statuses = db.GetBuildHistory( |
| build_config, db.NUM_RESULTS_NO_LIMIT, |
| start_date=start_date, end_date=end_date) |
| |
| # Collect chrome versions from successful runs in a Set. |
| chrome_versions = Set() |
| for status in master_statuses: |
| if status['status'] == 'pass': |
| metadata = db.GetMetadata(status['id']) |
| if metadata is not None: |
| chrome_version = metadata['chrome_version'] |
| if chrome_version is not None: |
| chrome_versions.add(chrome_version) |
| |
| return len(chrome_versions) |
| |
| |
| def GetBuildSuccessRates(build_statuses): |
| """Get the success rate for each builder. |
| |
| Args: |
| build_statuses: Array of cidb result dictionaries. |
| |
| Returns: |
| A dictionary of success rates by build. |
| """ |
| config_successes = {} |
| for build_status in build_statuses: |
| config = build_status['build_config'] |
| success = build_status['status'] == 'pass' |
| config_successes.setdefault(config, []).append(success) |
| config_successes.setdefault('total', []).append(success) |
| |
| success_rates = {} |
| for key, successes in config_successes.iteritems(): |
| success_rates[key] = Percent(sum(successes), len(successes)) |
| return success_rates |
| |
| |
| def GetStageSuccessRates(build_statuses): |
| """Get the success rate for each stage. |
| |
| Args: |
| build_statuses: Array of cidb result dictionaries. |
| |
| Returns: |
| An dictionary of success rates by stage. |
| """ |
| config_successes = {} |
| for build_status in build_statuses: |
| for stage in build_status['stages']: |
| status = stage['status'] |
| # TODO(stevenjb): Handle 'planned', 'forgiven', 'inflight'? |
| if (status == 'skipped' or |
| status == 'planned' or status == 'forgiven' or status == 'inflight'): |
| continue |
| success = stage['status'] != 'fail' |
| config_successes.setdefault(stage['name'], []).append(success) |
| |
| success_rates = {} |
| for key, successes in config_successes.iteritems(): |
| success_rates[key] = Percent(sum(successes), len(successes)) |
| |
| return success_rates |
| |
| |
| def FindAndSortStageStats(focus_build, builds_timings): |
| """Return a list of stage names, sorted by median start time. |
| |
| The stage names returned will exist in the focus_build, or stage stages, but |
| might not be in both. |
| |
| Args: |
| focus_build: BuildTiming object for a single build, or None. |
| builds_timings: List of BuildTiming objects. |
| |
| Returns: |
| tuple ( |
| [stage names as strings, sorted by start time.], |
| {name: StageTiming from focus_build}, |
| {name: StageTiming with TimeDeltaStats}, |
| ) |
| """ |
| stage_stats = CalculateStageStats(builds_timings) |
| |
| # Map name to StageTiming. |
| focus_stages = {} |
| if focus_build: |
| focus_stages = {s.name: s for s in focus_build.stages} |
| stats_stages = {s.name: s for s in stage_stats} |
| |
| # Order the stage names to display, sorted by median start time. |
| stage_names = list(set(focus_stages.keys() + stats_stages.keys())) |
| def name_key(name): |
| f, s = focus_stages.get(name), stats_stages.get(name) |
| return s.start.median if s and s.start else f.start or datetime.timedelta() |
| stage_names.sort(key=name_key) |
| |
| return stage_names, focus_stages, stats_stages |
| |
| |
| def GroupBuildsByMonths(timings): |
| """Break a list into distinct months. |
| |
| Args: |
| timings: A list of BuildTiming objects sorted by start time. |
| |
| Returns: |
| A list of lists of BuildTiming objects. Each sublist represents one month. |
| """ |
| # Break builds into months. |
| all_months = [] |
| month = 0 |
| month_timings = [] |
| for timing in timings: |
| if timing.start.month == month: |
| month_timings.append(timing) |
| else: |
| month = timing.start.month |
| month_timings = [timing] |
| all_months.append(month_timings) |
| |
| return all_months |
| |
| |
| def GroupBuildsByConfig(builds_timings): |
| """Turn a list of BuildTiming objects into a map based on config name. |
| |
| This is useful when looking at waterfall groups (cq, canary, etc), if you |
| want stats per builder in that group. |
| |
| Args: |
| builds_timings: List of BuildTiming objects to display stats for. |
| |
| Returns: |
| A dictionary of the form {config_name: [BuildTiming, ...]} |
| """ |
| config_map = {} |
| for b in builds_timings: |
| config_map.setdefault(b.build_config, []).append(b) |
| |
| return config_map |
| |
| |
| def Percent(numerator, denominator): |
| """Convert two integers into a display friendly percentage string. |
| |
| Percent(5, 10) -> ' 50%' |
| Percent(5, 5) -> '100%' |
| Percent(1, 100) -> ' 1%' |
| Percent(1, 1000) -> ' 0%' |
| |
| Args: |
| numerator: Integer. |
| denominator: Integer. |
| |
| Returns: |
| string formatted result. |
| """ |
| return '%3d%%' % int(100 * (numerator / float(denominator))) |
| |
| |
| def Report(output, description, focus_build, builds_timings, |
| stages=True, trending=False, csv=False): |
| """Generate a report describing our stats. |
| |
| Args: |
| output: A file object to write the report to. |
| description: A user friendly string description what the report covers. |
| focus_build: A BuildTiming object for a build to compare against stats. |
| builds_timings: List of BuildTiming objects to display stats for. |
| stages: Include a per-stage break down in the report. |
| trending: Display the listed builds as broken down by month. |
| csv: Output data in CSV format for spreadsheet import. |
| """ |
| builds_timings.sort(key=lambda b: b.id) |
| |
| if csv: |
| return ReportCsv(output, description, focus_build, builds_timings, |
| stages=stages) |
| |
| output.write('%s\n' % description) |
| |
| if builds_timings: |
| output.write('Averages for %s Builds: %s - %s\n' % |
| (len(builds_timings), |
| builds_timings[0].id, |
| builds_timings[-1].id)) |
| |
| success, timeouts, total, build_stats = CalculateBuildStats(builds_timings) |
| output.write(' %s success %s timeouts %s %s\n' % ( |
| focus_build.duration if focus_build else '', |
| Percent(success, total), |
| Percent(timeouts, total), |
| build_stats, |
| )) |
| |
| if stages: |
| output.write('\n') |
| stage_names, focus_stages, stats_stages = FindAndSortStageStats( |
| focus_build, builds_timings) |
| |
| # Display info about each stage. |
| for name in stage_names: |
| output.write('%s:\n' % name) |
| f, s = focus_stages.get(name), stats_stages.get(name) |
| output.write(' start: %s %s\n' % (f.start if f else '', |
| s.start if s else '')) |
| output.write(' duration: %s %s\n' % (f.duration if f else '', |
| s.duration if s else '')) |
| output.write(' finish: %s %s\n' % (f.finish if f else '', |
| s.finish if s else '')) |
| |
| if trending: |
| all_months = GroupBuildsByMonths(builds_timings) |
| output.write('\n') |
| |
| # Report stats per month. |
| for month_timings in all_months: |
| success, timeouts, total, build_stats = CalculateBuildStats(month_timings) |
| |
| prefix = '%s-%s:' % (month_timings[0].start.year, |
| month_timings[0].start.month) |
| output.write('%s success %s timeouts %s %s\n' % ( |
| prefix.ljust(9), |
| Percent(success, total), |
| Percent(timeouts, total), |
| build_stats, |
| )) |
| |
| if stages: |
| _, _, month_stage_stags = FindAndSortStageStats(None, |
| month_timings) |
| |
| for name in stage_names: |
| s = month_stage_stags.get(name) |
| if s: |
| output.write(' %s:\n' % name) |
| output.write(' start: %s\n' % (s.start,)) |
| output.write(' duration: %s\n' % (s.duration,)) |
| output.write(' finish: %s\n' % (s.finish,)) |
| |
| |
| def ReportCsv(output, description, focus_build, builds_timings, stages=True): |
| """Generate a report describing our stats. |
| |
| Args: |
| output: A file object to write the report to. |
| description: A user friendly string description what the report covers. |
| focus_build: A BuildTiming object for a build to compare against stats. |
| builds_timings: List of BuildTiming objects to display stats for. |
| stages: Include a per-stage break down in the report. |
| """ |
| builds_timings.sort(key=lambda b: b.id) |
| |
| stats_header = ['success', 'median', 'mean', 'min', 'max'] |
| |
| desc_row = [ |
| description, |
| 'Averages for %s Builds: %s - %s' % ( |
| len(builds_timings), |
| builds_timings[0].id, |
| builds_timings[-1].id, |
| ) |
| ] |
| headers = [''] |
| subheaders = [''] |
| rows = [] |
| |
| def AddStatsHeaders(name): |
| headers.extend([name, '', '', '', '']) |
| subheaders.extend(stats_header) |
| |
| AddStatsHeaders('Build') |
| |
| # Discover full list of stage names. |
| stage_names, focus_stats_stages, stats_stages = FindAndSortStageStats( |
| focus_build, builds_timings) |
| |
| # Focus build row. |
| if focus_build: |
| row = ['Focus', focus_build.success, focus_build.duration, '', '', ''] |
| rows.append(row) |
| |
| if stages: |
| for stage_name in stage_names: |
| s = focus_stats_stages.get(stage_name) |
| row.extend(['', s.duration, '', '', '']) |
| |
| # All builds row. |
| successes, _, total, build_stats = CalculateBuildStats(builds_timings) |
| row = ['ALL', |
| Percent(successes, total), |
| build_stats.median, build_stats.mean, |
| build_stats.min, build_stats.max] |
| |
| if stages: |
| for stage_name in stage_names: |
| AddStatsHeaders(stage_name) |
| s = stats_stages.get(stage_name) |
| row.append('') |
| row.extend(s.duration) |
| |
| rows.append(row) |
| |
| # Report stats per month. |
| for month_timings in GroupBuildsByMonths(builds_timings): |
| successes, _, total, month_stats = CalculateBuildStats(month_timings) |
| prefix = '%s-%s' % (month_timings[0].start.year, |
| month_timings[0].start.month) |
| row = [prefix, |
| Percent(successes, total), |
| month_stats.median, month_stats.mean, |
| month_stats.min, month_stats.max] |
| |
| if stages: |
| _, _, month_stage_stags = FindAndSortStageStats(None, month_timings) |
| |
| for stage_name in stage_names: |
| s = month_stage_stags.get(stage_name) |
| if s: |
| row.append('') |
| row.extend(s.duration) |
| else: |
| row.extend(('', '', '', '', '')) |
| |
| rows.append(row) |
| |
| # Sanity check. |
| assert len(headers) == len(subheaders) |
| for row in rows: |
| assert len(row) == len(headers), '%s\n%s' % (row, headers) |
| |
| def ColumnsToCsvText(columns): |
| return ', '.join(['"%s"' % c for c in columns]) + '\n' |
| |
| # Produce final results. |
| output.write(ColumnsToCsvText(desc_row)) |
| output.write(ColumnsToCsvText(headers)) |
| output.write(ColumnsToCsvText(subheaders)) |
| for row in rows: |
| output.write(ColumnsToCsvText(row)) |
| |
| |
| def StabilityReport(output, description, builds_timings): |
| """Generate a report describing general health of our builds. |
| |
| Args: |
| output: A file object to write the report to. |
| description: A user friendly string description what the report covers. |
| builds_timings: List of BuildTiming objects to display stats for. |
| """ |
| assert builds_timings |
| |
| builds_timings.sort(key=lambda b: b.id, reverse=True) |
| |
| output.write('%s\n' % description) |
| |
| timeout_map = GroupBuildsByConfig(builds_timings) |
| |
| # Get a list of all build_configs present. |
| build_configs = timeout_map.keys() |
| |
| stats_map = {name: CalculateBuildStats(timeout_map[name]) |
| for name in build_configs} |
| |
| # Sort lowest success rate to the highest. |
| # TODO: Make this sorting configurable. |
| build_configs.sort(key=lambda n: stats_map[n][0] / float(stats_map[n][2])) |
| |
| max_name_len = max(len(name) for name in build_configs) |
| |
| for name in build_configs: |
| successes, timeouts, total, _ = stats_map[name] |
| |
| justified_name = ('%s:' % name).ljust(max_name_len + 1) |
| |
| output.write('%s %s successes %s timeouts %d builds.\n' % |
| (justified_name, |
| Percent(successes, total), |
| Percent(timeouts, total), |
| total)) |
| |
| def PerBuildStageStabilityReport(output, description, per_build_stage_stats): |
| """Generate a report showing stage failures by builder. |
| |
| Args: |
| output: A file object to write the report to. |
| description: A user friendly string description what the report covers. |
| per_build_stage_stats: A map of each builder to a dict of the individual |
| stages. |
| """ |
| assert per_build_stage_stats |
| |
| output.write('\n%s\n\n' % description) |
| |
| sorted_builders = sorted(per_build_stage_stats) |
| for builder in sorted_builders: |
| output.write('%s\n' % builder) |
| # Calculate the max length stage name for aligning the percentages. |
| max_stage_name_len = max(len(name) for name in |
| per_build_stage_stats[builder]) |
| |
| sorted_stages = sorted(per_build_stage_stats[builder]) |
| for stage in sorted_stages: |
| successes = 0 |
| valid_stages = 0 |
| |
| for status in per_build_stage_stats[builder][stage]: |
| # We only care about 'pass, fail, or aborted'. Assuming that 'missing' |
| # isn't a failure. |
| if status['status'] not in ('pass', 'fail', 'aborted'): |
| continue |
| |
| valid_stages += 1 |
| if status['status'] == 'pass': |
| successes += 1 |
| |
| stage_name = ('%s' % stage).ljust(max_stage_name_len + 1) |
| # We'll ignore 100% success rates. |
| if successes == valid_stages: |
| continue |
| if valid_stages: |
| output.write(' %s %s (%d/%d)\n' % (stage_name, |
| Percent(successes, valid_stages), |
| successes, valid_stages)) |
| |
| def SuccessReport(output, description, build_success_rates, stage_success_rates, |
| chrome_uprevs): |
| """Generate a report describing the success rate of the builders. |
| |
| Report includes success rate by builder and by stage. |
| |
| Args: |
| output: A file object to write the report to. |
| description: A user friendly string description what the report covers. |
| build_success_rates: Dictionary of success rates by builder. |
| stage_success_rates: Dictionary of success rates by stage. |
| chrome_uprevs: Number of chrome uprevs. |
| """ |
| assert build_success_rates |
| |
| output.write('\n%s\n\n' % description) |
| |
| output.write('\nChrome uprevs: %d\n' % chrome_uprevs) |
| |
| stage_keys = stage_success_rates.keys() |
| if len(stage_keys): |
| output.write('Stages with failures:\n\n') |
| stage_keys.sort() |
| for key in stage_keys: |
| success_rate = stage_success_rates[key] |
| if success_rate == '100%': |
| continue |
| output.write('%s Success rate: %s.\n' % (key, success_rate)) |
| output.write('\n') |
| |
| output.write('Builders:\n\n') |
| builder_keys = build_success_rates.keys() |
| builder_keys.sort() |
| for key in builder_keys: |
| if key == 'total': |
| continue |
| output.write('%s Success rate: %s.\n' % (key, build_success_rates[key])) |
| output.write('\nTotal success rate for %s: %s.\n' % |
| (description, build_success_rates['total'])) |
| |
| |
| def SuccessReportCsv(output, build_success_rates, stage_success_rates, |
| chrome_uprevs): |
| """Generate a CSV report with the success rate of the builders. |
| |
| Report includes success rate by builder and by stage. |
| |
| Args: |
| output: A file object to write the report to. |
| build_success_rates: Dictionary of success rates by builder. |
| stage_success_rates: Dictionary of success rates by stage. |
| chrome_uprevs: Number of chrome uprevs. |
| """ |
| assert build_success_rates |
| |
| # Header |
| output.write('description,success\n') |
| |
| # Chrome uprevs |
| output.write('chrome_uprevs,%d\n' % chrome_uprevs) |
| |
| # Builder success rates |
| output.write('builder:%s,%s\n' % ('total', build_success_rates['total'])) |
| builder_keys = build_success_rates.keys() |
| builder_keys.sort() |
| for key in builder_keys: |
| if key == 'total': |
| continue |
| output.write('builder:%s,%s\n' % (key, build_success_rates[key])) |
| |
| # Stage success rates |
| stage_keys = stage_success_rates.keys() |
| if len(stage_keys): |
| stage_keys.sort() |
| for key in stage_keys: |
| output.write('stage:%s,%s\n' % (key, stage_success_rates[key])) |