* feat(cli): add --v8-flags plumbing to configure V8 at startup (#130) Obscura had no way to pass flags to V8, so users hitting OOM on pages larger than ~5MB had no escape hatch. Adds a generic passthrough rather than a one-off heap knob, so future V8 tuning needs no new code. - New `obscura_js::set_v8_flags` (Once-guarded, empty-string no-op). - Global `--v8-flags <FLAGS>` arg with `allow_hyphen_values = true`, applied once in `main` before any isolate is created. - Usage: `obscura --v8-flags "--max-old-space-size=4096" fetch <url>`. Closes #130. * test(cli): expand --v8-flags coverage with normalize helper Extract normalize_v8_flags() from main() so the trim/empty-check logic is unit-testable without needing to invoke V8. Adds parse-coverage tests for --v8-flags with serve/scrape subcommands and empty-string values, plus pure-function tests for the normalizer.
This commit is contained in:
1 parent
7b209dc655
commit
cb701fc2d8
3 files changed
+154
-1
No files matched your search
@@ -27,6 +27,12 @@ struct Args {
|
|||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
|
|
||||||
|
/// Pass raw flags to V8, in the same form V8/Chromium/Node accept
|
||||||
|
/// (e.g. `"--max-old-space-size=4096 --max-semi-space-size=64 --expose-gc"`).
|
||||||
|
/// Applied once at startup before any isolate is created.
|
||||||
|
#[arg(long, value_name = "FLAGS", allow_hyphen_values = true)]
|
||||||
|
v8_flags: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@@ -182,6 +188,18 @@ fn merge_proxy(global_proxy: Option<String>, command_proxy: Option<String>) -> O
|
|||||||
command_proxy.or(global_proxy)
|
command_proxy.or(global_proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize a raw `--v8-flags` value into the string we'll hand to V8.
|
||||||
|
/// Returns `None` when the user didn't pass the flag, passed an empty string,
|
||||||
|
/// or passed only whitespace; in those cases V8 is left untouched.
|
||||||
|
fn normalize_v8_flags(raw: Option<&str>) -> Option<String> {
|
||||||
|
let trimmed = raw?.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(trimmed.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
@@ -196,6 +214,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.with_writer(std::io::stderr)
|
.with_writer(std::io::stderr)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
if let Some(flags) = normalize_v8_flags(args.v8_flags.as_deref()) {
|
||||||
|
tracing::info!("Applying V8 flags: {}", flags);
|
||||||
|
obscura_js::set_v8_flags(&flags);
|
||||||
|
}
|
||||||
|
|
||||||
let global_proxy = args.proxy.clone();
|
let global_proxy = args.proxy.clone();
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
@@ -906,7 +929,8 @@ fn dump_links(page: &Page) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
extract_readable_text, fetch_original_bytes, is_quiet_command, merge_proxy,
|
extract_readable_text, fetch_original_bytes, is_quiet_command, merge_proxy,
|
||||||
select_log_filter, write_or_print, write_or_print_bytes, Args, Command, DumpFormat,
|
normalize_v8_flags, select_log_filter, write_or_print, write_or_print_bytes, Args,
|
||||||
|
Command, DumpFormat,
|
||||||
};
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use obscura_dom::parse_html;
|
use obscura_dom::parse_html;
|
||||||
@@ -1065,6 +1089,96 @@ mod tests {
|
|||||||
assert!(!is_quiet_command(&None));
|
assert!(!is_quiet_command(&None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsed_v8_flags_global_arg() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"obscura",
|
||||||
|
"--v8-flags",
|
||||||
|
"--max-old-space-size=4096 --max-semi-space-size=64",
|
||||||
|
"fetch",
|
||||||
|
"https://example.com",
|
||||||
|
])
|
||||||
|
.expect("clap should accept --v8-flags as a global arg");
|
||||||
|
assert_eq!(
|
||||||
|
args.v8_flags.as_deref(),
|
||||||
|
Some("--max-old-space-size=4096 --max-semi-space-size=64"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v8_flags_default_is_none() {
|
||||||
|
let args = Args::try_parse_from(["obscura", "fetch", "https://example.com"])
|
||||||
|
.expect("clap should accept fetch without --v8-flags");
|
||||||
|
assert!(args.v8_flags.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsed_v8_flags_with_serve_subcommand() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"obscura",
|
||||||
|
"--v8-flags",
|
||||||
|
"--max-old-space-size=2048",
|
||||||
|
"serve",
|
||||||
|
"--port",
|
||||||
|
"9333",
|
||||||
|
])
|
||||||
|
.expect("clap should accept --v8-flags with serve");
|
||||||
|
assert_eq!(args.v8_flags.as_deref(), Some("--max-old-space-size=2048"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsed_v8_flags_with_scrape_subcommand() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"obscura",
|
||||||
|
"--v8-flags",
|
||||||
|
"--expose-gc",
|
||||||
|
"scrape",
|
||||||
|
"https://a.com",
|
||||||
|
"https://b.com",
|
||||||
|
])
|
||||||
|
.expect("clap should accept --v8-flags with scrape");
|
||||||
|
assert_eq!(args.v8_flags.as_deref(), Some("--expose-gc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsed_v8_flags_empty_string_is_accepted() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"obscura",
|
||||||
|
"--v8-flags",
|
||||||
|
"",
|
||||||
|
"fetch",
|
||||||
|
"https://example.com",
|
||||||
|
])
|
||||||
|
.expect("clap should accept empty --v8-flags value");
|
||||||
|
assert_eq!(args.v8_flags.as_deref(), Some(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_v8_flags_returns_none_when_unset() {
|
||||||
|
assert_eq!(normalize_v8_flags(None), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_v8_flags_returns_none_for_empty_or_whitespace() {
|
||||||
|
assert_eq!(normalize_v8_flags(Some("")), None);
|
||||||
|
assert_eq!(normalize_v8_flags(Some(" ")), None);
|
||||||
|
assert_eq!(normalize_v8_flags(Some("\t\n")), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_v8_flags_trims_surrounding_whitespace() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_v8_flags(Some(" --max-old-space-size=4096 ")).as_deref(),
|
||||||
|
Some("--max-old-space-size=4096"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_v8_flags_preserves_multi_flag_string() {
|
||||||
|
let input = "--max-old-space-size=4096 --max-semi-space-size=64 --expose-gc";
|
||||||
|
assert_eq!(normalize_v8_flags(Some(input)).as_deref(), Some(input));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parsed_fetch_quiet_resolves_to_off_filter() {
|
fn parsed_fetch_quiet_resolves_to_off_filter() {
|
||||||
let args = Args::try_parse_from([
|
let args = Args::try_parse_from([
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ extern crate html5ever;
|
|||||||
pub mod module_loader;
|
pub mod module_loader;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
|
pub mod v8_flags;
|
||||||
pub mod v8_lock;
|
pub mod v8_lock;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
|
|
||||||
pub use markdown::HTML_TO_MARKDOWN_JS;
|
pub use markdown::HTML_TO_MARKDOWN_JS;
|
||||||
|
pub use v8_flags::set_v8_flags;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
|
||||||
|
/// Apply user-supplied V8 flags exactly once, before the first isolate is
|
||||||
|
/// created.
|
||||||
|
///
|
||||||
|
/// `flags` is a raw V8 flag string in the same form V8/Chromium/Node accept
|
||||||
|
/// (e.g. `"--max-old-space-size=4096 --max-semi-space-size=64"`). An empty or
|
||||||
|
/// whitespace-only string is a no-op and does not consume the one-shot guard,
|
||||||
|
/// so a later non-empty call still takes effect.
|
||||||
|
///
|
||||||
|
/// V8 ignores `set_flags_from_string` once the platform is initialized, so the
|
||||||
|
/// first non-empty call must run before any `JsRuntime` is constructed.
|
||||||
|
/// Subsequent calls are silently dropped.
|
||||||
|
pub fn set_v8_flags(flags: &str) {
|
||||||
|
let trimmed = flags.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
INIT.call_once(|| {
|
||||||
|
deno_core::v8::V8::set_flags_from_string(trimmed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_is_noop() {
|
||||||
|
// Must not panic and must not consume the Once guard.
|
||||||
|
set_v8_flags("");
|
||||||
|
set_v8_flags(" ");
|
||||||
|
set_v8_flags("\t\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user