v / cmd / tools / fast / fast.v
332 lines · 300 sloc · 10.6 KB · 01cb8c2b6c8b36eb0cab463ef1b57f6e29dc6041
Raw
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.
4import os
5import time
6import arrays
7import log
8
9const args = arguments()
10const warmup_samples = 2
11
12const max_samples = 20
13
14const discard_highest_samples = 16
15
16const voptions = ' -skip-unused -show-timings -stats '
17
18const fast_dir = os.real_path(os.dir(@FILE))
19
20const fast_log_path = os.real_path(os.join_path(fast_dir, 'fast.log'))
21
22const vdir = os.real_path(os.dir(os.dir(os.dir(fast_dir))))
23
24fn 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
33fn lsystem(cmd string) int {
34 elog('lsystem: ${cmd}')
35 return os.system(cmd)
36}
37
38fn 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.
51fn 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
61fn 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
252fn 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
277fn 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
294fn 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