77
88from printo import describe_data_object
99from printo .reprs import superrepr
10+ from sigmatch import SignatureMismatchError
1011
12+ from microbenchmark ._render import draw_box , draw_histogram , terminal_width
1113from microbenchmark .arguments import arguments as Arguments # noqa: N812
1214from microbenchmark .benchmark_result import BenchmarkResult
1315
@@ -19,6 +21,7 @@ class _CliArgs:
1921 def __init__ (self ) -> None :
2022 self .number : int | None = None
2123 self .max_mean : float | None = None
24+ self .histogram : bool = False
2225
2326
2427class Scenario :
@@ -34,6 +37,13 @@ def __init__( # noqa: PLR0913
3437 ) -> None :
3538 if number < 1 :
3639 raise ValueError (f'number must be at least 1, got { number } ' )
40+ checker : Arguments = arguments if isinstance (arguments , Arguments ) else Arguments ()
41+ if not checker .match (function ):
42+ fn_name : object = getattr (function , '__name__' , repr (function ))
43+ raise SignatureMismatchError (
44+ f'Scenario arguments { checker !r} are incompatible with the '
45+ f'signature of { fn_name } ' ,
46+ )
3747 self .function : object = function
3848 self ._arguments : Arguments | None = arguments
3949 if name is None and hasattr (function , '__name__' ):
@@ -56,24 +66,29 @@ def run(self, warmup: int = 0) -> BenchmarkResult:
5666 self ._call_once ()
5767 timer ()
5868 durations : list [float ] = []
69+ loop_start = timer ()
5970 for _ in range (self .number ):
6071 start = timer ()
6172 self ._call_once ()
6273 end = timer ()
6374 durations .append (end - start )
75+ loop_end = timer ()
6476 return BenchmarkResult (
6577 scenario = self ,
6678 durations = tuple (durations ),
79+ total_duration = loop_end - loop_start ,
6780 is_primary = True ,
6881 )
6982
70- def cli (self ) -> None :
83+ def cli (self , argv : list [ str ] | None = None ) -> None :
7184 parser = argparse .ArgumentParser (description = self .doc or f'Benchmark: { self .name } ' )
7285 parser .add_argument ('--number' , type = int , default = None , help = 'Number of iterations' )
7386 parser .add_argument ('--max-mean' , type = float , default = None , dest = 'max_mean' ,
7487 help = 'Fail if mean time (seconds) exceeds this threshold' )
88+ parser .add_argument ('--histogram' , action = 'store_true' , default = False ,
89+ help = 'Append an ASCII histogram of per-call timings' )
7590 cli_args = _CliArgs ()
76- parser .parse_args (namespace = cli_args )
91+ parser .parse_args (argv , namespace = cli_args )
7792
7893 scenario = self
7994 if cli_args .number is not None :
@@ -87,7 +102,13 @@ def cli(self) -> None:
87102 )
88103
89104 result = scenario .run ()
90- _print_result (result )
105+ width = terminal_width ()
106+ lines = _render_result (result )
107+ if cli_args .histogram :
108+ lines .append ('' )
109+ lines .extend (draw_histogram (list (result .durations ), width - 4 , 8 ))
110+ box = draw_box (lines , width )
111+ sys .stdout .write ('\n ' .join (box ) + '\n ' )
91112
92113 if cli_args .max_mean is not None and result .mean > cli_args .max_mean :
93114 sys .stdout .write (
@@ -128,19 +149,23 @@ def _fn_call_str(function: object, arguments: Arguments | None) -> str:
128149 return describe_data_object (fn_name , args , kwargs )
129150
130151
131- def _print_result (result : BenchmarkResult ) -> None :
152+ def _render_result (result : BenchmarkResult ) -> list [ str ] :
132153 scenario = result .scenario
133154 assert scenario is not None
134155 call_str = _fn_call_str (scenario .function , scenario ._arguments )
135156 label_width = len ('p95 mean:' )
136- sys .stdout .write (f'benchmark: { scenario .name } \n ' )
137- sys .stdout .write (f'{ "call:" .ljust (label_width )} { call_str } \n ' )
157+ lines : list [str ] = []
158+ lines .append (f'benchmark: { scenario .name } ' )
159+ lines .append (f'{ "call:" .ljust (label_width )} { call_str } ' )
138160 if scenario .doc :
139- sys .stdout .write (f'{ "doc:" .ljust (label_width )} { scenario .doc } \n ' )
140- sys .stdout .write (f'{ "runs:" .ljust (label_width )} { scenario .number } \n ' )
141- sys .stdout .write (f'{ "mean:" .ljust (label_width )} { result .mean :.6f} s\n ' )
142- sys .stdout .write (f'{ "median:" .ljust (label_width )} { result .median :.6f} s\n ' )
143- sys .stdout .write (f'{ "best:" .ljust (label_width )} { result .best :.6f} s\n ' )
144- sys .stdout .write (f'{ "worst:" .ljust (label_width )} { result .worst :.6f} s\n ' )
145- sys .stdout .write (f'{ "p95 mean:" .ljust (label_width )} { result .p95 .mean :.6f} s\n ' )
146- sys .stdout .write (f'{ "p99 mean:" .ljust (label_width )} { result .p99 .mean :.6f} s\n ' )
161+ lines .append (f'{ "doc:" .ljust (label_width )} { scenario .doc } ' )
162+ lines .append (f'{ "runs:" .ljust (label_width )} { scenario .number } ' )
163+ lines .append (f'{ "mean:" .ljust (label_width )} { result .mean :.6f} s' )
164+ lines .append (f'{ "median:" .ljust (label_width )} { result .median :.6f} s' )
165+ lines .append (f'{ "best:" .ljust (label_width )} { result .best :.6f} s' )
166+ lines .append (f'{ "worst:" .ljust (label_width )} { result .worst :.6f} s' )
167+ lines .append (f'{ "p95 mean:" .ljust (label_width )} { result .p95 .mean :.6f} s' )
168+ lines .append (f'{ "p99 mean:" .ljust (label_width )} { result .p99 .mean :.6f} s' )
169+ lines .append (f'{ "total:" .ljust (label_width )} { result .total_duration :.6f} s' )
170+ lines .append (f'{ "fn total:" .ljust (label_width )} { result .functions_duration :.6f} s' )
171+ return lines
0 commit comments