Skip to content

Commit c97fa59

Browse files
committed
Update
[ghstack-poisoned]
2 parents 26d1ae2 + ef68c2d commit c97fa59

12 files changed

Lines changed: 242 additions & 186 deletions

File tree

aws/lambda/benchmark_regression_summary_report/common/benchmark_time_series_api_model.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import datetime as dt
21
from dataclasses import dataclass, field
3-
from typing import Any, Dict, List, Optional
2+
from typing import Any, Dict, List
43

54
import requests
65

@@ -63,24 +62,3 @@ def from_request(
6362
except Exception as e:
6463
raise RuntimeError(f"Malformed API payload: {e}")
6564
return cls(data=BenchmarkTimeSeriesApiData(time_series=ts, time_range=tr))
66-
67-
68-
def get_latest_meta_info(
69-
time_series: List[BenchmarkTimeSeriesItem],
70-
) -> Optional[dict[str, Any]]:
71-
if not time_series:
72-
return None
73-
74-
pts = [p for s in time_series for p in s.data]
75-
latest = max(
76-
pts,
77-
key=lambda p: dt.datetime.fromisoformat(
78-
p["granularity_bucket"].replace("Z", "+00:00")
79-
),
80-
)
81-
return {
82-
"commit": latest.get("commit", ""),
83-
"branch": latest.get("branch", ""),
84-
"timestamp": latest.get("granularity_bucket", ""),
85-
"workflow_id": latest.get("workflow_id", ""),
86-
}

aws/lambda/benchmark_regression_summary_report/common/regression_utils.py

Lines changed: 104 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import datetime as dt
12
import logging
2-
import math
3-
from typing import Any, Counter, Dict, List, Literal, Optional, Tuple, TypedDict
3+
from dataclasses import dataclass
4+
from typing import Any, Counter, Dict, List, Literal, Optional, TypedDict
45

5-
from common.benchmark_time_series_api_model import BenchmarkTimeSeriesApiData
6+
from common.benchmark_time_series_api_model import (
7+
BenchmarkTimeSeriesApiData,
8+
BenchmarkTimeSeriesItem,
9+
)
610
from common.config_model import BenchmarkConfig, RegressionPolicy
711
from dateutil.parser import isoparse
812

@@ -14,6 +18,28 @@
1418
]
1519

1620

21+
class TimeSeriesDataMetaInfo(TypedDict):
22+
commit: str
23+
branch: str
24+
timestamp: str
25+
workflow_id: str
26+
27+
28+
class TimeSeriesMetaInfo(TypedDict):
29+
start: TimeSeriesDataMetaInfo
30+
end: TimeSeriesDataMetaInfo
31+
32+
33+
@dataclass
34+
class BenchmarkRegressionSummary(TypedDict):
35+
total_count: int
36+
regression_count: int
37+
suspicious_count: int
38+
no_regression_count: int
39+
insufficient_data_count: int
40+
is_regression: int
41+
42+
1743
class BaselineResult(TypedDict):
1844
group_info: Dict[str, Any]
1945
orignal_item: Dict[str, Any]
@@ -33,31 +59,45 @@ class PerGroupResult(TypedDict, total=True):
3359
policy: Optional["RegressionPolicy"]
3460

3561

36-
def percentile(values: list[float], q: float):
37-
v = sorted(values)
38-
k = (len(v) - 1) * q
39-
f = math.floor(k)
40-
c = math.ceil(k)
41-
if f == c:
42-
return v[int(k)]
43-
return v[f] + (v[c] - v[f]) * (k - f)
62+
class BenchmarkRegressionReport(TypedDict):
63+
summary: BenchmarkRegressionSummary
64+
results: List[PerGroupResult]
65+
baseline_meta_data: TimeSeriesMetaInfo
66+
new_meta_data: TimeSeriesMetaInfo
67+
68+
69+
def get_regression_status(regression_summary: BenchmarkRegressionSummary) -> str:
70+
status = (
71+
"regression"
72+
if regression_summary.get("regression_count", 0) > 0
73+
else "suspicious"
74+
if regression_summary.get("suspicious_count", 0) > 0
75+
else "no_regression"
76+
)
77+
return status
4478

4579

4680
class BenchmarkRegressionReportGenerator:
4781
def __init__(
4882
self,
4983
config: BenchmarkConfig,
50-
latest_ts: BenchmarkTimeSeriesApiData,
84+
target_ts: BenchmarkTimeSeriesApiData,
5185
baseline_ts: BenchmarkTimeSeriesApiData,
5286
) -> None:
5387
self.metric_policies = config.policy.metrics
54-
self.latest_ts = self._to_data_map(latest_ts)
55-
self.baseline_raw = self._to_data_map(baseline_ts)
88+
self.baseline_ts_info = self._get_meta_info(baseline_ts.time_series)
89+
self.lastest_ts_info = self._get_meta_info(target_ts.time_series)
90+
self.target_ts = self._to_data_map(target_ts)
91+
self.baseline_ts = self._to_data_map(baseline_ts)
92+
93+
def generate(self) -> BenchmarkRegressionReport:
94+
if not self.baseline_ts or not self.target_ts:
95+
logger.warning("No baseline or target data found")
96+
raise ValueError("No baseline or target data found")
5697

