1use std::sync::{
14 atomic::{AtomicBool, Ordering},
15 OnceLock,
16};
17
18use crate::RuntimeSelection;
19
20static REPORT: OnceLock<Result<EnvironmentInitReport, String>> = OnceLock::new();
21static PRIVATE_POOLS: AtomicBool = AtomicBool::new(false);
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct GlobalPool {
26 pub intra_threads: usize,
27 pub inter_threads: usize,
28 pub spin: bool,
31}
32
33impl GlobalPool {
34 pub fn from_budget() -> Self {
37 Self {
38 intra_threads: cpu_thread_budget(physical_cores().unwrap_or(1))
39 .min(std::thread::available_parallelism().map_or(1, usize::from)),
40 inter_threads: 1,
41 spin: false,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Default)]
48pub struct EnvironmentOptions {
49 pub global_pool: Option<GlobalPool>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct EnvironmentInitOptions {
56 pub global_pool: Option<GlobalPool>,
58 pub defer_initialization: bool,
61 pub telemetry: Option<bool>,
64 pub tolerate_already_initialized: bool,
67 pub verbatim_loader_errors: bool,
69}
70
71impl Default for EnvironmentInitOptions {
72 fn default() -> Self {
73 Self {
74 global_pool: None,
75 defer_initialization: false,
76 telemetry: Some(false),
77 tolerate_already_initialized: false,
78 verbatim_loader_errors: false,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum EnvironmentStatus {
85 Initialized,
86 Deferred,
88 AlreadyInitialized,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct EnvironmentReport {
94 pub runtime_path: std::path::PathBuf,
95 pub runtime_info: String,
97 pub shared_pool_requested: bool,
98 pub shared_pool_active: bool,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct EnvironmentInitReport {
104 pub runtime_path: std::path::PathBuf,
107 pub runtime_info: String,
109 pub shared_pool_requested: bool,
110 pub shared_pool_active: bool,
111 pub status: EnvironmentStatus,
112}
113
114pub fn cpu_thread_budget(physical: usize) -> usize {
116 if physical <= 4 {
117 physical.saturating_sub(1).max(1)
118 } else {
119 4
120 }
121}
122
123#[cfg(target_os = "windows")]
124fn physical_cores() -> Option<usize> {
125 use windows_sys::Win32::System::SystemInformation::{
126 GetLogicalProcessorInformationEx, RelationProcessorCore,
127 };
128 let mut bytes = 0;
129 unsafe {
130 GetLogicalProcessorInformationEx(RelationProcessorCore, std::ptr::null_mut(), &mut bytes);
131 }
132 if bytes < 8 {
133 return None;
134 }
135 let mut storage = vec![0_u64; (bytes as usize).div_ceil(8)];
137 if unsafe {
138 GetLogicalProcessorInformationEx(
139 RelationProcessorCore,
140 storage.as_mut_ptr().cast(),
141 &mut bytes,
142 )
143 } == 0
144 {
145 return None;
146 }
147 let data = unsafe { std::slice::from_raw_parts(storage.as_ptr().cast::<u8>(), bytes as usize) };
148 count_core_records(data)
149}
150
151#[cfg(not(target_os = "windows"))]
152fn physical_cores() -> Option<usize> {
153 std::thread::available_parallelism().ok().map(usize::from)
154}
155
156pub fn count_core_records(data: &[u8]) -> Option<usize> {
160 let mut offset = 0;
161 let mut cores = 0;
162 while offset < data.len() {
163 let header = data.get(offset..offset + 8)?;
164 let relation = u32::from_ne_bytes(header[..4].try_into().ok()?);
165 let size = u32::from_ne_bytes(header[4..].try_into().ok()?) as usize;
166 if relation != 0 || size < 8 || size > data.len() - offset {
167 return None;
168 }
169 cores += 1;
170 offset += size;
171 }
172 (cores > 0).then_some(cores)
173}
174
175pub fn shared_pool_active() -> bool {
179 matches!(REPORT.get(), Some(Ok(r)) if r.shared_pool_active
180 || (r.status == EnvironmentStatus::Deferred && r.shared_pool_requested))
181 && !PRIVATE_POOLS.load(Ordering::Acquire)
182}
183
184pub fn init_environment(
187 selection: &RuntimeSelection,
188 options: &EnvironmentOptions,
189) -> Result<EnvironmentReport, String> {
190 init_environment_with_options(
191 selection,
192 &EnvironmentInitOptions {
193 global_pool: options.global_pool,
194 ..EnvironmentInitOptions::default()
195 },
196 )
197 .map(|report| EnvironmentReport {
198 runtime_path: report.runtime_path,
199 runtime_info: report.runtime_info,
200 shared_pool_requested: report.shared_pool_requested,
201 shared_pool_active: report.shared_pool_active,
202 })
203}
204
205pub fn init_environment_with_options(
210 selection: &RuntimeSelection,
211 options: &EnvironmentInitOptions,
212) -> Result<EnvironmentInitReport, String> {
213 init_once_with(&REPORT, options, || {
214 init_inner_with(selection, options, &mut OrtBackend)
215 })
216}
217
218fn init_once_with(
220 report: &OnceLock<Result<EnvironmentInitReport, String>>,
221 options: &EnvironmentInitOptions,
222 init: impl FnOnce() -> Result<EnvironmentInitReport, String>,
223) -> Result<EnvironmentInitReport, String> {
224 let mut initialized_here = false;
225 let result = report.get_or_init(|| {
226 initialized_here = true;
227 init()
228 });
229 let mut result = result.clone();
230 if !initialized_here && options.tolerate_already_initialized {
231 if let Ok(report) = &mut result {
232 report.status = EnvironmentStatus::AlreadyInitialized;
233 }
234 }
235 result
236}
237
238trait EnvironmentBackend {
239 type Builder;
240
241 fn load(&mut self, path: &std::path::Path) -> Result<Self::Builder, String>;
242 fn configure(
243 &mut self,
244 builder: Self::Builder,
245 options: &EnvironmentInitOptions,
246 ) -> Result<bool, String>;
247 fn initialize(&mut self) -> Result<String, String>;
248}
249
250fn init_inner_with(
251 selection: &RuntimeSelection,
252 options: &EnvironmentInitOptions,
253 backend: &mut impl EnvironmentBackend,
254) -> Result<EnvironmentInitReport, String> {
255 let builder = backend.load(&selection.path).map_err(|error| {
256 if options.verbatim_loader_errors {
257 error
258 } else {
259 format!("load {}: {error}", selection.path.display())
260 }
261 })?;
262 let committed = backend.configure(builder, options)?;
263 if !committed && !options.tolerate_already_initialized {
264 return Err("ORT was configured before rightkit-ort::init_environment".into());
265 }
266 let status = if !committed {
267 EnvironmentStatus::AlreadyInitialized
268 } else if options.defer_initialization {
269 EnvironmentStatus::Deferred
270 } else {
271 EnvironmentStatus::Initialized
272 };
273 let runtime_info = if options.defer_initialization {
274 String::new()
275 } else {
276 backend
277 .initialize()
278 .map_err(|error| format!("environment creation: {error}"))?
279 };
280 let shared_requested = options.global_pool.is_some();
281 Ok(EnvironmentInitReport {
282 runtime_path: selection.path.clone(),
283 runtime_info,
284 shared_pool_requested: shared_requested,
285 shared_pool_active: committed && shared_requested && !options.defer_initialization,
286 status,
287 })
288}
289
290struct OrtBackend;
291
292impl EnvironmentBackend for OrtBackend {
293 type Builder = ort::environment::EnvironmentBuilder;
294
295 fn load(&mut self, path: &std::path::Path) -> Result<Self::Builder, String> {
296 ort::init_from(path).map_err(|error| error.to_string())
297 }
298
299 fn configure(
300 &mut self,
301 mut builder: Self::Builder,
302 options: &EnvironmentInitOptions,
303 ) -> Result<bool, String> {
304 if let Some(telemetry) = options.telemetry {
305 builder = builder.with_telemetry(telemetry);
306 }
307 if let Some(pool) = options.global_pool {
308 let pool_options = ort::environment::GlobalThreadPoolOptions::default()
309 .with_intra_threads(pool.intra_threads)
310 .map_err(|error| error.to_string())?
311 .with_inter_threads(pool.inter_threads)
312 .map_err(|error| error.to_string())?
313 .with_spin_control(pool.spin)
314 .map_err(|error| error.to_string())?;
315 builder = builder.with_global_thread_pool(pool_options);
316 }
317 Ok(builder.commit())
318 }
319
320 fn initialize(&mut self) -> Result<String, String> {
321 ort::environment::Environment::current().map_err(|error| error.to_string())?;
322 Ok(ort::info().to_owned())
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::CandidateSource;
330
331 struct FakeBackend {
332 committed: bool,
333 load_error: Option<String>,
334 initialize_error: Option<String>,
335 configured: Option<EnvironmentInitOptions>,
336 initializations: usize,
337 }
338
339 impl Default for FakeBackend {
340 fn default() -> Self {
341 Self {
342 committed: true,
343 load_error: None,
344 initialize_error: None,
345 configured: None,
346 initializations: 0,
347 }
348 }
349 }
350
351 impl EnvironmentBackend for FakeBackend {
352 type Builder = ();
353
354 fn load(&mut self, _: &std::path::Path) -> Result<(), String> {
355 match &self.load_error {
356 Some(error) => Err(error.clone()),
357 None => Ok(()),
358 }
359 }
360
361 fn configure(&mut self, _: (), options: &EnvironmentInitOptions) -> Result<bool, String> {
362 self.configured = Some(options.clone());
363 Ok(self.committed)
364 }
365
366 fn initialize(&mut self) -> Result<String, String> {
367 self.initializations += 1;
368 match &self.initialize_error {
369 Some(error) => Err(error.clone()),
370 None => Ok("ORT Build Info: seam".into()),
371 }
372 }
373 }
374
375 fn selection() -> RuntimeSelection {
376 RuntimeSelection {
377 path: std::env::temp_dir().join(crate::runtime_filename()),
378 source: CandidateSource::Explicit,
379 diagnostics: Vec::new(),
380 }
381 }
382
383 #[test]
384 fn defaults_remain_eager_without_telemetry_or_shared_pool() {
385 let legacy = EnvironmentOptions { global_pool: None };
387 assert_eq!(
388 legacy.global_pool,
389 EnvironmentOptions::default().global_pool
390 );
391 let options = EnvironmentInitOptions::default();
392 assert_eq!(options.global_pool, None);
393 assert!(!options.defer_initialization);
394 assert_eq!(options.telemetry, Some(false));
395 assert!(!options.tolerate_already_initialized);
396 assert!(!options.verbatim_loader_errors);
397 let mut backend = FakeBackend::default();
398 let report = init_inner_with(&selection(), &options, &mut backend).unwrap();
399 assert_eq!(report.status, EnvironmentStatus::Initialized);
400 assert_eq!(backend.initializations, 1);
401 assert_eq!(backend.configured, Some(options));
402 assert!(!report.shared_pool_active);
403 assert!(!report.shared_pool_requested);
404 }
405
406 #[test]
407 fn deferred_mode_commits_options_with_default_telemetry_without_creating_environment() {
408 let options = EnvironmentInitOptions {
409 defer_initialization: true,
410 telemetry: None,
411 global_pool: Some(GlobalPool {
412 intra_threads: 2,
413 inter_threads: 1,
414 spin: false,
415 }),
416 ..Default::default()
417 };
418 let mut backend = FakeBackend::default();
419 let report = init_inner_with(&selection(), &options, &mut backend).unwrap();
420 assert_eq!(report.status, EnvironmentStatus::Deferred);
421 assert_eq!(backend.configured, Some(options));
422 assert_eq!(backend.initializations, 0);
423 assert!(report.runtime_info.is_empty());
424 assert!(report.shared_pool_requested);
425 assert!(!report.shared_pool_active);
426 }
427
428 #[test]
429 fn tolerant_second_init_reports_already_initialized_without_reconfiguration() {
430 let cache = OnceLock::new();
431 let mut backend = FakeBackend::default();
432 let options = EnvironmentInitOptions {
433 tolerate_already_initialized: true,
434 ..Default::default()
435 };
436 let first = init_once_with(&cache, &options, || {
437 init_inner_with(&selection(), &options, &mut backend)
438 })
439 .unwrap();
440 let second =
441 init_once_with(&cache, &options, || panic!("must not initialize twice")).unwrap();
442 assert_eq!(first.status, EnvironmentStatus::Initialized);
443 assert_eq!(second.status, EnvironmentStatus::AlreadyInitialized);
444 assert_eq!(second.runtime_path, first.runtime_path);
445 assert_eq!(second.runtime_info, first.runtime_info);
446 assert_eq!(backend.initializations, 1);
447 let default_repeat = init_once_with(&cache, &EnvironmentInitOptions::default(), || {
448 panic!("must reuse cache")
449 })
450 .unwrap();
451 assert_eq!(default_repeat, first);
452 }
453
454 #[test]
455 fn external_configuration_is_tolerated_only_when_requested() {
456 let mut backend = FakeBackend {
457 committed: false,
458 ..Default::default()
459 };
460 assert_eq!(
461 init_inner_with(
462 &selection(),
463 &EnvironmentInitOptions::default(),
464 &mut backend
465 )
466 .unwrap_err(),
467 "ORT was configured before rightkit-ort::init_environment"
468 );
469 assert_eq!(backend.initializations, 0);
470 let options = EnvironmentInitOptions {
471 tolerate_already_initialized: true,
472 global_pool: Some(GlobalPool {
473 intra_threads: 2,
474 inter_threads: 1,
475 spin: false,
476 }),
477 ..Default::default()
478 };
479 let report = init_inner_with(&selection(), &options, &mut backend).unwrap();
480 assert_eq!(report.status, EnvironmentStatus::AlreadyInitialized);
481 assert!(report.shared_pool_requested);
482 assert!(!report.shared_pool_active); assert_eq!(backend.initializations, 1);
484 let deferred = EnvironmentInitOptions {
485 defer_initialization: true,
486 ..options
487 };
488 let _ = init_inner_with(&selection(), &deferred, &mut backend).unwrap();
489 assert_eq!(backend.initializations, 1);
490 }
491
492 #[test]
493 fn loader_errors_are_verbatim_only_when_requested_and_cached_errors_stay_errors() {
494 let message = "loader original: missing dependency\n native detail";
495 let mut backend = FakeBackend {
496 load_error: Some(message.into()),
497 ..Default::default()
498 };
499 let selected = selection();
500 let contextual =
501 init_inner_with(&selected, &EnvironmentInitOptions::default(), &mut backend)
502 .unwrap_err();
503 assert_eq!(
504 contextual,
505 format!("load {}: {message}", selected.path.display())
506 );
507 let options = EnvironmentInitOptions {
508 verbatim_loader_errors: true,
509 tolerate_already_initialized: true,
510 ..Default::default()
511 };
512 let cache = OnceLock::new();
513 let error = init_once_with(&cache, &options, || {
514 init_inner_with(&selected, &options, &mut backend)
515 })
516 .unwrap_err();
517 assert_eq!(error, message);
518 assert_eq!(backend.initializations, 0);
519 assert!(backend.configured.is_none());
520 assert_eq!(
521 init_once_with(&cache, &options, || panic!("must preserve failure")).unwrap_err(),
522 message
523 );
524 }
525
526 #[test]
527 fn eager_environment_creation_failures_remain_errors() {
528 let mut backend = FakeBackend {
529 initialize_error: Some("create failed".into()),
530 ..Default::default()
531 };
532 let options = EnvironmentInitOptions {
533 verbatim_loader_errors: true,
534 ..Default::default()
535 };
536 assert_eq!(
537 init_inner_with(&selection(), &options, &mut backend).unwrap_err(),
538 "environment creation: create failed"
539 );
540 }
541}