Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Having compile time issues? Post here! #200

Closed
davidpdrsn opened this issue Aug 17, 2021 · 25 comments
Closed

Having compile time issues? Post here! #200

davidpdrsn opened this issue Aug 17, 2021 · 25 comments

Comments

@davidpdrsn
Copy link
Member

davidpdrsn commented Aug 17, 2021

Crates should not be slow to compile due to axum. If you're having compile issues please post a reproduction script here so we can fix it!

Note that compiles have been improved for the upcoming 0.2 release. If you're having issues try depending directly on main.

Known issues

These are the currently known compile time issues:

@sindreij
Copy link

sindreij commented Aug 20, 2021

I get really slow compile times when using boxed(). The compile-time is dependent on the number of routes, the more the worse the compile-time is.

In my real production-app, the compile time goes from 3 seconds to around a minute when using boxed.

I have a small reproduction here, where the effect is a bit less, but it still takes 10 seconds to compile (and 0.5 seconds without boxed).

https://github.com/sindreij/boxed-slow-axum

Update: Using main instead of 0.1.3 fixed it

Update: My bad, it's still slow. I have updated the example-code.

https://github.com/sindreij/boxed-slow-axum is now even simpler, and takes multiple minutes to compile on my computer (I stopped the process after 2.5 minutes). Looks like the time is exponential when increasing the number of routes. When not using boxed, it completes in under a second.

@paulzhang5511
Copy link

Every time i change the code, the compilation is very slow.

image

Example:

https://github.com/paulzhang5511/axum-compile-slow

@sindreij
Copy link

@davidpdrsn My bad, it's still slow. I have updated my original post.

@sindreij
Copy link

sindreij commented Aug 20, 2021

I also get a similar slowness when using nest also when the router is not boxed.

Example: https://github.com/sindreij/boxed-slow-axum/blob/nest/src/main.rs

@davidpdrsn
Copy link
Member Author

Compile times improve when we remove the trait bounds from the methods like route and nest. I'm gonna make a PR that does that later today.

However we cannot remove the bounds from boxed because they're necessary for boxing. So sadly nothing we can do there. Its a rustc bug apparently. I think we should investigate making a minimal reproduction script and filing an issue for rust.

@sindreij
Copy link

Okay, thanks. Until it's fixed, I work around it by defining all the routes in one place.

And btw, thanks for a great framework, it's really nice to use.

davidpdrsn added a commit that referenced this issue Aug 20, 2021
This improves compiles further when using lots of nested routes. Such as
the example posted
[here](#200 (comment)).

It seems rustc is really slow at checking bounds on these kinds of
intermediate builder methods. Should probably file an issue about that.
davidpdrsn added a commit that referenced this issue Aug 20, 2021
This improves compiles further when using lots of nested routes. Such as
the example posted
[here](#200 (comment)).

It seems rustc is really slow at checking bounds on these kinds of
intermediate builder methods. Should probably file an issue about that.
@davidpdrsn
Copy link
Member Author

@paulzhang5511 I just merged #220 which should help with your issue, if you remove .boxed() which is still problematic as mentioned above. Honestly not sure thats something we can fix in axum 😕

@ufoscout
Copy link

FYI, another workaround is also to declare all routes as .boxed().

E.g., this takes years to compile:

    let app = Router::new()
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a))
        .route("/a", get(get_a)).boxed();

and this is extremely fast:

    let app = Router::new()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed()
        .route("/a", get(get_a)).boxed();

BTW, I am not sure whether it has any runtime performance hit.

@davidpdrsn
Copy link
Member Author

BTW, I am not sure whether it has any runtime performance hit.

It will have some perf impact but exactly how big I don't know. That'd require benchmarking which I haven't done.

@nicrosengren
Copy link

After a great experience testing axum out by converting some of my warp based projects I ran into this problem with boxed(), and its a real blocker for me.

The problem is that boxed() is required for testing - since I need to be able to specify a type in order to get a Router against which I can run my tests.

Sorry if I'm missing something obvious here :)

@3olkin
Copy link
Contributor

3olkin commented Sep 7, 2021

@nicrgren you can add boxed() for every route. It s a bit annoying, but it remove compiler issue. And I don't feel that it's critically affect performance

@davidpdrsn
Copy link
Member Author

@nicrgren yes this is unfortunate. Here is a workaround:

use axum::{
    handler::{get, post},
    routing::BoxRoute,
    Json, Router,
};
use tower_http::trace::TraceLayer;

#[tokio::main]
async fn main() {
    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = std::net::TcpListener::bind(addr).unwrap();
    run(listener).await;
}

