// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module builder import runtime import time import v2.gen.cleanc const max_cleanc_pass5_jobs = 16 $if !windows { struct GenCleancChunkArgs { worker voidptr // &cleanc.Gen — pre-cloned worker work_items_ptr voidptr // &[]cleanc.Pass5WorkItem — work items to process } @[typedef] struct C.pthread_t {} fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int fn C.pthread_join(thread C.pthread_t, retval voidptr) int fn C.pthread_attr_init(attr voidptr) int fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int fn C.pthread_attr_destroy(attr voidptr) int fn gen_cleanc_chunk_thread(arg voidptr) voidptr { a := unsafe { &GenCleancChunkArgs(arg) } mut w := unsafe { &cleanc.Gen(a.worker) } items := unsafe { &[]cleanc.Pass5WorkItem(a.work_items_ptr) } w.gen_pass5_work_items(*items) return unsafe { nil } } } fn print_cleanc_parallel_step_time(stats_enabled bool, step string, elapsed time.Duration) { if !stats_enabled { return } println(' - C Gen/full ${step}: ${elapsed.milliseconds()}ms') } fn mark_cleanc_parallel_step(stats_enabled bool, mut sw time.StopWatch, stage_start time.Duration, step string) time.Duration { if !stats_enabled { return stage_start } now := sw.elapsed() print_cleanc_parallel_step_time(true, step, time.Duration(now - stage_start)) return now } fn (mut b Builder) gen_cleanc_parallel(mut gen cleanc.Gen) { stats_enabled := b.pref != unsafe { nil } && b.pref.stats mut stats_sw := time.new_stopwatch() mut stage_start := stats_sw.elapsed() emit_indices := gen.gen_pass5_pre() stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 pre') // Split large files into sub-file work items so no single huge file // (e.g. ssa/builder.v) pins the whole parallel phase. work_items := gen.build_pass5_work_items(emit_indices) n_items := work_items.len n_runtime_jobs := runtime.nr_jobs() n_jobs := cleanc_parallel_pass5_job_count(n_runtime_jobs, n_items) $if windows { gen.gen_pass5_files(emit_indices) stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 files') gen.gen_pass5_post() _ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post') return } $else { if n_items <= 1 || n_jobs <= 1 { // Fallback to sequential gen.gen_pass5_files(emit_indices) stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 files') gen.gen_pass5_post() _ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post') return } mut thread_ids := []C.pthread_t{len: n_jobs} mut args := []GenCleancChunkArgs{cap: n_jobs} mut workers := []voidptr{cap: n_jobs} // chunk_items: work items assigned to each worker. // chunk_indices: unique file indices each worker touches (for // new_pass5_worker's owned-file / cross-worker dedup bookkeeping). mut chunk_items := [][]cleanc.Pass5WorkItem{cap: n_jobs} mut chunk_indices := [][]int{cap: n_jobs} mut chunk_file_seen := []map[int]bool{cap: n_jobs} mut chunk_costs := []int{cap: n_jobs} mut chunk_idx := n_jobs if chunk_idx > n_items { chunk_idx = n_items } for ci := 0; ci < chunk_idx; ci++ { chunk_items << []cleanc.Pass5WorkItem{} chunk_indices << []int{} chunk_file_seen << map[int]bool{} chunk_costs << 0 } mut sorted_items := work_items.clone() for i := 1; i < sorted_items.len; i++ { mut j := i for j > 0 && sorted_items[j - 1].cost < sorted_items[j].cost { sorted_items[j - 1], sorted_items[j] = sorted_items[j], sorted_items[j - 1] j-- } } for item in sorted_items { mut target := 0 for ci := 1; ci < chunk_idx; ci++ { if chunk_costs[ci] < chunk_costs[target] { target = ci } } chunk_items[target] << item // Only the worker that emits a file's globals (a whole-file item, or the // first slice of a split file — both carry emit_globals) takes file-level // dedup ownership. A split file's later slices deliberately do NOT, so the // file's lazily/transitively emitted fns stay blocked in those workers and // only the owning worker can emit them; the explicit slice still emits via // the owner-scoped bypass in gen_file_range. This closes the duplicate / // reordered-emission hole that file-level ownership left open for files // split across workers. if item.emit_globals && item.file_idx !in chunk_file_seen[target] { chunk_file_seen[target][item.file_idx] = true chunk_indices[target] << item.file_idx } chunk_costs[target] += item.cost } stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 chunk split') for ci := 0; ci < chunk_idx; ci++ { w := gen.new_pass5_worker(chunk_indices[ci], ci) workers << voidptr(w) } stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 worker setup') // Set up args after all chunk_items are stable for ci := 0; ci < chunk_idx; ci++ { args << GenCleancChunkArgs{ worker: workers[ci] work_items_ptr: unsafe { voidptr(&chunk_items[ci]) } } } attr_buf := [64]u8{} attr := unsafe { voidptr(&attr_buf[0]) } C.pthread_attr_init(attr) C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024) for ci := 0; ci < chunk_idx; ci++ { C.pthread_create(unsafe { &thread_ids[ci] }, attr, gen_cleanc_chunk_thread, unsafe { voidptr(&args[ci]) }) } C.pthread_attr_destroy(attr) // Wait for all workers for ci := 0; ci < chunk_idx; ci++ { C.pthread_join(thread_ids[ci], unsafe { nil }) } stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 worker run') // Merge worker results in order for ci := 0; ci < chunk_idx; ci++ { w := unsafe { &cleanc.Gen(workers[ci]) } gen.merge_pass5_worker(w) } stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 merge') gen.print_pass5_file_times(8) gen.gen_pass5_post() _ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post') } } fn cleanc_parallel_pass5_job_count(n_runtime_jobs int, n_files int) int { if n_runtime_jobs <= 0 || n_files <= 0 { return 0 } mut n_jobs := n_runtime_jobs if n_jobs > max_cleanc_pass5_jobs { n_jobs = max_cleanc_pass5_jobs } if n_jobs > n_files { n_jobs = n_files } return n_jobs }