Source code for evergreen.metrics.versionmetrics
# -*- encoding: utf-8 -*-
"""Metrics for an evergreen version."""
from __future__ import absolute_import, division
from datetime import datetime
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
from structlog import get_logger
if TYPE_CHECKING:
from evergreen.build import Build
from evergreen.metrics.buildmetrics import BuildMetrics
from evergreen.version import Version
LOGGER = get_logger(__name__)
[docs]
class VersionMetrics(object):
"""Metrics about an evergreen version."""
def __init__(self, version: "Version") -> None:
"""
Create an instance of version metrics.
:param version: Version to analyze.
"""
self.version = version
self.total_processing_time = 0
self.task_success_count = 0
self.task_failure_count = 0
self.task_timeout_count = 0
self.task_system_failure_count = 0
self._create_times: List[datetime] = []
self._start_times: List[datetime] = []
self._finish_times: List[datetime] = []
self.build_metrics: List[BuildMetrics] = []
self.build_list: Optional[List[Build]] = None
[docs]
def calculate(self, task_filter_fn: Optional[Callable] = None) -> "VersionMetrics":
"""
Calculate metrics for the given build.
:param task_filter_fn: function to filter tasks included for metrics, should accept a task
argument.
:returns: self.
"""
self.build_list = self.version.get_builds()
for build in self.build_list:
self._count_build(build, task_filter_fn)
return self
@property
def create_time(self) -> Optional[datetime]:
"""
Time the first task of the version was created.
:return: Time first task was created.
"""
if self._create_times:
return min(self._create_times)
return None
@property
def start_time(self) -> Optional[datetime]:
"""
Time first task of version was started.
:return: Time first task was started.
"""
if self._start_times:
return min(self._start_times)
return None
@property
def end_time(self) -> Optional[datetime]:
"""
Time last task of version was completed.
:return: Time last task was completed.
"""
if self._finish_times:
return max(self._finish_times)
return None
@property
def makespan(self) -> Optional[float]:
"""
Wall clock duration of version.
:return: Duration of version in seconds.
"""
if self.start_time and self.end_time:
return (self.end_time - self.start_time).total_seconds()
return None
@property
def wait_time(self) -> Optional[float]:
"""
Wall clock duration until version was started.
:return: Duration until version was started in seconds.
"""
if self.start_time and self.create_time:
return (self.start_time - self.create_time).total_seconds()
return None
@property
def total_tasks(self) -> int:
"""
Get the total tasks in the version.
:return: total tasks
"""
return self.task_success_count + self.task_failure_count
@property
def pct_tasks_success(self) -> float:
"""
Get the percentage of successful tasks.
:return: Percentage of successful tasks.
"""
return self._percent_tasks(self.task_success_count)
@property
def pct_tasks_failure(self) -> float:
"""
Get the percentage of failure tasks.
:return: Percentage of failure tasks.
"""
return self._percent_tasks(self.task_failure_count)
@property
def pct_tasks_timeout(self) -> float:
"""
Get the percentage of timeout tasks.
:return: Percentage of timeout tasks.
"""
return self._percent_tasks(self.task_timeout_count)
@property
def pct_tasks_system_failure(self) -> float:
"""
Get the percentage of system failure tasks.
:return: Percentage of system failure tasks.
"""
return self._percent_tasks(self.task_system_failure_count)
def _percent_tasks(self, n_tasks: int) -> float:
"""
Calculate the percent of n_tasks out of total.
:param n_tasks: Number of tasks to calculate percent of.
:return: percentage n_tasks is out of total tasks.
"""
if self.total_tasks == 0:
return 0
return n_tasks / self.total_tasks
def _count_build(self, build: "Build", task_filter_fn: Optional[Callable]) -> None:
"""
Add stats for the given build to the metrics.
:param task_filter_fn: function to filter tasks included for metrics, should accept a task
argument.
:param build: Build to add.
"""
log = LOGGER.bind(build_id=build.id)
if build.activated:
log.debug("Processing metrics for build")
# If all tasks have been undispatched there is no data.
if not build.tasks or build.status_counts.undispatched == len(build.tasks):
log.warning("Build had no tasks or all tasks undispatched")
return
build_metrics = build.get_metrics(task_filter_fn)
if build_metrics:
self.build_metrics.append(build_metrics)
self.total_processing_time += build_metrics.total_processing_time
self.task_success_count += build_metrics.success_count
self.task_failure_count += build_metrics.failure_count
self.task_timeout_count += build_metrics.timed_out_count
self.task_system_failure_count += build_metrics.system_failure_count
if build_metrics.create_time:
self._create_times.append(build_metrics.create_time)
if build_metrics.start_time:
self._start_times.append(build_metrics.start_time)
if build_metrics.end_time:
self._finish_times.append(build_metrics.end_time)
[docs]
def as_dict(self, include_children: bool = False) -> Dict:
"""
Provide a dictionary representation.
:param include_children: Include child build tasks in dictionary.
:return: Dictionary of metrics.
"""
metric = {
"version": self.version.version_id,
"total_processing_time": self.total_processing_time,
"task_total": self.total_tasks,
"task_success_count": self.task_success_count,
"task_pct_success": self.pct_tasks_success,
"task_failure_count": self.task_failure_count,
"task_pct_failed": self.pct_tasks_failure,
"task_timeout_count": self.task_timeout_count,
"task_system_failure_count": self.task_system_failure_count,
}
if include_children:
metric["build_metrics"] = [bm.as_dict(include_children) for bm in self.build_metrics]
return metric
def __str__(self) -> str:
"""
Create string version of metrics.
:return: String version of metrics.
"""
process_time_min = self.total_processing_time / 60 if self.total_processing_time else 0
makespan = self.makespan if self.makespan else 0
makespan_min = self.makespan / 60 if self.makespan else 0
waittime = self.wait_time if self.wait_time else 0
waittime_min = self.wait_time / 60 if self.wait_time else 0
return """Version Id: {version}
Total Processing Time: {total_processing_time:.2f}s ({total_processing_time_min:.2f}m)
Makespan: {makespan:.2f}s ({makespan_min:.2f}m)
Wait Time: {waittime:.2f}s ({waittime_min:.2f}m)
Total Tasks: {task_total}
Successful Tasks: {task_success_count} ({task_pct_success:.2%})
Failed Tasks: {task_failure_count} ({task_pct_failed:.2%})
Timed Out Tasks: {task_timeout_count} ({task_pct_timed_out:.2%})
System Failure Tasks: {task_system_failure_count} ({task_pct_system_failure:.2%})
""".format(
version=self.version.version_id,
total_processing_time=self.total_processing_time,
total_processing_time_min=process_time_min,
makespan=makespan,
makespan_min=makespan_min,
waittime=waittime,
waittime_min=waittime_min,
task_total=self.total_tasks,
task_success_count=self.task_success_count,
task_pct_success=self.pct_tasks_success,
task_failure_count=self.task_failure_count,
task_pct_failed=self.pct_tasks_failure,
task_timeout_count=self.task_timeout_count,
task_pct_timed_out=self.pct_tasks_timeout,
task_system_failure_count=self.task_system_failure_count,
task_pct_system_failure=self.pct_tasks_system_failure,
).rstrip()