Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
1

glimpses of the future

from seated.ro [alt+shift+b] in programming

Glimpse v1.0 Glimpse can now build call graphs, showing you exactly how functions relate to each other in your codebase. Video # what does main call? glimpse code :main # what calls this function? (reverse call graph) glimpse code :process_request --callers # limit the depth glimpse code :build --depth 3 This works by parsing your code with tree-sitter, extracting function definitions and calls, then resolving those calls to their actual definitions. Precise mode Sometimes tree-sitter based resolution isn’t enough. Maybe you’re dealing with dynamic dispatch, generics, or just a language with particularly complex module resolution. For this, Glimpse can use LSPs to resolve definitions semantically. glimpse code :main --precise This spins up actual LSP servers and uses goto-definition / goto-implementation to resolve calls. It’s slower, but accurate. Glimpse will attempt to auto-install the LSP servers for you. Indexing Glimpse eagerly caches whatever it finds into an incremental index. But you can choose to pre-build the index ahead of time for instant queries. # build the index glimpse index build # with LSP for precise resolution glimpse index build --precise # check what you've got glimpse index status The index stores all the definitions, calls, and resolutions so subsequent queries are fast. Language support Glimpse now supports: Go, Rust, C, C++, Python, TypeScript, JavaScript, Zig, Java, Scala, Nix, Lua, Ruby, C#, Kotlin, Swift, and Haskell. Each language has custom tree-sitter queries for extracting definitions, calls, and imports. The grammars are downloaded and compiled automatically on first use. Try it # install cargo install glimpse # or with homebrew brew tap seatedro/glimpse && brew install glimpse # or with nix nix profile install github:seatedro/glimpse # then just glimpse code :main
1st Jan 2026

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from seated.ro

You fail to learn if you don't learn to fail

