<?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 (Articles only)</title><link>https://lalitm.com/</link><description>Articles from Lalit Maganti: my best work, posts I put real time into. For everything including notes, 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/articles/index.xml" rel="self" type="application/rss+xml"/><item><title>The git history command deserves more attention</title><link>https://lalitm.com/post/git-history/</link><pubDate>Mon, 13 Jul 2026 19:40:00 +0100</pubDate><guid>https://lalitm.com/post/git-history/</guid><description>Working with lots of changes in parallel on git can be painful. You end up juggling branches and commits, and running scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze.
jj, an alternative to git, gets discussed a lot these days (1, 2, 3, 4) and is often pitched as a solution. While I’m very sold on the problems jj is trying to solve, the way it solves them hasn’t quite hit home with me. Every 3 months, for the last 1.5 years, I try it out for a few days, really trying to make it part of my workflow but eventually I give up and go back to git.1
That’s where git history comes in. It’s an experimental command that arrived across two releases, 2.54 (April, reword and split subcommands) and 2.55 (June, fixup subcommand). It got a flurry of attention on each release day, and then, as far as I can tell, not much community discussion since. Which is a shame, because IMO it already delivers several of the benefits people tout for jj without needing to switch your whole workflow. And the cool thing is that it’s part of the core git distribution, so you can try it without installing anything.</description><content:encoded>&lt;p&gt;Working with lots of changes in parallel on git can be painful. You end up
juggling branches and commits, and running scary &lt;code&gt;rebase -i&lt;/code&gt; commands that can
leave your tree in a half-broken state if you so much as sneeze.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/jj-vcs/jj"&gt;&lt;code&gt;jj&lt;/code&gt;&lt;/a&gt;, an alternative to &lt;code&gt;git&lt;/code&gt;, gets discussed a
lot these days (&lt;a href="https://news.ycombinator.com/item?id=48692083"&gt;1&lt;/a&gt;,
&lt;a href="https://lobste.rs/s/beqyuc/evan_s_jujutsu_tutorial"&gt;2&lt;/a&gt;,
&lt;a href="https://lobste.rs/s/fg3sgh/jj_tui_terminal_user_interface_jujutsu"&gt;3&lt;/a&gt;,
&lt;a href="https://lobste.rs/s/27dxjg/petty_reason_we_didn_t_end_up_using_jj"&gt;4&lt;/a&gt;) and is
often pitched as a solution. While I&amp;rsquo;m very sold on the &lt;em&gt;problems&lt;/em&gt; &lt;code&gt;jj&lt;/code&gt; is
trying to solve, the way it solves them hasn&amp;rsquo;t quite hit home with me. Every 3
months, for the last 1.5 years, I try it out for a few days, really trying to
make it part of my workflow but eventually I give up and go back to git.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where &lt;code&gt;git history&lt;/code&gt; comes in. It&amp;rsquo;s an &lt;strong&gt;experimental&lt;/strong&gt;
&lt;a href="https://git-scm.com/docs/git-history"&gt;command&lt;/a&gt; that arrived across two
releases,
&lt;a href="https://github.com/git/git/blob/master/Documentation/RelNotes/2.54.0.adoc"&gt;2.54&lt;/a&gt;
(April, &lt;code&gt;reword&lt;/code&gt; and &lt;code&gt;split&lt;/code&gt; subcommands) and
&lt;a href="https://github.com/git/git/blob/master/Documentation/RelNotes/2.55.0.adoc"&gt;2.55&lt;/a&gt;
(June, &lt;code&gt;fixup&lt;/code&gt; subcommand). It got a flurry of attention on each release day,
and then, as far as I can tell, not much community discussion since. Which is a
shame, because IMO it already delivers several of the benefits people tout for
&lt;code&gt;jj&lt;/code&gt; without needing to switch your whole workflow. And the cool thing is that
it&amp;rsquo;s part of the core git distribution, so you can try it without installing
anything.&lt;/p&gt;
&lt;p&gt;There are three subcommands: &lt;code&gt;fixup&lt;/code&gt;, &lt;code&gt;reword&lt;/code&gt; and &lt;code&gt;split&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="fixup"&gt;fixup&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-history#Documentation/git-history.txt-fixupcommit"&gt;&lt;code&gt;git history fixup&lt;/code&gt;&lt;/a&gt;
fixes an old commit that has something wrong in it, then autorebases &lt;em&gt;all&lt;/em&gt; your
branches to match.&lt;/p&gt;
&lt;p&gt;You stage the fix as usual with &lt;code&gt;git add&lt;/code&gt;, then run &lt;code&gt;git history fixup &amp;lt;commit&amp;gt;&lt;/code&gt;
to fold those staged changes into the target commit. It&amp;rsquo;s like a
&lt;code&gt;git commit --fixup&lt;/code&gt; plus an autosquash rebase but with the extra magic that it
&lt;em&gt;also&lt;/em&gt; updates any other branch which contained that commit.&lt;/p&gt;
&lt;p&gt;That last part goes further than &lt;code&gt;git rebase --update-refs&lt;/code&gt;, which only moves
refs sitting inside the range you&amp;rsquo;re actively rebasing. &lt;code&gt;git history&lt;/code&gt; instead
finds and rewrites &lt;em&gt;every&lt;/em&gt; local branch descended from the commit (while also
having an option to limit it to only the current branch). On the other hand it
does &lt;em&gt;not&lt;/em&gt; work in the presence of merge commits which, for some usages of git,
is going to be a dealbreaker.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how it works in practice:&lt;/p&gt;
&lt;p&gt;Before, with a fix staged for &lt;code&gt;B&lt;/code&gt;:&lt;/p&gt;
&lt;span class="diagram-wrap"&gt;
&lt;img class="diagram-light" src="/diagrams/fixup-before-light.svg" alt="Commits A, B (buggy), C on feat-1 and D on feat-2 in a line, with a staged fix sitting on top of the tip D." loading="lazy" decoding="async"&gt;
&lt;img class="diagram-dark" src="/diagrams/fixup-before-dark.svg" alt="Commits A, B (buggy), C on feat-1 and D on feat-2 in a line, with a staged fix sitting on top of the tip D." loading="lazy" decoding="async"&gt;
&lt;/span&gt;
&lt;p&gt;After &lt;code&gt;git history fixup B&lt;/code&gt;:&lt;/p&gt;
&lt;span class="diagram-wrap"&gt;
&lt;img class="diagram-light" src="/diagrams/fixup-after-light.svg" alt="After fixup: A unchanged; B (now fixed), C and D rewritten with new hashes, branch tips feat-1 and feat-2 following." loading="lazy" decoding="async"&gt;
&lt;img class="diagram-dark" src="/diagrams/fixup-after-dark.svg" alt="After fixup: A unchanged; B (now fixed), C and D rewritten with new hashes, branch tips feat-1 and feat-2 following." loading="lazy" decoding="async"&gt;
&lt;/span&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;B*&lt;/code&gt; is &lt;code&gt;B&lt;/code&gt; with the fix folded in. Rewriting a commit gives it a new hash, so
&lt;code&gt;C&lt;/code&gt; and &lt;code&gt;D&lt;/code&gt; are automatically re-created on top as &lt;code&gt;C*&lt;/code&gt; and &lt;code&gt;D*&lt;/code&gt;, and the
&lt;code&gt;feat-1&lt;/code&gt; and &lt;code&gt;feat-2&lt;/code&gt; branch tips move with them.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The most important property, common to all three commands, is that it&amp;rsquo;s atomic:
it never leaves your tree in a half-broken state. It manages this by refusing
any operation that could produce a conflict.&lt;/p&gt;
&lt;p&gt;To be clear, this is strictly &lt;em&gt;less&lt;/em&gt; powerful than &lt;code&gt;jj&lt;/code&gt;. &lt;code&gt;jj&lt;/code&gt;
&lt;a href="https://docs.jj-vcs.dev/latest/conflicts/"&gt;treats conflicts as first class&lt;/a&gt; so
it can carry a conflicted state through a rebase and let you sort it out later.
&lt;code&gt;git history&lt;/code&gt; doesn&amp;rsquo;t do this yet &lt;em&gt;but&lt;/em&gt; the
&lt;a href="https://git-scm.com/docs/git-history#_limitations"&gt;docs&lt;/a&gt; leave the door open:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;This limitation is by design as history rewrites are not intended to be
stateful operations. The limitation can be lifted once (if) Git learns about
first-class conflicts.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So basically, this limitation may change in the future; excited to see if it
does!&lt;/p&gt;
&lt;h2 id="reword"&gt;reword&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-history#Documentation/git-history.txt-rewordcommit"&gt;&lt;code&gt;git history reword&lt;/code&gt;&lt;/a&gt;
updates the commit message on an old commit and automatically rebases everything
on top. This is very useful for going back and
fixing commit messages when the design shifts as you iterate.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git history reword &amp;lt;commit&amp;gt;&lt;/code&gt; opens your editor with that commit&amp;rsquo;s existing
message. You edit it, save, and the rest of the stack is rebuilt on top with the
branches following along. It&amp;rsquo;s exactly like fixup but for commit messages
instead of the tree contents.&lt;/p&gt;
&lt;p&gt;Because it only changes a message, &lt;code&gt;reword&lt;/code&gt; (like &lt;code&gt;split&lt;/code&gt; later) never touches
your index or working tree at all; it works purely on the commit graph. So both
let you rewrite a commit on a branch you don&amp;rsquo;t have checked out without
disturbing whatever you&amp;rsquo;re in the middle of.&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;span class="diagram-wrap"&gt;
&lt;img class="diagram-light" src="/diagrams/reword-before-light.svg" alt="Commits A, B with message &amp;#39;fix bug&amp;#39;, and C on feat-1." loading="lazy" decoding="async"&gt;
&lt;img class="diagram-dark" src="/diagrams/reword-before-dark.svg" alt="Commits A, B with message &amp;#39;fix bug&amp;#39;, and C on feat-1." loading="lazy" decoding="async"&gt;
&lt;/span&gt;
&lt;p&gt;After &lt;code&gt;git history reword B&lt;/code&gt;:&lt;/p&gt;
&lt;span class="diagram-wrap"&gt;
&lt;img class="diagram-light" src="/diagrams/reword-after-light.svg" alt="After reword: A unchanged; B rewritten with message &amp;#39;fix null deref in parser&amp;#39; and C rewritten on top." loading="lazy" decoding="async"&gt;
&lt;img class="diagram-dark" src="/diagrams/reword-after-dark.svg" alt="After reword: A unchanged; B rewritten with message &amp;#39;fix null deref in parser&amp;#39; and C rewritten on top." loading="lazy" decoding="async"&gt;
&lt;/span&gt;
&lt;p&gt;&lt;em&gt;Only &lt;code&gt;B&lt;/code&gt;&amp;rsquo;s message changes, but that still gives it a new hash, so &lt;code&gt;C&lt;/code&gt; is
rebuilt on top as &lt;code&gt;C*&lt;/code&gt; and &lt;code&gt;feat-1&lt;/code&gt; follows along.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="split"&gt;split&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-history#Documentation/git-history.txt-splitcommit--pathspec"&gt;&lt;code&gt;git history split&lt;/code&gt;&lt;/a&gt;
takes one commit and splits it into two, interactively picking what you care
about from each. It&amp;rsquo;s the equivalent of &lt;code&gt;git add -p&lt;/code&gt;, but
without needing gymnastics with &lt;code&gt;git rebase&lt;/code&gt;. I&amp;rsquo;ve found this to be the most
specialized of the three, but invaluable when I need it.&lt;/p&gt;
&lt;p&gt;Specifically, &lt;code&gt;git history split &amp;lt;commit&amp;gt;&lt;/code&gt; drops you into a hunk-by-hunk prompt
over that commit&amp;rsquo;s diff. The hunks you keep make up the first commit, the rest
fall into the second.&lt;/p&gt;
&lt;p&gt;Before, with &lt;code&gt;B&lt;/code&gt; bundling two unrelated changes:&lt;/p&gt;
&lt;span class="diagram-wrap"&gt;
&lt;img class="diagram-light" src="/diagrams/split-before-light.svg" alt="Commit B bundling two unrelated changes, between A and C on feat-1." loading="lazy" decoding="async"&gt;
&lt;img class="diagram-dark" src="/diagrams/split-before-dark.svg" alt="Commit B bundling two unrelated changes, between A and C on feat-1." loading="lazy" decoding="async"&gt;
&lt;/span&gt;
&lt;p&gt;After &lt;code&gt;git history split B&lt;/code&gt;:&lt;/p&gt;
&lt;span class="diagram-wrap"&gt;
&lt;img class="diagram-light" src="/diagrams/split-after-light.svg" alt="After split: A unchanged; B split into B1 and B2, and C rewritten on top as C*." loading="lazy" decoding="async"&gt;
&lt;img class="diagram-dark" src="/diagrams/split-after-dark.svg" alt="After split: A unchanged; B split into B1 and B2, and C rewritten on top as C*." loading="lazy" decoding="async"&gt;
&lt;/span&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;B&lt;/code&gt; becomes &lt;code&gt;B1&lt;/code&gt; and &lt;code&gt;B2&lt;/code&gt;, and &lt;code&gt;C&lt;/code&gt; is rebuilt on top of the pair as &lt;code&gt;C*&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Judging by how many people are using &lt;code&gt;jj&lt;/code&gt;, I do think there&amp;rsquo;s still some key
mental shift which I&amp;rsquo;m not yet making. And to be clear, &lt;code&gt;git history&lt;/code&gt; doesn&amp;rsquo;t
close the full gap: &lt;code&gt;jj&lt;/code&gt; still gives you an
&lt;a href="https://docs.jj-vcs.dev/latest/operation-log/"&gt;operation log with easy undo&lt;/a&gt;,
&lt;a href="https://docs.jj-vcs.dev/latest/working-copy/"&gt;models your working copy as a commit&lt;/a&gt;,
and can
&lt;a href="https://docs.jj-vcs.dev/latest/conflicts/"&gt;carry conflicts through a rebase&lt;/a&gt;,
none of which this is trying to do.&lt;/p&gt;
&lt;p&gt;But for now, &lt;code&gt;git history&lt;/code&gt; is a &lt;em&gt;big&lt;/em&gt; step forward in adopting many of the
pieces that attract people to &lt;code&gt;jj&lt;/code&gt;, and it&amp;rsquo;s already in the tool I use every
day. And the way the documentation is written makes me hopeful that more
improvements will be coming in upcoming releases!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;If there&amp;rsquo;s interest, happy to write up a post on my experience on this.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>Security analysis is finally reaching software's long tail</title><link>https://lalitm.com/post/perfetto-security-bugs-ai/</link><pubDate>Sun, 07 Jun 2026 16:26:00 +0100</pubDate><guid>https://lalitm.com/post/perfetto-security-bugs-ai/</guid><description>This post was formerly titled “17 bugs in 10 weeks from AI security scanning”. It was changed upon feedback from trusted readers who suggested, while accurate, it did not give a good sense of what I’m actually arguing in the post.
Over the last several weeks, I’ve been receiving more security bug reports for Perfetto’s trace processor than I ever have before, all of them found by AI. And I’m very happy about it! These are bugs that would almost certainly not have been found a year ago and it feels good to close these loopholes even though trace processor is by no means security critical.
For years, security researchers concentrated their time on the highest-stakes targets: kernels, cryptography libraries, password managers. But there’s a lot of code out there which is security-relevant but not truly security-critical. In my experience, these sorts of projects didn’t draw much attention. Now systems in the long tail can get that attention which they wouldn’t have before.</description><content:encoded>&lt;p&gt;&lt;em&gt;This post was formerly titled &amp;ldquo;17 bugs in 10 weeks from AI security scanning&amp;rdquo;.
It was changed upon feedback from trusted readers who suggested, while accurate,
it did not give a good sense of what I&amp;rsquo;m &lt;em&gt;actually&lt;/em&gt; arguing in the post.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Over the last several weeks, I&amp;rsquo;ve been receiving more security bug reports for
Perfetto&amp;rsquo;s trace processor than I ever have before, all of them found by AI. And
I&amp;rsquo;m very happy about it! These are bugs that would almost certainly not have
been found a year ago and it feels good to close these loopholes even though
trace processor is by no means security critical.&lt;/p&gt;
&lt;p&gt;For years, security researchers concentrated their time on the highest-stakes
targets: kernels, cryptography libraries, password managers. But there&amp;rsquo;s a lot
of code out there which is security-relevant but not truly security-critical. In
my experience, these sorts of projects didn&amp;rsquo;t draw much attention. Now systems
in the long tail can get that attention which they wouldn&amp;rsquo;t have before.&lt;/p&gt;
&lt;h2 id="why-is-this-happening"&gt;Why is this happening&lt;/h2&gt;
&lt;p&gt;Trace processor is a project which sits squarely in that long tail. It&amp;rsquo;s a C++
library (yes, Rust would be the obvious choice today but it&amp;rsquo;s not practical to
rewrite, see footnote &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;) for processing recorded traces of various
formats. These are typically traces you collected yourself or in your test infra
and process offline so &amp;ldquo;untrusted input&amp;rdquo; isn&amp;rsquo;t much of a concern.&lt;/p&gt;
&lt;p&gt;However, some people &lt;em&gt;do&lt;/em&gt; process traces they didn&amp;rsquo;t collect themselves (e.g.
user bug reports, automated collection from dogfood users). For those cases
we&amp;rsquo;ve strongly recommended sandboxing trace processor (e.g.
&lt;a href="https://gvisor.dev/"&gt;gvisor&lt;/a&gt;,
&lt;a href="https://developers.google.com/code-sandboxing/sandbox2"&gt;sandbox2&lt;/a&gt;, or
&lt;a href="https://google.github.io/minijail/"&gt;minijail&lt;/a&gt;) or, for even more sensitive use
cases, a VM.&lt;/p&gt;
&lt;p&gt;Beyond sandboxing, for catching issues proactively, we mainly relied on fuzzing
running internally in Google. These fuzzers occasionally surfaced real,
actionable bugs: we set them up to pass in arbitrary trace bytes (as this is the
main &amp;ldquo;attack surface&amp;rdquo;) but over time these became quite rare as they discovered
much of the low hanging fruit, which we quickly fixed. The bugs that remained
tend to live deep in the internals, reachable only with a very precisely crafted
sequence of bytes that a fuzzer is unlikely to hit by mutation alone.&lt;/p&gt;
&lt;p&gt;Apart from this, there has rarely been any bandwidth or resources for a human,
either a security expert or someone from my team, to spend lots of time finding
security issues in trace processor. There were always other parts of Perfetto
more worth spending security time on (e.g. the tracing service, on-device
profilers) as they&amp;rsquo;re actively running in production systems.&lt;/p&gt;
&lt;p&gt;All of this changed as of a couple of months ago. We started receiving bug
reports filed by some central team which appears to be running AI-based security
scanning against various projects throughout Google. Unfortunately, I have to be
hand wavy about what exactly they&amp;rsquo;re doing as their work doesn&amp;rsquo;t appear to be
public.&lt;/p&gt;
&lt;p&gt;Starting in early April, we had a slow drip of 1 bug a week, but since the end
of April this increased to a rate of several a week, with some days having 3 or
4 being opened in quick succession. This lasted until mid-May, at which point it
started tapering back to 1-2 a week with some weeks having none.&lt;/p&gt;
&lt;p&gt;I also want to say that the quality of the bugs is high. They&amp;rsquo;re well-described,
often with the relevant attacker model already worked out and even minimal fixes
proposed: basically everything I could ask for from a bug report. This matches
what both &lt;a href="https://daniel.haxx.se/blog/2026/04/22/high-quality-chaos/"&gt;curl&lt;/a&gt; and
&lt;a href="https://www.theregister.com/2026/03/26/greg_kroahhartman_ai_kernel/"&gt;Linux kernel&lt;/a&gt;
maintainers have noted about security bugs they&amp;rsquo;ve received, especially how
sharply quality has improved in the last few months.&lt;/p&gt;
&lt;p&gt;As I can only see the bugs that get filed against me, not the raw output of the
AI scanner, I don&amp;rsquo;t know exactly how much triage happens upstream. My guess is
there&amp;rsquo;s a human doing a light pass to drop obvious noise before reports reach
client teams, but judging from the rate at which bugs are opened and the way
they&amp;rsquo;re filed, I doubt anyone is deeply triaging each one.&lt;/p&gt;
&lt;p&gt;In total, we&amp;rsquo;ve received &lt;strong&gt;21&lt;/strong&gt; bugs (17 real issues and 4 not actionable),
which can be broken down into the following categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;10 bounds checking&lt;/strong&gt;: arbitrary trace data flowing into fixed-size buffers
or unchecked array indices, leading to out-of-bounds reads or writes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5 use-after-free&lt;/strong&gt;: back-pointers, pointer snapshots, or hashmap keys
outliving the object they refer to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1 stack overflow&lt;/strong&gt;: unbounded recursion when input is deeply nested.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1 access control&lt;/strong&gt;: not enforcing allowlists on some rare codepaths.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 closed as not actionable&lt;/strong&gt;: either where the chance of exploit was purely
hypothetical or where fixing would have required fundamental design changes
which were not worth the tiny security risk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All 17 real issues have been fixed, almost all shipping in Perfetto v56.0
&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id="how-it-feels-to-get-a-report"&gt;How it feels to get a report&lt;/h2&gt;
&lt;p&gt;How does receiving one of these reports actually feel? Well not as bad as you&amp;rsquo;d
think. Unlike a security-critical application like OpenSSL or curl, in trace
processor, a security issue is very unlikely to be a P0 I have to drop
everything to fix. Don&amp;rsquo;t get me wrong, it&amp;rsquo;s still a priority but one where I
have the luxury of taking a few days to figure out the right answer and can
release fixes according to our normal schedule, instead of trying to rush out a
CVE and get everyone to patch immediately.&lt;/p&gt;
&lt;p&gt;Also thankfully, because the majority of the issues are mechanical, the fixes
are generally quite straightforward.
&lt;a href="https://github.com/google/perfetto/pull/5586"&gt;Take this PR&lt;/a&gt;, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We build a key string into a fixed-size stack buffer while parsing some
metadata.&lt;/li&gt;
&lt;li&gt;The bounds check only runs in debug builds, and the metadata name comes
straight from the trace. Putting a long enough name means you would escape the
buffer.&lt;/li&gt;
&lt;li&gt;The fix is a simple matter of swapping the stack buffer for a std::string. The
code path is very cold (only once or twice in a trace) so the extra heap
allocation doesn&amp;rsquo;t matter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In fact, these sorts of issues are &lt;em&gt;so&lt;/em&gt; mechanical that I trust a coding agent
to just fix them with minimal guidance: take the well-written report, feed it to
the agent, and within ~10 minutes there&amp;rsquo;s a 10-20 line PR which fixes it. I
review every line thoroughly and make sure I understand it, but these tasks are
not difficult and firmly &lt;em&gt;inside&lt;/em&gt; the &amp;ldquo;jagged frontier&amp;rdquo; of what AI can do.&lt;/p&gt;
&lt;p&gt;I want to stress though that not every issue is mechanical or can be left to AI;
a few reports actually point more to design problems than incorrect function
implementations.
&lt;a href="https://github.com/google/perfetto/pull/5593"&gt;This use after free&lt;/a&gt; is a good
example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The problem was a state object held a back-pointer that could end up pointing
to freed memory given certain data appearing in the trace.&lt;/li&gt;
&lt;li&gt;The immediate dangling case was easy to patch by just having a callback which
invalidated the back-pointer on free. But this is a horrible hack which makes
the lifetimes of the objects involved impossible to reason about.&lt;/li&gt;
&lt;li&gt;The real problem here is that you had a child object whose parent could go
away before it, which really shouldn&amp;rsquo;t happen if this code is properly
architected.&lt;/li&gt;
&lt;li&gt;Fixing it properly meant restructuring the ownership model so the lifetime was
correct by construction.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The interesting thing was that this was a problem I was aware of and that I had
been meaning to clean up for close to a year but never got round to: the
security bug just gave me the push and justification to do it. This applied in a
couple of other bugs as well and made me internalize that security issues can
sometimes be correlated with deeper design flaws or hacky code so there are
wider benefits to &amp;ldquo;security scanning&amp;rdquo; than just the direct bugs they find.&lt;/p&gt;
&lt;h2 id="will-this-last"&gt;Will this last?&lt;/h2&gt;
&lt;p&gt;One thing I am wary of is how long this stream of bugs will keep up; I&amp;rsquo;m feeling
good about it given it&amp;rsquo;s only been going on for a couple of months, but I can
easily imagine that if this goes on for several more months, it might become
mentally exhausting.&lt;/p&gt;
&lt;p&gt;But my suspicion is that this will go to zero. Why? It&amp;rsquo;s to do with the pattern
of how these bugs are being filed. Each part of the codebase seems like it&amp;rsquo;s
getting a day or two of attention (and associated bugs) before moving on to a
different part. Repeats are rare, and the pace of bugs has slowed especially in
the last couple of weeks: we had a lot more in the start of May (several a week)
but now we&amp;rsquo;re down to 1-2 a week. There are a finite number of files, so
eventually my gut tells me they will run out.&lt;/p&gt;
&lt;p&gt;An important consideration is whether we&amp;rsquo;ll add new bugs faster than the scanner
can find old ones. My suspicion is no; the 17 real issues so far are from
scanning across 9 years of development. Even if that number triples before
things settle, the scanner is still working through years of accumulated code.
And we wrote a lot more code, a lot faster, in the earlier years of the project,
so the rate of new code being added now is lower than it once was.&lt;/p&gt;
&lt;p&gt;The other question is whether new model releases will find more complex design
issues rather than the simple issues we&amp;rsquo;re finding today. Those take
significantly more time and effort to fix and so would be a lot more painful if
we were to get many of those. I&amp;rsquo;m very unsure on this so we&amp;rsquo;ll just have to wait
and see!&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us&lt;/h2&gt;
&lt;p&gt;I feel Daniel Stenberg (curl maintainer) phrased it well in this
&lt;a href="https://daniel.haxx.se/blog/2026/05/11/mythos-finds-a-curl-vulnerability/"&gt;post&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Any project that has not scanned their source code with AI powered tooling
will likely find huge number of flaws, bugs and possible vulnerabilities with
this new generation of tools.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This rings very true to me. More broadly, I think folks will have one of three
experiences:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Untrusted input + security critical&lt;/em&gt; (e.g. curl, kernel, OpenSSL): many
complex reports, with a higher false positive rate than the other categories,
because there&amp;rsquo;s a lot of attention on the project and much of the low hanging
fruit would already have been picked in the critical codepaths. Though
codepaths for lesser-used functionality (e.g. legacy drivers) could end up in
category 2 instead.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Untrusted input + not previously audited&lt;/em&gt; (e.g. trace processor): a wave of
mechanical bugs at a manageable pace and low individual stress because the
project is not on a security critical code path. This is where both Daniel
and I expect AI security scanners to have the most impact.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;No untrusted input&lt;/em&gt; (internal tools, math libs, anything operating only on
trusted data): you probably won&amp;rsquo;t notice this shift at all.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My own case sits squarely in that second bucket. But I don&amp;rsquo;t want to
over-generalize from my experience, because there are three things that make
this manageable for me that wouldn&amp;rsquo;t be true for everyone: a) I&amp;rsquo;m paid to
maintain trace processor as part of my full time job; b) someone else is taking
the effort to run the AI scans and discover the bugs in the first place; c) the
reports appear to be lightly filtered by an upstream human reviewer, enough to
strip obvious noise but probably not a deep triage.&lt;/p&gt;
&lt;p&gt;To me, this points to a gap in the ecosystem: most open-source projects cannot
afford to have a dedicated team doing security scanning for them, and telling a
maintainer to stand up their own pipeline when their security risk is marginal
will restrict this to only the most motivated projects. I would guess we&amp;rsquo;re
going to see a lot more innovation in this space, including from the big AI
labs.&lt;/p&gt;
&lt;p&gt;All in all, I&amp;rsquo;m cautiously positive about my own experience: most of the bugs
are mechanical, a few have nudged long-overdue design cleanups, and the pace is
manageable. There&amp;rsquo;s plenty I don&amp;rsquo;t know about how this evolves: whether the pace
holds, whether future models start finding harder design issues. So this should
very much be treated as a snapshot, not a forecast!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;A common response I expect is: &amp;ldquo;if it&amp;rsquo;s parsing arbitrary binary data like
traces, it should be written in Rust.&amp;rdquo; In a vacuum I agree and if I was
writing trace processor from scratch today, I would definitely use Rust. But
&lt;em&gt;switching&lt;/em&gt; to Rust is unfortunately quite impractical; the library is a
significant amount of code and is embedded in hundreds of downstream tools,
many in environments that don&amp;rsquo;t have a Rust toolchain. Asking all our
embedders to start using Rust would be a significant burden and one I don&amp;rsquo;t
want to impose. Not to mention that our team doesn&amp;rsquo;t actually have any Rust
expertise so reviewing this code at a standard I want from trace processor
would be a significant productivity hit until folks got up to speed. And
unlike others in the industry, I don&amp;rsquo;t feel comfortable just rewriting the
whole project in one shot and calling it a day&amp;hellip;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;The couple of remaining bugs were found after the v56.0 release was cut and
are low-priority enough that it&amp;rsquo;s not worth rushing out a release for them.
They have already been fixed on main and will be fixed in an upcoming point
release.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>Don't answer the first question</title><link>https://lalitm.com/post/dont-answer-the-first-question/</link><pubDate>Sat, 16 May 2026 14:19:00 +0100</pubDate><guid>https://lalitm.com/post/dont-answer-the-first-question/</guid><description>In my work on Perfetto, a performance debugging tool, one question I get often is: “how do I split a Perfetto trace into multiple files?” Instead of answering directly, I say: “there isn’t an easy way to do that, but what’s leading you to collect traces large enough to want to split?”
This is one of my golden rules at work. When a user asks me something “weird”: don’t answer the first version of the question.
On the surface this might appear like I’m talking about the XY problem, but that stops one step short. It treats the user’s stated question as a puzzle to decode: figure out what they really meant, answer that, move on. I think we can go much further.
Instead, the confusion that produced the wrong question is itself an opening, and the conversation it sparks is valuable to both sides. The user walks away with a better mental model of the tool. I walk away with a clearer picture of where the product confuses people. And sometimes, between us, we figure out that the product itself needs to change.</description><content:encoded>&lt;p&gt;In my work on Perfetto, a performance debugging tool, one question I get often
is: “how do I split a Perfetto trace into multiple files?” Instead of answering
directly, I say: “there isn’t an easy way to do that, but what’s leading you to
collect traces large enough to want to split?”&lt;/p&gt;
&lt;p&gt;This is one of my golden rules at work. When a user asks me something &amp;ldquo;weird&amp;rdquo;:
&lt;em&gt;don&amp;rsquo;t answer the first version of the question&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;On the surface this might appear like I&amp;rsquo;m talking about the
&lt;a href="https://en.wikipedia.org/wiki/XY_problem"&gt;XY problem&lt;/a&gt;, but that stops one step
short. It treats the user&amp;rsquo;s stated question as a puzzle to decode: figure out
what they really meant, answer that, move on. I think we can go much further.&lt;/p&gt;
&lt;p&gt;Instead, the confusion that produced the wrong question is itself an opening,
and the conversation it sparks is valuable to both sides. The user walks away
with a better mental model of the tool. I walk away with a clearer picture of
where the product confuses people. And sometimes, between us, we figure out that
the product itself needs to change.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve written before about how I can still be a successful engineer while
&lt;a href="https://lalitm.com/software-engineering-outside-the-spotlight/"&gt;avoiding the spotlight&lt;/a&gt;.
While that covered the general strategy, this is one of the concrete tactics
that makes it work. I should also say this post is aimed at people who build
things for other engineers. If you&amp;rsquo;re building a consumer product, or a B2B
service, it will translate less directly, but the underlying instinct might
still be useful.&lt;/p&gt;
&lt;h2 id="diagnosing-the-ask"&gt;Diagnosing the ask&lt;/h2&gt;
&lt;p&gt;Some questions are easy, routine, and purely a matter of pointing at
documentation; those don&amp;rsquo;t merit much discussion here. The interesting cases are
where something is out of the ordinary, and it&amp;rsquo;s rare that the user will have
given me enough information in their first ask&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;So I run a mental checklist to figure out where to go next:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have I seen this before? If so, I might already have an answer to hand. If
not, it&amp;rsquo;s uncommon enough that I want to slow down.&lt;/li&gt;
&lt;li&gt;Does the question even sound reasonable compared to others I&amp;rsquo;ve seen? If not,
why might they be asking it, and is there a more normal question underneath?&lt;/li&gt;
&lt;li&gt;Does it fit the shape of the tool? Or is the user fighting the architecture
without realizing it?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once I&amp;rsquo;ve figured out what feels off, the next step is asking something that
will surface the missing context. I might say something like &amp;ldquo;well the answer to
your immediate question is X but that&amp;rsquo;s a pretty strange thing to ask for
because of reason Y. Can you tell me more about the wider problem you&amp;rsquo;re trying
to solve?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;This will probably be the start of a back and forth. How quickly it moves
depends on how well the user can communicate their thoughts. But we&amp;rsquo;ll usually
end up in one of a few places: they&amp;rsquo;re missing the philosophy of the tool, the
product is hiding the right path or the product itself needs to change.&lt;/p&gt;
&lt;h2 id="when-theyre-missing-the-philosophy"&gt;When they&amp;rsquo;re missing the philosophy&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s quite common for users to come to us not knowing what they want, or not
understanding the problem they&amp;rsquo;re trying to solve.&lt;/p&gt;
&lt;p&gt;To be clear, I&amp;rsquo;m not criticizing them for this; teams are often trying to solve
problems with limited time or resources, and they turn to new debugging tools
when they&amp;rsquo;re struggling to make progress. As a result, they often find the tool,
find it does most of what they want but doesn&amp;rsquo;t match their model of &amp;ldquo;how it
should work&amp;rdquo;. So they file a feature request.&lt;/p&gt;
&lt;p&gt;A common version of this: people come to Perfetto, see that a trace is a highly
detailed recording of what a device did over a window of time, realize you can
compute metrics from a Perfetto trace, and treat it as a holy grail solution to
all their problems. Want a frame rate? Count frames in the trace. Memory used by
an app? Look at the allocations and frees. In principle, any metric &lt;em&gt;could&lt;/em&gt; be
computed from a trace.&lt;/p&gt;
&lt;p&gt;But this is a bad idea for a simple reason: traces are expensive to collect and
process: you&amp;rsquo;re collecting all the data about the system rather than samply a
single number. You&amp;rsquo;re going to waste a lot of resources when instead, a
dedicated metric collection system would do the job much more
efficiently&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;My overarching point is that there&amp;rsquo;s a certain philosophy to how tools are
designed, and users often miss it because they&amp;rsquo;re focused on their immediate
problem.&lt;/p&gt;
&lt;p&gt;A big part of my job is teaching the team how to approach performance
engineering in the first place, not just explaining how to use Perfetto. It
means making people aware of the tools they have available, how to think about
things like startup, frame drops, memory, and power, and how to work with them
both in normal situations and when something goes wrong.&lt;/p&gt;
&lt;h2 id="when-the-right-path-is-hidden"&gt;When the right path is hidden&lt;/h2&gt;
&lt;p&gt;Other times the team understands the problem; they just don&amp;rsquo;t see how to put
existing tools together. Our tools are powerful by design, and we have to be
mindful that other teams might not understand the full range of what we&amp;rsquo;ve
built. It&amp;rsquo;s my job to figure out what they actually want. Often, something we
built for a different purpose can be repurposed to meet their needs.&lt;/p&gt;
&lt;p&gt;A perfect example here is what I already discussed: trace splitting. The
conversation goes something like, &amp;ldquo;&amp;hellip;what’s leading you to collect traces large
enough to want to split?&amp;rdquo; They say it&amp;rsquo;s because they have periods of interest in
a long trace and want to slice it up. Partly for performance, partly to make
visualizing easier.&lt;/p&gt;
&lt;p&gt;But then I point out that Perfetto already supports
&lt;a href="https://perfetto.dev/docs/getting-started/periodic-trace-snapshots"&gt;periodic trace snapshots&lt;/a&gt;,
short repeated recordings instead of one long one, which removes the need to
collect a long trace at all. They&amp;rsquo;re trying to solve a problem they shouldn&amp;rsquo;t be
having in the first place.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s always satisfying to see people say &amp;ldquo;that&amp;rsquo;s exactly what I needed!&amp;rdquo; even
though it&amp;rsquo;s not what they asked for. It means I successfully figured out what
they actually wanted rather than what they thought they did.&lt;/p&gt;
&lt;h2 id="when-the-product-needs-to-change"&gt;When the product needs to change&lt;/h2&gt;
&lt;p&gt;Occasionally, the response reveals something genuinely new, something that could
set us on the path to building something big. These cases are tricky: even when
the ask is novel, the asker often can&amp;rsquo;t tell you what they actually need.&lt;/p&gt;
&lt;p&gt;The cost of getting things wrong in foundational software is high, so I err on
the side of not building something until not having it hurts: multiple teams
have come to me saying &amp;ldquo;we want this.&amp;rdquo; Ideally by then we&amp;rsquo;ve found the essence
of what&amp;rsquo;s actually worth building. This is very unlikely to happen after just
one ask, so waiting is really powerful.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve made this mistake at Perfetto. Take ad-hoc UI customization. People wanted
to hack on the UI to suit their workflows, and they kept complaining about how
hard that was. So we let them, and immediately took on a huge amount of
technical debt. Every new feature had to interact with every existing one, and
the whole thing quickly became impossible to scale.&lt;/p&gt;
&lt;p&gt;It took us roughly one year to dig ourselves out of this hole by designing a
proper plugin API. The real need, which no one could articulate up front, was a
way to &lt;em&gt;personalize&lt;/em&gt; the UI to the needs of their team or use case without
affecting every other user. But of course no one was able to name this until it
was too late. So it was our responsibility to spot this early as the requests
flowed in.&lt;/p&gt;
&lt;p&gt;Allowing folks to &amp;ldquo;merge&amp;rdquo; Perfetto traces was one we got right. People asked
about it constantly, but we held off. We pointed people at workarounds and said
&amp;ldquo;we&amp;rsquo;ll see.&amp;rdquo; We knew doing it properly was a lot of work and easy to get wrong,
so we waited. We finally built it last year in a maintainable way, but only
because we understood the problem space so well in the first place.&lt;/p&gt;
&lt;h2 id="the-point"&gt;The point&lt;/h2&gt;
&lt;p&gt;The first version of a question is rarely the real one. Ask why before you
answer. Sometimes the answer teaches the user how the tool is meant to be used.
Sometimes it tells you the product is hiding the right path. Sometimes it tells
you there&amp;rsquo;s nothing worth building yet. And sometimes it sets you up to build
the next big thing once, not twice.&lt;/p&gt;
&lt;p&gt;The technique is simple, but easy to skip because the pull to be responsive is
constant, and every quick answer feels productive. Take the small step back
anyway. Both sides almost always walk away with more than they came in with.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;It&amp;rsquo;s always a pleasant surprise when that does happen!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Computing metrics from traces works fine for local development and most lab
tests. It breaks down when you need fine-grained measurement with low noise,
or when you&amp;rsquo;re collecting in the field on real users&amp;rsquo; devices.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>Eight years of wanting, three months of building with AI</title><link>https://lalitm.com/post/building-syntaqlite-ai/</link><pubDate>Sun, 05 Apr 2026 13:00:00 +0100</pubDate><guid>https://lalitm.com/post/building-syntaqlite-ai/</guid><description>For eight years, I’ve wanted a high-quality set of devtools for working with SQLite. Given how important SQLite is to the industry1, I’ve long been puzzled that no one has invested in building a really good developer experience for it2.
A couple of weeks ago, after ~250 hours of effort over three months3 on evenings, weekends, and vacation days, I finally released syntaqlite (GitHub), fulfilling this long-held wish. And I believe the main reason this happened was because of AI coding agents4.
Of course, there’s no shortage of posts claiming that AI one-shot their project or pushing back and declaring that AI is all slop. I’m going to take a very different approach and, instead, systematically break down my experience building syntaqlite with AI, both where it helped and where it was detrimental.
I’ll do this while contextualizing the project and my background so you can independently assess how generalizable this experience was. And whenever I make a claim, I’ll try to back it up with evidence from my project journal, coding transcripts, or commit history5.</description><content:encoded>&lt;p&gt;For eight years, I&amp;rsquo;ve wanted a high-quality set of devtools for working with
SQLite. Given how important SQLite is to the industry&lt;sup class="sn-ref" id="sn-ref-sqlite-industry"&gt;&lt;a href="#sn-sqlite-industry"&gt;1&lt;/a&gt;&lt;/sup&gt;, I&amp;rsquo;ve long been puzzled that no one has invested in building
a really good developer experience for it&lt;sup class="sn-ref" id="sn-ref-devtools"&gt;&lt;a href="#sn-devtools"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;A couple of weeks ago, after ~250 hours of effort over three months&lt;sup class="sn-ref" id="sn-ref-hours"&gt;&lt;a href="#sn-hours"&gt;3&lt;/a&gt;&lt;/sup&gt; on evenings, weekends, and vacation days, I finally
&lt;a href="/post/syntaqlite/"&gt;released syntaqlite&lt;/a&gt;
(&lt;a href="https://github.com/LalitMaganti/syntaqlite"&gt;GitHub&lt;/a&gt;), fulfilling this
long-held wish. And I believe the main reason this happened was because of AI
coding agents&lt;sup class="sn-ref" id="sn-ref-codingtools"&gt;&lt;a href="#sn-codingtools"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Of course, there&amp;rsquo;s no shortage of posts claiming that AI one-shot their project
or pushing back and declaring that AI is all slop. I&amp;rsquo;m going to take a very
different approach and, instead, systematically break down my experience
building syntaqlite with AI, both where it helped &lt;em&gt;and&lt;/em&gt; where it was
detrimental.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll do this while contextualizing the project and my background so you can
independently assess how generalizable this experience was. And whenever I make
a claim, I&amp;rsquo;ll try to back it up with evidence from my project journal, coding
transcripts, or commit history&lt;sup class="sn-ref" id="sn-ref-evidence"&gt;&lt;a href="#sn-evidence"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id="why-i-wanted-it"&gt;Why I wanted it&lt;/h2&gt;
&lt;p&gt;In my work on &lt;a href="https://docs.perfetto.dev"&gt;Perfetto&lt;/a&gt;, I maintain a SQLite-based
language for querying performance traces called
&lt;a href="https://perfetto.dev/docs/analysis/perfetto-sql-getting-started"&gt;PerfettoSQL&lt;/a&gt;.
It&amp;rsquo;s basically the same as SQLite but with a few extensions to make the trace
querying experience better. There are ~100K lines of PerfettoSQL internally in
Google and it&amp;rsquo;s used by a wide range of teams.&lt;/p&gt;
&lt;p&gt;Having a language which gets traction means your users also start expecting
things like formatters, linters, and editor extensions. I&amp;rsquo;d hoped that we could
adapt some SQLite tools from open source but the more I looked into it, the more
disappointed I was. What I found either wasn&amp;rsquo;t reliable enough, fast
enough&lt;sup class="sn-ref" id="sn-ref-speed-comparison"&gt;&lt;a href="#sn-speed-comparison"&gt;6&lt;/a&gt;&lt;/sup&gt;, or flexible enough to adapt to PerfettoSQL. There was
clearly an opportunity to build something from scratch, but it was never the
&amp;ldquo;most important thing we could work on&amp;rdquo;. We&amp;rsquo;ve been reluctantly making do with
the tools out there but always wishing for better.&lt;/p&gt;
&lt;p&gt;On the other hand, there &lt;em&gt;was&lt;/em&gt; the option to do something in my spare time. I
had built lots of open source projects in my teens&lt;sup class="sn-ref" id="sn-ref-holoirc"&gt;&lt;a href="#sn-holoirc"&gt;7&lt;/a&gt;&lt;/sup&gt; but this
had faded away during university when I felt that I just didn&amp;rsquo;t have the
motivation anymore. Being a maintainer is much more than just &amp;ldquo;throwing the code
out there&amp;rdquo; and seeing what happens. It&amp;rsquo;s triaging bugs, investigating crashes,
writing documentation, building a community, and, most importantly, having a
direction for the project.&lt;/p&gt;
&lt;p&gt;But the itch of open source (specifically freedom to work on what I wanted while
helping others) had never gone away. The SQLite devtools project was eternally
in my mind as &amp;ldquo;something I&amp;rsquo;d like to work on&amp;rdquo;. But there was another reason why
I kept putting it off: it sits at the intersection of being both hard &lt;em&gt;and&lt;/em&gt;
tedious.&lt;/p&gt;
&lt;h2 id="what-makes-it-hard-and-tedious"&gt;What makes it hard and tedious&lt;/h2&gt;
&lt;p&gt;If I was going to invest my personal time working on this project, I didn&amp;rsquo;t want
to build something that only helped Perfetto: I wanted to make it work for &lt;em&gt;any&lt;/em&gt;
SQLite user out there&lt;sup class="sn-ref" id="sn-ref-ambition"&gt;&lt;a href="#sn-ambition"&gt;8&lt;/a&gt;&lt;/sup&gt;. And this means parsing SQL &lt;em&gt;exactly&lt;/em&gt;
like SQLite.&lt;/p&gt;
&lt;p&gt;The heart of any language-oriented devtool is the parser. This is responsible
for turning the source code into a &amp;ldquo;parse tree&amp;rdquo; which acts as the central data
structure anything else is built on top of. If your parser isn&amp;rsquo;t accurate, then
your formatters and linters will inevitably inherit those inaccuracies; many of
the tools I found suffered from having parsers which approximated the SQLite
language rather than representing it precisely.&lt;/p&gt;
&lt;p&gt;Unfortunately, unlike many other languages, SQLite has no formal specification
describing how it should be parsed. It doesn&amp;rsquo;t expose a stable API for its
parser either. In fact, quite uniquely, in its implementation it doesn&amp;rsquo;t even
build a parse tree at all&lt;sup class="sn-ref" id="sn-ref-no-parse-tree"&gt;&lt;a href="#sn-no-parse-tree"&gt;9&lt;/a&gt;&lt;/sup&gt;! The only reasonable approach
left in my opinion is to carefully extract the relevant parts of SQLite&amp;rsquo;s source
code and adapt it to build the parser I wanted&lt;sup class="sn-ref" id="sn-ref-extraction"&gt;&lt;a href="#sn-extraction"&gt;10&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;This means getting into the weeds of SQLite source code, a fiendishly difficult
codebase to understand. The whole project is written in C in an
&lt;a href="https://sqlite.org/src/file?name=src/vdbe.c&amp;amp;ci=trunk"&gt;incredibly dense style&lt;/a&gt;;
I&amp;rsquo;ve spent days just understanding the virtual table
&lt;a href="https://www.sqlite.org/vtab.html"&gt;API&lt;/a&gt;&lt;sup class="sn-ref" id="sn-ref-vtab-nuance"&gt;&lt;a href="#sn-vtab-nuance"&gt;11&lt;/a&gt;&lt;/sup&gt; and
&lt;a href="https://sqlite.org/src/file?name=src/vtab.c&amp;amp;ci=trunk"&gt;implementation&lt;/a&gt;. Trying
to grasp the full parser stack was daunting.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the fact that there are &amp;gt;400 rules in SQLite which capture the full
surface area of its language. I&amp;rsquo;d have to specify in each of these &amp;ldquo;grammar
rules&amp;rdquo; how that part of the syntax maps to the matching node in the parse tree.
It&amp;rsquo;s extremely repetitive work; each rule is similar to all the ones around it
but also, by definition, different.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s not just the rules but also coming up with and writing tests to make
sure it&amp;rsquo;s correct, debugging if something is wrong, triaging and fixing the
inevitable bugs people filed when I got something wrong&amp;hellip;&lt;/p&gt;
&lt;p&gt;For years, this was where the idea died. Too hard for a side project&lt;sup class="sn-ref" id="sn-ref-complexity"&gt;&lt;a href="#sn-complexity"&gt;12&lt;/a&gt;&lt;/sup&gt;, too tedious to sustain motivation, too risky to invest months
into something that might not work.&lt;/p&gt;
&lt;h2 id="how-it-happened"&gt;How it happened&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been using coding agents since early 2025 (Aider, Roo Code, then Claude
Code since July) and they&amp;rsquo;d definitely been useful but never something I felt I
could trust a serious project to. But towards the end of 2025, the models seemed
to make a significant step forward in quality&lt;sup class="sn-ref" id="sn-ref-agents-got-good"&gt;&lt;a href="#sn-agents-got-good"&gt;13&lt;/a&gt;&lt;/sup&gt;. At the
same time, I kept hitting problems in Perfetto which would have been trivially
solved by having a reliable parser. Each workaround left the same thought in the
back of my mind: maybe it&amp;rsquo;s finally time to build it for real.&lt;/p&gt;
&lt;p&gt;I got some space to think and reflect over Christmas and decided to really
stress test the most maximalist version of AI: could I vibe-code the whole thing
using just Claude Code on the Max plan (£200/month)?&lt;/p&gt;
&lt;p&gt;Through most of January, I iterated, acting as semi-technical manager and
delegating almost all the design and all the implementation to Claude.
Functionally, I ended up in a reasonable place: a parser in C extracted from
SQLite sources using a bunch of Python scripts, a formatter built on top,
support for both the SQLite language and the PerfettoSQL extensions, all exposed
in a web playground.&lt;/p&gt;
&lt;p&gt;But when I reviewed the codebase in detail in late January, the downside was
obvious: the codebase was complete spaghetti&lt;sup class="sn-ref" id="sn-ref-spaghetti"&gt;&lt;a href="#sn-spaghetti"&gt;14&lt;/a&gt;&lt;/sup&gt;. I didn&amp;rsquo;t
understand large parts of the Python source extraction pipeline, functions were
scattered in random files without a clear shape, and a few files had grown to
several thousand lines. It was &lt;em&gt;extremely&lt;/em&gt; fragile; it solved the immediate
problem &lt;em&gt;but&lt;/em&gt; it was never going to cope with my larger vision, never mind
integrating it into the Perfetto tools. The saving grace was that it had proved
the approach was viable and generated more than 500 tests, many of which I felt
I could reuse.&lt;/p&gt;
&lt;p&gt;I decided to throw away everything and start from scratch while also switching
most of the codebase to Rust&lt;sup class="sn-ref" id="sn-ref-rust-not-c"&gt;&lt;a href="#sn-rust-not-c"&gt;15&lt;/a&gt;&lt;/sup&gt;. I could see that C was going
to make it difficult to build the higher level components like the validator and
the language server implementation. And as a bonus, it would also let me use the
same language for both the extraction and runtime instead of splitting it across
C and Python.&lt;/p&gt;
&lt;p&gt;More importantly, I completely changed my role in the project. I took ownership
of all decisions&lt;sup class="sn-ref" id="sn-ref-took-control"&gt;&lt;a href="#sn-took-control"&gt;16&lt;/a&gt;&lt;/sup&gt; and used it more as &amp;ldquo;autocomplete on
steroids&amp;rdquo; inside a much tighter process: opinionated design upfront, reviewing
every change thoroughly, fixing problems eagerly as I spotted them, and
investing in scaffolding (like linting, validation, and non-trivial
testing&lt;sup class="sn-ref" id="sn-ref-scaffolding"&gt;&lt;a href="#sn-scaffolding"&gt;17&lt;/a&gt;&lt;/sup&gt;) to check AI output automatically.&lt;/p&gt;
&lt;p&gt;The core features came together through February and the final stretch (upstream
test validation, editor extensions, packaging, docs) led to a 0.1 launch in
mid-March.&lt;/p&gt;
&lt;p&gt;But in my opinion, this timeline is the least interesting part of this story.
What I really want to talk about is what wouldn&amp;rsquo;t have happened without AI and
also the toll it took on me as I used it.&lt;/p&gt;
&lt;h2 id="ai-is-why-this-project-exists-and-why-its-as-complete-as-it-is"&gt;AI is why this project exists, and why it&amp;rsquo;s as complete as it is&lt;/h2&gt;
&lt;h3 id="overcoming-inertia"&gt;Overcoming inertia&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;a href="https://lalitm.com/llm-motivation-via-emotions/"&gt;written in the past&lt;/a&gt;
about how one of my biggest weaknesses as a software engineer is my tendency to
procrastinate when facing a big new project. Though I didn&amp;rsquo;t realize it at the
time, it could not have applied more perfectly to building syntaqlite.&lt;/p&gt;
&lt;p&gt;AI basically let me put aside all my doubts on technical calls, my uncertainty
of building the right thing and my reluctance to get started by giving me very
concrete problems to work on. Instead of &amp;ldquo;I need to understand how SQLite&amp;rsquo;s
parsing works&amp;rdquo;, it was &amp;ldquo;I need to get AI to suggest an approach for me so I can
tear it up and build something better&amp;quot;&lt;sup class="sn-ref" id="sn-ref-inertia-journal"&gt;&lt;a href="#sn-inertia-journal"&gt;18&lt;/a&gt;&lt;/sup&gt;. I work so much
better with concrete prototypes to play with and code to look at than endlessly
thinking about designs in my head, and AI lets me get to that point at a pace I
could not have dreamed about before. Once I took the first step, every step
after that was so much easier.&lt;/p&gt;
&lt;h3 id="faster-at-churning-code"&gt;Faster at churning code&lt;/h3&gt;
&lt;p&gt;AI turned out to be better than me at the act of writing code itself, assuming
that code is obvious. If I can break a problem down to &amp;ldquo;write a function with
this behaviour and parameters&amp;rdquo; or &amp;ldquo;write a class matching this interface,&amp;rdquo; AI
will build it faster than I would and, crucially, in a style that might well be
more intuitive to a future reader. It documents things I&amp;rsquo;d skip, lays out code
consistently with the rest of the project, and sticks to what you might call the
&amp;ldquo;standard dialect&amp;rdquo; of whatever language you&amp;rsquo;re working
in&lt;sup class="sn-ref" id="sn-ref-standard-dialect"&gt;&lt;a href="#sn-standard-dialect"&gt;19&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;That standardness is a double-edged sword. For the vast majority of code in any
project, standard is exactly what you want: predictable, readable, unsurprising.
But every project has pieces that are its edge, the parts where the value comes
from doing something non-obvious. For syntaqlite, that was the extraction
pipeline and the parser architecture. AI&amp;rsquo;s instinct to normalize was actively
harmful there, and those were the parts I had to design in depth and often
resorted to just writing myself.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the flip side: the same speed that makes AI great at obvious code
also makes it great at refactoring. If you&amp;rsquo;re using AI to generate code at
industrial scale, you &lt;em&gt;have&lt;/em&gt; to refactor constantly and
continuously&lt;sup class="sn-ref" id="sn-ref-refactoring-journal"&gt;&lt;a href="#sn-refactoring-journal"&gt;20&lt;/a&gt;&lt;/sup&gt;. If you don&amp;rsquo;t, things immediately get
out of hand. This was the central lesson of the vibe-coding month: I didn&amp;rsquo;t
refactor enough, the codebase became something I couldn&amp;rsquo;t reason about, and I
had to throw it all away. In the rewrite, refactoring became the core of my
workflow. After every large batch of generated code, I&amp;rsquo;d step back and ask &amp;ldquo;is
this ugly?&amp;rdquo; Sometimes AI could clean it up. Other times there was a large-scale
abstraction that AI couldn&amp;rsquo;t see but I could; I&amp;rsquo;d give it the direction and let
it execute&lt;sup class="sn-ref" id="sn-ref-refactor-pattern"&gt;&lt;a href="#sn-refactor-pattern"&gt;21&lt;/a&gt;&lt;/sup&gt;. If you have taste, the cost of a wrong
approach drops dramatically because you can restructure
quickly&lt;sup class="sn-ref" id="sn-ref-refactor-taste"&gt;&lt;a href="#sn-refactor-taste"&gt;22&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id="teaching-assistant"&gt;Teaching assistant&lt;/h3&gt;
&lt;p&gt;Of all the ways I used AI, research had by far the highest ratio of value
delivered to time spent.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve worked with interpreters and parsers before but I had never heard of
Wadler-Lindig pretty printing&lt;sup class="sn-ref" id="sn-ref-wadler-lindig"&gt;&lt;a href="#sn-wadler-lindig"&gt;23&lt;/a&gt;&lt;/sup&gt;. When I needed to build
the formatter, AI gave me a concrete and actionable lesson from a point of view
I could understand and pointed me to the papers to learn more. I could have
found this myself eventually, but AI compressed what might have been a day or
two of reading into a focused conversation where I could ask &amp;ldquo;but why does this
work?&amp;rdquo; until I actually got it.&lt;/p&gt;
&lt;p&gt;This extended to entire domains I&amp;rsquo;d never worked in. I have deep C++ and Android
performance expertise but had barely touched Rust tooling or editor extension
APIs. With AI, it wasn&amp;rsquo;t a problem: the fundamentals are the same, the
terminology is similar, and AI bridges the gap&lt;sup class="sn-ref" id="sn-ref-lateral-moves"&gt;&lt;a href="#sn-lateral-moves"&gt;24&lt;/a&gt;&lt;/sup&gt;. The VS
Code extension would have taken me a day or two of learning the API before I
could even start. With AI, I had a working extension within an hour.&lt;/p&gt;
&lt;p&gt;It was also invaluable for reacquainting myself with parts of the project I
hadn&amp;rsquo;t looked at for a few days&lt;sup class="sn-ref" id="sn-ref-context-reacquisition"&gt;&lt;a href="#sn-context-reacquisition"&gt;25&lt;/a&gt;&lt;/sup&gt;. I could control
how deep to go: &amp;ldquo;tell me about this component&amp;rdquo; for a surface-level refresher,
&amp;ldquo;give me a detailed linear walkthrough&amp;rdquo; for a deeper dive, &amp;ldquo;audit unsafe usages
in this repo&amp;rdquo; to go hunting for problems. When you&amp;rsquo;re context switching a lot,
you lose context fast. AI let me reacquire it on demand.&lt;/p&gt;
&lt;h3 id="more-than-id-have-built-alone"&gt;More than I&amp;rsquo;d have built alone&lt;/h3&gt;
&lt;p&gt;Beyond making the project exist at all, AI is also the reason it shipped as
complete as it did. Every open source project has a long tail of features that
are important but not critical: the things you know theoretically how to do but
keep deprioritizing because the core work is more pressing. For syntaqlite, that
list was long: editor extensions, Python bindings, a WASM playground, a docs
site, packaging for multiple ecosystems&lt;sup class="sn-ref" id="sn-ref-last-mile-list"&gt;&lt;a href="#sn-last-mile-list"&gt;26&lt;/a&gt;&lt;/sup&gt;. AI made these
cheap enough that skipping them felt like the wrong trade-off.&lt;/p&gt;
&lt;p&gt;It also freed up mental energy for UX&lt;sup class="sn-ref" id="sn-ref-ux-focus"&gt;&lt;a href="#sn-ux-focus"&gt;27&lt;/a&gt;&lt;/sup&gt;. Instead of spending
all my time on implementation, I could think about what a user&amp;rsquo;s first
experience should feel like: what error messages would actually help them fix
their SQL, how the formatter output should look by default, whether the CLI
flags were intuitive. These are the things that separate a tool people try once
from one they keep using, and AI gave me the headroom to care about them.
Without AI, I would have built something much smaller, probably no editor
extensions or docs site. AI didn&amp;rsquo;t just make the same project faster. It changed
what the project &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="where-ai-had-its-costs"&gt;Where AI had its costs&lt;/h2&gt;
&lt;h3 id="the-addiction"&gt;The addiction&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s an uncomfortable parallel between using AI coding tools and playing slot
machines&lt;sup class="sn-ref" id="sn-ref-addiction"&gt;&lt;a href="#sn-addiction"&gt;28&lt;/a&gt;&lt;/sup&gt;. You send a prompt, wait, and either get something
great or something useless. I found myself up late at night wanting to do &amp;ldquo;just
one more prompt,&amp;rdquo; constantly trying AI just to see what would happen even when I
knew it probably wouldn&amp;rsquo;t work. The sunk cost fallacy kicked in too: I&amp;rsquo;d keep at
it even in tasks it was clearly ill-suited for, telling myself &amp;ldquo;maybe if I
phrase it differently this time.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The tiredness feedback loop made it worse&lt;sup class="sn-ref" id="sn-ref-tiredness-loop"&gt;&lt;a href="#sn-tiredness-loop"&gt;29&lt;/a&gt;&lt;/sup&gt;. When I had
energy, I could write precise, well-scoped prompts and be genuinely productive.
But when I was tired, my prompts became vague, the output got worse, and I&amp;rsquo;d try
again, getting more tired in the process. In these cases, AI was probably slower
than just implementing something myself, but it was too hard to break out of the
loop&lt;sup class="sn-ref" id="sn-ref-ai-slower"&gt;&lt;a href="#sn-ai-slower"&gt;30&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id="losing-touch"&gt;Losing touch&lt;/h3&gt;
&lt;p&gt;Several times during the project, I lost my mental model of the
codebase&lt;sup class="sn-ref" id="sn-ref-losing-touch"&gt;&lt;a href="#sn-losing-touch"&gt;31&lt;/a&gt;&lt;/sup&gt;. Not the overall architecture or how things
fitted together. But the day-to-day details of what lived where, which functions
called which, the small decisions that accumulate into a working system. When
that happened, surprising issues would appear and I&amp;rsquo;d find myself at a total
loss to understand what was going wrong. I hated that feeling.&lt;/p&gt;
&lt;p&gt;The deeper problem was that losing touch created a communication
breakdown&lt;sup class="sn-ref" id="sn-ref-communication-breakdown"&gt;&lt;a href="#sn-communication-breakdown"&gt;32&lt;/a&gt;&lt;/sup&gt;. When you don&amp;rsquo;t have the mental
thread of what&amp;rsquo;s going on, it becomes impossible to communicate meaningfully
with the agent. Every exchange gets longer and more verbose. Instead of &amp;ldquo;change
FooClass to do X,&amp;rdquo; you end up saying &amp;ldquo;change the thing which does Bar to do X&amp;rdquo;.
Then the agent has to figure out what Bar is, how that maps to FooClass, and
sometimes it gets it wrong&lt;sup class="sn-ref" id="sn-ref-manager-analogy"&gt;&lt;a href="#sn-manager-analogy"&gt;33&lt;/a&gt;&lt;/sup&gt;. It&amp;rsquo;s exactly the same
complaint engineers have always had about managers who don&amp;rsquo;t understand the code
asking for fanciful or impossible things. Except now you&amp;rsquo;ve become that manager.&lt;/p&gt;
&lt;p&gt;The fix was deliberate: I made it a habit to read through the code immediately
after it was implemented and actively engage to see &amp;ldquo;how would I have done this
differently?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Of course, in some sense all of the above is also true of code I wrote a few
months ago (hence the
&lt;a href="https://text-incubation.com/AI+code+is+legacy+code+from+day+one"&gt;sentiment that AI code is legacy code&lt;/a&gt;),
but AI makes the drift happen faster because you&amp;rsquo;re not building the same muscle
memory that comes from originally typing it out.&lt;/p&gt;
&lt;h3 id="the-slow-corrosion"&gt;The slow corrosion&lt;/h3&gt;
&lt;p&gt;There were some other problems I only discovered incrementally over the three
months.&lt;/p&gt;
&lt;p&gt;I found that AI made me procrastinate on key design
decisions&lt;sup class="sn-ref" id="sn-ref-procrastination"&gt;&lt;a href="#sn-procrastination"&gt;34&lt;/a&gt;&lt;/sup&gt;. Because refactoring was cheap, I could
always say &amp;ldquo;I&amp;rsquo;ll deal with this later.&amp;rdquo; And because AI could refactor at the
same industrial scale it generated code, the cost of deferring felt low. But it
wasn&amp;rsquo;t: deferring decisions corroded my ability to think clearly because the
codebase stayed confusing in the meantime. The vibe-coding month was the most
extreme version of this. Yes, I understood the problem, but if I had been more
disciplined about making hard design calls earlier, I could have converged on
the right architecture much faster.&lt;/p&gt;
&lt;p&gt;Tests created a similar false comfort&lt;sup class="sn-ref" id="sn-ref-tests-insufficient"&gt;&lt;a href="#sn-tests-insufficient"&gt;35&lt;/a&gt;&lt;/sup&gt;. Having 500+
tests felt reassuring, and AI made it easy to generate more. But neither humans
nor AI are creative enough to foresee every edge case you&amp;rsquo;ll hit in the future;
there are several times in the vibe-coding phase where I&amp;rsquo;d come up with a test
case and realise the design of some component was completely wrong and needed to
be totally reworked. This was a significant contributor to my lack of trust and
the decision to scrap everything and start from scratch.&lt;/p&gt;
&lt;p&gt;Basically, I learned that the &amp;ldquo;normal rules&amp;rdquo; of software still apply in the AI
age: if you don&amp;rsquo;t have a fundamental foundation (clear architecture,
well-defined boundaries) you&amp;rsquo;ll be left eternally chasing bugs as they appear.&lt;/p&gt;
&lt;h3 id="no-sense-of-time"&gt;No sense of time&lt;/h3&gt;
&lt;p&gt;Something I kept coming back to was how little AI understood about the passage
of time&lt;sup class="sn-ref" id="sn-ref-no-sense-of-time"&gt;&lt;a href="#sn-no-sense-of-time"&gt;36&lt;/a&gt;&lt;/sup&gt;. It sees a codebase in a certain state but
doesn&amp;rsquo;t &lt;em&gt;feel&lt;/em&gt; time the way humans do. I can tell you what it feels like to use
an API, how it evolved over months or years, why certain decisions were made and
later reversed.&lt;/p&gt;
&lt;p&gt;The natural problem from this lack of understanding is that you either make the
same mistakes you made in the past and have to relearn the lessons &lt;em&gt;or&lt;/em&gt; you fall
into new traps which were successfully avoided the first time, slowing you down
in the long run. In my opinion, this is a similar problem to why losing a
high-quality senior engineer hurts a team so much: they carry history and
context that doesn&amp;rsquo;t exist anywhere else and act as a guide for others around
them.&lt;/p&gt;
&lt;p&gt;In theory, you can try to preserve this context by keeping specs and docs up to
date. But there&amp;rsquo;s a reason we didn&amp;rsquo;t do this before AI: capturing implicit
design decisions exhaustively is incredibly expensive and time-consuming to
write down. AI can help draft these docs, but because there&amp;rsquo;s no way to
automatically verify that it accurately captured what matters, a human still has
to manually audit the result. And that&amp;rsquo;s still time-consuming.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the context pollution problem. You never know when a design note
about API A will echo in API B. Consistency is a huge part of what makes
codebases work, and for that you don&amp;rsquo;t just need context about what you&amp;rsquo;re
working on right now but also about other things which were designed in a
similar way. Deciding what&amp;rsquo;s relevant requires exactly the kind of judgement
that institutional knowledge provides in the first place.&lt;/p&gt;
&lt;h2 id="relativity"&gt;Relativity&lt;/h2&gt;
&lt;p&gt;Reflecting on the above, the pattern of when AI helped and when it hurt was
fairly consistent.&lt;/p&gt;
&lt;p&gt;When I was working on something I already understood deeply, AI was excellent. I
could review its output instantly, catch mistakes before they landed and move at
a pace I&amp;rsquo;d never have managed alone. The parser rule generation is the clearest
example&lt;sup class="sn-ref" id="sn-ref-parser-rules"&gt;&lt;a href="#sn-parser-rules"&gt;37&lt;/a&gt;&lt;/sup&gt;: I knew exactly what each rule should produce, so
I could review AI&amp;rsquo;s output within a minute or two and iterate fast.&lt;/p&gt;
&lt;p&gt;When I was working on something I could describe but didn&amp;rsquo;t yet know, AI was
good but required more care. Learning Wadler-Lindig for the formatter was like
this: I could articulate what I wanted, evaluate whether the output was heading
in the right direction, and learn from what AI explained. But I had to stay
engaged and couldn&amp;rsquo;t just accept what it gave me.&lt;/p&gt;
&lt;p&gt;When I was working on something where I didn&amp;rsquo;t even know what I wanted, AI was
somewhere between unhelpful and harmful. The architecture of the project was the
clearest case: I spent weeks in the early days following AI down dead ends,
exploring designs that felt productive in the moment but collapsed under
scrutiny. In hindsight, I have to wonder if it would have been faster just
thinking it through without AI in the loop at all.&lt;/p&gt;
&lt;p&gt;But expertise alone isn&amp;rsquo;t enough. Even when I understood a problem deeply, AI
still struggled if the task had no objectively checkable answer&lt;sup class="sn-ref" id="sn-ref-verifiability"&gt;&lt;a href="#sn-verifiability"&gt;38&lt;/a&gt;&lt;/sup&gt;. Implementation has a right answer, at least at a local level:
the code compiles, the tests pass, the output matches what you asked for. Design
doesn&amp;rsquo;t. We&amp;rsquo;re still arguing about OOP decades after it first took off.&lt;/p&gt;
&lt;p&gt;Concretely, I found that designing the public API of syntaqlite was where this
hit home the hardest. I spent several days in early March doing nothing but API
refactoring, manually fixing things any experienced engineer would have
instinctively avoided but AI made a total mess of. There&amp;rsquo;s no test or objective
metric for &amp;ldquo;is this API pleasant to use&amp;rdquo; and &amp;ldquo;will this API help users solve
the problems they have&amp;rdquo; and that&amp;rsquo;s exactly why the coding agents did &lt;em&gt;so badly&lt;/em&gt;
at it.&lt;/p&gt;
&lt;p&gt;This takes me back to the days I was obsessed with physics and, specifically,
relativity. The laws of physics look simple and Newtonian in any small local
area, but zoom out and spacetime curves in ways you can&amp;rsquo;t predict from the local
picture alone. Code is the same: at the level of a function or a class, there&amp;rsquo;s
usually a clear right answer, and AI is excellent there. But architecture is
what happens when all those local pieces interact, and you can&amp;rsquo;t get good global
behaviour by stitching together locally correct components.&lt;/p&gt;
&lt;p&gt;Knowing where you are on these axes at any given moment is, I think, the core
skill of working with AI effectively.&lt;/p&gt;
&lt;h2 id="wrap-up"&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;Eight years is a long time to carry a project in your head. Seeing these SQLite
tools actually exist and function after only three months of work is a massive
win, and I’m fully aware they wouldn&amp;rsquo;t be here without AI.&lt;/p&gt;
&lt;p&gt;But the process wasn&amp;rsquo;t the clean, linear success story people usually post. I
lost an entire month to vibe-coding. I fell into the trap of managing a codebase
I didn&amp;rsquo;t actually understand, and I paid for that with a total rewrite.&lt;/p&gt;
&lt;p&gt;The takeaway for me is simple: AI is an incredible force multiplier for
implementation, but it’s a dangerous substitute for design. It’s brilliant at
giving you the right answer to a specific technical question, but it has no
sense of history, taste, or how a human will actually feel using your API. If
you rely on it for the &amp;ldquo;soul&amp;rdquo; of your software, you’ll just end up hitting a
wall faster than you ever have before.&lt;/p&gt;
&lt;p&gt;What I&amp;rsquo;d like to see more of from others is exactly what I&amp;rsquo;ve tried to do here:
honest, detailed accounts of building real software with these tools; not
weekend toys or one-off scripts but the kind of software that has to survive
contact with users, bug reports, and your own changing mind.&lt;/p&gt;</content:encoded></item><item><title>syntaqlite: high-fidelity devtools that SQLite deserves</title><link>https://lalitm.com/post/syntaqlite/</link><pubDate>Tue, 17 Mar 2026 05:35:45 +0000</pubDate><guid>https://lalitm.com/post/syntaqlite/</guid><description>Most SQL tools treat SQLite as a “flavor” of a generic SQL parser. They approximate the language, which means they break on SQLite-exclusive features like virtual tables, miss syntax like UPSERT, and ignore the 22 compile-time flags that change the syntax SQLite accepts.
So I built syntaqlite: an open-source parser, formatter, validator, and LSP built directly on SQLite’s own Lemon-generated grammar. It sees SQL exactly how SQLite sees it, no matter which version of SQLite you’re using or which feature flags you compiled with.
It ships as a CLI, VS Code extension, Claude Code LSP plugin, and C/Rust libraries.
There’s also a web playground which you can try now: paste any SQLite SQL and see parsing, formatting, and validation live in the browser, no install needed. Full documentation is available here.
Here’s syntaqlite in action:
Formatting with the CLI
&gt; syntaqlite fmt -e "select u.name,u.email,count(e.id) as events from users u join events e on e.user_id=u.id where u.signed_up_at&gt;=date('now','-30 days') group by u.name,u.email having count(e.id)&gt;10 order by events desc" SELECT u.name, u.email, count(e.id) AS events FROM users AS u JOIN events AS e ON e.user_id = u.id WHERE u.signed_up_at &gt;= date('now', '-30 days') GROUP BY u.name, u.email HAVING count(e.id) &gt; 10 ORDER BY events DESC; Validation with the CLI</description><content:encoded>&lt;p&gt;Most SQL tools treat SQLite as a &amp;ldquo;flavor&amp;rdquo; of a generic SQL parser. They approximate the language, which means they break on SQLite-exclusive features like &lt;a href="https://www.sqlite.org/vtab.html"&gt;virtual tables&lt;/a&gt;, miss syntax like &lt;a href="https://www.sqlite.org/lang_upsert.html"&gt;UPSERT&lt;/a&gt;, and ignore the &lt;a href="https://www.sqlite.org/compile.html"&gt;22 compile-time flags&lt;/a&gt; that change the syntax SQLite accepts.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://github.com/lalitMaganti/syntaqlite"&gt;syntaqlite&lt;/a&gt;: an open-source parser, formatter, validator, and LSP built directly on SQLite&amp;rsquo;s own Lemon-generated grammar. It sees SQL exactly how SQLite sees it, no matter which version of SQLite you&amp;rsquo;re using or which feature flags you compiled with.&lt;/p&gt;
&lt;p&gt;It ships as a &lt;a href="https://docs.syntaqlite.com/main/getting-started/cli"&gt;CLI&lt;/a&gt;, &lt;a href="https://docs.syntaqlite.com/main/getting-started/vscode"&gt;VS Code extension&lt;/a&gt;, &lt;a href="https://docs.syntaqlite.com/main/getting-started/claude-code"&gt;Claude Code LSP plugin&lt;/a&gt;, and &lt;a href="https://docs.syntaqlite.com/main/getting-started/c-parser/"&gt;C&lt;/a&gt;/&lt;a href="https://docs.syntaqlite.com/main/getting-started/rust/"&gt;Rust&lt;/a&gt; libraries.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a &lt;strong&gt;&lt;a href="https://playground.syntaqlite.com/"&gt;web playground&lt;/a&gt;&lt;/strong&gt; which you can try now: paste any SQLite SQL and see parsing, formatting, and validation live in the browser, no install needed. &lt;a href="https://docs.syntaqlite.com/"&gt;Full documentation is available here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s syntaqlite in action:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Formatting with the CLI&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; syntaqlite fmt -e &lt;span class="s2"&gt;&amp;#34;select u.name,u.email,count(e.id) as events from users u join events e on e.user_id=u.id where u.signed_up_at&amp;gt;=date(&amp;#39;now&amp;#39;,&amp;#39;-30 days&amp;#39;) group by u.name,u.email having count(e.id)&amp;gt;10 order by events desc&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;events&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="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&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="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&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="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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="k"&gt;WHERE&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 class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signed_up_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;now&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-30 days&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="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&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 class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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 class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&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="k"&gt;HAVING&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 class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&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="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&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 class="n"&gt;events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Validation with the CLI&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; syntaqlite --sqlite-version 3.37.0 validate &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -e &lt;span class="s2"&gt;&amp;#34;SELECT json_extract(data, &amp;#39;&lt;/span&gt;$&lt;span class="s2"&gt;.name&amp;#39;) FROM events;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;error: unknown function &amp;#39;json_extract&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --&amp;gt; &amp;lt;expression&amp;gt;:1:8
&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;1 | SELECT json_extract(data, &amp;#39;$.name&amp;#39;) FROM events;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ^~~~~~~~~~~~
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;LSP support in VSCode&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/syntaqlite-vscode.png" alt="VS Code showing syntaqlite catching a misspelled column name"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;LSP in Claude Code&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/syntaqlite-cc.png" alt="Claude Code using syntaqlite to catch and fix column name errors"&gt;&lt;/p&gt;
&lt;h2 id="why-it-exists"&gt;Why it exists&lt;/h2&gt;
&lt;p&gt;I maintain &lt;a href="https://perfetto.dev/docs/analysis/perfetto-sql-syntax"&gt;PerfettoSQL&lt;/a&gt;, a dialect of SQLite SQL used for trace analysis in &lt;a href="https://perfetto.dev/"&gt;Perfetto&lt;/a&gt;. Across Google there are 200K+ lines of the language. At that scale, you hit &lt;em&gt;every&lt;/em&gt; corner of it. I tried every SQLite tool I could find and time after time I was disappointed: false positives on valid syntax, misparsed statements, formatters that silently corrupted the SQL by changing semantics. It always came down to a hand-written or generic grammar approximating SQLite rather than matching it to the letter.&lt;/p&gt;
&lt;p&gt;Moreover, SQLite&amp;rsquo;s SQL is not one fixed language. Apart from flags that change the syntax, there are another 12 flags that gate &lt;a href="https://www.sqlite.org/compile.html"&gt;built-in functions&lt;/a&gt;, as well as APIs for programs to &lt;a href="https://www.sqlite.org/c3ref/create_function.html"&gt;register custom functions&lt;/a&gt; and &lt;a href="https://www.sqlite.org/vtab.html"&gt;virtual tables&lt;/a&gt;. In Perfetto, we make &lt;em&gt;extensive&lt;/em&gt; use of both virtual tables and functions and most tools just don&amp;rsquo;t give us the flexibility to treat these as first-class extensions of the language.&lt;/p&gt;
&lt;p&gt;The SQLite language also constantly evolves: in the last several years, 3.25.0 added window functions, 3.35.0 added &lt;code&gt;RETURNING&lt;/code&gt;, 3.38.0 added built-in JSON functions to the amalgamation. And because SQLite is embedded, you can&amp;rsquo;t assume everyone is on the latest version. This is certainly the case for Perfetto: people link against different versions of SQLite and we need to make sure our standard library is resilient to that.&lt;/p&gt;
&lt;p&gt;Now with all of this said, I could have built all these tools just for PerfettoSQL and called it a day. But the more I thought about it, the more I felt that the difference between &amp;ldquo;good tooling for PerfettoSQL&amp;rdquo; and &amp;ldquo;good tooling for SQLite&amp;rdquo; is so small that it&amp;rsquo;s worth solving for the wider use case. SQLite is ubiquitous, yet it still lacks the quality tooling other languages have had for over a decade.&lt;/p&gt;
&lt;p&gt;Android is one case I think could especially benefit. SQLite is integral to almost every Android app not to mention the platform itself. But the ecosystem is also famously fragmented: if you&amp;rsquo;re an Android app developer, you &lt;strong&gt;need&lt;/strong&gt; to reason about older platform versions, which in turn means older versions of SQLite.&lt;/p&gt;
&lt;p&gt;As an example, Android 13 ships with SQLite 3.32 (&lt;a href="https://developer.android.com/reference/android/database/sqlite/package-summary"&gt;link&lt;/a&gt;). If you use the &lt;code&gt;RETURNING&lt;/code&gt; keyword and your users are on Android 13, it will fail at runtime even though it passes fine on your test device. Wouldn&amp;rsquo;t it be nice to statically verify this on your host instead of discovering it on a real device?&lt;/p&gt;
&lt;p&gt;No tool I found handled all of this: the precise SQLite grammar, version matching, compile flags, dialect extensions. And that&amp;rsquo;s exactly why syntaqlite exists.&lt;/p&gt;
&lt;h2 id="highlights"&gt;Highlights&lt;/h2&gt;
&lt;h3 id="validate-semantics-with-great-error-messages"&gt;Validate semantics with great error messages&lt;/h3&gt;
&lt;p&gt;Parsing alone is table stakes. But I also wanted to catch common mistakes like reaching for &lt;code&gt;user_name&lt;/code&gt; when the column is actually &lt;code&gt;username&lt;/code&gt;. For this reason, syntaqlite also resolves table, column, and function references, catching errors like misspelled column names or references to tables that don&amp;rsquo;t exist.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also tried to make the error messages &lt;em&gt;really&lt;/em&gt; good: at the quality level you would expect to see from modern tools in other languages like rustc or tsc. This means both accurate, line/column level indication with snippets of the error but also &amp;ldquo;did you mean&amp;rdquo; hints, pointing you in the right direction. The error messages should be &lt;em&gt;significantly&lt;/em&gt; better than SQLite&amp;rsquo;s and it&amp;rsquo;s something I plan on making even better with time.&lt;/p&gt;
&lt;p&gt;As for correctness, I&amp;rsquo;ve tested syntaqlite against ~396K statements from &lt;a href="https://sqlite.org/testing.html"&gt;SQLite&amp;rsquo;s upstream test suite&lt;/a&gt; with ~99.7% agreement on parse acceptance &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. Before we get to 1.0, I fully expect this to reach 100%.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a &lt;a href="https://docs.syntaqlite.com/main/reference/comparison/"&gt;detailed comparison&lt;/a&gt; against other tools covering parser accuracy, formatter correctness, validator quality, and performance. Of course the benchmarks and stress tests were written by me, so take it with a grain of salt. But hopefully the results speak for themselves.&lt;/p&gt;
&lt;h3 id="lsp-for-editors-and-coding-agents"&gt;LSP for editors and coding agents&lt;/h3&gt;
&lt;p&gt;syntaqlite ships a full language server: completions, hover, go-to-definition, find references, rename, diagnostics, and formatting. Unlike most languages, SQL queries depend on an external schema that isn&amp;rsquo;t in the source file. The LSP needs to know about that schema to give you useful diagnostics.&lt;/p&gt;
&lt;p&gt;The most common way to provide this is with a &lt;code&gt;syntaqlite.toml&lt;/code&gt; at the root of your project &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. Point it at your &lt;code&gt;CREATE TABLE&lt;/code&gt; definitions (which you can dump using &lt;code&gt;.schema&lt;/code&gt; on your SQLite database; see the &lt;a href="https://docs.syntaqlite.com/main/guides/project-setup/"&gt;project setup guide&lt;/a&gt; for details) and syntaqlite uses those for autocompletion, diagnostics, and find references.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;**/*.sql&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;db/schema.sql&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells syntaqlite: for every &lt;code&gt;.sql&lt;/code&gt; file in the project, validate against the definitions in &lt;code&gt;db/schema.sql&lt;/code&gt;. You can point at multiple schema files, and scope different schemas to different directories.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;syntaqlite.toml&lt;/code&gt; can be used to configure other preferences like indentation for formatting and the SQLite version as well. The idea is that you check this into your repo and the whole team gets the same experience.&lt;/p&gt;
&lt;h3 id="it-validates-sql-inside-other-languages-experimental"&gt;It validates SQL inside other languages (experimental)&lt;/h3&gt;
&lt;p&gt;SQL is unusual in that it often lives &lt;em&gt;inside&lt;/em&gt; other languages: string literals in Python, template literals in TypeScript, raw strings in C++, and so on.&lt;/p&gt;
&lt;p&gt;As an experimental feature (and with caveats!), syntaqlite can extract and validate SQL &lt;em&gt;inside&lt;/em&gt; other languages, even if you have string interpolation. For example, in a Python f-string:&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="c1"&gt;# app.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&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="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;SELECT name, ROUDN(score, 2) FROM users WHERE id = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;syntaqlite validate --experimental-lang python app.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warning: unknown function &amp;#39;ROUDN&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --&amp;gt; app.py:4:23
&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;4 | f&amp;#34;SELECT name, ROUDN(score, 2) FROM users WHERE id = {user_id}&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; = help: did you mean &amp;#39;round&amp;#39;?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It handles the &lt;code&gt;{user_id}&lt;/code&gt; interpolation hole and still catches the function typo with a did-you-mean suggestion.&lt;/p&gt;
&lt;h3 id="and-more"&gt;And more&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQLite version- and flag-aware.&lt;/strong&gt; Tell syntaqlite which SQLite version and compile-time flags your target has; the formatter, validator, and LSP all respect them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom dialect support.&lt;/strong&gt; Define grammar, formatting, and validation rules, compile to a shared library, load at runtime. I built this for PerfettoSQL, but it works for any engine that extends SQLite&amp;rsquo;s syntax. For example, a mobile analytics engine with custom aggregation functions or a local-first app framework with its own replication primitives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast.&lt;/strong&gt; The formatter processes 3,500 lines of SQL in ~5ms. Full benchmarks on the &lt;a href="https://docs.syntaqlite.com/main/reference/comparison/"&gt;comparison page&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;/h2&gt;
&lt;p&gt;I plan on writing a detailed followup on the technical side, but I did want to give a short summary here.&lt;/p&gt;
&lt;p&gt;Fundamentally syntaqlite is a C/Rust/C sandwich:&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;+-------------+---------------+--------------+-------+--------------------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| VS Code | Claude Code | Playground | CLI | C bindings |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Extension | / Agents | (WASM) | | (Go, Kotlin...) |
&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;| Language Server (LSP) |
&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;| Formatter · Validator · Analyzer (Rust) |
&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;| Parser · Tokenizer · AST Arena (C, from SQLite) |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+-------------------------------------------------------------------------+
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The bottom layer is C: SQLite&amp;rsquo;s Lemon-generated grammar and tokenizer, plus an arena storing a &lt;a href="https://jhwlr.io/super-flat-ast/"&gt;super flat AST&lt;/a&gt;. This layer has no Rust dependency and can be used entirely on its own, which matters if you&amp;rsquo;re building a database engine or other tool in C/C++ that just needs to parse SQLite SQL &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The middle layer is Rust. It reads the C arena directly with no copying or deserialization. The formatter generates a &lt;a href="https://lindig.github.io/papers/strictly-pretty-2000.pdf"&gt;Wadler-Lindig document&lt;/a&gt; using an AST-walking interpreter; the semantic analyzer walks the same AST, resolving references and producing diagnostics with byte-accurate source locations.&lt;/p&gt;
&lt;p&gt;The top layer is C again, completing the sandwich: the Rust tools are exported back through FFI so C/C++ projects can link syntaqlite as a plain C library. This also makes bindings for other languages possible: Go, Kotlin/Java, and Python are planned before 1.0. Of course, if you&amp;rsquo;re using Rust directly you can just use the Rust library, skipping the top C layer.&lt;/p&gt;
&lt;p&gt;The obvious question I&amp;rsquo;m expecting is &amp;ldquo;why not just write everything in Rust&amp;rdquo;? I wanted the parser to work &lt;em&gt;exactly&lt;/em&gt; as SQLite does, which meant &lt;em&gt;literally&lt;/em&gt; extracting SQLite&amp;rsquo;s own code and adapting it to my needs. And writing it in C with the same constraints as SQLite itself (no dependencies beyond the C standard library) means it can be linked into practically anything: database engines, C++ projects like Perfetto&amp;rsquo;s trace processor, or any environment where SQLite runs.&lt;/p&gt;
&lt;p&gt;On the other hand, I really did not want to write a formatter, semantic analyzer, and LSP in C. While I do like the &amp;ldquo;simplicity&amp;rdquo; of C, writing a full JSON-RPC server and protocol sounds like a nightmare.&lt;/p&gt;
&lt;h2 id="where-this-is-going"&gt;Where this is going&lt;/h2&gt;
&lt;p&gt;In my head, there&amp;rsquo;s a clear roadmap for the project side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100% upstream parity.&lt;/strong&gt; The ~0.3% gap against SQLite&amp;rsquo;s test suite is mostly from not fully tracking built-in tables and virtual tables. It just needs another round of &amp;ldquo;source code introspection&amp;rdquo; across all the SQLite versions and integrating those tables into syntaqlite&amp;rsquo;s builtin catalog.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LSP enhancements.&lt;/strong&gt; Code actions, document symbols, inlay hints for inferred column types. All of these basically making the editor experience even better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Robust embedded SQL.&lt;/strong&gt; The Python/TypeScript support is really just hacked together with regexes as a proof of concept. My goal is to extend this to use proper parser libraries &lt;em&gt;and&lt;/em&gt; add support for more languages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polish and performance.&lt;/strong&gt; The focus for 0.1 was getting the architecture right. There&amp;rsquo;s room to improve performance, API surface, and documentation across the whole stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for the blog, this project was the reason I&amp;rsquo;ve been completely silent over the last month, but now I&amp;rsquo;m excited to start writing a lot more again. For one, there&amp;rsquo;s the technical followup for the decisions I took and what I learned along the way.&lt;/p&gt;
&lt;p&gt;For another, I used coding agents (mainly Claude Code) extensively while building syntaqlite. There were plenty of moments where following them blindly would have sunk the project, but I can also say with certainty that syntaqlite would not exist without them. I want to dive into that tension: what worked, what didn&amp;rsquo;t, and the complicated feelings I came away with.&lt;/p&gt;
&lt;p&gt;So if you work with SQLite SQL at any scale, I&amp;rsquo;d appreciate you giving syntaqlite a try and telling me what breaks! SQLite is a project which famously plans to be &lt;a href="https://sqlite.org/lts.html"&gt;supported until 2050&lt;/a&gt;. My hope is that syntaqlite will be there as a companion every step of the way, making the experience of writing SQLite SQL that much nicer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/lalitMaganti/syntaqlite"&gt;syntaqlite on GitHub&lt;/a&gt; | &lt;a href="https://docs.syntaqlite.com/"&gt;Docs&lt;/a&gt; | &lt;a href="https://playground.syntaqlite.com/"&gt;Playground&lt;/a&gt;&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Per-statement results are in &lt;a href="https://github.com/lalitMaganti/syntaqlite/tree/main/tests/upstream_baselines"&gt;&lt;code&gt;tests/upstream_baselines/&lt;/code&gt;&lt;/a&gt;. The remaining 0.3% is largely down to not perfectly matching the builtin tables and then edge cases around where and when column aliases come into scope: it&amp;rsquo;s actually really complicated!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;You can also pass schema files directly via &lt;code&gt;--schema&lt;/code&gt; on the command line without needing a config file.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;The plan is to replace the PerfettoSQL parser in Perfetto&amp;rsquo;s trace processor with this C layer.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>The surprising attention on sprites, exe.dev, and shellbox</title><link>https://lalitm.com/trying-sprites-exedev-shellbox/</link><pubDate>Thu, 29 Jan 2026 00:34:22 +0000</pubDate><guid>https://lalitm.com/trying-sprites-exedev-shellbox/</guid><description>Over the last few weeks, three new products have announced themselves on Hacker News to great success, each making the frontpage:
Sprites (fly.io) with 508 votes and hit #7 exe.dev with 457 votes and hit #2 shellbox.dev with 316 votes and hit #4 All three have a very simple pitch: they will give you full access to Linux virtual machines to act as a sandboxed developer environment in the cloud.
At first glance, the attention these have gotten is very head-scratching. The idea of a Linux VPS has been around for more than 20 years at this point and VPS providers like DigitalOcean and Hetzner are widely known and used in the industry. From a technological standpoint, there’s very little revolutionary here.
Is it price then? Well no: the hardware specs are pretty awful for what you pay. For example, exe.dev gives you 2 CPUs and 8GB RAM shared across your whole account for $20/month. For comparison, at Hetzner for roughly that price, you can get a single VPS with 16 CPUs and 32GB RAM…</description><content:encoded>&lt;p&gt;Over the last few weeks, three new products have announced themselves on Hacker
News to great success, each making the frontpage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sprites.dev"&gt;Sprites&lt;/a&gt; (fly.io) with
&lt;a href="https://news.ycombinator.com/item?id=46557825"&gt;508 votes&lt;/a&gt; and hit
&lt;a href="https://hnrankings.info/46557825/"&gt;#7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://exe.dev"&gt;exe.dev&lt;/a&gt; with
&lt;a href="https://news.ycombinator.com/item?id=46397609"&gt;457 votes&lt;/a&gt; and hit
&lt;a href="https://hnrankings.info/46397609/"&gt;#2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://shellbox.dev"&gt;shellbox.dev&lt;/a&gt; with
&lt;a href="https://news.ycombinator.com/item?id=46638629"&gt;316 votes&lt;/a&gt; and hit
&lt;a href="https://hnrankings.info/46638629/"&gt;#4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three have a very simple pitch: they will give you full access to Linux
virtual machines to act as a sandboxed developer environment in the cloud.&lt;/p&gt;
&lt;p&gt;At first glance, the attention these have gotten is very head-scratching. The
idea of a Linux VPS has been around for more than 20 years at this point and VPS
providers like DigitalOcean and Hetzner are widely known and used in the
industry. From a technological standpoint, there&amp;rsquo;s very little revolutionary
here.&lt;/p&gt;
&lt;p&gt;Is it price then? Well no: the hardware specs are pretty awful for what you pay.
For example, exe.dev gives you 2 CPUs and 8GB RAM &lt;strong&gt;shared across your whole
account&lt;/strong&gt; for $20/month. For comparison, at Hetzner for roughly that price, you
can get a single VPS with 16 CPUs and 32GB RAM&amp;hellip;&lt;/p&gt;
&lt;p&gt;And moving a local dev environment to the cloud has real downsides: added
latency, needing a network connection, making graphical access painful. If all
you wanted was the ability to partition hardware resources, we&amp;rsquo;ve had VMs and
containers for more than a decade. You can use Docker on any decent Linux
machine and spin up containers to your heart&amp;rsquo;s content. And with
&lt;a href="https://github.com/apple/container"&gt;apple/container&lt;/a&gt;, it&amp;rsquo;s very easy to launch
a semi-sandboxed Linux environment on macOS too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So why is there so much excitement around fancy VPSes?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="why-the-criticisms-miss-the-point"&gt;Why the criticisms miss the point&lt;/h2&gt;
&lt;p&gt;The real reasons are two-fold: we&amp;rsquo;re in the age of AI &lt;em&gt;and&lt;/em&gt; the user experience
of these products.&lt;/p&gt;
&lt;p&gt;First, sandboxing. If you&amp;rsquo;re using LLM coding agents and you&amp;rsquo;re not looking to
YOLO things, sandboxing matters. Most prominently, Simon Willison has been
&lt;a href="https://simonwillison.net/2025/Nov/2/new-prompt-injection-papers/"&gt;beating&lt;/a&gt;
&lt;a href="https://simonwillison.net/2025/Aug/9/bay-area-ai/"&gt;the&lt;/a&gt;
&lt;a href="https://simonwillison.net/2025/Sep/23/why-ai-systems-might-never-be-secure/"&gt;drum&lt;/a&gt;
on the security risks of agents, with sandboxing being a good way to cut off the
&amp;ldquo;Access to Private Data&amp;rdquo; part of the
&lt;a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/"&gt;Lethal Trifecta&lt;/a&gt;.
Segregating to a fresh VM, while by no means perfect, significantly limits the
blast radius compared to using your personal dev machine if something goes
wrong.&lt;/p&gt;
&lt;p&gt;Second, there&amp;rsquo;s been an explosion in prototyping projects due to tools like
Claude Code and Codex. They make it so easy to spin things up that developers
want more &amp;ldquo;fresh starts&amp;rdquo; instead of piling features onto existing projects. And
if an experiment doesn&amp;rsquo;t work out, you just delete the machine and start over
without figuring out how to clean up.&lt;/p&gt;
&lt;p&gt;But why these new services over a traditional VPS? Speed. All three are
optimized to get you from zero to &amp;ldquo;machine running Claude Code&amp;rdquo; in 1-2 minutes,
versus 20-30 with traditional providers. They also handle the annoying bits of
sharing web services: reverse proxy, TLS termination, DNS are all available
instantaneously and with zero fuss; it&amp;rsquo;s night and day vs trying to roll your
own.&lt;/p&gt;
&lt;p&gt;In some sense, this is developer environments catching up to what has happened
to production systems over the last decade. We&amp;rsquo;ve had infrastructure-as-code and
&lt;a href="http://cloudscaling.com/blog/cloud-computing/the-history-of-pets-vs-cattle/"&gt;&amp;ldquo;cattle not pets&amp;rdquo;&lt;/a&gt;
thinking in deployment since containers and Kubernetes took the world by storm.
GitHub Codespaces was a step in this direction for dev environments. Put another
way, this new crop of services are an evolution: they let you go beyond just the
&amp;ldquo;development&amp;rdquo; part and to the full end-to-end stack.&lt;/p&gt;
&lt;h2 id="speedrunning-the-services"&gt;Speedrunning the services&lt;/h2&gt;
&lt;p&gt;But enough about the abstract philosophy about why these services are having a
moment: I decided to try out all of these services to see for myself whether
they were actually useful and which one I liked the most.&lt;/p&gt;
&lt;p&gt;My motivating use case for this: I was looking to understand how
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy"&gt;COOP&lt;/a&gt;
and
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy"&gt;COEP&lt;/a&gt;
work in all modern browsers for building a new feature in Perfetto &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. This
was a perfect vibe-coding task: it was fully throw-away code for testing
something that would never directly end up in any prod project. And it was a web
based thing where I &lt;em&gt;needed&lt;/em&gt; https and certs etc. Literally could not design a
better use case for this.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a quick rundown of the services:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Pricing&lt;/th&gt;
&lt;th&gt;Specs&lt;/th&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;exe.dev&lt;/td&gt;
&lt;td&gt;$20/mo for up-to 25 VMs&lt;/td&gt;
&lt;td&gt;2 CPU, 8GB RAM, 25GB disk &lt;em&gt;shared across all 25 VMs&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;SSH + web shell + built-in AI agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sprites&lt;/td&gt;
&lt;td&gt;Usage-based ($0.07/CPU-hr, need to pay for RAM + storage also)&lt;/td&gt;
&lt;td&gt;Bursts to 8 CPU, 16GB RAM&lt;/td&gt;
&lt;td&gt;CLI tool for setup + SSH afterwards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shellbox&lt;/td&gt;
&lt;td&gt;Usage-based ($0.05/hr running, $0.005/hr stopped)&lt;/td&gt;
&lt;td&gt;2 CPU, 4GB RAM, 50GB SSD&lt;/td&gt;
&lt;td&gt;Pure SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Immediately, it&amp;rsquo;s clear that there are three different &amp;ldquo;dimensions&amp;rdquo; these
services are competing on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pricing model&lt;/strong&gt;: flat fee vs usage based&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maximum possible specs&lt;/strong&gt;: laughably poor vs somewhat reasonable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interface&lt;/strong&gt;: ssh vs custom CLI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s amazing how three tools in such a short space ended up in such different
places! But which one actually worked best for me?&lt;/p&gt;
&lt;h2 id="my-experiences"&gt;My experiences&lt;/h2&gt;
&lt;h3 id="shellbox"&gt;shellbox&lt;/h3&gt;
&lt;p&gt;The UX to interface with shellbox is very cool: everything (billing, creating
VMs, deleting VMs) is &lt;em&gt;all&lt;/em&gt; done via ssh! You ssh into a server and they use
your public key to figure out your account. You then add &amp;ldquo;credits&amp;rdquo; to your
account that you can then use. Having not seen any other service like this
before, I was already very impressed by the &amp;ldquo;cool factor&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/trying-sprites-exedev-shellbox/shellbox.png" alt="shellbox.png"&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately, that quickly faded as I found I couldn&amp;rsquo;t even try it without
paying first some amount of money. And the payment UX was rough: no indication
of how long I&amp;rsquo;d have to wait, and for a while their SSH status command kept
telling me I had no credits; they finally showed up 15 minutes later.&lt;/p&gt;
&lt;p&gt;Once in, I found Ubuntu 18.04: not exactly a &amp;ldquo;modern&amp;rdquo; environment (it&amp;rsquo;s actually
been EOL since 2023&amp;hellip;). And &lt;strong&gt;nothing&lt;/strong&gt; was pre-installed; I even had to
install Python myself! Certainly not ideal if the goal is &amp;ldquo;get to a usable dev
environment fast&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;But the real dealbreaker for me is that, in their world, they consider a VM
&amp;ldquo;stopped&amp;rdquo; &lt;em&gt;the moment you disconnect SSH&lt;/em&gt;. To my understanding, this means you
can&amp;rsquo;t run a webserver in the background even for a day or two to test something.
Instead you need to stay connected constantly, which for the sort of thing I was
trying to build, was very non-ideal.&lt;/p&gt;
&lt;h3 id="exedev"&gt;exe.dev&lt;/h3&gt;
&lt;p&gt;Firstly, I want to make clear that the homepage for exe.dev sucks, especially on
mobile: it doesn&amp;rsquo;t tell you anything about what you&amp;rsquo;re getting and just tells
you &amp;ldquo;ssh exe.dev&amp;rdquo;; I wasn&amp;rsquo;t about to SSH into a random server without
understanding why. You only understand what you&amp;rsquo;re getting by clicking &amp;ldquo;About&amp;rdquo;
at the bottom which takes you to their docs (which are actually decent!).&lt;/p&gt;
&lt;p&gt;Once you SSH in, though? The UX is fantastic. It recognized my public key,
looked it up against my GitHub account automatically &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; and pulled my email
and associated it to my account. On the VM, there was Ubuntu 24.04, Claude
pre-installed, recent Python. They clearly understand their user base and what
they&amp;rsquo;re trying to accomplish.&lt;/p&gt;
&lt;p&gt;And beyond the command line, there&amp;rsquo;s a web shell for when SSH isn&amp;rsquo;t convenient.
And they have a custom, web-based coding agent called
&lt;a href="https://github.com/boldsoftware/shelley"&gt;Shelley&lt;/a&gt; which works surprisingly well
(honestly, in many ways, nicer than Claude Code on web). I especially like their
ability to look at the web service running and take screenshots to understand
the state of the page; very cool!&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/trying-sprites-exedev-shellbox/shelly.png" alt="shelly.png"&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s night and day between the homepage and the actual product. The only
annoyance I ran into is that you can only expose a single public port, which I
needed in my particular case (but I can understand this is a somewhat niche
ask). Beyond that, I had a very nice experience.&lt;/p&gt;
&lt;h3 id="sprites"&gt;sprites&lt;/h3&gt;
&lt;p&gt;Having tried shellbox and exe.dev, the need to generate a token and install
their CLI for sprites made me feel like I&amp;rsquo;d left my magic SSH kingdom and was
back in the real world.&lt;/p&gt;
&lt;p&gt;Once installed, the process to create a VM and ssh in was very smooth. Claude
comes pre-installed, and interestingly, they configure it with
&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; by default! You&amp;rsquo;re given Ubuntu 25.04, so
relatively bleeding edge. Beyond that, it all felt very standard, you&amp;rsquo;re just
getting a VPS fast and fuss-free but with no other &amp;ldquo;magic&amp;rdquo; like exe.dev had.&lt;/p&gt;
&lt;p&gt;In general though, the vibe I got throughout my experience of interacting with
the tools and their web dashboard was that it felt very corporate&amp;quot; and designed
for companies with teams of developers.&lt;/p&gt;
&lt;p&gt;I think this feeling is best summed up by how their home page looks when logged
in:&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/trying-sprites-exedev-shellbox/sprites.png" alt="sprites.png"&gt;&lt;/p&gt;
&lt;p&gt;Not exactly flattering to see myself labelled as an organization!&lt;/p&gt;
&lt;h2 id="overall-assessment"&gt;Overall assessment&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;exe.dev&lt;/strong&gt; is my pick for solo developers. The flat $20/month cap feels
psychologically nice—you know your max spend. The 2 vCPUs might feel limiting,
but for Claude Code sessions where you&amp;rsquo;re not doing anything CPU-intensive, it&amp;rsquo;s
probably fine. And frankly, the UX is so good it makes up for a lot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;shellbox&lt;/strong&gt; has the simplest pricing model, but the user experience left me
underwhelmed. The environment is ancient, the payment flow was confusing, and
the SSH-disconnect behavior kills too many use cases. It feels a bit hacked
together.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sprites&lt;/strong&gt; is the most interesting technically. I love
&lt;a href="https://fly.io/blog/design-and-implementation/"&gt;their design blog post&lt;/a&gt;: the
ability to burst to 8 CPUs and 16GB RAM while only paying for what you use is
very clever and I can imagine it would be great if you were only planning light
usage. But the UX feels corporate, not magical. If I were running a company, I&amp;rsquo;m
reasonably certain sprites would be the way to go. But for solo experimentation,
exe.dev wins.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;You might find these services incredible or baffling depending on your stance on
AI and how much you&amp;rsquo;re willing to pay for convenience.&lt;/p&gt;
&lt;p&gt;These services aren&amp;rsquo;t targeting customers who care about &amp;ldquo;value for money&amp;rdquo; or
raw specs. They&amp;rsquo;re targeting developers who want frictionless access to
machines, without setting up VMs, wrestling with containers, or configuring
certificates to show off prototypes.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been won over by the neat UX and &amp;ldquo;joyous&amp;rdquo; feeling of exe.dev. I&amp;rsquo;ll probably
pay for it in months where I expect to do a lot of prototyping. Having tried the
alternatives (Docker, local VMs, traditional VPSes) it&amp;rsquo;s nice to have something
that just works.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://github.com/google/perfetto/discussions/3227"&gt;https://github.com/google/perfetto/discussions/3227&lt;/a&gt; if you&amp;rsquo;re interested!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;How, you might ask? Well if you didn&amp;rsquo;t know GitHub by default makes the all
SSH public keys &lt;em&gt;publicly accessible&lt;/em&gt; both via their API but also just by going
to github.com/username.keys. For example, for my account:
&lt;a href="https://github.com/lalitmaganti.keys"&gt;https://github.com/lalitmaganti.keys&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>Why Senior Engineers Let Bad Projects Fail</title><link>https://lalitm.com/post/why-senior-engineers-let-bad-projects-fail/</link><pubDate>Tue, 13 Jan 2026 10:48:00 +0000</pubDate><guid>https://lalitm.com/post/why-senior-engineers-let-bad-projects-fail/</guid><description>When I was a junior engineer, my manager would occasionally confide his frustrations to me in our weekly 1:1s. He would point out a project another team was working on and say, “I don’t believe that project will go anywhere, they’re solving the wrong problem.” I used to wonder, “But you are very senior, why don’t you just go and speak to them about your concerns?” It felt like a waste of his influence to not say anything.
So it’s quite ironic that I found myself last week explaining to a mentee why I thought a sister team’s project would have to pivot because they’d made a poor early design choice. And he rightfully asked me the same question I had years ago: “why don’t you just tell them your opinion?” It’s been on my mind ever since because I realized I’d changed my stance on it a lot over the years.</description><content:encoded>&lt;p&gt;When I was a junior engineer, my manager would occasionally confide his frustrations to me in our weekly 1:1s. He would point out a project another team was working on and say, &amp;ldquo;I don&amp;rsquo;t believe that project will go anywhere, they&amp;rsquo;re solving the wrong problem.&amp;rdquo; I used to wonder, &amp;ldquo;But you are very senior, why don&amp;rsquo;t you just go and speak to them about your concerns?&amp;rdquo; It felt like a waste of his influence to not say anything.&lt;/p&gt;
&lt;p&gt;So it&amp;rsquo;s quite ironic that I found myself last week explaining to a mentee why I thought a sister team&amp;rsquo;s project would have to pivot because they&amp;rsquo;d made a poor early design choice. And he rightfully asked me the same question I had years ago: &amp;ldquo;why don&amp;rsquo;t you just tell them your opinion?&amp;rdquo; It’s been on my mind ever since because I realized I’d changed my stance on it a lot over the years.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The answer is that being right and being effective are different.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In large companies, speaking up about what you see as a &amp;ldquo;bad project&amp;rdquo; is a good thing. But only in moderation. Sometimes the mark of seniority is realizing that arguing with people who won’t listen isn’t worth it; it’s better to save your counsel.&lt;/p&gt;
&lt;h2 id="bad-projects"&gt;Bad projects&lt;/h2&gt;
&lt;p&gt;What I mean by a &amp;ldquo;bad project&amp;rdquo; is many things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UX&lt;/strong&gt;: making product complicated, solving a problem which doesn&amp;rsquo;t exist, breaking existing workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical&lt;/strong&gt;: overcomplicated design, wrong library, poor performing architecture&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Political&lt;/strong&gt;: chasing hype cycles, exists primarily to justify a promotion&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s important to point out that for much of the lifecycle of a project, whether it&amp;rsquo;s &amp;ldquo;bad&amp;rdquo; is &lt;em&gt;highly&lt;/em&gt; subjective. Software engineering is largely a game of tradeoffs and making decisions which are not perfect but the best possible with the information available. There often can be disagreements on whether correct choices are made and it only becomes obvious much later on, potentially years after a project has shipped.&lt;/p&gt;
&lt;p&gt;But as you become more senior, you&amp;rsquo;ll start to have &amp;ldquo;taste&amp;rdquo; when it comes to software projects and that will cause you to look at some fraction of the software projects and feel &amp;ldquo;this doesn&amp;rsquo;t make sense&amp;rdquo;. And this gut feeling is the sign to me of a &amp;ldquo;bad project&amp;rdquo;, one which you can see in advance of when it&amp;rsquo;s obvious to everyone.&lt;/p&gt;
&lt;p&gt;Drawing on my personal experience, the most memorable example was a few years ago at Google . There was a high-profile announcement internally of a &amp;ldquo;game changer&amp;rdquo; project that sat right at the intersection of two extremely large organizations. It was technically amazing and elegant, and full of clever ideas for really hard problems.&lt;/p&gt;
&lt;p&gt;But I distinctly remember sitting in the room for the announcement, turning to my lead and whispering, &amp;ldquo;This project has no chance of succeeding, right?&amp;rdquo; He turned to me and just said, &amp;ldquo;Yup.&amp;rdquo; We both realized the problem immediately. The project was entirely based on a platform team asking a flagship product team to give up control of their core user flow: technically the right move, but no lead or PM would ever cede ownership of something that central to another team. Politically, this project was a total fantasy.&lt;/p&gt;
&lt;p&gt;The project kept quietly chugging away in the background for almost two years. Every time it got close to launch, it would get pushed back as &amp;ldquo;not ready yet.&amp;rdquo; Over time, we heard less and less about it until, eventually, the inevitable &amp;ldquo;strategic pivot&amp;rdquo; email appeared in my inbox. Resources were reallocated and the code was deleted. We were told the company &amp;ldquo;learned a lot from the effort,&amp;rdquo; but to me it felt like it was doomed from the beginning. Politics and solving the correct problem matter just as much as technical beauty.&lt;/p&gt;
&lt;h2 id="why-you-cannot-stop-them-all"&gt;Why you cannot stop them all&lt;/h2&gt;
&lt;p&gt;When I started noticing &amp;ldquo;bad projects&amp;rdquo; and I felt that I had some expertise to share, the temptation for me was to start calling them out. Reach out to the team doing it, tell them &amp;ldquo;this doesn&amp;rsquo;t make sense&amp;rdquo; and explain to them why. Use facts and logic to persuade.&lt;/p&gt;
&lt;p&gt;And I did do this. But only for a very short time before I realized that there are a lot of costs to doing this that I just wasn&amp;rsquo;t thinking about.&lt;/p&gt;
&lt;p&gt;Firstly, software companies have an inherent bias for action. They value speed and shipping highly. Concerns, by definition, &lt;em&gt;slow things down&lt;/em&gt; and mean people have to look at things which they hadn&amp;rsquo;t budgeted for. And so unless your concern is big enough to overcome the &amp;ldquo;push for landing&amp;rdquo;, there&amp;rsquo;s little chance for any meaningful change to come from you saying something. In fact, it&amp;rsquo;s very likely that you&amp;rsquo;ll be largely ignored.&lt;/p&gt;
&lt;p&gt;Related to this, even if the team &lt;em&gt;does&lt;/em&gt; take your concern seriously, you have to be careful not to do it too often. Once or twice, you might be seen as someone who is upholding &amp;ldquo;quality&amp;rdquo;. But do it too often and you quickly move to being seen as a &amp;ldquo;negative person&amp;rdquo;, someone who is constantly a problem maker, not a problem &amp;ldquo;fixer&amp;rdquo;. You rarely get credit for the disasters you prevented. Because nothing happened, people forget about it quickly.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the problem that every time you push back, you are potentially harming someone’s promotion packet or a VP’s &amp;ldquo;pet project.&amp;rdquo; You are at risk of burning bridges and creating &amp;ldquo;enemies&amp;rdquo;, at least of a sort. Having a few people who disagree in a big company with you is the cost of doing business, but if you have too many, it starts affecting your main work too.&lt;/p&gt;
&lt;p&gt;Finally, there is also the psychological impact. There is one of you and hundreds of engineers working in spaces that your expertise might help with. Your attention is finite, but the capacity for a large company to generate bad ideas is infinite. Speaking from experience, getting too involved in stopping these quickly can make you &lt;em&gt;very&lt;/em&gt; cynical about the state of the world. And this is really not a good place to be.&lt;/p&gt;
&lt;h2 id="manage-influence-like-a-bank-account"&gt;Manage influence like a bank account&lt;/h2&gt;
&lt;p&gt;So if you cannot stop all the bad projects, what do you do? You get strategic. Instead of trying to fix everything, view your influence as a bank account. You have a certain amount of &amp;ldquo;influence&amp;rdquo; coming in every month as you do your job, help people, ship successful projects, and generally remain low friction.&lt;/p&gt;
&lt;p&gt;Then, when it matters, you should be ready to make &amp;ldquo;withdrawals.&amp;rdquo; Every time you block something or raise concerns, no matter how small, you are writing a check against your balance. But not all checks are the same size:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The $5 Check:&lt;/strong&gt; A nitpick on a code review. Cheap, daily expense.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The $500 Check:&lt;/strong&gt; Challenging an architectural decision or pushing back on a timeline. Requires some savings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The $50,000 Check:&lt;/strong&gt; Trying to kill a VP&amp;rsquo;s pet project. This is a massive spend. You might only afford this once every few years.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem comes if you spend $5 on every minor inefficiency you see. If you are constantly saying &amp;ldquo;no&amp;rdquo; to small things, your account will be empty when you need to write the big check to stop a true disaster.&lt;/p&gt;
&lt;p&gt;If you &amp;ldquo;go overdrawn,&amp;rdquo; you enter political bankruptcy. People stop inviting you to meetings, they stop asking for your opinion, they essentially start working around you. Once you are bankrupt, your influence drops to zero and you not only harm your ability to influence things but also start hurting your own ability to get things done.&lt;/p&gt;
&lt;h2 id="when-to-spend-influence"&gt;When to spend influence&lt;/h2&gt;
&lt;p&gt;Given that we&amp;rsquo;ve now accepted that we cannot weigh in on everything, we need to figure out when it &lt;em&gt;does&lt;/em&gt; make sense to do so.&lt;/p&gt;
&lt;p&gt;The most important thing to do first is to be humble and evaluate whether you actually have the expertise to make a judgment. Seniority often brings opinions, but those are not always informed opinions. For example, while I have some frontend experience, I do not feel qualified to give deep advice on it because my knowledge is &amp;ldquo;enough to get by&amp;rdquo; rather than deep expertise that comes from long term ownership. It is easy to lose sight of the fact that high-quality judgments require informed opinions. If you find yourself in this position, see yourself as an opinionated observer and stop there.&lt;/p&gt;
&lt;p&gt;You must also internalize the fact that just because you say something does not make it the truth. You are raising awareness of a point of view, not issuing a decree. So if some team doesn&amp;rsquo;t listen to your concerns and decides to go ahead with what they were doing anyway, then you have to accept that and move on: at the end of the day, you&amp;rsquo;re an engineer, not a CEO with authority over them!&lt;/p&gt;
&lt;p&gt;Given these points, I use three main factors to decide when to speak up:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;How close is the project to my team?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If it goes wrong, how much impact will it have on my team?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If it goes wrong, how big will the problem be for the company?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Proximity.&lt;/strong&gt; If a project is close to you, the &amp;ldquo;price tag&amp;rdquo; of saying something is lower. If it is within your own team, the cost is near zero because you have high trust and a quick conversation often solves it. If it is in your broader organization, the price goes up; you have to spend social capital and potentially stake your reputation. If it is outside your org? The cost is often prohibitive. You have zero leverage, different reporting chains, and stopping it would require a massive withdrawal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Team Impact.&lt;/strong&gt; Sometimes another org does something that deeply affects your work. For example, because &lt;a href="https://perfetto.dev/"&gt;Perfetto&lt;/a&gt; (the performance tool I work on) has users throughout Google, sometimes a team will ask us to sign off on a very complex integration. This is a classic risk: if things go right, they get the credit, but if things go wrong, your leadership might expect &lt;em&gt;you&lt;/em&gt; to help solve a problem you didn&amp;rsquo;t create. In these cases, the payoff of speaking up is high because you are protecting your team.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Company Scale.&lt;/strong&gt; Finally, consider the blast radius. Some projects are self-contained; if they fail, they only take themselves down. Others are so intertwined with core systems that their failure causes widespread damage or creates technical debt that persists for years. These can be deadly to the long-term health of a project.&lt;/p&gt;
&lt;h2 id="how-to-act-with-bad-projects"&gt;How to act with bad projects&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s also not just about when you put your opinions forward but how you do it. There&amp;rsquo;s a very wide range of actions you can take depending on what you&amp;rsquo;re facing.&lt;/p&gt;
&lt;h3 id="when-you-intervene"&gt;When you intervene&lt;/h3&gt;
&lt;p&gt;The nuclear option is to directly say &amp;ldquo;we should not do this&amp;rdquo; and try to shut the project down. This almost always requires escalation to your leads and the leads of the owning team, requiring great conviction in both the fact that you&amp;rsquo;re right and that this project will be actively harmful. But on some occasions, this is the right thing to do, especially if the cost of not saying something can be existential to your project or team.&lt;/p&gt;
&lt;p&gt;A slightly softer but still quite risky variant of this is, instead of doing a direct escalation, you raise concerns in directly with the team. Usually this is done with a meeting with the team or a strongly worded &amp;ldquo;concern&amp;rdquo; or &amp;ldquo;rebuttal&amp;rdquo; doc. The goal is to speak in strong enough terms that the team themselves conclude that this the project might not be a good idea.&lt;/p&gt;
&lt;p&gt;Then there are the smaller interventions, nudging things in the right direction. These are perfect for when a team is about to do something that makes sense from a high level but they are going about this the wrong way. I see this often with Perfetto: a team sends a design doc proposing a complex use of Perfetto that I know will cause them pain later. I sit down with them, understand their actual problem, and guide them to a better solution. It costs an hour but saves them months. If you do it right, you can even be seen as a helper rather than a hindrance, even if you do slow down the team.&lt;/p&gt;
&lt;h3 id="when-you-dont"&gt;When you don&amp;rsquo;t&lt;/h3&gt;
&lt;p&gt;Sometimes you conclude that the ROI just isn&amp;rsquo;t there to do anything direct: the political momentum is too strong, or the issue is too small to justify spending any influence. At this point, what you do depends on how much your team is involved.&lt;/p&gt;
&lt;p&gt;If it overlaps with your team&amp;rsquo;s work heavily then it might be best to make some subtle contingency plans: reducing your dependency on it or building abstractions to cope if it goes away. There is also a long game trick here. Even a bad project usually has an &amp;ldquo;essence&amp;rdquo; of a good idea, a specific problem it was trying to solve or an insight it was based on. If it fits with your job, it&amp;rsquo;s often a good idea to take that essence and see if you can naturally incorporate a better version of that specific solution into your own project. That way, if the bad project stalls or gets canceled, you can be proactive instead of reactive to the fallout.&lt;/p&gt;
&lt;p&gt;Alternatively, if you&amp;rsquo;re not involved, it&amp;rsquo;s easy: just stay out of the picture. Vent to friendly colleagues in private, commiserate, but in public, live with the reality.&lt;/p&gt;
&lt;h3 id="managing-your-team-through-it"&gt;Managing your team through it&lt;/h3&gt;
&lt;p&gt;Finally, you must manage your own team through the process. If you can see the flaws in a project, other senior engineers probably see them too. Don&amp;rsquo;t try to gaslight them or &amp;ldquo;walk the company line&amp;rdquo; by pretending a bad project is actually good. It destroys trust.&lt;/p&gt;
&lt;p&gt;Instead, be honest about the facts on the ground without going into unnecessary political details. Tell them that you will do the best you can under these constraints.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So what did I tell my mentee? &amp;ldquo;I&amp;rsquo;ve learned that being right and being effective are different things. I could go tell them my concerns. They probably wouldn&amp;rsquo;t listen. I&amp;rsquo;d burn some goodwill. And in six months, nobody will remember that I called it, they&amp;rsquo;ll just remember I was the guy who tried to block their work&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re earlier in your career, you want to believe that good ideas win on merit, that if you just explain clearly enough, people will see reason. It took me quite some time to accept that big companies don&amp;rsquo;t work that way.&lt;/p&gt;
&lt;p&gt;But this doesn&amp;rsquo;t mean you stop caring. It means you get strategic about when to spend your credibility. Pick the battles where you can actually change the outcome, where your team will be hurt if you stay silent, where the cost of being wrong is low but the cost of the project failing is high.&lt;/p&gt;
&lt;p&gt;And for everything else? You vent to colleagues, you make quiet contingency plans, and you watch. Sometimes you learn something. Sometimes you&amp;rsquo;re wrong and the project actually works. And sometimes you get to feel that grim satisfaction of predicting exactly how things would fall apart.&lt;/p&gt;
&lt;p&gt;None of this is as satisfying as fixing everything. But it works and keeps me sane.&lt;/p&gt;</content:encoded></item><item><title>One Number I Trust: Plain-Text Accounting for a Multi-Currency Household</title><link>https://lalitm.com/post/one-number-i-trust/</link><pubDate>Thu, 01 Jan 2026 10:09:00 +0100</pubDate><guid>https://lalitm.com/post/one-number-i-trust/</guid><description>Two people. Eighteen accounts spanning checking, savings, credit cards, investments. Three currencies. Twenty minutes of work every week.
One net worth number I actually trust.
The payoff: A single, trustworthy net worth number growing over time.
No app did exactly what I needed, so I built my own personal finance system using plain-text accounting principles and a powerful Python library called Beancount. This post shows you how I handle imports, investments, multi-currency, and a two-person view.
How I got here It all started during the 2021 tax season. I had blocked out an entire weekend and was juggling statements, trying to compute capital gains, stressing about getting the numbers mixed up. “This is chaos”, I thought. “There must be a way to simplify this with automation”. Being a software engineer, I did what felt natural and hacked together a bunch of scripts on top of a database.</description><content:encoded>&lt;p&gt;Two people. Eighteen accounts spanning checking, savings, credit cards, investments. Three currencies. Twenty minutes of work every week.&lt;/p&gt;
&lt;p&gt;One net worth number I actually trust.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/01-payoff-net-worth.png" alt="Net Worth Chart growing over 2+ years" title="One net worth number to trust"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The payoff: A single, trustworthy net worth number growing over time.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;No app did exactly what I needed, so I built my own personal finance system using &lt;strong&gt;plain-text accounting&lt;/strong&gt; principles and a powerful Python library called &lt;strong&gt;Beancount&lt;/strong&gt;. This post shows you how I handle imports, investments, multi-currency, and a two-person view.&lt;/p&gt;
&lt;h2 id="how-i-got-here"&gt;How I got here&lt;/h2&gt;
&lt;p&gt;It all started during the 2021 tax season. I had blocked out an entire weekend and was juggling statements, trying to compute capital gains, stressing about getting the numbers mixed up. &amp;ldquo;This is chaos&amp;rdquo;, I thought. &amp;ldquo;There must be a way to simplify this with automation&amp;rdquo;. Being a software engineer, I did what felt natural and hacked together a bunch of scripts on top of a database.&lt;/p&gt;
&lt;p&gt;Though it worked and I kept using it day-to-day, by the next tax season the cracks became obvious. The code was hard to debug, random transactions went missing, and worst of all, the balances the scripts computed didn&amp;rsquo;t match the balances on my statements. I tried to fix it but the more I tried, the more I felt lost about what the system was really doing. Eventually I just gave up.&lt;/p&gt;
&lt;p&gt;Why did I fail so spectacularly? My entire approach was flawed from the start! I&amp;rsquo;d ignored centuries of accounting wisdom and repeated fundamental mistakes humanity solved long ago. So I learned from my mistakes and did the research. And over time I incrementally discovered &lt;strong&gt;double-entry bookkeeping&lt;/strong&gt;, &lt;strong&gt;plain-text accounting&lt;/strong&gt; and &lt;strong&gt;Beancount&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Fast forward to today, and I have a flexible, powerful, and private system, fully customized to how my brain works. Most transactions import automatically from PDF statements (counterintuitively, it&amp;rsquo;s often more reliable than CSV!). Tax time is a simple matter of checking always-fresh reports and copying numbers over. The weekly ritual is simple: download statements, categorize transactions in a web UI, run a bunch of scripts to regenerate, commit (I walk through this in more detail later).&lt;/p&gt;
&lt;p&gt;However, I want to be realistic: building a system like this takes time and effort.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; You will need to learn some basic accounting concepts, be comfortable with Python, and consistently spend time every week keeping things up-to-date. If your finances are simple or you just want day-to-day budgeting, this is almost certainly overkill. Apps like YNAB or even the humble spreadsheet work great.&lt;/p&gt;
&lt;p&gt;But if you want uncompromising control over how you look at your finances, read on.&lt;/p&gt;
&lt;h2 id="chapter-1-the-concepts"&gt;Chapter 1: The Concepts&lt;/h2&gt;
&lt;h4 id="double-entry-bookkeeping"&gt;Double-entry bookkeeping&lt;/h4&gt;
&lt;p&gt;Suppose on a Saturday, I transfer money from my checking account to a savings account. The money leaves on the same day but doesn&amp;rsquo;t show up on the other side until Monday. So where was it for those two days?&lt;/p&gt;
&lt;p&gt;In a &amp;ldquo;normal&amp;rdquo;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; personal finance system, the answer would be that it was just gone. That is, for those two days, there would be a drop in the total money in two accounts. But this is weird because in reality my &amp;ldquo;net worth&amp;rdquo; did not change, yet there&amp;rsquo;s no good way to represent this.&lt;/p&gt;
&lt;p&gt;Or suppose I pay $90 for a dinner for me and two friends. They pay me back a week later. Again, in this case the money is &amp;ldquo;gone&amp;rdquo; for that week. And even worse, the full $90 would be categorized as a &amp;ldquo;restaurant expense&amp;rdquo; while each $30 my friends paid would be &amp;ldquo;income&amp;rdquo;. But this is wrong. My expense is just $30 and the money they give me should be matched against the $60 they owe me.&lt;/p&gt;
&lt;p&gt;Both of these are fundamental problems with how so-called &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Single-entry_bookkeeping"&gt;single-entry bookkeeping&lt;/a&gt;&amp;rdquo; works: each account&amp;rsquo;s transactions and balance are tracked individually but without the context of the &amp;ldquo;whole&amp;rdquo;. In the case of the transfer, because we&amp;rsquo;re looking at each account in isolation, we lose the fact that even though the money has left one account, it&amp;rsquo;s really still part of the &amp;ldquo;pool of money&amp;rdquo; that belongs to us. Similarly, when our friends pay us back, we&amp;rsquo;re not tracking the fact that our friends owe us money when the original transaction happened and their payment later neutralizes the debt.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Double-entry_bookkeeping"&gt;Double-entry bookkeeping&lt;/a&gt; is the solution to both these problems. Businesses have been using it for hundreds of years&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt; to run their accounts, and it has powerful yet elegant ways to solve these problems and many others too.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s consider again the transfer. In double-entry bookkeeping, we would represent the initial move of money 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;Bank-Checking -1000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Transfer-In-Flight +1000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And when it arrives:&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;Transfer-In-Flight -1000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Savings-Account +1000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In both cases, we see the &amp;ldquo;golden rules&amp;rdquo; of double-entry bookkeeping:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Every transaction has at least two sides and the sum of all the sides is zero&lt;/em&gt;. -1000 + 1000 = 0. That is, transactions always &amp;ldquo;balance&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Every side of a transaction is an account&lt;/em&gt;, whether it exists in the real world or not.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It should be clear that &amp;ldquo;Bank-Checking&amp;rdquo; and &amp;ldquo;Savings-Account&amp;rdquo; are labels for your checking and savings accounts, respectively. But what is &amp;ldquo;Transfer-In-Flight&amp;rdquo;?&lt;/p&gt;
&lt;p&gt;Well, it&amp;rsquo;s also an account! It&amp;rsquo;s not an account you&amp;rsquo;ll find on your bank&amp;rsquo;s website, but within the double-entry system, it&amp;rsquo;s &lt;em&gt;just as real&lt;/em&gt;. Concretely, accounts in double-entry are just labels for a &amp;ldquo;bucket of money&amp;rdquo;. So there&amp;rsquo;s no &amp;ldquo;category to put this transaction under&amp;rdquo;, no &amp;ldquo;expense tracking&amp;rdquo;, no special &amp;ldquo;transfer tag&amp;rdquo;. Everything is an account.&lt;/p&gt;
&lt;p&gt;In this specific case, &lt;code&gt;Bank-Checking&lt;/code&gt;, &lt;code&gt;Savings-Account&lt;/code&gt;, and &lt;code&gt;Transfer-In-Flight&lt;/code&gt; are all a specific type of account: they are &lt;strong&gt;Asset&lt;/strong&gt; accounts. Assets are stuff you own; these can be real accounts (bank account, savings, stocks, bonds) or conceptual accounts (money in transit between accounts).&lt;/p&gt;
&lt;p&gt;Now let&amp;rsquo;s consider the dinner example. There are 4 sides to the transaction:&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;Credit-Card -90
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Restaurant-Expense +30
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Owes-Me:Alice +30
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Owes-Me:Bob +30
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, -90 + 30 + 30 + 30 = 0. It balances. All of &lt;code&gt;Credit-Card&lt;/code&gt;, &lt;code&gt;Restaurant-Expense&lt;/code&gt;, &lt;code&gt;Owes-Me:Alice&lt;/code&gt;, and &lt;code&gt;Owes-Me:Bob&lt;/code&gt; are just accounts.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Credit-Card&lt;/code&gt; is a different type of account though: it&amp;rsquo;s a &lt;strong&gt;Liability&lt;/strong&gt;. Liabilities are the opposite of assets: instead of stuff you own, they&amp;rsquo;re stuff you &lt;strong&gt;owe&lt;/strong&gt; to someone else. So for example, loans, credit cards, and mortgages are all liabilities.&lt;/p&gt;
&lt;p&gt;Why &lt;strong&gt;negative&lt;/strong&gt; 90? The rule is always: negative means money flowed &lt;em&gt;from&lt;/em&gt; this account and positive means it flowed &lt;em&gt;into&lt;/em&gt; this account. The credit card company fronted you $90, so that money flowed from your credit card to fund the purchase.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Restaurant-Expense&lt;/code&gt; is yet another type of account, an &lt;strong&gt;Expense&lt;/strong&gt; account. Expense accounts are money &amp;ldquo;leaving your world&amp;rdquo;. So any time you spend some money and you no longer have access to it, that&amp;rsquo;s an expense.&lt;/p&gt;
&lt;p&gt;Finally, &lt;code&gt;Owes-Me:Alice&lt;/code&gt; and &lt;code&gt;Owes-Me:Bob&lt;/code&gt; are also &lt;strong&gt;Assets&lt;/strong&gt;. Alice and Bob have promised to pay you back, and that promise has value, $30 each. It&amp;rsquo;s not cash in your pocket, but it&amp;rsquo;s money you have a claim on. In double-entry, anything with economic value you control is an asset, whether it&amp;rsquo;s a bank balance or an IOU.&lt;/p&gt;
&lt;p&gt;Later, when Alice pays you back:&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;Bank-Checking +30
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Owes-Me:Alice -30
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is just money moving from the &amp;ldquo;virtual&amp;rdquo; &lt;code&gt;Owes-Me:Alice&lt;/code&gt; to the &amp;ldquo;real&amp;rdquo; &lt;code&gt;Bank-Checking&lt;/code&gt; account. Both of these are still assets; it&amp;rsquo;s just the &lt;em&gt;type&lt;/em&gt; of asset that&amp;rsquo;s changing. So no money has &amp;ldquo;entered the system&amp;rdquo; at this point. You&amp;rsquo;re just settling the debt Alice owed you.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take one last example: a paycheck.&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;Bank-Checking +3000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Salary -3000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Bank-Checking&lt;/code&gt; is an &lt;strong&gt;Asset&lt;/strong&gt; as we&amp;rsquo;ve learned. But &lt;code&gt;Salary&lt;/code&gt; is a new account type, an &lt;strong&gt;Income&lt;/strong&gt; account. Just like &lt;strong&gt;Assets&lt;/strong&gt; and &lt;strong&gt;Liabilities&lt;/strong&gt; are opposites, so are &lt;strong&gt;Income&lt;/strong&gt; and &lt;strong&gt;Expenses&lt;/strong&gt;. Where &lt;strong&gt;Expenses&lt;/strong&gt; are money leaving your world, &lt;strong&gt;Income&lt;/strong&gt; is money entering it.&lt;/p&gt;
&lt;p&gt;Money flowed from Salary (source, negative) to Bank-Checking (destination, positive). The sign feels backwards: &amp;ldquo;I &lt;em&gt;received&lt;/em&gt; money, so why is Income &lt;em&gt;negative&lt;/em&gt;?&amp;rdquo; Because the sign shows direction of flow: income is where the money &lt;em&gt;came from&lt;/em&gt;, and your bank is where it &lt;em&gt;went to&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This is the one part of double-entry that takes repetition.&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt; Don&amp;rsquo;t try to make it intuitive; just trust the invariant: &lt;strong&gt;if your transaction sums to zero, you&amp;rsquo;ve got the signs right.&lt;/strong&gt; After a dozen transactions, the pattern becomes automatic.&lt;/p&gt;
&lt;p&gt;These four types of accounts cover 99% of what you&amp;rsquo;ll do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assets&lt;/strong&gt;: stuff you own (bank accounts, cash, investments, money owed to you)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Liabilities&lt;/strong&gt;: stuff you owe (credit cards, loans)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Income&lt;/strong&gt;: money entering your world (salary, interest, dividends)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expenses&lt;/strong&gt;: money leaving your world (groceries, rent, restaurants)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s a fifth type, &lt;strong&gt;Equity&lt;/strong&gt;, which is a catch-all &amp;ldquo;this money doesn&amp;rsquo;t fit elsewhere&amp;rdquo; bucket.&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; Suppose you start tracking an account that already has $1000 in it; that money came from &lt;strong&gt;somewhere&lt;/strong&gt; but you don&amp;rsquo;t have a record of that. It can&amp;rsquo;t be income because you already had it, and the other types don&amp;rsquo;t fit. That&amp;rsquo;s a good sign it belongs in equity. The good news is that you rarely interact with Equity directly. Generally, the software handles it for you, but there are some exceptions that we&amp;rsquo;ll cover in Chapter 2.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s so much more that could be said about double-entry bookkeeping. For further reading, I particularly like the &lt;a href="https://beancount.github.io/docs/the_double_entry_counting_method.html"&gt;double entry explainer&lt;/a&gt; in the Beancount docs. It goes through some more examples and expands into a bunch of related topics.&lt;/p&gt;
&lt;p&gt;But we now have the foundation which ensures that every transaction balances, every dollar is accounted for, and nothing slips through the cracks. But we still need a way to actually record and store these transactions.&lt;/p&gt;
&lt;h4 id="plain-text-accounting"&gt;Plain text accounting&lt;/h4&gt;
&lt;p&gt;One of the things I learned from writing my own finance system is that auditability is king. You need the ability to eyeball a transaction, ask yourself &amp;ldquo;does this look right,&amp;rdquo; and fix it if it doesn&amp;rsquo;t. And nothing beats being able to see and edit any transaction you&amp;rsquo;ve ever made in a text editor.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s one of the main things that drew me to the philosophy of &lt;a href="https://plaintextaccounting.org/"&gt;Plain Text Accounting&lt;/a&gt;. This is a set of principles on using plain text files as the &amp;ldquo;immutable source of truth&amp;rdquo; of your finances and then building scripts and tools on top of them to process, analyze, and visualize them.&lt;/p&gt;
&lt;p&gt;There are many other advantages to this I&amp;rsquo;ve come to appreciate over the years:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything is version controlled.&lt;/strong&gt; You can store these transaction files in a git repo, which has powerful effects. You can look at diffs to see what changed on any day. You can &lt;code&gt;git blame&lt;/code&gt; any transaction to see when and why it was added. You can &lt;code&gt;git tag&lt;/code&gt; important states of the repo (e.g. when taxes were filed, when a new job was started, when a big refactoring happened). You can &lt;code&gt;git checkout&lt;/code&gt; any previous state to see e.g. &amp;ldquo;how did my repo look last year&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&amp;rsquo;s private&lt;/strong&gt; so you never have to trust any third party with all your financial details. Everything can be stored in locations that you fully control.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There&amp;rsquo;s no lock-in.&lt;/strong&gt; Because everything is just a plain text file, it&amp;rsquo;s trivially easy to change how you want things to be represented: you don&amp;rsquo;t have to deal with apps with broken or messy CSV exports making it difficult to take your data elsewhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&amp;rsquo;s scriptable&lt;/strong&gt;. If you want to refactor something, compute a new breakdown or even rewrite your system entirely, all you need is to write a script. Whether you write it yourself or prompt an LLM to do it for you, the text-based format makes automation trivial.&lt;/p&gt;
&lt;p&gt;Plain text gives you the abstract idea of &amp;ldquo;storing transactions in text&amp;rdquo; but there&amp;rsquo;s still a bunch of questions. What&amp;rsquo;s the transaction syntax? How do you parse your files? How do you validate that everything balances, compute totals, and let you query the results? That&amp;rsquo;s where &lt;strong&gt;Beancount&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;h4 id="introducing-beancount"&gt;Introducing Beancount!&lt;/h4&gt;
&lt;p&gt;Over the years, people have written many plain-text accounting tools which answer all the questions above. The main ones you&amp;rsquo;ll find which have gained a lot of popularity are &lt;strong&gt;Ledger&lt;/strong&gt;, &lt;strong&gt;hledger&lt;/strong&gt;, and &lt;strong&gt;Beancount&lt;/strong&gt;. I&amp;rsquo;ve used each of them at some point in my plain text journey and all are solid choices. But I ended up on Beancount for a few reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&amp;rsquo;s a Python library&lt;/strong&gt;, not just a command-line tool. I can write importers that parse my bank&amp;rsquo;s PDF statements, generate transactions programmatically, and build custom reports, all in a language I already know.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strictness by default&lt;/strong&gt;. Accounts must be declared before use, so typos get caught immediately. Transactions must balance and there are immediate error messages if they don&amp;rsquo;t. The tool catches mistakes early rather than letting them propagate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plugin and tool ecosystem&lt;/strong&gt;. Beancount has a very rich set of libraries and tools which build on top of and integrate with it. Along with the core project, you get access to any and all of these projects you want to use. We&amp;rsquo;ll discuss this much more later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/beancount/fava"&gt;Fava&lt;/a&gt;&lt;/strong&gt;. The web UI which sits on top of the Beancount engine. It&amp;rsquo;s so good that people convert from other formats (using tools like &lt;a href="https://github.com/beancount/ledger2beancount"&gt;ledger2beancount&lt;/a&gt; or &lt;a href="https://github.com/shivanshu7/gnucash2beancount"&gt;gnucash2beancount&lt;/a&gt;) just to use it. Where Beancount gives you the reliable engine, Fava gives you the pretty yet powerful frontend:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reports&lt;/strong&gt;: balance sheet, income statement, transaction journal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query editor&lt;/strong&gt;: SQL-like queries, exportable to CSV&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Charts&lt;/strong&gt;: spending breakdowns, net worth over time, holdings by currency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error highlighting&lt;/strong&gt;: problems highlighted immediately&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extensibility&lt;/strong&gt;: plugins like &lt;a href="https://github.com/andreasgerstmayr/fava-dashboards"&gt;fava-dashboards&lt;/a&gt; and &lt;a href="https://github.com/andreasgerstmayr/fava-portfolio-returns"&gt;fava-portfolio-returns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this, we now understand enough of the basic concepts for us to get started trying out Beancount!&lt;/p&gt;
&lt;h2 id="chapter-2-getting-started"&gt;Chapter 2: Getting Started&lt;/h2&gt;
&lt;p&gt;The best way to learn plain-text accounting is to roll up your sleeves and try it out. So let&amp;rsquo;s pause the theory for a moment and get a real ledger running on your machine.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve built a companion repository, &lt;a href="https://github.com/LalitMaganti/beancount-blog-examples"&gt;LalitMaganti/beancount-blog-examples&lt;/a&gt;, which contains a cut-down version of the system I use day-to-day. The repo is organized into folders that match the chapters of this post (&lt;code&gt;chapter-2/&lt;/code&gt;, &lt;code&gt;chapter-3/&lt;/code&gt;, etc.), each building on the previous. It also includes a &lt;code&gt;demo/&lt;/code&gt; folder with 2+ years of synthetic history if you want to see the end result immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To get started, clone the repository:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/LalitMaganti/beancount-blog-examples.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; beancount-blog-examples
&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;# Run the demo to see the end result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./scripts/quickstart.sh demo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or start Chapter 2 to follow the guide&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./scripts/quickstart.sh chapter-2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will set up a Python environment, install dependencies, and open Fava at http://localhost:5000. Here&amp;rsquo;s what you should see.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/02-first-look-trial-balance.png" alt="Fava Trial Balance showing Assets, Income, and Expenses" title="First look at Fava"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;Fava's Trial Balance view shows assets, income, and expenses in one place.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/03-sunburst-expenses.png" alt="Sunburst chart breaking down expenses" title="Visualizing expenses"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;Sunburst charts make it easy to spot your biggest expense categories instantly.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click around the different tabs. You&amp;rsquo;ll find that Fava gives you the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Balance sheet&lt;/strong&gt;: the state of your accounts at the current point in time. Basically think of it as an aggregate view of all your finances. You can answer questions like &amp;ldquo;what&amp;rsquo;s my net worth now?&amp;rdquo;, &amp;ldquo;how much money do I owe across my credit cards?&amp;rdquo; or &amp;ldquo;how much have I gained from investments?&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Income statement&lt;/strong&gt;: the sum of all the money flows into/out of your accounts. Think of it as a sum of all the income and expenses across time. You can answer questions like &amp;ldquo;how much did I earn from my job?&amp;rdquo;, &amp;ldquo;how much did I spend on Amazon?&amp;rdquo; or &amp;ldquo;did I spend more or less than last year?&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transaction history&lt;/strong&gt;: a flat list of all transactions you have in your journal. A way to see and search any transaction you&amp;rsquo;ve made across any accounts in your system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/04-query-console.png" alt="Fava Query Console running a SQL query" title="SQL for your finances"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The Query Console lets you run SQL-like queries against your financial data.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s much more to Fava&amp;rsquo;s features (queries, multi-currency, plugins) as we&amp;rsquo;ll see later on in the post.&lt;/p&gt;
&lt;p&gt;You can also explore the text journal itself using a text editor. For example, here&amp;rsquo;s a grocery run and a payslip in the Beancount transaction format:&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;; chapter-2/src/transactions.beancount
&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;; Beancount auto-fills the second amount when it can be inferred.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-15 * &amp;#34;Tesco&amp;#34; &amp;#34;Weekly groceries&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Expenses:Groceries 85.50 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:HSBC:Current:GBP
&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;2024-01-25 * &amp;#34;Google&amp;#34; &amp;#34;January salary&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:HSBC:Current:GBP 3200.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Income:Lalit:UK:Google:Salary
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Go through the transactions and get a feel for the format. It might seem alien at first but trust me when I say soon it&amp;rsquo;ll feel like the most natural thing in the world!&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/05-ide-experience.png" alt="VS Code with Beancount extension showing syntax highlighting" title="The IDE Experience"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;With a VS Code extension, you get syntax highlighting and auto-completion for your accounts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now that you have a working system, I want to share the hard-won insights that aren&amp;rsquo;t in the official docs. This post isn&amp;rsquo;t going to be a full Beancount tutorial. The &lt;a href="https://beancount.github.io/docs/getting_started_with_beancount.html"&gt;official docs&lt;/a&gt; are excellent for that (Fava even has a &lt;a href="https://fava.pythonanywhere.com/example-beancount-file/income_statement/"&gt;demo&lt;/a&gt; that you can try without downloading anything!).&lt;/p&gt;
&lt;p&gt;Instead, I want to focus on the &lt;em&gt;architecture&lt;/em&gt;: the decisions I wish I&amp;rsquo;d made correctly from day one. I&amp;rsquo;m writing this as if I&amp;rsquo;m speaking to my past self.&lt;/p&gt;
&lt;h4 id="start-with-one-account"&gt;Start with one account&lt;/h4&gt;
&lt;p&gt;You don&amp;rsquo;t need to track everything on day one. Pick one account. Your main checking account is a good start. Get comfortable with the flow. Import statements, categorize transactions, check that balances match. Once that feels solid, add another account. Then another.&lt;/p&gt;
&lt;p&gt;I started with my HSBC current account. Now, I have my whole financial life inside the system and I trust it wholeheartedly. But this happened one account at a time. If I tried to do everything in one go, I would certainly have been overwhelmed and given up on the whole thing.&lt;/p&gt;
&lt;h4 id="opening-balances"&gt;Opening balances&lt;/h4&gt;
&lt;p&gt;Once you&amp;rsquo;ve picked your first account, you face an immediate problem: you may have opened that account years ago and there might already be thousands of transactions over that time. Trying to import them all in one go is another sure path to being overwhelmed and giving up.&lt;/p&gt;
&lt;p&gt;Instead, a better idea is to pick a &amp;ldquo;starting date&amp;rdquo; at which you say &amp;ldquo;I will import everything from this day onwards&amp;rdquo;. But that poses its own problem: you already had money in that account, how do you tell Beancount it exists?&lt;/p&gt;
&lt;p&gt;Well, Beancount has a &lt;code&gt;pad&lt;/code&gt; directive that creates the balancing entry automatically:&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;; chapter-2/src/balance.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Equity:Opening-Balances
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 pad Assets:Lalit:UK:HSBC:Current:GBP Equity:Opening-Balances
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-02 balance Assets:Lalit:UK:HSBC:Current:GBP 1500.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;pad&lt;/code&gt; directive tells Beancount: &amp;ldquo;whatever amount is needed to make the balance assertion true, take it from &lt;code&gt;Equity:Opening-Balances&lt;/code&gt; and put it in this account&amp;rdquo;. This is one of the rare cases you actually have to think about equity accounts (though not much beyond blindly using &lt;code&gt;Equity:Opening-Balances&lt;/code&gt;!).&lt;/p&gt;
&lt;h3 id="structure-that-scales"&gt;Structure That Scales&lt;/h3&gt;
&lt;p&gt;Adding a first account is easy and the second is straightforward, but adding a third, fourth, fifth&amp;hellip; and you can easily find that things start becoming jumbled and messy. Just like code, putting a little bit of thought into the organization upfront goes a long way. This part covers the architectural decisions you&amp;rsquo;ll thank yourself later for.&lt;/p&gt;
&lt;h4 id="naming-asset-and-liability-accounts"&gt;Naming Asset and Liability accounts&lt;/h4&gt;
&lt;p&gt;The structure of asset and liability account names is very important, much more than I initially gave them credit for. It&amp;rsquo;s a good idea to keep as much information in them as possible. Here&amp;rsquo;s what I&amp;rsquo;ve settled on:&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;; chapter-2/src/accounts.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Assets:Lalit:UK:HSBC:Current:GBP GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Assets:Lalit:UK:Barclays:Current:GBP GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Liabilities:Lalit:UK:AMEX:GBP GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The pattern 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;Type:Person:Region:Institution:Account:Currency
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Why this structure? Because it&amp;rsquo;s a lot easier to &lt;em&gt;remove detail&lt;/em&gt; than add it in later! I initially started by not having the country, the currency or my name in the account. But over time, I wanted to understand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How much money I have in the UK vs the US?&lt;/li&gt;
&lt;li&gt;How much cash (i.e. not investments) is in a certain currency?&lt;/li&gt;
&lt;li&gt;How much of our household wealth was in my wife&amp;rsquo;s accounts vs my own (discussed in more detail in Chapter 6)?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having this information in the account name is great because Beancount&amp;rsquo;s SQL syntax makes it very easy to filter on account names. Want &amp;ldquo;all UK assets&amp;rdquo;? Filter on &lt;code&gt;:UK:&lt;/code&gt;. Want &amp;ldquo;all HSBC accounts&amp;rdquo;? Filter on &lt;code&gt;:HSBC:&lt;/code&gt;. Want &amp;ldquo;all GBP cash&amp;rdquo;? Filter on &lt;code&gt;:GBP&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="powering-up-with-plugins"&gt;Powering up with Plugins&lt;/h4&gt;
&lt;p&gt;Once you have a structure, you want to ensure it stays clean. This is where Beancount&amp;rsquo;s &lt;strong&gt;Plugins&lt;/strong&gt; come in.&lt;/p&gt;
&lt;p&gt;Plugins are Python scripts that run when your ledger loads. They can validate data, modify entries, or even generate new transactions automatically. You load them in your journal file like this:&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;plugin &amp;#34;beancount.plugins.check_commodity&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Remember the transfer-in-flight pattern from Chapter 1? Here&amp;rsquo;s how it looks in Beancount:&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;; chapter-2/src/transactions.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;; Money leaves on Saturday
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-03-16 * &amp;#34;Transfer to Barclays&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:HSBC:Current:GBP -1000.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:Transfers:Internal 1000.00 GBP
&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;; Money arrives on Monday
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-03-18 * &amp;#34;Transfer from HSBC&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:Transfers:Internal -1000.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:Barclays:Current:GBP 1000.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once both transactions are recorded, the transit account balance returns to zero, confirming the transfer is complete. If you record one leg of a transfer but forget the other, the transit account will simply show a non-zero balance.&lt;/p&gt;
&lt;p&gt;Understanding where a non-zero balance in transfers is coming from is handled by my absolute favorite plugin, &lt;strong&gt;&lt;a href="https://github.com/redstreet/beancount_reds_plugins"&gt;beancount_reds_plugins.zerosum&lt;/a&gt;&lt;/strong&gt;. It&amp;rsquo;s responsible for matching both sides of a transaction and moving it to a separate account, meaning my &lt;code&gt;Transfers:Internal&lt;/code&gt; account only contains the actual &amp;ldquo;pending&amp;rdquo; transactions. Making this account empty is a surprisingly satisfying little &amp;ldquo;minigame&amp;rdquo; during my weekly imports (though it never lasts for long!).&lt;/p&gt;
&lt;p&gt;There are also a couple more plugins that make handling closed accounts better:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/beancount/beancount/blob/master/beancount/plugins/close_tree.py"&gt;beancount.plugins.close_tree&lt;/a&gt;&lt;/strong&gt; - When you close an account, automatically closes all child accounts too. Useful as you move your banking between institutions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/daniel-wells/beancount_checkclosed"&gt;beancount_checkclosed.check_closed&lt;/a&gt;&lt;/strong&gt; - Validates that closed accounts have zero balance and no transactions after the close date.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="the-boundary-of-your-system"&gt;The boundary of your system&lt;/h4&gt;
&lt;p&gt;As you add accounts one by one, you&amp;rsquo;ll inevitably see money flowing to places you haven&amp;rsquo;t set up yet. Say you transfer £500 to a Natwest savings account you haven&amp;rsquo;t added to the system. Where does it go?&lt;/p&gt;
&lt;p&gt;Specifically, use a named placeholder account in &lt;code&gt;Equity:Transfers&lt;/code&gt;:&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;; chapter-2/src/transactions.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-03-15 * &amp;#34;Transfer to savings (not yet tracked)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:HSBC:Current:GBP -500.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Equity:Transfers:Natwest-Savings 500.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This essentially says: &amp;ldquo;£500 went to Natwest Savings, which I&amp;rsquo;m not tracking yet&amp;rdquo;. Putting it in an equity account means it doesn&amp;rsquo;t pollute your balance sheet with incomplete information &lt;em&gt;or&lt;/em&gt; your income statement with false expenses.&lt;/p&gt;
&lt;p&gt;Note also the best practice of using a &lt;em&gt;named&lt;/em&gt; equity account per untracked destination, not a generic bucket; this was a mistake I made when I did this initially. You&amp;rsquo;ll thank yourself when you import your Natwest savings account later, you can just do a search-replace to rename the accounts &lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h4 id="organizing-your-repo"&gt;Organizing your repo&lt;/h4&gt;
&lt;p&gt;As you add these patterns (transit accounts, multiple institutions, liability accounts) your single &lt;code&gt;journal.beancount&lt;/code&gt; file will start to become unwieldy. Just like good software architecture, you want to organize upfront to be easy to maintain as the system continues to grow.&lt;/p&gt;
&lt;p&gt;This is what the structure looks like with the concepts we have right now (it&amp;rsquo;ll get more complicated as we go deeper!):&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;chapter-2/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── journal.beancount # Main entry point, includes everything
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── src/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── accounts.beancount # Account definitions
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── transactions.beancount # Primary transaction ledger
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── balance.beancount # Balance assertions
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/&lt;/code&gt; is what you write and edit (your &amp;ldquo;code&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;journal.beancount&lt;/code&gt; is the entry point that includes everything&lt;/li&gt;
&lt;li&gt;Later, &lt;code&gt;data/&lt;/code&gt; will hold inputs from the outside world (raw statements: PDFs, OFX, CSVs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This organization is a small change but can make a big difference in your subconscious feeling about the state of your finances!&lt;/p&gt;
&lt;h4 id="exit-ramp"&gt;Exit ramp&lt;/h4&gt;
&lt;p&gt;At this point, you have a solid foundation for tracking multiple accounts. If you stop here, you have a robust, auditable system for manual bookkeeping. You could continue adding transactions by hand indefinitely, and you&amp;rsquo;d still be miles ahead of any spreadsheet in terms of correctness and visibility.&lt;/p&gt;
&lt;p&gt;But manual entry is a chore, and as your financial life grows, it becomes a bottleneck. In the next chapter, we&amp;rsquo;ll see how to automate the tedious part: getting transactions from your bank statements into your ledger without losing the control that plain-text accounting gives you.&lt;/p&gt;
&lt;h2 id="chapter-3-automated-import"&gt;Chapter 3: Automated Import&lt;/h2&gt;
&lt;h4 id="automation-is-king-but-harder-than-it-looks"&gt;Automation is king, but harder than it looks&lt;/h4&gt;
&lt;p&gt;Inputting transactions by hand works for some people, but I don&amp;rsquo;t have the patience for it. Ever since I was young, I&amp;rsquo;ve always wanted to automate everything: it&amp;rsquo;s the reason why I became a software engineer in the first place!&lt;/p&gt;
&lt;p&gt;But full automation is a dead end. Most banks don&amp;rsquo;t offer APIs, and scraping breaks constantly. 2FA flows change &lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;, websites get redesigned, sessions expire. I tried this route, and it wasn&amp;rsquo;t worth it. Even in the US where aggregators like Plaid exist, coverage is patchy.&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt; In the UK, it&amp;rsquo;s impossible.&lt;/p&gt;
&lt;h4 id="the-hierarchy-of-data-sources"&gt;The hierarchy of data sources&lt;/h4&gt;
&lt;p&gt;So what actually works? Well ideally your financial institution gives you something you can write a script against. But what that might be is non-obvious and counter-intuitive. Here&amp;rsquo;s the hierarchy I&amp;rsquo;ve landed on over time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OFX is the gold standard.&lt;/strong&gt; If your bank offers it, use it. The format is standardized, transactions have unique IDs, and deduplication is straightforward. Life is easy if you have good OFX.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSV is the deceptive runner-up.&lt;/strong&gt; It seems like the logical choice. Structured data, right? But in practice, bank CSVs are often afterthoughts. I&amp;rsquo;ve seen column formats change without notice, &amp;ldquo;CSVs&amp;rdquo; that are actually weird custom formats spread over multiple lines, and rows coalesced in ways that lose critical information (like cost basis).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So what do you do when OFX isn&amp;rsquo;t available and CSV isn&amp;rsquo;t trustworthy? You turn to an unlikely hero.&lt;/p&gt;
&lt;h4 id="why-pdfs-beat-csvs"&gt;Why PDFs beat CSVs&lt;/h4&gt;
&lt;p&gt;It sounds backwards, but PDFs are often the most reliable data source available.&lt;/p&gt;
&lt;p&gt;Banks have a strong incentive to get PDFs right. Customers actually read them. They&amp;rsquo;re legal documents that get printed and filed. If a bank messes up a PDF statement, they hear about it immediately. If they break a CSV export, it might go months without anyone noticing.&lt;/p&gt;
&lt;p&gt;The key insight is that bank statement PDFs are almost always columnar. Of course, this relies on the PDF having a proper text layer; if your bank sends you scanned images, you&amp;rsquo;re out of luck (though I&amp;rsquo;ve yet to encounter one that does). When you convert them to text while preserving the layout, you get something that looks like this:&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;Date Details Paid out Paid in Balance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;15 Jan 24 TESCO STORES 1234 42.50 1,457.50
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;16 Jan 24 TFL TRAVEL 6.80 1,450.70
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;25 Jan 24 GOOGLE SALARY 3,200.00 4,650.70
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The columns are aligned by spaces, which means you can parse them as fixed-width data. The approach works in three steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Convert PDF to text&lt;/strong&gt;: Run &lt;code&gt;pdftotext -layout statement.pdf statement.txt&lt;/code&gt;. The &lt;code&gt;-layout&lt;/code&gt; flag preserves the original column alignment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Find the table boundaries&lt;/strong&gt;: Bank statements have predictable markers. HSBC uses &amp;ldquo;BALANCE BROUGHT FORWARD&amp;rdquo; at the start and &amp;ldquo;BALANCE CARRIED FORWARD&amp;rdquo; at the end. You extract just the transaction rows between these markers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Parse with fixed-width columns&lt;/strong&gt;: Pandas&amp;rsquo; &lt;code&gt;read_fwf&lt;/code&gt; function is designed exactly for this. You put this logic inside the &lt;code&gt;extract()&lt;/code&gt; method of your &lt;code&gt;beangulp&lt;/code&gt; importer class, where it converts the text into a DataFrame:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&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="c1"&gt;# Inside your Importer class&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_fwf&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;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringIO&lt;/span&gt;&lt;span class="p"&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;colspecs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&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;names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Date&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Details&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Paid out&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Paid in&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Balance&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="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The column positions come from inspecting the header row. In practice, I detect them dynamically by finding keywords like &amp;ldquo;Paid out&amp;rdquo; and &amp;ldquo;Paid in&amp;rdquo; in the header and using their character positions.&lt;/p&gt;
&lt;p&gt;Once you have a DataFrame, generating Beancount transactions is straightforward. You write a small class that iterates through this DataFrame and maps each row to a Beancount &lt;code&gt;Transaction&lt;/code&gt; object, filling in the date, amount, and payee. See my &lt;a href="https://github.com/LalitMaganti/beancount-lalitm/blob/main/src/beancount_lalitm/importers/hsbc.py"&gt;HSBC importer&lt;/a&gt; for a working example.&lt;/p&gt;
&lt;p&gt;For 95% of my banks, this approach works great. However, there is one bank where the text spacing becomes very strange and so I need to use something else. That&amp;rsquo;s when I turn to &lt;a href="https://tabula.technology/"&gt;Tabula&lt;/a&gt;, a Java CLI that extracts data tables from PDFs, even very complex ones.&lt;/p&gt;
&lt;p&gt;The main reason I don&amp;rsquo;t use it all the time is that it&amp;rsquo;s much slower. But it also succeeds in the cases where &lt;code&gt;pdftotext&lt;/code&gt; fails. I run it to get a structured JSON output, from which I create transactions (see my &lt;a href="https://github.com/LalitMaganti/beancount-lalitm/blob/main/src/beancount_lalitm/importers/hsbc_us_cc.py"&gt;HSBC US credit card importer&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m sure some readers will have worries about the fragility of what I&amp;rsquo;ve described here. I can tell you from experience that in three years, neither my UK nor my US HSBC PDF importer has ever broken. Neither has my Schwab one, and my Aviva one has only needed a single change. So I can personally vouch that this approach works and works &lt;strong&gt;well&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id="from-raw-data-to-transactions"&gt;From raw data to transactions&lt;/h4&gt;
&lt;p&gt;OK, so now you have statements. What do you do then? There are two pieces to the puzzle: parsing statements into transactions, and categorizing those transactions.&lt;/p&gt;
&lt;p&gt;For parsing, Beancount has an official importer framework called &lt;a href="https://beancount.github.io/docs/beangulp.html"&gt;beangulp&lt;/a&gt;. You write a Python class that knows how to read a particular file format (the &lt;a href="https://github.com/LalitMaganti/beancount-blog-examples/blob/main/chapter-3/importers/hsbc.py"&gt;skeleton importer&lt;/a&gt; in &lt;code&gt;chapter-3/&lt;/code&gt; already uses this API). Beangulp handles the mechanics: identifying which importer handles which file, extracting transactions, and deduplicating against existing entries.&lt;/p&gt;
&lt;p&gt;But beangulp just extracts transactions without making any judgment on which account the transaction should be booked against. It doesn&amp;rsquo;t know that Tesco is groceries or that British Airways is an airline. I go to a new restaurant. How does the system know that &lt;em&gt;is&lt;/em&gt; a restaurant?&lt;/p&gt;
&lt;p&gt;This leads to a very important conclusion: we &lt;strong&gt;cannot&lt;/strong&gt; fully automate importing transactions for bank accounts and credit cards. However, that doesn&amp;rsquo;t mean we have to enter things manually either. There&amp;rsquo;s a middle ground.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://github.com/jbms/beancount-import"&gt;beancount-import&lt;/a&gt;&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt; (Note: this is a standalone tool, distinct from Fava&amp;rsquo;s built-in import features). It&amp;rsquo;s a web UI that uses your beangulp importers and adds a categorization layer on top. You run it, it opens in your browser, and it presents pending transactions one by one for review. Think of it as a staging area where you approve or tweak before anything hits your ledger.&lt;/p&gt;
&lt;p&gt;For each transaction, it shows the raw data from your statement alongside a suggested categorization. You can accept the suggestion, override it with a different account, or skip it entirely. Once you decide, it moves to the next one. The interface is simple, mostly keyboard-driven and optimized for speed.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/06-import-ui.png" alt="Beancount Import Web UI" title="Visual Categorization"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The import UI allows you to manually categorize transactions, like this Tesco grocery run, while the system learns your preferences.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The categorization uses machine learning (old-school decision trees, running locally, no LLMs, no cloud). It learns from your previous choices: the first time you see &amp;ldquo;Tesco&amp;rdquo;, you pick &amp;ldquo;Expenses:Groceries&amp;rdquo;, and the second time it auto-suggests and you just hit Enter. After a few weeks, the system should know 90% of the types of transactions you make and it&amp;rsquo;s easy to correct the ones which it doesn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;re proficient, a month&amp;rsquo;s worth of transactions takes 5-10 minutes. Most are repeats and you&amp;rsquo;re just hitting Enter. You only pause on genuinely new merchants. This is the core of my weekly ritual, and why I&amp;rsquo;ve found categorization has not become a chore, even after doing it for years.&lt;/p&gt;
&lt;h4 id="exit-ramp-1"&gt;Exit ramp&lt;/h4&gt;
&lt;p&gt;With automated imports and semi-automated categorization, the &amp;ldquo;hard work&amp;rdquo; of bookkeeping is mostly solved. For many, this is the endgame: a perfect record of where every penny went, updated in minutes each week.&lt;/p&gt;
&lt;p&gt;But your net worth isn&amp;rsquo;t just cash in a bank account. It&amp;rsquo;s also the stocks, bonds, and funds that grow (or shrink) over time. Tracking these requires a few more tools to handle cost basis, dividends, and market prices. We&amp;rsquo;ll tackle those in Chapter 4.&lt;/p&gt;
&lt;h2 id="chapter-4-investments"&gt;Chapter 4: Investments&lt;/h2&gt;
&lt;p&gt;Now for investments. It&amp;rsquo;s where things get more interesting, and I think there&amp;rsquo;s less material out there covering the nitty-gritty. Here are some lessons I&amp;rsquo;ve learned over the years.&lt;/p&gt;
&lt;h4 id="the-unified-mental-model"&gt;The Unified Mental Model&lt;/h4&gt;
&lt;p&gt;The most important thing to realize is that Beancount treats &lt;strong&gt;everything&lt;/strong&gt; as a commodity.&lt;/p&gt;
&lt;p&gt;A share of Apple (&lt;code&gt;AAPL&lt;/code&gt;) is a commodity. A US Dollar (&lt;code&gt;USD&lt;/code&gt;) is a commodity. A British Pound (&lt;code&gt;GBP&lt;/code&gt;) is a commodity. While Beancount &lt;em&gt;can&lt;/em&gt; infer these on the fly, you&amp;rsquo;ll typically declare them explicitly in your journal (e.g., &lt;code&gt;2024-01-01 commodity AAPL&lt;/code&gt;).&lt;sup id="fnref:10"&gt;&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref"&gt;10&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;This explicit declaration is a small but critical architectural win: it prevents a simple typo from creating a phantom currency, and it provides the metadata that advanced reporting plugins (like those used to calculate your portfolio performance) rely on to work correctly.&lt;/p&gt;
&lt;p&gt;This means you don&amp;rsquo;t &amp;ldquo;buy stocks with money&amp;rdquo;. You simply exchange one commodity for another. The syntax for buying shares is identical to the syntax for exchanging currency.&lt;/p&gt;
&lt;h4 id="the-account-structure"&gt;The Account Structure&lt;/h4&gt;
&lt;p&gt;Just like with bank accounts, I break investments down by institution and security.&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;; chapter-4/src/accounts.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Assets:Lalit:US:IB:Brokerage:USD USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Assets:Lalit:US:IB:Brokerage:AAPL AAPL
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Assets:Lalit:UK:Vanguard:ISA:GBP GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Assets:Lalit:UK:Vanguard:ISA:VWRL VWRL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cash in a brokerage is just another holding named by currency (&lt;code&gt;GBP&lt;/code&gt;, &lt;code&gt;USD&lt;/code&gt;), while stock holdings use their ticker (&lt;code&gt;AAPL&lt;/code&gt;, &lt;code&gt;VWRL&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;But you also need to track the flows &lt;em&gt;generated&lt;/em&gt; by these assets: capital gains, dividends, commissions, and withholding taxes. I create specific accounts for each security:&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;2024-01-01 open Income:Lalit:US:IB:Brokerage:AAPL:Dividends USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Income:Lalit:US:IB:Brokerage:AAPL:Capital-Gains USD
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Why so granular?&lt;/strong&gt; It&amp;rsquo;s the same reason I name assets fully: aggregation up the tree is trivial; disaggregation after the fact is impossible. If you track all your dividends in a single &lt;code&gt;Income:Dividends&lt;/code&gt; account, it&amp;rsquo;s easy to know &amp;ldquo;how much dividends did I earn total?&amp;rdquo;. But if you want to know &amp;ldquo;what was my AAPL dividend yield this year?&amp;rdquo;, you&amp;rsquo;re out of luck. Track at the leaf (&lt;code&gt;Income:IB:AAPL:Dividends&lt;/code&gt;), and you can answer both questions.&lt;/p&gt;
&lt;h4 id="the-notation"&gt;The Notation&lt;/h4&gt;
&lt;p&gt;With our accounts defined, we can now record the actual movement of assets. We use &lt;code&gt;{}&lt;/code&gt; to denote cost (what we paid per unit) and &lt;code&gt;@&lt;/code&gt; to denote price (what the unit is worth now).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 1: Buying Stock&lt;/strong&gt;
Exchanging 1850 USD for 10 shares of Apple.&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;2024-01-10 * &amp;#34;BUY AAPL&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:US:IB:Brokerage:AAPL 10 AAPL {185.00 USD} @ 185.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:US:IB:Brokerage:USD -1850.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;{185.00 USD}&lt;/code&gt; is the &lt;strong&gt;cost basis&lt;/strong&gt; and the &lt;code&gt;@ 185.00 USD&lt;/code&gt; is the &lt;strong&gt;price&lt;/strong&gt;. Beancount uses the cost basis to track lots and calculate capital gains.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: Cost basis rules depend heavily on where you are. In the US, you track cost basis of individual lots. In the UK, we have special &amp;ldquo;Section 104&amp;rdquo; pooling rules.&lt;sup id="fnref:11"&gt;&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref"&gt;11&lt;/a&gt;&lt;/sup&gt; I discuss this more in Chapter 5.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 2: Buying Currency&lt;/strong&gt;
Exchanging 950 GBP for 98,000 INR.&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;2024-03-02 * &amp;#34;Wise&amp;#34; &amp;#34;GBP to INR&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:Wise:INR 98000.00 INR @@ 950.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:Wise:GBP -950.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the second example, &lt;code&gt;@@&lt;/code&gt; specifies the &lt;em&gt;total&lt;/em&gt; cost rather than the per-unit cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 3: Selling Stock&lt;/strong&gt;
Selling 5 shares of Apple at $190 (bought at $185).&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;2024-01-30 * &amp;#34;SELL AAPL&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:US:IB:Brokerage:AAPL -5 AAPL {185.00 USD} @ 190.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:US:IB:Brokerage:USD 950.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Income:Lalit:US:IB:Brokerage:AAPL:Capital-Gains -25.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we specify the lot we&amp;rsquo;re selling (&lt;code&gt;{185.00 USD}&lt;/code&gt;) and the price we&amp;rsquo;re selling it at (&lt;code&gt;@ 190.00 USD&lt;/code&gt;). The difference is the capital gain (or loss).&lt;/p&gt;
&lt;p&gt;But the principle is identical: &lt;code&gt;Assets:Wise:INR&lt;/code&gt; and &lt;code&gt;Assets:Brokerage:AAPL&lt;/code&gt; are just accounts holding commodities.&lt;/p&gt;
&lt;h4 id="automation"&gt;Automation&lt;/h4&gt;
&lt;p&gt;Investments are very different from normal accounts in that they can be &lt;strong&gt;fully automated&lt;/strong&gt;. No categorization needed: a buy is a buy, a dividend is a dividend. You don&amp;rsquo;t have new merchants to worry about.&lt;/p&gt;
&lt;p&gt;This means you can skip beancount-import&amp;rsquo;s web UI and run your beangulp importers directly with output going straight to the ledger. To help inspire you, I&amp;rsquo;ve open-sourced my personal collection of importers (IB, Vanguard, Schwab, and more) in the &lt;a href="https://github.com/LalitMaganti/beancount-lalitm/tree/main/src/beancount_lalitm/importers"&gt;beancount-lalitm&lt;/a&gt; repo.&lt;/p&gt;
&lt;h4 id="handling-account-sprawl"&gt;Handling Account Sprawl&lt;/h4&gt;
&lt;p&gt;However, one annoyance is that creating those granular accounts for every single stock (&lt;code&gt;...:AAPL:Dividends&lt;/code&gt;, &lt;code&gt;...:AAPL:Commissions&lt;/code&gt;, etc.) is tedious. To solve this, I wrote the &lt;a href="https://github.com/LalitMaganti/beancount-lalitm/blob/main/src/beancount_lalitm/plugins/ancillary_accounts.py"&gt;ancillary_accounts&lt;/a&gt; plugin. Instead of manual account creation, you just add metadata to the main holding account:&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;2023-02-01 open Assets:Lalit:US:IB:Brokerage:BAC BAC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ancillary_commission_currency: &amp;#34;USD&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ancillary_distribution_currency: &amp;#34;USD&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ancillary_withholding_tax_currency: &amp;#34;USD&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ancillary_capital_gains_currency: &amp;#34;USD&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The plugin automatically generates the corresponding income and expense accounts for you.&lt;/p&gt;
&lt;h4 id="corporate-actions"&gt;Corporate Actions&lt;/h4&gt;
&lt;p&gt;I also use a plugin called &lt;a href="https://github.com/LalitMaganti/beancount-lalitm/blob/main/src/beancount_lalitm/plugins/stock_split.py"&gt;stock_split&lt;/a&gt; to handle corporate actions. It retroactively adjusts historical transactions when a stock splits, keeping quantities and prices consistent with post-split values so your charts don&amp;rsquo;t show a sudden, fake drop in value.&lt;/p&gt;
&lt;h4 id="the-value-of-things-prices"&gt;The Value of Things (Prices)&lt;/h4&gt;
&lt;p&gt;We have the quantities (10 AAPL, 98,000 INR), but to calculate a single &amp;ldquo;Net Worth&amp;rdquo; number, we need to know what they are worth in your home currency. This requires prices for &lt;strong&gt;both&lt;/strong&gt;:&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;; chapter-4/src/prices.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-10 price AAPL 185.50 USD ; Stock price in USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-10 price USD 0.79 GBP ; Currency price in GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I automate this using a daily CI job. A script fetches the latest stock prices and forex rates from &lt;a href="https://www.alphavantage.co/"&gt;AlphaVantage&lt;/a&gt; and commits them to &lt;code&gt;prices.beancount&lt;/code&gt;. You can find the script &lt;a href="https://github.com/LalitMaganti/beancount-lalitm/blob/main/hack/fetch_prices.py"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In practice, I actually have three price files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prices.beancount&lt;/code&gt; - auto-fetched daily for as many securities as possible.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prices-manual.beancount&lt;/code&gt; - for securities without automatic feeds (like some pension funds). I input these manually once a month.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prices-delisted.beancount&lt;/code&gt; - historical prices for securities no longer trading. This saves me from making API calls which would fail anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="the-payoff"&gt;The Payoff&lt;/h4&gt;
&lt;p&gt;With this data, Fava comes alive. To see the full potential of these reports, I&amp;rsquo;ve included a &lt;code&gt;demo/&lt;/code&gt; folder in the companion repo with 2+ years of history. Run &lt;code&gt;./scripts/quickstart.sh demo&lt;/code&gt; and you&amp;rsquo;ll see the payoff.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Holdings&lt;/strong&gt; page now shows your positions with their cost basis and current market value.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/07-holdings-table.png" alt="Holdings view showing Cost Basis and Market Value" title="Tracking Investments"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The Holdings report automatically calculates the market value of your assets using live price data.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In the demo environment, you can also see how plugins like &lt;a href="https://github.com/andreasgerstmayr/fava-dashboards"&gt;&lt;strong&gt;fava-dashboards&lt;/strong&gt;&lt;/a&gt; build custom visualizations. The plugin uses beanquery (Beancount&amp;rsquo;s SQL-like query language) to fetch data and renders interactive charts. It’s the best way to track long-term trends and asset allocation at a glance.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/08a-dashboard-scorecards.png" alt="Dashboard showing top-level scorecards and savings rate heatmap" title="Dashboard Scorecards"&gt;
&lt;img src="/img/one-number-i-trust/08b-dashboard-allocation.png" alt="Dashboard showing asset allocation charts" title="Asset Allocation Dashboard"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;Custom dashboards (shown here using the demo data) allow you to track long-term trends and asset allocation at a glance.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And with &lt;a href="https://github.com/andreasgerstmayr/fava-portfolio-returns"&gt;&lt;strong&gt;fava-portfolio-returns&lt;/strong&gt;&lt;/a&gt;, you can calculate your true Time-Weighted Return (TWR) and Internal Rate of Return (IRR) to see if you&amp;rsquo;re actually beating the market. It accounts for cash flows properly, so adding money mid-year doesn&amp;rsquo;t inflate your returns.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/09-portfolio-returns.png" alt="Portfolio returns showing TWR and IRR" title="Portfolio Returns"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The portfolio returns plugin (using the demo data) calculates your actual investment performance, net of cash flows.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id="exit-ramp-2"&gt;Exit ramp&lt;/h4&gt;
&lt;p&gt;You now have a system that tracks your entire financial world: from the coffee you bought this morning to the capital gains in your brokerage account. For most people, this is a complete solution.&lt;/p&gt;
&lt;p&gt;However, as you collect more data, you&amp;rsquo;ll find you want to look at it in different ways. Maybe you want a simplified view for daily use and a detailed one for tax season. Or maybe you&amp;rsquo;re not the only person in your household. In the final chapters, we&amp;rsquo;ll see how to scale this system to handle multiple views and multiple people.&lt;/p&gt;
&lt;h2 id="chapter-5-multiple-views"&gt;Chapter 5: Multiple Views&lt;/h2&gt;
&lt;p&gt;Day-to-day, I want a simple view of my finances. Take-home pay as a single number, investments without tax calculations cluttering the screen. But at tax time, I need detail. Every payslip line item and capital gains calculated the way HMRC wants them. Recording the same transaction twice would be maintenance hell. So instead, I extract multiple views from a single journal.&lt;/p&gt;
&lt;p&gt;I think of these as &amp;ldquo;lenses&amp;rdquo; on the data. Some lenses &lt;strong&gt;aggregate&lt;/strong&gt;: rolling up transactions into balances, summaries, or dashboards. Others &lt;strong&gt;transform&lt;/strong&gt;: collapsing detail you don&amp;rsquo;t need day-to-day, or expanding it when you do. Both read from the same source files; nothing is duplicated.&lt;/p&gt;
&lt;h4 id="aggregated-views"&gt;Aggregated views&lt;/h4&gt;
&lt;p&gt;Fava is great for interactive exploration, but I also want textual snapshots I can version control. I have a script that generates daily summaries showing account balances, and a CI workflow that commits them automatically. In my setup this runs on a self-hosted Gitea instance on hardware I control, so the raw ledger never leaves machines I own. If you prefer, you can keep everything local-only or push to an encrypted remote; GitHub Actions works the same way if you&amp;rsquo;re comfortable with that trade-off. This gives me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A record of how balances changed day to day&lt;/li&gt;
&lt;li&gt;An immutable snapshot at tax time of what the system showed&lt;/li&gt;
&lt;li&gt;Git as audit trail: &amp;ldquo;What was my net worth on March 15th 2023?&amp;rdquo; is answerable with &lt;code&gt;git checkout&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key script is &lt;a href="https://github.com/LalitMaganti/beancount-blog-examples/blob/main/chapter-5/scripts/archive.py"&gt;archive.py&lt;/a&gt;. It uses the &lt;a href="https://github.com/beancount/beanquery"&gt;beanquery&lt;/a&gt; library to write SQL scripts over your journal and generate textual reports:&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="c1"&gt;# Generate balance sheet in GBP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; SELECT account,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; round(sum(number(convert(value(position, &amp;#39;2024-12-31&amp;#39;), &amp;#39;GBP&amp;#39;, &amp;#39;2024-12-31&amp;#39;))), 2) as value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; FROM OPEN ON 2024-01-01 CLOSE ON 2024-12-31 CLEAR
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; GROUP BY account
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; HAVING round(sum(number), 2) != 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; ORDER BY account;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I run this for each calendar year and tax year, generating files like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;networth.txt&lt;/code&gt; - Single-line net worth in each currency&lt;/li&gt;
&lt;li&gt;&lt;code&gt;balance-sheet.txt&lt;/code&gt; - Net worth breakdown by account&lt;/li&gt;
&lt;li&gt;&lt;code&gt;holdings.txt&lt;/code&gt; - Investment positions with cost basis and market value&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s what &lt;code&gt;networth.txt&lt;/code&gt; looks 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; gbp usd
&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; 15978.42 GBP 19973.02 USD
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the concrete &amp;ldquo;one number I trust&amp;rdquo;: a single net-worth snapshot in my reporting currency, generated from the full ledger and price data.&lt;/p&gt;
&lt;p&gt;And &lt;code&gt;holdings.txt&lt;/code&gt;:&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;account units curr avg_cost price book_val mkt_val
&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;Assets:Lalit:UK:HSBC:Current:GBP 4914.50 GBP 1.00 1.00 4914.50 4914.50
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Assets:Lalit:UK:Vanguard:ISA:VWRL 20.00 VWRL 96.00 97.50 1920.00 1950.00
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Assets:Lalit:US:IB:Brokerage:AAPL 5.00 AAPL 146.15 150.40 730.75 752.00
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My Gitea workflow is very simple too:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&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 class="nt"&gt;schedule&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 class="nt"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;00 7 * * *&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Run daily at 7am&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="nt"&gt;jobs&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 class="nt"&gt;update&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 class="nt"&gt;steps&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 class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;uv run scripts/archive.py outputs/ journal.beancount 2024-01-01 2024-12-31&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 class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;git commit -am &amp;#34;Regen reports&amp;#34; &amp;amp;&amp;amp; git push&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="transformed-views"&gt;Transformed views&lt;/h4&gt;
&lt;p&gt;Sometimes I want to change &lt;em&gt;how&lt;/em&gt; transactions work fundamentally. This is a more advanced technique: while Aggregated views &lt;em&gt;read&lt;/em&gt; data, Transformed views temporarily &lt;em&gt;rewrite&lt;/em&gt; it in memory to simplify reality.&lt;/p&gt;
&lt;p&gt;I have three transformed views, each for a different purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Net&lt;/strong&gt; - my daily driver. Collapses payslip details into a single take-home number.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gross&lt;/strong&gt; - breaks down payslip line items for tax time analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CGT&lt;/strong&gt; - a view that includes a &amp;ldquo;virtual currency&amp;rdquo; tracking capital gains the way my tax authority calculates them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The linchpin is the &lt;code&gt;rename_accounts&lt;/code&gt; plugin. It lets me keep one copy of all transactions and rename accounts on the fly to show or hide detail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gross vs net payslip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let me start with the simpler example. In gross view, my payslip shows every line item:&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;2024-01-25 * &amp;#34;Google&amp;#34; &amp;#34;January salary&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:HSBC:Current:GBP 3500.00 GBP ; Take-home pay
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Income:Lalit:UK:Google:Salary -5000.00 GBP ; Gross salary
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Expenses:Lalit:UK:Google:Income-Tax 1000.00 GBP ; Tax withheld
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Expenses:Lalit:UK:Google:National-Insurance 400.00 GBP ; NI contribution
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Expenses:Lalit:UK:Google:Pension 100.00 GBP ; Pension contribution
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Useful for analyzing my tax situation. But day-to-day, I don&amp;rsquo;t care about the breakdown. In net view, the same transaction collapses:&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;; chapter-5/journal-net.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;include &amp;#34;journal.beancount&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;plugin &amp;#34;beancount_reds_plugins.rename_accounts.rename_accounts&amp;#34; &amp;#34;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Income:Lalit:UK:Google:Salary&amp;#39;: &amp;#39;Income:Lalit:UK:Google:Net-Income&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Income:Lalit:UK:Google:Bonus&amp;#39;: &amp;#39;Income:Lalit:UK:Google:Net-Income&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Expenses:Lalit:UK:Google:Income-Tax&amp;#39;: &amp;#39;Income:Lalit:UK:Google:Net-Income&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Expenses:Lalit:UK:Google:National-Insurance&amp;#39;: &amp;#39;Income:Lalit:UK:Google:Net-Income&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Expenses:Lalit:UK:Google:Pension&amp;#39;: &amp;#39;Income:Lalit:UK:Google:Net-Income&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because Income is stored as a negative number and Expenses as positive numbers, merging them into one account mathematically subtracts the tax from the gross pay, leaving just the net amount. The difference should be obvious if I compare the Income Statements on Fava:&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/10-gross-view.png" alt="Gross Income Statement view showing all tax line items" title="Gross View"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The Gross view is essential for tax season, but the detailed line items for taxes and insurance often dwarf your actual spending data.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/11-net-view.png" alt="Net Income Statement view showing take-home pay" title="Net View"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The Net view collapses those details into a single take-home number, making your everyday expenses much easier to analyze.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id="tracking-capital-gains-for-tax"&gt;Tracking capital gains for tax&lt;/h4&gt;
&lt;p&gt;We can use this same renaming technique to handle a much more complex beast: &lt;strong&gt;Capital Gains Tax&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Your broker reports one gain number, but your tax authority may calculate another. In the UK, where I live, we have specific rules like &amp;ldquo;Section 104 pooling&amp;rdquo; (averaging cost basis) and &amp;ldquo;bed-and-breakfasting&amp;rdquo; (wash sale rules).&lt;/p&gt;
&lt;p&gt;To handle this, I use a virtual currency called &lt;code&gt;CGT-GBP&lt;/code&gt; that represents &amp;ldquo;pounds of gain HMRC cares about&amp;rdquo;. My plugin, &lt;a href="https://github.com/LalitMaganti/beancount-lalitm/blob/main/src/beancount_lalitm/plugins/uk_cgt_lots.py"&gt;&lt;code&gt;uk_cgt_lots&lt;/code&gt;&lt;/a&gt;, calculates this number and automatically appends a &lt;strong&gt;self-balancing pair of Equity postings&lt;/strong&gt; to the original sale transaction:&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;2024-06-15 * &amp;#34;SELL AAPL&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:US:IB:Brokerage:AAPL -10 AAPL {150.00 USD} @ 175.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:US:IB:Brokerage:USD 1750.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Income:Lalit:US:IB:Brokerage:AAPL:Capital-Gains -250.00 USD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ; The following postings are generated by the uk_cgt_lots plugin:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Equity:Taxable-Capital-Gains 195.00 CGT-GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Equity:Taxable-Capital-Gains-Placeholder -195.00 CGT-GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since both legs are in &lt;code&gt;Equity&lt;/code&gt;, they remain invisible on my Income Statement in my daily &amp;ldquo;Net&amp;rdquo; view. In fact, I use &lt;code&gt;rename_accounts&lt;/code&gt; to collapse them into a single account so they net to zero:&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;; journal-net.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;plugin &amp;#34;beancount_reds_plugins.rename_accounts.rename_accounts&amp;#34; &amp;#34;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Equity:Taxable-Capital-Gains-Placeholder&amp;#39; : &amp;#39;Equity:Taxable-Capital-Gains&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But when I want to see my tax liability, I switch to the &lt;strong&gt;CGT View&lt;/strong&gt;. This view renames the &amp;ldquo;Placeholder&amp;rdquo; to a visible Revenue account:&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;; journal-cgt.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;plugin &amp;#34;beancount_reds_plugins.rename_accounts.rename_accounts&amp;#34; &amp;#34;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Equity:Taxable-Capital-Gains-Placeholder&amp;#39; : &amp;#39;Revenues:Taxable-Capital-Gains&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, the -195.00 becomes Revenue, which shows up as profit on my tax report. The matching +195.00 remains in Equity. This allows me to have &amp;ldquo;Schrödinger&amp;rsquo;s Capital Gains&amp;rdquo;: they exist for the taxman, but not for my daily budget, all controlled by which view I load.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t calculate the tax owed since that&amp;rsquo;s too complicated with allowances, rates, and bands; the system just tracks the gains. At tax time, I sum up the CGT-GBP balance and do the actual calculation on the tax form.&lt;/p&gt;
&lt;h4 id="exit-ramp-3"&gt;Exit ramp&lt;/h4&gt;
&lt;p&gt;By separating your &amp;ldquo;source of truth&amp;rdquo; from your &amp;ldquo;lenses,&amp;rdquo; you get a system that grows with you. You can add new plugins or virtual currencies to solve specific problems (like taxes) without ever touching the raw transactions you&amp;rsquo;ve already imported.&lt;/p&gt;
&lt;p&gt;In the final chapter, we&amp;rsquo;ll see the ultimate application of this: combining two people&amp;rsquo;s financial lives into one unified view.&lt;/p&gt;
&lt;h2 id="chapter-6-two-people-one-number"&gt;Chapter 6: Two People, One Number&lt;/h2&gt;
&lt;p&gt;I got married at the start of the year, which brought a fundamental change to how I manage my finances. While many couples use joint accounts, we prefer to keep our individual accounts and perform occasional &amp;ldquo;normalization&amp;rdquo; transfers. However, we view our combined resources as shared household wealth.&lt;/p&gt;
&lt;p&gt;This created a reporting paradox that I had to solve in Beancount.&lt;/p&gt;
&lt;h4 id="the-paradox"&gt;The Paradox&lt;/h4&gt;
&lt;p&gt;When I transfer £500 to my wife for my share of the bills, two things are true simultaneously:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Individual Truth:&lt;/strong&gt; From my perspective, £500 is &amp;ldquo;gone&amp;rdquo; (an expense). From her perspective, £500 has &amp;ldquo;arrived&amp;rdquo; (income).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Household Truth:&lt;/strong&gt; For the household, the net worth hasn&amp;rsquo;t changed. Money just moved from the left pocket to the right pocket.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In a traditional system, you usually have to pick one truth. In Beancount, we can have both.&lt;/p&gt;
&lt;h4 id="the-composable-architecture"&gt;The Composable Architecture&lt;/h4&gt;
&lt;p&gt;To solve this, I treat the household as a composable system of three distinct entities: &lt;strong&gt;Me&lt;/strong&gt;, &lt;strong&gt;Her&lt;/strong&gt;, and &lt;strong&gt;Shared Definitions&lt;/strong&gt;. We use Beancount’s &lt;code&gt;include&lt;/code&gt; feature to build the specific &amp;ldquo;lens&amp;rdquo; we need at any given moment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lalit&amp;rsquo;s View&lt;/strong&gt; = Shared Definitions + Lalit&amp;rsquo;s Transactions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wife&amp;rsquo;s View&lt;/strong&gt; = Shared Definitions + Wife&amp;rsquo;s Transactions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Household View&lt;/strong&gt; = Shared Definitions + Lalit&amp;rsquo;s Transactions + Wife&amp;rsquo;s Transactions + &lt;em&gt;Translation Logic&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, the household view literally just includes the other files (alongside the translation logic we&amp;rsquo;ll see in a moment):&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;; chapter-6/total/journal-net.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;include &amp;#34;../common/src/commodities.beancount&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;include &amp;#34;../common/src/accounts.beancount&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;include &amp;#34;../lalit/src/journal.beancount&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;include &amp;#34;../wife/src/journal.beancount&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;; ... Translation Logic follows
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="implementation-the-directory-structure"&gt;Implementation: The Directory Structure&lt;/h4&gt;
&lt;p&gt;This architecture is reflected directly in the repository structure:&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;chapter-6/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── common/ # Shared configuration
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── src/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── accounts.beancount # Shared expense accounts (e.g. Expenses:Groceries)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── commodities.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── lalit/ # My stuff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── src/ # My ledger
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── data/ # My statement PDFs/CSVs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── wife/ # Wife&amp;#39;s stuff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── src/ # Her ledger
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── data/ # Her statement PDFs/CSVs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── total/ # Combined household view
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── journal-net.beancount # Entry point with Translation Logic
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="the-rules-of-engagement"&gt;The Rules of Engagement&lt;/h4&gt;
&lt;p&gt;For this to work without constant manual adjustment, we follow two simple rules:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rule 1: Assets and Liabilities are Private.&lt;/strong&gt;
Bank accounts always include the person&amp;rsquo;s name in the path (e.g., &lt;code&gt;Assets:Lalit:HSBC&lt;/code&gt; or &lt;code&gt;Assets:Wife:HSBC&lt;/code&gt;). We never use a generic &lt;code&gt;Assets:Checking&lt;/code&gt; account. Legal ownership of the cash always matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rule 2: Expenses are Public.&lt;/strong&gt;
Shared expenses like groceries or electricity use a generic name without a person prefix.&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;; chapter-6/common/src/accounts.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-01 open Expenses:Groceries GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When I buy groceries, I record it in my ledger using the shared account. We don&amp;rsquo;t track &amp;ldquo;who owes what&amp;rdquo; for individual grocery runs; we just track that the household spent the money. We accept that we lose the ability to split shared expenses by person, but the gain in simplicity is worth it.&lt;/p&gt;
&lt;h4 id="the-magic-solving-the-transfer-paradox"&gt;The &amp;ldquo;Magic&amp;rdquo;: Solving the Transfer Paradox&lt;/h4&gt;
&lt;p&gt;Finally, we use the &lt;code&gt;rename_accounts&lt;/code&gt; plugin in the &lt;code&gt;total/&lt;/code&gt; folder to resolve the transfer paradox.&lt;/p&gt;
&lt;p&gt;In my ledger, a transfer looks like a simple expense:&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;; chapter-6/lalit/src/transactions.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-20 * &amp;#34;Transfer to Wife&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Expenses:Lalit:Transfers:Wife 500.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Lalit:UK:HSBC:Current:GBP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In her ledger, it looks like income:&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;; chapter-6/wife/src/transactions.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024-01-20 * &amp;#34;Transfer from Lalit&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Assets:Wife:UK:HSBC:Current:GBP 500.00 GBP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Income:Wife:Transfers:Lalit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &amp;ldquo;Translation Logic&amp;rdquo; in the combined view renames these into a shared transit account:&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;; chapter-6/total/journal-net.beancount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;plugin &amp;#34;beancount_reds_plugins.rename_accounts.rename_accounts&amp;#34; &amp;#34;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Expenses:Lalit:Transfers:Wife&amp;#39;: &amp;#39;Assets:Household:Transfers:Internal&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;Income:Wife:Transfers:Lalit&amp;#39;: &amp;#39;Assets:Household:Transfers:Internal&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, when Fava loads the combined view, it sees £500 leave my account and enter &lt;code&gt;Assets:Household:Transfers:Internal&lt;/code&gt;, and then £500 leave that same account and enter her bank account. The transit account nets to zero, and our household net worth remains unchanged.&lt;/p&gt;
&lt;h4 id="the-result"&gt;The Result&lt;/h4&gt;
&lt;p&gt;This setup gives us the best of both worlds. I can maintain my own financial autonomy and see my personal &amp;ldquo;runway,&amp;rdquo; while we can simultaneously monitor our combined progress toward shared goals.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/one-number-i-trust/12-household-balance-sheet.png" alt="Combined Household Balance Sheet" title="Two people, one number"&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;em&gt;The final result: a unified household view that tracks legal ownership without sacrificing the "One Number" net worth total.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the full system (see &lt;a href="https://github.com/LalitMaganti/beancount-blog-examples/tree/main/chapter-6"&gt;chapter-6&lt;/a&gt; for the complete multi-person structure). But how do I actually use it week to week?&lt;/p&gt;
&lt;h2 id="the-weekly-ritual"&gt;The weekly ritual&lt;/h2&gt;
&lt;p&gt;Now here&amp;rsquo;s how I keep it current: the &amp;ldquo;20 minutes a week&amp;rdquo; I mentioned at the start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Getting statements&lt;/strong&gt;: During the week, banks email me saying a statement is available. Some attach PDFs directly; others require a login. Either way, I snooze the emails (I use inbox zero) until the weekend. For my wife&amp;rsquo;s accounts, I nudge her once a month and she drops stuff in a shared Drive folder.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Running imports&lt;/strong&gt;: On the weekend, I work through my snoozed emails. Note that I&amp;rsquo;m not updating every single account every week. I only download statements for the 3-4 accounts that saw activity; long-term investments often just get a monthly or quarterly check-in. I move each file to the correct &lt;code&gt;data/&lt;/code&gt; subfolder for that institution (e.g., &lt;code&gt;data/hsbc-uk-current/&lt;/code&gt;). My importer pipeline automatically runs &lt;code&gt;pdftotext&lt;/code&gt; to extract text so the parsers can read it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Categorizing&lt;/strong&gt;: I launch beancount-import, which opens a web UI in my browser:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python -m beancount_import.webserver --journal lalit/journal-net.beancount
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As I mentioned before, most transactions auto-categorize and I&amp;rsquo;m just hitting Enter: I buy groceries from the same place, pay my hosting costs to the same provider etc. New merchants do need some manual work, but it&amp;rsquo;s a matter of typing a few characters and again pressing Enter.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Formatting and checking&lt;/strong&gt;: I run &lt;code&gt;bean-format&lt;/code&gt; (a Beancount utility that normalizes indentation and aligns amounts) to keep my transactions file tidy. This makes git diffs cleaner. Then I open Fava for a quick sanity check:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fava lalit/journal-net.beancount
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Are transfer accounts zeroed out? Do expenses look legit? How are investments doing? If it all checks out, commit and push.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s it. I try to stick to doing this &lt;em&gt;every week&lt;/em&gt;, but sometimes I&amp;rsquo;m on holiday or just have other commitments. In that case, it&amp;rsquo;s 40 minutes every two weeks. The system is forgiving; I&amp;rsquo;m never behind for too long.&lt;/p&gt;
&lt;p&gt;I also have some automation helping me out: during the week, I have a CI workflow that runs daily, regenerates summaries, and commits them. Whenever I want, I can check the repo and see what the numbers look like. I particularly like this because I can easily see the before/after numbers in a single file, so I can spot check &amp;ldquo;does this make sense&amp;rdquo;. And of course at any time, I can also open Fava if I want to go a bit deeper.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That 2021 tax disaster feels like a lifetime ago. What started as &amp;ldquo;there must be a better way&amp;rdquo; became a system I actually trust. One number, always current, completely under my control.&lt;/p&gt;
&lt;p&gt;The specifics will evolve as life changes and tools improve, but three principles have held for years now. I expect them to hold for decades:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Double-entry everywhere.&lt;/strong&gt; Every transaction balances. Money never appears from nowhere or vanishes into nothing. When something doesn&amp;rsquo;t add up, you know immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plain text as the source of truth.&lt;/strong&gt; Your financial history lives in files you can read, diff, grep, and version control. No vendor lock-in, no opaque databases, no trusting a third party with your data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Track at the leaf.&lt;/strong&gt; Record transactions at the most granular level that makes sense. You can always aggregate up (&lt;code&gt;Income:Dividends&lt;/code&gt; from &lt;code&gt;Income:IB:AAPL:Dividends&lt;/code&gt;), but you can never disaggregate down. Capture the detail; collapse it later with views.&lt;/p&gt;
&lt;p&gt;Finance systems are deeply personal. This post isn&amp;rsquo;t meant to say this is &lt;em&gt;the&lt;/em&gt; system everyone should use, just what&amp;rsquo;s worked for me over several years.&lt;/p&gt;
&lt;p&gt;Each chapter here could be its own post, so if you want me to go deeper on imports, investments, or the multi-person setup, let me know!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;It took me dozens of hours spread over months. This post and companion repo should get you there faster.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;By &amp;ldquo;normal&amp;rdquo; I mean any personal finance system which works on the basis of just plain transactions and &amp;ldquo;tagging&amp;rdquo; or &amp;ldquo;categorizing them&amp;rdquo;. This includes spreadsheets and apps like Mint, Quicken, or Copilot.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Since 1494, when Luca Pacioli first codified the method in &lt;a href="https://en.wikipedia.org/wiki/Summa_de_arithmetica"&gt;&lt;em&gt;Summa de arithmetica&lt;/em&gt;&lt;/a&gt;. The practice existed among Italian merchants before that, but Pacioli was the first to publish it.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Real accountants use &amp;ldquo;Debits&amp;rdquo; and &amp;ldquo;Credits&amp;rdquo; to keep numbers positive. Beancount just uses plus and minus. It&amp;rsquo;s way more intuitive for coders, even if it makes accountants twitch.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;Yes I know technically that Equity means &amp;lsquo;Ownership&amp;rsquo;. But I&amp;rsquo;m trying to keep things simple for an audience new to this stuff, and equity is confusing enough in this simplified model without the abstract ideas which come from business bookkeeping.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;As I say, I initially just had an &amp;ldquo;Equity:Transfers&amp;rdquo; account and I put 3 different &amp;ldquo;untracked accounts&amp;rdquo; in that same bucket. When I finally imported these, I had to go through and recategorize all these transfers manually! Not the most fun day I&amp;rsquo;ve had&amp;hellip;&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;Trust me, I tried for years to maintain Puppeteer scrapers for all my bank statements. It became a second job to keep those up to date and eventually I started dreading my finances because of it. The day I deleted all that code, I felt a real burden had been lifted.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:8"&gt;
&lt;p&gt;Plaid also has privacy tradeoffs. Banks with OAuth support let you authenticate directly, but many still require you to &lt;a href="https://support-my.plaid.com/hc/en-us/articles/8117349507095-What-are-the-different-ways-third-party-apps-using-Plaid-can-connect-to-my-financial-accounts"&gt;hand Plaid your credentials&lt;/a&gt;, which they store and use to access your data.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:9"&gt;
&lt;p&gt;The original beancount-import doesn&amp;rsquo;t support Beancount v3. I maintain a &lt;a href="https://github.com/LalitMaganti/beancount-import"&gt;fork&lt;/a&gt; with v3 compatibility.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:10"&gt;
&lt;p&gt;The &lt;code&gt;beangrow&lt;/code&gt; plugin (used for performance reporting) specifically requires a &lt;code&gt;name&lt;/code&gt; metadata attribute on each commodity. For convenience, I typically set this to the symbol itself, but it can be any descriptive string like &amp;ldquo;Apple Inc.&amp;rdquo; or &amp;ldquo;Vanguard All-World ETF&amp;rdquo;.&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:11"&gt;
&lt;p&gt;US readers will be familiar with tracking individual lots instead. The principle of needing a tax-specific view applies regardless of jurisdiction.&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>Why I Ignore The Spotlight as a Staff Engineer</title><link>https://lalitm.com/software-engineering-outside-the-spotlight/</link><pubDate>Thu, 04 Dec 2025 00:15:00 +0000</pubDate><guid>https://lalitm.com/software-engineering-outside-the-spotlight/</guid><description>Lately I’ve been reading Sean Goedecke’s essays on being a Staff+ engineer. His work (particularly Software engineering under the spotlight and It’s Not Your Codebase) is razor-sharp and feels painfully familiar to anyone in Big Tech.
On paper, I fit the mold he describes: I’m a Senior Staff engineer at Google. Yet, reading his work left me with a lingering sense of unease. At first, I dismissed this as cynicism. After reflecting, however, I realized the problem wasn’t Sean’s writing but my reading.
Sean isn’t being bleak; he is accurately describing how to deal with a world where engineers are fungible assets and priorities shift quarterly. But my job looks nothing like that and I know deep down that if I tried to operate in that environment or in the way he described I’d burn out within months.
Instead I’ve followed an alternate path, one that optimizes for systems over spotlights, and stewardship over fungibility.</description><content:encoded>&lt;p&gt;Lately I’ve been reading &lt;a href="https://www.seangoedecke.com/"&gt;Sean Goedecke’s&lt;/a&gt; essays on being a Staff+ engineer. His work (particularly &lt;a href="https://www.seangoedecke.com/the-spotlight/"&gt;Software engineering under the spotlight&lt;/a&gt; and &lt;a href="https://www.seangoedecke.com/not-your-codebase/"&gt;It&amp;rsquo;s Not Your Codebase&lt;/a&gt;) is razor-sharp and feels painfully familiar to anyone in Big Tech.&lt;/p&gt;
&lt;p&gt;On paper, I fit the mold he describes: I&amp;rsquo;m a Senior Staff engineer at Google. Yet, reading his work left me with a lingering sense of unease. At first, I dismissed this as cynicism. After reflecting, however, I realized the problem wasn’t Sean’s writing but my reading.&lt;/p&gt;
&lt;p&gt;Sean isn&amp;rsquo;t being bleak; he is accurately describing how to deal with a world where engineers are fungible assets and priorities shift quarterly. But my job looks nothing like that and I know deep down that if I tried to operate in that environment or in the way he described I’d burn out &lt;strong&gt;within months&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead I&amp;rsquo;ve followed an alternate path, one that optimizes for &lt;strong&gt;systems over spotlights&lt;/strong&gt;, and &lt;strong&gt;stewardship over fungibility&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="we-live-in-different-worlds"&gt;We Live in Different Worlds&lt;/h2&gt;
&lt;p&gt;The foundational reason for our diverging paths is that Sean and I operate in entirely different worlds with different laws governing them.&lt;/p&gt;
&lt;p&gt;From &lt;a href="https://www.seangoedecke.com/about"&gt;Sean&amp;rsquo;s resume&lt;/a&gt;, my understanding is that he has primarily worked in &lt;strong&gt;product&lt;/strong&gt; teams &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; building for external customers. Business goals pivot quarterly, and success is measured by revenue or MAU. Optimizing for the &amp;ldquo;Spotlight&amp;rdquo; makes complete sense in this environment. Product development at big tech scale is a crowded room: VPs, PMs and UX designers all have strong opinions. To succeed, you &lt;em&gt;have&lt;/em&gt; to be agile and ensure you are working specifically on what executives are currently looking at.&lt;/p&gt;
&lt;p&gt;On the other hand, I&amp;rsquo;ve spent my entire career much more behind the scenes: in &lt;strong&gt;developer tools and infra&lt;/strong&gt; teams.&lt;/p&gt;
&lt;p&gt;My team&amp;rsquo;s customers are thousands of engineers in Android, Chrome, and throughout Google &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. End users of Google products don&amp;rsquo;t even know we exist; our focus is on making sure developers have the tools to collect product and performance metrics and debug issues using detailed traces.&lt;/p&gt;
&lt;p&gt;In this environment, our relationship with leadership is very different. We&amp;rsquo;re never the &amp;ldquo;hot project everyone wants,&amp;rdquo; so execs are not fighting to work with us. In fact, my team has historically struggled to hire PMs. The PM career ladder at Google incentivizes splashy external launches so we cannot provide good &amp;ldquo;promotion material&amp;rdquo; for them. Also, our feedback comes directly from engineers. Adding a PM in the middle causes a loss in translation, slowing down a tight, high-bandwidth feedback loop.&lt;/p&gt;
&lt;p&gt;All of this together means our team operates &amp;ldquo;bottom-up&amp;rdquo;: instead of execs telling us &amp;ldquo;you should do X&amp;rdquo;, we figure out what we think will have the most impact to our customers and work on building those features and tools. Execs ensure that we&amp;rsquo;re &lt;em&gt;actually&lt;/em&gt; solving these problems by considering our impact on more product facing teams.&lt;/p&gt;
&lt;h2 id="compounding-returns-of-stewardship"&gt;Compounding Returns of Stewardship&lt;/h2&gt;
&lt;p&gt;In the product environments Sean describes, where goals pivot quarterly and features are often experimental, &lt;strong&gt;speed&lt;/strong&gt; is the ultimate currency. You need to ship, iterate, and often move on before the market shifts. But in Infrastructure and Developer Experience, &lt;strong&gt;context&lt;/strong&gt; is the currency.&lt;/p&gt;
&lt;p&gt;Treating engineers as fungible assets destroys context. You might gain fresh eyes, but you lose the implicit knowledge of how systems actually break. Stewardship, staying with a system long-term, unlocks compounding returns that are impossible to achieve on a short rotation.&lt;/p&gt;
&lt;p&gt;The first is efficiency via &lt;strong&gt;pattern matching&lt;/strong&gt;. When you stay in one domain for years, new requests are rarely truly &amp;ldquo;new.&amp;rdquo; I am not just debugging code; I am debugging the intersection of my tools and hundreds of diverse engineering teams. When a new team comes to me with a &amp;ldquo;unique&amp;rdquo; problem, I can often reach back in time: &lt;em&gt;“We tried this approach in 2021 with the Camera team; here is exactly why it failed, and here is the architecture that actually works”.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But the more powerful return is &lt;strong&gt;systemic innovation&lt;/strong&gt;. If you rotate teams every year, you are limited to solving acute bugs that are visible &lt;em&gt;right now&lt;/em&gt;. Some problems, however, only reveal their shape over long horizons.&lt;/p&gt;
&lt;p&gt;Take &lt;strong&gt;Bigtrace&lt;/strong&gt;, a project I recently led; it was a solution that emerged solely because I stuck around long enough to see the shape of the problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start of 2023 (Observation):&lt;/strong&gt; I began noticing a pattern. Teams across Google were collecting terabytes or even petabytes of performance traces, but they were struggling to process them. Engineers were writing brittle, custom pipelines to parse data, often complaining about how slow and painful it was to iterate on their analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Most of 2023 (Research):&lt;/strong&gt; I didn&amp;rsquo;t jump to build a production system. Instead, I spent the best part of a year prototyping quietly in the background while working on other projects. I gathered feedback from these same engineers who had complained and because I had established long-term relationships, they gave me honest and introspective feedback. I learned what sort of UX, latency and throughput requirements they had and figured out how I could meet them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End of 2023 to Start of 2024 (Execution):&lt;/strong&gt; We built and launched Bigtrace, a distributed big data query engine for traces. Today, it processes over &lt;strong&gt;2 billion traces a month&lt;/strong&gt; and is a critical part of the daily workflow for 100+ engineers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I had followed the advice to &amp;ldquo;optimize for fungibility&amp;rdquo; (i.e. if I had switched teams in 2023 to chase a new project) &lt;strong&gt;Bigtrace would not exist.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead, I would have left during the research phase and my successor would have seen the same &amp;ldquo;noise&amp;rdquo; of engineers complaining. But without the historical context to recognize a missing puzzle piece, I think they would have struggled to build something like Bigtrace.&lt;/p&gt;
&lt;h2 id="the-power-of-no"&gt;The Power of &amp;ldquo;No&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;One of the most seductive arguments for chasing the &amp;ldquo;Spotlight&amp;rdquo; is that it guarantees resources and executive attention. But that attention is a double-edged sword.&lt;/p&gt;
&lt;p&gt;High-visibility projects are often volatile. They come with shifting executive whims, political maneuvering, and often end up in situations where long-term quality is sacrificed for short-term survival. For some engineers, navigating this chaos is a thrill. For those of us who care about system stability, it feels like a trap.&lt;/p&gt;
&lt;p&gt;The advantage of stewardship is that it generates a different kind of capital: &lt;strong&gt;trust&lt;/strong&gt;. When you have spent years delivering reliable tools, you earn the political capital to say &amp;ldquo;No&amp;rdquo; to the spotlight when it threatens the product.&lt;/p&gt;
&lt;p&gt;Recently, the spotlight has been on AI. Every team is under pressure to incorporate it. We have been asked repeatedly: &lt;em&gt;&amp;ldquo;Why don&amp;rsquo;t you integrate LLMs into Perfetto?&amp;rdquo;&lt;/em&gt; If I were optimizing for visibility, the answer would be obvious: build an LLM wrapper, demo it to leadership, and claim we are &amp;ldquo;AI-first.&amp;rdquo; It would be an easy win for my career.&lt;/p&gt;
&lt;p&gt;But as a steward of the system, I know that one of Perfetto’s core values is &lt;strong&gt;precision&lt;/strong&gt;. When a kernel developer is debugging a race condition, they need exact timestamps, not a hallucination. Users trust that when we tell them &amp;ldquo;X is the problem&amp;rdquo; that it actually &lt;em&gt;is&lt;/em&gt; the problem and they&amp;rsquo;re not going to go chasing their tail for the next week, debugging an issue which doesn&amp;rsquo;t exist.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s important not to take this too far: skepticism shouldn&amp;rsquo;t become obstructionism. With AI, it&amp;rsquo;s not &amp;ldquo;no forever&amp;rdquo; but &amp;ldquo;not until it can be done right&amp;rdquo; &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;A spotlight-seeking engineer might view this approach as a missed opportunity; I view it as protecting what makes our product great: user trust.&lt;/p&gt;
&lt;h2 id="the-alternate-currency-of-impact"&gt;The Alternate Currency of Impact&lt;/h2&gt;
&lt;p&gt;The most common fear engineers have about leaving the &amp;ldquo;Spotlight&amp;rdquo; is career stagnation. The logic goes: &lt;em&gt;If I’m not launching flashy features at Google I/O, and my work isn&amp;rsquo;t on my VP&amp;rsquo;s top 5 list, how will I ever get promoted to Staff+?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It is true that you lose the currency of &amp;ldquo;Executive Visibility.&amp;rdquo; But in infrastructure, you gain two alternate currencies that are just as valuable, and potentially more stable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shadow Hierarchy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In a product organization, you often need to impress your manager’s manager. In an infrastructure organization, you need to impress &lt;strong&gt;your customers’ managers&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I call this the &lt;strong&gt;Shadow Hierarchy.&lt;/strong&gt; You don&amp;rsquo;t need &lt;em&gt;your&lt;/em&gt; VP to understand the intricacies of your code. You need the Staff+ Engineers in &lt;em&gt;other&lt;/em&gt; critical organizations to &lt;strong&gt;need&lt;/strong&gt; your tools.&lt;/p&gt;
&lt;p&gt;When a Senior Staff Engineer in Pixel tells their VP, &lt;em&gt;&amp;ldquo;We literally cannot debug the next Pixel phone without Perfetto&amp;rdquo;&lt;/em&gt;, that statement carries immense weight. It travels up their reporting chain, crosses over at the Director/VP level, and comes back down to your manager.&lt;/p&gt;
&lt;p&gt;This kind of advocacy is powerful because it is technical, not political. It is hard to fake. When you are a steward of a critical system, your promotion packet is filled with testimonials from the most respected engineers in the company saying, &lt;em&gt;&amp;ldquo;This person&amp;rsquo;s work enabled our success&amp;rdquo;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Utility Ledger&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;While product teams might be poring over daily active users or revenue, we rely on metrics tracking &lt;strong&gt;engineering health&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Utility:&lt;/strong&gt; Every bug fixed using our tools is an engineer finding us useful. It is the purest measure of utility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Criticality:&lt;/strong&gt; If the Pixel team uses Perfetto to debug a launch-blocking stutter, or Chrome uses it to fix a memory leak, our impact is implicitly tied to their success.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ubiquity:&lt;/strong&gt; Capturing a significant percentage of the engineering population proves you&amp;rsquo;ve created a technical &amp;ldquo;lingua franca&amp;rdquo;. This becomes especially obvious when you see disconnected parts of the company collaborating with each other, using shared Perfetto traces as a &amp;ldquo;reference everyone understands&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scale:&lt;/strong&gt; Ingesting petabytes of data or processing billions of traces proves architectural resilience better than any design doc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you combine &lt;strong&gt;Criticality&lt;/strong&gt; (VIP teams need this) with &lt;strong&gt;Utility&lt;/strong&gt; (bugs are being fixed), you create a promotion case that is immune to executive reorganizations.&lt;/p&gt;
&lt;h2 id="archetypes-and-agency"&gt;Archetypes and Agency&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Staff Archetypes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I am far from the first to notice the idea of &amp;ldquo;there are multiple ways to be a staff software engineer&amp;rdquo;. In his book &lt;em&gt;&lt;a href="https://staffeng.com/guides/staff-archetypes/"&gt;Staff Engineer&lt;/a&gt;&lt;/em&gt;, Will Larson categorizes Staff-plus engineers into four distinct archetypes.&lt;/p&gt;
&lt;p&gt;Sean describes the &lt;strong&gt;Solver&lt;/strong&gt; or the &lt;strong&gt;Right Hand&lt;/strong&gt;: engineers who act as agents of executive will, dropping into fires and moving on once the problem is stabilized. I am describing the &lt;strong&gt;Architect&lt;/strong&gt; or the &lt;strong&gt;Tech Lead&lt;/strong&gt;: roles defined by long-term ownership of a specific domain and deep technical context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &amp;ldquo;Luck&amp;rdquo; Rebuttal&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I can hear the criticism already: &lt;em&gt;&amp;ldquo;You just got lucky finding your team. Most of us don&amp;rsquo;t have that luxury.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There are two caveats to all my advice in this post. First, the strategy I have employed so far requires a company profitable enough to sustain long-term infrastructure. This path generally does not exist in startups or early growth companies; it is optimized for Big Tech.&lt;/p&gt;
&lt;p&gt;Second, luck &lt;em&gt;does&lt;/em&gt; play a role in landing on a good team. It is very hard to accurately evaluate team and company culture from the outside. But while finding the team might have involved luck, staying there for almost a decade was a &lt;strong&gt;choice&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And, at least in my experience, my team is not particularly special: I can name five other teams in Android alone &lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;. Sure, they might have a director change here or a VP change there, but the core mission and the engineering team remained stable.&lt;/p&gt;
&lt;p&gt;The reason these teams seem rare is not that they don&amp;rsquo;t exist, but that they are often ignored. Because they don&amp;rsquo;t offer the rapid, visible &amp;ldquo;wins&amp;rdquo; of a product launch nor are they working on the &amp;ldquo;shiny cool features&amp;rdquo;, they attract less competition. If you are motivated by &amp;ldquo;shipping to billions of users&amp;rdquo; or seeing your friends and family use something you built, you won&amp;rsquo;t find that satisfaction here. That is the price of admission.&lt;/p&gt;
&lt;p&gt;But if you want to build long-term systems and are willing to trade external validation for deep technical ownership, you just need to look behind the curtain.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The tech industry loves to tell you to move fast. But there is another path. It is a path where leverage comes from depth, patience, and the quiet satisfaction of building the foundation that others stand on.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t have to chase the spotlight to have a meaningful, high-impact career at a big company. Sometimes, the most ambitious thing you can do is stay put, dig in, and build something that lasts. To sit with a problem space for years until you understand it well enough to build a Bigtrace.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;By product team I &lt;em&gt;don&amp;rsquo;t&lt;/em&gt; mean &amp;ldquo;frontend team&amp;rdquo;: even as a backend engineer, you are still working on some part of what is being served directly to end users.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;This is not exhaustive, &lt;a href="https://docs.perfetto.dev"&gt;Perfetto&lt;/a&gt; is open source and we &lt;em&gt;do&lt;/em&gt; also care about external developers but that&amp;rsquo;s &lt;em&gt;not&lt;/em&gt; why we get paid. From the company perspective, time we spent on open source bugs is &amp;ldquo;wasted&amp;rdquo; time but we do it because we believe in the mission of open source. I talked about this more in a recent post, &lt;a href="/perfetto-oss-company-prio/"&gt;On Perfetto, Open Source, and Company Priorities&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;For what it&amp;rsquo;s worth, LLMs might not even be the best solution to &amp;ldquo;let&amp;rsquo;s put AI into Perfetto&amp;rdquo;: in my opinion there is lots of value with &amp;ldquo;old school&amp;rdquo; machine learning techniques like neural networks. A lot of trace analysis is just pattern matching. This is something I&amp;rsquo;m hoping to explore more in the coming year!&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Android Kernel, Android System Health, Android Runtime, Android Camera HAL, Android Bionic&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>We stopped roadmap work for a week and fixed 189 bugs</title><link>https://lalitm.com/fixits-are-good-for-the-soul/</link><pubDate>Sat, 22 Nov 2025 18:20:00 +0000</pubDate><guid>https://lalitm.com/fixits-are-good-for-the-soul/</guid><description>It’s Friday at 4pm. I’ve just closed my 12th bug of the week. My brain is completely fried. And I’m staring at the bug leaderboard, genuinely sad that Monday means going back to regular work. Which is weird because I love regular work. But fixit weeks have a special place in my heart.
What’s a fixit, you ask? Once a quarter or so, my org with ~45 software engineers stops all regular work for a week. That means no roadmap work, no design work, no meetings or standups.
Instead, we fix the small things that have been annoying us and our users:
an error message that’s been unclear for two years a weird glitch when the user scrolls and zooms at the same time a test which runs slower than it should, slowing down CI for everyone The rules are simple: 1) no bug should take over 2 days and 2) all work should focus on either small end-user bugs/features or developer productivity.</description><content:encoded>&lt;p&gt;It&amp;rsquo;s Friday at 4pm. I&amp;rsquo;ve just closed my 12th bug of the week. My brain is completely fried. And I&amp;rsquo;m staring at the bug leaderboard, genuinely sad that Monday means going back to regular work. Which is weird because I &lt;strong&gt;love&lt;/strong&gt; regular work. But fixit weeks have a special place in my heart.&lt;/p&gt;
&lt;h2 id="whats-a-fixit-you-ask"&gt;What’s a fixit, you ask?&lt;/h2&gt;
&lt;p&gt;Once a quarter or so, my org with ~45 software engineers stops all regular work for a week. That means no roadmap work, no design work, no meetings or standups.&lt;/p&gt;
&lt;p&gt;Instead, we fix the small things that have been annoying us and our users:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an error message that&amp;rsquo;s been unclear for two years&lt;/li&gt;
&lt;li&gt;a weird glitch when the user scrolls and zooms at the same time&lt;/li&gt;
&lt;li&gt;a test which runs slower than it should, slowing down CI for everyone&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rules are simple: 1) no bug should take over 2 days and 2) all work should focus on either small end-user bugs/features or developer productivity.&lt;/p&gt;
&lt;p&gt;We also have a &amp;ldquo;points system&amp;rdquo; for bugs and a leaderboard showing how many points people have. And there’s a promise of t-shirts for various achievements: first bug fix, most points, most annoying bug, etc. It’s a simple structure, but it works surprisingly well.&lt;/p&gt;
&lt;h2 id="what-we-achieved"&gt;What we achieved&lt;/h2&gt;
&lt;p&gt;Some stats from this fixit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;189 bugs fixed&lt;/li&gt;
&lt;li&gt;40 people participated&lt;/li&gt;
&lt;li&gt;4 was the median number of bugs closed per person&lt;/li&gt;
&lt;li&gt;12 was maximum number of bugs closed by one person&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/5g47sxjucyfgvvp.png" alt="fixit burndown chart"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Bug Burndown Chart for the Q4'25 Fixit&lt;/center&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here are some of the highlights (sadly many people in my org work in internal-facing things so I cannot share their work!):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I closed a &lt;a href="https://github.com/google/perfetto/issues/154"&gt;feature request&lt;/a&gt; from 2021! It&amp;rsquo;s a classic fixit issue: a small improvement that never bubbled to the priority list. It took me &lt;strong&gt;one day&lt;/strong&gt; to implement. One day for something that sat there for &lt;strong&gt;four years&lt;/strong&gt;. And it&amp;rsquo;s going to provide a small but significant boost to every user&amp;rsquo;s experience of Perfetto.&lt;/li&gt;
&lt;li&gt;My colleague made this &lt;a href="https://github.com/google/perfetto/pull/3736"&gt;small change&lt;/a&gt; to improve team productivity. Just ~25 lines of code in a GitHub Action to avoid every UI developer taking two extra clicks to open the CI&amp;rsquo;s build. The response from the team speaks for itself:&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/4bd7ammdnpkiwbq.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;Such a simple change but the team loved it!&lt;/i&gt;&lt;/center&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I also fixed &lt;a href="https://github.com/google/perfetto/issues/713"&gt;this&lt;/a&gt; issue to provide a new &amp;ldquo;amalgamated&amp;rdquo; version of our SDK, allowing it to be easily integrated into projects. It’s one of those things that might be the difference between someone deciding to use us or not, but building it took just one hour of work (with liberal use of AI!).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-benefits-of-fixits"&gt;The benefits of fixits&lt;/h2&gt;
&lt;h3 id="for-the-product-craftsmanship-and-care"&gt;For the product: craftsmanship and care&lt;/h3&gt;
&lt;p&gt;I care deeply about any product I work on. That means asking big questions like &amp;ldquo;what should we build?&amp;rdquo; and &amp;ldquo;how do we make this fast?&amp;rdquo; But it also means asking smaller questions: &amp;ldquo;is this error message actually helpful?&amp;rdquo; or &amp;ldquo;would I be frustrated using this?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;A hallmark of any good product is attention to detail: a sense that someone has thought things through, and the pieces fit together to make a cohesive whole. And the opposite is true: a product with rough edges might be tolerated if there are no alternatives, but there will always be a sense of frustration and &amp;ldquo;I wish I could use something else&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Fixits are a great chance to work on exactly those details that separate good products from great ones. The small things your average user might not consciously notice, but absolutely will notice if they&amp;rsquo;re wrong.&lt;/p&gt;
&lt;h3 id="for-the-individual-doing-not-thinking"&gt;For the individual: doing, not thinking&lt;/h3&gt;
&lt;p&gt;I sometimes miss the feeling I had earlier in my career when I got to just fix things. See something broken, fix it, ship it the same day.&lt;/p&gt;
&lt;p&gt;The more senior you get in a big company, the less you do that. Most of your time becomes thinking about what to build next, planning quarters ahead, navigating tradeoffs and getting alignment.&lt;/p&gt;
&lt;p&gt;Fixits give me that early-career feeling back. You see the bug, you fix it, you ship it, you close it, you move on. There&amp;rsquo;s something deeply satisfying about work where the question isn&amp;rsquo;t &amp;ldquo;what should we do?&amp;rdquo; but rather &amp;ldquo;can I make this better?&amp;rdquo; And you get to answer that question multiple times in a week.&lt;/p&gt;
&lt;h3 id="for-the-team-morale-and-spirit"&gt;For the team: morale and spirit&lt;/h3&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/3vlng29gerbxakc.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;People sharing live updates in the Fixit chatroom&lt;/em&gt;&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;Having 40 people across two time zones all fixing bugs together adds a whole other dimension.&lt;/p&gt;
&lt;p&gt;The vibe of the office is different: normally we&amp;rsquo;re all heads-down on different projects, but during fixit the team spirit comes out strong. People share their bug fixes in chat rooms, post before-and-after screenshots and gather around monitors to demo a new feature or complain about a particularly nasty bug they&amp;rsquo;re wrestling.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/9wcbsgotrjqvfdz.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The daily update from Friday&lt;/center&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The leaderboard amplifies this energy. There&amp;rsquo;s a friendly sense of competition as people try and balance quick wins with meatier bugs they can share stories about.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a short update every morning about how the previous day went:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;total bugs fixed&lt;/li&gt;
&lt;li&gt;how many people have fixed at least one bug&lt;/li&gt;
&lt;li&gt;how many different products we&amp;rsquo;ve fixed things in&lt;/li&gt;
&lt;li&gt;who’s currently at the top of the leaderboard&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this creates real momentum, and people feel magnetically pulled into the effort.&lt;/p&gt;
&lt;h2 id="how-to-run-a-fixit"&gt;How to run a fixit&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve participated in 6 fixits over the years and I&amp;rsquo;ve learned a lot about what makes them successful. Here are a few things that matter more than you’d think.&lt;/p&gt;
&lt;h3 id="preparation-is-key"&gt;Preparation is key&lt;/h3&gt;
&lt;p&gt;Most of what makes a fixit work happens before the week even starts.&lt;/p&gt;
&lt;p&gt;All year round, we encourage everyone to tag bugs as &amp;ldquo;good fixit candidates&amp;rdquo; as they encounter them. Then the week before fixit, each subteam goes through these bugs and sizes them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;small (less than half a day)&lt;/li&gt;
&lt;li&gt;medium (less than a day)&lt;/li&gt;
&lt;li&gt;large (less than 2 days).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They assign points accordingly: 1, 2, or 4.&lt;/p&gt;
&lt;p&gt;We also create a shortlist of high-priority bugs we really want fixed. People start there and move to the full list once those are done. This pre-work is critical: it prevents wasting day one with people aimlessly searching for bugs to fix.&lt;/p&gt;
&lt;h3 id="the-2-day-hard-limit"&gt;The 2-day hard limit&lt;/h3&gt;
&lt;p&gt;In one of our early fixits, someone picked up what looked like a straightforward bug. It should have been a few hours, maybe half a day. But it turned into a rabbit hole. Dependencies on other systems, unexpected edge cases, code that hadn&amp;rsquo;t been touched in years.&lt;/p&gt;
&lt;p&gt;They spent the entire fixit week on it. And then the entire week after fixit trying to finish it. What started as a bug fix turned into a mini project.
The work was valuable! But they missed the whole point of a fixit. No closing bugs throughout the week. No momentum. No dopamine hits from shipping fixes. Just one long slog.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why we have the 2-day hard limit now. If something is ballooning, cut your losses. File a proper bug, move it to the backlog, pick something else. The limit isn&amp;rsquo;t about the work being worthless - it&amp;rsquo;s about keeping fixit feeling like fixit.&lt;/p&gt;
&lt;h3 id="number-of-people-matters"&gt;Number of people matters&lt;/h3&gt;
&lt;p&gt;We didn’t always do fixits with 40 people. Early on, this wasn’t an org-wide effort, just my subteam of 7 people. It worked okay: bugs got fixed and there was a sense of pride in making the product better. But it felt a bit hollow: in the bigger picture of our org, it didn’t feel like anyone else noticed or cared.&lt;/p&gt;
&lt;p&gt;At ~40 people, it feels like a critical mass that changes things significantly. The magic number is probably somewhere between 7 and 40. And it probably varies based on the team. But whatever the number is, the collective energy matters. If you’re trying this with 5 people, it might still be worth doing, but it probably won’t feel the same.&lt;/p&gt;
&lt;h3 id="gamification"&gt;Gamification&lt;/h3&gt;
&lt;p&gt;The points and leaderboard are more than a gimmick, but they have to be handled carefully.&lt;/p&gt;
&lt;p&gt;What works for us:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Points are coarse, not precise&lt;/strong&gt;: We deliberately use 1/2/4 points instead of trying to measure exact effort; the goal is “roughly right and fun”, not accurate performance evaluation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Celebrate breadth, not just volume.&lt;/strong&gt; We give t-shirts for things like “first bug fix”, “most annoying bug fixed”, not just “most points”. That keeps newer or less experienced engineers engaged.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visibility over prizes.&lt;/strong&gt; A shout-out in the daily update or an internal post often matters more than the actual t-shirt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No attachment to perf reviews&lt;/strong&gt;. This is important: fixit scores do &lt;strong&gt;not&lt;/strong&gt; feed into performance reviews. The moment they do, people will start gaming it and the good vibe will die.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ve had very little “gaming” in practice. Social norms do a good job of keeping people honest and 40 is still small enough that there&amp;rsquo;s a sense of &amp;ldquo;loyalty to the cause&amp;rdquo; from folks.&lt;/p&gt;
&lt;h2 id="the-ai-factor"&gt;The AI factor&lt;/h2&gt;
&lt;p&gt;The big challenge with fixits is context switching. Constantly changing what you&amp;rsquo;re working on means constantly figuring out new parts of the codebase, thinking about new problems.&lt;/p&gt;
&lt;p&gt;AI tools have mitigated this in a big way. The code they write is less important than their ability to quickly search through relevant files and summarize what needs to change. They might be right or wrong, but having that starting point really reduces the cognitive load. And sometimes (rarely) they one-shot a fix.&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://github.com/google/perfetto/pull/3774"&gt;docs change&lt;/a&gt; was a perfect example of the above: an update to our docs which catches out new contributors and AI was able to one-shot the fix.&lt;/p&gt;
&lt;p&gt;On the other hand, in my &lt;a href="https://github.com/google/perfetto/pull/3780"&gt;record page change&lt;/a&gt; it was more useful for giving me prototypes of what the code should look like and I had to put in significant effort to correct the bad UX it generated and its tendency to &amp;ldquo;over-generate&amp;rdquo; code. Even so, it got me to the starting line much faster.&lt;/p&gt;
&lt;h2 id="criticisms-of-fixits-and-why-i-still-like-them-anyway"&gt;Criticisms of fixits (and why I still like them anyway)&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve definitely come across people who question whether fixits are actually a good idea. Some of the criticisms are fair but overall I still think it’s worth it.&lt;/p&gt;
&lt;h3 id="isnt-this-just-admitting-you-ignore-bugs-the-rest-of-the-time"&gt;“Isn’t this just admitting you ignore bugs the rest of the time?”&lt;/h3&gt;
&lt;p&gt;To some extent, yes, this is an admission of the fact that &amp;ldquo;papercut&amp;rdquo; bugs are underweighted in importance, both by managers and engineers. It&amp;rsquo;s all too easy to tunnel on making sure a big project is successful and easier to ignore the small annoyances for users and the team.&lt;/p&gt;
&lt;p&gt;Fixits are a way of counterbalancing that somewhat and saying &amp;ldquo;actually those bugs matter too&amp;rdquo;. That&amp;rsquo;s not to say we don&amp;rsquo;t fix important bugs during regular work; we absolutely do. But fixits recognize that there should be a place for handling the “this is slightly annoying but never quite urgent enough” class of problems.&lt;/p&gt;
&lt;p&gt;The whole reason we started fixits in the first place is that we observed these bugs never get actioned. Given this, I think carving out some explicit time for it is a good thing.&lt;/p&gt;
&lt;h3 id="isnt-it-a-waste-to-pause-roadmap-work-for-a-whole-week"&gt;“Isn’t it a waste to pause roadmap work for a whole week?”&lt;/h3&gt;
&lt;p&gt;It’s definitely a tradeoff. 40 engineer-weeks is a &lt;em&gt;lot&lt;/em&gt; of manpower and there&amp;rsquo;s an argument to be made it should be used for actually solving roadmap problems.&lt;/p&gt;
&lt;p&gt;But I think this underweights the importance of polish of products to users. We’ve consistently found that the product feels noticeably better afterward (including positive comments from users about things they notice!) and there&amp;rsquo;s a sense of &lt;em&gt;pride&lt;/em&gt; in having a well-functioning product.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/qgw9gty38bftohb.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A user&amp;rsquo;s response to my PR to improve &lt;a href="https://github.com/google/perfetto/pull/3780"&gt;the Perfetto UI &amp;ldquo;record&amp;rdquo; page&lt;/a&gt;&lt;/center&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Also, many of the team productivity fixes compound (faster tests, clearer errors, smoother workflows) so the benefits carry forward well beyond the week itself.&lt;/p&gt;
&lt;h3 id="this-only-works-at-big-companies"&gt;This only works at big companies!&lt;/h3&gt;
&lt;p&gt;I agree that a full week might be too much for tiny teams or startups. But you can still borrow the idea in smaller chunks: a “fixit Friday” once a month, or a 2-day mini-fixit each quarter. The core idea is the same: protected, collective time to fix the stuff people complain about but no one schedules time to address.&lt;/p&gt;
&lt;h2 id="fixits-are-good-for-the-soul"&gt;Fixits are good for the soul&lt;/h2&gt;
&lt;p&gt;The official justification for fixits is that they improve product quality and
developer productivity. And of course they do this.&lt;/p&gt;
&lt;p&gt;But the unofficial reason I love them is simpler: it just feels good to fix things. It takes me back to a simpler time, and putting thought and attention into building great products is a big part of my ethos for how software engineering should be done. I wouldn’t want to work like that all the time. But I also wouldn’t want to work somewhere that never makes time for it.&lt;/p&gt;</content:encoded></item><item><title>When Good Technical Writing Isn't Enough</title><link>https://lalitm.com/on-why-i-write-this-blog/</link><pubDate>Wed, 12 Nov 2025 20:14:00 +0000</pubDate><guid>https://lalitm.com/on-why-i-write-this-blog/</guid><description>Last week, I came across Don’t Build an Audience. It’s a fascinating post and has been occupying a lot of my “free thinking” time. I strongly suggest reading it as it’s well written and excellently argued.
To summarise what it tries to say:
The market for written content on the web (blog posts, articles etc) is “efficient”: in other words, great work will find its audience without you needing to build a “following” first. “Liquidity providers” (influencers, curators, algorithms) are incentivized to share good content because it benefits them. They maintain credibility and engagement by surfacing quality work. Specifically called out are Substack, Alexey Guzey and Tyler Cowen. You can speed up “distribution” of what you write with minimal effort. Just 30 minutes to email a few key people and posts on the “right” platforms. Therefore, focus on creating excellent work rather than “building an audience”. If it’s truly good, the market will ensure it reaches “everyone who matters”. I’m a very strong believer in the core message of “make cool things rather than chase followers”. It’s something I’ve done throughout my life and I have zero interest in changing that going forward.
But I don’t agree with the argument that the market is efficient for all types of written content. I think the author is over-indexing on what they write about: topics accessible for a wide audience and do not require background knowledge (e.g. philosophical musings, economic theory, social dynamics).
I write about technical topics. Sometimes this can be so deeply in the weeds that at most a few thousand people in the world would care about it. But it might totally change how these people work: it might teach about how to debug performance in their code, let them know about a new approach to analysing data or even inspires them build an entirely new program.</description><content:encoded>&lt;p&gt;Last week, I came across &lt;a href="https://www.humaninvariant.com/blog/audience"&gt;Don&amp;rsquo;t Build an Audience&lt;/a&gt;. It&amp;rsquo;s a fascinating post and has been occupying a lot of my &amp;ldquo;free thinking&amp;rdquo; time. I strongly suggest reading it as it&amp;rsquo;s well written and excellently argued.&lt;/p&gt;
&lt;p&gt;To summarise what it tries to say:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The market for written content on the web (blog posts, articles etc) is &amp;ldquo;efficient&amp;rdquo;: in other words, great work will find its audience without you needing to build a &amp;ldquo;following&amp;rdquo; first.&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Liquidity providers&amp;rdquo; (influencers, curators, algorithms) are incentivized to share good content because it benefits them. They maintain credibility and engagement by surfacing quality work. Specifically called out are Substack, Alexey Guzey and Tyler Cowen.&lt;/li&gt;
&lt;li&gt;You can speed up &amp;ldquo;distribution&amp;rdquo; of what you write with minimal effort. Just 30 minutes to email a few key people and posts on the &amp;ldquo;right&amp;rdquo; platforms.&lt;/li&gt;
&lt;li&gt;Therefore, focus on creating excellent work rather than &amp;ldquo;building an audience&amp;rdquo;. If it&amp;rsquo;s truly good, the market will ensure it reaches &amp;ldquo;everyone who matters&amp;rdquo;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m a very strong believer in the core message of &amp;ldquo;make cool things rather than chase followers&amp;rdquo;. It&amp;rsquo;s something I&amp;rsquo;ve done throughout my life and I have zero interest in changing that going forward.&lt;/p&gt;
&lt;p&gt;But I don&amp;rsquo;t agree with the argument that the market is efficient for &lt;strong&gt;all&lt;/strong&gt; types of written content. I think the author is over-indexing on what they write about: topics accessible for a wide audience and do not require background knowledge (e.g. philosophical musings, economic theory, social dynamics).&lt;/p&gt;
&lt;p&gt;I write about &lt;a href="/cast-x-as-string-casts-to-integer-in-sqlite/"&gt;technical&lt;/a&gt; &lt;a href="/perfetto-swiss-army-knife/"&gt;topics&lt;/a&gt;. Sometimes this can be so deeply &lt;a href="/til-will-change-and-isolation-css-properties/"&gt;in the weeds&lt;/a&gt; that at most a few thousand people in the world would care about it. But it &lt;em&gt;might&lt;/em&gt; totally change how these people work: it might teach about how to debug performance in their code, let them know about a new approach to analysing data or even inspires them build an entirely new program.&lt;/p&gt;
&lt;p&gt;For technical content, the foundations of the &amp;ldquo;efficient market&amp;rdquo; as proposed break down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There are very few influencers or curators for technical niches. There&amp;rsquo;s no &amp;ldquo;Tyler Cowen of performance&amp;rdquo; or &amp;ldquo;Alexey Guzey of databases&amp;rdquo;. The people who would most benefit from these posts are engineers who are heads-down solving problems. They&amp;rsquo;re probably not sharing content for an audience. There are exceptions (e.g. &lt;a href="https://simonwillison.net/"&gt;Simon Willison&lt;/a&gt;) but, while technical, they still skew to the &amp;ldquo;accessible&amp;rdquo; side. They&amp;rsquo;re also few and far between.&lt;/li&gt;
&lt;li&gt;Substack is not good for &amp;ldquo;niche&amp;rdquo; content. Unlike TikTok or YouTube which are incredibly good at matching niche interests to the right people, Substack&amp;rsquo;s discovery still feels like it significantly prioritises content with wide appeal.&lt;/li&gt;
&lt;li&gt;In-depth technical writing is &amp;ldquo;high-attention&amp;rdquo;. You can accidentally watch a 10 TikTok videos about woodworking while procrastinating. You can read a blog post about how &lt;a href="https://www.humaninvariant.com/blog/pm-leak"&gt;prediction markets&lt;/a&gt; work on a daily commute. You probably wouldn&amp;rsquo;t accidentally read 5,000 words about database query optimization if you were not already interested.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The obvious rebuttal though is that, while Substack or curators may not work for technical writing, there &lt;strong&gt;are&lt;/strong&gt; alternative &amp;ldquo;liquidity providers&amp;rdquo;, namely a) link-aggregators like &lt;a href="https://news.ycombinator.com/"&gt;Hacker News&lt;/a&gt; and &lt;a href="https://lobste.rs/"&gt;Lobste.rs&lt;/a&gt; and b) technical subreddits and forums c) roundup newsletters like &lt;a href="https://tldr.tech/"&gt;TLDR&lt;/a&gt; and &lt;a href="https://programmingdigest.net/"&gt;Programming Digest&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, there is a very powerful effect working against you in all of these: if you post lots of content on the same topic, you are &lt;strong&gt;punished&lt;/strong&gt; by all of them. Specifically, there is always a feeling from readers of &amp;ldquo;didn&amp;rsquo;t I already see a post on this last week?&amp;rdquo;. And this comes from the value both the readers and platforms place on &lt;strong&gt;novelty&lt;/strong&gt;. For example, &lt;a href="/perfetto-swiss-army-knife/"&gt;Perfetto: Swiss Army Knife for Linux Client Tracing&lt;/a&gt; reached the &lt;a href="https://news.ycombinator.com/item?id=45771019"&gt;front page&lt;/a&gt; of Hacker News but I think it&amp;rsquo;s &lt;em&gt;very&lt;/em&gt; unlikely that another post mentioning Perfetto will do so within next several months.&lt;/p&gt;
&lt;p&gt;This is very weird because, as an expert in an area, you want to write &lt;em&gt;more&lt;/em&gt; about what you know about. But all of these technical &amp;ldquo;liquidity providers&amp;rdquo; want you to do the opposite: they incentivise you to &amp;ldquo;broaden out&amp;rdquo; and write about different topics constantly. Or at the very least, they prefer you &amp;ldquo;space out&amp;rdquo; posts so you&amp;rsquo;re not writing too much on the same topic in a compressed timeframe.&lt;/p&gt;
&lt;p&gt;To me, this means you have to be slightly more strategic when thinking about how to get your technical writing out there:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do build an audience&lt;/strong&gt;: just not in the the way the original post means. It assumes &amp;ldquo;audience building&amp;rdquo; means chasing followers and optimizing for engagement. But for technical writers, it&amp;rsquo;s instead connecting with a small group who value your specific expertise. When I write my n-th post about Perfetto, I&amp;rsquo;m writing for the people who are are interested enough in performance tooling to care. They follow precisely &lt;strong&gt;because&lt;/strong&gt; I keep going deep on the similar topics.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rotate through different platforms&lt;/strong&gt; when promoting articles. Hacker News for some articles, lobste.rs for others, LinkedIn for yet others. The choice depends on topic (different platforms prefer different &amp;ldquo;types&amp;rdquo; of content) but &lt;strong&gt;also&lt;/strong&gt; time since you last promoted something similar on that platform.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Accept that this is a long game&lt;/strong&gt;. The market might not be efficient in the short term, but over years, good articles will naturally bubble up and become &amp;ldquo;de-facto references&amp;rdquo; for people. So instead of virality, measure in terms of &amp;ldquo;mind-share&amp;rdquo; over the course of years.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Of course, this is much easier said than done because you have to write &amp;ldquo;in the dark&amp;rdquo; &lt;em&gt;hoping&lt;/em&gt; that what you write will have value in the long term without getting any immediate feedback!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So yes, build cool things. That remains the most important guiding principle as without that, there&amp;rsquo;s little point to the type of technical blog posts I like to write. But I think a little bit of strategic investment can go a long way to increase the chance of the &lt;em&gt;right&lt;/em&gt; people seeing them.&lt;/p&gt;</content:encoded></item><item><title>Perfetto: Swiss Army Knife for Linux Client Tracing</title><link>https://lalitm.com/perfetto-swiss-army-knife/</link><pubDate>Fri, 31 Oct 2025 00:00:00 +0000</pubDate><guid>https://lalitm.com/perfetto-swiss-army-knife/</guid><description>I gave a talk at the 2025 Tracing Summit last month titled “Perfetto: The Swiss Army Knife of Linux Client/Embedded Tracing”. My goal in this talk was to show how Linux kernel, systems and embedded developers can use Perfetto when debugging and root-causing performance issues in their respective domains. Even though the Perfetto UI is primarily built for viewing Android or Chrome traces, it is a flexible tool and can be used in many other ways!
The talk was recorded and is available on YouTube. Taking inspiration from Simon Willison, this post is an annotated presentation containing my slides and detailed notes on them. The talk also has a lot of UI demos: for these, I’ll have a screenshot but also a link to the relevant part of the video (videos are unbeatable for UI!).
First, what is Perfetto? Perfetto is fundamentally a suite of tools: it’s not just one thing but a bunch of different tools working together to help you debug and root-cause problems. This diagram shows how everything fits together, with the core parts of the Perfetto project in the center.
The recording tools for Perfetto consist of 1) an SDK for C++ apps 2) a daemon that can collect data from ftrace, /proc, /sys, and various kernel interfaces 3) another daemon that amalgamates trace data from multiple processes into a single trace file. These tools all speak the Perfetto protobuf format, a high-performance trace format designed to be very efficient to write but not to analyze or consume directly.
That’s where the trace processor comes in. It’s a C++ library that parses the protobuf format, but also a bunch of other widely used trace formats. It exposes this data via an SQL query interface to any embedding program including Perfetto UI (which is what most of this talk is about) and also to the Python API if you want to do ad-hoc scripting or analysis in Python.
There are also very common tracing/profiling formats used by the Linux community: perf.data, ftrace text format, Firefox profiler format, and many others. Perfetto supports quite a few of those directly. There’s also the Chrome JSON format (AKA the Trace Event Format) which is a simpler interchange format. It’s not the most efficient to read or write, but it does the job for a lot of use cases.
Often people write converters. They have their own tracing format, maybe proprietary, maybe open source but something we don’t know about, and it’s very common that people convert to one of the formats we understand, most commonly our protobuf or Chrome JSON formats.
The Perfetto UI is fundamentally a web-based trace visualizer, combining timeline visualization, user-driven selection/aggregation, and SQL queries all in one interface. Because it has the trace processor as a backend, it works with a bunch of different trace formats.
It’s very important to note that even though the Perfetto UI is web-based, everything happens inside your browser and trace data never leaves your system. You can even build it and host it yourself on any static server: we’ve made it extremely easy to do so!
At the start of 2025, we actually moved our whole development to GitHub. In the past, we used to develop on Android and GitHub was just a mirror. That’s no longer the case, GitHub is actually where we develop and take pull requests.
Most of this talk, I’m going to spend actually showing you how you can use the Perfetto UI to debug performance issues on Linux. I don’t want to show you an Android trace which needs a lot of context about how the Android system works and so you think, “oh, that was cool, but I didn’t really understand what was happening.”
So to make this talk more approachable, I wrote a straightforward demo program you can look at yourself! So it’s obviously not a production system but I’ve tried to make it as representative of the sort of issues we use Perfetto for every day.
It’s a Rust program which generates a Julia set and visualizes it over time. The technologies I used: Vulkan, GPU rendering and also multi-threaded CPU computation. So how it works is that computation of various parameters is happening on background threads, and then that’s being passed to the main thread for rendering.
And then, for demonstration purposes, there is a performance bug; rendering should run at 60 FPS, but every so often, the frame rate drops dramatically. Here’s what that looks like:
The code is on GitHub and if you’re interested in following along. The traces are there as well - you don’t have to collect the traces yourself, but you can if you want. All the instructions and information is in the README.
So the first suspicion we may have is that maybe it’s some CPU problem. A lot of engineers I know would reach for perf immediately whenever they see a problem like this. The main reason is that if perf can capture the problem, they can go straight to the line of code without needing to spend time debugging using more complex approaches.</description><content:encoded>&lt;p&gt;I gave a talk at the &lt;a href="https://tracingsummit.org/"&gt;2025 Tracing Summit&lt;/a&gt; last month titled &amp;ldquo;&lt;a href="https://cfp.tracingsummit.org/ts2025/talk/TKVU8W/"&gt;Perfetto: The Swiss Army Knife of Linux Client/Embedded Tracing&lt;/a&gt;&amp;rdquo;. My goal in this talk was to show how Linux kernel, systems and embedded developers can use &lt;a href="https://docs.perfetto.dev"&gt;Perfetto&lt;/a&gt; when debugging and root-causing performance issues in their respective domains. Even though the Perfetto UI is primarily built for viewing &lt;a href="https://perfetto.dev/docs/#why-would-you-use-perfetto-"&gt;Android or Chrome traces&lt;/a&gt;, it is a flexible tool and can be used in many other ways!&lt;/p&gt;
&lt;p&gt;The talk was recorded and is available on &lt;a href="https://www.youtube.com/watch?v=VzTwul2Qb3g"&gt;YouTube&lt;/a&gt;. Taking inspiration from &lt;a href="https://simonwillison.net/"&gt;Simon Willison&lt;/a&gt;, this post is an &lt;a href="https://simonwillison.net/2023/Aug/6/annotated-presentations/"&gt;annotated presentation&lt;/a&gt; containing my slides and detailed notes on them. The talk also has a lot of UI demos: for these, I&amp;rsquo;ll have a screenshot but also a link to the relevant part of the video (videos are unbeatable for UI!).&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%;"&gt;
&lt;iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/VzTwul2Qb3g" title="TS2025 - Perfetto: The Swiss Army Knife of Linux Client/Embedded Tracing" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;First, what is Perfetto? Perfetto is fundamentally a suite of tools: it&amp;rsquo;s not just one thing but a bunch of different tools working together to help you debug and root-cause problems. This diagram shows how everything fits together, with the core parts of the Perfetto project in the center.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-03.jpg" alt="diagram of all the pieces of Perfetto, Recording tools, protobuf format, trace processor, Perfetto UI, Python API, Format converters. Chrome JSON, Other profiling formats"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;The recording tools for Perfetto consist of 1) an &lt;a href="https://perfetto.dev/docs/getting-started/in-app-tracing"&gt;SDK for C++ apps&lt;/a&gt; 2) a &lt;a href="https://perfetto.dev/docs/getting-started/system-tracing"&gt;daemon&lt;/a&gt; that can collect data from ftrace, /proc, /sys, and various kernel interfaces 3) another &lt;a href="https://perfetto.dev/docs/concepts/service-model"&gt;daemon&lt;/a&gt; that amalgamates trace data from multiple processes into a single trace file. These tools all speak the Perfetto &lt;a href="https://perfetto.dev/docs/getting-started/converting"&gt;protobuf format&lt;/a&gt;, a high-performance trace format designed to be very efficient to write but not to analyze or consume directly.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where the &lt;a href="https://perfetto.dev/docs/analysis/trace-processor"&gt;trace processor&lt;/a&gt; comes in. It&amp;rsquo;s a C++ library that parses the protobuf format, but also a bunch of other widely used trace formats. It exposes this data via an SQL query interface to any embedding program including &lt;a href="https://perfetto.dev/docs/visualization/perfetto-ui"&gt;Perfetto UI&lt;/a&gt; (which is what most of this talk is about) and also to the &lt;a href="https://perfetto.dev/docs/analysis/trace-processor-python"&gt;Python API&lt;/a&gt; if you want to do ad-hoc scripting or analysis in Python.&lt;/p&gt;
&lt;p&gt;There are also very common tracing/profiling formats used by the Linux community: perf.data, ftrace text format, Firefox profiler format, and many others. Perfetto &lt;a href="https://perfetto.dev/docs/getting-started/other-formats"&gt;supports&lt;/a&gt; quite a few of those directly. There&amp;rsquo;s also the Chrome JSON format (AKA the &lt;a href="https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?tab=t.0"&gt;Trace Event Format&lt;/a&gt;) which is a simpler interchange format. It&amp;rsquo;s not the most efficient to read or write, but it does the job for a lot of use cases.&lt;/p&gt;
&lt;p&gt;Often people write &lt;a href="https://perfetto.dev/docs/getting-started/converting"&gt;converters&lt;/a&gt;. They have their own tracing format, maybe proprietary, maybe open source but something we don&amp;rsquo;t know about, and it&amp;rsquo;s very common that people convert to one of the formats we understand, most commonly our protobuf or Chrome JSON formats.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-04.jpg" alt="Slide showing Perfetto UI architecture with web-based trace visualizer features"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;The &lt;a href="https://perfetto.dev/docs/visualization/perfetto-ui"&gt;Perfetto UI&lt;/a&gt; is fundamentally a web-based trace visualizer, combining timeline visualization, user-driven selection/aggregation, and SQL queries all in one interface. Because it has the trace processor as a backend, it works with a bunch of different trace formats.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s very important to note that even though the Perfetto UI is web-based, everything happens inside your browser and trace data never leaves your system. You can even &lt;a href="https://perfetto.dev/docs/contributing/ui-getting-started"&gt;build it&lt;/a&gt; and host it yourself on any static server: we&amp;rsquo;ve made it extremely easy to do so!&lt;/p&gt;
&lt;p&gt;At the start of 2025, we actually moved our whole development to GitHub. In the past, we used to develop on Android and GitHub was just a mirror. That&amp;rsquo;s no longer the case, GitHub is actually where we develop and take pull requests.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-05.jpg" alt="Slide introducing the fractal renderer demo program with Vulkan and multi-threading"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;Most of this talk, I&amp;rsquo;m going to spend actually showing you how you can use the Perfetto UI to debug performance issues on Linux. I don&amp;rsquo;t want to show you an Android trace which needs a lot of context about how the Android system works and so you think, &amp;ldquo;oh, that was cool, but I didn&amp;rsquo;t really understand what was happening.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;So to make this talk more approachable, I wrote a &lt;a href="https://github.com/LalitMaganti/fractal-renderer-vk"&gt;straightforward demo program&lt;/a&gt; you can look at yourself! So it&amp;rsquo;s obviously not a production system but I&amp;rsquo;ve tried to make it as representative of the sort of issues we use Perfetto for every day.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a Rust program which generates a Julia set and visualizes it over time. The technologies I used: &lt;a href="https://vulkano.rs/"&gt;Vulkan&lt;/a&gt;, GPU rendering and also multi-threaded CPU computation. So how it works is that computation of various parameters is happening on background threads, and then that&amp;rsquo;s being passed to the main thread for rendering.&lt;/p&gt;
&lt;p&gt;And then, for demonstration purposes, there is a performance bug; rendering should run at 60 FPS, but every so often, the frame rate drops dramatically. Here&amp;rsquo;s what that looks like:&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/test.avif" alt="Animated visualization of Julia set fractal renderer showing frame rate drops"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;The code is on &lt;a href="https://github.com/LalitMaganti/fractal-renderer-vk"&gt;GitHub&lt;/a&gt; and if you&amp;rsquo;re interested in following along. The traces are there as well - you don&amp;rsquo;t have to collect the traces yourself, but you can if you want. All the instructions and information is in the README.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-06.jpg" alt="Slide titled &amp;ldquo;Suspicion #1: Maybe it&amp;rsquo;s a CPU problem?&amp;rdquo; with perf command examples"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;So the first suspicion we may have is that maybe it&amp;rsquo;s some CPU problem. A lot of engineers I know would reach for perf immediately whenever they see a problem like this. The main reason is that if perf can capture the problem, they can go straight to the line of code without needing to spend time debugging using more complex approaches.&lt;/p&gt;
&lt;p&gt;You can record a perf profile with &lt;code&gt;perf record -k mono -g ./fractal_renderer&lt;/code&gt;. The standard post-processing step which I think a lot of people do would be to &lt;a href="https://github.com/brendangregg/FlameGraph"&gt;generate&lt;/a&gt; an SVG flame graph out of this with &lt;code&gt;perf script | stack-collapse-perf.pl --all | flamegraph.pl &amp;gt; fractal-frame.svg&lt;/code&gt;. Here&amp;rsquo;s how that looks like for our perf profile:&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/fractal.svg" alt="Interactive flame graph showing CPU profiling data with thread names and function call stacks dominated by cos and sin operations"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;See also the &lt;a href="/img/fractal.svg"&gt;interactive version of the profile&lt;/a&gt;&lt;/center&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The flame graph shows the thread names at the bottom, and then stacked above are the call stacks of what the program is doing. The width represents how much time is spent in each function. In this case, the worker threads are spending most of their time in &lt;code&gt;cos&lt;/code&gt; and &lt;code&gt;sin&lt;/code&gt; operations, doing math computation. Exactly what you&amp;rsquo;d expect for this type of program. &lt;em&gt;Aside: Interestingly demangling didn&amp;rsquo;t seem to work for Rust out of the box which I find a bit unusual but I didn&amp;rsquo;t dig too much into why this was.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Looking at this, nothing really stands out. There&amp;rsquo;s no smoking gun that immediately reveals the problem. But here&amp;rsquo;s the fundamental limitation: the flame graph aggregates data across the entire trace, losing the time dimension. The performance problem we&amp;rsquo;re seeing happens every 2-3 seconds - brief drops in an otherwise normally functioning program.&lt;/p&gt;
&lt;p&gt;The flame graph shows me the aggregate, dominated by the correct behavior, making it nearly impossible to spot those occasional problematic moments. So how do I find the places where it&amp;rsquo;s doing the wrong thing?&lt;/p&gt;
&lt;p&gt;Well, that&amp;rsquo;s maybe where Perfetto can help you out a little bit! The thing I find lots of people don&amp;rsquo;t know is that perf actually &lt;em&gt;preserves&lt;/em&gt; timestamp information about when samples were taken: many tools drop this information but Perfetto is pretty good at showing that to you. I just need post-process the trace with &lt;code&gt;perf script &amp;gt; fractal.perftext&lt;/code&gt; to generate a text version of the profile which we can then open in the &lt;a href="https://ui.perfetto.dev"&gt;Perfetto UI&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;My demo talking through how to open the perf profile and navigate it starts at &lt;a href="https://youtu.be/VzTwul2Qb3g?t=551"&gt;9:11&lt;/a&gt; in the video.&lt;/em&gt;&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot3.png" alt="Perfetto UI screenshot showing perf profile timeline with thread tracks and CPU sample markers across time axis"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=590"&gt;9:50&lt;/a&gt; in the video&lt;/center&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The x-axis of what you&amp;rsquo;re seeing is time. And every horizontal line represents a thread: we call these lanes &amp;ldquo;tracks&amp;rdquo;. And each of the small arrowheads on the tracks are CPU samples for the associated thread.&lt;/p&gt;
&lt;p&gt;The behavior of the worker threads over time is really interesting. Most of the lifetime of the program, it&amp;rsquo;s doing continuous computation, basically. And then there&amp;rsquo;s this period of time in the middle where there&amp;rsquo;s this very interesting staircase pattern where it seems like only one thread is running at any one time: we&amp;rsquo;ll want to keep a note of this as this is quite important.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot4.png" alt="Perfetto UI screenshot demonstrating area selection feature with timeline selection and flame graph visualization below"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=637"&gt;10:37&lt;/a&gt; in the video&lt;/center&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One very cool thing about Perfetto is that it allows me to quickly generate visualizations of whatever I&amp;rsquo;m selecting on the timeline. We call this &amp;ldquo;area selection&amp;rdquo; and it&amp;rsquo;s where I drag my mouse and select a region both horizontally and vertically. This is on one track but even &lt;em&gt;across&lt;/em&gt; multiple tracks. So in the timeline it shows me the selection I&amp;rsquo;m making at the top, plus a flame graph at the bottom representing the aggregation of the samples in just that time period.&lt;/p&gt;
&lt;p&gt;The key advantage is that I can look at individual regions of time interactively. You can also do this with &lt;code&gt;flamegraph.pl&lt;/code&gt; but you need to pre-filter the data to what you&amp;rsquo;re looking for. This assumes you already know what the problem is before you find it. I always find that a bit counterintuitive. I prefer this view where I can see everything first, then decide &amp;ldquo;I want to look at this specific part in more detail.&amp;rdquo; That&amp;rsquo;s what Perfetto lets me do.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot5.png" alt="Perfetto UI screenshot showing enhanced flame graph with zoom, frame hiding, regex search, and bottom-up view features"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=751"&gt;12:31&lt;/a&gt; in the video&lt;/em&gt;&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;So now on to the flame graph itself: in the last year, I spent a bunch of time on improving the flame graph visualization in Perfetto UI. At Google, we have an internal tool called pprof (related to but not the same as the open-source one), and I&amp;rsquo;ve always loved the visualization it has. So I worked to make Perfetto&amp;rsquo;s flame graph look and behave very similarly.&lt;/p&gt;
&lt;p&gt;There are a bunch of features here. You can zoom in like most flame graph, but you can also say &amp;ldquo;I only want to look at the stack starting from this point&amp;rdquo; — it gets rid of everything above and starts fresh from there. If I don&amp;rsquo;t want to see a particular function like &lt;code&gt;sin&lt;/code&gt;, I can just hide the frame and it gets merged into its parent. The search is regex-based so I can also just type the specific things I&amp;rsquo;m interested in: it&amp;rsquo;s pretty powerful.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also something I don&amp;rsquo;t think I&amp;rsquo;ve seen in other visualizers (I might be wrong, please do correct me!): what I call &amp;ldquo;bottom up&amp;rdquo; view. Imagine you take all the leaves across your entire program and visualize who&amp;rsquo;s calling into them. If you have a leaf function that&amp;rsquo;s called everywhere in your program but reached from many different places, it&amp;rsquo;ll be split across your flame chart. This is the inverse — you&amp;rsquo;re looking at the leaves and asking &amp;ldquo;who is calling me?&amp;rdquo; This is particularly useful when you&amp;rsquo;re trying to optimize very low-level functions that are always at the leaf level — things like memcpy or hashmap operations.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-07.jpg" alt="Slide titled &amp;ldquo;Suspicion #2: Sleeping on what?&amp;rdquo; discussing need for scheduler trace analysis"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;So our main takeaway is that our worker threads all had gaps in their CPU execution in a staircase pattern. This means the threads weren&amp;rsquo;t actually on the CPU and that&amp;rsquo;s usually a sign that they&amp;rsquo;re sleeping.&lt;/p&gt;
&lt;p&gt;But sleeping on what? Locks? Disk? Network? What exactly is happening? To answer that, I need a scheduler trace to show me the wake-up patterns, who&amp;rsquo;s being scheduled when, what type of sleep threads are in, that sort of thing.&lt;/p&gt;
&lt;p&gt;We recently added support for trace-cmd&amp;rsquo;s text format in Perfetto (&lt;em&gt;aside: &lt;a href="https://github.com/google/perfetto/issues/1819"&gt;there&amp;rsquo;s&lt;/a&gt; an open issue to support the binary format too!&lt;/em&gt;). For this demo, I&amp;rsquo;m collecting sched_switch and sched_waking events with &lt;code&gt;sudo trace-cmd record -e sched:sched_switch -e sched:sched_waking -C mono ./fractal-renderer-vk&lt;/code&gt; and we can open it in the UI once we do &lt;code&gt;trace-cmd report -N &amp;gt; fractal.sched&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The demo showing off scheduler traces starts at &lt;a href="https://youtu.be/VzTwul2Qb3g?t=916"&gt;15:16&lt;/a&gt; in the video.&lt;/em&gt;&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot6.png" alt="Perfetto UI screenshot showing scheduler trace with CPU scheduling tracks at top, white regions indicating idle time"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=950"&gt;15:49&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;So this is what a scheduler trace looks like. Again as before, the x-axis is time. At the top of the trace, you&amp;rsquo;ve got the CPU scheduling tracks. For each CPU, they tell us what was scheduled on that CPU at each point in time. The white regions are where there&amp;rsquo;s nothing scheduled. As I zoom in, you start to see information like the name of the thread that&amp;rsquo;s scheduled, the TID of the thread, and so on. This is the CPU view of things, looking at it from a CPU perspective.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot7.png" alt="Perfetto UI screenshot showing thread state tracks with dark green for running, light green for runnable, and white for interruptible sleep"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1008"&gt;16:48&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;There&amp;rsquo;s also a thread perspective on scheduling: what we call this &amp;ldquo;thread state&amp;rdquo;. This shows the scheduling state of each thread at every point in time. Dark green represents the thread running, light green represents runnable (the thread is on the runqueue so it&amp;rsquo;s eligible to run, but the scheduler hasn&amp;rsquo;t actually let it run yet), and white represents interruptible sleep (S sleep). There are also orange regions for uninterruptible sleep (D sleep), though that doesn&amp;rsquo;t show up much in this trace.&lt;/p&gt;
&lt;p&gt;You can again see a very clear staircase-like pattern. Again, only one thread seems to be running at any one time. You&amp;rsquo;re maybe getting the sense of what the problem might be at this point.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot8.png" alt="Perfetto UI screenshot showing area selection on CPU track with tables displaying time spent by each thread"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1063"&gt;17:43&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;Like with the perf visualization, you can do area selections here too. I can select a region on a CPU and get tables showing time spent by each thread (shown in the screenshot above). I can also do this for thread state, you can also see that runnable time is basically negligible compared to running and sleeping, which is where most of the time is being spent.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot9.png" alt="Perfetto UI screenshot showing scheduler visualizations including run queue length and active CPU count metrics"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1092"&gt;18:12&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;There are also scheduler-specific visualizations like run queue length and active CPU count. You can see in the problematic region, the active CPU count also went down, which makes sense.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-08.jpg" alt="Slide titled &amp;ldquo;Suspicion #3: What is the program doing?&amp;rdquo; discussing need for application-level visibility"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;So the scheduler trace shows when threads are sleeping, but it&amp;rsquo;s not telling us why. What was the program doing in that region? What code was it running? The sequential pattern suggests some sort of serialization—something is causing only one thread to run at a time. But we need application-level visibility.&lt;/p&gt;
&lt;p&gt;Since this program was written in Rust, I just used the off-the-shelf &lt;a href="https://docs.rs/tracing/latest/tracing/"&gt;&lt;code&gt;tracing&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.rs/tracing-perfetto/latest/tracing_perfetto/"&gt;&lt;code&gt;tracing-perfetto&lt;/code&gt;&lt;/a&gt; crates; I did have to make some small &lt;a href="https://github.com/LalitMaganti/tracing-perfetto"&gt;modifications&lt;/a&gt; to tracing-perfetto that I&amp;rsquo;m hoping to upstream. These output a Perfetto trace without needing to use the Perfetto SDK. These libraries are also maintained by others: we don&amp;rsquo;t have any hand in them.&lt;/p&gt;
&lt;p&gt;All I needed to do was integrate these into my program and then add a command line switch to write out the collected trace to a file. So I just do &lt;code&gt;./fractal_renderer --trace fractal.pftrace&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;My demo for app tracing starts at &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1177"&gt;19:37&lt;/a&gt; in the video.&lt;/em&gt;&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot10.png" alt="Perfetto UI screenshot showing app trace with userspace instrumentation slices on thread tracks representing program activity"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1206"&gt;20:06&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;Looking at the app trace: as before x-axis is time and each of the tracks represents a thread. Instead of scheduling activity or CPU stack samples, this time the tracks is userspace instrumentation of the program itself. Each of these rectangles is a named period of time representing what the program was doing at that time.&lt;/p&gt;
&lt;p&gt;There are lots of different names for these in the tracing world (e.g. spans, intervals) but we call these &amp;ldquo;slices&amp;rdquo;. The main thread is rendering frames as you would expect and the workers are each computing tiles of that frame, which eventually feed back to the main thread and sync to the GPU.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot11.png" alt="Perfetto UI screenshot showing flow visualization with causal links between render frame and worker thread operations"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1425"&gt;23:42&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;There&amp;rsquo;s also this thing called flows, which shows the causal links between different pieces. Render frame is calling into all these places and causing them to happen. It&amp;rsquo;s sort of similar to wakeup graphs but for user space: basically, this is what caused me to start running this function.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot12.png" alt="Perfetto UI screenshot zoomed into problematic region showing 1.8 second duration for update adaptive quality operation"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1431"&gt;23:51&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;So you can see very clearly there&amp;rsquo;s a normal region and then a region where things are taking 1.8 seconds: almost certainly the cause of the frame drops.&lt;/p&gt;
&lt;p&gt;And if we look at the slice in question, it seems to be doing something called &amp;ldquo;update adaptive quality.&amp;rdquo; Basically, I wrote some code to dynamically change the rendering quality based on frame rate. If I&amp;rsquo;m running faster, I can render at higher quality. If I&amp;rsquo;m running slower, I can do lower quality. That&amp;rsquo;s what this adaptive quality thing is supposed to do.&lt;/p&gt;
&lt;p&gt;But clearly something has gone wrong. I&amp;rsquo;m causing frame drops because of updates to adaptive quality. A little bit ironic, to say the least. Now we know what the program is actually doing during that time span.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-09.jpg" alt="Slide titled &amp;ldquo;Putting it all together: The merged trace&amp;rdquo; explaining trace merging feature"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;So now we&amp;rsquo;ve looked at three different sources of debugging data separately. In perf, we got told there are few or no CPU samples and weird staircase patterns. In ftrace, we saw only one worker seems to be active and the rest are sleeping in interruptible sleep. And in app tracing, we see it&amp;rsquo;s because of adaptive quality updates that workers are working on, and they shouldn&amp;rsquo;t be doing it this way.&lt;/p&gt;
&lt;p&gt;Obviously, the theory is that it&amp;rsquo;s all the same problem. But we can confirm this theory with a very new (and still experimental) feature of the UI: trace merging.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: as this part of Perfetto is still experimental, if you want to try it yourself, you need to go to plugins and enable the &amp;ldquo;MultiTraceOpen&amp;rdquo; plugin. Also, unlike I what say in the talk, you do &lt;strong&gt;not&lt;/strong&gt; need to be on Autopush as all the features I showed in my talk are now available in Stable.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The demo showing off merged traces starts at &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1472"&gt;24:32&lt;/a&gt; in the video.&lt;/em&gt;&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot13.png" alt="Perfetto UI screenshot showing merged trace with CPU scheduling tracks, stack samples, and app instrumentation on single timeline"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1544"&gt;25:44&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;This is now the merged trace file containing all the different information. You can see the CPU scheduling tracks like we had in the scheduler trace, the stack samples like we had in the perf trace, and the app instrumentation, all beside each other on one timeline.
You can very clearly see the adaptive quality update running, then this period where it ran, and then it was done. It&amp;rsquo;s very cool to be able to see the pattern of how my program is sleeping and where and what it was running on one timeline.&lt;/p&gt;
&lt;p&gt;The thing is, you&amp;rsquo;ve always been able to do this with Perfetto if you collect a Perfetto trace with all of these integrated. The new capability is that this is now being brought to traces you didn&amp;rsquo;t collect with Perfetto. Collect it with whatever you like, and we&amp;rsquo;ll still manage to do this visualization. That&amp;rsquo;s the new capability we have.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot14.png" alt="Perfetto UI screenshot showing query interface with pivot tables and filter options for trace data analysis"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1798"&gt;29:58&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;There&amp;rsquo;s also a query interface which we&amp;rsquo;ve been building out recently and there are some pretty powerful things you can do with these tables. As well as the flat tables of data, there&amp;rsquo;s also a more dynamic pivot table and if I click the arrow on the left, I can get into a detailed filter table, similar to a spreadsheet but optimize for things people want to do on traces.&lt;/p&gt;
&lt;p&gt;For example, by doing an area select, I can first get a list of all the events that happened during a time region. Then using the table I can filter for things - say I only care about slices longer than some duration, like 319 microseconds for whatever reason. I can click and add a filter for things greater than this.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/screenshot15.png" alt="Perfetto UI screenshot demonstrating Show Debug Track feature with filtered events visualized as timeline track"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Screenshot from &lt;a href="https://youtu.be/VzTwul2Qb3g?t=1811"&gt;30:11&lt;/a&gt; in the video&lt;/em&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;There&amp;rsquo;s also a feature called &amp;ldquo;Show Debug Track&amp;rdquo; that&amp;rsquo;s also very powerful. The table shows rows with timestamped duration information, and this feature lets you visualize that data as a track on the timeline. It adds a track at the top showing the regions of time where these events happened—in this case, where ComputeTile took longer than a certain threshold.&lt;/p&gt;
&lt;p&gt;This is particularly useful for correlation analysis. For example, in Android, we&amp;rsquo;re often looking at system A and trying to understand its effect on system B. We find all the places where one thing is slow, then look for correlations. Being able to see a track in the UI where you can quickly have context and say &amp;ldquo;oh, during this period of time, this other thing was happening&amp;rdquo; is invaluable. It&amp;rsquo;s probably one of our most-used features.&lt;/p&gt;
&lt;p&gt;You can also copy the SQL query from these tables and start doing your own aggregations on top of it. This eases the burden of starting with a completely blank canvas. Instead of wondering &amp;ldquo;where do I even start querying my data? What tables is my data in?&amp;rdquo;, the UI gives you a starting query that you can build on without needing to write something from scratch.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-10.jpg" alt="Slide titled &amp;ldquo;The root cause&amp;rdquo; summarizing findings about dynamic quality updates causing frame drops"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;So the dynamic quality updates were stopping the world. Perf profiling showed the problem, scheduler traces found the sleeping pattern, app tracing confirmed it was the quality adjustment code, and the single timeline view let me see everything happening at once.&lt;/p&gt;
&lt;p&gt;Interestingly, I tried to fix this by removing the lock contention—which I thought was the bug I had introduced. But it turns out I shouldn&amp;rsquo;t have been doing this code in the workers in the first place. Even after removing the lock contention, just the CPU activity of doing that work was enough to cause frame drops. The right solution was to move it to a background thread. As part of debugging this for the demo, I discovered something even better that I could be doing.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-11.jpg" alt="Slide titled &amp;ldquo;Perfetto in the Open-Source Community&amp;rdquo; listing projects using Perfetto including Mesa, VizTracer, pthread_trace, magic-trace, sched-analyzer, and systing"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.perfetto.dev"&gt;Android and Chrome&lt;/a&gt; are our bread and butter—that&amp;rsquo;s what we officially support as a team, and why Google staffs us. But there are many other interesting uses.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.mesa3d.org/perfetto.html"&gt;Mesa&lt;/a&gt; uses Perfetto as one of its tracing systems. One thing I could have shown is collecting a Mesa trace alongside all the other traces we looked at—you can actually see what the GPU is doing at the same time, which would have been very cool, but I just didn&amp;rsquo;t have time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/gaogaotiantian/viztracer"&gt;VizTracer&lt;/a&gt; is function tracing for Python, similar to uftrace, but you don&amp;rsquo;t have to recompile anything or do anything special.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/dsharlet/pthread_trace"&gt;pthread_trace&lt;/a&gt; is for visualizing pthread mutex contention. The author has a very efficient way of doing this and writes protobuf using heavy use of constexpr to make it very low overhead. It&amp;rsquo;s a very interesting project.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/janestreet/magic-trace"&gt;magic-trace&lt;/a&gt; uses Intel Processor Trace for function-level tracing at the processor level with lower overhead. They wrote a converter from that to the Perfetto format so you can visualize that data in Perfetto.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/qais-yousef/sched-analyzer"&gt;Qais Yousef&amp;rsquo;s sched-analyzer&lt;/a&gt; enriches Perfetto traces with scheduler internals. It&amp;rsquo;s a very cool project that I find particularly interesting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/johnstultz-work/0ec4974e0929c4707bfd89c876ae4735"&gt;John Stultz&amp;rsquo;s all-in-one Perfetto recording script&lt;/a&gt; for kernel developers addresses a common complaint: &amp;ldquo;Perfetto is so complicated, I don&amp;rsquo;t know what events I want to record.&amp;rdquo; He just wrote a script that configures everything for you.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, &lt;a href="https://josefbacik.github.io/kernel/systing/debugging/2025/05/08/systing.html"&gt;Josef Bacik&amp;rsquo;s systing&lt;/a&gt; is a bit experimental but fascinating. He re-implemented tracing daemons on top of BPF instead of Perfetto&amp;rsquo;s native implementation, combining BPF-based tracing with perf stack traces in a single binary. He has a blog post explaining why. I thought it was a fascinating use of Perfetto.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-12.jpg" alt="Slide encouraging users to try the demo program and record their own traces"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;If you want to try this yourself, you can use the &lt;a href="https://github.com/LalitMaganti/fractal-renderer-vk"&gt;demo program I&amp;rsquo;ve provided&lt;/a&gt;. But even better: record traces on your own programs. Open scheduler traces and perf at the same time. Instrument your program, convert it to Perfetto format, and visualize everything together.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-13.jpg" alt="Slide promoting comprehensive tutorial for converting ad-hoc timestamped data to Perfetto format"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;If you want to convert ad-hoc timestamped data to Perfetto, we wrote a &lt;a href="https://perfetto.dev/docs/getting-started/converting"&gt;comprehensive tutorial&lt;/a&gt; with Python snippets for everything you might want to visualize. It covers all the features I showed and how to write Python code to generate them. We have a library for writing these traces: besides this one library, you don&amp;rsquo;t need to install anything else. You can go ahead and convert your own data to Perfetto.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-14.jpg" alt="Slide welcoming contributions and pull requests to the Perfetto project"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;p&gt;We&amp;rsquo;re very happy to accept contributions and review pull requests pretty quickly. We&amp;rsquo;ve had a lot of open-source contributors over the years and have been quite lucky with some very high-quality contributions. If you want to contribute yourself or have a feature you feel is missing, send it our way.&lt;/p&gt;
&lt;center&gt;
&lt;p&gt;&lt;img src="/img/perfetto-swiss-army-15.jpg" alt="Final slide with contact information and links to demo repo, documentation, and Perfetto UI"&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Demo repo: &lt;a href="https://github.com/LalitMaganti/fractal-renderer-vk"&gt;github.com/LalitMaganti/fractal-renderer-vk&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Learn more: &lt;a href="https://docs.perfetto.dev"&gt;docs.perfetto.dev&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Perfetto UI: &lt;a href="https://ui.perfetto.dev"&gt;ui.perfetto.dev&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Contact: &lt;a href="mailto:lalitm@google.com"&gt;lalitm@google.com&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>On Perfetto, Open Source, and Company Priorities</title><link>https://lalitm.com/perfetto-oss-company-prio/</link><pubDate>Fri, 17 Oct 2025 15:00:00 +0100</pubDate><guid>https://lalitm.com/perfetto-oss-company-prio/</guid><description>I recently stumbled across this post on lobste.rs about a project called traceboot which allows visualizing the Linux boot process using lightweight ftrace events and Perfetto. The author had some commentary about their experience trying to order tracks in Perfetto:
Ordering tracks with perfetto has been ridiculously complicated. It has taken the majority of the time of this project! Upstream’s answers are basically that the main user is Android (Perfetto is a Google project) so others come second if at all. While I get the reasons to do so, I read that as a caution against depending on it as a third-party. Google is notorious for (…) completely killing projects
Honestly? All of these points are right:
It is really unfortunate that doing something so simple took so much effort. It’s true that external users are supported at a lower priority than Android users. It’s also the case that Google has historically wound down projects when priorities shift The good news is that we just landed support for trace writers to specify explicitly how traces should be ordered with the JSON format in Perfetto without any extreme workarounds! This feature is already available on the “Canary” UI channel and on “Stable” within 3-4 weeks.</description><content:encoded>&lt;p&gt;I recently stumbled across
&lt;a href="https://lobste.rs/s/fl7ly9/traceboot_precise_lightweight_tracing"&gt;this post on lobste.rs&lt;/a&gt;
about a project called
&lt;a href="https://codeberg.org/SpecialSnowflake/traceboot"&gt;traceboot&lt;/a&gt; which allows
visualizing the Linux boot process using lightweight ftrace events and Perfetto.
The author had
&lt;a href="https://codeberg.org/SpecialSnowflake/traceboot#woes-with-a-rant-and-ideas-last-paragraph-of-this-section"&gt;some commentary&lt;/a&gt;
about their experience trying to order tracks in Perfetto:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ordering tracks with perfetto has been ridiculously complicated. It has taken
the majority of the time of this project! Upstream&amp;rsquo;s answers are basically
that the main user is Android (Perfetto is a Google project) so others come
second if at all. While I get the reasons to do so, I read that as a caution
against depending on it as a third-party. Google is notorious for (&amp;hellip;)
completely killing projects&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Honestly? All of these points are right:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is really unfortunate that doing something so simple took so much effort.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s true that external users are supported at a lower priority than Android
users.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s also the case that Google has historically wound down projects when
priorities shift&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The good news is that we just
&lt;a href="https://github.com/google/perfetto/pull/3273"&gt;landed&lt;/a&gt; support for trace
writers to specify explicitly how traces should be ordered with the JSON format
in Perfetto without any extreme workarounds! This feature is already available on
the &amp;ldquo;Canary&amp;rdquo; UI channel and on &amp;ldquo;Stable&amp;rdquo; within 3-4 weeks.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/perfetto-json-ordering.png" alt="Json Track ordering in action!"&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;i&gt;process_sort_index and thread_sort_index are now respected by Perfetto!&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;So we solved the imediate problem! But here&amp;rsquo;s what concerns me: I found this
issue totally by chance while browsing lobste.rs. There were several issues
filed in the Perfetto repo over several years asking for this feature
(&lt;a href="https://github.com/google/perfetto/issues/555"&gt;#555&lt;/a&gt;,
&lt;a href="https://github.com/google/perfetto/issues/764"&gt;#764&lt;/a&gt;,
&lt;a href="https://github.com/google/perfetto/issues/378"&gt;#378&lt;/a&gt;) but none of them managed
to communicate to us just how painful this issue was and the lengths people were
willing to go to work around it. Seeing the annoyance in such stark terms
definitely put into perspective the importance of this feature.&lt;/p&gt;
&lt;p&gt;You might be thinking: &amp;ldquo;surely if a feature is requested that much, you should
consider it important and work on fixing it&amp;rdquo;. And for a general open source
project, I&amp;rsquo;d agree with you. But there are some unique challenges which come
with being an open source project under the umbrella of a Big Tech company.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s all about how the Perfetto team fits into Google. We&amp;rsquo;re part of the Android
Diagnostics organization whose mandate is to build tools and systems to
root-cause problems on Android. Performance debugging would be impossible
without a system-wide tracing tool like Perfetto (which is why we even exist at
all!). But because we are funded by Android, it&amp;rsquo;s also expected that we
prioritise its needs.&lt;/p&gt;
&lt;p&gt;However, I, and others in the team, also deeply believe in the mission of open
source. We want to build something useful not just for Android engineers inside
Google but for the broader community as a whole. That creates a tension we
struggle with constantly: we value external users and their use cases, but the
reality of our team&amp;rsquo;s existence means Google&amp;rsquo;s priorities often win out.&lt;/p&gt;
&lt;p&gt;This tension is compounded by the fact that different types of users want
fundamentally different things from Perfetto. If you use Perfetto purely as a
trace viewer, the expectation is &amp;ldquo;I write the trace file and the Perfetto UI
should display exactly what I want&amp;rdquo;. But Android OS and app developers expect
&amp;ldquo;Perfetto should intelligently figure out what is most important in my trace and
highlight that to me.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;In the early days of Perfetto, we took on a lot of technical debt to meet the
immediate needs of Android developers. We built an &amp;lsquo;intelligence&amp;rsquo; layer that
automatically reordered tracks based on Android-specific heuristics. This was
great for our primary users, but it made it immensely difficult to later support
features for the &amp;lsquo;pure trace viewer&amp;rsquo; use case, like manually ordering tracks via
the JSON file, because the Android logic would always override it.&lt;/p&gt;
&lt;p&gt;The good news is that over the past 12 months, we&amp;rsquo;ve spent a lot of time trying
to decouple the &amp;ldquo;core&amp;rdquo; layer of Perfetto UI from the &amp;ldquo;intelligence&amp;rdquo; layer for
Android. This separation means we can now preserve the original intent from the
trace format (like &amp;ldquo;this track should be ordered here&amp;rdquo;) all the way to the UI,
rather than having it get lost or overridden by Android-specific heuristics
along the way. This allows us to build features like JSON track ordering without
creating impossible-to-maintain code that hurts us in the long run.&lt;/p&gt;
&lt;p&gt;It’s also understandable that people worry about Google’s project longevity.
Thankfully Perfetto&amp;rsquo;s core design principles of a) being 100% open source b)
fully client side significantly mitigate these risks. For example, running the
Perfetto UI is as simple as cloning the repo, running
&lt;code&gt;tools/install-build-deps --ui &amp;amp;&amp;amp; ui/run-dev-server&lt;/code&gt; and navigating to
&lt;code&gt;localhost:10000&lt;/code&gt;. The site is entirely static capable of being hosted anywhere.
There is no &amp;ldquo;backend server&amp;rdquo; to be taken down, everything runs in your browser
only. So if for some reason, if the Perfetto team was to disappear, it would be
trivially easy to host it yourself or even fork the project completely and start
maintaining it.&lt;/p&gt;
&lt;p&gt;With all that said, here are my takeaways from this experience:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There are still painpoints lurking in issues from the past that external
users are still hitting today. We should maybe do a pass over historical bugs
and figure out what issues are hiding there.&lt;/li&gt;
&lt;li&gt;Moving forward, we need to be more transparent and communicative about &lt;em&gt;why&lt;/em&gt;
we&amp;rsquo;re not prioritising a certain issue. I feel our terse replies about our
priorities are giving misleading indications on the value we place on
external users. Maybe we should also be more open about what we &lt;em&gt;are&lt;/em&gt;
prioritizing, even if that means we have to say &amp;ldquo;we&amp;rsquo;re not going to be able
to do much for external users this quarter&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;We also need to set up a regular routine for going through our bug tracker
and checking &amp;ldquo;has anything changed which now makes this bug possible&amp;rdquo; and if
so, we should follow up and post on the issue to make everyone aware of this
and keep bugs up to date.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&amp;rsquo;s also where community help would be invaluable: if you&amp;rsquo;re facing an issue
with Perfetto, tell us on &lt;a href="https://github.com/google/perfetto/issues/new"&gt;Github&lt;/a&gt; or on our
&lt;a href="https://groups.google.com/g/perfetto-dev"&gt;mailing list&lt;/a&gt;! Comment on old bugs if
one already exists or file new ones if they don&amp;rsquo;t. Bonus points if you can tell
us why the issue is so painful for you and what you&amp;rsquo;re doing to work around it
if it&amp;rsquo;s not fixed. The more information we have, the better judgement calls we
can make on prioritization.&lt;/p&gt;</content:encoded></item><item><title>V4L2 and Hardware Encoding on the Raspberry Pi</title><link>https://lalitm.com/hw-encoding-raspi/</link><pubDate>Mon, 01 Feb 2021 23:00:00 +0000</pubDate><guid>https://lalitm.com/hw-encoding-raspi/</guid><description>TLDR: Explain how the V4L2 M2M API works through the use-case of implementing hardware video encoding on the Raspberry Pi. This knowledge is generally useful as V4L2 is the de-facto generic API for hardware decoding and encoding on Linux.
Background
My journey started at this video on the excellent Craft Computing YouTube channel which showed how to setup TinyPilot, a Python app for KVM over IP which runs on a Raspberry Pi. Behind the scenes, TinyPilot uses ustreamer to read frames from a HDMI capture card and either exposes it over HTTP or writes it to shared memory. Along with the MJPEG output, support was recently added for encoding video using H264.
Even after messing with the source code, I could not get the H264 encoding working on my Pi running 64-bit Ubuntu with an error message of Can't create MMAL wrapper. Digging further, I ran into some insurmountable roadblocks with the approach taken by ustreamer and discovered the complex state of hardware encoding on the Pi.</description><content:encoded>&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt;: Explain how the V4L2 M2M API works through the use-case of
implementing hardware video encoding on the Raspberry Pi. This knowledge is
generally useful as V4L2 is the de-facto generic API for hardware decoding and
encoding on Linux.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Background&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;My journey started at
&lt;a href="https://www.youtube.com/watch?v=CyEpshm16HY&amp;amp;t=139s"&gt;this video&lt;/a&gt; on the
excellent
&lt;a href="https://www.youtube.com/channel/UCp3yVOm6A55nx65STpm3tXQ"&gt;Craft Computing&lt;/a&gt;
YouTube channel which showed how to setup TinyPilot, a Python app for KVM over
IP which runs on a Raspberry Pi. Behind the scenes, TinyPilot uses
&lt;a href="https://github.com/pikvm/ustreamer"&gt;ustreamer&lt;/a&gt; to read frames from a HDMI
capture card and either exposes it over HTTP or writes it to shared memory.
Along with the MJPEG output, support was recently added for encoding video using
H264.&lt;/p&gt;
&lt;p&gt;Even after messing with the source code, I could not get the H264 encoding
working on my Pi running 64-bit Ubuntu with an error message of
&lt;code&gt;Can't create MMAL wrapper&lt;/code&gt;. Digging further, I ran into some insurmountable
roadblocks with the approach taken by ustreamer and discovered the complex state
of hardware encoding on the Pi.&lt;/p&gt;
&lt;p&gt;In short, there are three userspace APIs for accessing the HW encoding and
decoding on the Pi&amp;rsquo;s Broadcom chip:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;OpenMAX Integration Layer (OpenMAX IL)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;This API came directly from Broadcom and was the original way to access
hardware codecs.&lt;/li&gt;
&lt;li&gt;Support was dropped by Broadcom a long time ago and there appears to be
&lt;a href="https://github.com/raspberrypi/Raspberry-Pi-OS-64bit/issues/98#issuecomment-715321951"&gt;no hope of it&lt;/a&gt;
ever working on 64-bit OSes.&lt;/li&gt;
&lt;li&gt;ustreamer
&lt;a href="https://github.com/pikvm/ustreamer/blob/master/src/ustreamer/encoders/omx/encoder.c"&gt;uses this API&lt;/a&gt;
to perform MJPEG encoding when specifically requested by the user.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;strong&gt;Multimedia Abstraction Layer (MMAL)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;This API was developed by the Pi developers as a replacement for OpenMAX and
backs tools like &lt;code&gt;raspistill&lt;/code&gt; and &lt;code&gt;raspivid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;While support for 64-bit was added
&lt;a href="https://github.com/raspberrypi/userland/commit/7d3c6b9f4c3ddeecefdeb2b882bada74a235249b"&gt;at one point&lt;/a&gt;,
it was subsequently
&lt;a href="https://github.com/raspberrypi/userland/commit/f97b1af1b3e653f9da2c1a3643479bfd469e3b74"&gt;reverted&lt;/a&gt;
and, currently, does not work.&lt;/li&gt;
&lt;li&gt;ustreamer uses
&lt;a href="https://github.com/pikvm/ustreamer/blob/master/src/ustreamer/h264/encoder.c"&gt;this API&lt;/a&gt;
to perform H264 encoding to shared memory.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="3"&gt;
&lt;li&gt;&lt;strong&gt;Video4Linux2 Memory to Memory (V4L2 M2M)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;V4L2 is Linux API which was historically used for video capture and output. In
2010, the M2M API was added, significantly extending V4L2 and adding support
for video codecs.&lt;/li&gt;
&lt;li&gt;The Raspberry Pi kernel
&lt;a href="https://github.com/raspberrypi/linux/blob/rpi-5.11.y/drivers/staging/vc04_services/bcm2835-codec/bcm2835-v4l2-codec.c"&gt;has a driver&lt;/a&gt;
which uses MMAL APIs in kernelspace and integrates with the rest of V4L2.&lt;/li&gt;
&lt;li&gt;Since this is just a generic kernel API, it works out of the box on 64-bit
OSes with programs including ffmpeg using it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since digging into the internals of MMAL seemed complex, even to knowledgable
folks in the Pi community, I decided to try to moving ustreamer&amp;rsquo;s H264 encoding
to use V4L2 instead. As a side benefit, since we&amp;rsquo;re using Linux kernel APIs,
this should also futureproof this code to any new revisions of the Pi.&lt;/p&gt;
&lt;p&gt;I quickly ran into the fact that, while lots of people have used V42L for
cameras and webcams, only the big projects like GStreamer and ffmpeg use it for
encoding. This meant there was no simple guide or example I could follow.
However, with a lot of poring over kernel documentation and trial and error, I
learnt enough to successfully perform H264 hardware encoding using V4L2 and
&lt;a href="https://github.com/LalitMaganti/ustreamer/commit/a14822a03da74c429928060d99a06e7a0a39d030"&gt;modified ustreamer&lt;/a&gt;
to support it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using V4L2: step by step&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generally, V4L2 works in either &amp;ldquo;capture&amp;rdquo; (frames being read off a device e.g.
camera or webcam) or &amp;ldquo;output&amp;rdquo; (frames being sent to a device e.g. a graphics
card) M2M combines &amp;ldquo;capture&amp;rdquo; and &amp;ldquo;output&amp;rdquo; into a single mode which allows
arbitrary transformations of frames. Video encoding is the obvious example of
this but decoding is also supported.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/hw-encoding-raspi/v4l2.png" alt="Diagram for V4L2 APIs"&gt;&lt;/p&gt;
&lt;p&gt;All communication with the hardware encoder on the Pi is done by opening a
special device in &lt;code&gt;/dev&lt;/code&gt; (&lt;code&gt;/dev/video11&lt;/code&gt; to be precise), performing &lt;code&gt;ioctls&lt;/code&gt; on
the file descriptor to control the encoding and using mmap-ed memory to pass the
frames between userspace and kernelspace.&lt;/p&gt;
&lt;p&gt;There are two phases to the encoding process: setup and streaming. In the setup
phase, we tell the encoder about things like the pixel format, resolution, frame
rate and setup the buffers to pass the frames. The streaming phase involves
writing the next raw frame of data to the kernel and reading back the encoded
frame.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: all error handling is omitted to make the code snippets smaller.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First, we open the special device for the video encoder. This gives an fd we can
use for &lt;code&gt;ioctls&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;linux/videodev2.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/dev/video11&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;O_RDWR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, we setup the &amp;ldquo;output&amp;rdquo; and &amp;ldquo;capture&amp;rdquo; devices; the most important things to
set are resolution and pixel format. The &amp;ldquo;capture&amp;rdquo; device will be implicitly set
to produce H264 frames with the same pixel format as the output but the
resolution needs to be set manually.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: confusingly &amp;ldquo;output&amp;rdquo; here refers to the raw frames being encoded and
&amp;ldquo;capture&amp;rdquo; to the encoded H264 output frames. A good way to think about this is
to consider how V4L2 was originally designed: frames are sent to output devices
like graphics cards and read from capture devices like cameras.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For 1080p raw frames in &lt;a href=""&gt;YUYV format&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_format&lt;/span&gt; &lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_pix_format_mplane&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pix_mp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_G_FMT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1920&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;mp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1080&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;mp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pixelformat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_PIX_FMT_YUYV&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_S_FMT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_G_FMT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1920&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;mp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1080&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_S_FMT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, we set the inter-frame interval to indirectly set the frame rate. For a
30FPS video:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_streamparm&lt;/span&gt; &lt;span class="n"&gt;stream&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;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&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;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE&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;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeperframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeperframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;denominator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_S_PARM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The most complex part of the setup is configuring the mmap buffers to send and
receive frames. Since we&amp;rsquo;re not trying to be performant, we use one capture
buffer and one output buffer.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;buffer&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="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;start&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_buffer&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_plane&lt;/span&gt; &lt;span class="n"&gt;plane&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// mmaps the buffers for the given type of device (capture or output).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_buffer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inner&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;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type&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;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_MEMORY_MMAP&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;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plane&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_QUERYBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;length&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;buffer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROT_READ&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;PROT_WRITE&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;MAP_SHARED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mem_offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_requestbuffers&lt;/span&gt; &lt;span class="n"&gt;buf&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_MEMORY_MMAP&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;buffer&lt;/span&gt; &lt;span class="n"&gt;output&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_REQBUFS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;output&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="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;buffer&lt;/span&gt; &lt;span class="n"&gt;capture&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_REQBUFS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, we &amp;ldquo;queue&amp;rdquo; up the capture and output buffers. This tells the encoder that
it has exclusive access to these buffers until we dequeue them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_QBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_QBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, we start the streaming process; this tells the kernel that it should
start reading raw frames from the output buffer and writing encoded frames to
the capture buffer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_STREAMON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_STREAMON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Streaming&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The actual encoding process is a straightforward loop where we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dequeue an output frame, write the next frame and queue it again.&lt;/li&gt;
&lt;li&gt;dequeue a capture frame, read the encoded frame and queue it again.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Assumed functions for reading a raw frame and writing an encoded frame.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Depending on application this can be from a file, a device or anywhere else.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;read_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;write_encoded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_buffer&lt;/span&gt; &lt;span class="n"&gt;buf&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_MEMORY_MMAP&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;v4l2_plane&lt;/span&gt; &lt;span class="n"&gt;out_planes&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;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;out_planes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_planes&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;out_planes&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;// Dequeue the output buffer, read the next frame and queue it back.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_DQBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;length&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;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bytesused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_QBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&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;// Dequeue the capture buffer, write out the encoded frame and queue it back.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE&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;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_DQBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;encoded_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;bytesused&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;write_encoded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoded_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VIDIOC_QBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;What I learned&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are several useful things I learned while working on this project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The kernel documentation had some excellent high-level explanations about how
the API worked
&lt;ul&gt;
&lt;li&gt;Specifically
&lt;a href="https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/dev-encoder.html"&gt;this docs page&lt;/a&gt;
was an exceptional resource as it lays out all the steps needed to perform
encoding&lt;/li&gt;
&lt;li&gt;Unfortunately, it only rarely provides code samples so a lot of trial and
error was still needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The
&lt;a href="https://github.com/raspberrypi/linux/blob/rpi-5.11.y/drivers/staging/vc04_services/bcm2835-codec/bcm2835-v4l2-codec.c"&gt;kernel source&lt;/a&gt;
was invaluable to figure out why error codes were being returned.
&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;ENOSYS&lt;/code&gt; error code is not helpful when it is implementation specific&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I discovered quite late in the project that loading the &lt;code&gt;bcm2835_codec&lt;/code&gt; kernel
module with debugging turned on (by setting the parameter &lt;code&gt;debug=3&lt;/code&gt;) allowed
visibility into log messages which I could cross reference with the kernel
sources.
&lt;ul&gt;
&lt;li&gt;This would have saved a bunch of time if I&amp;rsquo;d known this at the start&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Looking at &lt;code&gt;strace&lt;/code&gt; of ffmpeg trying to encode the same video as my program
allowed me to see if there was any hidden or different syscalls.
&lt;ul&gt;
&lt;li&gt;Instead of an explicit error being returned, I would sometimes end up being
unable to dequeue a capture or output frame, hanging ustreamer.&lt;/li&gt;
&lt;li&gt;This included several times when I had missed a required field in a setup
ioctl and when I had messed up the order of the queue/dequeue loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hopefully, this article has showed how to approach doing video encoding on the
Raspberry Pi from first principles. I&amp;rsquo;ve also tried to give an overview of the
V4L2 API; the beauty of a standard API means much of what I&amp;rsquo;ve said here is not
specific to the Pi but could be reused on any number of situations.&lt;/p&gt;
&lt;p&gt;There is a lot more interesting ground I could have covered including
non-blocking streaming, DMA and draining the encoder but this post has already
run long enough&amp;hellip;&lt;/p&gt;</content:encoded></item></channel></rss>