aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/cmd/tools/repeat.v
blob: e69cfdfde707a1ebdb58075e619ea6ecd6631306 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
module main

import os
import flag
import time
import term
import math
import scripting

struct CmdResult {
mut:
	runs    int
	cmd     string
	icmd    int
	outputs []string
	oms     map[string][]int
	summary map[string]Aints
	timings []int
	atiming Aints
}

struct Context {
mut:
	count                   int
	series                  int
	warmup                  int
	show_help               bool
	show_output             bool
	use_newline             bool // use \n instead of \r, so the last line is not overwritten
	fail_on_regress_percent int
	fail_on_maxtime         int // in ms
	verbose                 bool
	commands                []string
	results                 []CmdResult
	cmd_template            string // {T} will be substituted with the current command
	cmd_params              map[string][]string
	cline                   string // a terminal clearing line
	cgoback                 string
	nmins                   int // number of minimums to discard
	nmaxs                   int // number of maximums to discard
}

[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()
	}
}

struct Aints {
	values []int
mut:
	imin    int
	imax    int
	average f64
	stddev  f64
	nmins   int // number of discarded fastest results
	nmaxs   int // number of discarded slowest results
}

[unsafe]
fn (mut a Aints) free() {
	unsafe { a.values.free() }
}

fn new_aints(ovals []int, extreme_mins int, extreme_maxs int) Aints {
	mut res := Aints{
		values: ovals // remember the original values
		nmins: extreme_mins
		nmaxs: extreme_maxs
	}
	mut sum := i64(0)
	mut imin := math.max_i32
	mut imax := -math.max_i32
	// discard the extremes:
	mut vals := []int{}
	for x in ovals {
		vals << x
	}
	vals.sort()
	if vals.len > extreme_mins + extreme_maxs {
		vals = vals[extreme_mins..vals.len - extreme_maxs].clone()
	} else {
		vals = []
	}
	// statistical processing of the remaining values:
	for i in vals {
		sum += i
		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)
	}
	res.stddev = math.sqrt(devsum / f64(vals.len))
	// 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')
	return res
}

fn bold(s string) string {
	return term.colorize(term.bold, s)
}

fn (a Aints) str() string {
	return bold('${a.average:6.2f}') +
		'ms ± σ: ${a.stddev:4.1f}ms, min: ${a.imin:4}ms, max: ${a.imax:4}ms, runs:${a.values.len:3}, nmins:${a.nmins:2}, nmaxs:${a.nmaxs:2}'
}

const (
	max_fail_percent             = 100 * 1000
	max_time                     = 60 * 1000 // ms
	performance_regression_label = 'Performance regression detected, failing since '
)

fn main() {
	mut context := Context{}
	context.parse_options()
	context.run()
	context.show_diff_summary()
}

fn (mut context Context) parse_options() {
	mut fp := flag.new_flag_parser(os.args)
	fp.application(os.file_name(os.executable()))
	fp.version('0.0.1')
	fp.description('Repeat command(s) and collect statistics. NB: you have to quote each command, if it contains spaces.')
	fp.arguments_description('CMD1 CMD2 ...')
	fp.skip_executable()
	fp.limit_free_args_to_at_least(1)
	context.count = fp.int('count', `c`, 10, 'Repetition count.')
	context.series = fp.int('series', `s`, 2, 'Series count. `-s 2 -c 4 a b` => aaaabbbbaaaabbbb, while `-s 3 -c 2 a b` => aabbaabbaabb.')
	context.warmup = fp.int('warmup', `w`, 2, 'Warmup runs. These are done *only at the start*, and are ignored.')
	context.show_help = fp.bool('help', `h`, false, 'Show this help screen.')
	context.use_newline = fp.bool('newline', `n`, false, 'Use \\n, do not overwrite the last line. Produces more output, but easier to diagnose.')
	context.show_output = fp.bool('output', `O`, false, 'Show command stdout/stderr in the progress indicator for each command. NB: slower, for verbose commands.')
	context.verbose = fp.bool('verbose', `v`, false, 'Be more verbose.')
	context.fail_on_maxtime = fp.int('max_time', `m`, max_time, 'Fail with exit code 2, when first cmd takes above M milliseconds (regression).')
	context.fail_on_regress_percent = fp.int('fail_percent', `f`, max_fail_percent, 'Fail with exit code 3, when first cmd is X% slower than the rest (regression).')
	context.cmd_template = fp.string('template', `t`, '{T}', 'Command template. {T} will be substituted with the current command.')
	cmd_params := fp.string_multi('parameter', `p`, 'A parameter substitution list. `{p}=val1,val2,val2` means that {p} in the template, will be substituted with each of val1, val2, val3.')
	context.nmins = fp.int('nmins', `i`, 0, 'Ignore the BOTTOM X results (minimum execution time). Makes the results more robust to performance flukes.')
	context.nmaxs = fp.int('nmaxs', `a`, 1, 'Ignore the TOP X results (maximum execution time). Makes the results more robust to performance flukes.')
	for p in cmd_params {
		parts := p.split(':')
		if parts.len > 1 {
			context.cmd_params[parts[0]] = parts[1].split(',')
		}
	}
	if context.show_help {
		println(fp.usage())
		exit(0)
	}
	if context.verbose {
		scripting.set_verbose(true)
	}
	commands := fp.finalize() or {
		eprintln('Error: $err')
		exit(1)
	}
	context.commands = context.expand_all_commands(commands)
	context.results = []CmdResult{len: context.commands.len, cap: 20, init: CmdResult{
		outputs: []string{cap: 500}
		timings: []int{cap: 500}
	}}
	if context.use_newline {
		context.cline = '\n'
		context.cgoback = '\n'
	} else {
		context.cline = '\r' + term.h_divider('')
		context.cgoback = '\r'
	}
}

