Skip to content

Commit

Permalink
chore: more cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
zaida04 authored Jul 31, 2024
1 parent 6b97278 commit 1effe20
Showing 1 changed file with 7 additions and 7 deletions.
14 changes: 7 additions & 7 deletions src/content/posts/optimizing-rust-build.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ breakdown:

This is a simple multi-step Dockerfile that builds the Rust code in a Rust image and then copies the binary to an Alpine image. The build time for this Dockerfile was 238 seconds, which was unacceptable. The reason for this is that when you run `cargo build --release`, it builds all the dependencies (unless built before). However, since this step is tightly coupled with the source code building step, Docker can't cache our build dependencies. As a result, every time the source code changes, Docker has to rebuild all the dependencies, which is a huge waste of time.

After some investigation, I discovered that I could separate the install and dependency build steps from the source code build step using [cargo-chef](https://github.com/LukeMathWalker/cargo-chef). cargo-chef is a tool that analyzes your rust dependencies and spits out a recipe. This recipe is then used to install and build just your dependencies, completely decoupled from the source code building step. This way, Docker could cache the dependencies and only rebuild the source code when it changes. I ended up with the following Dockerfile.
After some investigation, I discovered that I could separate the install and dependency build steps from the source code build step using [cargo-chef](https://github.com/LukeMathWalker/cargo-chef). cargo-chef is a tool that analyzes your rust dependencies and spits out a recipe. This recipe (comparable to a package.json) is then used to install and build just your dependencies, completely decoupled from the source code building step. This way, Docker could cache the dependencies and only rebuild the source code when it changes. I ended up with the following Dockerfile.

### Using cargo-chef & alpine

Expand Down Expand Up @@ -100,8 +100,8 @@ CMD ["/app/adapter_runner"]

At first glance, this Dockerfile seems complex, but it can be broken down into three steps:

- Plan: This step analyzes the Cargo.toml file and generates a recipe for our dependencies. A recipe is comparable to a package.json in JavaScript, listing everything needed to build your project. This step is cached and only reruns when a dependency changes. By stubbing out the main.rs file, we can generate the recipe without copying all the code, which is important because we don't want to rerun this step every time the source code changes.
- Build: This step installs and builds the dependencies using the recipe generated in the previous step. It then copies the source code and builds it. On cached runs, this step will start at the COPY . . line, which is where the source code is copied. This is the big optimization that speeds up the build time.
- Plan: This step analyzes the Cargo.toml file and generates a recipe for our dependencies. This step is cached and only reruns when a dependency changes. By stubbing out the main.rs file, we can generate the recipe without copying all the code, which is important because we don't want to rerun this step every time the source code changes.
- Build: This step installs and builds the dependencies using the recipe generated in the previous step. It then copies the source code and builds it. On cached runs, this step will start at the `COPY . .` line, which is where the source code is copied. This is a big optimization that speeds up the build time.
- Run: This step copies the binary to an Alpine image and runs it.

Now, let's look at the results of this Dockerfile:
Expand All @@ -116,9 +116,9 @@ breakdown:

![Baseline vs v1 comparison](/imgs/build_time_comparison.png)

Wowza! The initial build time increased to 318.6 seconds. That's not good. However, the subsequent build time decreased to 95 seconds. This is because the initial build now includes the time to install cargo-chef and generate the recipe. The subsequent build time is significantly faster because the dependencies are cached, and only the source code is rebuilt. This is a huge improvement from the original 238 seconds.
Wowza! The initial build time increased to 318.6 seconds. That's not good. However, the subsequent build time decreased to 95 seconds. This is because the initial build now includes the time to install cargo-chef and generate the recipe. The subsequent build time is significantly faster because the dependencies are cached, and only the source code is rebuilt.

I decided to investigate why the initial build time was so high. I discovered two reasons: the time it took to install cargo-chef and the time it took to build the code. The first issue was easily solvable, as there was a cached image with cargo-chef installed, so I could just use that. I'll address the second point at the end of the article, where something surprising was discovered. For now, let's focus on further optimizing the Dockerfile.
I decided to investigate why the initial build time was so high. I discovered two reasons: the time it took to install cargo-chef and the time it took to run cargo build. The first issue was easily solvable, as there was a cached image available with cargo-chef installed. As for the second point, it got more complex. I'll talk about that at the end of the article. For now, let's focus on applying further optimizations.

### Using cargo-chef & slim-bookworm (final)

Expand Down Expand Up @@ -169,9 +169,9 @@ breakdown:

![Baseline vs v1 vs v2 comparison](/imgs/build_time_comparison_v2.png)

This is the final Dockerfile. The only changes I made were using a cached image with `cargo-chef` installed and switching from Alpine to slim-bookworm (Debian). But something unexpected happened: the time it took to run `cargo build --release` also decreased significantly. Why was that? It turns out that when using Alpine, you have to use musl over glibc. While musl is intended to be a lightweight alternative to glibc, it has performance issues with multi-core builds. [This article](https://www.tweag.io/blog/2023-08-10-rust-static-link-with-mimalloc/) and [this reddit post](https://www.reddit.com/r/rust/comments/gdycv8/why_does_musl_make_my_code_so_slow/) provide a deep dive into why this is the case (hint: it's the allocator). By switching over to slim-bookworm, we're back to using glibc, which is faster for multi-core builds.
This is the final Dockerfile. The only changes I made were using a cached image with `cargo-chef` installed and switching from Alpine to slim-bookworm (Debian). That removed the cargo-chef install, but something else also happened: the time it took to run `cargo build --release` also decreased significantly. Why was that? It turns out that when using Alpine, you have to use musl over glibc. While musl is intended to be a lightweight alternative to glibc, it has performance issues with multi-core builds. [This article](https://www.tweag.io/blog/2023-08-10-rust-static-link-with-mimalloc/) and [this reddit post](https://www.reddit.com/r/rust/comments/gdycv8/why_does_musl_make_my_code_so_slow/) provide a deep dive into why this is the case (hint: it's the allocator). By switching over to slim-bookworm, we're back to using glibc, which was faster in our case.

So, now we've achieved our final time. Looking at the stats, it's a clear improvement. On initial run, it's **65% faster than our baseline and 74% faster than `rust:alpine`**. On subsequent runs, it's **89.1% faster than our baseline and 72% faster than `rust:alpine`**. This is a huge improvement, and I'm happy with the results.
So, now we've achieved our final time. Looking at the stats, it's a clear improvement. On initial run, it's **65% faster than our baseline and 74% faster than `V1`**. On subsequent runs, it's **89.1% faster than our baseline and 72% faster than `V1`**. This is a huge improvement, and I'm happy with the results.

## Conclusion

Expand Down

0 comments on commit 1effe20

Please sign in to comment.