11# Licensed under a 3-clause BSD style license - see LICENSE.rst
22
33import argparse
4- import os
5- import subprocess
64
7- from .. import util
8- from ..benchmarks import Benchmarks
95from ..console import log
10- from ..environment import get_environments
11- from ..runner import run_benchmarks
12- from ..contrib .lightspeed .deps_db import BenchmarkId , LightspeedDB
13- from ..contrib .lightspeed .fingerprint import changed_files_with_fingerprints
6+ from ..contrib .lightspeed .session import LightspeedSession , MeasureResult
147from . import Command , common_args
158
16- _DEPS_DB_FILENAME = ".lightspeed_deps.db"
179
18-
19- def _get_git_changed_files ():
20- try :
21- out = subprocess .check_output (
22- ["git" , "diff" , "HEAD" , "--name-only" ], stderr = subprocess .DEVNULL ,
23- ).decode ().strip ()
24- return [os .path .abspath (p ) for p in out .splitlines ()] if out else []
25- except subprocess .CalledProcessError as exc :
26- raise util .UserError (f"'git diff HEAD' failed: { exc } " )
27-
28-
29- def _extract_results (asv_results , benchmarks ):
30- """Pull timing stats from a Results object into dict[BenchmarkId, dict]."""
31- out = {}
32- for name , benchmark in benchmarks .items ():
33- result_vals = asv_results ._results .get (name )
34- stats_list = asv_results ._stats .get (name )
35- if result_vals is None or stats_list is None :
36- continue
37- if benchmark .get ('params' ):
38- for param_idx , (val , stat ) in enumerate (zip (result_vals , stats_list )):
39- if val is not None and stat is not None :
40- out [BenchmarkId (name , param_idx )] = {'median' : val , ** stat }
41- else :
42- val = result_vals [0 ] if result_vals else None
43- stat = stats_list [0 ] if stats_list else None
44- if val is not None and stat is not None :
45- out [BenchmarkId (name )] = {'median' : val , ** stat }
46- return out
47-
48-
49- def _format_time (s ):
50- if s is None :
51- return "n/a"
52- if s >= 1.0 :
53- return f"{ s :.3f} s"
54- if s >= 1e-3 :
55- return f"{ s * 1e3 :.3f} ms"
56- if s >= 1e-6 :
57- return f"{ s * 1e6 :.3f} us"
58- return f"{ s * 1e9 :.3f} ns"
59-
60-
61- def _print_delta_table (deltas ):
62- if not deltas :
10+ def _print_delta_table (result : MeasureResult ):
11+ if not result .benchmarks :
6312 return
64- col_w = max (len (str ( bid )) for bid in deltas ) + 2
13+ col_w = max (len (name ) for name in result . benchmarks ) + 2
6514 header = f"{ 'benchmark' :<{col_w }} { 'baseline' :>12} { 'current' :>12} { 'delta' :>10} "
6615 print (header )
6716 print ("-" * len (header ))
68- for bid , info in sorted (deltas .items (), key = lambda x : str (x [0 ])):
69- pct = info ['delta' ] * 100
17+ for name , d in sorted (result .benchmarks .items ()):
18+ pct = d .delta_pct
19+ pct_str = f"{ '+' if pct >= 0 else '' } { pct :.1f} %" if pct is not None else "n/a"
7020 print (
71- f"{ str ( bid ) :<{col_w }} "
72- f"{ _format_time ( info [ 'baseline_median' ]) :>12} "
73- f"{ _format_time ( info [ 'current_median' ]) :>12} "
74- f"{ '+' if pct >= 0 else '' } { pct :>8.1f } % "
21+ f"{ name :<{col_w }} "
22+ f"{ d . baseline_str :>12} "
23+ f"{ d . current_str :>12} "
24+ f"{ pct_str :>10 } "
7525 )
7626
7727
@@ -88,8 +38,7 @@ def setup_arguments(cls, subparsers):
8838 "run only those benchmarks, and compare against the stored baseline.\n \n "
8939 "examples:\n "
9040 " asv measure_impacted --changed-files src/foo.py src/bar.py\n "
91- " asv measure_impacted --from-git-diff\n "
92- " asv measure_impacted --from-git-diff --factor 1.05"
41+ " asv measure_impacted --from-git-diff"
9342 ),
9443 )
9544 common_args .add_environment (parser , default_same = True )
@@ -107,10 +56,6 @@ def setup_arguments(cls, subparsers):
10756 "--step-id" , default = None , metavar = "ID" ,
10857 help = "Optional step label passed to _on_step_results() hook." ,
10958 )
110- parser .add_argument (
111- "--factor" , type = float , default = 1.1 ,
112- help = "Exit non-zero if any benchmark regresses by more than this factor (default: 1.1)." ,
113- )
11459 common_args .add_bench (parser )
11560 common_args .add_launch_method (parser )
11661 parser .set_defaults (func = cls .run_from_args )
@@ -123,87 +68,39 @@ def run_from_conf_args(cls, conf, args):
12368 changed_files = args .changed_files ,
12469 from_git_diff = args .from_git_diff ,
12570 step_id = args .step_id ,
126- factor = args .factor ,
127- env_spec = args .env_spec ,
128- bench = args .bench ,
129- launch_method = getattr (args , 'launch_method' , None ),
71+ launch_method = getattr (args , "launch_method" , None ),
13072 )
13173
13274 @classmethod
13375 def run (
13476 cls , conf , changed_files = None , from_git_diff = False ,
135- step_id = None , factor = 1.1 , env_spec = None , bench = None , launch_method = None ,
77+ step_id = None , launch_method = None ,
13678 ):
137- env_spec = env_spec or ["existing:same" ]
138- environments = list (get_environments (conf , env_spec ))
139- if not environments :
140- raise util .UserError ("No environments available" )
141- conf .dvcs = "none"
79+ if launch_method :
80+ conf .launch_method = launch_method
14281
143- benchmarks = Benchmarks .load (conf , regex = bench )
82+ session = LightspeedSession ._from_conf (conf )
83+ result = session .measure_impacted (
84+ from_git_diff = from_git_diff ,
85+ changed_files = changed_files ,
86+ )
14487
145- db_path = os .path .join (conf .results_dir , _DEPS_DB_FILENAME )
146- if not os .path .exists (db_path ):
147- raise util .UserError (
148- f"Dependency database not found: { db_path } \n "
149- "Run 'asv initialize_diffcheck --source-root <path>' first."
88+ if not result .benchmarks :
89+ log .info (
90+ f"No benchmarks affected "
91+ f"({ result .total_count } total, 0 selected)."
15092 )
151- db = LightspeedDB (db_path )
152-
153- baseline = {str (bid ): b for bid in db .get_all_benchmark_ids () if (b := db .get_baseline (bid ))}
154-
155- if from_git_diff :
156- changed_files = _get_git_changed_files ()
157- else :
158- changed_files = [os .path .abspath (p ) for p in (changed_files or [])]
159-
160- if not changed_files :
161- log .info ("No changed files — nothing to do." )
162- return 0
163-
164- changes = changed_files_with_fingerprints (changed_files , db .get_stored_fshas ())
165- affected_bids = db .get_affected_benchmark_ids (changes )
166- if not affected_bids :
167- log .info ("No benchmarks are affected by the changed files." )
16893 return 0
16994
170- affected_names = {bid .name for bid in affected_bids }
171- filtered = benchmarks .filter_out (set (benchmarks .keys ()) - affected_names )
172-
173- log .info (f"Running { len (filtered )} affected benchmark(s)..." )
174- env = environments [0 ]
175- env .create ()
176- lm = launch_method or getattr (conf , 'launch_method' , None ) or 'auto'
177- results = run_benchmarks (filtered , env , launch_method = lm )
178-
179- step_results = _extract_results (results , filtered )
180- deltas = {}
181- for bid , timing in step_results .items ():
182- base = baseline .get (str (bid ))
183- if base is None :
184- log .warning (f"No baseline for { bid } — skipping delta" )
185- continue
186- delta = (timing ['median' ] - base ['median' ]) / base ['median' ]
187- deltas [bid ] = {
188- 'baseline_median' : base ['median' ],
189- 'current_median' : timing ['median' ],
190- 'delta' : delta ,
191- ** timing ,
192- }
193-
194- cls ._on_step_results (results , deltas , step_id )
195- _print_delta_table (deltas )
196-
197- regressions = [bid for bid , d in deltas .items () if d ['delta' ] > (factor - 1 )]
198- if regressions :
199- log .warning (f"{ len (regressions )} benchmark(s) regressed by more than factor { factor } :" )
200- with log .indent ():
201- for bid in regressions :
202- log .warning (str (bid ))
203- return 1
95+ log .info (
96+ f"Ran { result .selected_count } /{ result .total_count } benchmark(s) "
97+ f"in { result .timing .total_s :.1f} s"
98+ )
99+ cls ._on_step_results (result , step_id )
100+ _print_delta_table (result )
204101 return 0
205102
206103 @classmethod
207- def _on_step_results (cls , results , deltas , step_id ):
104+ def _on_step_results (cls , result : MeasureResult , step_id ):
208105 """No-op hook. Override in a fork subclass to persist step results."""
209- pass
106+ pass
0 commit comments