If all your time is spent watching output tokens, where do your input tokens come from? Letting an agent rip on full auto is basically doom scrolling. Even worse if you're doom scrolling while the agent runs. We humans love frying our dopamine receptors. This feels great until you realize what you were offloading: the struggle. The part where you fail. Failure is the entire point. You don't make progress in the gym unless you take a set at least close to failure. The muscle only adapts when it's forced to. It is no different for the brain. Cognitive Atrophy It is very hard to admit to yourself that your skills have atrophied. It is even harder to admit this to other people. I will admit that over the past several months my brain has gotten smoother (and I wasn't even on Twitter much!). Recently, I had written an abstraction for my diff viewer (diffy), an element system with a macro that lets agents write html-like code in rust for native ui (they reason better with this). But it wasn't adopted everywhere in the repo yet, so when I asked for a new feature, the model decided to hand paint it straight to the viewport instead. Every behavior the element system gives you for free was just... missing. Text wasn't selectable. Hover highlights wouldn't go away. And since I wasn't looking closely, it iterated on the slop and produced more slop, more bugs. I just kept saying continue. I lost a whole day untangling it, and the funny part is that once I actually looked at what it had built, every bug was the same bug. When you hit a roadblock and your immediate reaction is to reach for something else (previously, this used to be other people, but now it is a language model) you are essentially skipping the part where you actually learn to solve the problem. It is funny how one of the best "learning tools" has turned out to be the number one cause (anecdotal. sue me) of the lack of learning! It's been a few months since I started writing this, and things have gotten more dire. Several major software services barely work now, grown engineers I once respected are writing somber posts about missing a language model that was banned for a while. Mourning. For model weights. It's all so dystopian. It didn't work, but boy was it beautiful. As the agents get better, one is basically expected to produce code at an alarming rate. The timeline to get something done is compressed but the time it takes to come up with solutions to hard problems has not. There are usually a few good abstractions one can come up with that balance the upsides and tradeoffs for most software problems. However it is currently trivial to turn your brain off and let the slop flow. The code will be complex. It might look like it all works, but something always breaks. And the solution to that? More slop. Software quality is collapsing as a result, and the societal expectation that engineers understand what they ship is disappearing. You never understood the code in the first place. So when you need to change it, you're asking the same stateless clanker to modify code it has no memory of writing. All output tokens and zero thinking tokens. A lower barrier of entry to write software doesn't imply the standards for good software must be lowered. The culture of doing things because they said you couldn't. The growing trend is to do things because you now can (supposedly), but we used to try and do things because we could not out of sheer stubbornness. Carmack and gang shipped QuakeWorld with client-side prediction over dial-up when the conventional wisdom was that twitch shooters over the internet were unplayable. This only happened because Quake's original netcode was laggy and everyone hated it. (They fixed it in a month.) George Dantzig arrived late to class, mistook two "unsolvable" statistics problems for homework, and solved them. Nobody told him they were impossible, so he just did the work. Andrew Wiles spent seven years alone in his attic working on Fermat's Last Theorem, a problem mathematicians had given up on for 350 years. He announced the proof, a reviewer found a hole in it, and he spent another year fixing that too. Notice that all three of them became who they are because of the struggle, not despite it. The people benefiting most from generative tools today, say Terence Tao or Mitchell Hashimoto, already put in the time, so when they offload work they're just skipping the typing. When people like you and me (if this is not you, then I apologize) offload, we skip the grind itself. With language models, easy tasks got easier, hard tasks stayed hard. The hard part was never the task itself. @codex how do I fix this I don't know, I am figuring this out as I go. The amount of time I have spent actually programming has been dropping month over month this year. I used to have a coding stats section on my website that would track hours I spent writing code split by language, recently I had updated it to this: and it made me quite sad. I do think that sometimes all you need is to realize that the thing you are doing is actually detrimental to your growth. Consistency matters more than one would assume. If you consistently take some time away from these tools and actually use your brain, that alone is already significantly better than offloading your thoughts. Solve the problems yourself. Or at least try, fail, and spend time thinking. There is seemingly no "learning" phase anymore. You are expected to just know things. Learning is fun, don't let anyone take this away from you. I've written about this before. It is probably going to be slow, learning takes time and effort. You will feel stupid (I feel stupid). This is a good feeling, because there exists a world where you are no longer stupid and the path towards it is learning. Books still exist! Libraries are still open, notebooks waiting to be written in. Read more. Write more. But If you really do care about improving yourself, be honest and use these models for what they are, highly efficient filters of zettabytes of data (the internet is estimated to be 175-240 zettabytes (10^{21} bytes)). It was extremely difficult to identify what one needed to read to learn niche topics even like 2 years ago. I remember asking a good friend of mine to recommend material to dive deep into learning about SIMD, and honestly there wasn't much stuff to read except the Intel Intrinsics Guide. And if you've ever taken a look at that, it is quite cancerous for a first-time reader. Language models are super useful here because you can point them at material and you can ask questions that pertain to the thing you care about and it will simply just tell you the correct things. Conclusion One good thing in this age of slop is to consume knowledge at an unbelievable pace. I don't necessarily mean using only model output for learning (I don't trust them to learn any topic more than a shallow amount), but rather using them to help sift through the plethora of information available out there and identifying the right things to read. Human slop exists too and using a language model to supplement your learning might help keep you sane (ironically). I like using these models to write code that I tell it to write (outside of work I enjoy doing it myself entirely), and I am largely disinterested in asking it what I should write. There are exceptions of course, because not everyone is working on scaling software services which has largely been solved (but slowly being forgotten), but that would be for you to decide. The best model you have access to (and it has solved continual learning) is, and always has been, the one inside your skull. It's time to scale up its input tokens.

12th Jul 2026 • 1 votes
Devlog 001

