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}