From e92a05b5518e5fd30559c96c5990b08657bbc3e4 Mon Sep 17 00:00:00 2001 From: Nathan Whitaker <17734409+nathanwhit@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:26:21 -0700 Subject: feat(serve): Opt-in parallelism for `deno serve` (#24920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `parallel` flag to `deno serve`. When present, we spawn multiple workers to parallelize serving requests. ```bash deno serve --parallel main.ts ``` Currently on linux we use `SO_REUSEPORT` and rely on the fact that the kernel will distribute connections in a round-robin manner. On mac and windows, we sort of emulate this by cloning the underlying file descriptor and passing a handle to each worker. The connections will not be guaranteed to be fairly distributed (and in practice almost certainly won't be), but the distribution is still spread enough to provide a significant performance increase. --- (Run on an Macbook Pro with an M3 Max, serving `deno.com` baseline:: ``` ❯ wrk -d 30s -c 125 --latency http://127.0.0.1:8000 Running 30s test @ http://127.0.0.1:8000 2 threads and 125 connections Thread Stats Avg Stdev Max +/- Stdev Latency 239.78ms 13.56ms 330.54ms 79.12% Req/Sec 258.58 35.56 360.00 70.64% Latency Distribution 50% 236.72ms 75% 248.46ms 90% 256.84ms 99% 268.23ms 15458 requests in 30.02s, 2.47GB read Requests/sec: 514.89 Transfer/sec: 84.33MB ``` this PR (`with --parallel` flag) ``` ❯ wrk -d 30s -c 125 --latency http://127.0.0.1:8000 Running 30s test @ http://127.0.0.1:8000 2 threads and 125 connections Thread Stats Avg Stdev Max +/- Stdev Latency 117.40ms 142.84ms 590.45ms 79.07% Req/Sec 1.33k 175.19 1.77k 69.00% Latency Distribution 50% 22.34ms 75% 223.67ms 90% 357.32ms 99% 460.50ms 79636 requests in 30.07s, 12.74GB read Requests/sec: 2647.96 Transfer/sec: 433.71MB ``` --- cli/args/flags.rs | 93 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 35 deletions(-) (limited to 'cli/args') diff --git a/cli/args/flags.rs b/cli/args/flags.rs index f8577ed1b..800d6ff5a 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -339,6 +339,7 @@ pub struct ServeFlags { pub watch: Option, pub port: u16, pub host: String, + pub worker_count: Option, } impl ServeFlags { @@ -349,6 +350,7 @@ impl ServeFlags { watch: None, port, host: host.to_owned(), + worker_count: None, } } } @@ -2693,6 +2695,9 @@ fn serve_subcommand() -> Command { .help("The TCP address to serve on, defaulting to 0.0.0.0 (all interfaces).") .value_parser(serve_host_validator), ) + .arg( + parallel_arg("multiple server workers", false) + ) .arg(check_arg(false)) .arg(watch_arg(true)) .arg(watch_exclude_arg()) @@ -2854,11 +2859,7 @@ Directory arguments are expanded to all contained files matching the glob .action(ArgAction::SetTrue), ) .arg( - Arg::new("parallel") - .long("parallel") - .help("Run test modules in parallel. Parallelism defaults to the number of available CPUs or the value in the DENO_JOBS environment variable.") - .conflicts_with("jobs") - .action(ArgAction::SetTrue) + parallel_arg("test modules", true) ) .arg( Arg::new("jobs") @@ -2901,6 +2902,18 @@ Directory arguments are expanded to all contained files matching the glob ) } +fn parallel_arg(descr: &str, jobs_fallback: bool) -> Arg { + let arg = Arg::new("parallel") + .long("parallel") + .help(format!("Run {descr} in parallel. Parallelism defaults to the number of available CPUs or the value in the DENO_JOBS environment variable.")) + .action(ArgAction::SetTrue); + if jobs_fallback { + arg.conflicts_with("jobs") + } else { + arg + } +} + fn types_subcommand() -> Command { Command::new("types").about( "Print runtime TypeScript declarations. @@ -4416,6 +4429,8 @@ fn serve_parse( .remove_one::("host") .unwrap_or_else(|| "0.0.0.0".to_owned()); + let worker_count = parallel_arg_parse(matches, false).map(|v| v.get()); + runtime_args_parse(flags, matches, true, true); // If the user didn't pass --allow-net, add this port to the network // allowlist. If the host is 0.0.0.0, we add :{port} and allow the same network perms @@ -4455,6 +4470,7 @@ fn serve_parse( watch: watch_arg_parse_with_paths(matches), port, host, + worker_count, }); Ok(()) @@ -4486,6 +4502,42 @@ fn task_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.subcommand = DenoSubcommand::Task(task_flags); } +fn parallel_arg_parse( + matches: &mut ArgMatches, + fallback_to_jobs: bool, +) -> Option { + if matches.get_flag("parallel") { + if let Ok(value) = env::var("DENO_JOBS") { + value.parse::().ok() + } else { + std::thread::available_parallelism().ok() + } + } else if fallback_to_jobs && matches.contains_id("jobs") { + // We can't change this to use the log crate because its not configured + // yet at this point since the flags haven't been parsed. This flag is + // deprecated though so it's not worth changing the code to use the log + // crate here and this is only done for testing anyway. + #[allow(clippy::print_stderr)] + { + eprintln!( + "⚠️ {}", + crate::colors::yellow(concat!( + "The `--jobs` flag is deprecated and will be removed in Deno 2.0.\n", + "Use the `--parallel` flag with possibly the `DENO_JOBS` environment variable instead.\n", + "Learn more at: https://docs.deno.com/runtime/manual/basics/env_variables" + )), + ); + } + if let Some(value) = matches.remove_one::("jobs") { + Some(value) + } else { + std::thread::available_parallelism().ok() + } + } else { + None + } +} + fn test_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.type_check_mode = TypeCheckMode::Local; runtime_args_parse(flags, matches, true, true); @@ -4552,36 +4604,7 @@ fn test_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.argv.extend(script_arg); } - let concurrent_jobs = if matches.get_flag("parallel") { - if let Ok(value) = env::var("DENO_JOBS") { - value.parse::().ok() - } else { - std::thread::available_parallelism().ok() - } - } else if matches.contains_id("jobs") { - // We can't change this to use the log crate because its not configured - // yet at this point since the flags haven't been parsed. This flag is - // deprecated though so it's not worth changing the code to use the log - // crate here and this is only done for testing anyway. - #[allow(clippy::print_stderr)] - { - eprintln!( - "⚠️ {}", - crate::colors::yellow(concat!( - "The `--jobs` flag is deprecated and will be removed in Deno 2.0.\n", - "Use the `--parallel` flag with possibly the `DENO_JOBS` environment variable instead.\n", - "Learn more at: https://docs.deno.com/runtime/manual/basics/env_variables" - )), - ); - } - if let Some(value) = matches.remove_one::("jobs") { - Some(value) - } else { - std::thread::available_parallelism().ok() - } - } else { - None - }; + let concurrent_jobs = parallel_arg_parse(matches, true); let include = if let Some(files) = matches.remove_many::("files") { files.collect() -- cgit v1.2.3