<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rust on Jon Seager</title><link>https://jnsgr.uk/tags/rust/</link><description>Recent content in Rust on Jon Seager</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 10 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jnsgr.uk/tags/rust/index.xml" rel="self" type="application/rss+xml"/><item><title>Brewlog: Coffee &amp; Agents</title><link>https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/</link><pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I’m really into speciality coffee. In July 2025 I started tracking my coffee habits with &lt;a href="https://roast.guide/" target="_blank" rel="noreferrer"&gt;Roastguide&lt;/a&gt;, a delightfully designed iOS app for people with a similar obsession to mine. It does a good job of tracking brews, roasters and bags, but over time I found myself wanting the data in a system that I controlled. I wanted to be able to query it, back it up, and adjust some of the flows to work better for how I brew and consume coffee.&lt;/p&gt;
&lt;p&gt;Despite my past scepticism of the previous generation of coding tools, recent developments have made them hard to ignore, and I wanted an excuse to build something substantial from scratch to get some experience.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://github.com/jnsgruk/brewlog" target="_blank" rel="noreferrer"&gt;b{rew}log&lt;/a&gt;, which is a self-hosted coffee logging platform. It tracks roasters, roasts, bags, brews, equipment, and cafe visits. It&amp;rsquo;s live at &lt;a href="https://coffee.jnsgr.uk" target="_blank" rel="noreferrer"&gt;coffee.jnsgr.uk&lt;/a&gt; if you want to poke around and witness the depths of my strange filter coffee obsession!&lt;/p&gt;
&lt;p&gt;This was by far the most complex project I&amp;rsquo;d built with &lt;a href="https://claude.com/product/claude-code" target="_blank" rel="noreferrer"&gt;Claude Code&lt;/a&gt;, or agentic coding tools in general. I&amp;rsquo;m responsible for the technology choices, architecture, and visual design, but I wrote almost none of the code myself.&lt;/p&gt;
&lt;p&gt;This post will cover the core features of Brewlog and how I designed the app, and finish with some observations and tips about agentic programming.&lt;/p&gt;
&lt;h2 id="core-features" class="relative group"&gt;Core Features &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#core-features" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Brewlog tracks roasters, roasts, bags, brews, equipment, and cafe visits. The landing page you see below summarises details of my last few brews, currently open bags of coffee and how much remains in each and some basic stats. When logged in, it provides quick controls to repeat a brew, or brew a particular coffee:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_445dd8a54a9a4ea8.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_7bd2a139c538b332.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_28dfa3482ef59c87.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_ceb4eac36acb5f30.webp 1130w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1130"
height="1682"
class="mx-auto my-0 rounded-md"
alt="brewlog landing page"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_a6bf0a68f91b6a7d.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_f4a29c4400db4876.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_a6bf0a68f91b6a7d.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_6d2fed53d5402f30.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01.png 1130w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Brews are coffees I’ve made myself. Each brew is logged against a specific bag and captures the recipe (dose, water, time, temperature), the equipment used, and tasting notes. Over time this will build up a history of what I&amp;rsquo;ve brewed, how I brewed it and how much I use the brewing gear I&amp;rsquo;ve accumulated over time. The image below shows the detail page for a specific brew:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_50bbb0b2b6963101.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_35a1e01fcdddafaf.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_29bb4eaed781336a.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_e31901b84c0d9203.webp 1130w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1130"
height="1340"
class="mx-auto my-0 rounded-md"
alt="detail of a specific brew in the application"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_efb40addffa8ed5e.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_2e550bb129e56e74.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_efb40addffa8ed5e.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_305a0558504abcb0.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04.png 1130w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Finally, brewlog tracks cafes I&amp;rsquo;ve visited, and &amp;ldquo;cups&amp;rdquo;, which are coffees I&amp;rsquo;ve enjoyed, but not brewed myself.&lt;/p&gt;
&lt;p&gt;The most fun part is the extensive stats page, which shows an interactive map of where my coffees are grown, roasted and drunk, as well as common flavour notes and brew times.&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_808a259ff87d6ed8.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_e2884e1516b66776.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_ac2b5c228a3d9507.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_de544f83159c15c5.webp 1130w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1130"
height="1682"
class="mx-auto my-0 rounded-md"
alt="coffee stats - map view and consumption numbers"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_12440ddec0cd6378.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_342f11bff0c05800.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_12440ddec0cd6378.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_2ce5ee59505317cb.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02.png 1130w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="llm-powered-bag-scanning" class="relative group"&gt;LLM-Powered Bag Scanning &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#llm-powered-bag-scanning" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Roastguide has a nice feature that allows you to take a picture of a bag of coffee and it&amp;rsquo;ll fetch the details. If my understanding is correct, their implementation relies on a database of roasters and coffees that the creators maintain. My app wouldn&amp;rsquo;t have access to that database, nor did I want to spend too much time building ingestion pipelines to store lots of data about all the possible coffees/roasters.&lt;/p&gt;
&lt;p&gt;In brewlog, I implemented this feature using LLM extraction: I take a photo and send it to a multi-modal model hosted by &lt;a href="https://openrouter.ai/" target="_blank" rel="noreferrer"&gt;OpenRouter&lt;/a&gt;. The &lt;a href="https://github.com/jnsgruk/brewlog/blob/edffb1679db3f17a8ed8e47735e8e1aff137e117/src/infrastructure/ai.rs#L11-L28" target="_blank" rel="noreferrer"&gt;prompt&lt;/a&gt; instructs the model to extract text from the image, perform a web search, and return a JSON object containing details of the coffee or roaster. OpenRouter enables me to switch models easily without worrying about API differences.&lt;/p&gt;
&lt;p&gt;In practice this works surprisingly well. Most speciality coffee bags are covered in useful information, and vision models are good at reading it. The main failure modes I see are mixing up roast names from the web and occasionally inventing tasting notes, but the process includes a review step which makes those cheap to correct.&lt;/p&gt;
&lt;p&gt;The cost is almost negligible per scan. I&amp;rsquo;ve been using &lt;a href="https://openrouter.ai/google/gemini-2.5-flash" target="_blank" rel="noreferrer"&gt;&lt;code&gt;google/gemini-2.5-flash&lt;/code&gt;&lt;/a&gt; for the LLM extraction feature, which results in a cost of around $0.01 - $0.02 per scan.&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_6001b8e6d92a665d.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_8b28c8a2ed6b8f2.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_3f53612e16cc5157.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_e04a32d09fe6bcd4.webp 1203w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1203"
height="969"
class="mx-auto my-0 rounded-md"
alt="adding a roaster with the option of fetching details using an llm with web search tool"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_c42d5bcb268c69d3.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_b8fcf892c9d1ebc0.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_c42d5bcb268c69d3.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_9b633d69b01861f.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03.png 1203w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="design-decisions" class="relative group"&gt;Design Decisions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#design-decisions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h3 id="backend" class="relative group"&gt;Backend &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#backend" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Brewlog is built with Rust and &lt;a href="https://github.com/tokio-rs/axum" target="_blank" rel="noreferrer"&gt;Axum&lt;/a&gt; for the backend. I&amp;rsquo;ve been &lt;a href="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/" target="_blank" rel="noreferrer"&gt;learning Rust&lt;/a&gt; for a while now, and wanted to push further with a more substantial project. Templates are handled by &lt;a href="https://github.com/djc/askama" target="_blank" rel="noreferrer"&gt;Askama&lt;/a&gt;, whose compile‑time templates worked well with Claude because template errors surface as Rust compiler errors, which are picked up and fixed by the agent automatically.&lt;/p&gt;
&lt;p&gt;The database is &lt;a href="https://www.sqlite.org/" target="_blank" rel="noreferrer"&gt;SQLite&lt;/a&gt;. A single file, easy to back up, easy to move around. For a single-user application like this, SQLite is more than sufficient and removes the need for a separate database server. &lt;a href="https://github.com/jnsgruk/brewlog/tree/main/migrations" target="_blank" rel="noreferrer"&gt;Migrations&lt;/a&gt; are embedded in the binary and &lt;a href="https://github.com/jnsgruk/brewlog/blob/edffb1679db3f17a8ed8e47735e8e1aff137e117/src/infrastructure/database.rs#L38" target="_blank" rel="noreferrer"&gt;run automatically&lt;/a&gt; on application startup.&lt;/p&gt;
&lt;h3 id="frontend" class="relative group"&gt;Frontend &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#frontend" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I wanted the app to feel modern, but keep much of the rendering server-side. I didn&amp;rsquo;t want to be serving a large client-side Javascript single-page application. I&amp;rsquo;ve been curious about the likes of &lt;a href="https://htmx.org/" target="_blank" rel="noreferrer"&gt;HTMX&lt;/a&gt;, and the observations made in a post from last year on &lt;a href="https://www.lorenstew.art/blog/eta-htmx-lit-stack" target="_blank" rel="noreferrer"&gt;building apps with Eta, HTMX and Lit&lt;/a&gt; really resonated with me.&lt;/p&gt;
&lt;p&gt;More recently I discovered &lt;a href="https://data-star.dev/" target="_blank" rel="noreferrer"&gt;Datastar&lt;/a&gt;, which has similar goals to &lt;a href="https://htmx.org/" target="_blank" rel="noreferrer"&gt;HTMX&lt;/a&gt;, but it&amp;rsquo;s newer and results in cleaner, simpler templates. Where HTMX swaps HTML fragments, Datastar adds a reactive signal system and can patch both HTML and JSON data from the server.&lt;/p&gt;
&lt;p&gt;This was a bit of a test for Claude Code, since Datastar was new enough that it likely didn&amp;rsquo;t feature in the training data for the model, but its ability to read and digest the documentation was quite startling.&lt;/p&gt;
&lt;p&gt;While occasionally the agent regressed to vanilla &lt;code&gt;fetch&lt;/code&gt; calls, or manual Javascript DOM manipulation, I treated that as a hole in my instructions, not the model. Each time it happened I &lt;a href="https://github.com/jnsgruk/brewlog/blob/main/CLAUDE.md#gotchas" target="_blank" rel="noreferrer"&gt;updated&lt;/a&gt; CLAUDE.md to reinforce the Datastar patterns and prevent the agent making the same mistake again.&lt;/p&gt;
&lt;h3 id="single-user-passkeys-only" class="relative group"&gt;Single User, Passkeys Only &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#single-user-passkeys-only" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Brewlog is deliberately single-user. I&amp;rsquo;m happy for people to self-host their own instance, but I don&amp;rsquo;t have much interest in providing a service more widely. The codebase is structured such that making it multi-user wouldn&amp;rsquo;t be a particularly complex task, but I&amp;rsquo;m not convinced I&amp;rsquo;ll ever do it.&lt;/p&gt;
&lt;p&gt;Perhaps unusually, authentication via username and password is not supported. Authentication is only supported using passkeys via &lt;a href="https://webauthn.io/" target="_blank" rel="noreferrer"&gt;WebAuthn&lt;/a&gt; or with an API token.&lt;/p&gt;
&lt;p&gt;Passkeys are phishing-resistant and significantly more difficult to brute-force both from an academic and a practical standpoint, which removes an entire class of abuse vector for my service.&lt;/p&gt;
&lt;h3 id="comprehensive-cli-client" class="relative group"&gt;Comprehensive CLI Client &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#comprehensive-cli-client" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;One of the many things I&amp;rsquo;ve learned from &lt;a href="https://github.com/niemeyer" target="_blank" rel="noreferrer"&gt;@niemeyer&lt;/a&gt; during my time at Canonical is to ensure that when new API endpoints are added to such applications, a corresponding CLI command or flag lands at the same time.&lt;/p&gt;
&lt;p&gt;He also illustrated to me the power of shipping a single binary that is both a server, and a client to itself. The brewlog binary runs the server that hosts the API and web UI, but also serves as a first-class API client for the app.&lt;/p&gt;
&lt;p&gt;If you’re building an API‑backed app, I highly recommend this pattern. It keeps the surface area small and gives you (and an agent&amp;hellip;) a first‑class, &lt;a href="https://github.com/jnsgruk/brewlog/blob/main/scripts/bootstrap-db.sh" target="_blank" rel="noreferrer"&gt;scriptable&lt;/a&gt; way to exercise the API and troubleshoot problems in data transformation.&lt;/p&gt;
&lt;h3 id="domain-driven-design" class="relative group"&gt;Domain-Driven Design &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#domain-driven-design" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The codebase follows a domain-driven design approach with four layers: domain (business logic only, no external dependencies), infrastructure (database, HTTP clients, third-party APIs), application (HTTP server, routes, middleware, services), and presentation (CLI commands and web view models):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌─────────────────────────────────────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Presentation │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌─────────────────────────┐ ┌──────────────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ CLI │ │ Web │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ roasters, roasts, bags │ │ views, templates │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ brews, cups, gear ... │ │ roasters, roasts, bags │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └─────────────────────────┘ └──────────────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────────────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Application │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Routes │ │ Services │ │ Server / State │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ /api/* │ │ BagService │ │ Axum router │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ /app/* │ │ BrewService │ │ AppState (DI) │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ │ │ RoastService .. │ │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └──────────────┘ └──────────────────┘ └─────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────────────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Domain (pure Rust — no framework dependencies) │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌──────────────────────┐ ┌─────────────────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Entities &amp;amp; Values │ │ Repository Traits │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ coffee/ roasters, │ │ trait RoasterRepository │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ roasts, bags, │ │ trait RoastRepository │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ brews, cups, gear │ │ trait BagRepository ... │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ auth/ users, │ │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ sessions, tokens │ │ Errors, IDs, Listing, │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ analytics/ │ │ Formatting │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └──────────────────────┘ └─────────────────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────────────────────────────┬──────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Infrastructure │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌────────────────────────────┐ │ ┌───────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Repositories (SQLite) │ │ │ External Clients │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ SqlRoasterRepository │ │ │ OpenRouter (AI) │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ SqlRoastRepository │ │ │ Foursquare │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ SqlBagRepository ... │ │ │ WebAuthn │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ (implement domain traits) │ │ │ Backup │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └────────────────────────────┘ │ └───────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────────────────────────────────┴──────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In this model, dependencies flow inward. The domain layer knows nothing about Axum, SQLite, or any other implementation detail. Repository traits are defined in the domain and implemented in the infrastructure layer. This creates flexibility in the future if, for example, I want to move to PostgreSQL rather than SQLite, or if I want to change how I store images, etc.&lt;/p&gt;
&lt;p&gt;It also simplifies testing. The domain-driven design approach encourages loose coupling and practices like dependency injection, which usually lead to simpler integration tests. For example, I could ask the agent for focused tests against pure domain logic without pulling in Axum or SQLite.&lt;/p&gt;
&lt;h2 id="agentic-coding-patterns" class="relative group"&gt;Agentic Coding Patterns &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#agentic-coding-patterns" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;A big motivation for this project was to learn some new technology and skills, and it certainly did that, but it also reinforced some patterns I&amp;rsquo;ve been using for a while now.&lt;/p&gt;
&lt;h3 id="pre-commit-hooks-as-a-contract" class="relative group"&gt;Pre-commit hooks as a contract &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#pre-commit-hooks-as-a-contract" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;ve never been too fond of &lt;a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks" target="_blank" rel="noreferrer"&gt;pre-commit hooks&lt;/a&gt;. I&amp;rsquo;ve always been disciplined in using formatters and linters in projects, but never enjoyed having them set up to run automatically. This changed when I started working with agents.&lt;/p&gt;
&lt;p&gt;I treat &lt;code&gt;pre‑commit&lt;/code&gt; as a contract between me and the agent. The agent is instructed to always fix linting/testing failures before returning to me for input or declaring success. In particular, I&amp;rsquo;ve been using &lt;a href="https://prek.j178.dev/" target="_blank" rel="noreferrer"&gt;prek&lt;/a&gt;, which is a Rust rewrite of &lt;a href="https://pre-commit.com/" target="_blank" rel="noreferrer"&gt;pre-commit&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="stricter-lints-for-agents" class="relative group"&gt;Stricter lints for agents &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#stricter-lints-for-agents" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;With agents, I&amp;rsquo;m also more inclined toward stricter, more pedantic lints to enforce properties like maximum line count in a function or file. When I&amp;rsquo;m writing code myself I make these changes instinctively. It&amp;rsquo;s obvious when a function becomes cumbersome when you&amp;rsquo;re editing by hand, but I noticed early on that an agent seems much more comfortable with 100+ line functions than I am.&lt;/p&gt;
&lt;p&gt;If you care about such constraints (function length, etc.), encode them as lints rather than hoping the agent internalises your preferences.&lt;/p&gt;
&lt;p&gt;As an aside, I&amp;rsquo;d never used &lt;a href="https://flake.parts" target="_blank" rel="noreferrer"&gt;flake-parts&lt;/a&gt; before, but its ability to automatically configure pre-commit hooks and formatting tools like &lt;a href="https://treefmt.com/latest/" target="_blank" rel="noreferrer"&gt;treefmt&lt;/a&gt; in a Nix dev shell is really slick.&lt;/p&gt;
&lt;h3 id="self-updating-instructions" class="relative group"&gt;Self-updating instructions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#self-updating-instructions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I hadn’t previously realised how good these tools are at writing their own instructions. I had been confused in the past at the length/complexity of &lt;code&gt;AGENTS.md&lt;/code&gt;/&lt;code&gt;CLAUDE.md&lt;/code&gt; files I&amp;rsquo;d seen, not realising that when you work with an agent to solve a problem, or remedy something it did, you can then prompt it to summarise what just happened in the &lt;code&gt;CLAUDE.md&lt;/code&gt; file to prevent it from happening again.&lt;/p&gt;
&lt;h3 id="plan-mode" class="relative group"&gt;Plan Mode &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#plan-mode" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;To begin with I made extensive use of &amp;ldquo;Plan Mode&amp;rdquo;. The premise is that the agent can explore the codebase, read docs online and work with you to plan new features, but it cannot make changes to the code or your system.&lt;/p&gt;
&lt;p&gt;This had been working well for me, but there were a couple of occasions where it seemed to get stuck in a &amp;ldquo;doom loop&amp;rdquo; trying to plan its way out of a complex failure.&lt;/p&gt;
&lt;p&gt;On one particular occasion, this lasted for over an hour. I then switched to &amp;ldquo;Agent Mode&amp;rdquo;, gave it a follow-up prompt that instructed it to inspect a running application on Kubernetes directly using &lt;code&gt;kubectl&lt;/code&gt;, and it was able to diagnose and solve the same problem in around 5 minutes. The problem here was that some &amp;ldquo;tools&amp;rdquo; are explicitly not permitted in &amp;ldquo;Plan Mode&amp;rdquo;, but depending on what you&amp;rsquo;re trying to achieve that can sometimes be a significant hindrance.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been favouring a different approach where I remain in &amp;ldquo;Agent Mode&amp;rdquo;, and prompt the agent that I&amp;rsquo;d like to work on a plan in a shared document (e.g. &lt;code&gt;./plans/new-feature.md&lt;/code&gt;), and that it may only edit that file until I say otherwise. I then work in a loop with the Agent where it writes a plan, I leave comments in-line in the file, and then it takes that into account - and repeat until I&amp;rsquo;m happy. Before asking the agent to implement the plan, I start a new session and ask it to implement the plan with fresh context. So far I&amp;rsquo;ve found this to be really effective.&lt;/p&gt;
&lt;h3 id="worktrees-and-parallel-agents" class="relative group"&gt;Worktrees and Parallel Agents &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#worktrees-and-parallel-agents" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Pretty soon after starting work on more complicated features, I became interested in running parallel agents. Sometimes an agent can take a long time to work through a well-developed, and in that time I often felt I could be working my way through other items on my roadmap.&lt;/p&gt;
&lt;p&gt;I initially tried to just run them concurrently on the same branch, but predictably they ended up fighting and making a bit of a mess. I&amp;rsquo;ve long been a fan of git &lt;a href="https://git-scm.com/docs/git-worktree" target="_blank" rel="noreferrer"&gt;worktrees&lt;/a&gt;, and they&amp;rsquo;re a perfect fit here - enabling me to run concurrent agents on separate instances of the codebase.&lt;/p&gt;
&lt;p&gt;Even with worktrees, I still tend to work on orthogonal concerns so I don&amp;rsquo;t end up with big merge conflicts, but being able to work on a frontend feature, a backend feature and the CI pipeline all at once is a nice upgrade.&lt;/p&gt;
&lt;h3 id="visual-prompting" class="relative group"&gt;Visual prompting &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#visual-prompting" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;When working on frontend components, I built a nice workflow around screenshotting the UI, marking it up with &lt;a href="https://flameshot.org/" target="_blank" rel="noreferrer"&gt;Flameshot&lt;/a&gt;, then pasting the image back into Claude Code with the next prompt.&lt;/p&gt;
&lt;p&gt;I found this to be a really effective way to drive iteration on web components. It felt closer to pair‑designing with a human: I’d circle spacing issues or misaligned elements, and the agent would propose CSS tweaks in response to what it saw.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I had a blast building Brewlog, and I&amp;rsquo;ve been using it every day for about a month now. It&amp;rsquo;s different from how I&amp;rsquo;ve approached projects in the past, in a way that&amp;rsquo;s both liberating and terrifying in equal measures.&lt;/p&gt;
&lt;p&gt;While some of the code in this project lacks the same attention to detail I might have applied, I likely would never have got around to building it without an agent.&lt;/p&gt;
&lt;p&gt;I was surprised what the process taught me. Every day I hacked on Brewlog with Claude Code, I learned something. Whether that was something about Tailwind CSS, or an architectural pattern.&lt;/p&gt;
&lt;p&gt;I think the key to useful output from these tools is that you remain the driver. You&amp;rsquo;re still responsible what you ship, which means giving the generated code the appropriate amount of review.&lt;/p&gt;
&lt;p&gt;Brewlog is live at &lt;a href="https://coffee.jnsgr.uk" target="_blank" rel="noreferrer"&gt;coffee.jnsgr.uk&lt;/a&gt; and runs comfortably on a free-tier instance on &lt;a href="https://fly.io/" target="_blank" rel="noreferrer"&gt;fly.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you’re into speciality coffee and like owning your data, give it a try and let me know how you get on.&lt;/p&gt;
&lt;p&gt;Finally, I&amp;rsquo;d like to thank the authors of Roastguide for the amazing work they&amp;rsquo;ve done on their app, the attention they pay to their community on Discord, and the fact that they were willing to get me a dump of my data I could use to populate the first deployment of Brewlog!&lt;/p&gt;</description></item><item><title>Adopting sudo-rs By Default in Ubuntu 25.10</title><link>https://jnsgr.uk/2025/05/adopting-sudo-rs-by-default-in-ubuntu/</link><pubDate>Tue, 06 May 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/05/adopting-sudo-rs-by-default-in-ubuntu/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/60583" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Following on from &lt;a href="https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/" target="_blank" rel="noreferrer"&gt;Carefully But Purposefully Oxidising Ubuntu&lt;/a&gt;, Ubuntu will be the first major Linux distribution to adopt &lt;code&gt;sudo-rs&lt;/code&gt; as the default implementation of &lt;code&gt;sudo&lt;/code&gt;, in partnership with the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The change will be effective from the release of Ubuntu 25.10. You can see the Trifecta Tech Foundation&amp;rsquo;s announcement &lt;a href="https://trifectatech.org/blog/memory-safe-sudo-to-become-the-default-in-ubuntu/" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-is-sudo-rs" class="relative group"&gt;What is &lt;code&gt;sudo-rs&lt;/code&gt;? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-is-sudo-rs" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;sudo-rs&lt;/code&gt; is a reimplementation of the traditional &lt;code&gt;sudo&lt;/code&gt; tool, written in Rust. It’s being developed by the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation (TTF)&lt;/a&gt;, a nonprofit focused on building secure, open source infrastructure components. The project is part of the Trifecta Tech Foundation&amp;rsquo;s &lt;a href="https://trifectatech.org/initiatives/privilege-boundary/" target="_blank" rel="noreferrer"&gt;Privilege Boundary initiative&lt;/a&gt;, which aims to handle privilege escalation with memory-safe alternatives.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo&lt;/code&gt; command has long served as the defacto means of privilege escalation on Linux. As described in the &lt;a href="https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/" target="_blank" rel="noreferrer"&gt;original post&lt;/a&gt;, Rust provides strong guarantees against certain classes of memory-safety issues, which is pivotal for components at the privilege boundary.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo-rs&lt;/code&gt; team is collaborating with &lt;a href="https://www.millert.dev/" target="_blank" rel="noreferrer"&gt;Todd Miller&lt;/a&gt;, who’s maintained the original &lt;code&gt;sudo&lt;/code&gt; for over thirty years. &lt;code&gt;sudo-rs&lt;/code&gt; should not be considered a fork in the road, but rather a handshake across generations of secure systems. Throughout the development of &lt;code&gt;sudo-rs&lt;/code&gt;, the TTF team have also made contributions to enhance the original &lt;code&gt;sudo&lt;/code&gt; implementation.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo-rs&lt;/code&gt; project is designed to be a drop in replacement for the original tool. For the vast majority of users, the upgrade should be completely transparent to their workflow. That said, &lt;code&gt;sudo-rs&lt;/code&gt; is a not a &amp;ldquo;blind&amp;rdquo; reimplementation. The developers are taking a &amp;ldquo;less is more&amp;rdquo; approach. This means that some features of the original &lt;code&gt;sudo&lt;/code&gt; may not be reimplemented if they serve only niche, or more recently considered &amp;ldquo;outdated&amp;rdquo; practices.&lt;/p&gt;
&lt;p&gt;Erik Jonkers, Chair of the Trifecta Tech Foundation explains:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While no piece of software - in any language - is flawless, we believe the transition to Rust in systems programming is a vital step forward, it is very exciting to see Ubuntu committing to &lt;code&gt;sudo-rs&lt;/code&gt; and taking the lead in moving the needle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="sponsoring-mainstream-adoption" class="relative group"&gt;Sponsoring Mainstream Adoption &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#sponsoring-mainstream-adoption" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Leading the mainstream adoption of a replacement to such a universally understood tool comes with responsibility. Before committing to ship &lt;code&gt;sudo-rs&lt;/code&gt; in Ubuntu 26.04 LTS, we&amp;rsquo;ll test the transition in Ubuntu 25.10. We&amp;rsquo;re also sponsoring the development of some specific items, which has manifested as &lt;a href="https://trifectatech.org/initiatives/workplans/sudo-rs/#current-work" target="_blank" rel="noreferrer"&gt;Milestone 5&lt;/a&gt; in the upstream project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Coarse-grained shell escape prevention (NOEXEC) on Linux (See &lt;a href="https://github.com/trifectatechfoundation/sudo-rs/pull/1073" target="_blank" rel="noreferrer"&gt;PR #1073&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The ability to control AppArmor profiles (First &lt;a href="https://github.com/trifectatechfoundation/sudo-rs/pull/1067" target="_blank" rel="noreferrer"&gt;PR #1067&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;sudoedit&lt;/code&gt; implementation&lt;/li&gt;
&lt;li&gt;Support for Linux Kernels older than version 5.9&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final item may seem out of place, but because Ubuntu 20.04 LTS is still in support, without this work there could be situations where &lt;code&gt;sudo&lt;/code&gt; fails to function if, for example, a 26.04 LTS OCI container was run on a 20.04 LTS host!&lt;/p&gt;
&lt;p&gt;The team have also already &lt;a href="https://github.com/trifectatechfoundation/sudo-rs/pull/1079" target="_blank" rel="noreferrer"&gt;begun work&lt;/a&gt; on ensuring that the test-suite is as compatible as possible with Ubuntu, to ensure any issues are caught early.&lt;/p&gt;
&lt;p&gt;This isn’t just about shipping a new binary. It’s about setting a direction. We&amp;rsquo;re not abandoning C, or even rewriting all the utilities ourselves, but by choosing to replace one of the most security-critical tools in the system with a memory-safe alternative, we&amp;rsquo;re making a statement: resilience and sustainability are not optional in the future of open infrastructure.&lt;/p&gt;
&lt;h2 id="progress-on-coreutils" class="relative group"&gt;Progress on &lt;code&gt;coreutils&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#progress-on-coreutils" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Since the initial announcement, we&amp;rsquo;ve been working hard to more clearly define a plan for the migration to uutils &lt;code&gt;coreutils&lt;/code&gt; in 25.10 and beyond. Similarly to our engagement with the Trifecta Tech Foundation, we&amp;rsquo;re also sponsoring the uutils project to ensure that some key gaps are closed before we ship 25.10. The sponsorship will primarily cover the development of SELinux support for common commands such as &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;The first step toward developing SELinux support was to &lt;a href="https://github.com/uutils/coreutils/pull/7440/files" target="_blank" rel="noreferrer"&gt;add support for automated testing in Github Actions&lt;/a&gt;, since then the maintainers have begun work on the actual implementation.&lt;/p&gt;
&lt;p&gt;The other feature we&amp;rsquo;re sponsoring is internationalisation support. At present, some of the utility implementations (such as &lt;code&gt;sort&lt;/code&gt;) have an incomplete understanding of locales, and therefore may yield unexpected results. We expect that these two features should land in time for us to ship in 25.10, and we&amp;rsquo;ll continue to work with the uutils project throughout the 26.04 LTS cycle to close any remaining gaps we identify in the interim release.&lt;/p&gt;
&lt;p&gt;One of the major concerns outlined in Julian&amp;rsquo;s post is about binary size. We&amp;rsquo;ve got a few tricks we can play here to get the size down, and there is already some conversation started &lt;a href="https://salsa.debian.org/rust-team/debcargo-conf/-/merge_requests/895" target="_blank" rel="noreferrer"&gt;upstream in Debian&lt;/a&gt; on how that might be achieved. There are also security implications, such as AppArmor’s lack of support for multi-call binaries. We’re currently working with the respective upstreams to discuss addressing this systematically, through in the interim we may need to build small wrapper binaries to enable compatibility with existing AppArmor profiles from the start.&lt;/p&gt;
&lt;h2 id="migration-mechanics" class="relative group"&gt;Migration Mechanics &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#migration-mechanics" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Julian Klode &lt;a href="https://discourse.ubuntu.com/t/migration-to-rust-coreutils-in-25-10/59708" target="_blank" rel="noreferrer"&gt;posted recently&lt;/a&gt; on the Ubuntu Discourse outlining the packaging plan that will enable us both to migrate transparently to uutils &lt;code&gt;coreutils&lt;/code&gt;, but also provide a convenient means for users to opt-out and switch back to GNU &lt;code&gt;coreutils&lt;/code&gt; if they wish, or if they identify a gap in the new implementation. I expect this will be rare, but we want to make sure it&amp;rsquo;s as easy as possible to revert, and will be documenting this in detail before release.&lt;/p&gt;
&lt;p&gt;Replacing coreutils isn&amp;rsquo;t as simple as swapping binaries. As an &lt;code&gt;Essential&lt;/code&gt; package, its replacement must work immediately upon unpacking without relying on maintainer scripts, and without conflicting files across packages. To solve this, we’re introducing new &lt;code&gt;coreutils-from-uutils&lt;/code&gt; and &lt;code&gt;coreutils-from-gnu&lt;/code&gt; packages, as well as &lt;code&gt;coreutils-from&lt;/code&gt; itself. For all the gory details, see the &lt;a href="https://discourse.ubuntu.com/t/migration-to-rust-coreutils-in-25-10/59708" target="_blank" rel="noreferrer"&gt;Discourse post&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;The packaging work required to switch to &lt;code&gt;sudo-rs&lt;/code&gt; is somewhat less complicated than with &lt;code&gt;coreutils&lt;/code&gt;. The package is already available in Ubuntu (which you can still test on Ubuntu 24.04, 24.10 and 25.04 with &lt;a href="https://github.com/jnsgruk/oxidizr" target="_blank" rel="noreferrer"&gt;oxidizr&lt;/a&gt;!), but unlike &lt;code&gt;coreutils&lt;/code&gt;, &lt;code&gt;sudo&lt;/code&gt; is not an &lt;code&gt;Essential&lt;/code&gt; package, so we&amp;rsquo;ll be able to make use of the Debian &lt;a href="https://wiki.debian.org/DebianAlternatives" target="_blank" rel="noreferrer"&gt;alternatives&lt;/a&gt; system for the transition.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Things are progressing nicely. We’ve established strong, productive relationships and are sponsoring work upstream to make these transitions viable.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve got a strategy for migrating the default implementation of &lt;code&gt;coreutils&lt;/code&gt; and &lt;code&gt;sudo&lt;/code&gt; in Ubuntu 25.10 which will enable a seamless revert in cases where that is desired. While &lt;code&gt;sudo-rs&lt;/code&gt; will be the default in 25.10, the original &lt;code&gt;sudo&lt;/code&gt; will remain available for users who need it, and we’ll be gathering feedback to ensure a smooth transition before the 26.04 LTS.&lt;/p&gt;
&lt;p&gt;Additionally, we&amp;rsquo;ve begun investigating the feasibility of providing &lt;a href="https://sequoia-pgp.org/" target="_blank" rel="noreferrer"&gt;SequoiaPGP&lt;/a&gt; and using it in APT instead of GnuPG. SequoiaPGP is a new OpenPGP library with a focus on safety and correctness, written in Rust. The GnuPG maintainers have recently forked the OpenPGP standard and are no longer compliant with it. Sequoia provides a modern alternative to GnuPG with strict behavior, and is already used in various other systems. More details to follow!&lt;/p&gt;
&lt;p&gt;Stay tuned!&lt;/p&gt;</description></item><item><title>Carefully But Purposefully Oxidising Ubuntu</title><link>https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/</link><pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/carefully-but-purposefully-oxidising-ubuntu/56995" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Last month I published &lt;a href="https://jnsgr.uk/2025/02/engineering-ubuntu-for-the-next-20-years/" target="_blank" rel="noreferrer"&gt;Engineering Ubuntu For The Next 20 Years&lt;/a&gt;, which outlines four key themes for how I intend to evolve Ubuntu in the coming years. In this post, I&amp;rsquo;ll focus on &amp;ldquo;Modernisation&amp;rdquo;. There are many areas we could look to modernise in Ubuntu: we could focus on the graphical shell experience, the virtualisation stack, core system utilities, default shell utilities, etc.&lt;/p&gt;
&lt;p&gt;Over the years, projects like GNU Coreutils have been instrumental in shaping the Unix-like experience that Ubuntu and other Linux distributions ship to millions of users. According to the GNU &lt;a href="https://www.gnu.org/software/coreutils/" target="_blank" rel="noreferrer"&gt;website&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The GNU Core Utilities are the basic file, shell and text manipulation utilities of the GNU operating system. These are the core utilities which are expected to exist on every operating system.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This package provides utilities which have become synonymous with Linux to many - the likes of &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, and &lt;code&gt;mv&lt;/code&gt;. In recent years, there has been an &lt;a href="https://uutils.github.io/" target="_blank" rel="noreferrer"&gt;effort&lt;/a&gt; to reimplement this suite of tools in Rust, with the goal of reaching 100% compatibility with the existing tools. Similar projects, like &lt;a href="https://github.com/trifectatechfoundation/sudo-rs" target="_blank" rel="noreferrer"&gt;sudo-rs&lt;/a&gt;, aim to replace key security-critical utilities with more modern, memory-safe alternatives.&lt;/p&gt;
&lt;p&gt;Starting with Ubuntu 25.10, my goal is to adopt some of these modern implementations as the default. My immediate goal is to make uutils&amp;rsquo; coreutils implementation the default in Ubuntu 25.10, and subsequently in our next Long Term Support (LTS) release, Ubuntu 26.04 LTS, if the conditions are right.&lt;/p&gt;
&lt;h2 id="but-why" class="relative group"&gt;But… why? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#but-why" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Performance is a frequently cited rationale for &amp;ldquo;Rewrite it in Rust&amp;rdquo; projects. While performance is high on my list of priorities, it&amp;rsquo;s not the primary driver behind this change. These utilities are at the heart of the distribution - and it&amp;rsquo;s the enhanced resilience and safety that is more easily achieved with Rust ports that are most attractive to me.&lt;/p&gt;
&lt;p&gt;The Rust language, its type system and its borrow checker (and its community!) work together to encourage developers to write safe, sound, resilient software. With added safety comes an increase in security guarantees, and with an increase in security comes an increase in overall resilience of the system - and where better to start than with the foundational tools that build the distribution?&lt;/p&gt;
&lt;p&gt;I recently read an &lt;a href="https://smallcultfollowing.com/babysteps/blog/2025/03/10/rust-2025-intro/" target="_blank" rel="noreferrer"&gt;article&lt;/a&gt; about targeting foundational software with Rust in 2025. Among other things, the article asserts that &amp;ldquo;foundational software needs performance, reliability — and productivity&amp;rdquo;. If foundational software fails, so do all of the other layers built on top. If foundational packages have performance bottlenecks, they become a floor on the performance achievable by the layers above.&lt;/p&gt;
&lt;p&gt;Ubuntu powers millions of devices around the world, from servers in your data centre, to safety critical systems in autonomous systems, so it behooves us to be absolutely certain we&amp;rsquo;re shipping the most resilient and trustworthy software we can.&lt;/p&gt;
&lt;p&gt;There are lots of ways to achieve this: we can provide &lt;a href="https://canonical.com/blog/12-year-lts-for-kubernetes" target="_blank" rel="noreferrer"&gt;long term support for projects like Kubernetes&lt;/a&gt;, we can &lt;a href="https://canonical.com/blog/canonicals-commitment-to-quality-management" target="_blank" rel="noreferrer"&gt;assure the code we write&lt;/a&gt;, and we can &lt;a href="https://canonical.com/blog/canonical-achieves-iso-21434-certification" target="_blank" rel="noreferrer"&gt;strive to achieve compliance with safety-centric standards&lt;/a&gt;, but another is by shipping software with the values of safety, soundness, correctness and resilience at their core.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not to throw shade on the existing implementations, of course. Many of these tools have been stable for many years, quietly improving performance and fixing bugs. A lovely side benefit of working on newer implementations, is that it &lt;a href="https://ferrous-systems.com/blog/testing-sudo-rs/" target="_blank" rel="noreferrer"&gt;sometimes facilitates&lt;/a&gt; improvements in the original upstream projects, too!&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve written about my desire to increase the number of Ubuntu contributors, and I think projects like this will help. Rust may present a steeper learning curve than C in some ways, but by providing such a strong framework around the use of memory it also lowers the chances that a contributor accidentally commits potentially unsafe code.&lt;/p&gt;
&lt;h2 id="introducing-oxidizr" class="relative group"&gt;Introducing &lt;code&gt;oxidizr&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introducing-oxidizr" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I did my homework before writing this post. I wanted to see how easy it was for me to live with these newer implementations and get a sense of their readiness for prime-time within the distribution. I also wanted a means of toggling between implementations so that I could easily switch back should I run into incompatibilities - and so &lt;a href="https://github.com/jnsgruk/oxidizr" target="_blank" rel="noreferrer"&gt;&lt;code&gt;oxidizr&lt;/code&gt;&lt;/a&gt; was born!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;oxidizr&lt;/code&gt; is a command-line utility for managing system experiments that replace traditional Unix utilities with modern Rust-based alternatives on Ubuntu systems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;oxidizr&lt;/code&gt; utility enables you to quickly swap in and out newer implementations of certain packages with &lt;em&gt;relatively&lt;/em&gt; low risk. It has the notion of &lt;em&gt;Experiments&lt;/em&gt;, where each experiment is a package that already exists in the archive that can be swapped in as an alternative to the default.&lt;/p&gt;
&lt;p&gt;Version &lt;a href="https://github.com/jnsgruk/oxidizr/releases/tag/v1.0.0" target="_blank" rel="noreferrer"&gt;1.0.0&lt;/a&gt; supports the following experiments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/uutils/coreutils" target="_blank" rel="noreferrer"&gt;uutils coreutils&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/uutils/findutils" target="_blank" rel="noreferrer"&gt;uutils findutils&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/uutils/diffutils" target="_blank" rel="noreferrer"&gt;uutils diffutils&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/trifectatechfoundation/sudo-rs" target="_blank" rel="noreferrer"&gt;sudo-rs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="how-does-it-work" class="relative group"&gt;How does it work? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#how-does-it-work" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Each experiment is subtly different since the paths of the utilities being replaced vary, but the process for enabling an experiment is generally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install the alternative package (e.g. &lt;code&gt;apt install rust-coreutils&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For each binary shipped in the new package:
&lt;ul&gt;
&lt;li&gt;Lookup the default path for that utility (e.g &lt;code&gt;which date&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Back up that file (e.g. &lt;code&gt;cp /usr/bin/date /usr/bin/.date.oxidizr.bak&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Symlink the new implementation in place (e.g. &lt;code&gt;ln -s /usr/bin/coreutils /usr/bin/date&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is also the facility to &amp;ldquo;disable&amp;rdquo; an experiment, which does the reverse of the sequence above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For each binary shipped in the new package:
&lt;ul&gt;
&lt;li&gt;Lookup the default path for the utility (e.g &lt;code&gt;which date&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Check for and restore any backed up versions (e.g &lt;code&gt;cp /usr/bin/.date.oxidizr.bak /usr/bin/date&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Uninstall the package (e.g. &lt;code&gt;apt remove rust-coreutils&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thereby returning the system back to its original state! The tool is covered by a suite of integration tests which illustrate this behaviour which you can find &lt;a href="https://github.com/jnsgruk/oxidizr/tree/ca955677b4f5549e5d7f06726f5c5cf1846fe448/tests" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="get-started" class="relative group"&gt;Get started &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#get-started" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;blockquote&gt;
&lt;p&gt;⚠️ WARNING ⚠️: &lt;code&gt;oxidizr&lt;/code&gt; is an experimental tool to play with alternatives to foundational system utilities. It may cause a loss of data, or prevent your system from booting, so use with caution!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are a couple of ways to get &lt;code&gt;oxidizr&lt;/code&gt; on your system. If you already use &lt;code&gt;cargo&lt;/code&gt;, you can do the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cargo install --git https://github.com/jnsgruk/oxidizr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Otherwise, you can download and install binary releases from &lt;a href="https://github.com/jnsgruk/oxidizr/releases" target="_blank" rel="noreferrer"&gt;Github&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Download version 1.0.0 and extract to /usr/bin/oxidizr&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sL &lt;span class="s2"&gt;&amp;#34;https://github.com/jnsgruk/oxidizr/releases/download/v1.0.0/oxidizr_Linux_&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;uname -m&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tar -xvzf - -C /usr/bin oxidizr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Once installed you can invoke &lt;code&gt;oxidizr&lt;/code&gt; to selectively enable/disable experiments. The default set of experiments in &lt;code&gt;v1.0.0&lt;/code&gt; is &lt;code&gt;rust-coreutils&lt;/code&gt; and &lt;code&gt;sudo-rs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Enable default experiments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Disable default experiments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr disable
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Enable just coreutils&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr &lt;span class="nb"&gt;enable&lt;/span&gt; --experiments coreutils
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Enable all experiments without prompting with debug logging enabled&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr &lt;span class="nb"&gt;enable&lt;/span&gt; --all --yes -v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Disable all experiments without prompting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr disable --all --yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The tool should work on all versions of Ubuntu after 24.04 LTS - though the &lt;code&gt;diffutils&lt;/code&gt; experiment is only available from Ubuntu 24.10 onward.&lt;/p&gt;
&lt;p&gt;The tool itself is stable and well covered with unit and integration tests, but nonetheless I&amp;rsquo;d urge you to start with a test virtual machine or a machine that &lt;em&gt;isn&amp;rsquo;t&lt;/em&gt; your production workstation or server! I&amp;rsquo;ve been running the &lt;code&gt;coreutils&lt;/code&gt; and &lt;code&gt;sudo-rs&lt;/code&gt; experiments for around 2 weeks now on my Ubuntu 24.10 machines and haven&amp;rsquo;t had many issues (more on that below…).&lt;/p&gt;
&lt;h2 id="how-to-help" class="relative group"&gt;How to Help &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#how-to-help" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re interested in helping out on this mission, then I&amp;rsquo;d encourage you to play with the packages, either by installing them yourself or using &lt;code&gt;oxidizr&lt;/code&gt;. Reply to the Discourse post with your experiences, file bugs and perhaps even dedicate some time to the relevant upstream projects to help with resolving bugs, implementing features or improving documentation, depending on your skill set.&lt;/p&gt;
&lt;p&gt;You can also join us to discuss on our &lt;a href="https://ubuntu.com/community/communications/matrix/onboarding" target="_blank" rel="noreferrer"&gt;Matrix instance&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="next-steps" class="relative group"&gt;Next Steps &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#next-steps" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Earlier this week, I met with &lt;a href="https://github.com/sylvestre" target="_blank" rel="noreferrer"&gt;@sylvestre&lt;/a&gt; to discuss my proposal to make uutils coreutils the default in Ubuntu 25.10. I was pleased to hear that he feels the project is ready for that level of exposure, so now we just need to work out the specifics. The Ubuntu Foundations team is already working up a plan for next cycle.&lt;/p&gt;
&lt;p&gt;There will certainly be a few rough edges we&amp;rsquo;ll need to work out. In my testing, for example, the only incompatibility I&amp;rsquo;ve come across is that the &lt;code&gt;update-initramfs&lt;/code&gt; script for Ubuntu uses &lt;code&gt;cp -Z&lt;/code&gt; to preserve &lt;code&gt;selinux&lt;/code&gt; labels when copying files. The &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt; and &lt;code&gt;ls&lt;/code&gt; commands from uutils &lt;a href="https://github.com/uutils/coreutils/issues/2404" target="_blank" rel="noreferrer"&gt;don&amp;rsquo;t yet support&lt;/a&gt; the &lt;code&gt;-Z&lt;/code&gt; flag, but I think we&amp;rsquo;ve worked out a way to unblock that work going forward, both in the upstream and in the next release of Ubuntu.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m going to do some more digging on &lt;a href="https://github.com/trifectatechfoundation/sudo-rs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;sudo-rs&lt;/code&gt;&lt;/a&gt; over the coming weeks, with a view to assessing a similar transition.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m really excited to see so much investment in the foundational utilities behind Linux. The uutils project seems to be picking up speed after their recent &lt;a href="https://fosdem.org/2025/schedule/event/fosdem-2025-6196-rewriting-the-future-of-the-linux-essential-packages-in-rust-/" target="_blank" rel="noreferrer"&gt;appearance at FOSDEM 2025&lt;/a&gt;, with efforts ongoing to rework &lt;a href="https://github.com/uutils/procps" target="_blank" rel="noreferrer"&gt;procps&lt;/a&gt;, &lt;a href="https://github.com/uutils/util-linux" target="_blank" rel="noreferrer"&gt;util-linux&lt;/a&gt; and more.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo-rs&lt;/code&gt; project is now maintained by the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation&lt;/a&gt;, who are focused on &amp;ldquo;open infrastructure software in the public interest&amp;rdquo; . Their &lt;a href="https://github.com/trifectatechfoundation/zlib-rs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;zlib-rs&lt;/code&gt;&lt;/a&gt; recently released v0.4.2, which appears to now be &lt;a href="https://trifectatech.org/blog/zlib-rs-is-faster-than-c/" target="_blank" rel="noreferrer"&gt;the fastest API-compatible zlib implementation&lt;/a&gt;. They&amp;rsquo;re also behind the &lt;a href="https://github.com/pendulum-project" target="_blank" rel="noreferrer"&gt;Pendulum Project&lt;/a&gt; and &lt;a href="https://github.com/pendulum-project/ntpd-rs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ntpd-rs&lt;/code&gt;&lt;/a&gt; for memory-safe time synchronisation.&lt;/p&gt;
&lt;p&gt;With Ubuntu, we&amp;rsquo;re in a position to drive awareness and adoption of these modern equivalents by making them either trivially available, or the default implementation for the world&amp;rsquo;s most deployed Linux distribution.&lt;/p&gt;
&lt;p&gt;We will need to do so carefully, and be willing to scale back on the ambition where appropriate to avoid diluting the promise of stability and reliability that the Ubuntu LTS releases have become known for, but I&amp;rsquo;m confident that we can make progress on these topics over the coming months.&lt;/p&gt;</description></item><item><title>Experimenting with Rust, Nix, K6 and Parca</title><link>https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/</link><pubDate>Sun, 01 Dec 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Over the past couple of weeks, I&amp;rsquo;ve been teaching myself &lt;a href="https://www.rust-lang.org/" target="_blank" rel="noreferrer"&gt;Rust&lt;/a&gt;. I don&amp;rsquo;t have a pressing need to write much Rust right now, but I&amp;rsquo;m intrigued by the promises of memory safety, and have been increasingly impressed at the quality of some of the software that the community produces. I also think that the concepts popularised by Rust, such as the &lt;a href="https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html" target="_blank" rel="noreferrer"&gt;borrow checker&lt;/a&gt;, will stick around in computing for many years to come and I&amp;rsquo;d like to have more hands-on experience with that.&lt;/p&gt;
&lt;p&gt;The Rust language also encourages the ideas of safety and soundness - sound code is (approximately) code that can&amp;rsquo;t cause memory corruption or exhibit undefined behaviour. You can read more in this excellent post &lt;a href="https://jacko.io/safety_and_soundness.html" target="_blank" rel="noreferrer"&gt;Safety and Soundness in Rust&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This blog post started out life as a post about Rust and my experience learning it, but I got interested in the performance of the server implementation I came up with and the post evolved into a post more about profiling and load testing!&lt;/p&gt;
&lt;h2 id="learning-rust" class="relative group"&gt;Learning Rust &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#learning-rust" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s been a little while since I dug into a new programming language, so I thought I&amp;rsquo;d mention how I went about it. In general I&amp;rsquo;m someone who learns best &amp;ldquo;by doing&amp;rdquo;. I normally try to read through some of the basic concepts, then jump to a project that will enable me to exercise them quite quickly. Fortunately, Rust has an &lt;em&gt;excellent&lt;/em&gt; official guide in the form of the &lt;a href="https://doc.rust-lang.org/book/" target="_blank" rel="noreferrer"&gt;Rust Book&lt;/a&gt;, which covers everything from the obligatory &lt;code&gt;Hello, World!&lt;/code&gt;, to concurrency, memory safety, publishing packages on &lt;a href="https://crates.io" target="_blank" rel="noreferrer"&gt;crates.io&lt;/a&gt; and more. I thoroughly recommend it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also picked up a copy of &lt;a href="https://rust-for-rustaceans.com/" target="_blank" rel="noreferrer"&gt;Rust for Rustaceans&lt;/a&gt; which was recommended by a couple of different colleagues - I intend to work through this next.&lt;/p&gt;
&lt;p&gt;So, after working through the Rust Book over the course of about a week in my spare time, I needed a project!&lt;/p&gt;
&lt;h2 id="rewriting-gosherve-in-rust" class="relative group"&gt;Rewriting &lt;code&gt;gosherve&lt;/code&gt; in Rust &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#rewriting-gosherve-in-rust" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In my &lt;a href="https://jnsgr.uk/2024/01/building-a-blog-with-go-nix-hugo/#serving-the-blog" target="_blank" rel="noreferrer"&gt;first post&lt;/a&gt; on this blog I talked about a small Go project I wrote several years ago named &lt;a href="https://github.com/jnsgruk/gosherve" target="_blank" rel="noreferrer"&gt;&lt;code&gt;gosherve&lt;/code&gt;&lt;/a&gt;. This was one of my first Go projects - a simple web server that can serve some static assets, and a set of short-links/redirects which are specified in a Github Gist. It&amp;rsquo;s been happily running my website for several years, and it felt like a small, but ambitious enough project for my first adventure into Rust - particuarly as over the years &lt;code&gt;gosherve&lt;/code&gt; has grown Prometheus &lt;a href="https://github.com/jnsgruk/gosherve/blob/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea/pkg/server/metrics.go" target="_blank" rel="noreferrer"&gt;metrics&lt;/a&gt; and the ability to serve assets from &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2/main.go#L24" target="_blank" rel="noreferrer"&gt;an embedded filesystem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As I anticipated, naming the new project was the hardest part. I landed on &lt;strong&gt;servy&lt;/strong&gt;, at least for now. I&amp;rsquo;m reasonably happy with &lt;a href="https://github.com/jnsgruk/servy" target="_blank" rel="noreferrer"&gt;the code&lt;/a&gt; - at the time of writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It can serve redirects&lt;/li&gt;
&lt;li&gt;It can serve embedded web assets&lt;/li&gt;
&lt;li&gt;It provides a metrics server on a separate port&lt;/li&gt;
&lt;li&gt;It has reasonable unit/integration test coverage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of the things I love about Go is the built-in ability to embed static assets into a binary through the &lt;code&gt;//go:embed&lt;/code&gt; directive, which gives you a pointer to an embedded filesystem. I was able to achieve a similar effect in Rust with &lt;a href="https://docs.rs/axum-embed/latest/axum_embed/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;axum-embed&lt;/code&gt;&lt;/a&gt;, which in turn builds upon &lt;a href="https://docs.rs/crate/rust-embed/latest" target="_blank" rel="noreferrer"&gt;&lt;code&gt;rust-embed&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I used Nix to create different variants of the build (i.e. a &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/servy.nix" target="_blank" rel="noreferrer"&gt;&amp;ldquo;vanilla&amp;rdquo;&lt;/a&gt; build, and one that serves my website by creating a &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/jnsgruk-content.nix" target="_blank" rel="noreferrer"&gt;derivation&lt;/a&gt; just for the web content, and another for the &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/jnsgruk.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;jnsgruk&lt;/code&gt; binary&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;You can run a copy of this website (frozen in time just before I wrote this post!) by running &lt;code&gt;nix run github:jnsgruk/servy#jnsgruk&lt;/code&gt;, or build a container for it by running &lt;code&gt;nix build github:jnsgruk/servy#jnsgruk-container&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I was expecting the Rust binary to be quite a lot smaller. I&amp;rsquo;m not sure why. The old (Go) binary weighs in at &lt;strong&gt;68MB&lt;/strong&gt;, where the new Rust binary comes in at &lt;strong&gt;67MB&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So I was right! I guess?! The result here is relatively uninteresting - a good chunk of both binaries is just the static assets (images!) that make up this site. At the time of writing the &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/jnsgruk-content.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;jnsgruk-content&lt;/code&gt;&lt;/a&gt; derivation evaluates at around 57MB - meaning there is 10MB and 9MB respectively for Go and Rust added by the &lt;em&gt;actual server code&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="basic-load-testing-with-k6" class="relative group"&gt;Basic Load Testing with &lt;code&gt;k6&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#basic-load-testing-with-k6" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been looking for an excuse to play with &lt;a href="https://k6.io/" target="_blank" rel="noreferrer"&gt;k6&lt;/a&gt; for a while. According to the &lt;a href="https://grafana.com/docs/k6/latest/" target="_blank" rel="noreferrer"&gt;documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Grafana k6 is an open-source, developer-friendly, and extensible load testing tool. k6 allows you to prevent performance issues and proactively improve reliability.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In very simple terms, to use &lt;code&gt;k6&lt;/code&gt; you define a script (in Javascript) that outlines a set of requests to make, their success criteria and (optionally) the strategy for ramping up load on the server. It has &lt;em&gt;many&lt;/em&gt; more features than I used for this project, but I was impressed with how simple it was to get started. I began by running the following (grabbing &lt;code&gt;k6&lt;/code&gt; from &lt;code&gt;nixpkgs&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start a shell with k6 available&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix shell github:NixOS/nixpkgs/nixos-unstable#k6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Init a new k6 script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ k6 new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized a new k6 &lt;span class="nb"&gt;test&lt;/span&gt; script in script.js. You can now execute it by running &lt;span class="sb"&gt;`&lt;/span&gt;k6 run script.js&lt;span class="sb"&gt;`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Now I just needed to work out what I wanted to test! I wanted a relatively simple test that requested a mix of web assets and redirects for a sustained period, to see how much throughput I could achieve with each of the server implementations. The template came with some sensible setup for the number of VUs (virtual users) and duration of the test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;k6&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;k6/http&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// A number specifying the number of VUs to run concurrently.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;vus&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// A string specifying the total duration of the test run.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;30s&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;VUs are an emulation of a user interacting with your service; each of them is an agent which will execute the test script. Each time the script is executed (by a VU), that&amp;rsquo;s known as an &amp;ldquo;iteration&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I wanted to test a variety of requests, so I first generated a list of valid URLs for my server. I used &lt;a href="https://github.com/edoardottt/cariddi" target="_blank" rel="noreferrer"&gt;&lt;code&gt;cariddi&lt;/code&gt;&lt;/a&gt; which is a web crawler written in Go, combined with &lt;code&gt;jq&lt;/code&gt; to create a list of valid paths in a JSON file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start a shell with k6 available&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix shell github:NixOS/nixpkgs/nixos-unstable#cariddi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run cariddi and generate the paths.json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ &lt;span class="nb"&gt;echo&lt;/span&gt; http://localhost:8080 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; cariddi -- -plain &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; cut -d&lt;span class="s2"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; -f4- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; jq -r -nR &lt;span class="s1"&gt;&amp;#39;[inputs | select(length&amp;gt;0)]&amp;#39;&lt;/span&gt; &amp;gt; paths.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Similarly I generated a list of valid redirects:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ curl -s &lt;span class="s2"&gt;&amp;#34;https://gist.githubusercontent.com/jnsgruk/b590f114af1b041eeeab3e7f6e9851b7/raw&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; cut -d&lt;span class="s2"&gt;&amp;#34; &amp;#34;&lt;/span&gt; -f1 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; jq -r -nR &lt;span class="s1"&gt;&amp;#39;[inputs | select(length&amp;gt;0)]&amp;#39;&lt;/span&gt; &amp;gt; redirects.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Now in my &lt;code&gt;k6&lt;/code&gt; script, I was able to read those files to create a list of URLs to &lt;a href="https://grafana.com/docs/k6/latest/javascript-api/k6-http/batch/" target="_blank" rel="noreferrer"&gt;batch&lt;/a&gt; &lt;code&gt;GET&lt;/code&gt; during the load test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Allow the host to be overridden by the &amp;#39;K6_HOST&amp;#39; env var.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;__ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;K6_HOST&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://localhost:8080&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Read the list of paths/redirects.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`./paths.json`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`./redirects.json`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Batch requests to redirects and static assets.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;GET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirects&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// For each response, ensure we get a 200/301/308/404
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// response - we shouldn&amp;#39;t see anything else.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m using a pretty naive &lt;a href="https://grafana.com/docs/k6/latest/using-k6/checks/" target="_blank" rel="noreferrer"&gt;check&lt;/a&gt; function here, though they can be expanded to check for other conditions in the response body, the size of the response, etc.&lt;/p&gt;
&lt;p&gt;You can see my final test script &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/script.js" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, I ran &lt;code&gt;k6&lt;/code&gt; and let it rip! This initial run was against &lt;code&gt;servy&lt;/code&gt;, running on my workstation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ k6 run script.js
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /&lt;span class="se"&gt;\ &lt;/span&gt; Grafana /‾‾/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /&lt;span class="se"&gt;\ &lt;/span&gt; / &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt; __ / /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / &lt;span class="se"&gt;\/&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;/ / / ‾‾&lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;‾&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / __________ &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;_&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\_\ &lt;/span&gt; &lt;span class="se"&gt;\_&lt;/span&gt;____/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; execution: &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; script: script.js
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; output: -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; scenarios: &lt;span class="o"&gt;(&lt;/span&gt;100.00%&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; scenario, &lt;span class="m"&gt;10&lt;/span&gt; max VUs, 1m0s max duration &lt;span class="o"&gt;(&lt;/span&gt;incl. graceful stop&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * default: &lt;span class="m"&gt;10&lt;/span&gt; looping VUs &lt;span class="k"&gt;for&lt;/span&gt; 30s &lt;span class="o"&gt;(&lt;/span&gt;gracefulStop: 30s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO&lt;span class="o"&gt;[&lt;/span&gt;0030&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;k6-reporter v2.3.0&lt;span class="o"&gt;]&lt;/span&gt; Generating HTML summary report &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;console
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ✓ status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; checks.........................: 100.00% ✓ &lt;span class="m"&gt;1331889&lt;/span&gt; ✗ &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data_received..................: &lt;span class="m"&gt;148&lt;/span&gt; GB 4.9 GB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data_sent......................: &lt;span class="m"&gt;163&lt;/span&gt; MB 5.4 MB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_blocked...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.41µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;390ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.72µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;17.3ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;3.6µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;4.91µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_connecting............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4ns &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;462.21µs p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_duration..............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;501.25µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.08µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;139.77µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.41ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;291.59µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;419.65µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;{&lt;/span&gt; expected_response:true &lt;span class="o"&gt;}&lt;/span&gt;...: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;501.25µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.08µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;139.77µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.41ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;291.59µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;419.65µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_failed................: 0.00% ✓ &lt;span class="m"&gt;0&lt;/span&gt; ✗ &lt;span class="m"&gt;1331889&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_receiving.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;379.39µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.13µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;29.07µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.23ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;108.31µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;177.05µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_sending...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.45µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;960ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.48µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.36ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;6.83µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;8.79µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_tls_handshaking.......: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_waiting...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;117.4µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33.65µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;98.19µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;23.03ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;176.34µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;225.09µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_reqs......................: &lt;span class="m"&gt;1331889&lt;/span&gt; 44333.219428/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; iteration_duration.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;38.08ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.92ms &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.46ms &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80.35ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;47.68ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;48.53ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; iterations.....................: &lt;span class="m"&gt;7881&lt;/span&gt; 262.326742/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; vus............................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; vus_max........................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;running &lt;span class="o"&gt;(&lt;/span&gt;0m30.0s&lt;span class="o"&gt;)&lt;/span&gt;, 00/10 VUs, &lt;span class="m"&gt;7881&lt;/span&gt; &lt;span class="nb"&gt;complete&lt;/span&gt; and &lt;span class="m"&gt;0&lt;/span&gt; interrupted iterations
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;default ✓ &lt;span class="o"&gt;[======================================]&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; VUs 30s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Nice! Between the 10 VUs we configured, &lt;code&gt;k6&lt;/code&gt; managed nearly 8000 iterations, and my new server responded with a total of 148GB of data!&lt;/p&gt;
&lt;h2 id="nix-ified-load-testing" class="relative group"&gt;Nix-ified Load Testing &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#nix-ified-load-testing" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Now I&amp;rsquo;d figured out the basics of &lt;code&gt;k6&lt;/code&gt;, I wanted to create some infrastructure that would allow me to run load tests in &lt;em&gt;consistent environments&lt;/em&gt; against both old and new implementations of my server.&lt;/p&gt;
&lt;p&gt;A couple of ideas came to mind here - the first of which was the NixOS &lt;a href="https://nixcademy.com/posts/nixos-integration-tests/" target="_blank" rel="noreferrer"&gt;integration test driver&lt;/a&gt;. I absolutely love this feature of Nix, and the scripts for interacting with the driver are nice and simple to maintain. One slight irritation in this case is that the test machines don&amp;rsquo;t have access to the internet - which is where the redirects map is fetched from in my precompiled server binaries. It&amp;rsquo;s certainly possible to get clever with a fake redirects server and some DNS shennanigans in the test machines, but I opted instead to build simple &lt;code&gt;nixosConfiguration&lt;/code&gt;s which could be started as VMs, similar to the approach taken in a &lt;a href="https://jnsgr.uk/2024/02/nixos-vms-in-github-actions/" target="_blank" rel="noreferrer"&gt;previous post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wanted to automate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The creation of a VM with both the Go and Rust versions of my server&lt;/li&gt;
&lt;li&gt;The generation of the &lt;code&gt;paths.json&lt;/code&gt; and &lt;code&gt;redirects.json&lt;/code&gt; files I created above&lt;/li&gt;
&lt;li&gt;Running the &lt;code&gt;k6&lt;/code&gt; load test&lt;/li&gt;
&lt;li&gt;Fetching the results from the load test&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="defining-test-vms" class="relative group"&gt;Defining Test VMs &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#defining-test-vms" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I created a new repository with a &lt;code&gt;flake.nix&lt;/code&gt; which took &lt;code&gt;nixpkgs&lt;/code&gt;, my &lt;code&gt;jnsgr.uk&lt;/code&gt; repo and my &lt;code&gt;servy&lt;/code&gt; repo as inputs, pinned to the latest revisions at the time of writing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;web server benchmarking flake&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:nixos/nixpkgs/nixpkgs-unstable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:jnsgruk/jnsgr.uk/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:jnsgruk/servy/71a337317defc779c6c55d486e20d104c5d478f2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Next up I needed a virtual machine definition, which I first defined as a &lt;code&gt;nixosConfiguration&lt;/code&gt; in the flake&amp;rsquo;s outputs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... inputs ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;forAllSystems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;genAttrs&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86_64-linux&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aarch64-linux&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# A minimal NixOS virtual machine which used for testing craft applications.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixosConfigurations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forAllSystems&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;benchvm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixosSystem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;specialArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="sr"&gt;./vm.nix&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The actual machine configuration lives in &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/vm.nix" target="_blank" rel="noreferrer"&gt;vm.nix&lt;/a&gt;. This is a standard NixOS configuration, with the addition of some elements that define the specs of the virtual machine used to boot it (cores, memory, disk, etc.). In this case the machine is configured with just a &lt;code&gt;root&lt;/code&gt; user, and the password &lt;code&gt;password&lt;/code&gt;. This is not an ideal setup from a security standpoint, but these VMs were only run on my workstation (behind NAT) for a short period of time and the plain text password eased the automation I&amp;rsquo;m about to describe. A more robust approach would have been to put my SSH public keys into the authorized keys definition for the user, but then you folks wouldn&amp;rsquo;t have been able to play along as easily!&lt;/p&gt;
&lt;p&gt;In this particular configuration, the VM is set up like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;virtualisation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;forwardPorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;host&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2222&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;guest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;memorySize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;diskSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;With that in place, we can now boot the VM (in the background):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix run .#nixosConfigurations.benchvm.config.system.build.vm -- --daemonize --display none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ ssh -p &lt;span class="m"&gt;2222&lt;/span&gt; root@localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The authenticity of host &lt;span class="s1"&gt;&amp;#39;[localhost]:2222 ([127.0.0.1]:2222)&amp;#39;&lt;/span&gt; can&lt;span class="s1"&gt;&amp;#39;t be established.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;ED25519 key fingerprint is SHA256:icnH3EQAzmjdfCkyPWFljQWaVSCaXdP2M+ekKXd0NlY.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;This key is not known by any other names.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;Warning: Permanently added &amp;#39;&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;localhost&lt;span class="o"&gt;]&lt;/span&gt;:2222&lt;span class="err"&gt;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;ED25519&lt;span class="o"&gt;)&lt;/span&gt; to the list of known hosts.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;root@localhost&lt;span class="o"&gt;)&lt;/span&gt; Password:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;root@benchvm:~&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="wrapping-k6" class="relative group"&gt;Wrapping &lt;code&gt;k6&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#wrapping-k6" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Now on to automating the test itself. When configuring the VM, I made sure to include a &lt;code&gt;systemd&lt;/code&gt; &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/vm.nix#L34-L56" target="_blank" rel="noreferrer"&gt;unit&lt;/a&gt; for each of the server implementations, so next I wanted to write a simple script that would execute the load test for each implementation and fetch the results.&lt;/p&gt;
&lt;p&gt;Before that, I modified the &lt;code&gt;k6&lt;/code&gt; script to not only output a report to &lt;code&gt;stdout&lt;/code&gt;, but also to a text file, a JSON file and a rendered HTML file (I didn&amp;rsquo;t know at the time which I&amp;rsquo;d prefer, and I wanted to survey the options!). I achieved this by defining the &lt;a href="https://grafana.com/docs/k6/latest/results-output/end-of-test/custom-summary/#about-handlesummary" target="_blank" rel="noreferrer"&gt;&lt;code&gt;handleSummary&lt;/code&gt;&lt;/a&gt; function &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/script.js#L31-L38" target="_blank" rel="noreferrer"&gt;in the &lt;code&gt;k6&lt;/code&gt; script&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;summary.json&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;summary.html&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;htmlReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;summary.txt&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;textSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enableColors&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;textSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enableColors&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;\n\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;First I scripted the generation of the paths and the execution of the test script, and packaged the combination up as a Nix package for installation into the test machine. This is just a &lt;code&gt;bash&lt;/code&gt; &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/k6test" target="_blank" rel="noreferrer"&gt;script&lt;/a&gt; which ensures only the specified implementation is running (with &lt;code&gt;systemd&lt;/code&gt;), generates the list of URLs to test and then runs &lt;code&gt;k6&lt;/code&gt; against the server. That script is packaged as a &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L70-L94" target="_blank" rel="noreferrer"&gt;Nix package&lt;/a&gt; called &lt;code&gt;k6test&lt;/code&gt; which contains both my &lt;code&gt;bash&lt;/code&gt; script and the &lt;code&gt;k6&lt;/code&gt; test script; the Nix package is then &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/vm.nix#L58" target="_blank" rel="noreferrer"&gt;added to the VM configuration&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="automating-test-execution" class="relative group"&gt;Automating Test Execution &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#automating-test-execution" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I borrowed the &lt;a href="https://github.com/jnsgruk/crafts-flake/blob/51025f3c4ea463644935dae8434f82a606a56742/test/vm-exec" target="_blank" rel="noreferrer"&gt;&lt;code&gt;vm-exec&lt;/code&gt;&lt;/a&gt; script from a past project, renaming it &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/benchvm-exec" target="_blank" rel="noreferrer"&gt;&lt;code&gt;benchvm-exec&lt;/code&gt;&lt;/a&gt;, and created a new script called &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/benchvm-test" target="_blank" rel="noreferrer"&gt;&lt;code&gt;benchvm-test&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;\e[92m[&lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;\e[0m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Set some SSH options to ignore host key errors and make logging quieter.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This is a bad idea in general, but here is used to faciliate comms with&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# a brand new VM each time.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;-P &lt;span class="m"&gt;2222&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;#34;UserKnownHostsFile=/dev/null&amp;#34;&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;#34;StrictHostKeyChecking=no&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info &lt;span class="s2"&gt;&amp;#34;Running k6test against &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;benchvm-exec k6test &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info &lt;span class="s2"&gt;&amp;#34;Collecting results files&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sshpass -ppassword scp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; root@localhost:summary.json &lt;span class="s2"&gt;&amp;#34;summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sshpass -ppassword scp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; root@localhost:summary.html &lt;span class="s2"&gt;&amp;#34;summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sshpass -ppassword scp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; root@localhost:summary.txt &lt;span class="s2"&gt;&amp;#34;summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info &lt;span class="s2"&gt;&amp;#34;Results available in summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt, summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json and summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This was the final piece of the puzzle for now; after building &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L60-L67" target="_blank" rel="noreferrer"&gt;and packaging&lt;/a&gt; this script and &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L50" target="_blank" rel="noreferrer"&gt;defining the test VM as a package&lt;/a&gt; in the flake, I&amp;rsquo;d enabled the following workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the devShell for the server-bench project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix develop github:jnsgruk/server-bench
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Build &amp;amp; run the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ run-benchvm-vm --daemonize --display none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start, and load test the gosherve based server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-test jnsgruk-go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start, and load test the Rust based server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-test jnsgruk-rust
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# All done, power down the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-exec poweroff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Excellent! Now on to some actual testing!&lt;/p&gt;
&lt;h2 id="initial-load-test-results" class="relative group"&gt;Initial Load Test Results &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#initial-load-test-results" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Having seen the results for &lt;code&gt;servy&lt;/code&gt; earlier, I started up the existing &lt;code&gt;gosherve&lt;/code&gt; based server and ran the same test, only to discover quite a delta in the results. Where &lt;code&gt;servy&lt;/code&gt; managed 4.9 GB/s throughout the test, &lt;code&gt;gosherve&lt;/code&gt; only achieved 697 MB/s (sending 21GB in total). The full results are in the details box below, but overall it managed much lower performance across the board on the measurements that &lt;code&gt;k6&lt;/code&gt; makes.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Initial &lt;code&gt;gosherve&lt;/code&gt; load-test results&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 189449 ✗ 0
data_received..................: 21 GB 697 MB/s
data_sent......................: 26 MB 848 kB/s
http_req_blocked...............: avg=2.02µs min=400ns med=1.42µs max=19.76ms p(90)=2.29µs p(95)=2.77µs
http_req_connecting............: avg=203ns min=0s med=0s max=19.52ms p(90)=0s p(95)=0s
http_req_duration..............: avg=9.3ms min=29.49µs med=9.36ms max=152.39ms p(90)=13.16ms p(95)=14.76ms
{ expected_response:true }...: avg=9.3ms min=29.49µs med=9.36ms max=152.39ms p(90)=13.16ms p(95)=14.76ms
http_req_failed................: 0.00% ✓ 0 ✗ 189449
http_req_receiving.............: avg=106.56µs min=4.88µs med=48.7µs max=6.01ms p(90)=215.06µs p(95)=381.38µs
http_req_sending...............: avg=4.81µs min=950ns med=3.97µs max=19.71ms p(90)=5.93µs p(95)=6.98µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=9.19ms min=22.85µs med=9.26ms max=152.2ms p(90)=12.96ms p(95)=14.51ms
http_reqs......................: 189449 6281.256789/s
iteration_duration.............: avg=268.14ms min=176.88ms med=250.61ms max=433.23ms p(90)=334.14ms p(95)=342.1ms
iterations.....................: 1121 37.1672/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;I was quite surprised by this: my expectation was that the Go HTTP server would outperform what I&amp;rsquo;d put together in Rust. I decided to look a little deeper and see if I could figure out why, or at least why the delta was so big.&lt;/p&gt;
&lt;h2 id="profiling-gosherve-with-parca" class="relative group"&gt;Profiling &lt;code&gt;gosherve&lt;/code&gt; with Parca &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#profiling-gosherve-with-parca" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For last couple of years, I&amp;rsquo;ve been playing with &lt;a href="https://parca.dev" target="_blank" rel="noreferrer"&gt;Parca&lt;/a&gt;, which is a continuous profiling tool written in Go by the folks at Polar Signals. I wrote about it back in 2022 &lt;a href="https://discourse.charmhub.io/t/continuous-profiling-for-juju-parca-on-machines-and-kubernetes/6815" target="_blank" rel="noreferrer"&gt;on Charmhub&lt;/a&gt;, demonstrating how Parca could be used for profiling applications deployed with Juju.&lt;/p&gt;
&lt;h3 id="instrumenting-gosherve" class="relative group"&gt;Instrumenting &lt;code&gt;gosherve&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#instrumenting-gosherve" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;There are two components to the open source Parca offering - the server backend, and a zero-instrumentation, eBPF-based &lt;a href="https://github.com/parca-dev/parca-agent" target="_blank" rel="noreferrer"&gt;agent&lt;/a&gt; which can be used for on-CPU profiling of any workload. Because &lt;code&gt;gosherve&lt;/code&gt; is Go based, I didn&amp;rsquo;t need the agent so long as I enabled the &lt;code&gt;pprof&lt;/code&gt; endpoint in my server, which is trivial for almost any Go application. I took &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2/main.go" target="_blank" rel="noreferrer"&gt;&lt;code&gt;main.go&lt;/code&gt;&lt;/a&gt; and applied the following changes to ensure that I could hit the &lt;code&gt;pprof&lt;/code&gt; endpoints on port &lt;code&gt;6060&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;diff --git a/main.go b/main.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;index 964ceb2..374c440 100644
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;--- a/main.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+++ b/main.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;@@ -10,6 +10,9 @@ import (
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;log/slog&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;os&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;#34;net/http&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ _ &amp;#34;net/http/pprof&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;github.com/jnsgruk/gosherve/pkg/logging&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;github.com/jnsgruk/gosherve/pkg/server&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;@@ -29,6 +32,11 @@ func main() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flag.Parse()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; logging.SetupLogger(*logLevel)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ go func() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ http.ListenAndServe(&amp;#34;:6060&amp;#34;, nil)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ slog.Info(&amp;#34;pprof server started on port 6060&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ }()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Create an fs.FS from the embedded filesystem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; fsys, err := fs.Sub(publicFS, &amp;#34;public&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if err != nil {
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Next I fired up Parca (having &lt;a href="https://github.com/NixOS/nixpkgs/pull/359635" target="_blank" rel="noreferrer"&gt;first packaged it&lt;/a&gt; for NixOS 😉) and &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/parca/parca.yaml#L11" target="_blank" rel="noreferrer"&gt;configured it&lt;/a&gt; to scrape the new &lt;code&gt;jnsgruk&lt;/code&gt; binary while I ran the same test against it, which resulted in the following profile (which you can &lt;a href="https://pprof.me/20cdda5097bf379ffd282679e29ee32b" target="_blank" rel="noreferrer"&gt;explore yourself on pprof.me&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_b7f370c9787a6a87.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_2e044e16f34c8a1.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_e14618bcf0a96fe4.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_96ae66342724a104.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1702"
height="1799"
class="mx-auto my-0 rounded-md"
alt="pre-optimisation icicle graph representing the cpu profile of gosherve during a load test"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_8faa6e00b9e7decd.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_76aacc9b7f044124.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_8faa6e00b9e7decd.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_cbe2acf47a023708.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_a0e7f911c48d6eb7.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Looking at this, we can see that around a third of the time was spent in garbage collection. This seemed high; it was more prominent on this run than others but it was persistently a high portion of the CPU time. Something to loop back to! However, inside the route handler itself there are two things that stood out to me:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_b4b345b2f13dbb92.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_dcc48e6ec09cb8c5.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_80b24a836ba222c6.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_266d93fd1f999e77.webp 1193w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1193"
height="979"
class="mx-auto my-0 rounded-md"
alt="zoomed in cpu profile showing activity in the route handler"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_7c530c6ab68fe57c.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_f2a831e1bc1b1a5e.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_7c530c6ab68fe57c.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_8bbbb86555338ad5.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02.png 1193w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="initial-optimisations" class="relative group"&gt;Initial optimisations &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#initial-optimisations" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;In the red box, you can see about a third of the time spent in the route handler was in creating SHA1 sums, which is happening &lt;a href="https://github.com/jnsgruk/gosherve/blob/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea/pkg/server/route_handler.go#L70" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt; to calculate the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag" target="_blank" rel="noreferrer"&gt;ETag&lt;/a&gt; for content before serving it. This could probably be optimised - either by changing how the ETag is calculated (i.e. not hashing the whole file contents), or by caching ETags in memory, particularly given that in the case of my website, &lt;code&gt;gosherve&lt;/code&gt; only serves assets which are embedded (read only) in the binary, so the ETag will never change for a given file for a particular compiled binary.&lt;/p&gt;
&lt;p&gt;In the blue box, we can see that another third of the time spent is on refreshing the redirects. This seemed strange; the redirects don&amp;rsquo;t change throughout the life of the test, and we&amp;rsquo;re only requesting known redirects from our &lt;code&gt;redirects.json&lt;/code&gt; file. This seemed much more likely to be a culprit in the poor load test results, because unlike calculating a SHA1 sum (which is almost entirely CPU-bound), this function is making an outbound (TLS) connection to Github and potentially blocking on IO before parsing the response, rechecking the redirects map for a match, etc.&lt;/p&gt;
&lt;p&gt;As I looked at the code, I noticed an obvious (and somewhat embarrassing!) mistake I&amp;rsquo;d made when implementing &lt;code&gt;gosherve&lt;/code&gt;. Because the server tries to parse redirects &lt;em&gt;before&lt;/em&gt; looking for files with a matching path, every request for a file causes the redirects map to be updated from its upstream source 🤦. Looking back at the new &lt;code&gt;servy&lt;/code&gt; implementation, I hadn&amp;rsquo;t made the same mistake - perhaps this was the reason the Rust version was &lt;em&gt;so much faster&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;I made a &lt;a href="https://github.com/jnsgruk/gosherve/commit/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea" target="_blank" rel="noreferrer"&gt;small change&lt;/a&gt; to &lt;code&gt;gosherve&lt;/code&gt; and re-ran the test, which resulted in &lt;a href="https://pprof.me/e216e90fb0176ffbf11e7fac23b99682" target="_blank" rel="noreferrer"&gt;this profile&lt;/a&gt; and quite a substantial bump in request performance!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checks.........................: 100.00% ✓ &lt;span class="m"&gt;913783&lt;/span&gt; ✗ &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_received..................: &lt;span class="m"&gt;101&lt;/span&gt; GB 3.4 GB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_sent......................: &lt;span class="m"&gt;123&lt;/span&gt; MB 4.1 MB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_blocked...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.21µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;390ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.12µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;14.12ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.19µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;5.7µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_connecting............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;70ns &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;14.09ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_duration..............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.8ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.12µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;899.13µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;31.31ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.86ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;6.45ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;{&lt;/span&gt; expected_response:true &lt;span class="o"&gt;}&lt;/span&gt;...: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.8ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.12µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;899.13µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;31.31ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.86ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;6.45ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_failed................: 0.00% ✓ &lt;span class="m"&gt;0&lt;/span&gt; ✗ &lt;span class="m"&gt;913783&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_receiving.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;261.88µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.85µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;63.59µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;17.8ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;620.15µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;1.31ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_sending...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.21µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.05µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5.21µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.73ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;9.87µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;13.07µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_tls_handshaking.......: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_waiting...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.53ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30.81µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;656.71µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30.76ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.38ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;5.91ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_reqs......................: &lt;span class="m"&gt;913783&lt;/span&gt; 30435.133157/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iteration_duration.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;55.5ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;28.86ms &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;55.18ms &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;77.07ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;61.02ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;62.98ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iterations.....................: &lt;span class="m"&gt;5407&lt;/span&gt; 180.089545/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus............................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus_max........................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The overall throughput increased by five times, with a decrease in latency across the board. Still around a third less overall throughput than the Rust server, but a huge improvement nonetheless.&lt;/p&gt;
&lt;p&gt;I subsequently experimented with (naively) removing the ETag calculation to see whether or not it was worth implementing some caching - but it actually resulted in very little difference in throughput and CPU utilisation. Besides, while the calculation takes some CPU time, in a real-life deployment it reduces the overall load on the server by ensuring that &lt;code&gt;http.ServeContent&lt;/code&gt; only sends actual content when there is a new version, relying more heavily on the user&amp;rsquo;s browser cache. My &lt;code&gt;k6&lt;/code&gt; tests (deliberately) weren&amp;rsquo;t sending the right headers (such as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match" target="_blank" rel="noreferrer"&gt;&lt;code&gt;If-None-Match&lt;/code&gt;&lt;/a&gt;) to benefit from the behaviour one might expect from a browser, and it was just requesting the same files over and over again with the equivalent of a cold/empty cache.&lt;/p&gt;
&lt;h3 id="reducing-allocations" class="relative group"&gt;Reducing Allocations &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#reducing-allocations" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Now back to all those allocations, and the time spent in garbage collection as a result&amp;hellip; &lt;code&gt;gosherve&lt;/code&gt; was spending about a third of it&amp;rsquo;s time in the &lt;code&gt;io.ReadFile&lt;/code&gt; function, and given the amount of time spent in garbage collection, any optimisation made to the number of allocations on the heap would likely yield another big performance increase.&lt;/p&gt;
&lt;p&gt;The problem lied in the &lt;a href="https://github.com/jnsgruk/gosherve/blob/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea/pkg/server/route_handler.go#L63-L67" target="_blank" rel="noreferrer"&gt;&lt;code&gt;routeHandler&lt;/code&gt;&lt;/a&gt;, where I was reading the entire contents of each file that was being served into memory. &lt;code&gt;fs.ReadFile&lt;/code&gt; makes an allocation the size of the file it&amp;rsquo;s reading, meaning the contents of the file end up on the heap. In a situation like our load test - this means the entire contents of my website ended up on the heap, and the Go runtime was busily garbage collecting to clean up.&lt;/p&gt;
&lt;p&gt;I looked around at alternative ways to implement the same functionality in a more efficient manner. In Go 1.22, &lt;a href="https://pkg.go.dev/net/http@master#ServeFileFS" target="_blank" rel="noreferrer"&gt;&lt;code&gt;http.ServeFileFS&lt;/code&gt;&lt;/a&gt; was introduced as a counterpart to &lt;a href="https://pkg.go.dev/net/http@master#ServeFile" target="_blank" rel="noreferrer"&gt;&lt;code&gt;http.ServeFile&lt;/code&gt;&lt;/a&gt;. &lt;code&gt;http.ServeFileFS&lt;/code&gt; operates on an &lt;code&gt;fs.FS&lt;/code&gt; rather than the host filesystem. Under the hood, rather than reading the whole file into memory &lt;code&gt;ServeFileFS&lt;/code&gt; is using &lt;code&gt;io.Copy&lt;/code&gt; and thus &lt;a href="https://cs.opensource.google/go/go/&amp;#43;/refs/tags/go1.23.3:src/io/io.go;l=407" target="_blank" rel="noreferrer"&gt;&lt;code&gt;io.copyBuffer&lt;/code&gt;&lt;/a&gt; which allocates a single buffer that fits on the stack - fewer allocations means less memory usage, and less time spent occupying cores with garbage collection!&lt;/p&gt;
&lt;p&gt;The change to &lt;code&gt;gosherve&lt;/code&gt; was pretty &lt;a href="https://github.com/jnsgruk/gosherve/commit/c5647476a8c6bcdf703ef685809ad06007774b6f" target="_blank" rel="noreferrer"&gt;simple&lt;/a&gt; - you can see a slightly shortened diff below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;diff --git a/pkg/server/route_handler.go b/pkg/server/route_handler.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;index 2417e45..e0c9d68 100644
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;--- a/pkg/server/route_handler.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+++ b/pkg/server/route_handler.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;@@ -1,7 +1,6 @@
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // snip!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- // Try reading the file and return early if that fails
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- b, err := fs.ReadFile(*s.webroot, filepath)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- if err != nil {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- return false
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;-
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; w.Header().Set(&amp;#34;Cache-Control&amp;#34;, &amp;#34;public, max-age=31536000, must-revalidate&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- w.Header().Set(&amp;#34;ETag&amp;#34;, fmt.Sprintf(`&amp;#34;%d-%x&amp;#34;`, len(b), sha1.Sum(b)))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ w.Header().Set(&amp;#34;ETag&amp;#34;, fmt.Sprintf(`&amp;#34;%s-%d-%x&amp;#34;`, fi.Name(), fi.Size(), sha1.Sum([]byte(fi.ModTime().String()))))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- http.ServeContent(w, r, filepath, time.Now(), bytes.NewReader(b))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ http.ServeFileFS(w, r, *s.webroot, filepath)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s.metrics.responseStatus.WithLabelValues(strconv.Itoa(http.StatusOK)).Inc()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; l.Info(&amp;#34;served file&amp;#34;, slog.Group(&amp;#34;response&amp;#34;, &amp;#34;status_code&amp;#34;, http.StatusOK, &amp;#34;file&amp;#34;, filepath))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This change also necessitated a change to ETag calculation, which now only hashes the filename, size and modified time. There is a slight oddity in that files from an embedded filesystem have their timestamps all fixed (to avoid changes in static files messing with build reproducibility), but since the files cannot change during a binary&amp;rsquo;s lifetime, the ETag calculated from the modified timestamp is still just as valid.&lt;/p&gt;
&lt;p&gt;I quickly made a new build of &lt;code&gt;jnsgruk&lt;/code&gt; using a &lt;a href="https://go.dev/ref/mod#go-mod-file-replace" target="_blank" rel="noreferrer"&gt;locally referenced&lt;/a&gt; copy of &lt;code&gt;gosherve&lt;/code&gt; and ran &lt;code&gt;k6&lt;/code&gt;/&lt;code&gt;parca&lt;/code&gt; to check the outcome. You can explore the profile yourself &lt;a href="https://pprof.me/d28a5d25a34c4ed742cf397e72b43ed3" target="_blank" rel="noreferrer"&gt;on pprof.me&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_3dad8a8a81768f62.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_babadf94790b33ba.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_8c779802f200a61b.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_d6176e1511be897b.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1574"
height="1068"
class="mx-auto my-0 rounded-md"
alt="zoomed in cpu profile showing activity in the route handler post optimisation, without all the extra allocations"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_233c4dbdf63307de.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_ecff087adc597bc6.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_233c4dbdf63307de.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_734ef44999d7406d.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_7cd9bc9067fe5458.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here you can clearly see that the runtime is spending &lt;em&gt;much&lt;/em&gt; less time in garbage collection, and the call to &lt;code&gt;io.Copy&lt;/code&gt;/&lt;code&gt;io.copyBuffer&lt;/code&gt; without the calls to &lt;code&gt;fs.ReadFile&lt;/code&gt;. It&amp;rsquo;s now also much harder to spot the ETag generation - likely because we&amp;rsquo;re now only hashing a small string, rather than the whole file contents.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;k6&lt;/code&gt; results came back as follows 🚀:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checks.........................: 100.00% ✓ &lt;span class="m"&gt;1525300&lt;/span&gt; ✗ &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_received..................: &lt;span class="m"&gt;170&lt;/span&gt; GB 5.7 GB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_sent......................: &lt;span class="m"&gt;207&lt;/span&gt; MB 6.9 MB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_blocked...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;410ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.36µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;18.47ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;5.02µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;6.77µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_connecting............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;40ns &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;17.98ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_duration..............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;999.6µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.17µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;645.03µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24.22ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;2.34ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;3.09ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;{&lt;/span&gt; expected_response:true &lt;span class="o"&gt;}&lt;/span&gt;...: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;999.6µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.17µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;645.03µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24.22ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;2.34ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;3.09ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_failed................: 0.00% ✓ &lt;span class="m"&gt;0&lt;/span&gt; ✗ &lt;span class="m"&gt;1525300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_receiving.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;245.43µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.64µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;75.29µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;23.48ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;626.08µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;1.18ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_sending...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9.69µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5.49µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;19.54ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;10.86µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;14.46µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_tls_handshaking.......: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_waiting...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;744.47µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;28.09µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;395.81µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20.62ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;1.89ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;2.58ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_reqs......................: &lt;span class="m"&gt;1525300&lt;/span&gt; 50823.211595/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iteration_duration.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;34.41ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;11.91ms &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;34.44ms &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;67.75ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;37.6ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;38.61ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iterations.....................: &lt;span class="m"&gt;8716&lt;/span&gt; 290.418352/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus............................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus_max........................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The Go server is now in the lead! There isn&amp;rsquo;t a huge margin here between the Rust server and the Go server, but this is about the sort of difference I was expecting based on the reading I&amp;rsquo;d done before doing the testing.&lt;/p&gt;
&lt;h2 id="profiling-servy" class="relative group"&gt;Profiling &lt;code&gt;servy&lt;/code&gt;? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#profiling-servy" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I &lt;em&gt;did&lt;/em&gt; profile &lt;code&gt;servy&lt;/code&gt; using &lt;code&gt;parca-agent&lt;/code&gt;, but I&amp;rsquo;ve yet to look at the results in detail. The profile is a lot more complex (see below), partially as a result of how much work is going on under the hood when standing up an &lt;code&gt;axum&lt;/code&gt; server with &lt;code&gt;hyper&lt;/code&gt;, &lt;code&gt;tower&lt;/code&gt; and &lt;code&gt;tokio&lt;/code&gt;. I&amp;rsquo;m going to spend some more time with this profile over the coming weeks, but you can explore it for yourself &lt;a href="https://pprof.me/e6e8ad8c98d4c1406363312a9cec09b1/?color_by=filename" target="_blank" rel="noreferrer"&gt;on pprof.me&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_aaa7fb6101f64508.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_b6b418958567b5cd.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_4c2117bf11075aff.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_f6be7bd277688eed.webp 1297w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1297"
height="1936"
class="mx-auto my-0 rounded-md"
alt="crazy cpu profile for servy with lots of densely packed information on screen"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_69070aebbb6abb3d.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_9950977b539af752.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_69070aebbb6abb3d.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_393a8846648a767.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05.png 1297w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you do play with the profile on &lt;code&gt;pprof.me&lt;/code&gt;, try filtering by function name and entering &lt;code&gt;servy&lt;/code&gt;, and you&amp;rsquo;ll see the parts of the code I wrote highlighted on the profile to explore:&lt;/p&gt;
&lt;p&gt;&lt;a href="06.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_dd08b26f6f7a1751.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_323dfdfd62326307.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_1e109ba191282b1b.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_29b75dc9a5be1e09.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1387"
height="943"
class="mx-auto my-0 rounded-md"
alt="filtering by function name on pprof.me"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_e987abf4a0772495.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_6d5aa0cf511d0dd1.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_e987abf4a0772495.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_cb45d31250cb9e89.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_873da1b8aa292b52.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By the time you read this, the &lt;a href="https://github.com/NixOS/nixpkgs/pull/360132" target="_blank" rel="noreferrer"&gt;&lt;code&gt;parca-agent&lt;/code&gt; should be upstream&lt;/a&gt; into &lt;code&gt;nixpkgs&lt;/code&gt;, but in the mean time you can pull the package I included in the &lt;code&gt;server-bench&lt;/code&gt; repo and use that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the devShell for the server-bench project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix develop github:jnsgruk/server-bench
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo parca-agent -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --remote-store-address&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;grpc.polarsignals.com:443&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --remote-store-bearer-token&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --node&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or, on Ubuntu (see polarsignals.com/docs/setup-collection-snaps)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo snap install --classic parca-agent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In my Nix example above, I&amp;rsquo;m sending profiles to &lt;a href="https://www.polarsignals.com/" target="_blank" rel="noreferrer"&gt;Polar Signals Cloud&lt;/a&gt;, but you can also set the &lt;code&gt;remote-store-address&lt;/code&gt; to the address of a locally running &lt;code&gt;parca&lt;/code&gt; instance no problem.&lt;/p&gt;
&lt;p&gt;Depending on the language you&amp;rsquo;re profiling, you&amp;rsquo;ll either need to include debug info in the binary you&amp;rsquo;re profiling (which is what I did), or upload your source/debug info manually. There are more instructions for that &lt;a href="https://www.polarsignals.com/docs/rust" target="_blank" rel="noreferrer"&gt;in the docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="final-results--comparisons" class="relative group"&gt;Final Results &amp;amp; Comparisons &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#final-results--comparisons" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I wanted to gather some test results from all three targets across a number of different machine configurations; my website runs on a teeny-tiny &lt;a href="https://fly.io" target="_blank" rel="noreferrer"&gt;Fly.io&lt;/a&gt; instance with 1 vCPU and 256MB RAM, so I was interested to see how performance changed on machines with varying configurations. I&amp;rsquo;ve included a summary in the table below which highlights just the &lt;code&gt;data_received&lt;/code&gt; measurement from each test, and you can see the full reports from &lt;code&gt;k6&lt;/code&gt; by expanding the details box:&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Expand for full `k6` reports&lt;/summary&gt;
&lt;details&gt;
&lt;summary&gt;1 vCPU/256MB RAM/1 VUs - &lt;code&gt;gosherve&lt;/code&gt; (old)&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 23322 ✗ 0
data_received..................: 2.6 GB 86 MB/s
data_sent......................: 3.1 MB 105 kB/s
http_req_blocked...............: avg=2.41µs min=400ns med=1.28µs max=1.04ms p(90)=1.87µs p(95)=2.27µs
http_req_connecting............: avg=50ns min=0s med=0s max=508.13µs p(90)=0s p(95)=0s
http_req_duration..............: avg=7.57ms min=49.54µs med=7.71ms max=143.13ms p(90)=9.42ms p(95)=10.65ms
{ expected_response:true }...: avg=7.57ms min=49.54µs med=7.71ms max=143.13ms p(90)=9.42ms p(95)=10.65ms
http_req_failed................: 0.00% ✓ 0 ✗ 23322
http_req_receiving.............: avg=98.03µs min=4.46µs med=57.25µs max=7.69ms p(90)=177.19µs p(95)=250.6µs
http_req_sending...............: avg=5.7µs min=970ns med=3.1µs max=480.07µs p(90)=4.79µs p(95)=9.26µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=7.47ms min=37.26µs med=7.62ms max=142.9ms p(90)=9.24ms p(95)=10.33ms
http_reqs......................: 23322 776.881114/s
iteration_duration.............: avg=217.52ms min=187.06ms med=198.66ms max=359.52ms p(90)=276.89ms p(95)=280.19ms
iterations.....................: 138 4.59693/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;1 vCPU/256MB RAM/1 VUs - &lt;code&gt;gosherve&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 158691 ✗ 0
data_received..................: 18 GB 587 MB/s
data_sent......................: 21 MB 714 kB/s
http_req_blocked...............: avg=1.89µs min=379ns med=1.08µs max=1.15ms p(90)=1.62µs p(95)=1.98µs
http_req_connecting............: avg=11ns min=0s med=0s max=655.05µs p(90)=0s p(95)=0s
http_req_duration..............: avg=1.04ms min=48.25µs med=537.57µs max=31.33ms p(90)=2.63ms p(95)=3.66ms
{ expected_response:true }...: avg=1.04ms min=48.25µs med=537.57µs max=31.33ms p(90)=2.63ms p(95)=3.66ms
http_req_failed................: 0.00% ✓ 0 ✗ 158691
http_req_receiving.............: avg=91.12µs min=3.66µs med=20.8µs max=23.05ms p(90)=156.29µs p(95)=287.17µs
http_req_sending...............: avg=6.47µs min=900ns med=2.57µs max=2.22ms p(90)=4.06µs p(95)=6.21µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=949.02µs min=35.88µs med=466.14µs max=31.3ms p(90)=2.46ms p(95)=3.47ms
http_reqs......................: 158691 5287.057691/s
iteration_duration.............: avg=31.95ms min=21.16ms med=30.39ms max=107.46ms p(90)=36.19ms p(95)=41.18ms
iterations.....................: 939 31.284365/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;1 vCPU/256MB RAM/1 VUs - &lt;code&gt;servy&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 148213 ✗ 0
data_received..................: 16 GB 547 MB/s
data_sent......................: 18 MB 605 kB/s
http_req_blocked...............: avg=1.5µs min=350ns med=971ns max=1.11ms p(90)=1.48µs p(95)=1.82µs
http_req_connecting............: avg=11ns min=0s med=0s max=451.97µs p(90)=0s p(95)=0s
http_req_duration..............: avg=928.79µs min=69.84µs med=671.38µs max=55.59ms p(90)=1.57ms p(95)=2.18ms
{ expected_response:true }...: avg=928.79µs min=69.84µs med=671.38µs max=55.59ms p(90)=1.57ms p(95)=2.18ms
http_req_failed................: 0.00% ✓ 0 ✗ 148213
http_req_receiving.............: avg=136.98µs min=3.76µs med=17.14µs max=46.31ms p(90)=103.7µs p(95)=192.52µs
http_req_sending...............: avg=5.33µs min=890ns med=2.35µs max=2.99ms p(90)=3.69µs p(95)=5.05µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=786.47µs min=55.89µs med=629.47µs max=17.92ms p(90)=1.43ms p(95)=1.96ms
http_reqs......................: 148213 4940.080272/s
iteration_duration.............: avg=34.19ms min=16.57ms med=25.68ms max=109.45ms p(90)=55.08ms p(95)=57.83ms
iterations.....................: 877 29.231244/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;2 vCPU/4GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt; (old)&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 116272 ✗ 0
data_received..................: 13 GB 426 MB/s
data_sent......................: 16 MB 518 kB/s
http_req_blocked...............: avg=2.25µs min=380ns med=1.06µs max=4.3ms p(90)=1.69µs p(95)=2.05µs
http_req_connecting............: avg=475ns min=0s med=0s max=4.27ms p(90)=0s p(95)=0s
http_req_duration..............: avg=15.37ms min=29.8µs med=14.28ms max=145.82ms p(90)=24.79ms p(95)=29.78ms
{ expected_response:true }...: avg=15.37ms min=29.8µs med=14.28ms max=145.82ms p(90)=24.79ms p(95)=29.78ms
http_req_failed................: 0.00% ✓ 0 ✗ 116272
http_req_receiving.............: avg=174.41µs min=4.15µs med=28.84µs max=21.95ms p(90)=241.72µs p(95)=703.97µs
http_req_sending...............: avg=7.22µs min=890ns med=2.98µs max=5.58ms p(90)=5.8µs p(95)=9.15µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=15.19ms min=22.98µs med=14.11ms max=144.5ms p(90)=24.48ms p(95)=29.37ms
http_reqs......................: 116272 3837.442876/s
iteration_duration.............: avg=439.41ms min=323.27ms med=430.04ms max=567.74ms p(90)=505.87ms p(95)=516.96ms
iterations.....................: 688 22.706763/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;2 vCPU/4GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 362167 ✗ 0
data_received..................: 40 GB 1.3 GB/s
data_sent......................: 49 MB 1.6 MB/s
http_req_blocked...............: avg=2.23µs min=360ns med=770ns max=12.31ms p(90)=1.34µs p(95)=1.66µs
http_req_connecting............: avg=117ns min=0s med=0s max=1.84ms p(90)=0s p(95)=0s
http_req_duration..............: avg=4.7ms min=32.38µs med=3.94ms max=47.81ms p(90)=9.04ms p(95)=11.63ms
{ expected_response:true }...: avg=4.7ms min=32.38µs med=3.94ms max=47.81ms p(90)=9.04ms p(95)=11.63ms
http_req_failed................: 0.00% ✓ 0 ✗ 362167
http_req_receiving.............: avg=196.28µs min=3.38µs med=15.42µs max=25.62ms p(90)=142.41µs p(95)=896.21µs
http_req_sending...............: avg=8.44µs min=890ns med=2.19µs max=18.13ms p(90)=3.63µs p(95)=6.34µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=4.5ms min=25.57µs med=3.77ms max=45.46ms p(90)=8.74ms p(95)=11.13ms
http_reqs......................: 362167 12040.737924/s
iteration_duration.............: avg=140.21ms min=80.54ms med=139.82ms max=221.91ms p(90)=153.25ms p(95)=157.95ms
iterations.....................: 2143 71.24697/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;2 vCPU/4GB RAM/10 VUs - &lt;code&gt;servy&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 464412 ✗ 0
data_received..................: 51 GB 1.7 GB/s
data_sent......................: 57 MB 1.9 MB/s
http_req_blocked...............: avg=1.99µs min=360ns med=720ns max=14.38ms p(90)=1.13µs p(95)=1.42µs
http_req_connecting............: avg=92ns min=0s med=0s max=6.71ms p(90)=0s p(95)=0s
http_req_duration..............: avg=3.67ms min=44.91µs med=3.07ms max=60.17ms p(90)=5.9ms p(95)=7.98ms
{ expected_response:true }...: avg=3.67ms min=44.91µs med=3.07ms max=60.17ms p(90)=5.9ms p(95)=7.98ms
http_req_failed................: 0.00% ✓ 0 ✗ 464412
http_req_receiving.............: avg=206.58µs min=3.41µs med=14.06µs max=56.96ms p(90)=56.97µs p(95)=143.43µs
http_req_sending...............: avg=7.12µs min=870ns med=1.62µs max=14.84ms p(90)=3.01µs p(95)=5.78µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=3.45ms min=35.61µs med=3.01ms max=24.64ms p(90)=5.7ms p(95)=7.54ms
http_reqs......................: 464412 15448.582291/s
iteration_duration.............: avg=109.3ms min=63.59ms med=108.88ms max=161.77ms p(90)=120.71ms p(95)=124.65ms
iterations.....................: 2748 91.41173/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;16 vCPU/16GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt; (old)&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 119990 ✗ 0
data_received..................: 13 GB 439 MB/s
data_sent......................: 16 MB 534 kB/s
http_req_blocked...............: avg=2.05µs min=440ns med=1.55µs max=972.62µs p(90)=2.32µs p(95)=2.74µs
http_req_connecting............: avg=38ns min=0s med=0s max=275.54µs p(90)=0s p(95)=0s
http_req_duration..............: avg=14.82ms min=35.49µs med=15.16ms max=148.26ms p(90)=21.12ms p(95)=23.89ms
{ expected_response:true }...: avg=14.82ms min=35.49µs med=15.16ms max=148.26ms p(90)=21.12ms p(95)=23.89ms
http_req_failed................: 0.00% ✓ 0 ✗ 119990
http_req_receiving.............: avg=104.7µs min=5.07µs med=43.71µs max=8.96ms p(90)=228.22µs p(95)=396.05µs
http_req_sending...............: avg=4.52µs min=940ns med=3.99µs max=944.81µs p(90)=6.28µs p(95)=7.86µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=14.71ms min=23.97µs med=15.05ms max=148.25ms p(90)=20.95ms p(95)=23.7ms
http_reqs......................: 119990 3957.203279/s
iteration_duration.............: avg=426.57ms min=343.33ms med=422.38ms max=526.97ms p(90)=491ms p(95)=502.47ms
iterations.....................: 710 23.415404/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;16 vCPU/16GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 1297244 ✗ 0
data_received..................: 144 GB 4.8 GB/s
data_sent......................: 175 MB 5.8 MB/s
http_req_blocked...............: avg=2.83µs min=419ns med=1.63µs max=7.97ms p(90)=2.95µs p(95)=3.78µs
http_req_connecting............: avg=2ns min=0s med=0s max=142.69µs p(90)=0s p(95)=0s
http_req_duration..............: avg=1.22ms min=31.96µs med=697.97µs max=36.9ms p(90)=3.16ms p(95)=4.07ms
{ expected_response:true }...: avg=1.22ms min=31.96µs med=697.97µs max=36.9ms p(90)=3.16ms p(95)=4.07ms
http_req_failed................: 0.00% ✓ 0 ✗ 1297244
http_req_receiving.............: avg=234.98µs min=4.21µs med=39.77µs max=35.6ms p(90)=640.71µs p(95)=1.24ms
http_req_sending...............: avg=7.19µs min=930ns med=3.91µs max=30.11ms p(90)=6.84µs p(95)=9.22µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=980.01µs min=19.74µs med=465.75µs max=16.64ms p(90)=2.75ms p(95)=3.68ms
http_reqs......................: 1297244 43221.02723/s
iteration_duration.............: avg=39.08ms min=23.12ms med=38.84ms max=76ms p(90)=43.87ms p(95)=45.71ms
iterations.....................: 7676 255.745723/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;16 vCPU/16GB RAM/10 VUs - &lt;code&gt;servy&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 1407770 ✗ 0
data_received..................: 156 GB 5.2 GB/s
data_sent......................: 172 MB 5.7 MB/s
http_req_blocked...............: avg=2.28µs min=420ns med=1.64µs max=3.96ms p(90)=3µs p(95)=3.94µs
http_req_connecting............: avg=2ns min=0s med=0s max=292.26µs p(90)=0s p(95)=0s
http_req_duration..............: avg=512.52µs min=40.21µs med=177.39µs max=45.65ms p(90)=444.41µs p(95)=623.84µs
{ expected_response:true }...: avg=512.52µs min=40.21µs med=177.39µs max=45.65ms p(90)=444.41µs p(95)=623.84µs
http_req_failed................: 0.00% ✓ 0 ✗ 1407770
http_req_receiving.............: avg=324.43µs min=4.02µs med=26.56µs max=45.31ms p(90)=88.04µs p(95)=147.94µs
http_req_sending...............: avg=4.33µs min=969ns med=3.35µs max=4.11ms p(90)=5.86µs p(95)=7.31µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=183.74µs min=31.45µs med=132.09µs max=8.96ms p(90)=344.35µs p(95)=465.04µs
http_reqs......................: 1407770 46854.747614/s
iteration_duration.............: avg=36.03ms min=5.19ms med=45.31ms max=77.29ms p(90)=48.06ms p(95)=48.95ms
iterations.....................: 8330 277.247027/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;/details&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;&lt;/th&gt;
&lt;th style="text-align: left"&gt;&lt;code&gt;gosherve (old)&lt;/code&gt;&lt;/th&gt;
&lt;th style="text-align: left"&gt;&lt;code&gt;gosherve&lt;/code&gt;&lt;/th&gt;
&lt;th style="text-align: left"&gt;&lt;code&gt;servy&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;1 vCPU/256MB RAM/1 VUs&lt;/td&gt;
&lt;td style="text-align: left"&gt;86MB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;587MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;547MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;2 vCPU/4GB RAM/10 VUs&lt;/td&gt;
&lt;td style="text-align: left"&gt;426MB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;1.3GB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;1.7GB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;16 vCPU/16GB RAM/10 VUs&lt;/td&gt;
&lt;td style="text-align: left"&gt;439MB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;4.8GB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;5.2GB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Interestingly, it seems that the &lt;code&gt;servy&lt;/code&gt; variant actually outperformed &lt;code&gt;gosherve&lt;/code&gt; again in these VM-based tests, where it didn&amp;rsquo;t when run directly on my machine. In any case, the gap is now &lt;em&gt;much&lt;/em&gt; smaller between the two, and &lt;code&gt;gosherve&lt;/code&gt; still seems to maintain an edge on very small machines.&lt;/p&gt;
&lt;p&gt;Note I had to drop the VUs to &lt;code&gt;1&lt;/code&gt; on the smallest iteration, or I was seeing all three of the variants getting OOM-killed!&lt;/p&gt;
&lt;p&gt;I also added a &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L6" target="_blank" rel="noreferrer"&gt;new flake input&lt;/a&gt; in the &lt;code&gt;server-bench&lt;/code&gt; repo that adds the &lt;code&gt;gosherve&lt;/code&gt; based repo again, but pinned to the pre-optimised version.&lt;/p&gt;
&lt;p&gt;As such, if you want to try reproducing any of these results and measure old vs. new vs. &lt;code&gt;servy&lt;/code&gt;, you just need to do the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the devShell for the server-bench project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix develop github:jnsgruk/server-bench
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Build &amp;amp; run the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ run-benchvm-vm --daemonize --display none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Edit the core/memory count as required&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ vim vm.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start, and load test a server, choosing your variety&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-test &amp;lt;jnsgruk-go-old&lt;span class="p"&gt;|&lt;/span&gt;jnsgruk-go&lt;span class="p"&gt;|&lt;/span&gt;jnsgruk-rust&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Power down the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-exec poweroff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The results will be gathered in a collection of &lt;code&gt;summary-*.&amp;lt;txt|html|json&amp;gt;&lt;/code&gt; files for you to inspect.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This was supposed to be a blog about learning Rust, but I&amp;rsquo;m pleased with where it ended up! This was by no means a particularly in-depth dive into measuring the performance of web servers, focusing almost entirely on the request throughput. There are other factors that could be taken into consideration - such as the resulting memory pressure on the machine, CPU usage, etc. I may come back to this in another post with more comprehensive measurement of both.&lt;/p&gt;
&lt;p&gt;Hopefully this post illustrated the power of load testing and profiling tools - even when only used at quite a surface level, and demonstrated how Nix can be used to create robust structures around existing tools to help with such workflows.&lt;/p&gt;
&lt;p&gt;I owe a thank you to &lt;a href="https://github.com/brancz" target="_blank" rel="noreferrer"&gt;Frederic&lt;/a&gt; from Polar Signals, who gave me a bunch of helpful tips along the way while I was using Parca and profiling &lt;code&gt;gosherve&lt;/code&gt;. After my initial improvement in the request handler, Frederic helped me track down and remove the use of &lt;code&gt;fs.ReadFile&lt;/code&gt; and further improve the performance of &lt;code&gt;gosherve&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still very early in my Rust development, so if you&amp;rsquo;ve spotted something horrible in &lt;code&gt;servy&lt;/code&gt;, or you&amp;rsquo;ve got experience with &lt;code&gt;k6&lt;/code&gt; and &lt;code&gt;parca&lt;/code&gt;, then reach out and let me know! I&amp;rsquo;d love to see the creative ways people use these tools.&lt;/p&gt;
&lt;p&gt;The performance gap between &lt;code&gt;servy&lt;/code&gt; and &lt;code&gt;gosherve&lt;/code&gt; is now quite narrow. Close enough, in fact, that they seem to win over one another depending on different conditions in the test environment, and never by a particularly high margin. I haven&amp;rsquo;t migrated this site over yet, but perhaps I will in the near future. Besides, Fly.io are &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2/fly.toml#L27-L30" target="_blank" rel="noreferrer"&gt;already kindly limiting the damage&lt;/a&gt; that can be done by too many requests for me!&lt;/p&gt;
&lt;p&gt;See you next time!&lt;/p&gt;</description></item></channel></rss>