This is a general devlog covering what I’ve been up to over the past few months! Haskell Hakyll is a Haskell library for generating static websites (yes, you do not, in fact, need Next.js for your personal site). The previous iteration of my website was written with Go + templ, and it was not static. I wrote everything myself, and it was, quite frankly, a horror to maintain. The templ LSP actually hinders more than it helps, and since I did not statically generate the content, it was kind of slow for a personal website. I did not like it. I added some very minor CSS fixes and a theme toggle button for my friend’s new website ludwigabap.com, and I saw the light that is Hakyll. :kneel: So I nerd-sniped myself into rewriting my personal website in Hakyll so that it generates beautiful HTML at build time (it uses Tailwind, too). All the dynamic bits of the website, like my silly stats and coding time, are served from a single Rust Axum server binary on my Hetzner VM that serves other stuff too (Memegrep’s server, among other things). Nix-pill This segues into my next nerd-snipe, or rather, my favorite new pill: the Nix pill. My entire website is built with a single Nix flake, including the Hakyll build step, the Cargo build step, and generating the resulting Docker image. Although, to be honest, the learning curve is very steep and I simply do not have it in me to master this language anytime soon, I like the principles, and it seems to me the least bad build system out there. I do most of my work on a MacBook running a NixOS VM as well. (seatedro/dotnix on GitHub.) It’s so much nicer to have a real dev workflow versus whatever the fuck macOS is. Sorry, but Homebrew is not good software. Ember The reason I started programming was that I was playing a game called Midtown Madness as a wee lad and decided that one day I would build a cool game like that. So, after 15 years, I’ve decided to embark on a small adventure to build a cool physics simulation (and eventually a voxel) engine. It’s written entirely in Zig because I enjoy writing Zig and did not want to touch C++ (although there were times I thought about committing this sin). When Ember gets far enough, I might write separate devlogs, but with what’s written so far, there isn’t enough to justify one. I was knee-deep in the mines because I wanted extremely specific features like multi-viewport docking with ImGui, and the off-the-shelf Zig libraries did not want to ship that for some reason. So there were many adventures in getting this shit to compile neatly. Ember now uses SDL3 for windowing and has an abstract rendering API (rudimentary, but the backends are in place) with SDLRenderer3, OpenGL, and WGPU-native backends that can be neatly switched at comptime. I’m using a bunch of cool resources to learn more about how to do this because I’ve never really written any game-dev related code before. I just want to build some cool simulations, and I will get there no matter what. github / seatedro / ember Thanatos After 5 years, I have finally built a new personal computer. I will be running NixOS as my main operating system with Hyprland and Wayland. I will also have Windows installed on a separate drive mainly for video games, though I expect to use Linux more often. It’s so nice not having to worry about my 256 GB SSD getting filled up on my MacBook Air. :> Exa Also, I joined Exa last month to work on the back-end team, and it has been so fun! I am soaking up as much knowledge from my significantly smarter peers at a good pace. As per custom, I revisited @ludwigABAP’s post: On becoming competitive when joining a new company. (PS: it’s on a new website.) Fin Back to the code cave I go—going to extract the polymorphic.

13th Jul 2025 • 1 votes
rabbit hole learning

Look everything up Pretty much the best way to learn that I have found is to refrain from suprressing your innate curosity and let it go wild. Most of the time, you will encounter a term or concept that you do not know, instead of glancing at it briefly, go all in. Google it, read the wiki page, found someone’s blog post? Read it. Watch that youtube video (I only do this if it’s not 3 hours like your average Sphaerophoria stream). Dive head first into rabbit holes. I’ll outline an average day where I have two types of goals: - Concrete goals: Like finish implementing X feature in Y project - Loose goals: Learn about X or Y. I will include rough timestamps but they are pretty much meaningless because productivity levels vary immensely throughout the day. 9:00 AM Decided to learn about document parsing. Links: EPUB specification, I found out that EPUBs are just an archive with HTML, and nearly had a crisis. (Now I know.) Immediate questions: so I need to parse the EPUB, extract metadata, fetch the XML and parse that too. Eventually I would have to use a WebView to render the HTML/CSS on the screen to render the book. (Unfortunate.) Found out that in order to read the PDF spec, you need to pay like 350 swiss francs! Spend time perusing SwiftUI docs, and asking grok about how I can render things on the screen with Swift. Fiddle with XCode, marvel at how Swift gets pretty much everything right but uses func ... for declaring functions. It has Result<T, E> though, so forgiven. 11:00 AM Began reading Computer Systems: A Programmer’s Perspective (CSAPP) Studied numeric representations, created Anki cards for hexadecimal conversion 12:00 PM Rabbit holed into learning so much about UTF-8 Bookmarked to learn more about UTF-8 and UTF-16 and writing a parser for it later (ILY @zack_overflow) 1:00 PM Taking a break, lunch + watching a movie. 2:30 PM Read Jon Olick’s single file resize implementation in C++ as reference (ILY @gizmobly) to roll my own resize library for use in glyph Rabbit holed into learning more about Sinc filters and the Lanczos Kernel. 3:30 PM Shifted to working on memegrep(v2). This is where I already know the goals I want to accomplish before the day ends. I implemented a pub/sub flow to help with scale when users upload their private memes. Rough sketch in mind: user uploads meme(s) → server receives req → insert skeleton into db → queue upload → return 201 to user immediately → worker picks up task Spent the next 6 hours coding without even realizing 6 hours had passed (bliss) Ended up with multi-file upload, search, deployed a CLIP model, added all the scaffolding needed in the UI for this. 9:30 PM I posted something about pointers on twitter and ended up reading some history about the nomenclature just for fun. Links: pointers, handles are the better pointers Also saw something about reference counting being used in the libvips library earlier in the day, so decided to check out their implementation since i’ve only used it in rust quite often. reference counting, Rc and Arc 11:00 PM Was in bed and saw a post linking an amazing article by Valve on Source Engine Networking, so ended up being a nice and light read. At the end of the day I ended up with more questions, but I definitely had more answers than when I started! Here’s a DAG of my exploration for fun:

