1+ import datetime as dt
12import 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+ )
610from common .config_model import BenchmarkConfig , RegressionPolicy
711from dateutil .parser import isoparse
812
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+
1743class 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
4680class 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