Performance serving static files with Rust and Hyper This seems to be working fine, but it performance testing with wrk is poor compared to what I've come up in other languages. Shouldn't Rust excel in serving static files?


How can I make this faster?
Are there any other potential problems here?




extern crate hyper;
extern crate futures;

use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::ffi::OsStr;
use hyper::StatusCode;
use futures::future::FutureResult;
use hyper::server::{Http, Service, Request, Response};

mod api;

struct Router;
impl Service for Router {
    type Request = Request;
    type Response = Response;
    type Error = hyper::Error;
    type Future = FutureResult&lt;Response, hyper::Error&gt;;

    fn call(&amp;self, req: Request) -&gt; Self::Future {
        futures::future::ok(match req.path() {
            "/json" =&gt; api::default::json(req),
            _ =&gt; static_file(req.path()),
        })
    }
}

fn main() {
    let addr = "127.0.0.1:5000".parse().unwrap();
    let server = Http::new().bind(&amp;addr, || Ok(Router)).unwrap();
    println!("Listening on http://{} with 1 thread...", server.local_addr().unwrap());
    server.run().unwrap();
}

pub fn static_file(path: &amp;str) -&gt; Response {
    let mut f = match File::open("src/static".to_owned() + path) {
        Ok(f) =&gt; f,
        Err(_) =&gt; {
            return Response::new().with_status(StatusCode::NotFound)
        },
    };

    let mut data = String::new();
    match f.read_to_string(&amp;mut data) {
        Ok(_) =&gt; {},
        Err(_) =&gt; {
            return Response::new().with_status(StatusCode::NotFound)
        },
    };

    let mut headers = hyper::header::Headers::new();
    match Path::new(&amp;path).extension().and_then(OsStr::to_str) {
        Some(ext) =&gt; match ext {
            "html" =&gt; headers.set_raw("Content-Type", "text/html"),
            "css" =&gt; headers.set_raw("Content-Type", "text/css"),
            "js" =&gt; headers.set_raw("Content-Type", "application/javascript"),
            _ =&gt; headers.set_raw("Content-Type", "text/plain"),
        },
        None =&gt; headers.set_raw("Content-Type", "text/plain"),
    };

    Response::new()
        .with_status(StatusCode::Ok)
        .with_headers(headers)
        .with_body(data)
}
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&lt;Future&lt;Item = Self::Response, Error = Self::Error&gt;&gt;;

    fn call(&amp;self, req: Request) -&gt; 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(&amp;mut data)?;
            Ok(data)
        });

        let mut headers = hyper::header::Headers::new();

        const UNKNOWN_CONTENT_TYPE: &amp;str = "text/plain";
        let content_type = match Path::new(req.path()).extension().and_then(OsStr::to_str) {
            Some(ext) =&gt; match ext {
                "html" =&gt; "text/html",
                "css" =&gt; "text/css",
                "js" =&gt; "application/javascript",
                _ =&gt; UNKNOWN_CONTENT_TYPE,
            },
            None =&gt; 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(&amp;addr, move || Ok(Arc::clone(&amp;router)))
        .unwrap();

    println!(
        "Listening on http://{} with 1 thread...",
        server.local_addr().unwrap()
    );
    server.run().unwrap();
}