2020-10-01 20:05:27 +03:00
module main
import os
import flag
import time
2020-10-01 23:25:29 +03:00
import term
import math
2020-10-01 20:05:27 +03:00
import scripting
struct CmdResult {
mut :
2020-10-15 16:17:52 +03:00
runs int
cmd string
icmd int
2020-10-01 20:05:27 +03:00
outputs [ ] string
2020-10-15 16:17:52 +03:00
oms map [ string ] [ ] int
2020-10-01 23:25:29 +03:00
summary map [ string ] Aints
2020-10-01 20:05:27 +03:00
timings [ ] int
2020-10-01 23:25:29 +03:00
atiming Aints
2020-10-01 20:05:27 +03:00
}
2020-10-15 16:17:52 +03:00
2020-10-01 20:05:27 +03:00
struct Context {
mut :
2020-10-15 16:17:52 +03:00
count int
series int
warmup int
show_help bool
show_output bool
2020-12-04 19:11:43 +03:00
use_newline bool // use \n instead of \r, so the last line is not overwritten
2020-10-02 10:57:58 +03:00
fail_on_regress_percent int
2020-10-15 16:17:52 +03:00
fail_on_maxtime int // in ms
verbose bool
commands [ ] string
results [ ] CmdResult
2020-11-29 17:13:45 +03:00
cmd_template string // {T} will be substituted with the current command
cmd_params map [ string ] [ ] string
2020-10-15 16:17:52 +03:00
cline string // a terminal clearing line
2020-12-04 19:11:43 +03:00
cgoback string
2020-12-04 20:18:58 +03:00
nmins int // number of minimums to discard
nmaxs int // number of maximums to discard
2020-10-01 20:05:27 +03:00
}
2021-03-23 23:13:47 +03:00
[ unsafe ]
fn ( mut result CmdResult ) free ( ) {
unsafe {
result . cmd . free ( )
result . outputs . free ( )
result . oms . free ( )
result . summary . free ( )
result . timings . free ( )
result . atiming . free ( )
}
}
[ unsafe ]
fn ( mut context Context ) free ( ) {
unsafe {
context . commands . free ( )
context . results . free ( )
context . cmd_template . free ( )
context . cmd_params . free ( )
context . cline . free ( )
context . cgoback . free ( )
}
}
2020-10-01 23:25:29 +03:00
struct Aints {
2021-03-08 21:52:13 +03:00
values [ ] int
2020-10-01 23:25:29 +03:00
mut :
2020-10-15 16:17:52 +03:00
imin int
imax int
2020-10-01 23:25:29 +03:00
average f64
2020-10-15 16:17:52 +03:00
stddev f64
2020-12-04 20:18:58 +03:00
nmins int // number of discarded fastest results
nmaxs int // number of discarded slowest results
2020-10-01 23:25:29 +03:00
}
2020-10-15 16:17:52 +03:00
2021-03-23 23:13:47 +03:00
[ unsafe ]
fn ( mut a Aints ) free ( ) {
unsafe { a . values . free ( ) }
}
2020-12-04 20:18:58 +03:00
fn new_aints ( ovals [ ] int , extreme_mins int , extreme_maxs int ) Aints {
2020-10-15 16:17:52 +03:00
mut res := Aints {
2020-12-04 20:18:58 +03:00
values : ovals // remember the original values
nmins : extreme_mins
nmaxs : extreme_maxs
2020-10-15 16:17:52 +03:00
}
2020-10-01 23:25:29 +03:00
mut sum := i64 ( 0 )
2020-10-15 16:17:52 +03:00
mut imin := math . max_i32
2020-10-01 23:25:29 +03:00
mut imax := - math . max_i32
2020-12-04 20:18:58 +03:00
// discard the extremes:
2021-03-23 19:54:37 +03:00
mut vals := [ ] int { }
for x in ovals {
vals << x
}
2020-12-04 20:18:58 +03:00
vals . sort ( )
if vals . len > extreme_mins + extreme_maxs {
2021-03-23 19:54:37 +03:00
vals = vals [ extreme_mins .. vals . len - extreme_maxs ] . clone ( )
2020-12-04 20:18:58 +03:00
} else {
vals = [ ]
}
// statistical processing of the remaining values:
2020-10-01 23:25:29 +03:00
for i in vals {
2020-10-15 16:17:52 +03:00
sum += i
2020-10-01 23:25:29 +03:00
if i < imin {
imin = i
}
if i > imax {
imax = i
}
}
res . imin = imin
res . imax = imax
if vals . len > 0 {
res . average = sum / f64 ( vals . len )
}
//
mut devsum := f64 ( 0.0 )
for i in vals {
x := f64 ( i ) - res . average
devsum += ( x * x )
}
2020-10-15 16:17:52 +03:00
res . stddev = math . sqrt ( devsum / f64 ( vals . len ) )
2020-12-04 20:18:58 +03:00
// eprintln('\novals: $ovals\n vals: $vals\n vals.len: $vals.len | res.imin: $res.imin | res.imax: $res.imax | res.average: $res.average | res.stddev: $res.stddev')
2020-10-01 23:25:29 +03:00
return res
}
2020-10-15 16:17:52 +03:00
2021-07-27 12:35:54 +03:00
fn bold ( s string ) string {
return term . colorize ( term . bold , s )
}
2020-10-15 16:17:52 +03:00
fn ( a Aints ) str ( ) string {
2021-07-27 12:35:54 +03:00
return bold ( ' $ { a . average : 6 .2 f } ' ) +
2020-12-04 20:18:58 +03:00
' m s ± σ : $ { a . stddev : 4 .1 f } m s , m i n : $ { a . imin : 4 } m s , m a x : $ { a . imax : 4 } m s , r u n s : $ { a . values . len : 3 } , n m i n s : $ { a . nmins : 2 } , n m a x s : $ { a . nmaxs : 2 } '
2020-10-15 16:17:52 +03:00
}
2020-10-01 23:25:29 +03:00
2020-10-02 10:57:58 +03:00
const (
2020-12-04 20:18:58 +03:00
max_fail_percent = 100 * 1000
2020-10-15 16:17:52 +03:00
max_time = 60 * 1000 // ms
2020-10-09 11:06:00 +03:00
performance_regression_label = ' P e r f o r m a n c e r e g r e s s i o n d e t e c t e d , f a i l i n g s i n c e '
2020-10-02 10:57:58 +03:00
)
2020-10-15 16:17:52 +03:00
fn main ( ) {
2020-10-01 20:05:27 +03:00
mut context := Context { }
2022-10-16 09:28:57 +03:00
context . parse_options ( ) !
2020-10-01 20:46:45 +03:00
context . run ( )
context . show_diff_summary ( )
}
2022-10-16 09:28:57 +03:00
fn ( mut context Context ) parse_options ( ) ! {
2020-10-01 20:05:27 +03:00
mut fp := flag . new_flag_parser ( os . args )
fp . application ( os . file_name ( os . executable ( ) ) )
fp . version ( ' 0 . 0 . 1 ' )
2022-03-06 20:01:22 +03:00
fp . description ( ' R e p e a t c o m m a n d ( s ) a n d c o l l e c t s t a t i s t i c s . N o t e : y o u h a v e t o q u o t e e a c h c o m m a n d , i f i t c o n t a i n s s p a c e s . ' )
2020-10-01 20:05:27 +03:00
fp . arguments_description ( ' C M D 1 C M D 2 . . . ' )
fp . skip_executable ( )
2022-10-16 09:28:57 +03:00
fp . limit_free_args_to_at_least ( 1 ) !
2020-10-02 13:28:05 +03:00
context . count = fp . int ( ' c o u n t ' , ` c ` , 10 , ' R e p e t i t i o n c o u n t . ' )
context . series = fp . int ( ' s e r i e s ' , ` s ` , 2 , ' S e r i e s c o u n t . ` - s 2 - c 4 a b ` = > a a a a b b b b a a a a b b b b , w h i l e ` - s 3 - c 2 a b ` = > a a b b a a b b a a b b . ' )
context . warmup = fp . int ( ' w a r m u p ' , ` w ` , 2 , ' W a r m u p r u n s . T h e s e a r e d o n e * o n l y a t t h e s t a r t * , a n d a r e i g n o r e d . ' )
2020-10-01 20:05:27 +03:00
context . show_help = fp . bool ( ' h e l p ' , ` h ` , false , ' S h o w t h i s h e l p s c r e e n . ' )
2020-12-04 20:18:58 +03:00
context . use_newline = fp . bool ( ' n e w l i n e ' , ` n ` , false , ' U s e \\ n , d o n o t o v e r w r i t e t h e l a s t l i n e . P r o d u c e s m o r e o u t p u t , b u t e a s i e r t o d i a g n o s e . ' )
2022-03-06 20:01:22 +03:00
context . show_output = fp . bool ( ' o u t p u t ' , ` O ` , false , ' S h o w c o m m a n d s t d o u t / s t d e r r i n t h e p r o g r e s s i n d i c a t o r f o r e a c h c o m m a n d . N o t e : s l o w e r , f o r v e r b o s e c o m m a n d s . ' )
2020-10-01 20:05:27 +03:00
context . verbose = fp . bool ( ' v e r b o s e ' , ` v ` , false , ' B e m o r e v e r b o s e . ' )
2020-10-09 11:06:00 +03:00
context . fail_on_maxtime = fp . int ( ' m a x _ t i m e ' , ` m ` , max_time , ' F a i l w i t h e x i t c o d e 2 , w h e n f i r s t c m d t a k e s a b o v e M m i l l i s e c o n d s ( r e g r e s s i o n ) . ' )
context . fail_on_regress_percent = fp . int ( ' f a i l _ p e r c e n t ' , ` f ` , max_fail_percent , ' F a i l w i t h e x i t c o d e 3 , w h e n f i r s t c m d i s X % s l o w e r t h a n t h e r e s t ( r e g r e s s i o n ) . ' )
2022-10-27 17:52:30 +03:00
context . cmd_template = fp . string ( ' t e m p l a t e ' , ` t ` , r '{T}' , r 'Command template. {T} will be substituted with the current command.' )
cmd_params := fp . string_multi ( ' p a r a m e t e r ' , ` p ` , r 'A parameter substitution list. `{p}=val1,val2,val2` means that {p} in the template, will be substituted with each of val1, val2, val3.' )
2020-12-04 20:18:58 +03:00
context . nmins = fp . int ( ' n m i n s ' , ` i ` , 0 , ' I g n o r e t h e B O T T O M X r e s u l t s ( m i n i m u m e x e c u t i o n t i m e ) . M a k e s t h e r e s u l t s m o r e r o b u s t t o p e r f o r m a n c e f l u k e s . ' )
context . nmaxs = fp . int ( ' n m a x s ' , ` a ` , 1 , ' I g n o r e t h e T O P X r e s u l t s ( m a x i m u m e x e c u t i o n t i m e ) . M a k e s t h e r e s u l t s m o r e r o b u s t t o p e r f o r m a n c e f l u k e s . ' )
2020-11-29 17:13:45 +03:00
for p in cmd_params {
parts := p . split ( ' : ' )
if parts . len > 1 {
context . cmd_params [ parts [ 0 ] ] = parts [ 1 ] . split ( ' , ' )
}
}
2020-10-01 20:05:27 +03:00
if context . show_help {
println ( fp . usage ( ) )
exit ( 0 )
}
if context . verbose {
scripting . set_verbose ( true )
}
2020-11-29 17:13:45 +03:00
commands := fp . finalize ( ) or {
2021-02-28 22:24:29 +03:00
eprintln ( ' E r r o r : $ err ' )
2020-10-01 20:05:27 +03:00
exit ( 1 )
}
2020-11-29 17:13:45 +03:00
context . commands = context . expand_all_commands ( commands )
2021-05-07 14:40:52 +03:00
context . results = [ ] CmdResult { len : context . commands . len , cap : 20 , init : CmdResult {
outputs : [ ] string { cap : 500 }
timings : [ ] int { cap : 500 }
2021-03-23 19:54:37 +03:00
} }
2020-12-04 19:11:43 +03:00
if context . use_newline {
context . cline = ' \n '
context . cgoback = ' \n '
} else {
context . cline = ' \r ' + term . h_divider ( ' ' )
context . cgoback = ' \r '
}
2020-10-01 23:25:29 +03:00
}
2022-05-30 19:15:05 +03:00
fn flushed_print ( s string ) {
print ( s )
flush_stdout ( )
}
2020-10-01 23:25:29 +03:00
fn ( mut context Context ) clear_line ( ) {
2022-05-30 19:15:05 +03:00
flushed_print ( context . cline )
2020-10-01 20:46:45 +03:00
}
2020-11-29 17:13:45 +03:00
fn ( mut context Context ) expand_all_commands ( commands [ ] string ) [ ] string {
mut all_commands := [ ] string { }
for cmd in commands {
2022-10-27 17:52:30 +03:00
maincmd := context . cmd_template . replace ( r '{T}' , cmd )
2021-03-23 19:54:37 +03:00
mut substituted_commands := [ ] string { }
substituted_commands << maincmd
2020-11-29 17:13:45 +03:00
for paramk , paramlist in context . cmd_params {
for paramv in paramlist {
mut new_substituted_commands := [ ] string { }
for cscmd in substituted_commands {
scmd := cscmd . replace ( paramk , paramv )
new_substituted_commands << scmd
}
2021-03-23 19:54:37 +03:00
for sc in new_substituted_commands {
substituted_commands << sc
}
2020-11-29 17:13:45 +03:00
}
}
2021-03-23 19:54:37 +03:00
for sc in substituted_commands {
all_commands << sc
}
2020-11-29 17:13:45 +03:00
}
mut unique := map [ string ] int { }
for x in all_commands {
if x . contains ( ' { ' ) && x . contains ( ' } ' ) {
continue
}
unique [ x ] = 1
}
return unique . keys ( )
}
2020-10-01 20:46:45 +03:00
fn ( mut context Context ) run ( ) {
2020-10-02 13:28:05 +03:00
mut run_warmups := 0
2020-10-15 16:17:52 +03:00
for si in 1 .. context . series + 1 {
2020-10-02 13:28:05 +03:00
for icmd , cmd in context . commands {
mut runs := 0
mut duration := 0
mut sum := 0
mut oldres := ' '
println ( ' S e r i e s : $ { si : 4 } / $ { context . series : - 4 } , c o m m a n d : $ cmd ' )
if context . warmup > 0 && run_warmups < context . commands . len {
2020-10-15 16:17:52 +03:00
for i in 1 .. context . warmup + 1 {
2022-05-30 19:15:05 +03:00
flushed_print ( ' $ { context . cgoback } w a r m i n g u p r u n : $ { i : 4 } / $ { context . warmup : - 4 } f o r $ { cmd : - 50 s } t o o k $ { duration : 6 } m s . . . ' )
2021-07-20 11:17:08 +03:00
mut sw := time . new_stopwatch ( )
2021-03-08 21:52:13 +03:00
res := os . execute ( cmd )
if res . exit_code != 0 {
continue
}
2020-10-02 13:28:05 +03:00
duration = int ( sw . elapsed ( ) . milliseconds ( ) )
}
run_warmups ++
}
context . clear_line ( )
2020-10-15 16:17:52 +03:00
for i in 1 .. ( context . count + 1 ) {
avg := f64 ( sum ) / f64 ( i )
2022-05-30 19:15:05 +03:00
flushed_print ( ' $ { context . cgoback } A v e r a g e : $ { avg : 9 .3 f } m s | r u n : $ { i : 4 } / $ { context . count : - 4 } | t o o k $ { duration : 6 } m s ' )
2020-10-02 18:10:25 +03:00
if context . show_output {
2022-05-30 19:15:05 +03:00
flushed_print ( ' | r e s u l t : $ { oldres : s } ' )
2020-10-02 13:28:05 +03:00
}
2021-07-20 11:17:08 +03:00
mut sw := time . new_stopwatch ( )
2020-12-04 19:11:43 +03:00
res := scripting . exec ( cmd ) or { continue }
2020-10-01 20:13:19 +03:00
duration = int ( sw . elapsed ( ) . milliseconds ( ) )
2020-10-02 13:28:05 +03:00
if res . exit_code != 0 {
eprintln ( ' $ { i : 10 } n o n 0 e x i t c o d e f o r c m d : $ cmd ' )
continue
}
2021-03-23 19:54:37 +03:00
trimed_output := res . output . trim_right ( ' \r \n ' )
trimed_normalized := trimed_output . replace ( ' \r \n ' , ' \n ' )
lines := trimed_normalized . split ( ' \n ' )
2021-03-23 23:13:47 +03:00
for line in lines {
context . results [ icmd ] . outputs << line
}
2020-10-02 13:28:05 +03:00
context . results [ icmd ] . timings << duration
sum += duration
runs ++
oldres = res . output . replace ( ' \n ' , ' ' )
2020-10-01 20:13:19 +03:00
}
2020-10-02 13:28:05 +03:00
context . results [ icmd ] . cmd = cmd
context . results [ icmd ] . icmd = icmd
2020-10-06 08:12:09 +03:00
context . results [ icmd ] . runs += runs
2020-12-04 20:18:58 +03:00
context . results [ icmd ] . atiming = new_aints ( context . results [ icmd ] . timings , context . nmins ,
context . nmaxs )
2020-10-02 13:28:05 +03:00
context . clear_line ( )
2022-05-30 19:15:05 +03:00
flushed_print ( context . cgoback )
2020-10-15 16:17:52 +03:00
mut m := map [ string ] [ ] int { }
2021-05-06 21:46:35 +03:00
ioutputs := context . results [ icmd ] . outputs
for o in ioutputs {
2020-10-02 13:28:05 +03:00
x := o . split ( ' : ' )
if x . len > 1 {
k := x [ 0 ]
v := x [ 1 ] . trim_left ( ' ' ) . int ( )
m [ k ] << v
}
2020-10-09 11:06:00 +03:00
}
2020-10-02 13:28:05 +03:00
mut summary := map [ string ] Aints { }
2020-10-15 16:17:52 +03:00
for k , v in m {
2020-10-06 08:12:09 +03:00
// show a temporary summary for the current series/cmd cycle
2020-12-04 20:18:58 +03:00
s := new_aints ( v , context . nmins , context . nmaxs )
2020-10-02 13:28:05 +03:00
println ( ' $ k : $ s ' )
summary [ k ] = s
2020-10-01 20:05:27 +03:00
}
2020-10-06 08:12:09 +03:00
// merge current raw results to the previous ones
2021-02-16 14:46:12 +03:00
old_oms := context . results [ icmd ] . oms . move ( )
2020-10-15 16:17:52 +03:00
mut new_oms := map [ string ] [ ] int { }
for k , v in m {
2020-10-06 08:12:09 +03:00
if old_oms [ k ] . len == 0 {
new_oms [ k ] = v
} else {
new_oms [ k ] << old_oms [ k ]
new_oms [ k ] << v
}
}
2021-02-16 14:46:12 +03:00
context . results [ icmd ] . oms = new_oms . move ( )
2020-10-15 16:17:52 +03:00
// println('')
2020-10-01 23:25:29 +03:00
}
2020-10-01 21:06:32 +03:00
}
2020-10-06 08:12:09 +03:00
// create full summaries, taking account of all runs
2020-10-15 16:17:52 +03:00
for icmd in 0 .. context . results . len {
2020-10-06 08:12:09 +03:00
mut new_full_summary := map [ string ] Aints { }
2020-10-15 16:17:52 +03:00
for k , v in context . results [ icmd ] . oms {
2020-12-04 20:18:58 +03:00
new_full_summary [ k ] = new_aints ( v , context . nmins , context . nmaxs )
2020-10-06 08:12:09 +03:00
}
2021-02-16 14:46:12 +03:00
context . results [ icmd ] . summary = new_full_summary . move ( )
2020-10-06 08:12:09 +03:00
}
2020-10-01 20:46:45 +03:00
}
2020-10-15 16:17:52 +03:00
2020-10-01 20:46:45 +03:00
fn ( mut context Context ) show_diff_summary ( ) {
2020-10-15 16:17:52 +03:00
context . results . sort_with_compare ( fn ( a & CmdResult , b & CmdResult ) int {
2020-10-01 23:25:29 +03:00
if a . atiming . average < b . atiming . average {
return - 1
}
if a . atiming . average > b . atiming . average {
return 1
}
return 0
} )
2020-10-02 13:28:05 +03:00
println ( ' S u m m a r y ( c o m m a n d s a r e o r d e r e d b y a s c e n d i n g m e a n t i m e ) , a f t e r $ context . series s e r i e s o f $ context . count r e p e t i t i o n s : ' )
2020-10-01 23:25:29 +03:00
base := context . results [ 0 ] . atiming . average
2020-10-02 10:57:58 +03:00
mut first_cmd_percentage := f64 ( 100.0 )
2021-03-23 19:54:37 +03:00
mut first_marker := ' '
2020-10-01 23:25:29 +03:00
for i , r in context . results {
2021-03-23 19:54:37 +03:00
first_marker = ' '
2020-10-02 10:57:58 +03:00
cpercent := ( r . atiming . average / base ) * 100 - 100
if r . icmd == 0 {
2021-07-27 12:35:54 +03:00
first_marker = bold ( ' > ' )
2020-10-02 10:57:58 +03:00
first_cmd_percentage = cpercent
}
2020-12-04 20:18:58 +03:00
println ( ' $ first_marker $ { ( i + 1 ) : 3 } | $ { cpercent : 5 .1 f } % s l o w e r | $ { r . cmd : - 57 s } | $ r . atiming ' )
2020-10-02 10:57:58 +03:00
}
2020-10-09 11:06:00 +03:00
$ if debug context ? {
println ( ' c o n t e x t : $ context ' )
}
if int ( base ) > context . fail_on_maxtime {
2022-05-30 19:15:05 +03:00
flushed_print ( performance_regression_label )
2020-10-15 16:17:52 +03:00
println ( ' a v e r a g e t i m e : $ { base : 6 .1 f } m s > $ context . fail_on_maxtime m s t h r e s h o l d . ' )
2020-10-09 11:06:00 +03:00
exit ( 2 )
}
2020-10-02 10:57:58 +03:00
if context . fail_on_regress_percent == max_fail_percent || context . results . len < 2 {
return
}
fail_threshold_max := f64 ( context . fail_on_regress_percent )
if first_cmd_percentage > fail_threshold_max {
2022-05-30 19:15:05 +03:00
flushed_print ( performance_regression_label )
2020-10-02 10:57:58 +03:00
println ( ' $ { first_cmd_percentage : 5 .1 f } % > $ { fail_threshold_max : 5 .1 f } % t h r e s h o l d . ' )
2020-10-09 11:06:00 +03:00
exit ( 3 )
2020-10-01 23:25:29 +03:00
}
2020-10-01 20:05:27 +03:00
}