| 1 | // Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | import os |
| 5 | import time |
| 6 | import arrays |
| 7 | import log |
| 8 | |
| 9 | const args = arguments() |
| 10 | const warmup_samples = 2 |
| 11 | |
| 12 | const max_samples = 20 |
| 13 | |
| 14 | const discard_highest_samples = 16 |
| 15 | |
| 16 | const voptions = ' -skip-unused -show-timings -stats ' |
| 17 | |
| 18 | const fast_dir = os.real_path(os.dir(@FILE)) |
| 19 | |
| 20 | const fast_log_path = os.real_path(os.join_path(fast_dir, 'fast.log')) |
| 21 | |
| 22 | const vdir = os.real_path(os.dir(os.dir(os.dir(fast_dir)))) |
| 23 | |
| 24 | fn elog(msg string) { |
| 25 | line := '${time.now().format_ss_micro()} ${msg}\n' |
| 26 | if mut f := os.open_append(fast_log_path) { |
| 27 | f.write_string(line) or {} |
| 28 | f.close() |
| 29 | } |
| 30 | log.info(msg) |
| 31 | } |
| 32 | |
| 33 | fn lsystem(cmd string) int { |
| 34 | elog('lsystem: ${cmd}') |
| 35 | return os.system(cmd) |
| 36 | } |
| 37 | |
| 38 | fn lexec(cmd string) string { |
| 39 | elog(' lexec: ${cmd}') |
| 40 | return os.execute(cmd).output.trim_right('\r\n') |
| 41 | } |
| 42 | |
| 43 | fn main() { |
| 44 | // ensure all log messages will be visible to the observers, even if the program panics |
| 45 | log.use_stdout() |
| 46 | log.set_always_flush(true) |
| 47 | |
| 48 | total_sw := time.new_stopwatch() |
| 49 | elog('fast.html generator start') |
| 50 | defer { |
| 51 | elog('fast.html generator end, total: ${total_sw.elapsed().milliseconds():6} ms') |
| 52 | } |
| 53 | |
| 54 | mut ccompiler_path := 'tcc' |
| 55 | if vdir.contains('/tmp/cirrus-ci-build') { |
| 56 | ccompiler_path = 'clang' |
| 57 | } |
| 58 | if args.contains('-clang') { |
| 59 | ccompiler_path = 'clang' |
| 60 | } |
| 61 | elog('fast_dir: ${fast_dir} | vdir: ${vdir} | compiler: ${ccompiler_path}') |
| 62 | |
| 63 | os.chdir(fast_dir)! |
| 64 | if !os.exists('${vdir}/v') && !os.is_dir('${vdir}/vlib') { |
| 65 | elog('fast.html generator needs to be located in `v/cmd/tools/fast`') |
| 66 | exit(1) |
| 67 | } |
| 68 | if !os.exists('table.html') { |
| 69 | os.create('table.html')! |
| 70 | } |
| 71 | |
| 72 | if !args.contains('-noupdate') { |
| 73 | elog('Fetching updates...') |
| 74 | ret := lsystem('${vdir}/v up') |
| 75 | if ret != 0 { |
| 76 | elog('failed to update V, exit_code: ${ret}') |
| 77 | return |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | // fetch the last commit's hash |
| 82 | commit := lexec('git rev-parse HEAD')[..8] |
| 83 | if os.exists('fast.vlang.io/index.html') { |
| 84 | uploaded_index := os.read_file('fast.vlang.io/index.html')! |
| 85 | if uploaded_index.contains('>${commit}<') { |
| 86 | elog('NOTE: commit ${commit} had been benchmarked already.') |
| 87 | if !args.contains('-force') { |
| 88 | elog('nothing more to do') |
| 89 | return |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | os.chdir(vdir)! |
| 95 | message := lexec('git log --pretty=format:"%s" -n1 ${commit}') |
| 96 | commit_date := lexec('git log -n1 --pretty="format:%at" ${commit}') |
| 97 | date := time.unix(commit_date.i64()) |
| 98 | |
| 99 | elog('Benchmarking commit ${commit} , with commit message: "${message}", commit_date: ${commit_date}, date: ${date}') |
| 100 | |
| 101 | // build an optimized V |
| 102 | if args.contains('-do-not-rebuild-vprod') { |
| 103 | if !os.exists('vprod') { |
| 104 | elog('Exiting, since if you use `-do-not-rebuild-vprod`, you should already have a `${vdir}/vprod` executable, but it is missing!') |
| 105 | return |
| 106 | } |
| 107 | } else { |
| 108 | elog(' Building vprod...') |
| 109 | if args.contains('-noprod') { |
| 110 | lexec('./v -o vprod cmd/v') // for faster debugging |
| 111 | } else { |
| 112 | lexec('./v -o vprod -prod -prealloc cmd/v') |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | if !args.contains('-do-not-rebuild-caches') { |
| 117 | elog('clearing caches...') |
| 118 | // cache vlib modules |
| 119 | lexec('${vdir}/v wipe-cache') |
| 120 | lexec('${vdir}/v -o vwarm_caches -cc ${ccompiler_path} cmd/v') |
| 121 | } |
| 122 | |
| 123 | // measure |
| 124 | diff1 := measure('${vdir}/vprod ${voptions} -o v.c cmd/v', 'v.c') |
| 125 | diff2 := measure('${vdir}/vprod ${voptions} -cc ${ccompiler_path} -o v2 cmd/v', 'v2') |
| 126 | diff3 := 0 // measure('${vdir}/vprod -native ${vdir}/cmd/tools/1mil.v', 'native 1mil') |
| 127 | diff4 := measure('${vdir}/vprod ${voptions} -cc ${ccompiler_path} examples/hello_world.v', |
| 128 | 'hello.v') |
| 129 | vc_size := os.file_size('v.c') / 1000 |
| 130 | scan, parse, check, cgen, vlines := measure_steps_minimal(vdir)! |
| 131 | |
| 132 | html_message := message.replace_each(['<', '<', '>', '>']) |
| 133 | |
| 134 | os.chdir(fast_dir)! |
| 135 | // place the new row on top |
| 136 | table := os.read_file('table.html')! |
| 137 | new_table := |
| 138 | ' <tr> |
| 139 | <td>${date.format()}</td> |
| 140 | <td><a target=_blank href="https://github.com/vlang/v/commit/${commit}">${commit}</a></td> |
| 141 | <td>${html_message}</td> |
| 142 | <td>${diff1}ms</td> |
| 143 | <td>${diff2}ms</td> |
| 144 | <td>${diff3}ms</td> |
| 145 | <td>${diff4}ms</td> |
| 146 | <td>${vc_size} KB</td> |
| 147 | <td>${parse}ms</td> |
| 148 | <td>${check}ms</td> |
| 149 | <td>${cgen}ms</td> |
| 150 | <td>${scan}ms</td> |
| 151 | <td>${vlines}</td> |
| 152 | <td>${int(f64(vlines) / f64(diff1) * 1000.0)}</td> |
| 153 | </tr>\n' + |
| 154 | table.trim_space() + '\n' |
| 155 | os.write_file('table.html', new_table)! |
| 156 | |
| 157 | // regenerate index.html |
| 158 | header := os.read_file('header.html')! |
| 159 | footer := os.read_file('footer.html')! |
| 160 | mut res := os.create('index.html')! |
| 161 | res.writeln(header)! |
| 162 | res.writeln(new_table)! |
| 163 | res.writeln(footer)! |
| 164 | res.close() |
| 165 | |
| 166 | // upload the result to github pages |
| 167 | if args.contains('-upload') { |
| 168 | $if freebsd { |
| 169 | // Note: tcc currently can not compile vpm on FreeBSD, due to its dependence on net.ssl and net.mbedtls, so force using clang instead: |
| 170 | elog('FreeBSD: compiling the VPM tool with clang...') |
| 171 | lexec('${vdir}/vprod -cc clang ${vdir}/cmd/tools/vpm/') |
| 172 | os.chdir('${fast_dir}/docs.vlang.io/docs_generator/')! |
| 173 | elog('FreeBSD: installing the dependencies for the docs generator...') |
| 174 | lexec('${vdir}/vprod install') |
| 175 | os.chdir(fast_dir)! |
| 176 | } |
| 177 | |
| 178 | os.chdir('${fast_dir}/fast.vlang.io/')! |
| 179 | elog('Uploading to fast.vlang.io/ ...') |
| 180 | lexec('git checkout gh-pages') |
| 181 | os.mv('../index.html', 'index.html')! |
| 182 | elog(' adding changes...') |
| 183 | lexec('git commit -am "update fast.vlang.io for commit ${commit}"') |
| 184 | elog(' pushing...') |
| 185 | lexec('git push origin gh-pages') |
| 186 | elog(' uploading to fast.vlang.io/ done') |
| 187 | os.chdir(fast_dir)! |
| 188 | |
| 189 | os.chdir('${fast_dir}/docs.vlang.io/')! |
| 190 | elog('Uploading to docs.vlang.io/ ...') |
| 191 | elog(' pulling upstream changes...') |
| 192 | lexec('git pull') |
| 193 | elog(' running build.vsh...') |
| 194 | lexec('${vdir}/vprod run build.vsh') |
| 195 | elog(' adding new docs...') |
| 196 | lexec('git add .') |
| 197 | elog(' commiting...') |
| 198 | lexec('git commit -am "update docs for commit ${commit}"') |
| 199 | elog(' pushing...') |
| 200 | lexec('git push') |
| 201 | elog(' uploading to fast.vlang.io/ done') |
| 202 | os.chdir(fast_dir)! |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | // measure returns milliseconds |
| 207 | fn measure(cmd string, description string) int { |
| 208 | elog(' Measuring ${description}, warmups: ${warmup_samples}, samples: ${max_samples}, discard: ${discard_highest_samples}, with cmd: `${cmd}`') |
| 209 | for _ in 0 .. warmup_samples { |
| 210 | os.system(cmd) |
| 211 | } |
| 212 | mut runs := []int{} |
| 213 | for r in 0 .. max_samples { |
| 214 | sw := time.new_stopwatch() |
| 215 | os.execute(cmd) |
| 216 | sample := int(sw.elapsed().milliseconds()) |
| 217 | runs << sample |
| 218 | elog(' Sample ${r + 1:2}/${max_samples:2} ... ${sample} ms') |
| 219 | } |
| 220 | runs.sort() |
| 221 | elog(' runs before discarding: ${runs}, avg: ${f64(arrays.sum(runs) or { 0 }) / runs.len:5.2f}') |
| 222 | // Discard the highest times, since on AWS, they are caused by random load spikes, |
| 223 | // that are unpredictable, add noise and skew the statistics, without adding useful |
| 224 | // insights: |
| 225 | for _ in 0 .. discard_highest_samples { |
| 226 | runs.pop() |
| 227 | } |
| 228 | elog(' runs after discarding: ${runs}, avg: ${f64(arrays.sum(runs) or { 0 }) / runs.len:5.2f}') |
| 229 | return int(f64(arrays.sum(runs) or { 0 }) / runs.len) |
| 230 | } |
| 231 | |
| 232 | fn measure_steps_minimal(vdir string) !(int, int, int, int, int) { |
| 233 | elog('measure_steps_minimal ${vdir}, samples: ${max_samples}') |
| 234 | mut scans, mut parses, mut checks, mut cgens, mut vliness := []int{}, []int{}, []int{}, []int{}, []int{} |
| 235 | for i in 0 .. max_samples { |
| 236 | scan, parse, check, cgen, vlines, cmd := measure_steps_one_sample(vdir) |
| 237 | scans << scan |
| 238 | parses << parse |
| 239 | checks << check |
| 240 | cgens << cgen |
| 241 | vliness << vlines |
| 242 | elog(' [${i:2}/${max_samples:2}] scan: ${scan} ms, min parse: ${parse} ms, min check: ${check} ms, min cgen: ${cgen} ms, min vlines: ${vlines} ms, cmd: ${cmd}') |
| 243 | } |
| 244 | scan, parse, check, cgen, vlines := arrays.min(scans)!, arrays.min(parses)!, arrays.min(checks)!, arrays.min(cgens)!, arrays.min(vliness)! |
| 245 | elog('measure_steps_minimal => min scan: ${scan} ms, min parse: ${parse} ms, min check: ${check} ms, min cgen: ${cgen} ms, min vlines: ${vlines} ms') |
| 246 | return scan, parse, check, cgen, vlines |
| 247 | } |
| 248 | |
| 249 | fn measure_steps_one_sample(vdir string) (int, int, int, int, int, string) { |
| 250 | cmd := '${vdir}/vprod ${voptions} -o v.c cmd/v' |
| 251 | resp := os.execute(cmd) |
| 252 | |
| 253 | mut scan, mut parse, mut check, mut cgen, mut vlines := 0, 0, 0, 0, 0 |
| 254 | lines := resp.output.split_into_lines() |
| 255 | if lines.len == 3 { |
| 256 | parse = lines[0].before('.').int() |
| 257 | check = lines[1].before('.').int() |
| 258 | cgen = lines[2].before('.').int() |
| 259 | } else { |
| 260 | ms_lines := lines.map(it.split(' ms ')) |
| 261 | for line in ms_lines { |
| 262 | if line.len == 2 { |
| 263 | if line[1] == 'SCAN' { |
| 264 | scan = line[0].int() |
| 265 | } |
| 266 | if line[1] == 'PARSE' { |
| 267 | parse = line[0].int() |
| 268 | } |
| 269 | if line[1] == 'CHECK' { |
| 270 | check = line[0].int() |
| 271 | } |
| 272 | if line[1] == 'C GEN' { |
| 273 | cgen = line[0].int() |
| 274 | } |
| 275 | } else { |
| 276 | // fetch number of V lines |
| 277 | if line[0].contains('V') && line[0].contains('source') && line[0].contains('size') { |
| 278 | start := line[0].index(':') or { 0 } |
| 279 | end := line[0].index('lines,') or { 0 } |
| 280 | s := line[0][start + 1..end] |
| 281 | vlines = s.trim_space().int() |
| 282 | } |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | return scan, parse, check, cgen, vlines, cmd |
| 287 | } |
| 288 | |