fn (mut context Context) clear_line() {
	print(context.cline)
}

fn (mut context Context) expand_all_commands(commands []string) []string {
	mut all_commands := []string{}
	for cmd in commands {
		maincmd := context.cmd_template.replace('{T}', cmd)
		mut substituted_commands := []string{}
		substituted_commands << maincmd
		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
				}
				for sc in new_substituted_commands {
					substituted_commands << sc
				}
			}
		}
		for sc in substituted_commands {
			all_commands << sc
		}
	}
	mut unique := map[string]int{}
	for x in all_commands {
		if x.contains('{') && x.contains('}') {
			continue
		}
		unique[x] = 1
	}
	return unique.keys()
}

fn (mut context Context) run() {
	mut run_warmups := 0
	for si in 1 .. context.series + 1 {
		for icmd, cmd in context.commands {
			mut runs := 0
			mut duration := 0
			mut sum := 0
			mut oldres := ''
			println('Series: ${si:4}/${context.series:-4}, command: $cmd')
			if context.warmup > 0 && run_warmups < context.commands.len {
				for i in 1 .. context.warmup + 1 {
					print('${context.cgoback}warming up run: ${i:4}/${context.warmup:-4} for ${cmd:-50s} took ${duration:6} ms ...')
					mut sw := time.new_stopwatch()
					res := os.execute(cmd)
					if res.exit_code != 0 {
						continue
					}
					duration = int(sw.elapsed().milliseconds())
				}
				run_warmups++
			}
			context.clear_line()
			for i in 1 .. (context.count + 1) {
				avg := f64(sum) / f64(i)
				print('${context.cgoback}Average: ${avg:9.3f}ms | run: ${i:4}/${context.count:-4} | took ${duration:6} ms')
				if context.show_output {
					print(' | result: ${oldres:s}')
				}
				mut sw := time.new_stopwatch()
				res := scripting.exec(cmd) or { continue }
				duration = int(sw.elapsed().milliseconds())
				if res.exit_code != 0 {
					eprintln('${i:10} non 0 exit code for cmd: $cmd')
					continue
				}
				trimed_output := res.output.trim_right('\r\n')
				trimed_normalized := trimed_output.replace('\r\n', '\n')
				lines := trimed_normalized.split('\n')
				for line in lines {
					context.results[icmd].outputs << line
				}
				context.results[icmd].timings << duration
				sum += duration
				runs++
				oldres = res.output.replace('\n', ' ')
			}
			context.results[icmd].cmd = cmd
			context.results[icmd].icmd = icmd
			context.results[icmd].runs += runs
			context.results[icmd].atiming = new_aints(context.results[icmd].timings, context.nmins,
				context.nmaxs)
			context.clear_line()
			print(context.cgoback)
			mut m := map[string][]int{}
			ioutputs := context.results[icmd].outputs
			for o in ioutputs {
				x := o.split(':')
				if x.len > 1 {
					k := x[0]
					v := x[1].trim_left(' ').int()
					m[k] << v
				}
			}
			mut summary := map[string]Aints{}
			for k, v in m {
				// show a temporary summary for the current series/cmd cycle
				s := new_aints(v, context.nmins, context.nmaxs)
				println('  $k: $s')
				summary[k] = s
			}
			// merge current raw results to the previous ones
			old_oms := context.results[icmd].oms.move()
			mut new_oms := map[string][]int{}
			for k, v in m {
				if old_oms[k].len == 0 {
					new_oms[k] = v
				} else {
					new_oms[k] << old_oms[k]
					new_oms[k] << v
				}
			}
			context.results[icmd].oms = new_oms.move()
			// println('')
		}
	}
	// create full summaries, taking account of all runs
	for icmd in 0 .. context.results.len {
		mut new_full_summary := map[string]Aints{}
		for k, v in context.results[icmd].oms {
			new_full_summary[k] = new_aints(v, context.nmins, context.nmaxs)
		}
		context.results[icmd].summary = new_full_summary.move()
	}
}

fn (mut context Context) show_diff_summary() {
	context.results.sort_with_compare(fn (a &CmdResult, b &CmdResult) int {
		if a.atiming.average < b.atiming.average {
			return -1
		}
		if a.atiming.average > b.atiming.average {
			return 1
		}
		return 0
	})
	println('Summary (commands are ordered by ascending mean time), after $context.series series of $context.count repetitions:')
	base := context.results[0].atiming.average
	mut first_cmd_percentage := f64(100.0)
	mut first_marker := ''
	for i, r in context.results {
		first_marker = ' '
		cpercent := (r.atiming.average / base) * 100 - 100
		if r.icmd == 0 {
			first_marker = bold('>')
			first_cmd_percentage = cpercent
		}
		println(' $first_marker${(i + 1):3} | ${cpercent:5.1f}% slower | ${r.cmd:-57s} | $r.atiming')
	}
	$if debugcontext ? {
		println('context: $context')
	}
	if int(base) > context.fail_on_maxtime {
		print(performance_regression_label)
		println('average time: ${base:6.1f} ms > $context.fail_on_maxtime ms threshold.')
		exit(2)
	}
	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 {
		print(performance_regression_label)
		println('${first_cmd_percentage:5.1f}% > ${fail_threshold_max:5.1f}% threshold.')
		exit(3)
	}
}