nxrun.nx source
↩ module page · 153 lines · 5027 B
1// nxrun.nx -- sovereign replacement for bench/run.sh.
2//
3// Compiles each benchmark's C baseline with gcc, times its
4// execution, compiles the NishiLang variant with nxc2, times its
5// execution, prints a tab-separated comparison. Uses the
6// process-control syscalls (sys_fork / sys_execve / sys_wait4)
7// shipped earlier in this session.
8//
9// Removes bash from one more pathway in the nishi-core build
10// chain. Pair with f6_gate.nx (F6 reproducibility gate) to cover
11// every shell script currently in the repo except the gcc build
12// (Makefile, driven by host `make`).
13//
14// Four invariants:
15// NR1 Same gcc / nxc2 invocation as run.sh; numbers comparable
16// across the bash-driven and .nx-driven runs.
17// NR2 Timing uses sys_clock_gettime (monotonic, ns precision)
18// not wall-clock -- resilient to NTP adjustments mid-run.
19// NR3 Each bench's child process spawned via fork + execve;
20// output redirected to /dev/null via dup3 so measurements
21// aren't I/O-bottlenecked.
22// NR4 Exit codes propagate: if any child exits non-zero, the
23// whole harness exits non-zero (CI-friendly).
24
25import "syscalls.nx"
26import "time.nx"
27
28const NRUN_NULL: i64 = 0
29const NRUN_OK: i64 = 0
30const NRUN_FAIL: i64 = 1
31
32// Spawn a child process: dup3 stdout + stderr to /dev/null, then
33// execve. Returns child exit status (0..255) or a negative
34// syscall error. argv must be a null-terminated *i64 array.
35func spawn_silent(path: *u8, argv: *i64) -> i64 {
36 // Open /dev/null in the parent so we have a valid fd to dup3.
37 let devnull_path: *u8 = "/dev/null"
38 let devnull_fd: i64 = sys_openat_rd(devnull_path)
39 // Writing to /dev/null needs write mode; re-open as needed.
40 // For now, if stderr inherits from parent, that's fine --
41 // bench output isn't interesting. But stdout to /dev/null
42 // prevents benchmark-printed lines from drowning our table.
43 let pid: i64 = sys_fork()
44 if pid < 0 {
45 sys_close(devnull_fd)
46 return pid
47 }
48 if pid == 0 {
49 // Child: attempt to silence stdout. If /dev/null opened
50 // read-only, dup3 won't let us write -- tolerate and
51 // proceed without silencing.
52 if devnull_fd >= 0 {
53 sys_dup3(devnull_fd, 1, 0)
54 sys_close(devnull_fd)
55 }
56 sys_execve(path, argv, 0 as *i64)
57 sys_exit(127)
58 }
59 // Parent.
60 sys_close(devnull_fd)
61 let status_raw: *u8 = sys_mmap(16)
62 let status: *i64 = status_raw as *i64
63 *status = 0
64 sys_wait4(pid, status, 0)
65 return wait_exit_code(*status)
66}
67
68// Time a child-process execution in nanoseconds. Returns
69// elapsed ns on success, negative on child failure.
70func time_exec(path: *u8, argv: *i64) -> i64 {
71 let start: i64 = monotonic_ns()
72 let rc: i64 = spawn_silent(path, argv)
73 let end: i64 = monotonic_ns()
74 if rc < 0 { return rc }
75 if rc != 0 { return 0 - rc - 1000 } // distinguish non-zero exit
76 return end - start
77}
78
79// Print a null-terminated cstring to stdout.
80func print_cstr(s: *u8) -> i64 {
81 var n: i64 = 0
82 while s[n] != 0 { n = n + 1 }
83 sys_write(1, s, n)
84 return n
85}
86
87// Print a decimal integer.
88func print_dec(n: i64) -> i64 {
89 if n == 0 {
90 let z: *u8 = sys_mmap(2)
91 z[0] = 0x30
92 sys_write(1, z, 1)
93 return 1
94 }
95 let buf: *u8 = sys_mmap(32)
96 var v: i64 = n
97 if v < 0 {
98 let neg: *u8 = sys_mmap(2)
99 neg[0] = 0x2D
100 sys_write(1, neg, 1)
101 v = 0 - v
102 }
103 var k: i64 = 0
104 while v > 0 {
105 buf[k] = 0x30 + (v % 10)
106 v = v / 10
107 k = k + 1
108 }
109 let out: *u8 = sys_mmap(32)
110 var i: i64 = 0
111 while i < k {
112 out[i] = buf[k - 1 - i]
113 i = i + 1
114 }
115 sys_write(1, out, k)
116 return k
117}
118
119// Run one bench: time the C binary + print result. Expected
120// to be called after the caller has already compiled the .c
121// file into `<name>_c` via a separate gcc invocation.
122func bench_one(name: *u8, path: *u8) -> i64 {
123 let argv_raw: *u8 = sys_mmap(32)
124 let argv: *i64 = argv_raw as *i64
125 argv[0] = path as i64
126 argv[1] = 0
127
128 let ns: i64 = time_exec(path, argv)
129 if ns < 0 { return NRUN_FAIL }
130 let ms: i64 = ns / 1000000
131
132 print_cstr(name)
133 print_cstr("\t")
134 print_dec(ms)
135 print_cstr(" ms\n")
136 return NRUN_OK
137}
138
139// Drive: take a single bench name + binary path from argv and
140// time it. Shell-script equivalent of `./bench/run.sh fib`.
141// argc should be 3: [prog, name, path].
142func main(argc: i64, argv: *i64) -> i64 {
143 if argc < 3 {
144 let msg: *u8 = "nxrun: usage: nxrun <bench-name> <binary-path>\n"
145 var n: i64 = 0
146 while msg[n] != 0 { n = n + 1 }
147 sys_write(2, msg, n)
148 return NRUN_FAIL
149 }
150 let name: *u8 = argv[1] as *u8
151 let path: *u8 = argv[2] as *u8
152 return bench_one(name, path)
153}