Skip to content

Commit 382a574

Browse files
committed
Update
[ghstack-poisoned]
2 parents efd8e40 + 4140e74 commit 382a574

11 files changed

Lines changed: 355 additions & 203 deletions

File tree

aws/lambda/benchmark_regression_summary_report/common/regression_utils.py

Lines changed: 137 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import datetime as dt
12
import logging
2-
import math
3-
import statistics
4-
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
55

6-
from common.benchmark_time_series_api_model import BenchmarkTimeSeriesApiData
6+
from common.benchmark_time_series_api_model import (
7+
BenchmarkTimeSeriesApiData,
8+
BenchmarkTimeSeriesItem,
9+
)
710
from common.config_model import BenchmarkConfig, RegressionPolicy
811
from dateutil.parser import isoparse
912

@@ -15,8 +18,31 @@
1518
]
1619

1720

18-
class BaselineItem(TypedDict):
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+
43+
class BaselineResult(TypedDict):
1944
group_info: Dict[str, Any]
45+
orignal_item: Dict[str, Any]
2046
value: float
2147

2248

@@ -27,37 +53,51 @@ class BenchmarkValueItem(TypedDict):
2753

2854
class PerGroupResult(TypedDict, total=True):
2955
group_info: Dict[str, Any]
30-
baseline: Optional[float]
56+
baseline_item: Optional[Dict[str, Any]]
3157
points: List[Any]
3258
label: RegressionClassifyLabel
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}
@@ -87,11 +127,11 @@ def detect_regressions_with_policies(
87127

88128
base_item = baseline_map.get(key)
89129
if not base_item:
90-
logger.warning("Skip. No baseline item found for %s", gi)
130+
logger.warning("Skip. No baseline item found for %s", key)
91131
results.append(
92132
PerGroupResult(
93133
group_info=gi,
94-
baseline=None,
134+
baseline_item=None,
95135
points=[],
96136
label="insufficient_data",
97137
policy=None,
@@ -104,65 +144,79 @@ def detect_regressions_with_policies(
104144
results.append(
105145
PerGroupResult(
106146
group_info=gi,
107-
baseline=None,
147+
baseline_item=None,
108148
points=[],
109149
label="insufficient_data",
110150
policy=None,
111151
)
112152
)
113153
continue
114-
115154
baseline_aggre_mode = policy.baseline_aggregation
116-
baseline_value = self._get_baseline(base_item, baseline_aggre_mode)
117-
if baseline_value is None or len(points) == 0:
155+
baseline_result = self._get_baseline(base_item, baseline_aggre_mode)
156+
if (
157+
not baseline_result
158+
or not baseline_result["orignal_item"]
159+
or len(points) == 0
160+
):
118161
logger.warning(
119-
"baseline_value is %s, len(points) == %s",
120-
baseline_value,
162+
"No valid baseline result found, baseline_item is %s, len(points) == %s",
163+
baseline_result,
121164
len(points),
122165
)
123166
results.append(
124167
PerGroupResult(
125168
group_info=gi,
126-
baseline=None,
169+
baseline_item=None,
127170
points=[],
128171
label="insufficient_data",
129172
policy=policy,
130173
)
131174
)
132175
continue
133176

177+
orignal_baseline_obj = baseline_result["orignal_item"]
178+
134179
# Per-point violations (True = regression)
135180
flags: List[bool] = [
136-
policy.is_violation(p["value"], baseline_value["value"]) for p in points
181+
policy.is_violation(p["value"], baseline_result["value"])
182+
for p in points
137183
]
138184
label = self.classify_flags(flags, min_points=min_points)
139185

140186
enriched_points = [{**p, "flag": f} for p, f in zip(points, flags)]
141187
results.append(
142188
PerGroupResult(
143189
group_info=gi,
144-
baseline=baseline_value["value"],
190+
baseline_item=orignal_baseline_obj,
145191
points=enriched_points,
146192
label=label,
147193
policy=policy,
148194
)
149195
)
150-
151196
logger.info("Done. Generated %s regression results", len(results))
152197
summary = self.summarize_label_counts(results)
153-
return results, summary
154198

155-
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:
156209
counts = Counter(self._label_str(r["label"]) for r in results)
157210
total_count = len(results)
158-
return {
211+
summmary: BenchmarkRegressionSummary = {
159212
"total_count": total_count,
160213
"regression_count": counts.get("regression", 0),
161214
"suspicious_count": counts.get("suspicious", 0),
162215
"no_regression_count": counts.get("no_regression", 0),
163216
"insufficient_data_count": counts.get("insufficient_data", 0),
164217
"is_regression": int(counts.get("regression", 0) > 0),
165218
}
219+
return summmary
166220

167221
def _label_str(self, x) -> str:
168222
# Robust: works for str or Enum-like labels
@@ -202,39 +256,33 @@ def _to_data_map(
202256
def _get_baseline(
203257
self,
204258
data: BenchmarkValueItem,
205-
mode: str = "mean",
259+
mode: str = "max",
206260
field: str = "value",
207-
) -> Optional[BaselineItem]:
261+
) -> Optional[BaselineResult]:
208262
"""
209263
calculate the baseline value based on the mode
210-
mode: mean, p90, max, min, latest, p50, p95
264+
mode: mean, p90, max, min, target, p50, p95
211265
"""
212-
values = [float(d[field]) for d in data["values"] if field in d]
213-
if not values:
266+
items = [d for d in data["values"] if field in d]
267+
if not items:
214268
return None
215269

216-
if mode == "mean":
217-
val = statistics.fmean(values)
218-
elif mode == "p90":
219-
val = percentile(values, 0.9)
220-
elif mode == "max":
221-
val = max(values)
270+
if mode == "max":
271+
baseline_obj = max(items, key=lambda d: float(d[field]))
222272
elif mode == "min":
223-
val = min(values)
224-
elif mode == "latest":
225-
val = values[-1]
273+
baseline_obj = min(items, key=lambda d: float(d[field]))
274+
elif mode == "target":
275+
baseline_obj = items[-1]
226276
elif mode == "earliest":
227-
val = values[0]
228-
elif mode == "p50":
229-
val = percentile(values, 0.5)
230-
elif mode == "p95":
231-
val = percentile(values, 0.95)
277+
baseline_obj = items[0]
232278
else:
233279
logger.warning("Unknown mode: %s", mode)
234280
return None
235-
result: BaselineItem = {
281+
282+
result: BaselineResult = {
236283
"group_info": data["group_info"],
237-
"value": val,
284+
"value": float(baseline_obj[field]),
285+
"orignal_item": baseline_obj,
238286
}
239287
return result
240288

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