ci: emit machine-readable TIMINGS block at end of run_unittest_files (#25232)
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -5,7 +6,7 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable, List, Optional, Union
|
from typing import Callable, Dict, List, Optional, Union
|
||||||
|
|
||||||
from sglang.srt.debug_utils import cuda_coredump
|
from sglang.srt.debug_utils import cuda_coredump
|
||||||
from sglang.srt.utils.common import kill_process_tree
|
from sglang.srt.utils.common import kill_process_tree
|
||||||
@@ -110,6 +111,19 @@ def write_github_step_summary(content: str):
|
|||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_relative_path(p: str) -> str:
|
||||||
|
"""Return path stripped to repo-relative form (e.g. 'test/srt/foo.py').
|
||||||
|
|
||||||
|
Used in the machine-readable TIMINGS block so downstream scrapers
|
||||||
|
get a stable key regardless of CI runner checkout layout.
|
||||||
|
"""
|
||||||
|
if not os.path.isabs(p):
|
||||||
|
p = os.path.join(os.getcwd(), p)
|
||||||
|
marker = "/sglang/"
|
||||||
|
idx = p.rfind(marker)
|
||||||
|
return p[idx + len(marker) :] if idx >= 0 else p
|
||||||
|
|
||||||
|
|
||||||
def run_unittest_files(
|
def run_unittest_files(
|
||||||
files: Union[List[TestFile], List[CIRegistry]],
|
files: Union[List[TestFile], List[CIRegistry]],
|
||||||
timeout_per_file: float,
|
timeout_per_file: float,
|
||||||
@@ -140,6 +154,9 @@ def run_unittest_files(
|
|||||||
passed_tests = []
|
passed_tests = []
|
||||||
failed_tests = []
|
failed_tests = []
|
||||||
retried_tests = [] # Track which tests were retried
|
retried_tests = [] # Track which tests were retried
|
||||||
|
# Per-file elapsed seconds, latest attempt wins. Consumed by the
|
||||||
|
# TIMINGS block emitted at the end of this function.
|
||||||
|
file_elapsed: Dict[str, float] = {}
|
||||||
|
|
||||||
for i, file in enumerate(files):
|
for i, file in enumerate(files):
|
||||||
if isinstance(file, CIRegistry):
|
if isinstance(file, CIRegistry):
|
||||||
@@ -181,6 +198,7 @@ def run_unittest_files(
|
|||||||
process.wait()
|
process.wait()
|
||||||
|
|
||||||
elapsed = time.perf_counter() - file_tic
|
elapsed = time.perf_counter() - file_tic
|
||||||
|
file_elapsed[filename] = elapsed
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f".\n.\nEnd ({i}/{len(files) - 1}):\n{filename=}, {elapsed=:.0f}, {estimated_time=}\n.\n.\n"
|
f".\n.\nEnd ({i}/{len(files) - 1}):\n{filename=}, {elapsed=:.0f}, {estimated_time=}\n.\n.\n"
|
||||||
@@ -247,6 +265,10 @@ def run_unittest_files(
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
kill_process_tree(process.pid)
|
kill_process_tree(process.pid)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
# TimeoutError aborts run_one_file before its elapsed write;
|
||||||
|
# record the timeout cap as an upper bound so the file still
|
||||||
|
# appears in the TIMINGS block below.
|
||||||
|
file_elapsed[filename] = float(timeout_per_file)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"\n✗ TIMEOUT: {filename} after {timeout_per_file} seconds\n"
|
f"\n✗ TIMEOUT: {filename} after {timeout_per_file} seconds\n"
|
||||||
)
|
)
|
||||||
@@ -290,6 +312,26 @@ def run_unittest_files(
|
|||||||
logger.info(f" {test} ({attempts} attempts, {result})")
|
logger.info(f" {test} ({attempts} attempts, {result})")
|
||||||
logger.info(f"{'='*60}\n")
|
logger.info(f"{'='*60}\n")
|
||||||
|
|
||||||
|
# Machine-readable timings block for downstream scrapers/dashboards.
|
||||||
|
# One JSON object per executed file (post-retry: only the latest
|
||||||
|
# attempt's elapsed is recorded). Files skipped via fail-fast
|
||||||
|
# (continue_on_error=False) are omitted. Job wall-clock is read
|
||||||
|
# separately from the GitHub Actions API by consumers, so we don't
|
||||||
|
# emit any aggregate fields here.
|
||||||
|
passed_set = set(passed_tests)
|
||||||
|
logger.info("========== TIMINGS BEGIN ==========")
|
||||||
|
for fname, elapsed in file_elapsed.items():
|
||||||
|
logger.info(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"file": _repo_relative_path(fname),
|
||||||
|
"passed": fname in passed_set,
|
||||||
|
"elapsed": round(elapsed),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("========== TIMINGS END ==========")
|
||||||
|
|
||||||
# Write GitHub Step Summary only if retries occurred
|
# Write GitHub Step Summary only if retries occurred
|
||||||
if retried_tests:
|
if retried_tests:
|
||||||
passed_on_retry = [t for t, _, r in retried_tests if r == "passed"]
|
passed_on_retry = [t for t, _, r in retried_tests if r == "passed"]
|
||||||
|
|||||||
Reference in New Issue
Block a user