<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Imprint</title><description>I&apos;m an early-career engineer who writes to think. Expect {distributed, operating, database} systems, neuro, and whatever I&apos;m currently building or breaking.</description><link>https://bnjoroge.com/</link><item><title>On Customizable software</title><link>https://bnjoroge.com/posts/on-customizable-software/</link><guid isPermaLink="true">https://bnjoroge.com/posts/on-customizable-software/</guid><description>A hands-on write-up of using coding agents to trace and patch Codex ACP so ChatGPT login works in Zed remote projects, and what that experience felt like in practice.</description><pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;On Customizable software.&lt;/h1&gt;
&lt;p&gt;Much of how we built software has changed over the past year or so. Agents are much more capable than they were even just 6 months ago. It&apos;s kind of surreal how trivial it&apos;s gotten to customize software to your own liking. For instance, I use &lt;a href=&quot;https://github.com/b-nnett/codex-plusplus&quot;&gt;codex++&lt;/a&gt;, a tweak system that allows you to somewhat customize the Codex MacOS desktop app. I&apos;ve built plugins to do things like use Pierre&apos;s new &lt;a href=&quot;https://diffs.com&quot;&gt;diffs tool&lt;/a&gt; instead of the native diff viewer Codex supports or any of the existing tweaks such as support for &lt;code&gt;/goal&lt;/code&gt;  feature.  These are relatively trivial though so I wanted to document my experience using agents for slightly more complex features.&lt;/p&gt;
&lt;p&gt;I use Zed as my primary editor these days. It&apos;s gotten much better since when I tried it out maybe 2 years ago. Basic primitives generally work well. I also use their Agent Context Protocol(ACP) integration which allows me to use my existing Codex/Claude subs in Zed&apos;s agent panel. The codex ACP, via the subscription,  however doesnt quite work in remote projects. That was frustrating because the whole point of the setup was to use Codex naturally from inside Zed, not to manage a separate auth mode only because the project happened to be remote. It would work fine for local zed projects, but you&apos;d have to use an API key for remote projects.&lt;/p&gt;
&lt;h2&gt;Tracing the bug&lt;/h2&gt;
&lt;p&gt;I had an initial hypothesis that either Zed was explicitly disabling the browser-based login path for remote projects or The Codex ACP adapter only knew how to authenticate through a local browser callback flow and had no headless fallback. Armed with the hypothesis, I spun up codex. Code archaeology, root-cause analysis, patching, validation, issue triage, fork setup, release wrangling, and PR prep was all driven by GPT 5.4 and 5.4-mini interchangeably.&lt;/p&gt;
&lt;p&gt;&quot;Codex in Zed&quot; is really several systems glued together. Zed knows how to install and launch registry agents, &lt;code&gt;codex-acp&lt;/code&gt; is the ACP adapter process Zed actually talks to and upstream Codex owns the real auth flows and token storage behavior. Codex added support for device-auth which is what made it possible to fix. The PR is &lt;a href=&quot;https://github.com/zed-industries/codex-acp/pull/256&quot;&gt;here&lt;/a&gt; but the gist of it was keeping local projects on the existing browser callback login flow, in &lt;code&gt;NO_BROWSER&lt;/code&gt; mode, keep ChatGPT auth available and switch the headless path to device-code auth instead of hiding it. I definitely did not want to wait until Zed releases this fix, if at all, so I just patched the binary that contains this fix, and it&apos;s been my primary way of using it.&lt;/p&gt;
&lt;h2&gt;Rust compile times are insane&lt;/h2&gt;
&lt;p&gt;This is my first time using Rust for a relatively large project, and man those compile times are brutal. This was not even a giant from-scratch product build. It was a targeted patch in a repo that pulls in a serious dependency graph through upstream Codex. Even when the code changes were modest, the build and link cycles were long enough that they shaped the entire experience.  That changed how the agent workflow felt.&lt;/p&gt;
&lt;p&gt;There was one point where we tried to produce polished release assets and got dragged through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;missing &lt;code&gt;pkg-config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;missing &lt;code&gt;libssl-dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;missing &lt;code&gt;libcap-dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a stale apt repository on the Ubuntu box&lt;/li&gt;
&lt;li&gt;fork release workflow assumptions that only made sense in upstream CI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That whole stretch made the session feel less like &quot;AI is doing software for me&quot; and more like &quot;AI is helping me stay in the loop while I fight the usual systems bullshit.&quot;&lt;/p&gt;
&lt;h2&gt;What building with agents felt like&lt;/h2&gt;
&lt;p&gt;A few things stood out to me.&lt;/p&gt;
&lt;p&gt;First, the best use of the agents was not raw generation. It was disciplined persistence. We kept narrowing from symptom to mechanism: From figuring out where does Zed decide this is headless to where is the ChatGPT method filtered. Does upstream Codex already support device code or is this a Zed limitation, a &lt;code&gt;codex-acp&lt;/code&gt; limitation, or both?&lt;/p&gt;
&lt;p&gt;Second, the models were useful as research and synthesis partners, especially when I already had a hunch but needed to ground it in code. They were good at turning a vague suspicion into a concrete explanation with file paths and behavior.
Third, the systems boundary still matters a lot. The agent was most effective when the task was scoped, source-backed and verifiable. It was less magical when the task turned into orchestration across GitHub, local builds, remote VMs, and release asset handling. Still helpful, just less elegant. And fourth, I found myself wanting a better &quot;long-running task&quot; feel. When compilers are going and release assets are uploading, you do not need more intelligence so much as better ergonomics around waiting, status, and recovery. There&apos;s still so much to innovate here.&lt;/p&gt;
&lt;h2&gt;Where I landed&lt;/h2&gt;
&lt;p&gt;The whole thing took maybe 5 hours, and most of it was actually I/O from either the LLM or the compilers, not my &quot;processing speed&quot;. It was also a good reminder that the hard part of agent-assisted engineering is often not the coding. It is the long tail: build systems, environment mismatches, release mechanics, and staying clearheaded while waiting for Rust to compile half the planet.&lt;/p&gt;
</content:encoded><category>engineering</category><category>ai</category><category>tools</category><author>Bill Njoroge</author></item><item><title>dfs and bfs in the wild</title><link>https://bnjoroge.com/posts/dfs-and-bfs-in-the-wild/</link><guid isPermaLink="true">https://bnjoroge.com/posts/dfs-and-bfs-in-the-wild/</guid><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been building a static analysis tool that detects concurrency bugs in Go programs. One of the rules it enforces is if a goroutine loops over a channel using &lt;code&gt;for range&lt;/code&gt;, that channel needs to be closed at some point, otherwise the goroutine will block forever and leak.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
go func() {

for range ch { }

}()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To check whether &lt;code&gt;close(ch)&lt;/code&gt; actually happens, the tool looks at the code&apos;s control flow graph (CFG).  &lt;a href=&quot;https://bnjoroge.me/posts/cfg-data-flow-analysis-static-single-assignmentssa-applied-to-go-concurrency/&quot;&gt;This post &lt;/a&gt; explains them in slightly more detail. A CFG is just a map of how the program executes: it breaks a function into basic blocks (straight-line sequences of instructions) and draws arrows between them wherever the code can branch.&lt;/p&gt;
&lt;p&gt;My first attempt was a simple BFS over this graph. Starting from the goroutine launch, I checked whether there was any path through the graph that eventually reached a &lt;code&gt;close(ch)&lt;/code&gt;. If so, I assumed the channel was properly closed.&lt;/p&gt;
&lt;p&gt;This approach worked until I tested it against something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
func f(cond bool) {

	ch := make(chan int)
	
	go func() {
	
		for range ch { }
		
	}()

	if cond {
	
		close(ch)
	
	}

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BFS found a path to &lt;code&gt;close(ch)&lt;/code&gt; and concluded the channel was safe. It ignored the branch where &lt;code&gt;cond&lt;/code&gt; is false and the function returns without ever closing the channel. That goroutine leaks, and the tool said nothing.&lt;/p&gt;
&lt;p&gt;The problem is that BFS stops as soon as it finds what it&apos;s looking for. It found one safe path and called it done. But I didn&apos;t need to know whether a close was reachable on some path. I needed to know whether it was guaranteed on every path.&lt;/p&gt;
&lt;p&gt;That&apos;s a different question, and it calls for DFS. Using DFS, I traced every path from the goroutine launch to the end of the function. If any path reached the end without passing through &lt;code&gt;close(ch)&lt;/code&gt; first, the channel was not reliably closed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
var dfs func(node int) bool

dfs = func(n int) bool {

	if n == closeBlock {
	
	return false // this path is safe, stop here
	
	}

if len(successors[n]) == 0 {

	return true // reached the end without closing, this is the leak

}

for _, next := range successors[n] {

	if dfs(next) {
	
		return true
	
	}

}

	return false
	
	}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DFS is well suited here because it commits to exploring a single path all the way to its endpoint before backtracking. That commitment is exactly what makes it possible to say &quot;no path escapes&quot; rather than merely &quot;some path is safe.&quot;&lt;/p&gt;
&lt;p&gt;For the conditional example, DFS traces the &lt;code&gt;if cond&lt;/code&gt; branch and finds &lt;code&gt;close(ch)&lt;/code&gt; — fine, that path is safe. Then it backs up and traces the else branch, reaches the function exit, and notices it never closed the channel. That&apos;s the leak. The tool flags it.&lt;/p&gt;
&lt;p&gt;Often times, data structures and algorithms feel abit too academic or theretical, but it&apos;s always fun when you need to reach for them in practice. Most of the time the graph is implicit in the structure of the problem, not handed to you as an adjacency list. Recognizing that the CFG was a graph, and that &quot;guaranteed execution&quot; was a path coverage question, was the step that made the right algorithm obvious.&lt;/p&gt;
</content:encoded><category/><category>data structures</category><category>compilers</category><author>Bill Njoroge</author></item><item><title>CFG, Data Flow Analysis and SSA</title><link>https://bnjoroge.com/posts/cfg-data-flow-analysis-static-single-assignmentssa-applied-to-go-concurrency/</link><guid isPermaLink="true">https://bnjoroge.com/posts/cfg-data-flow-analysis-static-single-assignmentssa-applied-to-go-concurrency/</guid><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Compilers use static analysis to determine where transformations can be safely applied. Control flow and data flow analysis are two techniques often used in compiler optimization. Control-flow analysis seeks to understand the flow of control between operations, and data-flow analysis(DFA) analyses the flow of actual values through the code and operations. SSA is an intermediate representation that embeds a Control Flow Graph(CFG), and makes DFA relatively trivial.&lt;/p&gt;
&lt;p&gt;I&apos;ll be looking at it from a concurrency-focused lens, not so much code optimization. Understanding how both controls and data flow then becomes especially important when you want to understand how different concurrency primitives are applied. The question i&apos;ll be exploring is whether a goroutine will leak.&lt;/p&gt;
&lt;p&gt;To motivate this, let&apos;s look at a simple example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f(cond bool) {
    ch := make(chan int)
    go func() { for range ch {} }()  // line 4
    if cond {
        close(ch)  // line 6 — after goroutine, but only reachable when cond==true
    }
    // when cond==false: goroutine leaks
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the above example, we want to be able to detect if there is a goroutine leak,  an instance where the channel is not closed after the goroutine blocks forever because the channel it ranges over is never closed. One trivial way to check for this is to literally compare the line-number ordering. If the &lt;code&gt;close(ch)&lt;/code&gt; is after the goroutine, then we can probably assume that the goroutine is not going to leak(assuming we can correctly match the channel identifier). This approach, however, wouldnt work in the above case because the channel is closed conditionally.&lt;/p&gt;
&lt;p&gt;With a CFG, you can ask some questions like &lt;em&gt;Does every path from the goroutine launch to function exit pass through a close?&lt;/em&gt; This is called a &lt;em&gt;dominance&lt;/em&gt; or &lt;em&gt;post-dominance&lt;/em&gt; check.&lt;/p&gt;
&lt;p&gt;This is an illustration of how the control flows in the example above.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    entry[&quot;entry: ch = make(chan int)&quot;]
    goStmt[&quot;go func() { range ch }&quot;]
    ifCond{&quot;cond?&quot;}
    closeCh[&quot;close(ch)&quot;]
    noClose[&quot;(no close)&quot;]
    ret[&quot;return&quot;]

    entry --&amp;gt; goStmt
    goStmt --&amp;gt; ifCond
    ifCond --&amp;gt;|true| closeCh
    ifCond --&amp;gt;|false| noClose
    closeCh --&amp;gt; ret
    noClose --&amp;gt; ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To understand CFGs, we need to understand what post-dominators are. A node X post-dominates node Y if every path from Y to the function exit must pass through X. The post-dominator tree encodes this relationship: X post-dominates Y if and only if X is an ancestor of Y in the tree. We can look at the post-dominator tree for this code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    ret[&quot;return (EXIT)&quot;]
    ifCond{&quot;cond?&quot;}
    goStmt[&quot;go func() { range ch }&quot;]
    entry[&quot;entry: ch = make(chan int)&quot;]
    closeCh[&quot;close(ch)&quot;]
    noClose[&quot;(no close)&quot;]

    ret --&amp;gt; ifCond
    ret --&amp;gt; closeCh
    ret --&amp;gt; noClose
    ifCond --&amp;gt; goStmt
    goStmt --&amp;gt; entry
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look at where &lt;code&gt;close(ch)&lt;/code&gt; sits in the post-dominator tree. It hangs directly off return. It&apos;s not an ancestor of &lt;code&gt;go func() { range ch }&lt;/code&gt;. That tells us close(ch) does not post-dominate the goroutine launch. So basically, there exists a path from the goroutine launch to function exit that never passes through &lt;code&gt;close(ch)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Compare that with &lt;code&gt;ifCond&lt;/code&gt;, which is an ancestor of &lt;code&gt;goStmt&lt;/code&gt; in the post-dominator tree. Every path from the goroutine launch to the exit must pass through the &lt;code&gt;cond&lt;/code&gt; branch.&lt;/p&gt;
&lt;p&gt;So back to the question: &quot;does the channel get closed on every path after the goroutine starts?&quot; reduces to: &quot;is &lt;code&gt;close(ch)&lt;/code&gt; an ancestor of &lt;code&gt;go func()&lt;/code&gt; in the post-dominator tree?&quot; Here, the answer is no which tells us the goroutine can leak.&lt;/p&gt;
&lt;h2&gt;Data flow Analysis&lt;/h2&gt;
&lt;p&gt;Now, consider another extension to the example above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f() {
    ch := make(chan int)
    x := ch
    go func() { for range x {} }()
    close(ch)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, we are assigning &lt;code&gt;ch&lt;/code&gt; to another variable. The flow is trivially linear so you would think our initial line-numbering tool would work? But it actually wouldnt. A post-dominance check here would be fine in the sense that &lt;code&gt;close(ch)&lt;/code&gt; is indeed on every path, but the &lt;code&gt;range&lt;/code&gt; loops over &lt;code&gt;x&lt;/code&gt; and we are closing &lt;code&gt;ch&lt;/code&gt;. And so we would end up assuming that the channel &lt;code&gt;x&lt;/code&gt; that the goroutine is ranging(not sure that&apos;s a word) over was not closed.  We clearly need a deeper understanding of the flow of values. That&apos;s essentially where DFA comes into place. DFA tracks values. The specific form of DFA that helps here is called &lt;em&gt;reaching definitions&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ch := make(chan int)   // def₁: ch = &amp;lt;new channel&amp;gt;
x := ch               // def₂: x = ch
go func() {
    for range x {}     // using x. which definition reaches here?
}()
close(ch)              // using ch — which definition reaches here?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what the DFA looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    def1[&quot;def1: ch = make(chan int)&quot;]
    def2[&quot;def2: x = ch&quot;]
    useX[&quot;use: range x&quot;]
    useCh[&quot;use: close(ch)&quot;]

    def1 --&amp;gt;|&quot;ch flows to&quot;| def2
    def1 --&amp;gt;|&quot;ch flows to&quot;| useCh
    def2 --&amp;gt;|&quot;x flows to&quot;| useX
    def1 -.-&amp;gt;|&quot;same underlying value&quot;| useX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;def₁ creates the channel. def₂ copies it into x. The reaching definition for &lt;code&gt;range x&lt;/code&gt; is def₂, which in turn got its value from def₁. The reaching definition for &lt;code&gt;close(ch)&lt;/code&gt; is def₁ directly. Both trace back to the same &lt;code&gt;make(chan int)&lt;/code&gt;  so they operate on the same channel.&lt;/p&gt;
&lt;h2&gt;Static Single Assignment.&lt;/h2&gt;
&lt;p&gt;If you imagine a case like in a real-world program where you have branches, loops, or reassignments, doing this kind of analysis on raw code is a lot of work. SSA makes this relatively easy to do, and go makes it even easier with the &lt;code&gt;ssa&lt;/code&gt; package. In SSA, every variable is assigned exactly once. If the original code assigns a variable twice, SSA creates two different names. Here&apos;s what the example looks like in SSA form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;t0 = make chan int     ; the one and only channel value
t1 = t0                ; x := ch so SSA shows t1 IS t0
go func() {
    range t1           ; uses t1, which IS t0
}
close(t0)              ; same value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you process the SSA, and not the raw code, you can see that &lt;code&gt;range&lt;/code&gt; and &lt;code&gt;close&lt;/code&gt; operate on the same object.  This example was fairly trivial and to be honest, might be handled by regular data flow analysis. What makes SSA more interesting is when you have different control flows that need to merged.  So let&apos;s look at another example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f(cond bool) {
    ch1 := make(chan int)
    ch2 := make(chan int)
    var x chan int
    if cond {
        x = ch1
    } else {
        x = ch2
    }
    close(x)  // which channel does this close?
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fairly reasonable answer would be either channel would be closed right? I mean depending on if &lt;code&gt;cond&lt;/code&gt; is true or false. In SSA, this would be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;t0 = make chan int       ; ch1
t1 = make chan int       ; ch2
if cond goto block1 else block2

block1:
  jump block3

block2:
  jump block3

block3:
  t2 = phi [block1: t0, block2: t1]   ; &quot;x is t0 if we came from block1, t1 if block2&quot;
  close(t2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks a bit scary, but really the most important thing here is the &lt;code&gt;phi&lt;/code&gt; node. This essentially encodes that its value depends on the path we took. This allows us to define SSA&apos;s exclusive &apos;every variable is assigned once&apos; property. For concurrency, specifically these phi node representations are important because they can represent runtime conditions such as whether to use a buffered or unbuffered channel or which server to connect to.&lt;/p&gt;
&lt;p&gt;What we&apos;ve looked at are just single-function definitions. A more common use-case is when channels are created at one place, passed to consumers, producers or cleanup functions.&lt;/p&gt;
&lt;p&gt;Take this example for instance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func produce(ch chan&amp;lt;- int) {
    for i := 0; i &amp;lt; 5; i++ {
        ch &amp;lt;- i
    }
    close(ch)
}

func consume(ch &amp;lt;-chan int) {
    go func() {
        for range ch {}
    }()
}

func main() {
    ch := make(chan int)
    consume(ch)
    produce(ch)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Within main, the CFG is straight-line. SSA tells us that consume and produce both receive the same channel value (the &lt;code&gt;MakeChan&lt;/code&gt; from &lt;code&gt;main&lt;/code&gt;). But neither of them tell us whether the goroutine launched inside consume eventually sees a close on its channel. There&apos;s three different functions here, and none of the analysis we&apos;ve done earlier can connect them. As you would imagine, we need some kind of call-graph.
Enter &lt;strong&gt;Interprocedural analysis&lt;/strong&gt;.  The call-graph looks something like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    main[&quot;main()&quot;]
    consume[&quot;consume(ch)&quot;]
    produce[&quot;produce(ch)&quot;]
    anonFn[&quot;go func() { range ch }&quot;]

    main --&amp;gt;|&quot;calls&quot;| consume
    main --&amp;gt;|&quot;calls&quot;| produce
    consume --&amp;gt;|&quot;launches goroutine&quot;| anonFn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells us who calls whom, but what we really wanna know is what happens to the channel. You could absolutely walk through each callee body and re-analyze it, but you quickly run into performance issues if say &lt;code&gt;produce&lt;/code&gt; is called multiple times, or recursively.  A better approach is to track a compact description of what a function does with its parameters without re-analyzing its entire body. This is called a function summary. You compute it once, then reuse it everywhere the function is called. Summaries are also nice because you can do cross-package analysis. The contrived function summaries look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;produce(ch chan&amp;lt;- int):
    sends to:  param#0
    closes:    param#0
    launches:  (none)

consume(ch &amp;lt;-chan int):
    sends to:  (none)
    closes:    (none)
    launches:  goroutine that ranges param#0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the perspective of main:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main():
    ch = make(chan int)

    consume(ch):
        → launches goroutine that ranges ch

    produce(ch):
        → sends to ch
        → closes ch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see here that &lt;code&gt;produce&lt;/code&gt; does indeed close the same &lt;code&gt;ch&lt;/code&gt; we created in &lt;code&gt;main&lt;/code&gt; which answers our question from earlier.&lt;/p&gt;
&lt;p&gt;We started with a deceptively simple question: does a goroutine leak? Answering it pulled us through four layers of analysis. Control flow analysis gave us the CFG, post-dominance and the ability to reason about which paths exist. Data flow analysis gave us reaching definitions — the ability to track which values flow along those paths. SSA unified both into a single representation where the answers are structural, not computed. Interprocedural analysis extended the picture across function boundaries through call graphs and function summaries. Each technique answers a different dimension of the same question: paths, values, and boundaries. The goroutine leak was the motivating example, but these same tools generalize to any concurrency question you can think of really. Double closes, sends to channels no one receives from, mutexes held across goroutine boundaries. Just need to ask the right questions!&lt;/p&gt;
</content:encoded><category/><category>go</category><category>compilers</category><category>concurrency</category><author>Bill Njoroge</author></item><item><title>Connecting to multiple tailscale networks on a single host</title><link>https://bnjoroge.com/posts/connecting-to-multiple-tailscale-networks-on-a-single-host/</link><guid isPermaLink="true">https://bnjoroge.com/posts/connecting-to-multiple-tailscale-networks-on-a-single-host/</guid><description>Running two separate Tailscale tailnets on one Linux machine using network namespaces and a second tailscaled instance.</description><pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Tailscale is one of those tools that quietly becomes load-bearing in your life. I use it at work, at home, and I&apos;ve run it on machines I barely remember owning. So naturally the moment I wanted to connect to &lt;em&gt;two&lt;/em&gt; tailnets simultaneously, on the same laptop, I was bummed to discover you basically can&apos;t.&lt;/p&gt;
&lt;p&gt;Logging out and back in works, sure. Doing that twelve times a day adds up.&lt;/p&gt;
&lt;p&gt;Tailscale runs a single daemon, &lt;code&gt;tailscaled&lt;/code&gt;, which creates one tunnel interface (&lt;code&gt;tailscale0&lt;/code&gt;) and plants itself firmly in the host&apos;s networking stack. It assumes it owns the networking environment. Connecting to a second tailnet means evicting the first one.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The core idea&lt;/h2&gt;
&lt;p&gt;The easiest way to get a completely separate networking stack is to, well, run VMs on the host. I use this approach on my Mac with &lt;a href=&quot;https://orbstack.dev/&quot;&gt;Orbstack&lt;/a&gt;. Orbstack machines are almost-like full vms, but much faster and lighter. You can also use Lima or Colima to achieve the same. The basic idea is the host connects to one tailnet, the VM connects to another, and they never interact.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host machine → tailnet A
VM → tailnet B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Linux, you can get the exact same isolation without the hypervisor overhead using &lt;strong&gt;network namespaces&lt;/strong&gt;. A network namespace gives a process its own interfaces, routing table, and firewall rules. Software running inside it can&apos;t even tell it&apos;s sharing a kernel with anything else. Containers work like this under the hood. But running a VPN client inside a container usually means handing it elevated privileges so it can create tunnel interfaces and mess with routing tables.&lt;/p&gt;
&lt;p&gt;Another approach worth considering is a &lt;strong&gt;userspace networking stack&lt;/strong&gt; via SOCKS5 proxy. Instead of creating actual tunnel interfaces, Tailscale can run with &lt;code&gt;--tun=userspace-networking&lt;/code&gt;, routing all traffic through a SOCKS5 proxy on localhost. This doesn&apos;t require elevated privileges and works well if your applications understand SOCKS5.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tailscaled --tun=userspace-networking --socks5-server=localhost:1890 --socket /home/bnjoroge/.local/share/tailscale1/tailscaled.sock --statedir /home/bnjoroge/.local/share/tailscale1/ &amp;amp;
$ tailscaled --tun=userspace-networking --socks5-server=localhost:1056 --socket /home/bnjoroge/.local/share/tailscale2/tailscaled.sock --statedir /home/bnjoroge/.local/share/tailscale2/ &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, the SOCKS5 approach has a significant limitation: most applications don&apos;t understand SOCKS5 natively. You&apos;d need to configure each tool individually—SSH via ProxyCommand, curl with &lt;code&gt;--socks5&lt;/code&gt;, browsers with manual proxy settings. For anything that doesn&apos;t support SOCKS5, you&apos;d have to layer an HTTP proxy on top, which adds complexity and breaks protocols like SSH that don&apos;t work through HTTP proxies. If you try to use Docker or Orbstack to isolate apps that need the proxy, you&apos;re back to managing VMs, which defeats the purpose of avoiding hypervisor overhead.&lt;/p&gt;
&lt;p&gt;For my use case where I want CLI tools, databases, and services to transparently connect to different tailnets without per-application configuration, the proxy approach doesn&apos;t cut it. Working directly with namespaces gives you a completely separate networking stack where everything just works. Any tool, any protocol, no configuration needed beyond the initial setup.&lt;/p&gt;
&lt;p&gt;You might think Docker would be perfect here. Containers use namespaces and cgroups under the hood, which is exactly what we need for isolation and resource limiting. VPN daemons, though, need to create tunnel interfaces and manipulate routing tables, which requires CAP_NET_ADMIN and CAP_SYS_MODULE capabilities. Running a containerized Tailscale instance involves some friction.
You could run with the privileged flag and lose the isolation benefit, or pass specific capabilities and end up doing low-level networking setup inside the container anyway. You could also use host networking mode to let the container reach the host&apos;s network stack, but then you&apos;re back to a single shared network namespace and can&apos;t run two Tailscale instances simultaneously. Even with custom bridge networks and veth pairs between containers, you&apos;re essentially recreating the namespace setup inside Docker&apos;s abstraction layer, which adds complexity without much benefit. Docker shines for application isolation and multi-tenancy where you want resource limits, but for this specific task of running network daemons with their own stacks, the tooling overhead outweighs the containerization benefits.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Let&apos;s get into it.&lt;/h2&gt;
&lt;p&gt;We&apos;ll start by creating the namespace.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns add ts-b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify it&apos;s there:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip netns list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Right now it&apos;s basically a loopback interface and no internet access or anything really. So we need to hook it up with a virtual ethernet cable. A &lt;strong&gt;veth pair&lt;/strong&gt; is exactly what it sounds like: a virtual cable with two ends. Anything going into one end comes out the other.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip link add veth-host type veth peer name veth-ns
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both ends currently live in the host namespace. We need to fix that by setting one end to the host and the other in the tailscale one.&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;sudo ip link set veth-ns netns ts-b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the topology looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host namespace
    |
    veth-host
    |
  [cable]
    |
    veth-ns
    |
Namespace ts-b
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Host side:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip addr add 10.200.1.1/24 dev veth-host
sudo ip link set veth-host up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Namespace side:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns exec ts-b ip addr add 10.200.1.2/24 dev veth-ns
sudo ip netns exec ts-b ip link set veth-ns up
sudo ip netns exec ts-b ip link set lo up
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Without a default route, traffic from inside the namespace has nowhere to go. We tell it to send everything to the host:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns exec ts-b ip route add default via 10.200.1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;sudo sysctl -w net.ipv4.ip_forward=1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Now we need to handle NAT (Network Address Translation) so traffic leaving the namespace appears to come from the host. The namespace has its own private IP range (10.200.1.0/24), but external systems on the internet only know how to reach the host&apos;s IP. The host needs to &quot;masquerade&quot; outgoing traffic from the namespace, rewriting the source IP to the host&apos;s address.&lt;/p&gt;
&lt;p&gt;First, find your main network interface by running &lt;code&gt;ip link show&lt;/code&gt; and looking for your primary Ethernet or Wi-Fi interface (usually &lt;code&gt;eth0&lt;/code&gt;, &lt;code&gt;enp3s0&lt;/code&gt;, or something like &lt;code&gt;wlan0&lt;/code&gt;). Then add the masquerading rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo iptables -t nat -A POSTROUTING -s 10.200.1.0/24 -o eth0 -j MASQUERADE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This says: &quot;Any packet leaving the host out interface &lt;code&gt;eth0&lt;/code&gt; with source IP in 10.200.1.0/24 (our namespace), rewrite the source IP to the host&apos;s IP.&quot; Without this, the remote system would see packets coming from 10.200.1.2, have no way to route back to that private IP, and drop the response.&lt;/p&gt;
&lt;p&gt;But masquerading alone isn&apos;t enough. The Linux kernel has a &lt;code&gt;FORWARD&lt;/code&gt; chain that controls whether packets can traverse between interfaces. By default on many distributions, this chain has a policy of &lt;code&gt;DROP&lt;/code&gt;, meaning it refuses to forward anything unless explicitly allowed. We need to allow traffic from the namespace (veth-host) to the outside world (eth0) and allow responses back:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo iptables -A FORWARD -i veth-host -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o veth-host -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first rule allows outgoing traffic from the namespace. The second allows incoming traffic only if it&apos;s related to a connection we initiated (the &lt;code&gt;conntrack&lt;/code&gt; module tracks connection state). This way, the namespace can initiate connections, but random inbound traffic from the internet still gets dropped.&lt;/p&gt;
&lt;p&gt;Note: If your system uses &lt;code&gt;nftables&lt;/code&gt; instead of &lt;code&gt;iptables&lt;/code&gt;, add the equivalent rules to your nftables config instead of mixing firewall systems. You can check with &lt;code&gt;iptables --version&lt;/code&gt; or &lt;code&gt;nft list ruleset&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Before bringing up Tailscale, it&apos;s worth checking that the namespace can actually reach the internet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns exec ts-b ping -c 1 1.1.1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that works, the namespace has a way out and Tailscale has a fair shot at coming up cleanly.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;We&apos;ve got the namespace setup, so just need to launch the second &lt;code&gt;tailscaled&lt;/code&gt; inside the namespace, pointing it at its own state file and socket so it doesn&apos;t know the first one exists. If you want this to survive reboots, make sure to use a persistent path under something like &lt;code&gt;/var/lib/&lt;/code&gt; and &lt;code&gt;/var/run/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Btw &lt;code&gt;tailscaled&lt;/code&gt; runs in the foreground. Either open a second terminal for the next step, background it, or wire it up to a service manager later.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns exec ts-b tailscaled \
  --state=/tmp/tailscale-b.state \
  --socket=/tmp/tailscale-b.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;In another terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns exec ts-b tailscale \
  --socket=/tmp/tailscale-b.sock \
  up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authenticate the url and you are pretty much all set. The namespace spins up its own &lt;code&gt;tailscale0&lt;/code&gt; interface.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Check the host is still connected to tailnet A:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tailscale status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the namespace is connected to tailnet B:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns exec ts-b tailscale \
  --socket=/tmp/tailscale-b.sock \
  status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And just to see it with your own eyes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns exec ts-b ip addr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two &lt;code&gt;tailscale0&lt;/code&gt; interfaces, two separate tailnets, one machine.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What we actually built&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Linux host
│
├─ root namespace
│   tailscale0 → tailnet A
│
└─ namespace ts-b
    tailscale0 → tailnet B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each namespace has its own routing table,  firewall state, and network interfaces. They share a kernel but otherwise have no idea about each other.&lt;/p&gt;
&lt;p&gt;Once you have two separate networking stacks on one machine, you could let them talk to each other. Maybe you wanna easily send something from one tailnet to the other.  The host can selectively forward traffic between namespaces, which effectively turns it into a bridge between tailnets. From there it&apos;s a short walk to things like controlled access gateways between environments, smooth migration paths when you&apos;re moving infrastructure between networks, or exposing specific services from one tailnet into another without fully merging them. And of course, you get the rest of Tailscale&apos;s benefits like funnel, ssh etc. I spend most of my time split between a mac and linux machines, so I use both this approach and via Orbstack machines.&lt;/p&gt;
</content:encoded><category>linux</category><category>networking</category><author>Bill Njoroge</author></item><item><title>Infra and devtool themes I&apos;m excited about in 2024</title><link>https://bnjoroge.com/posts/infra-and-devtool-themes-2024/</link><guid isPermaLink="true">https://bnjoroge.com/posts/infra-and-devtool-themes-2024/</guid><description>A survey of the infrastructure layer beneath foundation models — from compute and deployment to prompt tooling, vector databases, and AI-native dev tools.</description><pubDate>Thu, 29 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This post was originally published on &lt;a href=&quot;https://substack.com/home/post/p-141139671&quot;&gt;Substack&lt;/a&gt; in January 2024. Re-published here with minor edits for tense and clarity.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So much happened in 2023, especially in cloud, data, and ML infrastructure, developer tools, and of course the craze of LLMs. Below are some themes that emerged prior to or became more mainstream in 2023, which I am excited about in 2024 and beyond. I should also mention these are merely interesting trends, not predictions. It’s an archive to look back to a few years down the line to see how they panned out.&lt;/p&gt;
&lt;h2&gt;data (and ml) stacks continue to be more composable&lt;/h2&gt;
&lt;p&gt;This trend predates 2023. Ever since the early 2000s, we’ve had many options at every layer of the data stack: new hardware (GPUs/specialized chips), different compute engines (dask/spark), databases, table formats, specialized engines (druid/clickhouse), SQL dialects, high-level dataframe APIs (pandas/polars).&lt;/p&gt;
&lt;p&gt;These options are great for builders but often come with a pretty expensive integration tax. Having a standardized interface, much like the JVM bytecode or LLVM IR, streamlines data exchange and ensures interoperability. Apache Arrow (originally just an in-memory columnar data specification, but now including low-level tools like Flight SQL, ADBC, and Datafusion) was one of the first projects that pioneered this trend.&lt;/p&gt;
&lt;p&gt;The other exciting component is &lt;a href=&quot;https://substrait.io/&quot;&gt;Substrait&lt;/a&gt;, which represents compute operations across different SQL parsers and execution engines. This is particularly useful for scenarios where users employ different frameworks (pandas/polars) or languages depending on data scale, or compile different SQL dialects/query languages like &lt;a href=&quot;https://www.malloydata.dev/&quot;&gt;Malloy&lt;/a&gt;. These components are implementation details and should be abstracted away from users. I&apos;m excited to see more high-level Arrow-native and Substrait-native data systems like &lt;a href=&quot;https://github.com/oap-project/gluten&quot;&gt;Gluten&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On the ML infra side, I am excited about projects like &lt;a href=&quot;https://www.ray.io/&quot;&gt;Ray&lt;/a&gt;, which contains multiple composable tools, &lt;a href=&quot;https://carton.run/&quot;&gt;Carton&lt;/a&gt; (which allows you to write application code in a different language from your Python ML inference), or &lt;a href=&quot;https://www.run.house/&quot;&gt;run.house&lt;/a&gt;. Even in LLMs, composability is key, with concepts such as LLM routing and model chaining.&lt;/p&gt;
&lt;h2&gt;s3 continues to re-define data infra&lt;/h2&gt;
&lt;p&gt;S3 (and compatible object storage like Cloudflare&apos;s R2) has become the default choice for source-of-truth persistent storage, moving away from disk-based volumes that most infrastructure products used previously. Initial use cases included data warehouses (&lt;a href=&quot;https://databend.rs/&quot;&gt;Databend&lt;/a&gt;), analytics databases (&lt;a href=&quot;https://chaossearch.com/&quot;&gt;Chaossearch&lt;/a&gt;), search engines (&lt;a href=&quot;https://quickwit.io/&quot;&gt;Quickwit&lt;/a&gt;), and columnar log storage (&lt;a href=&quot;https://www.datadoghq.com/blog/engineering/introducing-husky/&quot;&gt;Husky&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Recent use cases include serverless Postgres providers (&lt;a href=&quot;https://neon.tech/&quot;&gt;Neon&lt;/a&gt;), streaming platforms (&lt;a href=&quot;https://www.warpstream.com/&quot;&gt;Warpstream&lt;/a&gt;), vector databases (&lt;a href=&quot;https://lancedb.github.io/&quot;&gt;LanceDB&lt;/a&gt;), and even file systems or key-value stores. You get a lot for &quot;free&quot; by leveraging an S3 backend. Most hard distributed systems challenges — durability, availability, and consistency — are better delegated to battle-tested systems.&lt;/p&gt;
&lt;p&gt;One trade-off is higher latency. AWS introduced a new storage class, &lt;a href=&quot;https://press.aboutamazon.com/2023/11/aws-announces-the-general-availability-of-amazon-s3-express-one-zone&quot;&gt;S3 Express 1Z&lt;/a&gt;, whose access speed is up to 10x faster but costs more. While it&apos;s slower than Redis, it is faster than standard S3 while providing IAM and security policies out of the box.&lt;/p&gt;
&lt;h2&gt;postgres continues to be the universal database platform&lt;/h2&gt;
&lt;p&gt;Postgres has become the &lt;a href=&quot;https://db-engines.com/en/ranking_trend/system/PostgreSQL&quot;&gt;default database&lt;/a&gt; of choice, growing significantly faster than alternatives. Developers are using it for data warehousing (&lt;a href=&quot;https://www.hydra.so/&quot;&gt;Hydra&lt;/a&gt;), vector search (&lt;a href=&quot;https://github.com/pgvector/pgvector&quot;&gt;pgvector&lt;/a&gt;), machine learning (&lt;a href=&quot;https://postgresml.org/&quot;&gt;PostgresML&lt;/a&gt;), and search and analytics (&lt;a href=&quot;https://www.paradedb.com/&quot;&gt;ParadeDB&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Not having to manage separate infrastructure for each use case is a massive productivity unlock for data teams. Bundlers like &lt;a href=&quot;https://github.com/omnigres/omnigres&quot;&gt;Omnigres&lt;/a&gt; take this further by including caching, auth, and deployment logic. Putting logic in the database was an anti-pattern back in the day — maybe we are coming full circle?&lt;/p&gt;
&lt;h2&gt;local-first finally becoming mainstream&lt;/h2&gt;
&lt;p&gt;Local-first software has been buzzing with the rise of multiplayer applications like Google Docs. The &lt;a href=&quot;https://www.inkandswitch.com/local-first/&quot;&gt;benefits&lt;/a&gt; — security, privacy, offline capabilities — are clear, though the tooling remains nascent compared to client-server.&lt;/p&gt;
&lt;p&gt;As developers, we still grapple with CRDTs and syncing complexities, but interesting projects are bridging the gap:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQLite-based&lt;/strong&gt;: &lt;a href=&quot;https://vlcn.io/docs/cr-sqlite/intro&quot;&gt;cr-sqlite&lt;/a&gt;, &lt;a href=&quot;https://sqlsync.dev/&quot;&gt;SQLSync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Postgres-centric&lt;/strong&gt;: &lt;a href=&quot;https://electric-sql.com/&quot;&gt;ElectricSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full-stack&lt;/strong&gt;: &lt;a href=&quot;https://www.triplit.dev/&quot;&gt;Triplit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ml in database internals&lt;/h2&gt;
&lt;p&gt;There are many databases &lt;em&gt;for&lt;/em&gt; ML, but fewer production use cases for applying classical ML &lt;em&gt;within&lt;/em&gt; core database operations. &lt;a href=&quot;https://ottertune.com/&quot;&gt;OtterTune&lt;/a&gt; is the gold standard for configuration tuning, but I&apos;m excited about applications in query optimization (beyond simple costs), learned indices, join order planning, compression techniques, and workload prediction.&lt;/p&gt;
&lt;h2&gt;workflow engines&lt;/h2&gt;
&lt;p&gt;Everything is a workflow. Durable Execution especially has been a hot space. While &lt;a href=&quot;https://temporal.io/&quot;&gt;Temporal&lt;/a&gt; remains the de-facto standard, the ecosystem is evolving rapidly. Managing state is hard, and I am excited to see how this space converges.&lt;/p&gt;
&lt;h2&gt;minimizing the feedback loop&lt;/h2&gt;
&lt;p&gt;The current dev workflow — working locally, pushing to a branch, waiting for CI, code review — is often painful. I&apos;m excited about ideas that reduce this loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local emulation&lt;/strong&gt;: &lt;a href=&quot;https://localstack.cloud/&quot;&gt;LocalStack&lt;/a&gt;, &lt;a href=&quot;https://www.winglang.io/&quot;&gt;Wing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote collaboration&lt;/strong&gt;: &lt;a href=&quot;https://tunnel.dev/&quot;&gt;Tunnel&lt;/a&gt;, &lt;a href=&quot;https://zed.dev/&quot;&gt;Zed&lt;/a&gt; collaboration features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local CI&lt;/strong&gt;: &lt;a href=&quot;https://dagger.io/&quot;&gt;Dagger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ephemeral dev environments&lt;/strong&gt;: Reducing the reliance on a messy localhost setup.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;web assembly (wasm)&lt;/h2&gt;
&lt;p&gt;Wasm has been the rage for a while, but its most interesting use case is extending projects by running code in high-level languages within restricted environments (like UDFs in databases). Projects like &lt;a href=&quot;https://extism.org/&quot;&gt;Extism&lt;/a&gt;, &lt;a href=&quot;https://www.pingcap.com/blog/how-webassembly-powers-databases-build-a-udf-engine-with-wasm/&quot;&gt;TiDB UDFs&lt;/a&gt;, &lt;a href=&quot;https://www.convex.dev/&quot;&gt;Convex&lt;/a&gt;, and &lt;a href=&quot;https://spacetimedb.com/&quot;&gt;SpacetimeDB&lt;/a&gt; are proving that Wasm is a powerful layer for interoperability.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I had a ton of fun writing this. A lot of these areas have interesting, unsolved technical challenges. Let me know what areas you are excited about or working on!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This post was originally published on Substack in January 2024. Re-published here with minor edits for tense and clarity.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>engineering</category><category>infrastructure</category><author>Bill Njoroge</author></item><item><title>AI-native software infrastructure: what I was excited about in 2023</title><link>https://bnjoroge.com/posts/ai-native-software-infrastructure/</link><guid isPermaLink="true">https://bnjoroge.com/posts/ai-native-software-infrastructure/</guid><description>A survey of the infrastructure layer beneath foundation models — from compute and deployment to prompt tooling, vector databases, and AI-native dev tools.</description><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This post was originally published on &lt;a href=&quot;https://medium.com/@williamsriunge/ai-native-software-infrastructure-what-im-excited-about-in-2023-9556474a6430&quot;&gt;Medium&lt;/a&gt; in January 2023. Re-published here with minor edits for tense and clarity.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Over the past decades, we&apos;ve seen different platform shifts — from the web to the cloud to mobile — create immense value. Everyone has been speculating on what the post-mobile platform shift is (from DeFi to 5G etc). AI (loosely referring to deep learning in this context) is deservedly poised to be the next platform upon which billions of value will be derived.&lt;/p&gt;
&lt;p&gt;Over the past two decades, AI has seen remarkable progress, but most of the models have been task-specific. After Google Research released the Transformers Architecture in a 2017 research paper, &lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;Attention is All You Need&lt;/a&gt;, which proposed a new architecture that was much easier to parallelize (read: better performance), quicker to train and has the ability to generalize across discrete tasks, the term &lt;strong&gt;Foundation Models&lt;/strong&gt; (FMs) became more mainstream. From the State of AI report, &quot;The Transformer architecture has expanded far beyond NLP and is emerging as a general purpose architecture for ML&quot;. FMs have two crucial attributes: &lt;strong&gt;emergence&lt;/strong&gt; (ability to exhibit new behaviors implicitly) and &lt;strong&gt;generalizability&lt;/strong&gt;, the ability to be used as a base for multiple use cases.&lt;/p&gt;
&lt;p&gt;As with every technology shift, the applications are what&apos;s always exciting but as an infrastructure nerd, I tend to lean more on the enablers of the application layer. As they say, in every gold rush, you ideally want to be the one selling shovels. I write about a few areas I was particularly excited about both as a developer and as a (budding) VC with a particular interest in investing in technology startups.&lt;/p&gt;
&lt;h2&gt;infrastructure&lt;/h2&gt;
&lt;p&gt;Over the past couple of years, we saw an increasing focus on building the infrastructure to run, deploy and manage models at scale, leading to the emergence of the practice, &lt;a href=&quot;https://ml-ops.org/&quot;&gt;MLOps&lt;/a&gt; (which encapsulates data validation, model testing, evaluation, deployment, versioning, etc). For enterprises to reliably productionize FMs, however, there&apos;s a need for an extended version of MLOps. Since retraining FMs, and in particular LLMs (a variation of FMs trained on corpus amounts of textual data), is prohibitively expensive given how incredibly huge they are (GPT-3 has 175 billion parameters), there has been a huge focus on using a data-driven approach, necessitating improved ways of managing FMs at scale.&lt;/p&gt;
&lt;h2&gt;integrating FMs with different entities&lt;/h2&gt;
&lt;p&gt;This is more of a &quot;middle layer&quot;, actually. Combining FMs with computation or external memory/knowledge exponentially increases their capabilities. The most common implementation of this was through LLMs, by &lt;a href=&quot;https://langchain.readthedocs.io/&quot;&gt;Langchain&lt;/a&gt;. Langchain offered interesting capabilities such as agents that execute different &quot;actions&quot; (using a context manager), or memory to enable persistence across different agent calls, interoperability between different LLMs, and the ability to test, template, experiment and emulate various prompts at scale.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gpt-index.readthedocs.io/en/latest/&quot;&gt;GPT-Index&lt;/a&gt; was also a very exciting project that offered a simple and extensible interface between external data and your LLMs. It helped resolve prompt-size limitations allowing you to query external data instead of updating the model&apos;s weights.&lt;/p&gt;
&lt;p&gt;I was excited about more infra innovation around support for multiple modalities — allowing for absolute interoperability and even more exciting applications — an area I was exploring with medical data while at Hopkins.&lt;/p&gt;
&lt;h2&gt;better tooling for prompt engineering&lt;/h2&gt;
&lt;p&gt;Prompt Engineering proved to be a very effective way of improving the accuracy of LLMs&apos; outputs. Various techniques emerged such as &lt;strong&gt;zero-shot&lt;/strong&gt; (prompt with no examples) and &lt;strong&gt;few-shot&lt;/strong&gt; (prompt with one or &lt;em&gt;n&lt;/em&gt; examples). Getting the right prompt involves a lot of iterations, and being able to do that at the enterprise level requires solid infrastructure. For instance, changing the order of the few shots, or versioning the various inputs can influence the LLM performance.&lt;/p&gt;
&lt;p&gt;I was also excited about ideas such as &lt;a href=&quot;https://arxiv.org/pdf/2212.06094.pdf&quot;&gt;Language Model Programming&lt;/a&gt;, which sought to provide more expressiveness and granularity when querying LLMs, increasing not only the accuracy of the outputs but also yielding cost savings.&lt;/p&gt;
&lt;p&gt;Additionally, I thought we&apos;d see search engines or databases specifically for storing prompts and the corresponding outputs. At an organization level, this would ensure better reproducibility, especially across workflows.&lt;/p&gt;
&lt;h2&gt;compute infra&lt;/h2&gt;
&lt;p&gt;Training and deploying FMs is extremely expensive. For reference, with the lowest-cost GPU, the cost of training GPT-3 was &lt;a href=&quot;https://lambdalabs.com/blog/demystifying-gpt-3&quot;&gt;$4.6 Million&lt;/a&gt;. Additionally, for every copy of a foundation model tweaked to serve a new purpose — such as a model that translates to French and another that translates to Mandarin — you have to host a new version of that model. To achieve massive scale, there is a need to make compute less expensive.&lt;/p&gt;
&lt;p&gt;There were various approaches to reduce the training costs such as using hardware-specific processors (ASICs) or Google TPUs. Other companies such as Mosaic ML &lt;a href=&quot;https://www.mosaicml.com/blog/gpt-3-quality-for-500k&quot;&gt;demonstrated&lt;/a&gt; that using various software-centric approaches such as data parallelism can significantly improve the cost/performance ratio for FMs. &lt;strong&gt;Data-centric ML&lt;/strong&gt;, as coined by &lt;a href=&quot;https://landing.ai/data-centric-ai/&quot;&gt;Andrew Ng&lt;/a&gt;, was going to be even more relevant. I thought we&apos;d see a more active focus on low-hanging optimizations to significantly reduce the cost of deploying FMs, whether that was medium-sized implementations of LLMs, such as &lt;a href=&quot;https://github.com/karpathy/nanoGPT&quot;&gt;nanoGPT&lt;/a&gt;, new &lt;a href=&quot;https://arxiv.org/abs/2205.05198&quot;&gt;parallelism&lt;/a&gt; techniques, or reducing the re-computation of different transformer layers.&lt;/p&gt;
&lt;p&gt;Once the FMs have been deployed, another infrastructure layer is needed to enable applications to do &lt;strong&gt;inference&lt;/strong&gt; (making predictions using new data — or rather, a lot of matrix multiplications) from the models. This particular infrastructure has to handle low latency and high throughput. I was excited about different ways to accelerate inference, whether that&apos;s by using hardware architectures (FPGA, TPU, etc), software (graph compilers, etc), or algorithms (pruning, quantization, etc). One company building in this space that I was excited about was &lt;a href=&quot;https://www.modular.com/blog/increasing-development-velocity-of-giant-ai-models&quot;&gt;Modular&lt;/a&gt;, which was solving hard problems at the intersection of compilers and AI.&lt;/p&gt;
&lt;h2&gt;deployment infra&lt;/h2&gt;
&lt;p&gt;A huge focus on open-source and community-led development was critical to ensuring AI becomes more mainstream. Meta&apos;s FAIR released &lt;a href=&quot;https://ai.facebook.com/blog/multiray-large-scale-AI-models/&quot;&gt;Multi-ray&lt;/a&gt;, a platform for running state-of-the-art AI models at scale that allows multiple models to run on the same input and share the majority of processing costs while incurring only a small per-model cost. While that was Meta-specific, I thought we&apos;d see organizations with AI-intensive workloads adopt firm-wide frameworks to deploy their FMs across different business units.&lt;/p&gt;
&lt;p&gt;In a similar vein to MLOps, I thought Infrastructure as Code (IaC) would become even more relevant for productionizing (open source) FMs. The current de-facto tool is Hashicorp&apos;s Terraform. At Nvidia, that past summer, one of my projects was to transition my team&apos;s ML workflow orchestration pipeline from a YAML-based approach to a more declarative framework using high-level languages. This introduced benefits such as composability, flexibility, less error-prone config files, and much less redundancy.&lt;/p&gt;
&lt;h2&gt;security and safety infra&lt;/h2&gt;
&lt;p&gt;Current FMs are huge, multi-billion-parameter black boxes that make it incredibly hard to not only explain but also assess risks and vulnerabilities of using the model. These security guarantees are absolutely necessary for use cases such as healthcare or finance. It is thus imperative to ensure that — even as the attack surface increases with downstream applications — the potential vulnerabilities, which can include model artefacts, corrupted training data, potential to expose data after fine-tuning, and package dependency vulnerabilities such as the recent one in &lt;a href=&quot;https://pytorch.org/blog/compromised-nightly-dependency/&quot;&gt;PyTorch&apos;s nightly build&lt;/a&gt;, are mitigated.&lt;/p&gt;
&lt;p&gt;Just as important is to think about how the security framework between infrastructure providers (such as OpenAI or Anthropic) and applications will evolve. I thought we&apos;d see a shared responsibility model, similar to that of cloud companies, where both players play a role in ensuring security guarantees.&lt;/p&gt;
&lt;h2&gt;tooling&lt;/h2&gt;
&lt;h3&gt;embeddings infra&lt;/h3&gt;
&lt;p&gt;From OpenAI, embeddings are &quot;numerical representations of concepts converted to number sequences, which make it easy for computers to understand the relationships between those concepts&quot;. One of the most remarkable developments by OpenAI that never got as much public limelight was their &lt;a href=&quot;https://openai.com/blog/new-and-improved-embedding-model/&quot;&gt;new Embeddings Model&lt;/a&gt;, which outperformed their previous model at most tasks while being 99.98% cheaper and having a context window that&apos;s twice as large. I was excited to see more projects that not only host embeddings from multiple models (performance varies across tasks) but also allow for fine-tuning, ideally with an on-premise option.&lt;/p&gt;
&lt;h3&gt;vector databases&lt;/h3&gt;
&lt;p&gt;Unstructured data (which includes images, video, text, and audio) accounts for &lt;a href=&quot;https://www.ibm.com/ibm/history/ibm100/us/en/icons/takmi/&quot;&gt;80-90%&lt;/a&gt; of any organization&apos;s data. Being able to index, store and search across them instead of human-generated labels or tags is exactly what vector databases were meant to solve. They have direct use cases especially in building better semantic search applications and recommendation systems. Open source vector databases on my radar included &lt;a href=&quot;https://www.pinecone.io/&quot;&gt;Pinecone&lt;/a&gt;, &lt;a href=&quot;https://weaviate.io/&quot;&gt;Weaviate&lt;/a&gt;, and &lt;a href=&quot;https://milvus.io/&quot;&gt;Milvus&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;AI-native dev tools&lt;/h3&gt;
&lt;p&gt;I always think of developer tools as a hidden 10x multiplier for not only engineering productivity but also output. Current AI-native dev tools such as GitHub Copilot and Replit&apos;s GhostWriter only scratch the surface of what is possible. I thought we&apos;d see AI seep even further down the dev tool stack with eventually the ability to not only generate but also execute code and perform optimizations ad-hoc. Some attempts at this included &lt;a href=&quot;https://cursor.so/&quot;&gt;Cursor&lt;/a&gt; (AI-native code editor), &lt;a href=&quot;https://github.com/plasma-umass/scalene&quot;&gt;Scalene&lt;/a&gt; (AI-optimized Python profiler), and even more ambitious ideas such as building a GPT-only backend.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;While most consumer-centric generative AI applications captured much of the public limelight, I believed most of the value accrual would — just like in enterprise software — be verticalized. AI would become as table-stakes as cloud-native or mobile-native has been over the past decade. Rather than building multiple models for different use cases and datasets, companies would focus on using proprietary data to enhance foundation models and using them to build more intelligent applications. 2023 was going to be a very defining year for AI, especially at the infrastructure layer.&lt;/p&gt;
</content:encoded><category>engineering</category><category>ai</category><author>Bill Njoroge</author></item><item><title>Python Attribute Lookup</title><link>https://bnjoroge.com/posts/wilt-1/</link><guid isPermaLink="true">https://bnjoroge.com/posts/wilt-1/</guid><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Python Attribute Lookup&lt;/h3&gt;
&lt;p&gt;In an effort to understand the very basics, I&apos;ve been implementing classes in Python from scratch, starting with the functions and dictionaries. I was curious how to implement attribute lookup how python actually does it. So I went down a rabbit hole and came across this &lt;a href=&quot;https://blog.peterlamut.com/2018/11/04/python-attribute-lookup-explained-in-detail/&quot;&gt;article&lt;/a&gt; that covers it exhaustively. Learned a great deal about how alot of the higher level ORMS, frameworks like FastAPI and dataclasses are implemented.&lt;/p&gt;
&lt;p&gt;Some key takeaways are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Instance variables take precedence over class variables.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;class Foo():
x = &apos;Foo class attribute&apos;
    
x = Foo()
x.name 

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From above, x.name essentially does a &lt;code&gt;x.__get__attribute(&quot;name&quot;)&lt;/code&gt; which checks in the instance&apos;s attribute dict first, doesnt find it, then checks in the &lt;code&gt;class.__dict__&lt;/code&gt; which it does find.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;data descriptors take precedece over instance, class and non-data descriptors.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also have things called descriptors. These are basically just objects that change how you access the different object attributes. There&apos;s two:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a) Data descriptors&lt;/li&gt;
&lt;li&gt;b) Non-data descriptors.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Data descriptors have  &lt;code&gt;__set__&lt;/code&gt;, &lt;code&gt;__delete__&lt;/code&gt; and &lt;code&gt;__get__&lt;/code&gt; methods implemented while non-data descriptors only have &lt;code&gt;__get__&lt;/code&gt; implemented. The easiest example of this is the &lt;code&gt;@property&lt;/code&gt; object. Under the hood, it&apos;s basically a data descriptor.&lt;/p&gt;
&lt;p&gt;The data descriptor is the first thing that Python looks up when trying to get an attribute. These property descriptors also override an instance&apos;s attributes, non-data descriptors, and plain class attributes.&lt;/p&gt;
&lt;p&gt;To illustrate this,  here&apos;s an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo:
    @property
    def x(self):
        return &quot;computed&quot;

obj = Foo()
obj.__dict__[&quot;x&quot;] = &quot;shadow?&quot;
obj.x

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will actually return &lt;code&gt;computed&lt;/code&gt; and not &lt;code&gt;shadow&lt;/code&gt; because during &lt;code&gt;@property&lt;/code&gt; is a data descriptor, thus Python will look at it&apos;s &lt;code&gt;__set__&lt;/code&gt; or &lt;code&gt;__get__&lt;/code&gt; methods first which x is. Thus it will return before we saw the instance &lt;code&gt;obj&lt;/code&gt; attribute &lt;code&gt;shadow&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So the general approach is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Data descriptor (&lt;code&gt;property&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Instance &lt;code&gt;__dict__&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Non‑data descriptor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Class attribute&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MRO&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;__getattr__&lt;/code&gt;
In the case of where you have multiple inheritances, python uses Method resolution Order to access the attribute, specifically the C3 linearization algorithm.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Borrowing an image and example from the above article:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A(object): pass

class B(object):

    x = x from B

class C(A, B):pass

class D(B):

    x = x from D
    
class E(C, D): pass

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![[Pasted image 20260301141019.png]]&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; E.__mro__
(__main__.E, __main__.C, __main__.A, __main__.D, __main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C3 linearization produces an MRO that satisfies three constraints:&lt;/p&gt;
&lt;h4&gt;1. &lt;strong&gt;Local precedence order&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;If a class lists bases as &lt;code&gt;class C(A, B)&lt;/code&gt;, then &lt;code&gt;A&lt;/code&gt; must appear before &lt;code&gt;B&lt;/code&gt; in the MRO of &lt;code&gt;C&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;2. &lt;strong&gt;Monotonicity&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;A subclass’s MRO must preserve the order of its parents’ MROs. This prevents “jumps” that would break inheritance consistency.&lt;/p&gt;
&lt;h4&gt;3. &lt;strong&gt;No contradictions&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;If two parents disagree on ordering, Python must find a consistent merge or raise an error.&lt;/p&gt;
&lt;p&gt;This has made it so much easier to understand what frameworks like FastAPI and ORMs do behind the scenes.&lt;/p&gt;
</content:encoded><category/><category>python</category><author>Bill Njoroge</author></item><item><title>Binary Heaps in Python</title><link>https://bnjoroge.com/posts/binary-heaps-in-python/</link><guid isPermaLink="true">https://bnjoroge.com/posts/binary-heaps-in-python/</guid><description>Understanding binary heaps and their implementation in Python</description><pubDate>Tue, 11 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A binary heap is a data structure where the topmost element is either the smallest or the largest. It&apos;s useful in cases where you need to keep track of some sort of order or even sort elements as in the sorting algorithm variant. I like to think of them as a binary tree where, in the case of a min heap, the root node is the smallest element in entire tree, and in the case of a max heap, it&apos;s the largest element in the entire tree. This is also going to be in Python, which as of &lt;a href=&quot;https://docs.python.org/3/library/heapq.html#heapq.heapify_max&quot;&gt;python 3.10+ has max heap support via &lt;code&gt;heapify_max()&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By default, I&apos;ll be referring to a minheap when I mention a heap, and will explicitly mention the max heap. I&apos;m also going to be keeping the tree balanced in most cases by assuming they are complete binary trees. That gives us a better chance at getting optimal performance of &lt;strong&gt;log(n)&lt;/strong&gt;. If the tree is skewed heavily towards either side, that degenerates to &lt;strong&gt;O(N)&lt;/strong&gt; time complexity.&lt;/p&gt;
&lt;p&gt;We rely on the &lt;strong&gt;heap property&lt;/strong&gt; which is basically that, for every node &lt;em&gt;i&lt;/em&gt; with parent &lt;em&gt;p&lt;/em&gt; , the key in &lt;em&gt;p&lt;/em&gt; is smaller or equal to the key in &lt;em&gt;i&lt;/em&gt;.  It&apos;s important to note that a heap is not globally ordered per se, but partially ordered. At each level, neither child is guaranteed to be sorted with respect to the other.&lt;/p&gt;
&lt;p&gt;In the image below, the first tree is a minheap, and as you can see 6 is the least element in the tree, while in the second tree, 17 is the largest element in the tree.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/heap-min-max-comparison.png&quot; alt=&quot;Min heap and max heap comparison&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can represent a heap using its level order traversal in an array. For the &lt;em&gt;ith&lt;/em&gt; element in an array, you can access the children as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;left_child = 2*i + 1
right_child = 2*i + 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inversely, you can access the parent node of a specific child by doing the reverse of the above for some child node &lt;em&gt;i&lt;/em&gt;. We want to take the floor because for the right child, &lt;em&gt;(i-1)/2&lt;/em&gt; would not give you an integer for some &lt;em&gt;even i&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parent_node = (i-1)//2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A heap supports the typical operations of a data structure such as being able to insert, delete(from a specific point), find nodes and being able to build it from a list.
To insert to the heap, we want to do it at the end of the heap. This is fairly easy, but unfortunately it might violate the heap property if the value to be inserted is larger than the parent( and that might be the case for the parent&apos;s parent). So we somehow need to maintain this property by recursively comparing it with the parent. This is often called &lt;strong&gt;percolating up&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the worst case, we might need to keep doing it until we get to the root of the tree so in the worst case, the time complexity for doing an insert is bounded by the height of the tree. Since we assumed it&apos;s a complete balanced binary tree, that&apos;s approximately &lt;em&gt;log(n)&lt;/em&gt;. Technically there&apos;s some constant just before it but we can ignore it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def insert(heap, value):
    heap.append(value)
    i = len(heap) - 1
    
    # Percolate up
    while i &amp;gt; 0:
        parent = (i - 1) // 2
        if heap[parent] &amp;lt;= heap[i]:
            break  # Heap property satisfied
        heap[parent], heap[i] = heap[i], heap[parent]
        i = parent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For deletion operations, we need to pop the root which is the only interface to do it. We also need to maintain the heap order, so ensuring that whatever value we replace as the root maintains the heap order. To do so, we need to &lt;em&gt;percolate down&lt;/em&gt; We can replace the last element as the root, and then, we can ensure this new root maintains the heap order. So we need to swap the root with the smaller child and recursively do it until we get to some leaf node. Ends up again being about &lt;em&gt;log(N)&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def pop_min(heap):
    if not heap:
        return None
    
    min_val = heap[0]
    heap[0] = heap[-1]
    heap.pop()
    
    # Percolate down
    i = 0
    while True:
        left = 2*i + 1
        right = 2*i + 2
        smallest = i
        
        if left &amp;lt; len(heap) and heap[left] &amp;lt; heap[smallest]:
            smallest = left
        if right &amp;lt; len(heap) and heap[right] &amp;lt; heap[smallest]:
            smallest = right
        
        if smallest == i:
            break  # Heap property satisfied
        
        heap[i], heap[smallest] = heap[smallest], heap[i]
        i = smallest
    
    return min_val
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can build a heap in two ways. Inserting the elements one at a time or using the entire list. If you insert using proper heap operations, the cost is O(n log n). If you insert into a sorted list, the cost is O(n²). Heapify is O(n), which is even better. We can find the place to insert it at using binary search so &lt;em&gt;log(n)&lt;/em&gt; but inserting in the middle of a python list is actually &lt;em&gt;O(n)&lt;/em&gt; because you have to shift the elements over. If you are inserting n items, the cumulative cost is &lt;em&gt;O(n)²&lt;/em&gt; .&lt;/p&gt;
&lt;p&gt;The other approach is to build the heap directly from the list itself. Python exposes a &lt;em&gt;heapify()&lt;/em&gt; api that makes this trivial and has a time complexity of &lt;em&gt;O(n)&lt;/em&gt;. The gist of how it works is that, since a binary heap, is a complete tree, most of the nodes are at the bottom or close to it, so the actual cost to &lt;em&gt;sift down&lt;/em&gt; is very cheap, maybe 0 or 1. We start by identifying the last parent node using the formula above. We assume the leaf nodes are valid heaps since they have no child nodes.  In fact, while the math can get a bit complicated the code is relatively straightforward. It&apos;s some variations of this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def heapify(arr):
    n = len(arr)
    # Start from last parent node
    for i in reversed(range(n // 2)):
        sift_down(arr, i, n)

# For an input like n=6, we would go to nodes 2, 1, 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using Python&apos;s &lt;code&gt;heapq&lt;/code&gt; module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import heapq

# Create a min heap from a list
heap = [3, 1, 4, 1, 5, 9, 2, 6]
heapq.heapify(heap)  # O(n) - in-place transformation

# Insert an element
heapq.heappush(heap, 0)  # O(log n)

# Pop the minimum element
min_val = heapq.heappop(heap)  # O(log n)

# Push then pop (more efficient than separate calls)
val = heapq.heappushpop(heap, 7)  # O(log n)

# Pop then push
val = heapq.heapreplace(heap, 8)  # O(log n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For max heaps in python versions less than 3.10, since &lt;code&gt;heapq&lt;/code&gt; supports min heaps natively, you can negate values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Max heap simulation using min heap
max_heap = [-x for x in [3, 1, 4, 1, 5]]
heapq.heapify(max_heap)

# Get max (negate back)
max_val = -heapq.heappop(max_heap)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or use Python 3.10+ &lt;code&gt;heapify_max()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from heapq import heapify_max, _heappop_max

max_heap = [3, 1, 4, 1, 5]
heapify_max(max_heap)
max_val = _heappop_max(max_heap)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;ve looked at binary heaps, and some of their operations. The next post is going to be focused on patterns that occur when dealing with binary heap types of problems in interviews.&lt;/p&gt;
</content:encoded><category>leetcode</category><author>Bill Njoroge</author></item></channel></rss>