Skip to main content

browser_automation_cli/
runtime_util.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Tokio runtime builders tuned for **one-shot CLI** with **bounded parallelism**.
3//!
4//! # Workload
5//!
6//! **Mista / I/O-bound** (Chrome CDP + HTTP). Async coordinates wait; CPU work
7//! uses Rayon or `spawn_blocking` (see [`crate::concurrency`]).
8//!
9//! # Product latency model
10//!
11//! Wall time is dominated by **Chrome CDP / network I/O**, not Rust CPU. Rules
12//! `rules_rust_latencia_reduzir` + `rules_rust_paralelismo` require:
13//!
14//! - **Bounded runtimes** — worker count from [`crate::concurrency::browser_worker_threads`]
15//!   (not unbounded `num_cpus` on a one-shot agent process; hard-capped at 8).
16//! - **No blocking work on async workers** without `spawn_blocking`.
17//! - **No PGO/BOLT/isolcpus/mlockall** as product defaults (daemon/HFT ops;
18//!   product law is BORN→EXECUTE→FINALIZE→DIE).
19//!
20//! # Latency budgets (agent-facing, host-local, release build)
21//!
22//! | Path | P99 budget (order of magnitude) | Notes |
23//! |------|----------------------------------|-------|
24//! | Clap parse + doctor offline quick | ≤ **50 ms** | No Chrome; meta path |
25//! | `--help` cold | ≤ **80 ms** | First process image load |
26//! | JSON envelope encode (small) | ≤ **100 µs** | Criterion / unit |
27//! | Chrome launch + first CDP | **seconds** | External; not Rust hot path |
28//!
29//! Budgets are **ceilings for regression detection**, not SLOs for trading.
30//! Re-measure with `scripts/latency-baseline.sh` after runtime changes.
31//!
32//! # Runtime flavours
33//!
34//! | Helper | Flavour | Use |
35//! |--------|---------|-----|
36//! | [`block_on_browser_timeout`](crate::browser::block_on_browser_timeout) | multi-thread, **budgeted** workers | CDP event fan-out |
37//! | [`block_on_io`] | multi-thread, budgeted (I/O pipelines) | HTTP scrape / batch / crawl |
38//!
39//! Never create an unbounded `new_multi_thread()` without the concurrency budget.
40
41use crate::concurrency::{browser_max_blocking_threads, browser_worker_threads};
42use crate::error::{CliError, ErrorKind};
43
44/// Thread name prefix for browser runtime workers (`bac-browser-0`, …).
45pub const BROWSER_THREAD_NAME: &str = "bac-browser";
46
47/// Thread name prefix for I/O multi-thread runtimes.
48pub const IO_THREAD_NAME: &str = "bac-io";
49
50/// Build the multi-thread runtime used for Chrome CDP sessions.
51///
52/// # Parallelism notes
53///
54/// - Workers: [`browser_worker_threads`] from process budget / `--max-concurrency`.
55/// - Blocking pool: [`browser_max_blocking_threads`].
56/// - Named threads for `perf` / `tokio-console` attribution.
57pub fn build_browser_runtime() -> Result<tokio::runtime::Runtime, CliError> {
58    let workers = browser_worker_threads();
59    let blocking = browser_max_blocking_threads();
60    tokio::runtime::Builder::new_multi_thread()
61        .enable_all()
62        .worker_threads(workers)
63        .max_blocking_threads(blocking)
64        .thread_name(BROWSER_THREAD_NAME)
65        .build()
66        .map_err(|e| {
67            CliError::new(
68                ErrorKind::Software,
69                format!("Failed to create browser tokio runtime: {e}"),
70            )
71        })
72}
73
74/// Build a multi-thread runtime for HTTP / offline async fan-out.
75///
76/// Uses the same budgeted worker count as the browser runtime so batch scrape
77/// and crawl can drive concurrent sockets without a second unbounded pool.
78pub fn build_io_runtime() -> Result<tokio::runtime::Runtime, CliError> {
79    let workers = browser_worker_threads();
80    let blocking = browser_max_blocking_threads();
81    tokio::runtime::Builder::new_multi_thread()
82        .enable_all()
83        .worker_threads(workers)
84        .max_blocking_threads(blocking)
85        .thread_name(IO_THREAD_NAME)
86        .build()
87        .map_err(|e| {
88            CliError::new(
89                ErrorKind::Software,
90                format!("Failed to create I/O tokio runtime: {e}"),
91            )
92        })
93}
94
95/// Drive an async I/O future to completion on a budgeted multi-thread runtime.
96///
97/// Use for HTTP scrape, batch scrape, crawl, and other non-CDP async entered
98/// from synchronous CLI handlers. Prefer
99/// [`crate::browser::block_on_browser_timeout`] for Chrome CDP.
100pub fn block_on_io<F, T>(fut: F) -> Result<T, CliError>
101where
102    F: std::future::Future<Output = Result<T, CliError>>,
103{
104    let rt = build_io_runtime()?;
105    rt.block_on(fut)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn browser_runtime_builds() {
114        let rt = build_browser_runtime().expect("browser rt");
115        let n = rt.block_on(async { 1 + 1 });
116        assert_eq!(n, 2);
117    }
118
119    #[test]
120    fn io_runtime_block_on_io() {
121        let v = block_on_io(async { Ok::<_, CliError>(42u32) }).expect("io");
122        assert_eq!(v, 42);
123    }
124
125    #[test]
126    fn worker_budget_is_bounded() {
127        assert!(browser_worker_threads() >= 2);
128        assert!(browser_worker_threads() <= 8);
129        assert!(browser_max_blocking_threads() <= 16);
130        assert!(crate::concurrency::effective_limit() <= crate::concurrency::HARD_CAP);
131    }
132}