async fn run(listener: std::net::TcpListener) {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route(
            "/json",
            post(|payload: Json<serde_json::Value>| async move {
                Json(serde_json::json!({ "data": payload.0 }))
            }),
        )
        // We can still add middleware
        .layer(TraceLayer::new_for_http());

    let addr = listener.local_addr().unwrap();
    tracing::debug!("listening on {}", addr);

    axum::Server::from_tcp(listener)
        .unwrap()
        .serve(app.into_make_service())
        .await
        .unwrap();
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::{self, Request, StatusCode};
    use serde_json::{json, Value};
    use std::net::{SocketAddr, TcpListener};

    // You can also spawn a server and talk to it like any other HTTP server:
    #[tokio::test]
    async fn a_test() {
        let listener = TcpListener::bind("0.0.0.0:0".parse::<SocketAddr>().unwrap()).unwrap();
        let addr = listener.local_addr().unwrap();

        tokio::spawn(run(listener));

        let client = hyper::Client::new();

        let response = client
            .request(
                Request::builder()
                    .uri(format!("http://{}", addr))
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
        assert_eq!(&body[..], b"Hello, World!");
    }
}

@nicrosengren
Copy link

@z4rx Yeah, that's also a good solution. I'll try it out. Thanks for helping out!

@davidpdrsn thanks so much! I'll try it out

@sunli829
Copy link
Contributor

This code compiles very slowly. When the memory usage of rustc reaches 20G, I stop compiling.

    async fn handler() -> &'static str {
        "hello"
    }

    let service = axum::handler::get(
        handler
            .layer(axum::AddExtensionLayer::new(1))
            .layer(axum::AddExtensionLayer::new((1, 1)))
            .layer(axum::AddExtensionLayer::new((1, 1, 1)))
            .layer(axum::AddExtensionLayer::new((1, 1, 1, 1)))
            .layer(axum::AddExtensionLayer::new((1, 1, 1, 1, 1)))
            .layer(axum::AddExtensionLayer::new((1, 1, 1, 1, 1, 1)))
            .layer(axum::AddExtensionLayer::new((1, 1, 1, 1, 1, 1, 1)))
            .layer(axum::AddExtensionLayer::new((1, 1, 1, 1, 1, 1, 1, 1))),
    );

@davidpdrsn
Copy link
Member Author

@sunli829 good catch! Thats is worth investigating.

It works and compiles instantly using tower::ServiceBuilder (which I personally prefer for adding multiple middleware anyway):

Router::new().route(
    "/",
    get(handler.layer(
        ServiceBuilder::new()
            .layer(AddExtensionLayer::new(1))
            .layer(AddExtensionLayer::new((1, 1)))
            .layer(AddExtensionLayer::new((1, 1, 1)))
            .layer(AddExtensionLayer::new((1, 1, 1, 1)))
            .layer(AddExtensionLayer::new((1, 1, 1, 1, 1)))
            .layer(AddExtensionLayer::new((1, 1, 1, 1, 1, 1)))
            .layer(AddExtensionLayer::new((1, 1, 1, 1, 1, 1, 1)))
            .layer(AddExtensionLayer::new((1, 1, 1, 1, 1, 1, 1, 1)))
            .into_inner(),
    )),
);

This compiles in 0.2 seconds on my machine.

Would still be worth investing what you posted though.

@sunli829
Copy link
Contributor

This compiles in 0.2 seconds on my machine.

Would still be worth investing what you posted though.

Thank you for your answer. I realized that my rustc version is rustc 1.56.0-nightly. When I switched to rustc 1.54.0, the memory leak disappeared and the compilation was completed quickly. 😁

@davidpdrsn
Copy link
Member Author

I have found a workaround for the issues with boxed. If we create a layer applies the same middleware as boxed does it compiles quickly:

#![recursion_limit = "1024"]

use axum::{
    body::{box_body, Body, BoxBody, Bytes, HttpBody},
    handler::get,
    http::{Request, Response},
    routing::Layered,
    BoxError, Router,
};
use std::{marker::PhantomData, net::SocketAddr};
use tower::{buffer::Buffer, util::BoxService, Layer, Service, ServiceBuilder};
use tower_http::map_response_body::MapResponseBodyLayer;