9th Mar 2025 • 1 votes
two weeks is all you need

two-weeks I built a website (twoweeksisallyouneed dot com) with just Claude 3.5 Sonnet, zero lines of code written by me. Why I did this So a couple weeks back I had a computer vision midterm and i was allowed a single page of notes. I decided to use Claude to generate a cheat sheet in LaTeX. It was crazy lol, I was able to cook up something usable in 15 mins. pic.twitter.com/0tvIsdv34d — ro/nin (@seatedro) October 24, 2024 Someone had asked me to make a similar cheatsheet for ML, but I thought why not get claude to make an entire website instead? Early days I didn’t really give claude any specific information except what I wanted to build. My plan was as follows: > build out the UI skeleton first > populate with some dummy data > set up a content pipeline > tie up the ui with the data > fix bugs Claude decided to use react (Shocking). With just a couple of chats I was able to get the retro/hacker/matrix style UI down. (It’s going to be hard for frontend engineers to keep up with AI at this rate) I spent some time building and adding secrets/easter eggs to the website which no one has found yet lmaoooo. If/when all the easter eggs are found, I will open source the repository. Good hunting until then bros. Frustrations Things got really annoying, really fast. As soon as I wanted to build some sort of content pipeline, everything went to shit. Claude, no matter how smart of an AI, is not human. It did not think ahead. If I was going to build out this website I would not have started with a barebones react/vite app. I would have probably gone for a full stack framework like sveltekit instead. Generating content for the website was/is a nightmare. LLMs hallucinate, this is known, but did you also know how incredibly frustrating it is to get them to follow instructions? There were multiple instances where Claude (with all the 40% of project context of javascript code) generated a TypeScript interface and proceeded to spit TypeScript code. This project has 0 TypeScript???? The new edit in place nonsense was getting on my nerves. Half the time the output artifact wouldn’t even change, and the other half it would mess up the changes. I have to mention “please use a new artifact” if I wanted any real usable code. I was enthusiastic about building something with just Claude the first few days, then I started getting weary, and then eventually I wanted to take a sabbatical from using AI. When the project context grows, (think like 20% or more), Claude seems to have a hard time using that information. Often times I found myself hitting send, and Claude would spit some nonsense, I would hit stop and paste any relevant code directly and then get some useful code. Context is probably the biggest annoyance I’ve had with LLMs. Random thoughts Perplexity (with claude 3.5) is great (to an extent) because it’s essentially a RAG search so I was able to get somewhat up to date content for the resources and references sections from it. I feel like I was able to ship something of decent quality for sure, but I lost a lot of brain cells during the process. Do not take away from the programmer the only thing he wishes to do, program. Proompting I didn’t particularly do anything unique to get the best out of my prompts/chats. I did do everything in a Project on claude dot ai though, which let me set Project Instructions like so: BE ENTHUSIASTIC. WE ARE GOING TO CHANGE THE WAY THE WORLD LEARNS WITH THIS WEBSITE🚀🚀🚀🚀 LLMs seem to be much more open to doing anything you ask if you gaslight them, so go ahead and do it. Here are some prompts that I used while building the website: PROMPT ------ This is AWESOME: * For the loading screen add some text that says "you can learn anything in two weeks" * The ascii text is also weird, it says erain? is that supposed to mean something? * Where did all my placeholder topics go? * MOAR scrt scan lines * The matrix rain needs to fall vertically and it should be subtle. On mouse move it should get a bit brighter (the char under the mouse) What else do you think you could add? Surprise me One time I asked Claude to add something and it started changing the existing UI????? PROMPT: ------ OKAY, we have a beautiful boot sequence now. let's flesh out the main content screen. First let's fix some bugs: * The animations restart everytime i move my mouse instead of continue organically * What i mean is, when i move my mouse over the windows or around the matrix rain, the matrix rain restarts and so do the typewriter effect on the window title Work on these * [REDACTED] Easter eggs: * Fun Features: * "Power saving mode" that dims everything except what you're reading ....[REDACTED] So many times I ended up editing the prompt and adding lines like this: PLEASE DON'T CHANGE THE EXISTING UI. IT LOOKS GOOD BRO. I’ll just add some random prompts here PROMPT ------ Broski, we need to make the UI responsive and shit. Lot of people are reporting issues with it on mobile and smaller screens PROMPT ------ what sort of content do u think we should add? i was thinking things like formulae, charts (for phd), code blocks (for eng), research papers, youtube video links, blog posts etc. Let's think out loud how we're gonna do this before we proceed PROMPT ------ `pasted_code.jsx` Here is my current dashboard. Here is some sample concept content i have: `pasted_data.json` I need you to render this beautifully in the dashboard. Make any and all changes needed. It needs to look beautiful. And anything else u feel would be good. List out the things you're adding before adding it okay? Concluding thoughts This was a good experiment, a success. If you have a clear vision for what you want your product to be, then AI can help you achieve that vision quite well! However, I don’t think I will be using AI for the foreseeable future. I feel like my learning is stagnating the more i use AI and I want to write my slop code with my own two hands. I might use avante.nvim to quickly write some duplicated code here and there but by god, I miss coding. Actual coding.

