Skip to content

Commit ee3346a

Browse files
committed
Refactor to expose command
1 parent c0cd559 commit ee3346a

4 files changed

Lines changed: 583 additions & 224 deletions

File tree

Lines changed: 18 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,11 @@
11
# Licensed under a 3-clause BSD style license - see LICENSE.rst
22

33
import argparse
4-
import itertools
5-
import os
64

7-
from .. import util
8-
from ..benchmarks import Benchmarks
95
from ..console import log
10-
from ..environment import get_environments
11-
from ..repo import NoRepository
12-
from ..runner import run_benchmarks
13-
from ..contrib.lightspeed.deps_db import BenchmarkId, LightspeedDB
14-
from ..contrib.lightspeed.survey import run_survey
6+
from ..contrib.lightspeed.session import LightspeedSession
157
from . import Command, common_args
168

17-
_DEPS_DB_FILENAME = ".lightspeed_deps.db"
18-
19-
20-
def _get_all_bids(benchmarks):
21-
bids = []
22-
for name, benchmark in benchmarks.items():
23-
params = benchmark.get('params')
24-
if params:
25-
for i in range(len(list(itertools.product(*params)))):
26-
bids.append(BenchmarkId(name, i))
27-
else:
28-
bids.append(BenchmarkId(name))
29-
return bids
30-
31-
32-
def _store_baseline_from_results(asv_results, benchmarks, db):
33-
"""Write timing stats from a Results object into the baseline table."""
34-
for name, benchmark in benchmarks.items():
35-
result_vals = asv_results._results.get(name)
36-
stats_list = asv_results._stats.get(name)
37-
if result_vals is None or stats_list is None:
38-
continue
39-
if benchmark.get('params'):
40-
for param_idx, (val, stat) in enumerate(zip(result_vals, stats_list)):
41-
if val is not None and stat is not None:
42-
db.store_baseline(BenchmarkId(name, param_idx), val, stat)
43-
else:
44-
val = result_vals[0] if result_vals else None
45-
stat = stats_list[0] if stats_list else None
46-
if val is not None and stat is not None:
47-
db.store_baseline(BenchmarkId(name), val, stat)
48-
499

5010
class InitializeDiffcheck(Command):
5111
@classmethod
@@ -85,57 +45,27 @@ def run_from_conf_args(cls, conf, args):
8545
return cls.run(
8646
conf=conf,
8747
source_root=args.source_root,
88-
env_spec=args.env_spec,
8948
force=args.force,
90-
bench=args.bench,
91-
launch_method=getattr(args, 'launch_method', None),
49+
launch_method=getattr(args, "launch_method", None),
9250
)
9351

9452
@classmethod
95-
def run(cls, conf, source_root, env_spec=None, force=False, bench=None, launch_method=None):
96-
source_root = os.path.abspath(source_root)
97-
if not os.path.isdir(source_root):
98-
raise util.UserError(f"--source-root is not a directory: {source_root}")
99-
100-
env_spec = env_spec or ["existing:same"]
101-
environments = list(get_environments(conf, env_spec))
102-
if not environments:
103-
raise util.UserError("No environments available")
104-
conf.dvcs = "none"
105-
106-
try:
107-
benchmarks = Benchmarks.load(conf, regex=bench)
108-
log.info(f"Loaded {len(benchmarks)} benchmark(s) from benchmarks.json")
109-
except util.UserError:
110-
log.info("benchmarks.json not found — discovering benchmarks...")
111-
benchmarks = Benchmarks.discover(conf, NoRepository(), environments, [None], regex=bench)
112-
benchmarks.save()
113-
log.info(f"Discovered and saved {len(benchmarks)} benchmark(s)")
53+
def run(cls, conf, source_root, force=False, launch_method=None):
54+
if launch_method:
55+
conf.launch_method = launch_method
11456

115-
if not benchmarks:
116-
log.error("No benchmarks found")
117-
return 1
57+
session = LightspeedSession._from_conf(conf)
58+
result = session.initialize_diffcheck(source_root, force=force)
11859

119-
db_path = os.path.join(conf.results_dir, _DEPS_DB_FILENAME)
120-
db = LightspeedDB(db_path)
121-
all_bids = _get_all_bids(benchmarks)
122-
123-
if not force and all(db.has_baseline(bid) for bid in all_bids):
124-
log.info("Baseline already exists for all benchmarks. Use --force to re-run.")
125-
return 0
126-
127-
log.info("Pass 1: coverage survey")
128-
with log.indent():
129-
run_survey(conf.benchmark_dir, source_root, db, all_bids, verbose=True)
130-
131-
env = environments[0]
132-
env.create()
133-
lm = launch_method or getattr(conf, 'launch_method', None) or 'auto'
134-
log.info("Pass 2: baseline timing")
135-
results = run_benchmarks(benchmarks, env, launch_method=lm)
136-
137-
_store_baseline_from_results(results, benchmarks, db)
138-
139-
stored = sum(1 for bid in all_bids if db.has_baseline(bid))
140-
log.info(f"Done. Baseline stored for {stored}/{len(all_bids)} benchmark(s) in {db_path}")
60+
log.info(
61+
f"Done. {len(result.benchmarks_discovered)} benchmark(s) discovered, "
62+
f"{len(result.benchmarks_impactable)} impactable, "
63+
f"{result.source_files_covered} source file(s) covered."
64+
)
65+
log.info(f"Dependency DB: {result.deps_db_path}")
66+
if result.timing.phases:
67+
log.info(
68+
f"Timing: survey={result.timing.phases['coverage']:.1f}s "
69+
f"benchmarks={result.timing.phases['benchmarking']:.1f}s"
70+
)
14171
return 0

asv/commands/measure_impacted.py

Lines changed: 33 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,27 @@
11
# Licensed under a 3-clause BSD style license - see LICENSE.rst
22

33
import argparse
4-
import os
5-
import subprocess
64

7-
from .. import util
8-
from ..benchmarks import Benchmarks
95
from ..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
147
from . 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

asv/contrib/lightspeed/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .session import ( # noqa: F401
2+
LightspeedSession,
3+
InitResult,
4+
MeasureResult,
5+
BenchmarkDelta,
6+
TimingInfo,
7+
ASVError,
8+
ConfigError,
9+
BenchmarkError,
10+
NoBenchmarksError,
11+
)

0 commit comments

Comments
 (0)