#[tokio::main]
async fn main() {
    let app = my_app();

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

fn my_app() -> Router<Layered<BoxedService<Body>>> {
    Router::new()
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        // have the specify the request body type here
        // otherwise it takes forever to compile
        .layer(BoxLayer::<Body, _>::new())
}

// layer that applies that same middleware as `Router::boxed`
struct BoxLayer<ReqBody, ResBody> {
    _marker: PhantomData<(ReqBody, ResBody)>,
}

impl<ReqBody, ResBody> BoxLayer<ReqBody, ResBody> {
    fn new() -> Self {
        Self {
            _marker: PhantomData,
        }
    }
}

impl<S, ReqBody, ResBody> Layer<S> for BoxLayer<ReqBody, ResBody>
where
    S: Service<Request<ReqBody>, Response = Response<ResBody>> + Send + 'static,
    S::Error: Into<BoxError> + Send,
    S::Future: Send,
    ReqBody: Send + 'static,
    ResBody: HttpBody<Data = Bytes> + Send + Sync + 'static,
    ResBody::Error: Into<BoxError>,
{
    type Service = BoxedService<ReqBody>;

    fn layer(&self, inner: S) -> Self::Service {
        ServiceBuilder::new()
            .buffer(1024)
            .layer(BoxService::layer())
            .map_err(Into::into)
            .layer(MapResponseBodyLayer::new(box_body))
            .service(inner)
    }
}

type BoxedService<ReqBody> =
    Buffer<BoxService<Request<ReqBody>, Response<BoxBody>, BoxError>, Request<ReqBody>>;

@fluxxu
Copy link

fluxxu commented Oct 21, 2021

I have found a workaround for the issues with boxed. If we create a layer applies the same middleware as boxed does it compiles quickly:

#![recursion_limit = "1024"]

use axum::{
    body::{box_body, Body, BoxBody, Bytes, HttpBody},
    handler::get,
    http::{Request, Response},
    routing::Layered,
    BoxError, Router,
};
use std::{marker::PhantomData, net::SocketAddr};
use tower::{buffer::Buffer, util::BoxService, Layer, Service, ServiceBuilder};
use tower_http::map_response_body::MapResponseBodyLayer;

#[tokio::main]
async fn main() {
    let app = my_app();

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

fn my_app() -> Router<Layered<BoxedService<Body>>> {
    Router::new()
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        .route("/", get(|| async {}))
        // have the specify the request body type here
        // otherwise it takes forever to compile
        .layer(BoxLayer::<Body, _>::new())
}

// layer that applies that same middleware as `Router::boxed`
struct BoxLayer<ReqBody, ResBody> {
    _marker: PhantomData<(ReqBody, ResBody)>,
}

impl<ReqBody, ResBody> BoxLayer<ReqBody, ResBody> {
    fn new() -> Self {
        Self {
            _marker: PhantomData,
        }
    }
}

impl<S, ReqBody, ResBody> Layer<S> for BoxLayer<ReqBody, ResBody>
where
    S: Service<Request<ReqBody>, Response = Response<ResBody>> + Send + 'static,
    S::Error: Into<BoxError> + Send,
    S::Future: Send,
    ReqBody: Send + 'static,
    ResBody: HttpBody<Data = Bytes> + Send + Sync + 'static,
    ResBody::Error: Into<BoxError>,
{
    type Service = BoxedService<ReqBody>;

    fn layer(&self, inner: S) -> Self::Service {
        ServiceBuilder::new()
            .buffer(1024)
            .layer(BoxService::layer())
            .map_err(Into::into)
            .layer(MapResponseBodyLayer::new(box_body))
            .service(inner)
    }
}

type BoxedService<ReqBody> =
    Buffer<BoxService<Request<ReqBody>, Response<BoxBody>, BoxError>, Request<ReqBody>>;

@davidpdrsn
The workaround broke on rust 1.56.0
My project can be built on 1.55.0 in a reasonable time, but after upgrading to 1.56.0 today it was blocked forever.

@sondr3
Copy link

sondr3 commented Oct 21, 2021

I ran into the same problem of extremely slow compile times on 1.56, both sondr3/cv-aas and sondr3/web take forever to compile.

@davidpdrsn
Copy link
Member Author

Well alright. Rust giveth and then taketh away. I'll look into it.

Might have to go with #393.

@sondr3
Copy link

sondr3 commented Oct 21, 2021

Might be worth creating an issue in the Rust repo, going from a couple of seconds to build to a few minutes is a serious compile time regression imho.

@davidpdrsn
Copy link
Member Author

Yep we should do that. Just haven't had great luck with the issues we've filed already 🤷 You're welcome to file an issue and link it from here.

I'm afk the rest of the week but will look into it next week.

@dnaka91
Copy link

dnaka91 commented Oct 22, 2021

Having the same problem with my project. Even a simple cargo check never seems to complete.

For now I pinned it to rust 1.55 to get around it. You can reproduce it by switching to stable in the rust-toolchain.toml.

https://github.com/dnaka91/marmalade

@davidpdrsn
Copy link
Member Author

I believe #404 fixed all known compile time issues. All services are now boxed internally which makes the size of the router type constant so the type no longer grows as you add more routes.

It'll be shipped in 0.3 which is coming within a week or two 🤞

@davidpdrsn
Copy link
Member Author

I'll close this now since 0.3 is out and fixed all issues mentioned here. If someone still has compile time issues I'd very interested to hear about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants