<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/feed.css?v=208f42b55cff" type="text/css"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:site="https://lalitm.com/feed/ns#"><channel><title>Lalit Maganti (Notes only)</title><link>https://lalitm.com/</link><description>Notes from Lalit Maganti: short observations and things I learned. For everything including articles, use /index.xml.</description><site:notice>This is a feed.
Feeds let you subscribe to updates from this site using a feed reader. Copy this page's URL from your address bar and paste it into your reader.
New to feeds? Read: https://aboutfeeds.com</site:notice><docs>https://aboutfeeds.com</docs><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 17 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://lalitm.com/notes/index.xml" rel="self" type="application/rss+xml"/><item><title>What I'm Changing After A Year Of Blogging</title><link>https://lalitm.com/post/what-i-m-changing-after-a-year-of-blogging/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate><guid>https://lalitm.com/post/what-i-m-changing-after-a-year-of-blogging/</guid><description>Writing for the world is something which has ebbed and flowed for me over time. I’ve always liked sharing my thoughts, starting with websites all the way back when I was a kid. In times when I sustained it, I had various “blog”-like things for technical projects which would often veer into sharing personal beliefs as well.
It’s been a year since my latest attempt at what I expect to be a lifelong endeavor. So I’ve been reflecting on how it’s gone, and that’s led me to make some decisions on how I want to rework things going forward.
Raising the importance of articles I have two goals with my writing:
I want the freedom to write what I want and put as much or as little effort into it as I like. I care a lot about presenting the work I think is good in the best way possible to readers. Although it might not seem like it, these goals are somewhat in conflict with each other. Sometimes I’ll write up TILs or low-effort posts which may not be that interesting to most people, and I don’t want these mixed up with my best work in the eyes of readers encountering me for the first time.</description><content:encoded>&lt;p&gt;Writing for the world is something which has ebbed and flowed for me over time.
I&amp;rsquo;ve always liked sharing my thoughts, starting with websites all the way back
when I was a kid. In times when I sustained it, I had various &amp;ldquo;blog&amp;rdquo;-like things
for technical projects which would often veer into sharing personal beliefs as
well.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been a year since my latest attempt at what I expect to be a lifelong
endeavor. So I&amp;rsquo;ve been reflecting on how it&amp;rsquo;s gone, and that&amp;rsquo;s led me to make
some decisions on how I want to rework things going forward.&lt;/p&gt;
&lt;h2 id="raising-the-importance-of-articles"&gt;Raising the importance of articles&lt;/h2&gt;
&lt;p&gt;I have two goals with my writing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I want the freedom to write what I want and put as much or as little effort
into it as I like.&lt;/li&gt;
&lt;li&gt;I care a lot about presenting the work I think is good in the best way
possible to readers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Although it might not seem like it, these goals are somewhat in conflict with
each other. Sometimes I&amp;rsquo;ll write up TILs or low-effort posts which may not be
that interesting to most people, and I don&amp;rsquo;t want these mixed up with my best
work in the eyes of readers encountering me for the first time.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m by no means unique in this; lots of internet writers have the same dilemma
and take different approaches to dealing with it. Two options I considered are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;High-effort content on the blog, low-effort content on social media.&lt;/li&gt;
&lt;li&gt;Separating content on the blog into high-effort (articles, essays) and
low-effort (notes, snippets, TILs, etc.).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I don&amp;rsquo;t like option 1 because I don&amp;rsquo;t particularly like social media. I was off
it completely for years and have begrudgingly come back because it&amp;rsquo;s useful for
sharing things with people, but it&amp;rsquo;s not something I enjoy. More importantly, I
want everything I write to live on my own site, under my control, rather than
scattered across platforms I don&amp;rsquo;t own.&lt;/p&gt;
&lt;p&gt;Instead I&amp;rsquo;ve taken option 2: I&amp;rsquo;ve decided to more strongly separate articles and
notes from each other.&lt;/p&gt;
&lt;h2 id="knock-on-effects-on-substack"&gt;Knock-on effects on Substack&lt;/h2&gt;
&lt;p&gt;The main way I&amp;rsquo;m doing this: I&amp;rsquo;m &lt;em&gt;no longer going to be sending out notes over
email on Substack&lt;/em&gt;. I initially conceived of Substack as a &amp;ldquo;newsletter&amp;rdquo; of
sorts, rounding up everything new on the blog since the last edition. But over
time I&amp;rsquo;ve come to dislike the effect this has: I don&amp;rsquo;t feel like sending out a
newsletter until a high-effort article is ready to go with it. If nothing is
ready, I end up just waiting, sometimes for more than a month. Why? Because I
don&amp;rsquo;t want a newsletter edition which doesn&amp;rsquo;t put my best self forward.&lt;/p&gt;
&lt;p&gt;This might all be a me thing: I don&amp;rsquo;t know how many of my newsletter readers
mind if I send them low-effort things. But I feel bad about it, and I think
that&amp;rsquo;s enough for me to not do it.&lt;/p&gt;
&lt;p&gt;So instead I plan on reworking it from a newsletter into an articles-only feed.
When an article is ready, I&amp;rsquo;ll publish it on the blog and send it out over email
on the same day. I&amp;rsquo;m going to try to do this on Saturdays, though I can&amp;rsquo;t
promise I&amp;rsquo;ll always manage it.&lt;/p&gt;
&lt;p&gt;At the bottom of each email, I&amp;rsquo;ll also leave a short &amp;ldquo;since I last sent you an
article, I published thoughts on X, Y and Z&amp;rdquo;. But it&amp;rsquo;ll be a one line footnote,
not the full content as I do today.&lt;/p&gt;
&lt;p&gt;If folks &lt;em&gt;do&lt;/em&gt; want to subscribe to everything, my suggestion (see the
&lt;a href="https://lalitm.com/page/subscribe/"&gt;subscribe page&lt;/a&gt;) is to use RSS or an
RSS-to-email service. Setting one up on my side would require either managing
emails myself (which I think would be painful), manually copying posts to
Substack (the tedium disincentivizes me from writing the short posts), or paying
for a second email service alongside Substack (which I don&amp;rsquo;t want to do unless
someone will actually use it). If you want everything from me in your inbox,
&lt;a href="mailto:lalitmaganti@gmail.com"&gt;reach out&lt;/a&gt; as it will help me decide if I should
change my mind.&lt;/p&gt;
&lt;h2 id="site-changes"&gt;Site changes&lt;/h2&gt;
&lt;p&gt;This also knocks on to the homepage. In my previous
&lt;a href="https://lalitm.com/post/homepage-redesign-light-mode-and-more/"&gt;redesign post&lt;/a&gt;
I discussed how I switched to a two-pane dashboard feel and categorized what I
wrote into three things: essays, write-ups and notes.&lt;/p&gt;
&lt;p&gt;While this was certainly cool and I liked it, I also constantly fell into the
trap of having content which could debatably fall into either essays or
write-ups. So I&amp;rsquo;ve erased that distinction: both are now simply articles,
reflecting the effort and thought I&amp;rsquo;ve put into them. Notes remain much the same
as before.&lt;/p&gt;
&lt;p&gt;I also went back to a single chronological feed, because readers gave me the
feedback that they liked seeing at a glance if something was new, rather than
scanning across three distinct sections to try to work it out. Articles are
still clearly differentiated from notes though: they&amp;rsquo;re bigger, bolder and have
explicit labels.&lt;/p&gt;
&lt;p&gt;Unrelatedly, I also spent a bunch of time improving RSS. After dogfooding my own
feeds, I realized I had made some real mistakes in the way the feed was
generated: content was losing all its links and being shortened. Now full
articles are published properly to the feed (though things like sidenotes still
won&amp;rsquo;t look the same as on the website).&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;All in all, I feel a lot happier after figuring out how I wanted to handle high-
vs low-effort content and deciding on making the Substack articles-only. My best
work now gets presented in the way I want, and that makes me feel a lot freer to
publish the imperfect stuff too.&lt;/p&gt;</content:encoded></item><item><title>Perfetto v57: fixing PyTorch traces, plus journald logs and an AI skill</title><link>https://lalitm.com/post/perfetto-v57/</link><pubDate>Fri, 03 Jul 2026 00:23:00 +0100</pubDate><guid>https://lalitm.com/post/perfetto-v57/</guid><description>We just released Perfetto v57 and I wanted to share the new things I’m most excited about. This is something I wanted to do for past releases but I just never quite got round to it. It’s also something I plan on doing more of going forward: there might even be dedicated pieces if I think the feature deserves it!
PyTorch traces reliably work! What I’m most excited about in this release isn’t a feature but a bugfix. If you used the PyTorch profiler and opened the resulting trace in Perfetto, there was a decent chance some of your events would just not show up; specifically this would happen when these events overlapped each other on a single track.
Technically, PyTorch is in the wrong here. The Chrome Trace Event (JSON) format says duration events on a track have to nest and can’t overlap; if you need overlap, you’re supposed to use async events. chrome://tracing appears to handle them, but its rendering is actually buggy as soon as a trace has real overlaps; people just learned to live with it. So when a bug came in January about overlapping events being broken, I closed it as working-as-intended because I couldn’t see any easy fix from our end.</description><content:encoded>&lt;p&gt;We just
&lt;a href="https://github.com/google/perfetto/releases/tag/v57.1"&gt;released Perfetto v57&lt;/a&gt;
and I wanted to share the new things I&amp;rsquo;m most excited about. This is something I
wanted to do for past releases but I just never quite got round to it. It&amp;rsquo;s also
something I plan on doing more of going forward: there might even be dedicated
pieces if I think the feature deserves it!&lt;/p&gt;
&lt;h2 id="pytorch-traces-reliably-work"&gt;PyTorch traces reliably work!&lt;/h2&gt;
&lt;p&gt;What I&amp;rsquo;m most excited about in this release isn&amp;rsquo;t a feature but a bugfix. If you
used the
&lt;a href="https://docs.pytorch.org/tutorials/recipes/recipes/profiler_recipe.html"&gt;PyTorch profiler&lt;/a&gt;
and opened the resulting trace in Perfetto, there was a decent chance some of
your events would just not show up; specifically this would happen when these
events overlapped each other on a single track.&lt;/p&gt;
&lt;p&gt;Technically, PyTorch is in the wrong here. The Chrome Trace Event (JSON) format
says duration events on a track have to nest and can&amp;rsquo;t overlap; if you need
overlap, you&amp;rsquo;re supposed to use async events. &lt;code&gt;chrome://tracing&lt;/code&gt; appears to
handle them, but its rendering is actually buggy as soon as a trace has real
overlaps; people just learned to live with it. So when a
&lt;a href="https://github.com/google/perfetto/issues/4280"&gt;bug&lt;/a&gt; came in January about
overlapping events being broken, I closed it as working-as-intended because I
couldn&amp;rsquo;t see any easy fix from our end.&lt;/p&gt;
&lt;p&gt;Then over time, I realized it wasn&amp;rsquo;t just one person with a weird trace. The bug
had affected many different PyTorch traces, and even
&lt;a href="https://github.com/google/perfetto/issues/6277"&gt;JAX traces&lt;/a&gt;. I even discovered
that people were maintaining forks and hosting old versions of Perfetto just to
deal with this issue. This is basically
&lt;a href="https://www.hyrumslaw.com/"&gt;Hyrum&amp;rsquo;s Law&lt;/a&gt;: it doesn&amp;rsquo;t much matter what the spec
says, once enough people rely on some behaviour, that behaviour is your
contract.&lt;/p&gt;
&lt;p&gt;This is also a very similar situation to what I discussed in
&lt;a href="https://lalitm.com/perfetto-oss-company-prio/"&gt;Perfetto and open source&lt;/a&gt;: it
was genuinely hard for me to tell just how many people it was affecting at the
beginning: if I had understood, I would have dealt with this very differently.&lt;/p&gt;
&lt;p&gt;In any case, in v57, Perfetto now imports the overlapping events instead of
dropping them, overflowing the extras onto a different line in the same UI
track. I do have to credit &lt;a href="https://github.com/i-komarov"&gt;@i_komarov&lt;/a&gt; who helped
me understand how big this issue was and to converge on the fix.&lt;/p&gt;
&lt;h2 id="recording-systemd-journald-logs"&gt;Recording systemd journald logs&lt;/h2&gt;
&lt;p&gt;Perfetto has always tried to give you as much context as possible about what the
system is doing and put it all on a single timeline. One missing piece of that
on Linux has been in logs from journald; there&amp;rsquo;s a ton of useful information
there but until now it was hard to correlate that with e.g. scheduling or other
app level information.&lt;/p&gt;
&lt;p&gt;With v57, there&amp;rsquo;s now a &lt;code&gt;linux.systemd_journald&lt;/code&gt; data source that records
journald entries straight into the trace, on the same timeline as everything
else. You can filter by minimum syslog priority, by &lt;code&gt;SYSLOG_IDENTIFIER&lt;/code&gt;, or by
systemd unit, so you are not pulling in the entire journal when you only care
about one service.&lt;/p&gt;
&lt;p&gt;To show it in action, here&amp;rsquo;s
&lt;a href="https://ui.perfetto.dev/#!/?url=https://lalitm.com/traces/journald-demo.pftrace"&gt;a trace I recorded on my own machine&lt;/a&gt;.
Alongside a few log lines I generated myself, it happened to capture a real
udisks SMART error that fired while I was recording; exactly the kind of thing
you&amp;rsquo;d otherwise miss.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/perfetto-v57/journald-logs.png" alt="Journald logs on the Perfetto timeline, with the log panel showing the captured entries including a udisks SMART error"&gt;&lt;/p&gt;
&lt;p&gt;I have to shout out &lt;a href="https://github.com/staticfloat"&gt;@staticfloat&lt;/a&gt; who
contributed this feature; thanks a lot for doing this!&lt;/p&gt;
&lt;h2 id="an-ai-skill-for-recording-and-analysis"&gt;An AI skill for recording and analysis&lt;/h2&gt;
&lt;p&gt;For a while now, people have been building their own AI tooling around Perfetto.
&lt;a href="https://github.com/Gracker/SmartPerfetto"&gt;SmartPerfetto&lt;/a&gt; and
&lt;a href="https://github.com/antarikshc/perfetto-mcp"&gt;perfetto-mcp&lt;/a&gt; are two public
examples I&amp;rsquo;ve come across but many teams inside Google have built their versions
of these which each handle a subset of Perfetto&amp;rsquo;s functionality. To us, this is
a sign that there&amp;rsquo;s demand out there to build something upstream so that
everyone can benefit.&lt;/p&gt;
&lt;p&gt;v57 includes an installable AI skill that works with Claude Code, Codex, Pi,
Opencode, Antigravity, and anything else that speaks the Agent Skills format. It
explains to the agent how to record traces (Android only for now!) and then
query them. It also includes dedicated workflows for debugging Android memory, a
specific area of focus for us; we plan on expanding these workflows over time
including to Linux.&lt;/p&gt;
&lt;p&gt;The skill is just one piece of a much larger AI plan, which I wrote up as
&lt;a href="https://github.com/google/perfetto/discussions/5763"&gt;an RFC&lt;/a&gt;. We will be doing
much more in this space in the coming months.&lt;/p&gt;
&lt;h2 id="state-tracks"&gt;State tracks&lt;/h2&gt;
&lt;p&gt;State tracks are a new kind of primitive in Perfetto, sitting alongside slices
and counters. They represent something that is always in exactly one state: a
connection that is open or closed, a device that is on or off, a worker that is
idle or busy. You could already model this with slices, by emitting a slice that
starts when a state begins and ends when it changes. This works but you have to
be careful to emit events &lt;em&gt;exactly&lt;/em&gt; at the same timestamps or you end up having
gaps.&lt;/p&gt;
&lt;p&gt;With a state track you don&amp;rsquo;t emit start and end events at all. At each point
where the state changes, you emit one event that says what the state is now. The
previous state ends automatically. Clearing the track marks it idle. This just
makes it a lot simpler to manage.&lt;/p&gt;
&lt;p&gt;While the simpler emission model is the obvious win, the reason we made it a
first-class primitive goes further than that. Because a state track holds
exactly one value at any moment, it can never overlap itself, and the trace
processor can lean on that guarantee to treat it specially. You can attach
arguments that change as the state does. And in ring-buffer traces, where old
events get overwritten as the buffer wraps, you can periodically re-emit the
current state, so a trace that starts mid-stream still knows what state
everything is in.&lt;/p&gt;
&lt;p&gt;State tracks are still quite new so there are likely to be some rough edges
here, but we think they&amp;rsquo;ll become an important primitive: because they&amp;rsquo;re
guaranteed never to overlap, you can do things with them you simply couldn&amp;rsquo;t
before.&lt;/p&gt;
&lt;h2 id="querying-traces-over-https"&gt;Querying traces over HTTPS&lt;/h2&gt;
&lt;p&gt;I recently discovered that quite a few folks have written scripts which download
a trace and then run queries against it with the trace_processor CLI. Which made
me think: why don&amp;rsquo;t we just support that natively inside the CLI itself?&lt;/p&gt;
&lt;p&gt;So in v57, &lt;code&gt;trace_processor&lt;/code&gt; takes an &lt;code&gt;http(s)&lt;/code&gt; URL or a &lt;code&gt;ui.perfetto.dev&lt;/code&gt; share
link directly. It streams the trace and runs your query with no local copy. Here
it is against the journald trace from the previous section:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ trace_processor query \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;https://lalitm.com/traces/journald-demo.pftrace&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;select log_source, count(*) as n from logs where msg is not null group by log_source&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;&amp;#34;log_source&amp;#34;,&amp;#34;n&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;systemd_journald&amp;#34;,30
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s one of those small polish features which you wouldn&amp;rsquo;t necessarily think of
immediately but when you have it, you&amp;rsquo;d never want to go back.&lt;/p&gt;
&lt;h2 id="lightning-round"&gt;Lightning round&lt;/h2&gt;
&lt;p&gt;A few smaller things also landed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shift + mouse wheel scrolls the timeline horizontally.&lt;/strong&gt; A tiny change that
removes a daily papercut when you are panning around a wide trace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The query results grid got a real overhaul.&lt;/strong&gt; You can sort, reorder, and
hide columns, the sidebar collapses, and SQL errors now show up in the grid
instead of disappearing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adjacent slices that share a boundary render with a 1px gap,&lt;/strong&gt; so a run of
back-to-back slices reads as separate slices rather than one solid block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saved textproto record configs are editable,&lt;/strong&gt; saving the trouble of
downloading the config, deleting it, editing it locally, and then reuploading.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="wrap-up"&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Overall this was a pretty solid release of Perfetto; it&amp;rsquo;s always satisfying to
fix a bug blocking a lot of people. AI skills and state tracks are both first
steps in a much longer journey, and journald logs are an important new
capability that improves our Linux support. The rest are mostly a bunch of small
quality of life improvements which are always nice to see!&lt;/p&gt;</content:encoded></item><item><title>On "When impressive performance gains do not matter"</title><link>https://lalitm.com/post/on-when-impressive-performance/</link><pubDate>Tue, 30 Jun 2026 16:25:00 +0100</pubDate><guid>https://lalitm.com/post/on-when-impressive-performance/</guid><description>When impressive performance gains do not matter is a very nice article covering some ways in which going after performance alone is not sufficient without considering the wider picture. It resonated a lot with how I think about performance.
If there are multiple bottlenecks in the pipeline—and with these systems, this is common—the overall throughput will not improve until every last bottleneck is removed.
His focus is on distributed systems bottlenecks, but I’ve hit the same “do-nothing” speedups when optimizing client side programs. Usually this comes from spending a lot of time thinking something was the bottleneck when it wasn’t. CPU profiling is where this bites me most: it tells me “function X is taking 30% of the cycles” and I think “oooo, there’s a lot of gains to be made there”. I build a microbenchmark for X, optimize it and there’s only a marginal gain at the high level. While disappointing, I’ve become used to it over time and internalized that performance is highly non-linear and actually knowing where the problem lies is really hard.</description><content:encoded>&lt;p&gt;&lt;a href="https://blog.colinbreck.com/when-impressive-performance-gains-do-not-matter/"&gt;When impressive performance gains do not matter&lt;/a&gt;
is a very nice article covering some ways in which going after performance alone
is not sufficient without considering the wider picture. It resonated a lot with
how I think about performance.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If there are multiple bottlenecks in the pipeline—and with these systems, this
is common—the overall throughput will not improve until every last bottleneck
is removed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;His focus is on distributed systems bottlenecks, but I&amp;rsquo;ve hit the same
&amp;ldquo;do-nothing&amp;rdquo; speedups when optimizing client side programs. Usually this comes
from spending a lot of time &lt;em&gt;thinking&lt;/em&gt; something was the bottleneck when it
wasn&amp;rsquo;t. CPU profiling is where this bites me most: it tells me &amp;ldquo;function X is
taking 30% of the cycles&amp;rdquo; and I think &amp;ldquo;oooo, there&amp;rsquo;s a lot of gains to be made
there&amp;rdquo;. I build a microbenchmark for X, optimize it and there&amp;rsquo;s only a marginal
gain at the high level. While disappointing, I&amp;rsquo;ve become used to it over time
and internalized that performance is highly non-linear and actually knowing
where the problem lies is really hard.&lt;/p&gt;
&lt;p&gt;One specific pattern I&amp;rsquo;ve run into several times: if you are processing a lot of
data and that requires you to hit main memory, you can do quite a bit of
computation at the same time on that data for free. This is because out-of-order
processors are very good at doing computation while stalled on RAM. So removing
instructions in paths like that won&amp;rsquo;t actually matter; what matters much more is
your data structures and reducing how much memory you&amp;rsquo;re touching.&lt;/p&gt;
&lt;p&gt;While I&amp;rsquo;m on this topic, I have to shout out Matt Kulukundis&amp;rsquo;s
&lt;a href="https://www.youtube.com/watch?v=ncHmEUmJZf4"&gt;CppCon talk on Abseil&amp;rsquo;s flat hash map&lt;/a&gt;,
as it&amp;rsquo;s what helped me distill my experiences into actual rules of thumb around
building performant systems. I would highly recommend you watch it if you are
remotely interested in performance!&lt;/p&gt;
&lt;p&gt;As for the post, I also like the closing reflection:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Performance work can be incredibly challenging, but it is also a discipline
for intimately understanding complex systems and engineering better products.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That matches my own experience: the lasting value isn&amp;rsquo;t any single speedup, it&amp;rsquo;s
the improved understanding of the system you&amp;rsquo;re building and is invaluable for
making things better in the long term.&lt;/p&gt;</content:encoded></item><item><title>syntaqlite 0.6: SQLite dot commands and pyodide</title><link>https://lalitm.com/post/syntaqlite-06/</link><pubDate>Wed, 17 Jun 2026 21:53:00 +0100</pubDate><guid>https://lalitm.com/post/syntaqlite-06/</guid><description>Since my original launch post for syntaqlite, I’ve been quietly working away on it in the background. A lot of the work has been fixing correctness bugs which I discovered as I integrated it into production as the parser for PerfettoSQL in the Perfetto trace processor: as I wrote previously, this has been my dream for over 8 years so it’s amazing to see it finally realized.
Just today, I released syntaqlite 0.6, the most interesting release since the original launch, so I wanted to talk about what’s new.
The biggest step forward for real world applicability is that we now support SQLite dot commands:
.print 'running foo now'; .read foo.sql; .print 'running bar now'; .read bar.sql; select * from baz; -- ^ baz will error if it's not part of the schema! SQLite scripts are very common in the wild and in the past we would simply error on dot commands like .read and .print, causing spurious warnings and an inability to format files like this properly. Now, these lines will be silently ignored while still parsing, formatting and validating the SQL inside!</description><content:encoded>&lt;p&gt;Since my &lt;a href="https://lalitm.com/post/syntaqlite/"&gt;original launch post&lt;/a&gt; for syntaqlite,
I&amp;rsquo;ve been quietly working away on it in the background. A lot of the work has
been fixing correctness bugs which I discovered as I integrated it into
production as the parser for PerfettoSQL in the Perfetto trace processor: as I
wrote previously, this has been my dream for over 8 years so it&amp;rsquo;s amazing to see
it finally realized.&lt;/p&gt;
&lt;p&gt;Just today, I released
&lt;a href="https://github.com/LalitMaganti/syntaqlite/releases/tag/v0.6.0"&gt;syntaqlite 0.6&lt;/a&gt;,
the most interesting release since the original launch, so I wanted to talk
about what&amp;rsquo;s new.&lt;/p&gt;
&lt;p&gt;The biggest step forward for real world applicability is that we now support
&lt;em&gt;SQLite dot commands&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;running foo now&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&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="k"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&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="n"&gt;print&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;running bar now&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&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="k"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&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;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&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;-- ^ baz will error if it&amp;#39;s not part of the schema!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SQLite scripts are very common in the wild and in the past we would simply error
on dot commands like &lt;code&gt;.read&lt;/code&gt; and &lt;code&gt;.print&lt;/code&gt;, causing spurious warnings and an
inability to format files like this properly. Now, these lines will be silently
ignored while still parsing, formatting and validating the SQL inside!&lt;/p&gt;
&lt;p&gt;In followup releases, I plan on making syntaqlite more aware of the &lt;em&gt;semantics&lt;/em&gt;
of these dot commands, especially &lt;code&gt;.read&lt;/code&gt;: we should be able to do things like
verify the file exists or to use the contents of those files for populating
schemas etc.&lt;/p&gt;
&lt;p&gt;Along with this, the release of
&lt;a href="https://blog.pyodide.org/posts/314-release/"&gt;Pyodide 314.0&lt;/a&gt; last week means
that we can now publish Python wheels to PyPI for Pyodide: I&amp;rsquo;ve taken advantage
of that to immediately add support for it. So you can now go to the
&lt;a href="https://pyodide.org/en/stable/console.html"&gt;Pyodide playground&lt;/a&gt; and run the
following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;micropip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;micropip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;syntaqlite&amp;#39;&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;syntaqlite&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syntaqlite&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Syntaqlite&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="c1"&gt;# Formatting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select 1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword_case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;upper&amp;#39;&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;# SELECT 1;&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;# Analysis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syntaqlite&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Schema&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;ddl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;CREATE TABLE users (id INT, name TEXT, email TEXT, active INT)&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;SELECT nme FROM users WHERE active = 1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&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;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;syntaqlite&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnalysisOutput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TEXT&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;render_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;syntaqlite&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RenderOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;query.sql&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="c1"&gt;# warning: unknown column &amp;#39;nme&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# --&amp;gt; query.sql:1:8&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;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1 | SELECT nme FROM users WHERE active = 1&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;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# = help: did you mean &amp;#39;name&amp;#39;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is another step on the journey towards a 1.0 release which I can now see
appearing on the horizon!&lt;/p&gt;</content:encoded></item><item><title>TIL: Iroh: peer-to-peer networking for app developers</title><link>https://lalitm.com/post/til-iroh/</link><pubDate>Tue, 16 Jun 2026 02:36:00 +0100</pubDate><guid>https://lalitm.com/post/til-iroh/</guid><description>I came across Iroh (via, via) today as it hit 1.0 and found it a really interesting solution to a problem I knew existed but had not thought a lot about.
Judging from the comment sections, it seems pretty clear that lots of people are confused as to exactly what Iroh is. I don’t think their launch post does their product justice at all, and their tagline is “IP addresses break, dial keys instead” which sounds cool, but if you think about it for just a second, you’ll end up with lots of questions. The biggest one is: “so how is this different from a mesh VPN like Tailscale, ZeroTier, Netbird, etc.?”
It’s only after reading a lot of developers’ comments on the threads that I feel I understand: Iroh is aimed at application developers who want to communicate P2P between machines running their app, while mesh networks are aimed at network admins who want to connect devices they own/manage together.</description><content:encoded>&lt;p&gt;I came across &lt;a href="https://www.iroh.computer/"&gt;Iroh&lt;/a&gt;
(&lt;a href="https://news.ycombinator.com/item?id=48542480"&gt;via&lt;/a&gt;,
&lt;a href="https://lobste.rs/s/cslljn/iroh_1_0_dial_keys_not_ips"&gt;via&lt;/a&gt;) today as it hit
1.0 and found it a really interesting solution to a problem I knew existed but
had not thought a lot about.&lt;/p&gt;
&lt;p&gt;Judging from the comment sections, it seems pretty clear that &lt;em&gt;lots&lt;/em&gt; of people
are confused as to exactly &lt;em&gt;what&lt;/em&gt; Iroh is. I don&amp;rsquo;t think their
&lt;a href="https://www.iroh.computer/blog/v1"&gt;launch post&lt;/a&gt; does their product justice at
all, and their tagline is &amp;ldquo;IP addresses break, dial keys instead&amp;rdquo; which sounds
cool, but if you think about it for just a second, you&amp;rsquo;ll end up with lots of
questions. The biggest one is: &amp;ldquo;so how is this different from a mesh VPN like
Tailscale, ZeroTier, Netbird, etc.?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s only after reading a lot of developers&amp;rsquo; comments on the threads that I feel
I understand: Iroh is aimed at &lt;em&gt;application&lt;/em&gt; developers who want to communicate
P2P between machines running their app, while mesh networks are aimed
at &lt;em&gt;network admins&lt;/em&gt; who want to connect devices they own/manage together.&lt;/p&gt;
&lt;p&gt;Imagine you&amp;rsquo;re developing a peer-to-peer messaging app where, on one side, the
user is on a mobile device which constantly roams between WiFi/mobile data (so
doesn&amp;rsquo;t have a stable IP address), and the other side is a laptop sitting behind
a NAT and a CGNAT.&lt;/p&gt;
&lt;p&gt;If I want to get these folks to talk to each other, I need some mechanism to
deal with two problems at once: the mobile&amp;rsquo;s address keeps changing as it roams,
and neither side has a stable public IP the other can reach directly. Now,
historically to fix these problems, the developer would need to have an
intermediary server which both endpoints talk to so they can share state, create
a protocol to exchange IP addresses, and do NAT hole-punching to set up the
connection, falling back to acting as a real relay if none of that is possible
[1].&lt;/p&gt;
&lt;p&gt;What Iroh does instead is standardise this as a set of libraries and protocols
as well as offer it as a managed service that you can just drop into your app.
They also appear to be leaning heavily on standards like QUIC instead of
reinventing the wheel, which is always nice to see.&lt;/p&gt;
&lt;p&gt;Overall, this is a pretty neat idea for a difficult problem! I&amp;rsquo;m definitely
curious to try it out; I don&amp;rsquo;t have a use case in mind, but I&amp;rsquo;m now on the
lookout for one!&lt;/p&gt;
&lt;p&gt;[1] If you want to learn more on this, I would strongly suggest reading this
excellent
&lt;a href="https://tailscale.com/blog/how-nat-traversal-works"&gt;Tailscale article&lt;/a&gt; which I
learned a lot from myself.&lt;/p&gt;</content:encoded></item><item><title>Which country voted the best at Eurovision?</title><link>https://lalitm.com/post/which-country-voted-best-at-eurovision/</link><pubDate>Sun, 17 May 2026 15:30:00 +0100</pubDate><guid>https://lalitm.com/post/which-country-voted-best-at-eurovision/</guid><description>Eurovision was on yesterday. I’ve never been interested much in the musical side but the weird political dynamics of Eurovision voting have always fascinated me; I tune in each year just for them and somewhat snarky commentary of Graham Norton, the UK commentator.
As I was watching the jury votes come in, a question popped into my head: Which country has voted the best in Eurovision? That is, which country was best at picking the eventual top 10 and in the right order?
Strangely enough, while there’s plenty of work on voting blocs and bilateral biases at Eurovision, most of it asks who votes for whom; I wanted to ask who votes accurately. I couldn’t find anyone asking the question that way, so I decided to do some data analysis myself.
The metric To begin to answer this question, I first needed to formalize what “best” even means. That is, some mathematical notion of “good” and “bad”.</description><content:encoded>&lt;p&gt;Eurovision was on yesterday. I&amp;rsquo;ve never been interested much in the musical side
but the weird political dynamics of Eurovision voting have always fascinated me;
I tune in each year just for them and somewhat snarky commentary of Graham
Norton, the UK commentator.&lt;/p&gt;
&lt;p&gt;As I was watching the jury votes come in, a question popped into my head: Which
country has &lt;em&gt;voted&lt;/em&gt; the best in Eurovision? That is, which country was best at
picking the eventual top 10 and in the right order?&lt;/p&gt;
&lt;p&gt;Strangely enough, while there&amp;rsquo;s
&lt;a href="https://en.wikipedia.org/wiki/Voting_at_the_Eurovision_Song_Contest#Regional_bloc_voting"&gt;plenty&lt;/a&gt;
&lt;a href="https://arxiv.org/abs/physics/0505071"&gt;of&lt;/a&gt;
&lt;a href="https://www.jasss.org/9/2/1.html"&gt;work&lt;/a&gt; on voting blocs and bilateral biases at
Eurovision, most of it asks &lt;em&gt;who votes for whom&lt;/em&gt;; I wanted to ask &lt;em&gt;who votes
accurately&lt;/em&gt;. I couldn&amp;rsquo;t find anyone asking the question that way, so I decided
to do some data analysis myself.&lt;/p&gt;
&lt;h2 id="the-metric"&gt;The metric&lt;/h2&gt;
&lt;p&gt;To begin to answer this question, I first needed to formalize what &amp;ldquo;best&amp;rdquo; even
means. That is, some mathematical notion of &amp;ldquo;good&amp;rdquo; and &amp;ldquo;bad&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The simplest measure I could come up with is &amp;ldquo;of the 10 countries you gave
points to, how many were in the actual top 10?&amp;rdquo;. The problem is that it can&amp;rsquo;t
tell apart a voter who gave the eventual winner 12 from one who gave them 1.
That feels wrong: an allocation is a &lt;em&gt;ranking&lt;/em&gt;, not just a set, so position
ought to matter.&lt;/p&gt;
&lt;p&gt;To weight position, I needed some scheme for how much position 1 matters
compared to position 10. Turns out Eurovision itself has already answered this
question: the 12, 10, 8, 7, 6, 5, 4, 3, 2, 1 it hands out is exactly such a
scheme. So I just reused it.&lt;/p&gt;
&lt;p&gt;So I tried the next simplest thing: for each country, I have two numbers: what
the voter awarded it, and how many points it actually received at the end.
Multiply them together, sum across all countries, and divide by the maximum
value that sum could possibly take if the voter had ranked everything perfectly
(awarded 12 to the actual winner, 10 to second place, and so on). The result is
between 0 and 1, where 1 is a perfect vote.&lt;/p&gt;
&lt;p&gt;A worked example. The actual jury top 5 in 2023, by points received, was Sweden
(340), Israel (177), Italy (176), Finland (150), Estonia (146). Imagine a
voter&amp;rsquo;s jury panel awarded (12 → Italy, 10 → Sweden, 8 → Estonia, 7 → Israel, 6
→ Finland).&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the voter&amp;rsquo;s score:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Voter awarded&lt;/th&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;× Actual points&lt;/th&gt;
&lt;th&gt;=&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Italy&lt;/td&gt;
&lt;td&gt;× 176&lt;/td&gt;
&lt;td&gt;2,112&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Sweden&lt;/td&gt;
&lt;td&gt;× 340&lt;/td&gt;
&lt;td&gt;3,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Estonia&lt;/td&gt;
&lt;td&gt;× 146&lt;/td&gt;
&lt;td&gt;1,168&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Israel&lt;/td&gt;
&lt;td&gt;× 177&lt;/td&gt;
&lt;td&gt;1,239&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Finland&lt;/td&gt;
&lt;td&gt;× 150&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,819&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And here&amp;rsquo;s the perfect score for this voter, what they&amp;rsquo;d have got if they had
awarded the 12 to the actual top scorer, the 10 to second, and so on:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Voter awarded&lt;/th&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;× Actual points&lt;/th&gt;
&lt;th&gt;=&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Sweden&lt;/td&gt;
&lt;td&gt;× 340&lt;/td&gt;
&lt;td&gt;4,080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Israel&lt;/td&gt;
&lt;td&gt;× 177&lt;/td&gt;
&lt;td&gt;1,770&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Italy&lt;/td&gt;
&lt;td&gt;× 176&lt;/td&gt;
&lt;td&gt;1,408&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Finland&lt;/td&gt;
&lt;td&gt;× 150&lt;/td&gt;
&lt;td&gt;1,050&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Estonia&lt;/td&gt;
&lt;td&gt;× 146&lt;/td&gt;
&lt;td&gt;876&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9,184&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So this voter&amp;rsquo;s score is 8,819 / 9,184 = &lt;strong&gt;0.96&lt;/strong&gt;. Close to perfect: the same
five countries are there, just slightly out of order.&lt;/p&gt;
&lt;h2 id="is-it-the-right-metric"&gt;Is it the right metric?&lt;/h2&gt;
&lt;p&gt;Obviously this is not the only metric I could have used and there are &lt;em&gt;many&lt;/em&gt;
other candidates out there: least-squares distance to the perfect vote, NDCG@10
(a standard ranking measure used in search), and Pearson correlation.&lt;/p&gt;
&lt;p&gt;I went with the one above because I wanted the simplest thing which still
worked. Least-squares is the only &amp;ldquo;simpler&amp;rdquo; alternative I can think of, but its
scores end up squashed into a narrow range. The best voter across nine contests
only reaches around 0.42 out of a theoretical max of 1.0, which is the highest
score in the data but doesn&amp;rsquo;t &lt;em&gt;read&lt;/em&gt; like a good number. The metric above sits
more naturally on a 0-to-1 scale: the best voters land in the 0.8s, the worst in
the 0.6s, and 1.0 still means a perfect vote.&lt;/p&gt;
&lt;p&gt;In any case, I did run &lt;em&gt;all&lt;/em&gt; the metrics listed above and they mostly agreed,
with the same countries clustered at the top and bottom regardless of which one
I used. But the headline #1 &lt;em&gt;did&lt;/em&gt; shift between the top three or four countries
depending on which metric I picked.&lt;/p&gt;
&lt;h2 id="the-answer"&gt;The answer&lt;/h2&gt;
&lt;p&gt;I picked 2016 as the start because that&amp;rsquo;s when Eurovision split each country&amp;rsquo;s
contribution into separate jury and televote slates, each awarding their own 12,
10, 8, &amp;hellip; points. Older contests used a single combined vote, so the data
doesn&amp;rsquo;t decompose the same way.&lt;/p&gt;
&lt;p&gt;Across these nine contests, here&amp;rsquo;s the full table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Spain&lt;/td&gt;
&lt;td&gt;0.815&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Lithuania&lt;/td&gt;
&lt;td&gt;0.810&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Belgium&lt;/td&gt;
&lt;td&gt;0.804&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;0.798&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Netherlands&lt;/td&gt;
&lt;td&gt;0.797&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Malta&lt;/td&gt;
&lt;td&gt;0.792&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Sweden&lt;/td&gt;
&lt;td&gt;0.790&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Austria&lt;/td&gt;
&lt;td&gt;0.789&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Iceland&lt;/td&gt;
&lt;td&gt;0.788&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Norway&lt;/td&gt;
&lt;td&gt;0.783&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Israel&lt;/td&gt;
&lt;td&gt;0.778&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Latvia&lt;/td&gt;
&lt;td&gt;0.777&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Finland&lt;/td&gt;
&lt;td&gt;0.777&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Moldova&lt;/td&gt;
&lt;td&gt;0.774&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Poland&lt;/td&gt;
&lt;td&gt;0.773&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Romania&lt;/td&gt;
&lt;td&gt;0.771&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;Estonia&lt;/td&gt;
&lt;td&gt;0.770&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;San Marino&lt;/td&gt;
&lt;td&gt;0.770&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Ireland&lt;/td&gt;
&lt;td&gt;0.762&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Armenia&lt;/td&gt;
&lt;td&gt;0.762&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;Cyprus&lt;/td&gt;
&lt;td&gt;0.761&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;Denmark&lt;/td&gt;
&lt;td&gt;0.757&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;Albania&lt;/td&gt;
&lt;td&gt;0.754&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Slovenia&lt;/td&gt;
&lt;td&gt;0.751&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Australia&lt;/td&gt;
&lt;td&gt;0.749&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;Azerbaijan&lt;/td&gt;
&lt;td&gt;0.746&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;Serbia&lt;/td&gt;
&lt;td&gt;0.745&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;Portugal&lt;/td&gt;
&lt;td&gt;0.743&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;Switzerland&lt;/td&gt;
&lt;td&gt;0.739&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;Czechia&lt;/td&gt;
&lt;td&gt;0.735&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;Croatia&lt;/td&gt;
&lt;td&gt;0.733&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;United Kingdom&lt;/td&gt;
&lt;td&gt;0.723&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;0.712&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;Georgia&lt;/td&gt;
&lt;td&gt;0.707&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;North Macedonia&lt;/td&gt;
&lt;td&gt;0.706&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;Greece&lt;/td&gt;
&lt;td&gt;0.691&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;Bulgaria&lt;/td&gt;
&lt;td&gt;0.688&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;Ukraine&lt;/td&gt;
&lt;td&gt;0.675&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;Italy&lt;/td&gt;
&lt;td&gt;0.666&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;Montenegro&lt;/td&gt;
&lt;td&gt;0.625&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Spain edges it, but only barely. The top seven are within 0.025 of each other,
narrower than the year-to-year variation of any individual country, so calling
Spain &amp;ldquo;the&amp;rdquo; best voter is too strong a statement.&lt;/p&gt;
&lt;p&gt;A few countries often discussed in bloc-voting analyses do show up near the
bottom; parts of the Balkans (Croatia, North Macedonia, Bulgaria, Montenegro)
and Greece. But it&amp;rsquo;s only suggestive: others from the same blocs (Cyprus,
Albania, Serbia, Armenia) sit comfortably mid-table, so this metric isn&amp;rsquo;t really
measuring bloc voting; it&amp;rsquo;s measuring accuracy, which is a different thing.
Properly disentangling the two would need more analysis work I don&amp;rsquo;t really want
to do!&lt;/p&gt;
&lt;h2 id="a-note-on-data"&gt;A note on data&lt;/h2&gt;
&lt;p&gt;All voting data is from
&lt;a href="https://github.com/EurovisionAPI/dataset"&gt;EurovisionAPI/dataset&lt;/a&gt;, a scraped
mirror of the EBU&amp;rsquo;s published results. As a sanity check on the two most recent
contests I parsed the Wikipedia voting tables for 2024 and 2025 and diffed every
cell against the dataset and got no differences.&lt;/p&gt;
&lt;p&gt;All my code is available on
&lt;a href="https://github.com/LalitMaganti/eurovision-correlation-analysis"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>Rendering 100k trace events faster with exponential search</title><link>https://lalitm.com/post/exponential-search/</link><pubDate>Tue, 03 Feb 2026 21:07:04 +0000</pubDate><guid>https://lalitm.com/post/exponential-search/</guid><description>We’ve recently been looking into optimizing rendering performance of the Perfetto UI on large traces. We discovered that there was some inefficiency in our data fetching logic, especially when you’re very zoomed out.
In this case, there can be a lot of slices (spans) which are so small that they take less than one pixel of width. So for each pixel, we need to figure out “what is the event which we should draw for this pixel”. Over time we’ve come to the conclusion that the best thing to draw is the slice with the largest duration in that pixel.
We can break this into two sub-problems:
What is the range of events which correspond to each pixel? What is the event with the maximum duration for that pixel? We’re going to focus on 1) in this post as that’s where the slowdown was. 2) is fascinating but also surprisingly orthogonal. If you’re interested, I would suggest reading this excellent post from Tristan Hume explaining the basic algorithm we use.</description><content:encoded>&lt;p&gt;We&amp;rsquo;ve recently been looking into optimizing rendering performance of the Perfetto UI on large traces. We discovered that there was some inefficiency in our data fetching logic, especially when you&amp;rsquo;re very zoomed out.&lt;/p&gt;
&lt;p&gt;In this case, there can be a lot of slices (spans) which are so small that they take less than one pixel of width. So for each pixel, we need to figure out &amp;ldquo;what is the event which we should draw for this pixel&amp;rdquo;. Over time we&amp;rsquo;ve come to the conclusion that the best thing to draw is the slice with the largest duration in that pixel.&lt;/p&gt;
&lt;p&gt;We can break this into two sub-problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What is the range of events which correspond to each pixel?&lt;/li&gt;
&lt;li&gt;What is the event with the maximum duration for that pixel?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We&amp;rsquo;re going to focus on 1) in this post as that&amp;rsquo;s where the slowdown was. 2) is fascinating but also surprisingly orthogonal. If you&amp;rsquo;re interested, I would suggest reading &lt;a href="https://thume.ca/2021/03/14/iforests/"&gt;this excellent post&lt;/a&gt; from Tristan Hume explaining the basic algorithm we use.&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s formalize our setup for 1): we have a sorted array containing N (where N is O(100k)) int64 timestamps. The timestamps come directly from the Perfetto trace file so we cannot make any assumptions on values or the distribution. But generally speaking, traces tend to have a mix of dense clusters of events (i.e. lot of activity) and large gaps between these clusters (i.e. not much going on). So it makes sense to optimize for this sort of data pattern.&lt;/p&gt;
&lt;p&gt;We then need to find the indexes corresponding to the &amp;ldquo;lower bound&amp;rdquo; of M regularly-spaced timestamps (A, A+S, A+2S&amp;hellip; B), where each corresponds to a pixel boundary and S corresponds to the &amp;ldquo;time in the trace represented by one pixel&amp;rdquo;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Data: | | | · · | | | | | | | | | | · · · | | · · · · | | | | | | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; dense sparse dense
&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;Queries: ↓ ↓ ↓ ↓ ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A A+S A+2S A+3S B
&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;Result: lb(A) lb(A+S) lb(A+2S) lb(A+3S) lb(B)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The obvious solution, and what we&amp;rsquo;ve been doing for the last few years, is to just binary search each of the M values independently giving us O(M × log N). This is very fast when N is reasonable but when we get into O(100k) on high resolution screens (meaning M is also relatively large), you end up taking several milliseconds to do these queries.&lt;/p&gt;
&lt;p&gt;My first approach was to use &amp;ldquo;static search trees&amp;rdquo; to speed up the searches; ever since I had come across &lt;a href="https://curiouscoding.nl/posts/static-search-tree/"&gt;this amazing post&lt;/a&gt; last year, I had been itching to find a place to use them. They worked great and definitely made things faster, but it was a &lt;em&gt;lot&lt;/em&gt; of code and required hand-written AVX and a deep understanding, which I knew would be difficult to get through review. And I had this nagging feeling there was something much simpler I could do.&lt;/p&gt;
&lt;p&gt;Eventually I had a key realization: I wasn&amp;rsquo;t using the fact that the &lt;em&gt;queries themselves are sorted&lt;/em&gt;! By using this, we can do &lt;em&gt;much&lt;/em&gt; better and achieve O(M + log(N)) performance instead!&lt;/p&gt;
&lt;p&gt;How? Using a cool technique called &lt;a href="https://en.wikipedia.org/wiki/Exponential_search"&gt;exponential search&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;You start by binary searching for A just like before giving you an index I. But for every timestamp after that, instead of binary searching, you do something which looks like a hybrid between linear search and binary search.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Found A at index i, now searching for A+S:
&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;Index: i i+1 i+2 i+3 i+4 i+5 i+6 i+7 i+8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Data: [A] [ ] [ ] [ ] [ ] [A+S] [ ] [ ] [ ]
&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; start +1 +2 +4 +8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;A+S &amp;lt;A+S &amp;lt;A+S &amp;gt;A+S
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; overshot!
&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; binary search here
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Take A + S: at I+1, I+2, I+4, I+8&amp;hellip;, you check if the timestamp at that position is greater than A + S. If not, you continue to the next one. If it is, then you stop and instead binary search between the previous position you checked and the current one.&lt;/p&gt;
&lt;p&gt;This is much faster than just binary searching both theoretically &lt;em&gt;and&lt;/em&gt; practically. Theoretically, notice how we scan the same memory &lt;strong&gt;at most twice&lt;/strong&gt;; once for the exponential scan, once in the binary search afterwards. This is unlike the &amp;ldquo;binary search everything&amp;rdquo; approach where the same piece of memory could be touched M times, if the data layout was pathological. This is where the O(M × log N) -&amp;gt; O(M + log N) improvement comes from.&lt;/p&gt;
&lt;p&gt;Note that this &lt;em&gt;also&lt;/em&gt; applies to static search trees which also have O(M × log N) complexity; exponential search turned out to beat them too!&lt;/p&gt;
&lt;p&gt;Practically, the initial exponential scan is great for CPUs which love scanning through memory linearly instead of jumping around randomly like binary search does. By significantly reducing the binary search range, you significantly reduce the chance of cache misses.&lt;/p&gt;
&lt;p&gt;A final micro-optimization you can do is, when the number of indices you are binary searching is fewer than 16 (empirically chosen), you can just replace with a linear scan instead. At those sizes, you find that the branching and looping from binary search is actually &lt;em&gt;slower&lt;/em&gt; than brute force checking everything (especially with the auto-vectorizer helping you out!).&lt;/p&gt;
&lt;p&gt;We switched to exponential search in &lt;a href="https://github.com/google/perfetto/pull/4648"&gt;this Perfetto PR&lt;/a&gt; and in our microbenchmark (on real trace data!), we saw an improvement from 1.5ms to 180us - a speedup of 8x! This, along with a bunch of other improvements we made, should help make the Perfetto UI faster and smoother on large traces!&lt;/p&gt;</content:encoded></item><item><title>Homepage Redesign, Light Mode and More</title><link>https://lalitm.com/post/homepage-redesign-light-mode-and-more/</link><pubDate>Sat, 06 Dec 2025 23:50:00 +0000</pubDate><guid>https://lalitm.com/post/homepage-redesign-light-mode-and-more/</guid><description>Just a quick note documenting some recent changes to the blog. Nothing groundbreaking, but enough accumulated updates that I wanted to write them down.
Homepage Redesign The homepage used to be a chronological list of articles; your classic default blog format. I’ve redesigned it into more of a two-pane “dashboard” feel. The reason? My content is quite varied: I have long essays and technical write-ups but also short TIL or notes posts (like this one!). Having a way to differentiate between them while also scanning across the titles of different types of posts felt really valuable.
I was inspired by this post which I stumbled across.
Light Mode Returns A couple of days ago, a reader emailed me and asked if I could add a light mode to the blog because they were reading in a bright environment. Though personally I prefer dark mode, I appreciate not everyone agrees so it made sense to add.</description><content:encoded>&lt;p&gt;Just a quick note documenting some recent changes to the blog. Nothing groundbreaking, but enough accumulated updates that I wanted to write them down.&lt;/p&gt;
&lt;h2 id="homepage-redesign"&gt;Homepage Redesign&lt;/h2&gt;
&lt;p&gt;The homepage used to be a chronological list of articles; your classic default blog format. I&amp;rsquo;ve redesigned it into more of a two-pane &amp;ldquo;dashboard&amp;rdquo; feel. The reason? My content is quite varied: I have long essays and technical write-ups but also short TIL or notes posts (like this one!). Having a way to differentiate between them while also scanning across the titles of different types of posts felt really valuable.&lt;/p&gt;
&lt;p&gt;I was inspired by &lt;a href="https://neugierig.org/software/blog/2011/09/blog-format.html"&gt;this post&lt;/a&gt; which I stumbled across.&lt;/p&gt;
&lt;h2 id="light-mode-returns"&gt;Light Mode Returns&lt;/h2&gt;
&lt;p&gt;A couple of days ago, a reader emailed me and asked if I could add a light mode to the blog because they were reading in a bright environment. Though personally I prefer dark mode, I appreciate not everyone agrees so it made sense to add.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the funny part: the blog actually &lt;em&gt;had&lt;/em&gt; a light mode in its early days. I removed it because maintaining two color schemes with the site generator setup I had (Hugo, PaperMod Theme, Tailwind, custom CSS rules on top) was a significant maintenance cost.&lt;/p&gt;
&lt;p&gt;I only endured all this complexity because I thought offloading the theming to someone else meant I could focus on the writing and not turn this into yet another &amp;ldquo;programming project&amp;rdquo;. But, unfortunately, I have very strong feelings about how I want things to look. Trying to work within the theme&amp;rsquo;s extension points while also being opinionated was far more trouble than doing everything myself.&lt;/p&gt;
&lt;p&gt;So I ripped out all of that complexity. Stripped Tailwind, simplified the CSS dramatically, went back to basics. Once I did that, adding light mode became trivial. A few CSS variables and a toggle: done!&lt;/p&gt;
&lt;p&gt;I briefly considered writing my own static site generator (inspired by this &lt;a href="https://nobloat.org/articles/2025-07-01-hello-blog.html"&gt;article&lt;/a&gt;) but Hugo does the job well enough (if you stay away from its dark corners) that I abandoned the attempt after a few hours.&lt;/p&gt;
&lt;h2 id="typography-refresh"&gt;Typography Refresh&lt;/h2&gt;
&lt;p&gt;I also did a bunch of tweaks to the typography:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fonts:&lt;/strong&gt; Moved from system fonts to &lt;a href="https://fonts.google.com/specimen/Inter"&gt;Inter&lt;/a&gt; for headings and body content, &lt;a href="https://fonts.google.com/specimen/Source+Code+Pro"&gt;Source Code Pro&lt;/a&gt; for code blocks and monospace usage. Ensures everyone gets the same experience and makes readability consistent; on some machines, I felt legibility suffered if the system font was poor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Size:&lt;/strong&gt; Bumped up font sizes throughout. The previous sizing felt a bit hard to read, the new one should be easier on the eye.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Links:&lt;/strong&gt; Changed how link underlining works by increasing the gap between the letters and underline. Makes the links look clearer. Also links now have a bit of tint when you hover which should make interacting with them nicer.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>TIL: Number in man page titles (e.g. sleep(3))</title><link>https://lalitm.com/til-number-in-man-page-titles-e-g-sleep-3/</link><pubDate>Mon, 17 Nov 2025 13:10:00 +0000</pubDate><guid>https://lalitm.com/til-number-in-man-page-titles-e-g-sleep-3/</guid><description>If you do Linux systems programming, you will have likely pored over man pages, either on the command line or, my personal preference, using the excellent man7.org or linux.die.net. I’ve always seen the numbers in sleep(3) and read(2) and idly wondered what they meant, but never actually bothered to look them up.
That is, until a review comment on a pull request:
// Behaves like man 2 basename
reviewer: nit: it’s not a syscall, so “man 2” is incorrect
So I looked it up. The answer was in the man(1) page (also accessible via the delightful man man command):
The table below shows the section numbers of the manual followed by the types of pages they contain.
1 Executable programs or shell commands 2 System calls (functions provided by the kernel) 3 Library calls (functions within program libraries) (... less common section numbers) So my colleague was right and the code should have read // behaves like man 3 basename as basename(3) is a libc library call.</description><content:encoded>&lt;p&gt;If you do Linux systems programming, you will have likely pored over &lt;code&gt;man&lt;/code&gt; pages, either on the command line or, my personal preference, using the excellent &lt;a href="https://man7.org"&gt;man7.org&lt;/a&gt; or &lt;a href="https://linux.die.net/man/"&gt;linux.die.net&lt;/a&gt;.
I&amp;rsquo;ve always seen the numbers in &lt;a href="https://man7.org/linux/man-pages/man3/sleep.3.html"&gt;&lt;code&gt;sleep(3)&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://man7.org/linux/man-pages/man2/read.2.html"&gt;&lt;code&gt;read(2)&lt;/code&gt;&lt;/a&gt; and idly wondered what they meant, but never actually bothered to look them up.&lt;/p&gt;
&lt;p&gt;That is, until a review comment on a pull request:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;// Behaves like man 2 basename&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;reviewer: nit: it&amp;rsquo;s not a syscall, so &amp;ldquo;man 2&amp;rdquo; is incorrect&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I looked it up. The answer was in the &lt;a href="https://man7.org/linux/man-pages/man1/man.1.html"&gt;&lt;code&gt;man(1)&lt;/code&gt;&lt;/a&gt; page (also accessible via the delightful &lt;code&gt;man man&lt;/code&gt; command):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The table below shows the section numbers of the manual followed by the types of pages they contain.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 1 Executable programs or shell commands
2 System calls (functions provided by the kernel)
3 Library calls (functions within program libraries)
(... less common section numbers)
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;So my colleague was right and the code should have read &lt;code&gt;// behaves like man 3 basename&lt;/code&gt; as &lt;a href="https://man7.org/linux/man-pages/man3/basename.3.html"&gt;&lt;code&gt;basename(3)&lt;/code&gt;&lt;/a&gt; is a libc library call.&lt;/p&gt;
&lt;p&gt;And there&amp;rsquo;s more to these numbers than what the &lt;code&gt;man man&lt;/code&gt; page describes: various letters can also be attached as suffixes to the numbers. For example, &lt;code&gt;p&lt;/code&gt; indicates a POSIX man page like with &lt;a href="https://man7.org/linux/man-pages/man3/free.3p.html"&gt;&lt;code&gt;free(3p)&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;x&lt;/code&gt; indicates X documentation like with &lt;a href="https://man7.org/linux/man-pages/man3/ncurses.3x.html"&gt;&lt;code&gt;ncurses(3x)&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s embarrassing to admit I&amp;rsquo;ve been using man pages for years but only learned this today.&lt;/p&gt;</content:encoded></item><item><title>CAST(x AS STRING) casts to integer in SQLite</title><link>https://lalitm.com/cast-x-as-string-casts-to-integer-in-sqlite/</link><pubDate>Mon, 03 Nov 2025 13:15:00 +0000</pubDate><guid>https://lalitm.com/cast-x-as-string-casts-to-integer-in-sqlite/</guid><description>As an “SQLite consultant” for my local area of Google, I often have people come to me having written SQL like:
SELECT CAST(bar AS STRING) AS baz FROM foo and ask me “Why is baz always an integer?! Have I hit an SQLite bug?”.
I have to again reach for my list of “odd quirks that SQLite has that people don’t know about”. Because this is not a bug, at least according to the SQLite manual.
Instead, the correct way to write the above query is:
SELECT CAST(bar AS TEXT) AS baz FROM foo The reason for this? Quoting from “Determination of Column Affinity”
For tables not declared as STRICT, the affinity of a column is determined by the declared type of the column, according to the following rules in the order shown:
If the declared type contains the string “INT” then it is assigned INTEGER affinity. If the declared type of the column contains any of the strings “CHAR”, “CLOB”, or “TEXT” then that column has TEXT affinity. Notice that the type VARCHAR contains the string “CHAR” and is thus assigned TEXT affinity. If the declared type for a column contains the string “BLOB” or if no type is specified then the column has affinity BLOB. If the declared type for a column contains any of the strings “REAL”, “FLOA”, or “DOUB” then the column has REAL affinity. Otherwise, the affinity is NUMERIC.
STRING does not match any of the numbered rules and so fallback to NUMERIC affinity which, in the general case, means integer.
Due to SQLite’s staunch stance of being “backwards compatible” there’s very little chance of this paper-cut ever going away. But at least next time someone comes to me with this issue, I’ll be able to link to this post instead of writing the same thing for the nth time :)</description><content:encoded>&lt;p&gt;As an &amp;ldquo;SQLite consultant&amp;rdquo; for my local area of Google, I often have people come to me having written SQL like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SELECT CAST(bar AS STRING) AS baz
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FROM foo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and ask me &amp;ldquo;Why is baz always an integer?! Have I hit an SQLite bug?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I have to again reach for my list of &amp;ldquo;odd quirks that SQLite has that people don&amp;rsquo;t know about&amp;rdquo;. Because this is &lt;em&gt;not&lt;/em&gt; a bug, at least according to the SQLite manual.&lt;/p&gt;
&lt;p&gt;Instead, the correct way to write the above query is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SELECT CAST(bar AS TEXT) AS baz
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FROM foo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The reason for this? Quoting from &lt;a href="https://www.sqlite.org/datatype3.html#affname"&gt;&amp;ldquo;Determination of Column Affinity&amp;rdquo;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For tables not declared as STRICT, the affinity of a column is determined by the declared type of the column, according to the following rules in the order shown:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If the declared type contains the string &amp;ldquo;INT&amp;rdquo; then it is assigned INTEGER affinity.&lt;/li&gt;
&lt;li&gt;If the declared type of the column contains any of the strings &amp;ldquo;CHAR&amp;rdquo;, &amp;ldquo;CLOB&amp;rdquo;, or &amp;ldquo;TEXT&amp;rdquo; then that column has TEXT affinity. Notice that the type VARCHAR contains the string &amp;ldquo;CHAR&amp;rdquo; and is thus assigned TEXT affinity.&lt;/li&gt;
&lt;li&gt;If the declared type for a column contains the string &amp;ldquo;BLOB&amp;rdquo; or if no type is specified then the column has affinity BLOB.&lt;/li&gt;
&lt;li&gt;If the declared type for a column contains any of the strings &amp;ldquo;REAL&amp;rdquo;, &amp;ldquo;FLOA&amp;rdquo;, or &amp;ldquo;DOUB&amp;rdquo; then the column has REAL affinity.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Otherwise, the affinity is NUMERIC.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;STRING&lt;/code&gt; does not match any of the numbered rules and so fallback to &lt;code&gt;NUMERIC&lt;/code&gt; affinity which, in the general case, means integer.&lt;/p&gt;
&lt;p&gt;Due to SQLite&amp;rsquo;s staunch stance of being &amp;ldquo;backwards compatible&amp;rdquo; there&amp;rsquo;s very little chance of this paper-cut ever going away. But at least next time someone comes to me with this issue, I&amp;rsquo;ll be able to link to this post instead of writing the same thing for the nth time :)&lt;/p&gt;</content:encoded></item><item><title>TIL: Diátaxis: Systematic technical documentation</title><link>https://lalitm.com/til-diataxis-systematic-technical-documentation/</link><pubDate>Sun, 02 Nov 2025 23:44:00 +0000</pubDate><guid>https://lalitm.com/til-diataxis-systematic-technical-documentation/</guid><description>A few weeks ago, I wrote about “The Documentation System” and how valuable I found it. As I dug deeper into researching how best to apply the principles outlined there, I came across Diátaxis. Written by the same author after they left Divio, Diátaxis is a distillation of all the principles with much more guidance in how to apply the framework (e.g. giving more examples), diving more into the philosophy and in general being a more comprehensive view into how to write great technical docs.
I’m blown away by how well the framework (and Diátaxis makes clear it is a framework not a rigid set of rules) is explained. It’s patently obvious that the author really understands technical documentation and they truly have given a gift to the industry by writing it up.
Finding Diátaxis has only made me more motivated to deeply absorb its principles and see how to best apply it to all technical documentation I refactor and/or write going forward.</description><content:encoded>&lt;p&gt;A few weeks ago, I &lt;a href="/til-the-documentation-system-divio/"&gt;wrote&lt;/a&gt; about &amp;ldquo;The Documentation System&amp;rdquo; and how valuable I found it. As I dug deeper into researching how best to apply the principles outlined there, I came across &lt;a href="https://diataxis.fr/"&gt;Diátaxis&lt;/a&gt;. Written by the same author after they left &lt;a href="https://www.divio.com/"&gt;Divio&lt;/a&gt;, Diátaxis is a distillation of all the principles with much more guidance in how to apply the framework (e.g. giving more examples), diving more into the philosophy and in general being a more comprehensive view into how to write great technical docs.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m blown away by how well the framework (and Diátaxis &lt;a href="https://diataxis.fr/how-to-use-diataxis/#use-diataxis-as-a-guide-not-a-plan"&gt;makes clear&lt;/a&gt; it is a framework &lt;strong&gt;not&lt;/strong&gt; a rigid set of rules) is explained. It&amp;rsquo;s patently obvious that the author really understands technical documentation and they truly have given a gift to the industry by writing it up.&lt;/p&gt;
&lt;p&gt;Finding Diátaxis has only made me more motivated to deeply absorb its principles and see how to best apply it to all technical documentation I refactor and/or write going forward.&lt;/p&gt;</content:encoded></item><item><title>TIL: Public File Hosting with 0x0.st</title><link>https://lalitm.com/til-public-file-hosting-with-0x0-st/</link><pubDate>Sat, 25 Oct 2025 15:13:00 +0100</pubDate><guid>https://lalitm.com/til-public-file-hosting-with-0x0-st/</guid><description>Sometimes you run into a truly inspirational piece of software that it’s a wonder even exists. I found 0x0.st recently and it very much falls into this bucket.
It’s essentially a simple, no-account, temporary file hosting site where you can just upload a file using a simple HTTP POST and the site will host it for you for between 30 days and 1 year depending on the size of the file. Uploading a file is as simple as:
curl -F 'file=@yourfile.ext' https://0x0.st and you’ll get back a URL that you can share or use in the future.
I thought all the services like this had long since disappeared especially in the age of increasing scrutiny by government of content that websites host. It turns out though that from this discussion on HN, there are actually a bunch of similar services (transfer.sh, bashupload.com, chunk.io) but 0x0.st was the only one which I was able to access without an error.</description><content:encoded>&lt;p&gt;Sometimes you run into a truly inspirational piece of software that it&amp;rsquo;s a wonder even exists. I found &lt;a href="https://0x0.st/"&gt;0x0.st&lt;/a&gt; recently and it very much falls into this bucket.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s essentially a simple, no-account, temporary file hosting site where you can just upload a file using a simple HTTP POST and the site will host it for you for between 30 days and 1 year depending on the size of the file. Uploading a file is as simple as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -F &amp;#39;file=@yourfile.ext&amp;#39; https://0x0.st
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and you&amp;rsquo;ll get back a URL that you can share or use in the future.&lt;/p&gt;
&lt;p&gt;I thought all the services like this had long since disappeared especially in the age of increasing scrutiny by government of content that websites host. It turns out though that from this discussion on &lt;a href="https://news.ycombinator.com/item?id=35491520"&gt;HN&lt;/a&gt;, there are actually a bunch of similar services (transfer.sh, bashupload.com, chunk.io) but 0x0.st was the only one which I was able to access without an error.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m particularly impressed by the &lt;a href="https://movsw.0x0.st/@0x0"&gt;transparency&lt;/a&gt; of the owner of 0x0.st about the challenges they face and how they deal with the problems of abuse, malware etc. Many of these are precisely what I thought were insurmountable of hosting a service like this but apparently there are people out there willing to overcome them!&lt;/p&gt;</content:encoded></item><item><title>TIL: `will-change` and `isolation` CSS properties</title><link>https://lalitm.com/til-will-change-and-isolation-css-properties/</link><pubDate>Fri, 24 Oct 2025 00:20:00 +0100</pubDate><guid>https://lalitm.com/til-will-change-and-isolation-css-properties/</guid><description>The Perfetto UI is a fascinating project to work on because it often faces performance problems which you wouldn’t see in more “normal” webapps. I learn about all sorts of weird web features by reviewing PRs for the UI.
Two such features I just learned about:
will-change (PR): a hint, to be used sparingly, to tell the browser that a certain property of a DOM element will change in the near future. isolation (PR): I feel MDN didn’t do a great job of explaining so I asked Claude which gave a much more useful answer: isolation is a CSS property that creates a new stacking context for an element. Think of a stacking context as a self-contained z-index universe. Elements within one stacking context can layer on top of each other, but their z-index values only matter relative to siblings within that same context.</description><content:encoded>&lt;p&gt;The Perfetto UI is a fascinating project to work on because it often faces performance problems which you wouldn&amp;rsquo;t see in more &amp;ldquo;normal&amp;rdquo; webapps. I learn about all sorts of weird web features by reviewing PRs for the UI.&lt;/p&gt;
&lt;p&gt;Two such features I just learned about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/will-change"&gt;will-change&lt;/a&gt; (&lt;a href="https://github.com/google/perfetto/pull/3350/files#diff-c57fa71e0c282ce81f65b10226043f6d89c4105afd3f5ec5813b8d9d5e034101R128"&gt;PR&lt;/a&gt;): a hint, to be used sparingly, to tell the browser that a certain property of a DOM element will change in the near future.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/isolation"&gt;isolation&lt;/a&gt; (&lt;em&gt;&lt;a href="https://github.com/google/perfetto/pull/3381/files#diff-c57fa71e0c282ce81f65b10226043f6d89c4105afd3f5ec5813b8d9d5e034101R119"&gt;PR&lt;/a&gt;&lt;/em&gt;):
I feel MDN didn&amp;rsquo;t do a great job of explaining so I asked Claude which gave a much more useful answer:
&lt;blockquote&gt;
&lt;p&gt;isolation is a CSS property that creates a new stacking context for an element. Think of a stacking context as a self-contained z-index universe. Elements within one stacking context can layer on top of each other, but their z-index values only matter relative to siblings within that same context.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>TIL: The Documentation System (Divio)</title><link>https://lalitm.com/til-the-documentation-system-divio/</link><pubDate>Sun, 19 Oct 2025 19:38:00 +0100</pubDate><guid>https://lalitm.com/til-the-documentation-system-divio/</guid><description>The Perfetto team spent a lot of time earlier this year rewriting our documentation to be more useful for the average developer. We struggled a lot to figure out “what is the best way to structure our documentation”.
When reading Examples are the best documentation today, I came across The Documentation System, an approach to structuring developer documentation. Reading through it, I couldn’t help but marvel at how clear and concisely it laid out the ideas we were struggling to come up with from first principles.
Specifically, I really like how it breaks down documentation into four quadrants:
Tutorials: take your user through a series of steps to complete some task with your project. How-to guides: solve a real world problem with your project. Reference guides: give technical explanations of how things work. Explanations: clarify a particular topic or area and how the project fits into the bigger tech landscape. I think we got most of this right in our documentation rewrite but we definitely muddled some of these areas together, making our pages less clear than they could be. I plan on spending some time later this year correcting our docs to match this system closer.</description><content:encoded>&lt;p&gt;The Perfetto team spent a lot of time earlier this year rewriting our documentation to be more useful for the average developer. We struggled a lot to figure out &amp;ldquo;what is the best way to structure our documentation&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;When reading &lt;a href="https://rakhim.exotext.com/examples-are-the-best-documentation"&gt;Examples are the best documentation&lt;/a&gt; today, I came across &lt;a href="https://docs.divio.com/documentation-system/"&gt;The Documentation System&lt;/a&gt;, an approach to structuring developer documentation. Reading through it, I couldn&amp;rsquo;t help but marvel at how clear and concisely it laid out the ideas we were struggling to come up with from first principles.&lt;/p&gt;
&lt;p&gt;Specifically, I really like how it breaks down documentation into four quadrants:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.divio.com/documentation-system/tutorials/"&gt;&lt;strong&gt;Tutorials&lt;/strong&gt;&lt;/a&gt;: take your user through a series of steps to complete some task with your project.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.divio.com/documentation-system/how-to-guides/"&gt;&lt;strong&gt;How-to guides&lt;/strong&gt;&lt;/a&gt;: solve a real world problem with your project.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.divio.com/documentation-system/reference/"&gt;&lt;strong&gt;Reference guides&lt;/strong&gt;&lt;/a&gt;: give technical explanations of how things work.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.divio.com/documentation-system/explanation/"&gt;&lt;strong&gt;Explanations&lt;/strong&gt;&lt;/a&gt;: clarify a particular topic or area and how the project fits into the bigger tech landscape.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I think we got &lt;em&gt;most&lt;/em&gt; of this right in our documentation rewrite but we definitely muddled some of these areas together, making our pages less clear than they could be. I plan on spending some time later this year correcting our docs to match this system closer.&lt;/p&gt;</content:encoded></item><item><title>What Makes a Good Tool for Claude Code</title><link>https://lalitm.com/writing-tools-for-claude-code/</link><pubDate>Sun, 12 Oct 2025 00:00:00 +0000</pubDate><guid>https://lalitm.com/writing-tools-for-claude-code/</guid><description>I’ve been using Claude Code extensively for personal projects, and similar AI coding tools at work. Recently I came across this excellent blog post that resonated with a lot of my experience.
One part stuck with me though: Noah emphasizes that tools fail with LLMs when they’re “overly complex,” with the Unix philosophy being particularly well-suited for tool calling. But then I thought about git.
Git breaks the Unix philosophy completely. It’s sprawling, stateful, and complex. And yet Claude Code handles it effortlessly. It composes commands that, even after 10+ years of daily git usage, I wouldn’t think to use. It handles rebasing, cherry-picking, complex resets—stuff that trips up experienced developers regularly.
So if simplicity and the Unix philosophy aren’t the whole story, what else matters?
I’ve come up with three “hallmarks” of a good tool for tool calling with LLMs.
1. It’s been around for a long time and/or is used by lots of people</description><content:encoded>&lt;p&gt;I&amp;rsquo;ve been using Claude Code extensively for personal projects, and similar AI
coding tools at work. Recently I came across
&lt;a href="https://www.alephic.com/writing/the-magic-of-claude-code"&gt;this excellent blog post&lt;/a&gt;
that resonated with a lot of my experience.&lt;/p&gt;
&lt;p&gt;One part stuck with me though: Noah emphasizes that tools fail with LLMs when
they&amp;rsquo;re &amp;ldquo;overly complex,&amp;rdquo; with the Unix philosophy being particularly
well-suited for tool calling. But then I thought about &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Git breaks the Unix philosophy completely. It&amp;rsquo;s sprawling, stateful, and
complex. And yet Claude Code handles it effortlessly. It composes commands that,
even after 10+ years of daily git usage, I wouldn&amp;rsquo;t think to use. It handles
rebasing, cherry-picking, complex resets—stuff that trips up experienced
developers regularly.&lt;/p&gt;
&lt;p&gt;So if simplicity and the Unix philosophy aren&amp;rsquo;t the whole story, what else
matters?&lt;/p&gt;
&lt;p&gt;I’ve come up with three “hallmarks” of a good tool for tool calling with LLMs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. It’s been around for a long time and/or is used by lots of people&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Examples: Unix tools like &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;—but also &lt;code&gt;git&lt;/code&gt;,
&lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt;. Every Stack Overflow thread, blog post, and tutorial
using these tools has likely ended up in the training data. Claude isn’t
reasoning from first principles—it’s drawing on millions of examples.&lt;/p&gt;
&lt;p&gt;This is why git works despite its complexity: Claude has effectively memorized
decades of collective wisdom.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. It has really good documentation (built-in help or external docs)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even if a tool isn’t widely used, great documentation can bridge the gap. I’ve
been building a finance system on top of
&lt;a href="https://beancount.github.io/docs/"&gt;Beancount&lt;/a&gt;—a double-entry accounting system
that’s definitely not mainstream (maybe I’ll write a post about this in the
future). Claude Code handles it surprisingly well because Beancount has
exceptional documentation. When I point Claude at the docs, it can figure out
the directive syntax, transaction formats, and account structures without
necessarily having seen millions of examples in its training data.&lt;/p&gt;
&lt;p&gt;Good &lt;code&gt;--help&lt;/code&gt; text matters. Clear external documentation matters. If Claude can
discover how your tool works, it can use it effectively.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Good error messages&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Error messages, especially those with suggestions like “you used X, did you mean
Y?”, can be tremendously helpful for LLMs. The best example is the Rust
compiler: it gives errors like “you typed foobar, did you mean foobaz?” and
Claude Code can actually use that feedback to correct itself.&lt;/p&gt;
&lt;p&gt;This might be one reason why people feel Claude Code is particularly good at
Rust programming—the compiler is essentially coaching it through mistakes in
real time.&lt;/p&gt;
&lt;p&gt;This isn’t to say the Unix philosophy is wrong — it’s that Unix tools work well
with Claude Code for different reasons than we might think. Tools like sed and
awk nail hallmarks 1 and 2: they’ve been around for decades (massive training
data) and have extensive man pages (great documentation). The fact that they
follow “do one thing well” is almost incidental to their success with LLMs.&lt;/p&gt;
&lt;p&gt;So if I’m building a tool today, I can’t make it instantly popular, but I can
make it understandable. That means good documentation, clear error messages, and
a few solid examples of how it’s used. Those aren’t new ideas — they’ve always
mattered. The difference now is that there’s much higher ROI in doing them than
there used to be.&lt;/p&gt;
&lt;p&gt;In other words, we used to write docs for humans. Now, we’re also writing for
AIs that help humans. That shift changes the economics of good documentation in
a big way.&lt;/p&gt;</content:encoded></item><item><title>Harnessing Frustration: Using LLMs to Overcome Activation Energy</title><link>https://lalitm.com/llm-motivation-via-emotions/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid>https://lalitm.com/llm-motivation-via-emotions/</guid><description>One of my biggest weaknesses as a software engineer is procrastination when facing a new project. When the scope is unclear, I have a tendency to wait until I feel I’ve “felt out” the problem to start doing anything. I know I’ll feel better and work much faster when I get “stuck in” but I still struggle with that first step, overcoming the “activation energy” required to engage with the details.
LLMs have been a game-changer for me in this respect: I can just throw a couple of sentences at them with the shape of the problem. This leads to one of two outcomes:
The LLM comes up with a good solution, usually in a slightly different way than what I was thinking. I realize “oh wow the solution is much simpler than I thought”. Straight away I start thinking about the consequences of implementing and improving what the LLM suggested. The LLM comes up with a solution that I intuitively recognize as “wrong”. My immediate reaction is frustration (“How could it get it so wrong”) which leads me to go back and forth with the model, explaining to it why its solution could not possibly work. But in the process of arguing with the model, my brain is churning away and generating variations or different approaches that could work. After a while, even if the AI is still on the wrong track, the debate will trigger a moment of inspiration where suddenly the solution will come to me. I’ll excitedly start up a new conversation and start working through it with the model. The key is the emotional reaction I have immediately to the LLM’s response, either excitement or frustration. By harnessing this immediate feedback loop, I get my brain out of its passive, procrastination mode. It’s almost like a jolt: either I’m thrilled because it’s simpler than I thought, or I’m spurred to action by the urge to correct a perceived ‘wrong’ answer. This forces me to engage with the problem in a meaningful way.</description><content:encoded>&lt;p&gt;One of my biggest weaknesses as a software engineer is procrastination when
facing a new project. When the scope is unclear, I have a tendency to wait until
I feel I&amp;rsquo;ve &amp;ldquo;felt out&amp;rdquo; the problem to start doing anything. I know I&amp;rsquo;ll feel
better and work much faster when I get &amp;ldquo;stuck in&amp;rdquo; but I still struggle with that
first step, overcoming the &amp;ldquo;activation energy&amp;rdquo; required to engage with the
details.&lt;/p&gt;
&lt;p&gt;LLMs have been a game-changer for me in this respect: I can just throw a couple
of sentences at them with the shape of the problem. This leads to one of two
outcomes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The LLM comes up with a good solution, usually in a slightly different way
than what I was thinking. I realize &amp;ldquo;oh wow the solution is much simpler than
I thought&amp;rdquo;. Straight away I start thinking about the consequences of
implementing and improving what the LLM suggested.&lt;/li&gt;
&lt;li&gt;The LLM comes up with a solution that I intuitively recognize as &amp;ldquo;wrong&amp;rdquo;. My
immediate reaction is frustration (&amp;ldquo;How could it get it so wrong&amp;rdquo;) which
leads me to go back and forth with the model, explaining to it why its
solution could not &lt;em&gt;possibly&lt;/em&gt; work. But in the process of arguing with the
model, my brain is churning away and generating variations or different
approaches that &lt;em&gt;could&lt;/em&gt; work. After a while, even if the AI is still on the
wrong track, the debate will trigger a moment of inspiration where suddenly
the solution will come to me. I&amp;rsquo;ll excitedly start up a new conversation and
start working through it with the model.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key is the emotional reaction I have immediately to the LLM&amp;rsquo;s response,
either &lt;strong&gt;excitement or frustration&lt;/strong&gt;. By harnessing this immediate feedback
loop, I get my brain out of its passive, procrastination mode. It&amp;rsquo;s almost like
a jolt: either I&amp;rsquo;m thrilled because it&amp;rsquo;s simpler than I thought, or I&amp;rsquo;m spurred
to action by the urge to correct a perceived &amp;lsquo;wrong&amp;rsquo; answer. This forces me to
engage with the problem in a meaningful way.&lt;/p&gt;
&lt;p&gt;For what it&amp;rsquo;s worth, this experience is very similar to talking through a
problem with another engineer: the advantage of LLMs is that it&amp;rsquo;s available 24/7
and I never have to worry about my problem being &amp;ldquo;too insignificant&amp;rdquo; to bother
someone with. The simple act of articulating my thoughts and hearing a response
seems sufficient, regardless of the response&amp;rsquo;s quality.&lt;/p&gt;
&lt;p&gt;So next time you&amp;rsquo;re procrastinating a problem don&amp;rsquo;t wait around for inspiration.
Outsource the first step. Throw the problem at an LLM and see what happens.
Whether excitement or frustration, by channeling that emotion you can solve your
problem faster.&lt;/p&gt;</content:encoded></item></channel></rss>