Initial feedback:
- Reading to a string is wasteful if you just want to dump data. A
Vec is simpler since you don't care about UTF-8 validity; this will also allow for images to be served. - Returning "Not found" for a failure to read seems like a lie. Should be a HTTP 5xx-series error.
- Avoid code duplication by returning the content type from the match and setting it afterward.
text/plain is an unconventional default content type, generally I see application/octet-stream. - Extract a constant for the default content type instead of repeating it.
Turning towards performance, start by performing a base-line performance test of a failure URL:
$ wrk http://127.0.0.1:5000/nothing Running 10s test @ http://127.0.0.1:5000/nothing 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 290.75us 44.99us 1.50ms 90.46% Req/Sec 17.00k 537.99 18.05k 73.76% 341686 requests in 10.10s, 26.72MB read Non-2xx or 3xx responses: 341686 Requests/sec: 33831.33 Transfer/sec: 2.65MB
Then a test with an existing but blank file:
$ wrk http://127.0.0.1:5000/exists Running 10s test @ http://127.0.0.1:5000/exists 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 432.23us 77.26us 1.67ms 85.00% Req/Sec 11.49k 586.77 12.74k 65.84% 231023 requests in 10.10s, 25.34MB read Requests/sec: 22874.91 Transfer/sec: 2.51MB
Then a test with a 1 MiB file:
$ wrk http://127.0.0.1:5000/big Running 10s test @ http://127.0.0.1:5000/big 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 38.58ms 5.60ms 94.66ms 95.67% Req/Sec 129.41 16.53 171.00 85.50% 2595 requests in 10.06s, 2.53GB read Requests/sec: 257.93 Transfer/sec: 257.96MB
This isn't terrible. The biggest problem I see is that you are using an asynchronous library in a completely synchronous manner. Using a threadpool to offload loading the files increases throughput to ~3x of the previous value:
$ wrk http://127.0.0.1:5000/big Running 10s test @ http://127.0.0.1:5000/big 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 13.60ms 3.75ms 62.03ms 84.27% Req/Sec 370.48 43.96 415.00 89.50% 7403 requests in 10.04s, 7.23GB read Requests/sec: 737.26 Transfer/sec: 737.35MB
Its possible caching might help even further, but I didn't attempt it.
extern crate futures; extern crate futures_spawn; extern crate futures_threadpool; extern crate hyper; use futures::Future; use futures_spawn::SpawnHelper; use futures_threadpool::ThreadPool; use hyper::StatusCode; use hyper::server::{Http, Request, Response, Service}; use std::ffi::OsStr; use std::fs::File; use std::io::Read; use std::path::Path; use std::sync::Arc; struct Router { pool: ThreadPool, } impl Service for Router { type Request = Request; type Response = Response; type Error = hyper::Error; type Future = Box<Future<Item = Self::Response, Error = Self::Error>>; fn call(&self, req: Request) -> Self::Future { let full_path = "src/static".to_owned() + req.path(); let data = self.pool.spawn_fn(move || { let mut f = File::open(full_path)?; let mut data = Vec::new(); f.read_to_end(&mut data)?; Ok(data) }); let mut headers = hyper::header::Headers::new(); const UNKNOWN_CONTENT_TYPE: &str = "text/plain"; let content_type = match Path::new(req.path()).extension().and_then(OsStr::to_str) { Some(ext) => match ext { "html" => "text/html", "css" => "text/css", "js" => "application/javascript", _ => UNKNOWN_CONTENT_TYPE, }, None => UNKNOWN_CONTENT_TYPE, }; headers.set_raw("Content-Type", content_type); let r = data.map(|data| { Response::new() .with_status(StatusCode::Ok) .with_headers(headers) .with_body(data) }); Box::new(r) } } fn main() { let addr = "127.0.0.1:5000".parse().unwrap(); let router = Arc::new(Router { pool: ThreadPool::new_num_cpus(), }); let server = Http::new() .bind(&addr, move || Ok(Arc::clone(&router))) .unwrap(); println!( "Listening on http://{} with 1 thread...", server.local_addr().unwrap() ); server.run().unwrap(); }
apimodule so no answerer will be able to actually compile and test your code so see if it's slow or fast. \$\endgroup\$