Skip to content

Commit 7134f48

Browse files
authored
v2: self-host & codegen performance + fix v -profile (#27387)
1 parent 7a7cf78 commit 7134f48

16 files changed

Lines changed: 1459 additions & 240 deletions

File tree

vlib/v/gen/c/profile.v

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ fn (mut g Gen) profile_fn(fn_decl ast.FnDecl) {
1919
g.defer_profile_code = ''
2020
} else {
2121
measure_fn_name := if g.pref.os == .macos { 'time__vpc_now_darwin' } else { 'time__vpc_now' }
22-
fn_profile_counter_name := 'vpc_${cfn_name}'
22+
// Prefix the counter names with a unique per-function index. Without it the derived
23+
// names collide: a function `…__lower`'s call counter is `vpc_…__lower_calls` (u64),
24+
// which is identical to a function `…__lower_calls`'s time accumulator `vpc_…__lower_calls`
25+
// (double) — a "redefinition with a different type" C error. The same holds for the
26+
// `_only_current` suffix. The index makes every base name unambiguous.
27+
fn_profile_counter_name := 'vpc_${g.pcs.len}_${cfn_name}'
2328
fn_profile_counter_name_calls := '${fn_profile_counter_name}_calls'
2429
g.writeln('')
2530
should_restore_v__profile_enabled := g.pref.profile_fns.len > 0

vlib/v2/builder/builder.v

Lines changed: 422 additions & 10 deletions
Large diffs are not rendered by default.

vlib/v2/builder/cache_headers.v

Lines changed: 152 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ fn (b &Builder) imports_headers_stamp_path() string {
258258
return cache_path_join(b.core_cache_dir(), '${imports_cache_name}.vh.stamp')
259259
}
260260

261+
fn (b &Builder) v2compiler_headers_stamp_path() string {
262+
return cache_path_join(b.core_cache_dir(), '${v2compiler_cache_name}.vh.stamp')
263+
}
264+
261265
fn (b &Builder) imports_manifest_path() string {
262266
return cache_path_join(b.core_cache_dir(), '${imports_cache_name}.manifest')
263267
}
@@ -298,6 +302,14 @@ fn (b &Builder) cached_header_paths() []string {
298302
return paths
299303
}
300304

305+
fn (b &Builder) v2compiler_header_paths() []string {
306+
mut paths := []string{cap: v2compiler_cached_module_names.len}
307+
for module_name in v2compiler_cached_module_names {
308+
paths << b.core_header_path(module_name)
309+
}
310+
return paths
311+
}
312+
301313
fn cached_header_module_paths() []string {
302314
mut paths := core_cached_module_paths.clone()
303315
for module_path in veb_cached_module_paths {
@@ -848,19 +860,34 @@ fn (b &Builder) virtuals_header_stamp_for_modules(groups []CachedVirtualModule)
848860
return lines.join('\n')
849861
}
850862

863+
fn stamp_tracked_file_line(line string) (string, string, bool) {
864+
mut path_start := -1
865+
for prefix in ['entry:', 'source:', 'compiler:', 'emit:', 'compiler_exe:'] {
866+
if line.starts_with(prefix) {
867+
path_start = prefix.len
868+
break
869+
}
870+
}
871+
if path_start < 0 {
872+
if line.starts_with('/') || line.starts_with('./') || line.starts_with('../') {
873+
path_start = 0
874+
} else {
875+
return '', '', false
876+
}
877+
}
878+
colon_idx := line.last_index(':') or { return '', '', false }
879+
if colon_idx <= path_start {
880+
return '', '', false
881+
}
882+
return line[path_start..colon_idx], line[colon_idx + 1..], true
883+
}
884+
851885
fn stamp_file_lines_are_fresh(stamp string) bool {
852886
for line in stamp.split_into_lines() {
853-
if !(line.starts_with('entry:') || line.starts_with('source:')
854-
|| line.starts_with('compiler:')) {
887+
file, expected_mtime, tracked := stamp_tracked_file_line(line)
888+
if !tracked {
855889
continue
856890
}
857-
colon_idx := line.last_index(':') or { return false }
858-
path_idx := line.index(':') or { return false }
859-
if colon_idx <= path_idx {
860-
return false
861-
}
862-
file := line[path_idx + 1..colon_idx]
863-
expected_mtime := line[colon_idx + 1..]
864891
if !os.exists(file) {
865892
return false
866893
}
@@ -987,6 +1014,37 @@ fn (b &Builder) can_use_cached_import_headers_for_parse() bool {
9871014
return false
9881015
}
9891016

1017+
fn (b &Builder) can_use_cached_v2compiler_headers_for_parse() bool {
1018+
if !b.is_cmd_v2_self_build() || b.pref.no_cache || b.pref.skip_builtin {
1019+
return false
1020+
}
1021+
if !b.ensure_core_cache_dir() {
1022+
return false
1023+
}
1024+
if !b.can_use_cached_module_bundle_for_parse(v2compiler_cache_name, false) {
1025+
return false
1026+
}
1027+
expected_stamp := b.header_stamp_for_modules(v2compiler_cached_module_paths)
1028+
current_stamp := os.read_file(b.v2compiler_headers_stamp_path()) or { return false }
1029+
if current_stamp != expected_stamp || !stamp_file_lines_are_fresh(current_stamp) {
1030+
return false
1031+
}
1032+
for header_path in b.v2compiler_header_paths() {
1033+
if !os.exists(header_path) || os.file_size(header_path) == 0 {
1034+
return false
1035+
}
1036+
}
1037+
return true
1038+
}
1039+
1040+
// v2compiler_headers_consumed_for_parse reports whether the generated v2compiler .vh module
1041+
// headers can ever be read back by the parser. v2compiler header parse-reuse is currently
1042+
// disabled (the generated headers are not yet complete/safe), so generating them is dead work on
1043+
// a cold self-build. Set V2_V2COMPILER_VH=1 to re-enable generation while iterating on that path.
1044+
fn (b &Builder) v2compiler_headers_consumed_for_parse() bool {
1045+
return os.getenv('V2_V2COMPILER_VH') != ''
1046+
}
1047+
9901048
fn (b &Builder) can_use_cached_virtual_headers_for_parse(groups []CachedVirtualModule) bool {
9911049
if groups.len == 0 || b.pref.no_cache || b.pref.skip_builtin {
9921050
return false
@@ -1071,6 +1129,21 @@ fn (b &Builder) cached_import_parse_path(module_path string) ?string {
10711129
}
10721130
return header_path
10731131
}
1132+
if b.can_use_cached_v2compiler_headers_for_parse() {
1133+
for i, cached_module_path in v2compiler_cached_module_paths {
1134+
if module_path != cached_module_path {
1135+
continue
1136+
}
1137+
if i >= v2compiler_cached_module_names.len {
1138+
return none
1139+
}
1140+
header_path := b.core_header_path(v2compiler_cached_module_names[i])
1141+
if !os.exists(header_path) || os.file_size(header_path) == 0 {
1142+
return none
1143+
}
1144+
return header_path
1145+
}
1146+
}
10741147
return none
10751148
}
10761149

@@ -1253,6 +1326,69 @@ fn (mut b Builder) ensure_import_module_headers(module_names []string) {
12531326
os.write_file(b.imports_headers_stamp_path(), expected_stamp) or {}
12541327
}
12551328

1329+
fn (mut b Builder) ensure_v2compiler_module_headers() {
1330+
if !b.is_cmd_v2_self_build() || !b.ensure_core_cache_dir() {
1331+
return
1332+
}
1333+
expected_stamp := b.header_stamp_for_modules(v2compiler_cached_module_paths)
1334+
mut has_headers := true
1335+
for header_path in b.v2compiler_header_paths() {
1336+
if !os.exists(header_path) || os.file_size(header_path) == 0 {
1337+
has_headers = false
1338+
break
1339+
}
1340+
}
1341+
if has_headers {
1342+
current_stamp := os.read_file(b.v2compiler_headers_stamp_path()) or { '' }
1343+
if current_stamp == expected_stamp {
1344+
return
1345+
}
1346+
}
1347+
header_source_files := b.v2compiler_header_source_files()
1348+
source_fn_returns := b.source_fn_return_types(v2compiler_cached_module_paths)
1349+
for module_name in v2compiler_cached_module_names {
1350+
header_ast := b.build_module_header_ast(header_source_files, module_name) or { return }
1351+
mut gen := v.new_gen(b.pref)
1352+
gen.gen(header_ast)
1353+
mut header_source := sanitize_header_source(gen.output_string(), source_fn_returns)
1354+
source_struct_fields := b.source_struct_field_types_for_module(module_name)
1355+
header_source = repair_missing_struct_field_types(header_source, source_struct_fields)
1356+
if header_source.len == 0 {
1357+
for cleanup_name in v2compiler_cached_module_names {
1358+
os.rm(b.core_header_path(cleanup_name)) or {}
1359+
}
1360+
os.rm(b.v2compiler_headers_stamp_path()) or {}
1361+
return
1362+
}
1363+
if !header_source.ends_with('\n') {
1364+
header_source += '\n'
1365+
}
1366+
os.write_file(b.core_header_path(module_name), header_source) or { return }
1367+
}
1368+
os.write_file(b.v2compiler_headers_stamp_path(), expected_stamp) or {}
1369+
}
1370+
1371+
fn (mut b Builder) v2compiler_header_source_files() []ast.File {
1372+
mut needed := map[string]bool{}
1373+
for module_name in v2compiler_cached_module_names {
1374+
needed[module_name] = true
1375+
}
1376+
mut found := map[string]bool{}
1377+
for file in b.files {
1378+
if file.name == '' || file.name.ends_with('.vh') {
1379+
continue
1380+
}
1381+
module_name := ast_file_module_name(file)
1382+
if module_name in needed {
1383+
found[module_name] = true
1384+
}
1385+
}
1386+
if found.len == needed.len {
1387+
return b.files
1388+
}
1389+
return b.parse_module_source_files_for_headers(v2compiler_cached_module_paths)
1390+
}
1391+
12561392
fn (mut b Builder) ensure_virtual_module_headers(groups []CachedVirtualModule) {
12571393
if groups.len == 0 || !b.ensure_core_cache_dir() {
12581394
return
@@ -2018,9 +2154,15 @@ fn (b &Builder) header_const_type_expr(module_name string, field ast.FieldInit)
20182154

20192155
fn header_const_value_is_safe(expr ast.Expr) bool {
20202156
return match expr {
2021-
ast.BasicLiteral, ast.StringLiteral, ast.StringInterLiteral, ast.Ident {
2157+
ast.BasicLiteral, ast.Ident {
20222158
true
20232159
}
2160+
ast.StringLiteral {
2161+
!expr.value.contains('\n') && !expr.value.contains('\r')
2162+
}
2163+
ast.StringInterLiteral {
2164+
expr.values.all(!it.contains('\n') && !it.contains('\r'))
2165+
}
20242166
ast.SelectorExpr {
20252167
header_const_selector_lhs_is_safe(expr.lhs)
20262168
}

vlib/v2/builder/fast_relink_test.v

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module builder
2+
3+
import os
4+
import v2.pref
5+
6+
// Concern 1: the pre-parse self-host fast relink must never fire for a request
7+
// that gen_cleanc() treats as "generation only" — otherwise a warm-cache
8+
// `-o foo.c cmd/v2/v2.v` links an executable into foo.c instead of writing C.
9+
fn test_fast_relink_skips_generation_only_outputs() {
10+
// A `.c` output is generation-only regardless of how it would be built.
11+
b_c := new_builder(&pref.Preferences{ backend: .cleanc })
12+
assert b_c.fast_relink_output_is_generation_only('/tmp/v2.c')
13+
assert b_c.fast_relink_output_is_generation_only('out.c')
14+
15+
// A shared library is generation-only.
16+
b_shared := new_builder(&pref.Preferences{ backend: .cleanc, is_shared_lib: true })
17+
assert b_shared.fast_relink_output_is_generation_only('/tmp/lib')
18+
19+
// A normal local executable build is NOT generation-only — the fast relink
20+
// is allowed to proceed (subject to its cache/freshness checks).
21+
b_exe := new_builder(&pref.Preferences{ backend: .cleanc })
22+
assert b_exe.can_compile_cleanc_locally() // sanity: host target compiles locally
23+
assert !b_exe.fast_relink_output_is_generation_only('/tmp/v3')
24+
}
25+
26+
// Concern 2: the fast relink trusts the cc/cc_flags recorded in main.stamp, so a
27+
// pre-parse fingerprint of the flag inputs that do NOT need parsing must change
28+
// when the compiler / prod-shared mode / env CFLAGS change, and stay stable
29+
// otherwise. A changed fingerprint is what invalidates a would-be stale relink.
30+
fn test_preparse_flag_fingerprint_tracks_flag_settings() {
31+
base := new_builder(&pref.Preferences{ backend: .cleanc })
32+
fp0 := base.preparse_flag_fingerprint()
33+
34+
// Stable for identical settings.
35+
assert new_builder(&pref.Preferences{ backend: .cleanc }).preparse_flag_fingerprint() == fp0
36+
37+
// A different C compiler changes it.
38+
b_cc := new_builder(&pref.Preferences{ backend: .cleanc, ccompiler: 'some-other-cc' })
39+
assert b_cc.preparse_flag_fingerprint() != fp0
40+
41+
// -prod changes it (different optimization flags).
42+
b_prod := new_builder(&pref.Preferences{ backend: .cleanc, is_prod: true })
43+
assert b_prod.preparse_flag_fingerprint() != fp0
44+
45+
// -shared changes it (no -flto, different link mode).
46+
b_shared := new_builder(&pref.Preferences{ backend: .cleanc, is_shared_lib: true })
47+
assert b_shared.preparse_flag_fingerprint() != fp0
48+
49+
// Env CFLAGS (V2CFLAGS) change it.
50+
prev := os.getenv('V2CFLAGS')
51+
os.setenv('V2CFLAGS', '-DV2_FAST_RELINK_TEST', true)
52+
fp_env := new_builder(&pref.Preferences{ backend: .cleanc }).preparse_flag_fingerprint()
53+
if prev == '' {
54+
os.unsetenv('V2CFLAGS')
55+
} else {
56+
os.setenv('V2CFLAGS', prev, true)
57+
}
58+
assert fp_env != fp0
59+
}

0 commit comments

Comments
 (0)