@@ -6,6 +6,7 @@ mod config;
66mod execute;
77mod fixtures;
88
9+ use anyhow:: { bail, Context } ;
910use clap:: Parser ;
1011use config:: { ListOptions , ProcessArgs , RunOptions } ;
1112use phd_tests:: phd_testcase:: { Framework , FrameworkParameters } ;
@@ -46,55 +47,114 @@ async fn main() -> anyhow::Result<()> {
4647 Ok ( ( ) )
4748}
4849
50+ fn guess_max_reasonable_parallelism (
51+ default_guest_cpus : u8 ,
52+ default_guest_memory_mib : u64 ,
53+ ) -> anyhow:: Result < u16 > {
54+ // Assume no test starts more than 3 VMs. This is a really conservative
55+ // guess to make sure we don't cause tests to fail simply because we ran
56+ // too many at once.
57+ const MAX_VMS_GUESS : u64 = 3 ;
58+ let cpus_per_runner = default_guest_cpus as u64 * MAX_VMS_GUESS ;
59+ let memory_mib_per_runner =
60+ ( default_guest_memory_mib * MAX_VMS_GUESS ) as usize ;
61+
62+ /// Miniscule wrapper for `sysconf(3C)` calls that checks errors.
63+ fn sysconf ( cfg : i32 ) -> std:: io:: Result < i64 > {
64+ // Safety: sysconf is an FFI call but we don't change any system
65+ // state, it won't cause unwinding, etc.
66+ let res = unsafe { libc:: sysconf ( cfg) } ;
67+ // For the handful of variables that can be queried, the variable is
68+ // defined and won't error. Technically if the variable is
69+ // unsupported, `-1` is returned without changing `errno`. In such
70+ // cases, returning errno might be misleading!
71+ //
72+ // Instead of trying to disambiguate this, and in the knowledge
73+ // these calls as we make them should never fail, just fall back to
74+ // a more general always-factually-correct.
75+ if res == -1 {
76+ return Err ( std:: io:: Error :: new (
77+ std:: io:: ErrorKind :: Other ,
78+ format ! ( "could not get sysconf({})" , cfg) ,
79+ ) ) ;
80+ }
81+ Ok ( res)
82+ }
83+
84+ let online_cpus = sysconf ( libc:: _SC_NPROCESSORS_ONLN)
85+ . expect ( "can get number of online processors" )
86+ as u64 ;
87+ // We're assuming that the system running tests is relatively idle other
88+ // than the test runner itself. Overprovisioning CPUs will make everyone
89+ // sad but should not fail tests, at least...
90+ let lim_by_cpus = online_cpus / cpus_per_runner;
91+
92+ let ctl = bhyve_api:: VmmCtlFd :: open ( ) ?;
93+ let reservoir =
94+ ctl. reservoir_query ( ) . context ( "failed to query reservoir" ) ?;
95+ let mut vmm_mem_limit = reservoir. vrq_free_sz ;
96+
97+ // The reservoir will be 0MiB by default if the system has not been
98+ // configured with a particular size.
99+ if reservoir. vrq_alloc_sz == 0 {
100+ // If the reservoir is not configured, we'll try to make do with
101+ // system memory and implore someone to earmark memory for test VMs in
102+ // the future.
103+ let page_size: usize = sysconf ( libc:: _SC_PAGESIZE)
104+ . expect ( "can get page size" )
105+ . try_into ( )
106+ . expect ( "page size is reasonable" ) ;
107+ let total_pages: usize = sysconf ( libc:: _SC_PHYS_PAGES)
108+ . expect ( "can get physical pages in the system" )
109+ . try_into ( )
110+ . expect ( "physical page count is reasonable" ) ;
111+
112+ let installed_mb = page_size * total_pages;
113+ // /!\ Arbitrary choice warning /!\
114+ //
115+ // It would be a little rude to spawn so many VMs that we cause the
116+ // system running tests to empty the whole ARC and swap. If there's no
117+ // reservior, though, we're gonna use *some* amount of memory that isn't
118+ // explicitly earmarked for bhyve, though. 1/4th is just a "feels ok"
119+ // fraction.
120+ vmm_mem_limit = installed_mb / 4 ;
121+
122+ eprintln ! (
123+ "phd-runner sees the VMM reservior is unconfigured, and will use \
124+ up to 25% of system memory ({}MiB) for test VMs. Please consider \
125+ using `cargo run --bin rsrvrctl set <size MiB>` to set aside \
126+ memory for test VMs.",
127+ vmm_mem_limit
128+ ) ;
129+ }
130+
131+ let lim_by_mem = vmm_mem_limit / memory_mib_per_runner;
132+
133+ Ok ( std:: cmp:: min ( lim_by_cpus as u16 , lim_by_mem as u16 ) )
134+ }
135+
49136async fn run_tests ( run_opts : & RunOptions ) -> anyhow:: Result < ExecutionStats > {
50- let parallelism = run_opts. parallelism . unwrap_or_else ( || {
51- // Assume no test starts more than 4 VMs. This is a really conservative
52- // guess to make sure we don't cause tests to fail simply because we ran
53- // too many at once.
54- let cpus_per_runner = run_opts. default_guest_cpus as u64 * 4 ;
55- let memory_mib_per_runner = run_opts. default_guest_memory_mib * 4 ;
56-
57- // I assume there's a smarter way to do this in illumos. sorry!!
58- let lgrpinfo = std:: process:: Command :: new ( "/usr/bin/lgrpinfo" )
59- . args ( [ "-mc" , "-u" , "m" ] )
60- . output ( )
61- . expect ( "can run lgrpinfo" ) ;
62- assert_eq ! ( lgrpinfo. status. code( ) , Some ( 0 ) ) ;
63- let lgrpinfo_output_str =
64- String :: from_utf8 ( lgrpinfo. stdout ) . expect ( "utf8 output" ) ;
65- let lines: Vec < & str > = lgrpinfo_output_str. split ( "\n " ) . collect ( ) ;
66- let cpuline = lines[ 1 ] ;
67- let cpu_range = cpuline
68- . strip_prefix ( "\t CPUS: " )
69- . expect ( "`CPUs` line starts as expected" ) ;
70- let mut cpu_range_parts = cpu_range. split ( "-" ) ;
71- let cpu_low = cpu_range_parts. next ( ) . expect ( "range has a low" ) ;
72- let cpu_high = cpu_range_parts. next ( ) . expect ( "range has a high" ) ;
73- let ncpus = cpu_high. parse :: < u64 > ( ) . expect ( "can parse cpu_low" )
74- - cpu_low. parse :: < u64 > ( ) . expect ( "can parse cpu_high" ) ;
75-
76- let lim_by_cpus = ncpus / cpus_per_runner;
77-
78- let memoryline = lines[ 2 ] ;
79- let memory = memoryline
80- . strip_prefix ( "\t Memory: " )
81- . expect ( "`Memory` line starts as expected" ) ;
82- let installed = memory
83- . split ( "," )
84- . next ( )
85- . expect ( "memory line is comma-separated elements" ) ;
86- let installed_mb = installed
87- . strip_prefix ( "installed " )
88- . expect ( "memory line starts with installed" )
89- . strip_suffix ( "M" )
90- . expect ( "memory line ends with M" )
91- . parse :: < u64 > ( )
92- . expect ( "can parse memory MB" ) ;
93-
94- let lim_by_mem = installed_mb / memory_mib_per_runner;
95-
96- std:: cmp:: min ( lim_by_cpus as u16 , lim_by_mem as u16 )
97- } ) ;
137+ let parallelism = if let Some ( parallelism) = run_opts. parallelism {
138+ if parallelism == 0 {
139+ bail ! ( "Parallelism of 0 was requested; cannot run tests under these conditions!" ) ;
140+ }
141+ parallelism
142+ } else {
143+ let res = guess_max_reasonable_parallelism (
144+ run_opts. default_guest_cpus ,
145+ run_opts. default_guest_memory_mib ,
146+ ) ?;
147+ if res == 0 {
148+ bail ! (
149+ "Inferred a parallelism of 0; this is probably because there \
150+ is not much available memory for test VMs? Consider checking \
151+ reservoir configuration."
152+ ) ;
153+ }
154+ res
155+ } ;
156+
157+ info ! ( "running tests with max parallelism of {}" , parallelism) ;
98158
99159 // /!\ Arbitrary choice warning /!\
100160 //
0 commit comments