Time drift can occur during LAVA job execution due to transitions between three different clocks. The process begins in the GitLab job [1], using the CI_JOB_STARTED_AT variable. If SSH is enabled, we then connect to the DUT through an Alpine-based SSH client container inside the LAVA dispatcher [2], where some GitLab-related steps are timestamped by lava_job_submitter. Finally, the DUT [3] runs and uses the setup-test-env.sh helper to handle GitLab sections, potentially using a third distinct clock. Signed-off-by: Guilherme Gallo <guilherme.gallo@collabora.com> Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/35222>
158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
# When changing this file, you need to bump the following
|
|
# .gitlab-ci/image-tags.yml tags:
|
|
# ALPINE_X86_64_LAVA_TRIGGER_TAG
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timedelta, UTC
|
|
from math import floor
|
|
from typing import TYPE_CHECKING, Any, Optional
|
|
|
|
from lava.utils.console_format import CONSOLE_LOG
|
|
|
|
if TYPE_CHECKING:
|
|
from lava.utils.log_section import LogSectionType
|
|
|
|
|
|
# TODO: Add section final status to assist with monitoring
|
|
@dataclass
|
|
class GitlabSection:
|
|
id: str
|
|
header: str
|
|
type: LogSectionType
|
|
start_collapsed: bool = False
|
|
suppress_end: bool = False
|
|
suppress_start: bool = False
|
|
timestamp_relative_to: Optional[datetime] = None
|
|
escape: str = "\x1b[0K"
|
|
colour: str = f"{CONSOLE_LOG['FG_CYAN']}"
|
|
__start_time: Optional[datetime] = field(default=None, init=False)
|
|
__end_time: Optional[datetime] = field(default=None, init=False)
|
|
|
|
@classmethod
|
|
def section_id_filter(cls, value: str) -> str:
|
|
return str(re.sub(r"[^\w_-]+", "-", value))
|
|
|
|
def __post_init__(self) -> None:
|
|
self.id = self.section_id_filter(self.id)
|
|
|
|
@property
|
|
def has_started(self) -> bool:
|
|
return self.__start_time is not None
|
|
|
|
@property
|
|
def has_finished(self) -> bool:
|
|
return self.__end_time is not None
|
|
|
|
@property
|
|
def start_time(self) -> Optional[datetime]:
|
|
return self.__start_time
|
|
|
|
@property
|
|
def end_time(self) -> Optional[datetime]:
|
|
return self.__end_time
|
|
|
|
def get_timestamp(self, time: datetime) -> str:
|
|
unix_ts = datetime.timestamp(time)
|
|
return str(int(unix_ts))
|
|
|
|
def section(self, marker: str, header: str, time: datetime) -> str:
|
|
preamble = f"{self.escape}section_{marker}"
|
|
collapse = marker == "start" and self.start_collapsed
|
|
collapsed = "[collapsed=true]" if collapse else ""
|
|
section_id = f"{self.id}{collapsed}"
|
|
|
|
timestamp = self.get_timestamp(time)
|
|
before_header = ":".join([preamble, timestamp, section_id])
|
|
if self.timestamp_relative_to and self.start_time is not None:
|
|
delta = self.start_time - self.timestamp_relative_to
|
|
# time drift can occur because we are dealing with timestamps from different sources
|
|
# clamp the delta to 0 if it's negative
|
|
delta = max(delta, timedelta(seconds=0))
|
|
reltime = f"[{floor(delta.seconds / 60):02}:{(delta.seconds % 60):02}] "
|
|
else:
|
|
reltime = ""
|
|
colored_header = f"{self.colour}{reltime}{header}\x1b[0m" if header else ""
|
|
header_wrapper = "\r" + f"{self.escape}{colored_header}"
|
|
|
|
return f"{before_header}{header_wrapper}"
|
|
|
|
def __str__(self) -> str:
|
|
status = "NS" if not self.has_started else "F" if self.has_finished else "IP"
|
|
delta = self.delta_time()
|
|
elapsed_time = "N/A" if delta is None else str(delta)
|
|
return (
|
|
f"GitlabSection({self.id}, {self.header}, {self.type}, "
|
|
f"SC={self.start_collapsed}, S={status}, ST={self.start_time}, "
|
|
f"ET={self.end_time}, ET={elapsed_time})"
|
|
)
|
|
|
|
def __enter__(self) -> "GitlabSection":
|
|
if start_log_line := self.start():
|
|
print(start_log_line)
|
|
return self
|
|
|
|
def __exit__(
|
|
self,
|
|
*args: list[Any],
|
|
**kwargs: dict[str, Any],
|
|
) -> None:
|
|
if end_log_line := self.end():
|
|
print(end_log_line)
|
|
|
|
def start(self) -> str:
|
|
assert not self.has_finished, "Starting an already finished section"
|
|
self.__start_time = datetime.now(tz=UTC)
|
|
return self.print_start_section()
|
|
|
|
def print_start_section(self) -> str:
|
|
if self.suppress_start:
|
|
return ""
|
|
if self.__start_time is None:
|
|
raise RuntimeError("Start time is not set.")
|
|
return self.section(marker="start", header=self.header, time=self.__start_time)
|
|
|
|
def end(self) -> str:
|
|
assert self.__start_time is not None, "Ending an uninitialized section"
|
|
self.__end_time = datetime.now(tz=UTC)
|
|
if self.__end_time < self.__start_time:
|
|
print(
|
|
CONSOLE_LOG["FG_YELLOW"]
|
|
+ f"Warning: Section {self.id} ended before it started, clamping the delta time to 0"
|
|
+ CONSOLE_LOG["RESET"]
|
|
)
|
|
return self.print_end_section()
|
|
|
|
def print_end_section(self) -> str:
|
|
if self.suppress_end:
|
|
return ""
|
|
if self.__end_time is None:
|
|
raise RuntimeError("End time is not set.")
|
|
return self.section(marker="end", header="", time=self.__end_time)
|
|
|
|
def _delta_time(self) -> Optional[timedelta]:
|
|
"""
|
|
Return the delta time between the start and end of the section.
|
|
If the section has not ended, return the delta time between the start and now.
|
|
If the section has not started and not ended, return None.
|
|
"""
|
|
if self.__start_time is None:
|
|
return None
|
|
|
|
if self.__end_time is None:
|
|
return datetime.now(tz=UTC) - self.__start_time
|
|
|
|
return self.__end_time - self.__start_time
|
|
|
|
def delta_time(self) -> Optional[timedelta]:
|
|
"""
|
|
Clamp the delta time to zero if it's negative, time drift can occur since we have timestamps
|
|
coming from GitLab jobs, LAVA dispatcher and DUTs.
|
|
"""
|
|
delta = self._delta_time()
|
|
if delta is None:
|
|
return None
|
|
return max(delta, timedelta(seconds=0))
|