Skip to main content

rightkit_ort/
environment.rs

1//! Process-wide ORT environment initialisation (merged from HeardRight
2//! `heardright-onnx-asr/environment.rs`, product experiment switches removed).
3//!
4//! Call [`init_environment`] once, before any session is built or any worker
5//! thread starts. It loads the selected runtime explicitly, disables telemetry,
6//! optionally requests one shared global CPU thread pool, and materialises the
7//! C environment immediately by default (committing only stores Rust-side
8//! options). Deferred mode still checks/loads the dylib, but leaves C environment
9//! creation to the first ORT use, matching `ort::init_from(...).commit()`.
10//! If a shared pool cannot be had, sessions fall back to private pools and the
11//! report says so; nothing is silently reconfigured.
12
13use 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/// Shared CPU pool request.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct GlobalPool {
26    pub intra_threads: usize,
27    pub inter_threads: usize,
28    /// Busy-wait in idle workers. Off by default: HeardRight measured spinning
29    /// as pure CPU cost for bursty dictation workloads.
30    pub spin: bool,
31}
32
33impl GlobalPool {
34    /// Budget derived from physical cores via [`cpu_thread_budget`], one inter
35    /// thread, no spinning.
36    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/// Original eager-initialization options, retained for existing struct literals.
47#[derive(Debug, Clone, Default)]
48pub struct EnvironmentOptions {
49    /// `None` keeps per-session private pools.
50    pub global_pool: Option<GlobalPool>,
51}
52
53/// Extended initialization policy; defaults match [`EnvironmentOptions`].
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct EnvironmentInitOptions {
56    /// `None` keeps per-session private pools.
57    pub global_pool: Option<GlobalPool>,
58    /// Commit options now, create the C environment on first use. The dylib
59    /// is still loaded now, so loader failures remain reportable startup errors.
60    pub defer_initialization: bool,
61    /// `Some(false)` disables telemetry (default); `None` leaves ort's builder
62    /// default unchanged; `Some(true)` explicitly enables telemetry.
63    pub telemetry: Option<bool>,
64    /// Accept an environment configured outside this crate. Repeated successful
65    /// calls report AlreadyInitialized; existing options are never replaced.
66    pub tolerate_already_initialized: bool,
67    /// Return the loader's Display message exactly, without a path prefix.
68    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    /// Options committed; C environment creation deferred to first use.
87    Deferred,
88    /// Earlier configuration retained, including configuration outside this crate.
89    AlreadyInitialized,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct EnvironmentReport {
94    pub runtime_path: std::path::PathBuf,
95    /// ORT build info string reported by the loaded library.
96    pub runtime_info: String,
97    pub shared_pool_requested: bool,
98    pub shared_pool_active: bool,
99}
100
101/// Extended report for [`init_environment_with_options`].
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct EnvironmentInitReport {
104    /// Selected path; when external configuration already exists, ort retains
105    /// its loaded library & this path is not proof of that library's identity.
106    pub runtime_path: std::path::PathBuf,
107    /// ORT build info; empty when C environment initialization was deferred.
108    pub runtime_info: String,
109    pub shared_pool_requested: bool,
110    pub shared_pool_active: bool,
111    pub status: EnvironmentStatus,
112}
113
114/// HeardRight's measured budget: leave headroom on small machines, cap at four.
115pub 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    // u64 storage supplies required alignment; record sizes are variable.
136    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
156/// Count `RelationProcessorCore` records in a `GetLogicalProcessorInformationEx`
157/// buffer; rejects truncated or foreign records. Public so Windows hosts and
158/// tests can validate topology buffers.
159pub 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
175/// True when this crate committed a shared pool configuration. In deferred
176/// mode, ORT creates that pool on first use before session options consult it.
177/// Externally configured pools are unknown & conservatively return false.
178pub 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
184/// Initialise ORT exactly once for the process. Later calls return the first
185/// outcome unchanged (options of later calls are ignored).
186pub 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
205/// Initialise once under extended policy. Later calls reuse the first outcome;
206/// tolerance reports [`EnvironmentStatus::AlreadyInitialized`] on repeated
207/// success & accepts a configuration committed outside this crate. Neither
208/// existing options nor a loaded runtime can be replaced through this API.
209pub 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
218// A caller-local OnceLock & backend keep tests independent of process ORT state.
219fn 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        // Original public struct literals remain source-compatible.
386        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); // Never attribute ignored pool options to ORT.
483        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}