| 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 | res := os.execute(cmd) |
| 41 | if res.exit_code != 0 { |
| 42 | elog(' lexec FAILED, exit_code: ${res.exit_code}, output:\n${res.output}') |
| 43 | } |
| 44 | return res.output.trim_right('\r\n') |
| 45 | } |
| 46 | |
| 47 | // lexec_check runs cmd, logs its exit code and output, and returns true on success. |
| 48 | // Use it for critical steps where a failure should short-circuit the rest of a sequence, |
| 49 | // e.g. when a failed `git pull` would otherwise leave the docs.vlang.io repo in a state |
| 50 | // where the subsequent `git push` is silently a no-op. |
| 51 | fn lexec_check(cmd string) bool { |
| 52 | elog(' lexec_check: ${cmd}') |
| 53 | res := os.execute(cmd) |
| 54 | if res.exit_code != 0 { |
| 55 | elog(' lexec_check FAILED, exit_code: ${res.exit_code}, output:\n${res.output}') |
| 56 | return false |
| 57 | } |
| 58 | return true |
| 59 | } |
| 60 | |
| 61 | fn main() { |
| 62 | // ensure all log messages will be visible to the observers, even if the program panics |
| 63 | log.use_stdout() |
| 64 | log.set_always_flush(true) |
| 65 | |
| 66 | total_sw := time.new_stopwatch() |
| 67 | elog('fast.html generator start') |
| 68 | defer { |
| 69 | elog('fast.html generator end, total: ${total_sw.elapsed().milliseconds():6} ms') |
| 70 | } |
| 71 | |
| 72 | mut ccompiler_path := 'tcc' |
| 73 | if vdir.contains('/tmp/cirrus-ci-build') { |
| 74 | ccompiler_path = 'clang' |
| 75 | } |
| 76 | if args.contains('-clang') { |
| 77 | ccompiler_path = 'clang' |
| 78 | } |
| 79 | elog('fast_dir: ${fast_dir} | vdir: ${vdir} | compiler: ${ccompiler_path}') |
| 80 | |
| 81 | os.chdir(fast_dir)! |
| 82 | if !os.exists('${vdir}/v') && !os.is_dir('${vdir}/vlib') { |
| 83 | elog('fast.html generator needs to be located in `v/cmd/tools/fast`') |
| 84 | exit(1) |
| 85 | } |
| 86 | if !os.exists('table.html') { |
| 87 | os.create('table.html')! |
| 88 | } |
| 89 | |
| 90 | if !args.contains('-noupdate') { |
| 91 | elog('Fetching updates...') |
| 92 | ret := lsystem('${vdir}/v up') |
| 93 | if ret != 0 { |
| 94 | elog('failed to update V, exit_code: ${ret}') |
| 95 | // A failed `git pull --rebase` (e.g. on a shallow CI checkout) |
| 96 | // leaves the worktree with conflict markers in source files, |
| 97 | // which would break any subsequent V compilation step. Restore |
| 98 | // a clean state before returning. |
| 99 | lsystem('cd ${vdir} && git rebase --abort') |
| 100 | return |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // fetch the last commit's hash |
| 105 | commit := lexec('git rev-parse HEAD')[..8] |
| 106 | if os.exists('fast.vlang.io/index.html') { |
| 107 | uploaded_index := os.read_file('fast.vlang.io/index.html')! |
| 108 | if uploaded_index.contains('>${commit}<') { |
| 109 | elog('NOTE: commit ${commit} had been benchmarked already.') |
| 110 | if !args.contains('-force') { |
| 111 | elog('nothing more to do') |
| 112 | return |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | os.chdir(vdir)! |
| 118 | message := lexec('git log --pretty=format:"%s" -n1 ${commit}') |
| 119 | commit_date := lexec('git log -n1 --pretty="format:%at" ${commit}') |
| 120 | date := time.unix(commit_date.i64()) |
| 121 | |
| 122 | elog('Benchmarking commit ${commit} , with commit message: "${message}", commit_date: ${commit_date}, date: ${date}') |
| 123 | |
| 124 | // build an optimized V |
| 125 | if args.contains('-do-not-rebuild-vprod') { |
| 126 | if !os.exists('vprod') { |
| 127 | elog('Exiting, since if you use `-do-not-rebuild-vprod`, you should already have a `${vdir}/vprod` executable, but it is missing!') |
| 128 | return |
| 129 | } |
| 130 | } else { |
| 131 | elog(' Building vprod...') |
| 132 | if args.contains('-noprod') { |
| 133 | lexec('./v -o vprod cmd/v') // for faster debugging |
| 134 | } else { |
| 135 | lexec('./v -o vprod -prod -prealloc cmd/v') |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | if !args.contains('-do-not-rebuild-caches') { |
| 140 | elog('clearing caches...') |
| 141 | // cache vlib modules |
| 142 | lexec('${vdir}/v wipe-cache') |
| 143 | lexec('${vdir}/v -o vwarm_caches -cc ${ccompiler_path} cmd/v') |
| 144 | } |
| 145 | |
| 146 | // measure |
| 147 | diff1 := measure('${vdir}/vprod ${voptions} -o v.c cmd/v', 'v.c') |
| 148 | diff2 := measure('${vdir}/vprod ${voptions} -cc ${ccompiler_path} -o v2 cmd/v', 'v2') |
| 149 | diff3 := 0 // measure('${vdir}/vprod -native ${vdir}/cmd/tools/1mil.v', 'native 1mil') |
| 150 | diff4 := measure('${vdir}/vprod ${voptions} -cc ${ccompiler_path} examples/hello_world.v', |
| 151 | 'hello.v') |
| 152 | vc_size := os.file_size('v.c') / 1000 |
| 153 | scan, parse, check, cgen, vlines := measure_steps_minimal(vdir)! |
| 154 | |
| 155 | html_message := message.replace_each(['<', '<', '>', '>']) |
| 156 | |
| 157 | os.chdir(fast_dir)! |
| 158 | // place the new row on top |
| 159 | table := os.read_file('table.html')! |
| 160 | new_table := |
| 161 | ' <tr> |
| 162 | <td>${date.format()}</td> |
| 163 | <td><a target=_blank href="https://github.com/vlang/v/commit/${commit}">${commit}</a></td> |
| 164 | <td>${html_message}</td> |
| 165 | <td>${diff1}ms</td> |
| 166 | <td>${diff2}ms</td> |
| 167 | <td>${diff3}ms</td> |
| 168 | <td>${diff4}ms</td> |
| 169 | <td>${vc_size} KB</td> |
| 170 | <td>${parse}ms</td> |
| 171 | <td>${check}ms</td> |
| 172 | <td>${cgen}ms</td> |
| 173 | <td>${scan}ms</td> |
| 174 | <td>${vlines}</td> |
| 175 | <td>${int(f64(vlines) / f64(diff1) * 1000.0)}</td> |
| 176 | </tr>\n' + |
| 177 | table.trim_space() + '\n' |
| 178 | os.write_file('table.html', new_table)! |
| 179 | |
| 180 | // regenerate index.html |
| 181 | header := os.read_file('header.html')! |
| 182 | footer := os.read_file('footer.html')! |
| 183 | mut res := os.create('index.html')! |
| 184 | res.writeln(header)! |
| 185 | res.writeln(new_table)! |
| 186 | res.writeln(footer)! |
| 187 | res.close() |
| 188 | |
| 189 | // upload the result to github pages |
| 190 | if args.contains('-upload') { |
| 191 | $if freebsd { |
| 192 | // Note: tcc currently can not compile vpm on FreeBSD, due to its dependence on net.ssl and net.mbedtls, so force using clang instead: |
| 193 | elog('FreeBSD: compiling the VPM tool with clang...') |
| 194 | lexec('${vdir}/vprod -cc clang ${vdir}/cmd/tools/vpm/') |
| 195 | os.chdir('${fast_dir}/docs.vlang.io/docs_generator/')! |
| 196 | elog('FreeBSD: installing the dependencies for the docs generator...') |
| 197 | lexec('${vdir}/vprod install') |
| 198 | os.chdir(fast_dir)! |
| 199 | } |
| 200 | |
| 201 | os.chdir('${fast_dir}/fast.vlang.io/')! |
| 202 | elog('Uploading to fast.vlang.io/ ...') |
| 203 | lexec('git checkout gh-pages') |
| 204 | os.mv('../index.html', 'index.html')! |
| 205 | elog(' adding changes...') |
| 206 | lexec('git commit -am "update fast.vlang.io for commit ${commit}"') |
| 207 | elog(' pushing...') |
| 208 | lexec('git push origin gh-pages') |
| 209 | elog(' uploading to fast.vlang.io/ done') |
| 210 | os.chdir(fast_dir)! |
| 211 | |
| 212 | os.chdir('${fast_dir}/docs.vlang.io/')! |
| 213 | elog('Uploading to docs.vlang.io/ ...') |
| 214 | elog(' pulling upstream changes...') |
| 215 | if !lexec_check('git pull') { |
| 216 | elog(' skipping docs.vlang.io upload: `git pull` failed') |
| 217 | } else if !lexec_check('${vdir}/vprod run build.vsh') { |
| 218 | elog(' skipping docs.vlang.io upload: `vprod run build.vsh` failed') |
| 219 | } else { |
| 220 | elog(' adding new docs...') |
| 221 | lexec('git add .') |
| 222 | // `git diff --cached --quiet` exits 0 when there is nothing staged, |
| 223 | // 1 when there are staged changes, and >1 on real errors (e.g. a |
| 224 | // broken index). Treat >1 as an error rather than as "changes exist". |
| 225 | diff_cmd := 'git diff --cached --quiet' |
| 226 | elog(' lexec: ${diff_cmd}') |
| 227 | diff_res := os.execute(diff_cmd) |
| 228 | match diff_res.exit_code { |
| 229 | 0 { |
| 230 | elog(' nothing to commit; skipping push to docs.vlang.io/') |
| 231 | } |
| 232 | 1 { |
| 233 | elog(' commiting...') |
| 234 | lexec('git commit -m "update docs for commit ${commit}"') |
| 235 | elog(' pushing...') |
| 236 | if !lexec_check('git push') { |
| 237 | elog(' WARNING: `git push` to docs.vlang.io/ failed') |
| 238 | } else { |
| 239 | elog(' uploading to docs.vlang.io/ done') |
| 240 | } |
| 241 | } |
| 242 | else { |
| 243 | elog(' skipping docs.vlang.io upload: `${diff_cmd}` errored, exit_code: ${diff_res.exit_code}, output:\n${diff_res.output}') |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | os.chdir(fast_dir)! |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | // measure returns milliseconds |
| 252 | fn measure(cmd string, description string) int { |
| 253 | elog(' Measuring ${description}, warmups: ${warmup_samples}, samples: ${max_samples}, discard: ${discard_highest_samples}, with cmd: `${cmd}`') |
| 254 | for _ in 0 .. warmup_samples { |
| 255 | os.system(cmd) |
| 256 | } |
| 257 | mut runs := []int{} |
| 258 | for r in 0 .. max_samples { |
| 259 | sw := time.new_stopwatch() |
| 260 | os.execute(cmd) |
| 261 | sample := int(sw.elapsed().milliseconds()) |
| 262 | runs << sample |
| 263 | elog(' Sample ${r + 1:2}/${max_samples:2} ... ${sample} ms') |
| 264 | } |
| 265 | runs.sort() |
| 266 | elog(' runs before discarding: ${runs}, avg: ${f64(arrays.sum(runs) or { 0 }) / runs.len:5.2f}') |
| 267 | // Discard the highest times, since on AWS, they are caused by random load spikes, |
| 268 | // that are unpredictable, add noise and skew the statistics, without adding useful |
| 269 | // insights: |
| 270 | for _ in 0 .. discard_highest_samples { |
| 271 | runs.pop() |
| 272 | } |
| 273 | elog(' runs after discarding: ${runs}, avg: ${f64(arrays.sum(runs) or { 0 }) / runs.len:5.2f}') |
| 274 | return int(f64(arrays.sum(runs) or { 0 }) / runs.len) |
| 275 | } |
| 276 | |
| 277 | fn measure_steps_minimal(vdir string) !(int, int, int, int, int) { |
| 278 | elog('measure_steps_minimal ${vdir}, samples: ${max_samples}') |
| 279 | mut scans, mut parses, mut checks, mut cgens, mut vliness := []int{}, []int{}, []int{}, []int{}, []int{} |
| 280 | for i in 0 .. max_samples { |
| 281 | scan, parse, check, cgen, vlines, cmd := measure_steps_one_sample(vdir) |
| 282 | scans << scan |
| 283 | parses << parse |
| 284 | checks << check |
| 285 | cgens << cgen |
| 286 | vliness << vlines |
| 287 | 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}') |
| 288 | } |
| 289 | scan, parse, check, cgen, vlines := arrays.min(scans)!, arrays.min(parses)!, arrays.min(checks)!, arrays.min(cgens)!, arrays.min(vliness)! |
| 290 | elog('measure_steps_minimal => min scan: ${scan} ms, min parse: ${parse} ms, min check: ${check} ms, min cgen: ${cgen} ms, min vlines: ${vlines} ms') |
| 291 | return scan, parse, check, cgen, vlines |
| 292 | } |
| 293 | |
| 294 | fn measure_steps_one_sample(vdir string) (int, int, int, int, int, string) { |
| 295 | cmd := '${vdir}/vprod ${voptions} -o v.c cmd/v' |
| 296 | resp := os.execute(cmd) |
| 297 | |
| 298 | mut scan, mut parse, mut check, mut cgen, mut vlines := 0, 0, 0, 0, 0 |
| 299 | lines := resp.output.split_into_lines() |
| 300 | if lines.len == 3 { |
| 301 | parse = lines[0].before('.').int() |
| 302 | check = lines[1].before('.').int() |
| 303 | cgen = lines[2].before('.').int() |
| 304 | } else { |
| 305 | ms_lines := lines.map(it.split(' ms ')) |
| 306 | for line in ms_lines { |
| 307 | if line.len == 2 { |
| 308 | if line[1] == 'SCAN' { |
| 309 | scan = line[0].int() |
| 310 | } |
| 311 | if line[1] == 'PARSE' { |
| 312 | parse = line[0].int() |
| 313 | } |
| 314 | if line[1] == 'CHECK' { |
| 315 | check = line[0].int() |
| 316 | } |
| 317 | if line[1] == 'C GEN' { |
| 318 | cgen = line[0].int() |
| 319 | } |
| 320 | } else { |
| 321 | // fetch number of V lines |
| 322 | if line[0].contains('V') && line[0].contains('source') && line[0].contains('size') { |
| 323 | start := line[0].index(':') or { 0 } |
| 324 | end := line[0].index('lines,') or { 0 } |
| 325 | s := line[0][start + 1..end] |
| 326 | vlines = s.trim_space().int() |
| 327 | } |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | return scan, parse, check, cgen, vlines, cmd |
| 332 | } |
| 333 | |