diff --git a/README.md b/README.md index 4a5b287c..299d8c17 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,9 @@ Apache License, Version 2.0, without any additional terms or conditions. Rocket is designed to be performant. At this time, its performance is [bottlenecked by the Hyper HTTP library](https://github.com/SergioBenitez/Rocket/issues/17). Even so, Rocket -currently performs _better_ than the latest version of Hyper on a simple "Hello, -world!" benchmark. Rocket also performs better than the Iron web framework: +currently performs _significantly better_ than the latest version of +asynchronous Hyper on a simple "Hello, world!" benchmark. Rocket also performs +_significantly better_ than the Iron web framework: **Machine Specs:** @@ -157,45 +158,45 @@ world!" benchmark. Rocket also performs better than the Iron web framework: * **Processor:** Intel Xeon X5675 @ 3.07GHz * **Operating System:** Mac OS X v10.11.6 -**Rocket v0.1.4** (8 LOC) results (best of 3, +/- 1000 req/s, +/- 5us latency): +**Rocket v0.2-rc** (8 LOC) results (best of 3, +/- 2000 req/s, +/- 5us latency): Running 10s test @ http://localhost:80 - 2 threads and 10 connections + 1 threads and 18 connections Thread Stats Avg Stdev Max +/- Stdev - Latency 162.40us 28.41us 377.00us 69.21% - Req/Sec 29.88k 1.29k 33.72k 70.30% - 600412 requests in 10.10s, 83.60MB read - Requests/sec: 59448.48 - Transfer/sec: 8.28MB + Latency 153.01us 42.25us 449.00us 75.54% + Req/Sec 75.58k 11.75k 90.22k 54.46% + 758044 requests in 10.10s, 105.55MB read + Requests/sec: 75051.28 + Transfer/sec: 10.45MB -**Hyper v0.10.0-a.0** (46 LOC) results (best of 3, +/- 2000 req/s, +/- 10us latency): +**Hyper v0.11.0-a.0 (1/12/2016)** (46 LOC) results (best of 3, +/- 5000 req/s, +/- 30us latency): Running 10s test @ http://localhost:80 - 2 threads and 10 connections + 1 threads and 18 connections Thread Stats Avg Stdev Max +/- Stdev - Latency 175.12us 40.38us 429.00us 70.79% - Req/Sec 28.00k 2.41k 36.79k 72.28% - 562692 requests in 10.10s, 81.57MB read - Requests/sec: 55715.98 - Transfer/sec: 8.08MB + Latency 287.81us 77.09us 606.00us 70.47% + Req/Sec 59.94k 6.01k 79.72k 71.00% + 596231 requests in 10.00s, 83.02MB read + Requests/sec: 59621.32 + Transfer/sec: 8.30MB -**Iron v0.4.0** (11 LOC) results (best of 3, +/- 1000 req/s, +/- 5us latency): +**Iron v0.5.0** (11 LOC) results (best of 3, +/- 1000 req/s, +/- 5us latency): Running 10s test @ http://localhost:80 - 2 threads and 10 connections + 1 threads and 18 connections Thread Stats Avg Stdev Max +/- Stdev - Latency 189.93us 40.05us 2.06ms 67.57% - Req/Sec 25.80k 2.26k 34.01k 77.72% - 518575 requests in 10.10s, 64.79MB read - Requests/sec: 51346.00 - Transfer/sec: 6.41MB + Latency 512.36us 5.57ms 149.99ms 99.60% + Req/Sec 58.25k 11.61k 70.47k 46.00% + 579227 requests in 10.00s, 80.65MB read + Requests/sec: 57920.73 + Transfer/sec: 8.06MB **Summary:** - * Rocket throughput higher by 6.7% (higher is better) compared to Hyper. - * Rocket throughput higher by 15.8% (higher is better) compared to Iron. - * Rocket latency lower by 7.3% (lower is better) compared to Hyper. - * Rocket latency lower by 14.5% (lower is better) compared to Iron. + * Rocket throughput higher by 25.9% (higher is better) compared to Hyper. + * Rocket throughput higher by 29.6% (higher is better) compared to Iron. + * Rocket latency lower by 46.8% (lower is better) compared to Hyper. + * Rocket latency lower by 70.1% (lower is better) compared to Iron. ### Future Improvements diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml index d644f6f8..3640d3e4 100644 --- a/examples/config/Rocket.toml +++ b/examples/config/Rocket.toml @@ -4,6 +4,7 @@ [development] address = "localhost" port = 8000 +workers = 1 log = "normal" hi = "Hello!" is_extra = true @@ -12,12 +13,14 @@ is_extra = true address = "0.0.0.0" port = 80 log = "normal" +workers = 8 # don't use this key! generate your own and keep it private! session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" [production] address = "0.0.0.0" port = 80 +workers = 12 log = "critical" # don't use this key! generate your own and keep it private! session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index af8a2883..fa46db9e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,6 +18,7 @@ term-painter = "^0.2" log = "^0.3" url = "^1" toml = "^0.2" +num_cpus = "1" # cookie = "^0.3" [dependencies.hyper] diff --git a/lib/src/config/builder.rs b/lib/src/config/builder.rs index f54fa5a3..011b3a64 100644 --- a/lib/src/config/builder.rs +++ b/lib/src/config/builder.rs @@ -13,6 +13,8 @@ pub struct ConfigBuilder { pub address: String, /// The port to serve on. pub port: u16, + /// The number of workers to run in parallel. + pub workers: u16, /// How much information to log. pub log_level: LoggingLevel, /// The session key. @@ -30,6 +32,7 @@ impl ConfigBuilder { environment: config.environment, address: config.address, port: config.port, + workers: config.workers, log_level: config.log_level, session_key: None, extras: config.extras, @@ -50,6 +53,13 @@ impl ConfigBuilder { self } + /// Sets the `workers` in `self` to `workers` and returns the structure. + #[inline(always)] + pub fn workers(mut self, workers: u16) -> Self { + self.workers = workers; + self + } + /// Sets the `log_level` in `self` to `log_level` and returns the structure. #[inline(always)] pub fn log_level(mut self, log_level: LoggingLevel) -> Self { @@ -84,6 +94,7 @@ impl ConfigBuilder { let mut config = Config::new(self.environment)?; config.set_address(self.address)?; config.set_port(self.port); + config.set_workers(self.workers); config.set_log_level(self.log_level); config.set_extras(self.extras); diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index 59fd3a84..4952925e 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -9,22 +9,25 @@ use std::env; use config::Environment::*; use config::{self, Value, ConfigBuilder, Environment, ConfigError}; +use num_cpus; use logger::LoggingLevel; /// The core configuration structure. pub struct Config { + /// The environment that this configuration corresponds to. + pub environment: Environment, /// The address to serve on. pub address: String, /// The port to serve on. pub port: u16, + /// The number of workers to run concurrently. + pub workers: u16, /// How much information to log. pub log_level: LoggingLevel, - /// The environment that this configuration corresponds to. - pub environment: Environment, - /// The path to the configuration file this config belongs to. - pub config_path: PathBuf, /// Extra parameters that aren't part of Rocket's core config. pub extras: HashMap, + /// The path to the configuration file this config belongs to. + pub config_path: PathBuf, /// The session key. session_key: RwLock>, } @@ -75,37 +78,43 @@ impl Config { "Configuration files must be rooted in a directory.")); } + // Note: This may truncate if num_cpus::get() > u16::max. That's okay. + let default_workers = ::std::cmp::max(num_cpus::get(), 2) as u16; + Ok(match env { Development => { Config { + environment: Development, address: "localhost".to_string(), port: 8000, + workers: default_workers, log_level: LoggingLevel::Normal, session_key: RwLock::new(None), extras: HashMap::new(), - environment: env, config_path: config_path, } } Staging => { Config { + environment: Staging, address: "0.0.0.0".to_string(), port: 80, + workers: default_workers, log_level: LoggingLevel::Normal, session_key: RwLock::new(None), extras: HashMap::new(), - environment: env, config_path: config_path, } } Production => { Config { + environment: Production, address: "0.0.0.0".to_string(), port: 80, + workers: default_workers, log_level: LoggingLevel::Critical, session_key: RwLock::new(None), extras: HashMap::new(), - environment: env, config_path: config_path, } } @@ -132,8 +141,9 @@ impl Config { /// /// * **address**: String /// * **port**: Integer (16-bit unsigned) - /// * **session_key**: String (192-bit base64) + /// * **workers**: Integer (16-bit unsigned) /// * **log**: String + /// * **session_key**: String (192-bit base64) /// pub fn set(&mut self, name: &str, val: &Value) -> config::Result<()> { if name == "address" { @@ -141,13 +151,18 @@ impl Config { self.set_address(address_str)?; } else if name == "port" { let port = parse!(self, name, val, as_integer, "an integer")?; - if port < 0 { - return Err(self.bad_type(name, val.type_str(), "an unsigned integer")); - } else if port > (u16::max_value() as i64) { + if port < 0 || port > (u16::max_value() as i64) { return Err(self.bad_type(name, val.type_str(), "a 16-bit unsigned integer")) } self.set_port(port as u16); + } else if name == "workers" { + let workers = parse!(self, name, val, as_integer, "an integer")?; + if workers < 0 || workers > (u16::max_value() as i64) { + return Err(self.bad_type(name, val.type_str(), "a 16-bit unsigned integer")); + } + + self.set_workers(workers as u16); } else if name == "session_key" { let key = parse!(self, name, val, as_str, "a string")?; self.set_session_key(key)?; @@ -178,7 +193,11 @@ impl Config { } pub fn set_port(&mut self, port: u16) { - self.port = port as u16; + self.port = port; + } + + pub fn set_workers(&mut self, workers: u16) { + self.workers = workers; } pub fn set_session_key>(&mut self, key: K) -> config::Result<()> { @@ -284,8 +303,8 @@ impl Config { impl fmt::Debug for Config { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Config[{}] {{ address: {}, port: {}, log_level: {:?}", - self.environment, self.address, self.port, self.log_level)?; + write!(f, "Config[{}] {{ address: {}, port: {}, workers: {}, log: {:?}", + self.environment, self.address, self.port, self.workers, self.log_level)?; for (key, value) in self.extras() { write!(f, ", {}: {}", key, value)?; @@ -300,6 +319,7 @@ impl PartialEq for Config { fn eq(&self, other: &Config) -> bool { self.address == other.address && self.port == other.port + && self.workers == other.workers && self.log_level == other.log_level && self.environment == other.environment && self.extras == other.extras diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 20ca910e..e641cc09 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -35,6 +35,8 @@ //! * examples: `"localhost"`, `"0.0.0.0"`, `"1.2.3.4"` //! * **port**: _[integer]_ a port number to listen on //! * examples: `"8000"`, `"80"`, `"4242"` +//! * **workers**: _[integer]_ the number of concurrent workers to use +//! * examples: `"12"`, `"1"`, `"4"` //! * **log**: _[string]_ how much information to log; one of `"normal"`, //! `"debug"`, or `"critical"` //! * **session_key**: _[string]_ a 192-bit base64 encoded string (32 @@ -58,11 +60,13 @@ //! [development] //! address = "localhost" //! port = 8000 +//! workers = max(number_of_cpus, 2) //! log = "normal" //! //! [staging] //! address = "0.0.0.0" //! port = 80 +//! workers = max(number_of_cpus, 2) //! log = "normal" //! # don't use this key! generate your own and keep it private! //! session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" @@ -70,11 +74,16 @@ //! [production] //! address = "0.0.0.0" //! port = 80 +//! workers = max(number_of_cpus, 2) //! log = "critical" //! # don't use this key! generate your own and keep it private! //! session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" //! ``` //! +//! The `workers` parameter is computed by Rocket automatically; the value above +//! is not valid TOML syntax. When manually specifying the number of workers, +//! the value should be an integer: `workers = 10`. +//! //! The "global" pseudo-environment can be used to set and/or override //! configuration parameters globally. A parameter defined in a `[global]` table //! sets, or overrides if already present, that parameter in every environment. @@ -502,6 +511,7 @@ mod test { let config_str = r#" address = "1.2.3.4" port = 7810 + workers = 21 log = "critical" session_key = "01234567890123456789012345678901" template_dir = "mine" @@ -512,6 +522,7 @@ mod test { let mut expected = default_config(Development) .address("1.2.3.4") .port(7810) + .workers(21) .log_level(LoggingLevel::Critical) .session_key("01234567890123456789012345678901") .extra("template_dir", "mine") @@ -655,6 +666,66 @@ mod test { "#.to_string(), TEST_CONFIG_FILENAME).is_err()); } + #[test] + fn test_good_workers_values() { + // Take the lock so changing the environment doesn't cause races. + let _env_lock = ENV_LOCK.lock().unwrap(); + env::set_var(CONFIG_ENV, "stage"); + + check_config!(RocketConfig::parse(r#" + [stage] + workers = 1 + "#.to_string(), TEST_CONFIG_FILENAME), { + default_config(Staging).workers(1) + }); + + check_config!(RocketConfig::parse(r#" + [stage] + workers = 300 + "#.to_string(), TEST_CONFIG_FILENAME), { + default_config(Staging).workers(300) + }); + + check_config!(RocketConfig::parse(r#" + [stage] + workers = 65535 + "#.to_string(), TEST_CONFIG_FILENAME), { + default_config(Staging).workers(65535) + }); + } + + #[test] + fn test_bad_workers_values() { + // Take the lock so changing the environment doesn't cause races. + let _env_lock = ENV_LOCK.lock().unwrap(); + env::remove_var(CONFIG_ENV); + + assert!(RocketConfig::parse(r#" + [development] + workers = true + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + + assert!(RocketConfig::parse(r#" + [production] + workers = "hello" + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + + assert!(RocketConfig::parse(r#" + [staging] + workers = -1 + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + + assert!(RocketConfig::parse(r#" + [staging] + workers = 65536 + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + + assert!(RocketConfig::parse(r#" + [staging] + workers = 105836 + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + } + #[test] fn test_good_log_levels() { // Take the lock so changing the environment doesn't cause races. diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 99e74fbc..87abb98a 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -95,6 +95,7 @@ extern crate term_painter; extern crate hyper; extern crate url; extern crate toml; +extern crate num_cpus; #[cfg(test)] #[macro_use] extern crate lazy_static; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 91cca200..cf871fd9 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -315,6 +315,7 @@ impl Rocket { White.paint(&config.address), White.paint(&config.port)); info_!("logging: {:?}", White.paint(config.log_level)); + info_!("workers: {}", White.paint(config.workers)); let session_key = config.take_session_key(); if session_key.is_some() { @@ -485,6 +486,7 @@ impl Rocket { White.bold().paint("http://"), White.bold().paint(&full_addr)); - server.handle(self).unwrap(); + let threads = self.config.workers as usize; + server.handle_threads(self, threads).unwrap(); } }