57-
def generate(self) -> Tuple[List[PerGroupResult], Dict[str, Any]]:
5898
return self.detect_regressions_with_policies(
59-
self.baseline_raw,
60-
self.latest_ts,
99+
self.baseline_ts,
100+
self.target_ts,
61101
metric_policies=self.metric_policies,
62102
)
63103

@@ -68,11 +108,11 @@ def detect_regressions_with_policies(
68108
*,
69109
metric_policies: Dict[str, RegressionPolicy],
70110
min_points: int = 2,
71-
) -> Tuple[List[PerGroupResult], Dict[str, Any]]:
111+
) -> BenchmarkRegressionReport:
72112
"""
73113
For each dp_map:
74114
- choose policy based on targeting metric from group_info['metric'] (ex passrate, geomean ..)
75-
- calculate baseline value based on policy.baseline_aggregation (ex mean, p90, max, min, latest, p50, p95)
115+
- calculate baseline value based on policy.baseline_aggregation (ex mean, p90, max, min, target, p50, p95)
76116
- use baseline value to generate violation flag list for each point, using policy.is_violation(value, baseline)
77117
- classify with labels to detect regression, using self.classify_flags(flags, min_points)
78118
Returns a list of Regression result {group_info, baseline, values, flags, label, policy}
@@ -153,22 +193,30 @@ def detect_regressions_with_policies(
153193
policy=policy,
154194
)
155195
)
156-
157196
logger.info("Done. Generated %s regression results", len(results))
158197
summary = self.summarize_label_counts(results)
159-
return results, summary
160198

161-
def summarize_label_counts(self, results: list[PerGroupResult]):
199+
return BenchmarkRegressionReport(
200+
summary=summary,
201+
results=results,
202+
baseline_meta_data=self.baseline_ts_info,
203+
new_meta_data=self.lastest_ts_info,
204+
)
205+
206+
def summarize_label_counts(
207+
self, results: list[PerGroupResult]
208+
) -> BenchmarkRegressionSummary:
162209
counts = Counter(self._label_str(r["label"]) for r in results)
163210
total_count = len(results)
164-
return {
211+
summmary: BenchmarkRegressionSummary = {
165212
"total_count": total_count,
166213
"regression_count": counts.get("regression", 0),
167214
"suspicious_count": counts.get("suspicious", 0),
168215
"no_regression_count": counts.get("no_regression", 0),
169216
"insufficient_data_count": counts.get("insufficient_data", 0),
170217
"is_regression": int(counts.get("regression", 0) > 0),
171218
}
219+
return summmary
172220

173221
def _label_str(self, x) -> str:
174222
# Robust: works for str or Enum-like labels
@@ -213,7 +261,7 @@ def _get_baseline(
213261
) -> Optional[BaselineResult]:
214262
"""
215263
calculate the baseline value based on the mode
216-
mode: mean, p90, max, min, latest, p50, p95
264+
mode: mean, p90, max, min, target, p50, p95
217265
"""
218266
items = [d for d in data["values"] if field in d]
219267
if not items:
@@ -223,7 +271,7 @@ def _get_baseline(
223271
baseline_obj = max(items, key=lambda d: float(d[field]))
224272
elif mode == "min":
225273
baseline_obj = min(items, key=lambda d: float(d[field]))
226-
elif mode == "latest":
274+
elif mode == "target":
227275
baseline_obj = items[-1]
228276
elif mode == "earliest":
229277
baseline_obj = items[0]
@@ -294,3 +342,34 @@ def _resolve_policy(
294342
return None
295343
m = metric.lower()
296344
return metric_policies.get(m)
345+
346+
def _get_meta_info(
347+
self,
348+
time_series: List[BenchmarkTimeSeriesItem],
349+
) -> TimeSeriesMetaInfo:
350+
pts = [p for s in time_series for p in s.data]
351+
end_data = max(
352+
pts,
353+
key=lambda p: dt.datetime.fromisoformat(
354+
p["granularity_bucket"].replace("Z", "+00:00")
355+
),
356+
)
357+
start_data = min(
358+
pts,
359+
key=lambda p: dt.datetime.fromisoformat(
360+
p["granularity_bucket"].replace("Z", "+00:00")
361+
),
362+
)
363+
end: TimeSeriesDataMetaInfo = {
364+
"commit": end_data.get("commit", ""),
365+
"branch": end_data.get("branch", ""),
366+
"timestamp": end_data.get("granularity_bucket", ""),
367+
"workflow_id": end_data.get("workflow_id", ""),
368+
}
369+
start: TimeSeriesDataMetaInfo = {
370+
"commit": start_data.get("commit", ""),
371+
"branch": start_data.get("branch", ""),
372+
"timestamp": start_data.get("granularity_bucket", ""),
373+
"workflow_id": start_data.get("workflow_id", ""),
374+
}
375+
return {"start": start, "end": end}

0 commit comments

Comments
 (0)