21st Nov 2024 • 1 votes

More in programming

Clip of me singing Despard in Ruddigore in 2013

A clip of me singing a funny song from Gilbert and Sullivan’s Ruddigore back in 2013

6 hours ago • 1 votes
How and Why fork() Uses Copy-on-Write

In this video, we look at why fork() needs copy-on-write, how it works inside the kernel, and a memory usage problem that Instagram encountered with Python.

12 hours ago • 1 votes
What we lost when we lost comments

Comments require commitment, but they’re worth it.

18 hours ago • 1 votes
Lighthouse map

Lovely global map with animated lights sweeping the waters

20 hours ago • 1 votes
Warming up the Puma master before it forks

Basecamp 5 runs on Puma in cluster mode: one master process with preload_app! and 63 single-threaded workers per host, deployed as a Docker container with Kamal. We serve Basecamp from several sites. Each site has its own web hosts and a read replica of the database, and writes go to a single primary database in one of them. On our busiest hosts, each deploy left up to 2,000 requests waiting while the new workers warmed up. We reduced those queues by running signed-in requests through the app in the Puma master, before it forked the workers. Why 63 single-threaded workers? Basecamp has always served web requests from processes rather than threads. It ran on Unicorn, which only does processes, until we moved to Puma in January 2025, and we kept the same setup: workers (Concurrent.physical_processor_count * 1.3).ceil threads 1, 1 preload_app! On a 48-core host that’s 63 workers, each handling one request at a time. We chose 1.3 after benchmarking HEY in 2023, when we moved our apps out of the cloud and onto our own hardware. We tested several combinations of workers and threads with a mix of GET and POST requests on a 32-vCPU VM. Every multithreaded configuration we tested was slower and handled fewer requests than single-threaded workers. Adding workers beyond about 1.2 to 1.3 per vCPU brought little benefit. The threaded workers spent a lot of their time waiting for Ruby’s global VM lock. That made single-threaded workers a good fit for this workload, and we use the same setup for Basecamp. An app that spends more time waiting on its database or other services may benefit from more threads, so benchmark your own app. The other reason is the app itself. Basecamp has class-level state in places and has never needed to be thread-safe. With one request per process, it still doesn’t. Processes do use more memory than threads, and preload_app! reduces the difference. The master loads the app once and the workers share its memory through copy-on-write until they write to it. Shopify’s comparison of Ruby execution models explains the trade-off well. In the HEY benchmark the best setup came to about 260 MB of PSS per core, where PSS counts each shared page once, split between the processes using it, and the gap to a threaded setup was smaller than we’d expected. What Puma does on each host when a container starts: one master, then 63 forked workers that share its memory until they write to it. Two things about this setup matter for the rest of the post. A worker that’s compiling or loading something is fully blocked — there’s no other thread to pick up the next request. And whatever the master has in memory before it forks, all 63 workers share. Whatever they build after the fork, they build 63 times. What happens when we deploy Kamal starts the new container alongside the old one, and kamal-proxy moves the host’s traffic across as soon as the health check passes. At that moment, the new workers have handled health checks but no customer requests. preload_app! means the master loads the app once and the workers inherit it through fork. That covers the code. It doesn’t cover anything Ruby and Rails set up on first use: YJIT compiled code. YJIT compiles a method once it’s been called a certain number of times. The master calls very little during boot, so every worker compiles the same methods again on its own first requests. Compiled templates. Action View turns each ERB template into a Ruby method the first time it’s rendered. The schema cache. Active Record reads each model’s columns from the database the first time that model is used. Inline caches and memoized values throughout Ruby, Rails and the app. All 63 workers did all of this at once, while serving the traffic the old container had been handling a second earlier. In the test environment with YJIT on, the first request to a project page on a cold process took 652 ms, 151 ms of it YJIT compiling. The same request to a warm process took 28 ms. In production, CPU time per request peaked at around 200 ms while kamal-proxy moved traffic to the new container, against about 30 ms once the workers had warmed up. A host with spare CPU absorbs this. Every one of our web hosts has 48 cores and 63 workers, but each Amsterdam host serves around 250 requests per second, against 25 to 60 at our other sites. In Amsterdam the slow first requests turned into a queue. At a peak-hour deploy, the Puma backlog on an Amsterdam host reached anywhere from 250 to 2,238 requests, and kamal-proxy’s p99 response time hit about 10 seconds. Eron, our Director of Operations, had been tracking this since June. Another server in Amsterdam would help, but it would take weeks to arrive, so we also wanted to make deploys cheaper on the hardware we already had. What didn’t work We tried a few things first. In June, Donal tested the first two on a single Amsterdam host, comparing it with its neighbors, and they ruled out two likely causes. Warming each worker’s database connections. Puma’s before_fork hook clears the master’s connections, and each worker opened its own on its first request. Opening them in before_worker_boot instead made no difference. Queries on a freshly booted production host were already under a millisecond, so connections weren’t the problem. A synthetic request in each worker. Next, each worker made a few requests in before_worker_boot to an internal controller that touched every model. That ran the middleware, routing and Active Record paths, but it ran them in 63 workers at once — exactly the CPU spike we were trying to avoid. And a request with no real data renders no real views, so most of the app stayed cold. Spreading YJIT compilation out. Delaying YJIT in each worker by a random interval spread the compiling out over a few minutes, but every worker still ran interpreted until its delay ended. The queue didn’t change. Reforking from a warm worker. This is what Shopify’s Pitchfork does: let one worker serve traffic until it’s warm, then fork the others from it. Puma has an experimental version called fork_worker, and on beta it worked — the reforked workers were warm after three to five requests, where fresh ones took up to 30 seconds. But with fork_worker the template is worker 0, and it keeps serving requests. If it exits, the workers waiting to be forked never start (puma/puma#3596). If it gets no traffic, the refork never happens, which is what we saw on beta. Instacart have a mold_worker patch that promotes a warm worker to a template that stops serving, but it isn’t in a Puma release. We have a branch of it, and we may come back to it. That last experiment did show us where the fix was, though. Everything a warm worker has that a cold one lacks is in its memory, and fork copies memory. The master already has the app loaded. It just never runs it. Run the requests in the master So now, before the master binds its socket and forks, it makes the app’s own requests, in-process, the way a signed-in user would. Rack has a hook for exactly this. Rack::Builder#warmup takes a block that’s called once with the built app, before the server starts. rails server builds the app from config.ru, so the change to boot is one line: require_relative "config/environment" warmup { WarmUp.configured.run } if ENV["WARM_UP"] run Rails.application With preload_app! this runs in the master, and the workers inherit whatever it did. Puma binds its socket after the app is built, so until the warm-up finishes the health check’s connection is refused and kamal-proxy keeps retrying. No request reaches a worker that hasn’t been warmed. The warm-up has three steps. After precompiling the views, it gives the page requests and schema loading a shared 20-second budget, checked before each page or model. 1. Precompile the views actionview_precompiler reads every template for its render calls and compiles each one with the locals it’s passed. For us that’s 1,394 templates in about two seconds. A first request to a project page then compiles 2 templates instead of 44. 2. Request the pages, signed in A small browser class makes the requests through Rack::MockRequest, with the two cookies a real sign-in sets, then goes back for each page’s lazy Turbo frames: class WarmUp::Browser def initialize(signed_in_as:) @client = Rack::MockRequest.new(Rails.application) @headers = { "HTTP_USER_AGENT" => "Basecamp warm-up", "HTTP_COOKIE" => cookie_for(signed_in_as), "bc3.warm_up" => true } end def visit(path) page = get(path) frames_in(page).each { |id, src| get(src, "HTTP_TURBO_FRAME" => id) } end private def get(path, headers = {}) @client.get("https://#{host}#{path}", @headers.merge(headers)) end def frames_in(page) Nokogiri::HTML5(page.body).css("turbo-frame[src]").map { |frame| [ frame["id"], frame["src"] ] } end end The requests are signed in. The user is a monitoring account we already use for automated checks, and the pages are its own project, Campfire, to-dos, documents and messages. Public pages weren’t enough: after warming up with signed-out pages only, the first signed-in request to the projects page still took 131 ms, because authentication, the signed-in controllers and their views had never run. With signed-in pages it took 40 ms. cookie_for writes the same signed cookie the sign-in controller does, using the app’s own cookie jar, so there’s no API token and no secret to store. The frames are followed. The busiest HTML requests in production aren’t pages at all but Turbo frames — the sidebar badge, the inbox, the navigation menus. The browser parses each page and requests its <turbo-frame src> URLs with the Turbo-Frame header, so those controllers and views get warmed too. Our first four pages turned into 60 requests. The requests are excluded from rate limiting. They are internal, so they do not count against the rate limits that apply to real visitors. 3. Load the rest of the schema The page requests load the schema for the models they touch. The last step loads the rest, from the read replica: ApplicationRecord.reading do models.lazy.take_while { time_left? }.each { |model| model.load_schema if model.table_exists? } end The step checks 261 models and loads any schema information still missing. Those database round trips add up when the primary is far away: outside a request, Active Record uses the writing role, and from a host a long way from the primary each round trip is tens of milliseconds. Reading from the local replica brings the step down from about 20 seconds to 3.5. The pages go first because they load most of the schema anyway. If the time budget runs out, the step stops, logs how many models it got through, and the workers load the rest on first use like they always did. Rails can also load the schema from a dumped cache file at boot (bin/rails db:schema:cache:dump), which would make this step unnecessary. We don’t ship one in our image yet, because the dump needs a database to read from at build time, and we have several databases to cover. It’s on the list. What to close before the fork Running requests in the master opens things the master never opened before, and every worker inherits them. Two processes writing to the same socket will corrupt each other’s traffic, so you need to know what’s open before you fork. The way to find out is to list the master’s open file descriptors — ls -l /proc/<pid>/fd — before and after a warm-up, in an environment set up like production. Development wasn’t enough for us: it stores files on disk, so our S3 connections only showed up in production. Then, for each thing that’s open, check how its library handles a fork. We found three kinds: Already handled. Plenty of libraries detect a fork on their own, either by recording the PID they connected from and reconnecting in the child, by opening per-process files, or by resetting their thread pools. Redis clients, metrics libraries and concurrency libraries tend to be in this group. Check, but you probably don’t need to do anything. Already closed. Database connections are the classic one, and most Puma configs already clear them in before_fork. Anything else that’s opened per process — we have a SQLite cache the workers open on boot — needs closing when the warm-up finishes. Needs a new step. HTTP clients with keep-alive connections are the ones to look for: cloud SDKs with connection pools, tracing exporters, error reporters. They usually have no fork handling at all. We empty the aws-sdk connection pools in before_fork, and we run the warm-up untraced so the OpenTelemetry exporter never opens its connection to Tempo in the first place. Once that’s done, before_fork finishes with Process.warmup, which Ruby 3.3 added for this purpose: a major GC, a heap compaction, and every surviving object promoted to the old generation, so the memory pages the workers share change as little as possible afterwards. Choosing the pages The first list was the four pages that ran the busiest requests on beta. Once the warm-up was live, production showed us which endpoints were still cold. For one deploy, we compared each endpoint’s mean duration in the six minutes after kamal-proxy moved traffic to the new container with the same endpoint an hour later, then multiplied the difference by the number of requests in those six minutes. That gives the extra time each endpoint cost us because it was cold: Endpoint Cold Warm Requests in 6 min Extra seconds Campfire 246 ms 70 ms 6,490 1,140 Projects (JSON API) 84 ms 50 ms 22,077 771 Docs & Files 262 ms 177 ms 4,996 421 To-dos tool 205 ms 113 ms 4,018 371 To-dos (JSON API) 33 ms 16 ms 18,738 320 The pages already in the warm-up showed what to expect: the project page kept a 36 ms gap after a deploy, and the to-do page 10 ms. We’ve proposed adding these five requests, and expect them to add about five to seven seconds to the page step. The two JSON endpoints were a surprise. The warm-up’s page list had no API requests in it, so nothing on the API path had run before the first real request: not the API controllers, and not the Jbuilder templates rendering real records. Precompiling the views covers JSON templates too, but it isn’t a substitute for running the request. Results The warm-up is on for all 68 web hosts. With the first four pages it took 12 to 16 seconds per host: about 2 seconds to precompile the views, 7 to 9 for the 60 requests, and 3.5 for the schema. Deploys take that much longer per host, and we raised the deploy timeout from 30 to 60 seconds to cover it. In Amsterdam, at a peak-hour deploy: During deploy Before After Peak Puma backlog per host 250–2,238 requests 19–223 requests Peak kamal-proxy p99 about 10 s 2.4–4.8 s Peak CPU time per request 201–214 ms 88–132 ms Peak database time per request 56–69 ms 39–47 ms The same eight hosts at three deploys on 1 October, an hour apart, as the warm-up went from one host to four to all eight. The deploy in the middle, with four hosts warmed and four not, shows why every host needed the warm-up. Each warmed host recovered faster on its own: mean request duration peaked at 130 to 173 ms, against 203 to 311 ms on the hosts that weren’t warmed. But the backlog on all eight was about the same, because they were all waiting on the same database. Mean request duration on each host at the 07:21 UTC deploy. Blue hosts warmed up in the master before forking, orange hosts did not. Memory came down too. The workers now share compiled templates, YJIT code and the schema with the master instead of each building their own copy. On beta, the view precompiler alone took a busy worker’s private memory from 174–202 MB to 119–135 MB. Thirty minutes after the deploy, the web containers used about 39 GB less memory than the previous day’s containers at the same age and traffic. Amsterdam served most of our traffic at the times we tested. In Amsterdam, each new container used about 2 GB less just after traffic moved to it, which lowers the peak while the old and new containers overlap. Working with Claude Claude Code helped throughout. It combed through the per-worker backlogs and per-endpoint timings in Prometheus and Loki after each deploy, worked out the cold-versus-warm cost of each endpoint, and prepared the changes and the pull request descriptions with the benchmarks in them. We decided what to try, deployed it and read the results. If you do this Warm the master before it forks. Compile common code and templates and load their schema in the master, so workers inherit that work. With preload_app!, Rack::Builder#warmup runs before the workers start accepting traffic. Use the app’s real requests. Public pages, internal endpoints and synthetic queries warm the paths they run and nothing else. Signed-in requests to real records, frames included, run what production runs. Measure the cold penalty per endpoint. The difference between an endpoint’s cold and warm duration, times its request count after a deploy, ranks the pages worth adding. Ours weren’t the ones we’d have guessed, and two of them were JSON. Check what the warm-up leaves open. List the master’s file descriptors after a warm-up and account for every one before the fork. Two of ours needed changes. Set a time budget. A warm-up that runs long on one slow host fails the deploy on that host. Ours gives the page requests and schema loading a shared 20-second budget, checked before each page or model, puts the most valuable pages first, and logs what it skipped. Reforking from a warm worker, as Pitchfork does, solves the same problem continuously rather than once at boot, and it would warm paths no fixed list of pages covers. We may still get there: our branch brings Instacart’s mold_worker up to date with Puma’s main branch and fixes the bugs we found in it. But warming the master works with the Puma we already run, took a few days to implement, and substantially reduced the queues after deployment.

yesterday • 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in