Files
mesa/.gitlab-ci/lava/utils/gitlab_section.py
Guilherme Gallo 9024e0df83 ci/lava: Don't fail if the section times mismatches
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>
2025-06-05 22:32:37 +00:00

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))