v2 / cmd / tools / fast / fast.v
287 lines · 256 sloc · 8.82 KB · 2332ecff4811b8c97dfda8e825170e9397962519
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 return os.execute(cmd).output.trim_right('\r\n')
41}
42
43fn 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
207fn 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
232fn 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
249fn 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