<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Fenils Blog</title><description>What does it mean to be strong Takamura-san?</description><link>https://fknil.pages.dev/</link><item><title>Upgrading to neovim 0.12</title><link>https://fknil.pages.dev/blog/neovim-v12/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/neovim-v12/</guid><pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently upgraded my neovim to 0.12.2, I have been chipping away at it slowly using &lt;code&gt;NVIM_APPNAME&lt;/code&gt; feature so that my day to day activities are not hindered. One of the things I found interesting in using &lt;code&gt;NVIM_APPNAME&lt;/code&gt; is, I saw a blog where author manually create all the &lt;code&gt;~/.local/share/nvim-next&lt;/code&gt; etc dirs, I did the same cause I was not aware, but later I realized that one can literally just say &lt;code&gt;NVIM_APPNAME=nvim-next ~/.local/share/bob/0.12/bin/nvim&lt;/code&gt; and neovim will automatically make all those dirs for you. Also yes, I use awesome &lt;a href=&quot;https://github.com/MordechaiHadad/bob&quot;&gt;bob-nvim&lt;/a&gt; for managing neovim versions. With that out of the way, lets get started!&lt;/p&gt;
&lt;h2&gt;Major changes&lt;/h2&gt;
&lt;p&gt;Cool, lets talk about major changes, listing them out first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vim.pack&lt;/code&gt;: Migrating to builtin package manager&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lsp&lt;/code&gt;: Ditching nvim-lspconfig etc for &lt;code&gt;vim.lsp.enable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ui2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;no vimscript in config&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Migrating to builtin package manager&lt;/h3&gt;
&lt;p&gt;Neovim now comes with an inbuilt package manager called &lt;code&gt;vim.pack&lt;/code&gt;, its currently experimental but is considered good enough for daily driving. I have used a bunch of package managers over time, vim-plugin, packer, lazy and now &lt;code&gt;vim.pack&lt;/code&gt;. While I don&apos;t care about them a lot, each migration has been inspired by reasons. vim-plug -&amp;gt; packer, lua shift of whole ecosystem. packer -&amp;gt; lazy.nvim, extra features like dependencies, clean config, etc and finally lazy -&amp;gt; vim.pack cause I want to reduce count of external dependencies. With all the changes happening upstream, I am really hopeful that some day my whole config will just fit in a small file!&lt;/p&gt;
&lt;p&gt;But I couldn&apos;t simply move to using vim.pack, I had to evaluate if it was strictly an upgrade. Factors I looked for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do I depend on dependencies feature of lazy?&lt;/li&gt;
&lt;li&gt;Does it slow down my startup times?&lt;/li&gt;
&lt;li&gt;Does separating config in &lt;code&gt;vim.pack&lt;/code&gt; make config structure worse?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For &lt;code&gt;dependencies&lt;/code&gt; section, I went through my plugin list and realized I had these dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;aerial.nvim on nvim-treesitter and nvim-web-devicons&lt;/li&gt;
&lt;li&gt;telescope on telescope-fzy-native, telescope-live-grep-args and plenary.nvim&lt;/li&gt;
&lt;li&gt;nvim-treesitter-context on nvim-treesitter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These don&apos;t seem that bad, all of them are loaded much later in the neovim startup process and I could just keep them in a particular order to make the resolution pass. Well I tried it and that did work out, so ticked this off.&lt;/p&gt;
&lt;p&gt;For my startup times, I used &lt;code&gt;nvim --startuptime startuptime.log .&lt;/code&gt;. If you are not familiar with this command, it instructs neovim to write a log of its startup activities and to get startup time you would look for log &lt;code&gt;--- NVIM STARTED ---&lt;/code&gt;, first number in that row is the amount of seconds it took to startup your neovim. I measured this and realized I hadn&apos;t used &quot;lazy&quot; in lazy.nvim package manager 😭. But I never felt the need to make it go faster, cause I wasn&apos;t able to perceive a delay when starting it. Day I start noticing the delay is the day I bring down my hammer. I had done this earlier for my &lt;a href=&quot;https://github.com/feniljain/blogs/tree/main/2024/faster-shell-boot&quot;&gt;shell&lt;/a&gt; too. But after the migration, timings seemed the same, actually a bit better than lazy.nvim, so I was already happy :)&lt;/p&gt;
&lt;p&gt;For config structure, I really liked how lazy forced a dir structure of &lt;code&gt;plugins&lt;/code&gt; and encouraged keeping config along side plugin installation line itself. It was a clean way. With &lt;code&gt;vim.pack&lt;/code&gt; I had two ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keep installation line with config in top level &lt;code&gt;plugin/&lt;/code&gt; dir&lt;/li&gt;
&lt;li&gt;keep all installation lines together and config separately&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted to maintain order remember? That can easily be done with a single &lt;code&gt;vim.pack.add&lt;/code&gt; and listing down all the plugins together with their install order. But with &lt;code&gt;plugin/&lt;/code&gt; dir, I would have to name files &lt;code&gt;0_&lt;/code&gt;, &lt;code&gt;1_&lt;/code&gt; etc. This is because files in &lt;code&gt;plugin/&lt;/code&gt; are loaded automatically by vim and neovim in sorted order. Naming files like that was a turn off for me, so I went with single &lt;code&gt;vim.pack.add&lt;/code&gt; call and created a new dir called &lt;code&gt;plugins/&lt;/code&gt; in &lt;code&gt;lua/&lt;/code&gt; dir and shoved all plugin related config there. I enforced &lt;a href=&quot;(https://github.com/feniljain/dotfiles/blob/9681a844aadc971dae416836e2dc75feb328b8d3/nvim/.config/nvim/lua/fenil/plugins/init.lua#L1-L8)&quot;&gt;setup order&lt;/a&gt; in &lt;code&gt;lua/plugins/init.lua&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With all of this out of the way, migration was really SMOOTH! And I kinda love that I am not pulling a heavy dependency like lazy.nvim in my dep tree :)&lt;/p&gt;
&lt;p&gt;If you have advanced usecases or want to understand the feature better, there are two awesome guides: official manual and then https://echasnovski.com/blog/2026-03-13-a-guide-to-vim-pack.html. Read it end-to-end, each section has something you can take away. It is the most comprehensive guide out there right now.&lt;/p&gt;
&lt;p&gt;One small trick I learned from the article above is placing &lt;code&gt;vim.loader.enable&lt;/code&gt; speeds up startup times for free and this is blessed on us by Folke himself!! Ofc I added that and instantly realized 25ms off the loading time xD&lt;/p&gt;
&lt;h3&gt;LSP&lt;/h3&gt;
&lt;p&gt;Neovim v0.12 also brings in more ergonomic LSP usage support. Now, you can place LSP server setting in &lt;code&gt;lsp/&lt;/code&gt; dir and just call &lt;code&gt;vim.lsp.enable(&amp;lt;file-name-in-lsp-dir&amp;gt;)&apos;&lt;/code&gt; and this is all the setup you need! &lt;code&gt;nvim-lspconfig&lt;/code&gt; is now reduced to just maintain settings for upstream LSP servers. As these rarely change, I just copied from upstream and placed in my &lt;code&gt;lsp/&lt;/code&gt; dir, I also realized I only use two of them now: rust_analyzer and taplo (toml LSP server, also for rust dev :P). With this, I was able to cut down a bunch of lines in my LSP config and also trim down deps of &lt;code&gt;nvim-lspconfig&lt;/code&gt; and &lt;code&gt;mason-lspconfig&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;ui2&lt;/h3&gt;
&lt;p&gt;This is an experimental feature where command line meets messages meets pager meets dialog windows. Honestly its best explained in &lt;a href=&quot;https://neovim.io/doc/user/lua/#ui2&quot;&gt;official docs&lt;/a&gt;. This was an interesting change which allowed me to trim down a dep I really liked: &lt;code&gt;fidget.nvim&lt;/code&gt;. It shows LSP progress on the bottom right corner. Now I do it in ui2 itself using &lt;a href=&quot;https://github.com/feniljain/dotfiles/blob/9681a844aadc971dae416836e2dc75feb328b8d3/nvim/.config/nvim/lua/fenil/config/autocmds.lua#L33-L48&quot;&gt;this&lt;/a&gt; autocommand, that&apos;s it, 15 lines are all we need. I get the progress in same place as command line without &lt;code&gt;Hit Enter&lt;/code&gt; prompts.&lt;/p&gt;
&lt;p&gt;This works best with &lt;code&gt;cmdheight = 0&lt;/code&gt; , which prevents &lt;code&gt;Hit Enter&lt;/code&gt; prompts. &lt;code&gt;cmdheight&lt;/code&gt; feature was merged in 0.11 itself, I had tried it then but it felt incomplete and weird, but now with ui2 it has the perfect UX, they have really nailed this! There&apos;s just one small hiccup, somehow I am not able to see marco record messages, I could reproduce it on master with minimal config so its definitely an upstream issue, hopefully that gets fixed, but till then I have &lt;a href=&quot;https://github.com/feniljain/dotfiles/blob/9681a844aadc971dae416836e2dc75feb328b8d3/nvim/.config/nvim/lua/fenil/config/autocmds.lua#L58-L73&quot;&gt;two hacky autocmds&lt;/a&gt; which almost do the same job just slightly worse 🙃.&lt;/p&gt;
&lt;h3&gt;No vimscript in config&lt;/h3&gt;
&lt;p&gt;I finally took the leap and ditched out all the vimscript from my config, its completely lua based now! I know I am very late to the party, but I really wanted to keep it around so that I can use it on VMs where vim is the default. Well what finally prompted this move was making a minimal vimscript based vim config, which I can easily drop anywhere and get productive with vim! &lt;a href=&quot;https://github.com/feniljain/dotfiles/blob/main/vim/minimal-vimrc&quot;&gt;Here&apos;s&lt;/a&gt; the minimal config for the curious. I also have a minimal tmux config in the same lines &lt;a href=&quot;https://github.com/feniljain/dotfiles/blob/main/tmux/.tmux.conf.minimal&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Bugs discovered&lt;/h2&gt;
&lt;p&gt;This is an interesting section cause I usually never come across any hiccups when upgrading, neovim is a super polished, heavily tested software. People are out there doing builds super frequently to test latest and greatest! ( I was one of them till few years ago :P )&lt;/p&gt;
&lt;p&gt;But this release I came across two interesting bugs!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macro recording message display with ui2 and cmdheight=0&lt;/li&gt;
&lt;li&gt;a memory segfault with ui2 + invalid rtp and syntax on!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;First one we have already discussed, I hope it gets fixed in an upcoming version or even next release is fine :P&lt;/p&gt;
&lt;p&gt;For the second one, this is something severe and I was very astonished to come across it! First, link to bug report: https://github.com/neovim/neovim/issues/39815&lt;/p&gt;
&lt;p&gt;minimal repro is just:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require(&apos;vim._core.ui2&apos;).enable()
vim.cmd [[
set rtp+=$LMAO &quot; Cannot be a non-existing dir, needs to be an env var which does not exist
set syntax
]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So conditions are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ui2 should be enabled&lt;/li&gt;
&lt;li&gt;rtp should be set to an env var which does not exist&lt;/li&gt;
&lt;li&gt;syntax should be on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason I came across this is because of this weird rtp setting I had set in &lt;a href=&quot;https://github.com/feniljain/dotfiles/blob/8ee7f08e3d5c978d3a667ff3479f4150d10ceeda/nvim/.config/nvim/plugin/sets.vim#L17&quot;&gt;my config&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set rtp+=$GOPATH/src/golang.org/x/lint/misc/vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have no recollection why I had added this, I have messed around a lot with my config over the years, so there are artifacts I still find in weird corners. But main point being, I don&apos;t have golang installed in my system, so &lt;code&gt;GOPATH&lt;/code&gt; is not set and hence satisfies condition we mentioned above. I am not exactly sure what is causing this crash in neovim internally, it needs some investigation 🧐.&lt;/p&gt;
&lt;p&gt;But yeah interesting times! I created a new APPNAME with nvim-debug and managed to track it down to this config and also reproduce on latest &lt;code&gt;master&lt;/code&gt;. I have reported it, let&apos;s see if someone upstream picks it up before I get my hands dirty 🏃.&lt;/p&gt;
&lt;h2&gt;Sides&lt;/h2&gt;
&lt;p&gt;I was checking &lt;code&gt;:checkhealth vim.lsp&lt;/code&gt; and realized I hadn&apos;t seen &lt;code&gt;:checkhealth&lt;/code&gt; in a while, I did that and boom, it was so much cleaner!! Now we have ✅ and ❌ and ⚠️ to show overall health of the features etc, and in general it looked really really clean!&lt;/p&gt;
&lt;p&gt;There are also other features like &lt;code&gt;:restart&lt;/code&gt;, etc which I didn&apos;t delve into much cause I wasn&apos;t sure how to make use of those features right now. There are bunch of other features too, do go through all the &lt;a href=&quot;https://neovim.io/doc/user/news-0.12/#news-0.12&quot;&gt;release notes&lt;/a&gt;. Amount of things I have realized by reading the release notes in detail is mind blowing, 100% recommended!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Overall, I am super happy with this new release, only thing I couldn&apos;t change this time is colorscheme, I didn&apos;t have any lined up to try out 😓.&lt;/p&gt;
&lt;p&gt;Except that, I am already looking forward to more amazing things in upcoming releases (multi-cursor looking at you). Till then, chao!&lt;/p&gt;
</content:encoded></item><item><title>Working with LLMs</title><link>https://fknil.pages.dev/blog/working-with-llms/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/working-with-llms/</guid><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As we usher in this new era of LLMs, it is interesting to see how different people are starting to work with them. And as a typical keyboard thudding monkey, I want to optimize my workflow too. Because a true master understands tools at it his/her disposal the best.&lt;/p&gt;
&lt;p&gt;The way I currently work with them is straight forward way popularized by Claude Code, plan with it first in plan mode, then jump into implementation. I try to manually approve everything, but still I lose context in the &quot;hit enter&quot; hell. To over come it, I sometimes just let it make all the changes and then go back and start editing it. Now, ideally this should work, you plan meticulously and once the plan is solid, bang on, all code will be perfect. Right? Right?&lt;/p&gt;
&lt;p&gt;I think that&apos;s a wrong model to think how software engineers work. Most of the times, we discover/realize things on the fly, and that could be as small as a super small limited scope change to a complete re-design. So it is more of an iterative loop rather than a one shot model. In that case, one would go in and out of plan mode refining the spec as they learn more.&lt;/p&gt;
&lt;h2&gt;My questions&lt;/h2&gt;
&lt;p&gt;But before trying to refine our process, lets try to come up with points I want answers to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;How do I know I have explored all the possible ways to attack a problem, could there a simpler solution?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Another is, breaking down abstractions at correct boundaries, I think LLMs struggle with this right now. I see a lot of people dumping code in places where it shouldn&apos;t belong in the first place. Why is it dumped there, cause no one cared enough to think about boundaries. Well, this was a problem before LLMs too, but its much more worse right now.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Writing code by hand is a process which forces one to slow down and look at the surrounding code, think about frictions we face when coming up with code. Just being lazy and realizing a lot of things. LLMs don&apos;t have that (1). How to bring back this process of slowing down? And in what form? Hand-write everything again?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;How do I trust the tests written by LLM? Amount of people who are not reading generated tests is baffling high. No one, literally no one I know is reading generated tests. They think if there are tests its enough. Amount of times I have found generated tests to not be helpful is actually very high. Like the saying of man goes: &quot;To know a man, check his trash&quot;. &quot;To know about an implementation, check its tests&quot;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;How to find subtle problems within the implementation, Antirez put it nicely: &lt;code&gt;&quot;but still things that superficially work do not mean they are optimal.&quot;&lt;/code&gt;(2)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;User workflows&lt;/h2&gt;
&lt;p&gt;Before we try to answer these questions, lets try to read Antirez&apos;s use of LLMs for array type support in redis (2) (3). Summarizing it the way I understood it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;He wrote first design draft completely by himself&lt;/li&gt;
&lt;li&gt;Brought in LLM, started attacking draft from different angles, this would have likely required him asking correct questions to LLM&lt;/li&gt;
&lt;li&gt;He read whole code line by line with extreme care. I liked this a lot: &lt;code&gt;but still things that superficially work do not mean they are optimal.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;He rewrote the whole implementation again in a mix of manual and LLM mode&lt;/li&gt;
&lt;li&gt;Extensive testing, a complete month dedicated to just that&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In his own words towards the end:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;For high quality system programming tasks you have to still be fully involved, but I ventured to a level of complexity that I would have otherwise skipped. AI provided the safety net for two things: certain massive tasks that are very tiring (like the 32 bit support that was added and tested later), and at the same time the virtual work force required to make sure there are no obvious bugs in complicated algorithms. To write the initial huge specification was the key to the successive work, as it was the key to review each single line of sparsearray.c and t_array.c and modifying everything was not a good fit.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we are at it, these are some ways I have seen people around me use it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clowns: Absolute direct vibe code, this is just dumb&lt;/li&gt;
&lt;li&gt;GreatPretenders: Give the problem to LLM, act like they understand it by saying: &quot;we manually accepted edits&quot;, test it on basic cases and ship to production.&lt;/li&gt;
&lt;li&gt;Meticulously try to plan things with it, try to attack from different angles. From here on two more routes:
&lt;ul&gt;
&lt;li&gt;Strategist: Write code using a LLM assisted autocomplete&lt;/li&gt;
&lt;li&gt;OldieGoldie: Write code completely by hand&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We are not going to talk about Clowns and GreatPretenders at all except one statement to these people, PLEASE stop making my life difficult.&lt;/p&gt;
&lt;h2&gt;Thoughts on my questions&lt;/h2&gt;
&lt;p&gt;Now that we have everyone&apos;s workflows in place, let&apos;s try to come back to our questions. (Answers in the same bullet point number as the question)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;I like Antirez&apos;s approach here, he took a month just to write the spec, and he didn&apos;t write first draft with the help of LLM, it was completely by himself. This is where I think Strategist and OldieGoldie&apos;s get defeated, I believe key point is: not reading the approach given by LLM first. Cause there are times, they just don&apos;t know, and they don&apos;t know what they don&apos;t know. They are not able to come up with few of the strategies you might come up with. You could call this the creative step or whatever. I have noticed, reading LLMs output first creates a bias in the mind, and also we might get hindsighted on asking the correct questions. That&apos;s why try to come up with a plan on your own and then work with LLM to try to attack it from different sides to solidify it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On this part, I think there are two steps where this comes up, first is when planning i.e. 1st step and next is when actually writing code by hand and noticing a friction point. First part is addressable during first step itself, this is usually the easy part. But when it comes to the latter, I think it correlates with 3rd point of mine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now this is a tricky one, one needs to slow down, we slowed down once in the initial planning phase, but when next? In the iterative cycle I mentioned above, how do we slow down during the actual implementation section to notice these frictions? Well one way is converting into OldieGoldie, it is slow but definitely works! Though one could be lost completely in implementation details and want to complete it fast, which would lead us to the pre-LLM era problem of people writing absolute horrendous code without respecting any abstractions.&lt;/p&gt;
&lt;p&gt;So, completely automated is bad, completely hand written is dicey, then Strategist wins? Well, I don&apos;t think so, again this is a point about slowing down, fancy autocompletes are not a great way to slow down and understand cross module dependencies. Well then what? I like Antirez&apos;s way here, seemingly he generated all code first as a PoC, realized few things during PoC to fix, re-generated it, assumed just reading everything in extreme detail would help but he didn&apos;t know the answer to: &lt;code&gt;but still things that superficially work do not mean they are optimal&lt;/code&gt;. So he went back and rewrote whole implementation in a mix of manual and AI-assisted mode.&lt;/p&gt;
&lt;p&gt;The difference between Strategist and this is using code as a throw-away signal, Antirez used the first version as a PoC, that&apos;s it. He then, rewrote the implementation in his own way completely, this makes the process so much faster and more context aware than one shotting the implementation and making abstractions etc on the fly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For this point, Antirez said two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Everything was working, and this type has massive testing, thanks, again to AI&quot;&lt;/li&gt;
&lt;li&gt;&quot;When this stage was done, I started, during the third month, to stress test the implementation in many different ways.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don&apos;t think there&apos;s info on what he did here. As such testing is a very subjective topic and how to do it properly for a particular system is a monster of its own. For now, I try to follow the same procedure as before, try to come up with test cases myself and then involve LLMs to expand upon them on their own and combine to form a better list. This helps avoid a bunch of test cases which add 1K lines of abstractions on their own to test a simple thing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For this one, I think 3rd point above goes in enough details about everything. Key to this point I believe is slowing down and reading code multiple times to try and think from different angles.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This kinda also lays out how I want to try using LLMs going forward.&lt;/p&gt;
&lt;h2&gt;Unanswered Questions in Antirezs&apos; article&lt;/h2&gt;
&lt;p&gt;Taking a small detour and going back to Antirez&apos;s article, I have a few things I would have liked to understand in more details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When he used in LLM in the planning phase, what part of it was him trying to probe questions out of LLM as an experienced user and what part of it was, LLM finding defects/improvements on its own?&lt;/li&gt;
&lt;li&gt;What part of codebases did he rewrite manually and what parts were rewritten using LLM? How did he decide which part to allocate to who?&lt;/li&gt;
&lt;li&gt;How did he approach testing in general, did he check LLMs generated tests in super detail? Did he rewrite them too? What did his one month of testing look like in detail? How did LLMs help outside unit tests?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It was interesting to think about these things while writing this article down. I would have not imagined myself thinking about these things because I had previously been haunted by a college senior of mine being too strict on writing down huge number of pages of LLD, HLD, PRD, etc etc for club projects. Ofc we never finished the projects which he was supervising. I still don&apos;t know if all of this was coherent or just random rambling. Well, there&apos;s one thing for sure, I have something new to try and I will make sure I keep the rigor up in LLM age! [4]&lt;/p&gt;
&lt;p&gt;Footnotes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1) https://bcantrill.dtrace.org/2026/04/12/the-peril-of-laziness-lost/&lt;/li&gt;
&lt;li&gt;(2) https://antirez.com/news/164&lt;/li&gt;
&lt;li&gt;(3) https://github.com/redis/redis/pull/15162&lt;/li&gt;
&lt;li&gt;(4) https://oxide-and-friends.transistor.fm/episodes/engineering-rigor-in-the-llm-age&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ClaudeHeads</title><link>https://fknil.pages.dev/blog/claude-heads/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/claude-heads/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;So firstly what are ClaudeHeads? They are people who have claude in place of their head. They literally think LLM is the only answer and can only think using them, they are just straight up bad at individual thinking, but that doesn&apos;t matter, cause LLM can solve everything given right context, given all the information in the world about thing which exists.&lt;/p&gt;
&lt;p&gt;For me this is a problem. I joined database industry cause I could not bear writing HTTP APIs for the rest of my life. I am not smart enough personally, but there are horrible software engineers out there, you would find shitty code in all parts of the software stack. But for something which is performance critical, needs to correct always, and is always the black box for programmers, that would need highest and purest levels of programmers, right right??? Well it seems I could only enjoy this dream for sometime, cause LLMs have given birth to ClaudeHeads. We use an open source project named datafusion and have based our database on it. Its not a direct stock integration, we have had to make a lot of changes according to our needs, and it seems distribution is still an unsolved problem there. Also main IP of planner stays with us, single execution is not a solved problem, but open source projects are very very good!&lt;/p&gt;
&lt;p&gt;Well, given that it is open source, of course LLMs are trained on it. Now, that is one part of the equation, in recent months they have also become good enough to interact with private parts of our codebase. Migration to a datafusion based engine is a recent enough project and we had been working hard to get performance on TPCDS-like benchmark[1], TPCH-like benchmark, Clickbench, and a bunch of internal benchmarks. We were very very slow as compared to our good old internal custom developed Java engine, as compared to Databricks, as compared to Snowflake. Whole team was heads down working on getting perf better than all of the above combined. As me and other senior colleagues took an &quot;old school&quot; methodological approach of looking at heap profiles, CPU flamegraphs, custom metrics collected by us, finding gaps in our understanding of the system, as not whole codebase was familiar to us yet. Here enters my ClaudeHead hero, who downloads research papers of Datafusion/Arrow etc, keeps them in a folder, keeps all TPCDS queries, their flamegraphs, heap profiles and metrics together, and send the agents to &quot;find perf improvements&quot;. The result was pretty shit with earlier models, but what about recent ones? You leave them for a night and they conjure up a bunch of things. Tho how do you test them?&lt;/p&gt;
&lt;p&gt;So, incidentally someone in my company developed a easy to use benchmark setup. What was left now, multiple branches started getting created, purely vibe coded and benchmarked in parallel. What ever improved perf was posted as it is after &quot;understanding from Claude summary&quot; to the channel and merged to main. Well the problem is &quot;understanding&quot; part is absent, if asked to reason about the change from different angles, like architectural correctness, my friend would turn around and just ask Claude. There&apos;s no head working there, it&apos;s just Claude. Well how do I know this? Cause I have asked questions around why some part of it didn&apos;t make sense in larger scheme of things. Why even tho metric shows there&apos;s nothing to optimize, you keep repeating there&apos;s an optimization in a specific region, without backed by proof, just because Claude said that. What&apos;s worse, this is a junior engineer just entering the field. Not good at coding, not good at databases, not good at CS fundamentals. But given LLMs, he can keep on posting perf optimizations and get them merged. One could argue, if those don&apos;t make sense why can&apos;t you prove them wrong? Well here comes the main point of article, my views on ClaudeHeads and how they are correct at times, but expert bullshitters at times. Back in the days, when no LLMs existed, if someone bullshitted, they had to put a LOT OF EFFORT to even get something remotely good out, btw this is considering that it was still considered easy, &lt;a href=&quot;https://en.wikipedia.org/wiki/Brandolini%27s_law&quot;&gt;Brandolini&apos;s law&lt;/a&gt;. During the process, they learned 100s of things and would definitely come out as a much better engineer, but now? Tell LLM to fire off and conjure stuff, what if that does not make sense in grand scheme of things and would literally break in just a different environment (someone shares my &lt;a href=&quot;https://x.com/siddharthkp/status/2046890064450302110&quot;&gt;feelings&lt;/a&gt;). Well that&apos;s benchmaxxing. But what if you could keep benchmaxxing again and again for each dataset. That&apos;s not exactly what&apos;s happening, but I am thinking through scenarios.&lt;/p&gt;
&lt;p&gt;Not understanding what changes you are making to me is the biggest risk of all time, and it breaks what I thought about before starting to work on databases. It&apos;s not a race of understanding, now it&apos;s a race of trying random folder structures with random bits of information to get the best output out of LLM models. Oh guess what, I am still stuck in the old model, and this has caused a big disadvantage to me. Not only am I slow now, I am also losing learning opportunities myself, just because someone decided to not understand them and rely completely on LLMs. I can pick up LLMs to speed up my work, but all I have ever learnt is, slow and steady wins the race. I believe it to my heart, mental models are the biggest factors of a product. That&apos;s the reason when someone who understands codebases deeply leaves, new team gets in frenzy. That&apos;s the reason losing a product person who understood product deeply, is such a big loss. They are hard to replace. Code was never the moat, mental models were the actual secret sauce. But in this case, trash out the mental models, we will just use LLMs to not just write code, but also think, not build mental models, just straight up outsource thinking.&lt;/p&gt;
&lt;p&gt;Writing code is amongst the best way to build mental models, slow, deliberate thinking is what dials down core ideas of anything. This applies to product building as well as programming. Having faced the &quot;friction&quot;, and letting mind battle with it is the best way. I recently read a blog post in similar vein, but talking about astrophysics: https://ergosphere.blog/posts/the-machines-are-fine/. It&apos;s an excellent read, do go through it.&lt;/p&gt;
&lt;p&gt;But yeah, this is my problem, sorry I am not slow, I am just not a ClaudeHead.&lt;/p&gt;
&lt;p&gt;[1] I say TPCDS-like cause I remember how badly PlanetScale was thrased in an official blog post when the data generator used by them did not actually comply with TPCDS specifications :upside_down:&lt;/p&gt;
</content:encoded></item><item><title>Estimating filter equality selectivity using NDVs</title><link>https://fknil.pages.dev/blog/ndv-filter-equality-selectivity/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/ndv-filter-equality-selectivity/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Now that I work on databases, I have a habit of keeping up with upstream datafusion PRs. Today I noticed an interesting PR talking about usage of NDVs in equality filter selectivity. I have always been fascinated by NDVs cause my colleagues in planner team always mention them as something super helpful. I started looking into the PR and it turned out to be a small one, but there was a review on it and honestly I did not understand it at all. So I sat down to do some reading on how this works.&lt;/p&gt;
&lt;p&gt;Firstly, what are NDVs? NDVs are number of distinct values, these are usually stored at parquet file level. We can also compute NDVs for a column, i.e. how many distinct values a single column contains.&lt;/p&gt;
&lt;p&gt;What is filter selectivity? For a filter supplied in a query, number of rows selected by it is called it&apos;s selectivity. For e.g. a filter which filters out 50 rows out of total 100 has a selectivity of 50%.&lt;/p&gt;
&lt;p&gt;Now lets understand how do they come together, lets say we have a join query like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from A where A.x = B.y;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this, we have our join condition as &lt;code&gt;A.x = B.y&lt;/code&gt;, if we can predict what will be the selectivity of this filter expression we can make interesting decisions based on it. A good example is: do we want to use partition-wise join or non-partition-wise join? ( i.e. if we have loads of rows to join on, we can distribute them across cores instead of doing it on single core itself )&lt;/p&gt;
&lt;p&gt;NDVs help us do exactly that, let&apos;s say we have a condition where &lt;code&gt;y = 42&lt;/code&gt;. And let&apos;s say &lt;code&gt;y&lt;/code&gt; column has 5 distinct values, that means our NDV count is 5. As we don&apos;t have exact histograms telling us about data distribution, we assume each value is &quot;uniformly distributed&quot; across whole column. For e.g. if &lt;code&gt;y&lt;/code&gt; column is made up of &lt;code&gt;{38,39,40,41,42}&lt;/code&gt; and has 100 values in total, we assume there are 20 values of 38, 20 values of 39 and so on. This assumption means probability of 42 getting matched is equal to all others distinct values i.e. 1 / 5. If we multiply this with total number of rows in the column, we get selectivity of &lt;code&gt;y = 42&lt;/code&gt; as 20. Here key point is understanding us assuming uniform distribution, if we had histograms, we could exactly tell how many rows have value 42 in the column, but NDVs work as next best case.&lt;/p&gt;
&lt;p&gt;This estimation of rows helps in join order estimation, join type estimation, etc.&lt;/p&gt;
&lt;p&gt;After understanding this I noticed there was a review comment on the PR and tried to decode that. Review was as follows&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;I think this new `1 / distinct_count` branch is a little too broad as written. Right now it fires whenever the pruned interval collapses to a single value, but that is not quite the same thing as proving we have an equality filter.

For example, if the incoming stats already describe a singleton interval, or if a conjunction of inequalities narrows the range to one point without actually adding any selectivity beyond the existing stats, we would still scale by `1 / NDV` here and end up under-estimating the row count.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was a total bouncer for me, it was so high that if this were a cricket match, umpire would call it a WIDE. But let&apos;s try to break it down, so author&apos;s current condition to use &lt;code&gt;1/NDV&lt;/code&gt; is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ...
	target.distinct_count
                    &amp;amp;&amp;amp; distinct_count &amp;gt; 0
                    &amp;amp;&amp;amp; !target_interval.lower().is_null()
                    &amp;amp;&amp;amp; target_interval.lower() == target_interval.upper() {...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this interval means zone maps i.e. min/max values of that column. In our case above &lt;code&gt;y&lt;/code&gt; would have min/max values as &lt;code&gt;{39,42}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Condition checks if NDV count is not zero and &lt;code&gt;target_interval&lt;/code&gt;&apos;s lower value is same as upper value, if everything passes we assume our filter selectivity as &lt;code&gt;1/NDV&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;According to reviewer, &lt;code&gt;1/NDV&lt;/code&gt; estimation is incorrect in following cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;if the incoming stats already describe a singleton interval&quot;&lt;/li&gt;
&lt;li&gt;&quot;if a conjunction of inequalities narrows the range to one point without actually adding any selectivity beyond the existing stats&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both of these reviews at the core address the problems of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shape of data changes as it gets processed by different operators
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;singleton does not guarantee an equality filter source
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lets try to understand above line with an example. Lets say we have two filters on our &lt;code&gt;y&lt;/code&gt; column due to some CTE/subquery etc:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;first being: &lt;code&gt;y &amp;gt;= 41 || y &amp;lt;= 42&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;and second being: &lt;code&gt;y &amp;gt; 33 AND y &amp;lt; 42&lt;/code&gt; (non equality condition)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After first filter we would have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bounds: &lt;code&gt;[41, 42]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;NDV count: 5 (notice it didn&apos;t change)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When we come to second filter and apply bounds to predicate we only get rows containing 41. Here we will predict selectivity as &lt;code&gt;1/NDV&lt;/code&gt;. This is the exact problem, lets say out of first filter we get 70 rows out i.e. first filter has selectivity of 70%. Now lets say we have 35 rows of &lt;code&gt;41&lt;/code&gt; and 35 rows of &lt;code&gt;42&lt;/code&gt;, after applying second filter 35 rows are remaining i.e. 50% selectivity. But, if we go by NDV route, we get &lt;code&gt;70/5&lt;/code&gt; i.e. 14 rows, that is a super low estimation!&lt;/p&gt;
&lt;p&gt;Our NDV count did not change as data flowed through both filters, same phenomenon can happen with different operators in the middle. We also saw that even though we got singleton interval as an&lt;/p&gt;
&lt;p&gt;This was an interesting dive, which confused me a lot at different places, even while writing this down!&lt;/p&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://learn.microsoft.com/en-us/sql/relational-databases/performance/cardinality-estimation-sql-server?view=sql-server-ver17&lt;/li&gt;
&lt;li&gt;https://github.com/apache/datafusion/pull/20789/&lt;/li&gt;
&lt;li&gt;https://blobs.duckdb.org/papers/tom-ebergen-msc-thesis-join-order-optimization-with-almost-no-statistics.pdf&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Chinaga Betta Hike</title><link>https://fknil.pages.dev/blog/chinaga-betta-hike/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/chinaga-betta-hike/</guid><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &apos;astro:assets&apos;;
import eucalyptus from &apos;@assets/blogs/chinaga-betta-hike/walk-through-eucalyptus-trees.jpeg&apos;;
import verticalWall from &apos;@assets/blogs/chinaga-betta-hike/vertical-wall-to-climb.jpeg&apos;;
import clouds from &apos;@assets/blogs/chinaga-betta-hike/amongst-the-clouds.jpeg&apos;;
import settlements from &apos;@assets/blogs/chinaga-betta-hike/tiny-settlements-between-hills.jpeg&apos;;&lt;/p&gt;
&lt;p&gt;Chinaga betta is a nice little day hike bear Bengaluru. It is said to be 2.1 kms one side, so in total of 4.2 kms up and down. It&apos;s a simple hike, can be done with family and friends. I recently got to visiti it and I am gonna mention how was the experience. First thing is it needs permit, so book it from arayna vihaara &lt;a href=&quot;https://aranyavihaara.karnataka.gov.in/&quot;&gt;website&lt;/a&gt;. Choose a convenient slot, in my time it was just 6 AM to 6:30 AM. This is needed cause it&apos;s said that a forest ranger will accompany you to the top, I say it that way, cause surprise surprise there was no one when we reached there.&lt;/p&gt;
&lt;p&gt;Trek starts from the base of temple &lt;a href=&quot;https://maps.app.goo.gl/9r7iUrNYPkihCvZp6&quot;&gt;Torana Anjaneya Swami Temple&lt;/a&gt;. This is where forest department is supposed to check your IDs before starting. There were a lot of locals when we reached there, its a temple which seemed super active and when we were returning it also seemed they were in the process of sacrificing a goat. We didn&apos;t stand back to see that. Well that&apos;s for a later bit, first in the start, when we were approaching base of the temple we were very scared as it was pitch black and when we entered forest side, it started to feel like off roading. So driving through a lonely road in night with no one in sight, was a bit concerning, but when we reached there and saw few fellow hikers we were relieved.&lt;/p&gt;
&lt;p&gt;We waited for forest ranger till 6:45, but when we realized we were played for a fool, we just started on our own. So yeah, 250 rupees went in vain :(&lt;/p&gt;
&lt;p&gt;Okay, next thing, let&apos;s talk about the actual trail. We followed trail from &lt;a href=&quot;https://www.gaiagps.com/public/NTAxgUNVldkLEmi99JQLIm1W/NTAxgUNVldkLEmi99JQLIm1W&quot;&gt;this&lt;/a&gt; website. I would divide whole hike into four sections, first is big temple to small temple, next is rocky/slaby tiring uphill section, next is flatlands and finally the last remaining part to the summit.&lt;/p&gt;
&lt;p&gt;Forest ranger is mostly not needed for the hike&apos;s majority, it&apos;s just that at the top, there&apos;s a vertical rock, which you have to climb and most people would not be comfortable doing that. I was a climber so I climbed it pretty easily (subtle flex xD). Hike starts at the back of the temple, and there&apos;s a outward protruding rock at the top, which has a flag above, that is your summit.&lt;/p&gt;
&lt;p&gt;First part when we start from the back of the temple, there&apos;s a trail which seems to go in the forest, follow that. Once you follow that you will reach another small temple. There will be two paths there, and just as Robert Frost&apos;s protagonist, we have to the road less taken. This is first part done, its just light walking, perfect warmup for the next tiring section.&lt;/p&gt;
&lt;p&gt;Next section is a where uphill starts, its through mildly dense forest, well it was less denser cause we could see people there had burnt a lot of trees to keep it clear for trail. This section is also where we saw sunrise. It would have been better to watch it from flatlands above, but we were late due to waiting for forest ranger :(&lt;/p&gt;
&lt;p&gt;This section is also slippery at times, two of my friends survived the slip, but it could get dangerous, so either wear good shoes or be extra careful where you are stepping and how you are shifting body weight. It shouldn&apos;t be a problem for most people, but extra caution is never harmful.&lt;/p&gt;
&lt;p&gt;There&apos;s a section between flatland and this uphill where you will start seeing eucalyptus trees. They are beautiful with little yellow flowers on them. This gave me a feeling of walking through magic forest of berserk. If locals hadn&apos;t burnt a lot of trees, I wonder if I would have declared it Garden of Eden.&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;
&amp;lt;Image src={eucalyptus} alt=&quot;Walk through eucalyptus trees&quot; style=&quot;width: 30%; height: 20%&quot; /&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;Also while going uphill you will notice white arrows, follow them. Though you could also just rely on the trail map you have downloaded, it was on OpenStreetMaps on IndiaHikes website so you could use any FOSS maps app to view it.&lt;/p&gt;
&lt;p&gt;Next section is flatlands, it is what the name says. I don&apos;t think it&apos;s called flatlands in any blog or something, but I am calling it that cause of minecraft biome xD This is one spot you can catch sunrise. Best would be top, but even this is fine. From here you would start getting views of surrounding area. Its serene, breathe in and get ready for the remaining part!&lt;/p&gt;
&lt;p&gt;Now, next is walking through some part of flatlands to reach farthermost bottom of last section, there should be a easily visible trail starting there. Just follow that.&lt;/p&gt;
&lt;p&gt;As you walk through the last section, you will come across two big rocks creating a narrow space between them. You have to squeeze and pass, that is also very slippery, but all I could think at that point is, if I could climb this chimney 😂&lt;/p&gt;
&lt;p&gt;Once you complete that you would reach the final vertical rock which you have to climb. There&apos;s a rope there to assist but it seemed we were the first one to reach so it was thrown above!? Not sure why would someone do that. It looks like this:
&amp;lt;p align=&quot;center&quot;&amp;gt;
&amp;lt;Image src={verticalWall} alt=&quot;Vertical wall to climb&quot; style=&quot;width: 30%; height: auto;&quot; /&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;It doesn&apos;t look much but there&apos;s no proper footing down below, so if you slip you can get injured. And getting up is one thing, getting down is more scary unless you have someone looking at your feet and telling you where the footholds are.&lt;/p&gt;
&lt;p&gt;They are also carved inside the rock, and not projecting outwards. Also part of the reason why when getting down you have to look for them. Well, getting back in our case, I was the guy who got pushed forward to climb to get rope from above. I had no safety, so my friends were a bit concerned but I was confident as I had climbed much higher rocks with much dicier footholds and handholds as compared to this in Hampi. I threw the rope down and assisted everyone else to climb above. As we reached above, we could get a much clearer view of everything around, it&apos;s a beautiful place. On one side, you are seeing mountains till eyes can reach, covered with a blanket of clouds. Next side, you see tiny settlements, with lesser hills, perfect for people to make actual small towns around. While it&apos;s not a lot of height, wind there was super strong. So if you are lean and light weight, please don&apos;t fly off xD&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;
&amp;lt;Image src={clouds} alt=&quot;Amongst the clouds&quot; style=&quot;width: 30%; height: auto;&quot; /&amp;gt;
&amp;lt;Image src={settlements} alt=&quot;Tiny settlements between small hills&quot; style=&quot;width: 30%; height: auto;&quot; /&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;We had a dog guide us the whole time, she was so cute and playful, unfortunately she disappeared when we completed the hike, so we couldn&apos;t treat her :( We also could not carry her to to topmost section as that was a climb on a straight rock. It wasn&apos;t the biggest but not possible for us to do with a dog.&lt;/p&gt;
&lt;p&gt;And yeah that&apos;s it, we came down the same path, but there&apos;s another surprise waiting for you after hike, we stopped by the Swandenahalli Lake. We think it&apos;s a small pond rather than a lake. We chilled there for some time, skipped some stones which itself was good enough to offset lake vs pond disappointment :)&lt;/p&gt;
&lt;p&gt;And that&apos;s it on the way back we tried Pavithra Idli Hotel&apos;s Benne thatte idili, vada and masala dose. They have been cooking since 1942 and are pretty famous, we had to wait for 10-15 minutes to get a seat on a Saturday morning. Benne Thatte idli wasn&apos;t upto to the hype for me, I have had better ones near in Jayanagar. But Masala dose was better and we watered everything off with a hot filter coffee, always the best part for me xD It&apos;s worth a try once :)&lt;/p&gt;
&lt;p&gt;And that&apos;s it, enjoy and have a nice trip.&lt;/p&gt;
&lt;p&gt;References: https://indiahikes.com/documented-trek/chinaga-betta-trek&lt;/p&gt;
</content:encoded></item><item><title>Understanding Snapshots in Apache Iceberg</title><link>https://fknil.pages.dev/blog/iceberg-snapshots/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/iceberg-snapshots/</guid><pubDate>Wed, 12 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;External Link&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://www.e6data.com/blog/apache-iceberg-snapshots-time-travel&lt;/li&gt;
&lt;li&gt;https://archive.is/VJ7pE&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Sink consistency in RisingWave</title><link>https://fknil.pages.dev/blog/risingwave-sink-consistency/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/risingwave-sink-consistency/</guid><pubDate>Wed, 12 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;NOTE: I am mostly writing this down to present to someone who is already familiar with the system, but I have laid down some ground work to make it slightly better. Write up is also heavily code referential, so sorry if that&apos;s not up your alley.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/risingwavelabs/risingwave&quot;&gt;RisingWave&lt;/a&gt; is a popular and open-source streaming database, it can work with a variety of different sources and sinks and has capabilities to provide performant real time analyses on streaming data along side service ad-hoc queries. Basically a lot of buzzwords.&lt;/p&gt;
&lt;p&gt;I have grown interest into the system and was trying to understand how it prevents data loss with so many different sinks in case one of it&apos;s compute node dies? We will be looking into handling of iceberg sink cause that&apos;s what I am working with these days. I am going to assume familiarity with iceberg already cause understanding that would take another several blog posts.&lt;/p&gt;
&lt;p&gt;One of the good features of iceberg is it&apos;s decoupling between data files and metadata files. One can take existing parquet files and create a table out of them easily. Work for the &lt;a href=&quot;https://github.com/apache/iceberg-rust/issues/932&quot;&gt;same&lt;/a&gt; is active in iceberg-rust. Even when comitting iceberg writers do the same, write data files first and then try to write metadata files, if they fail (they may fail cause another writer&apos;s commit would cause ACID guarantees to fail on table) they just have to re-generate metadata files and try to commit again.&lt;/p&gt;
&lt;p&gt;So writing data files vs committing are separate processes, same happens in iceberg-rs and hence RisingWave, for iceberg sink these are the locations where each occurs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing happens under &lt;code&gt;IcebergSinkWriter&lt;/code&gt; &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/blob/1a6eb0001c806c547de129d4cf66035ec66e4fe1/src/connector/src/sink/iceberg/mod.rs#L863&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Commiting happens &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/blob/1a6eb0001c806c547de129d4cf66035ec66e4fe1/src/connector/src/sink/iceberg/mod.rs#L1208&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Getting back to RisingWave, core idea of persisting such state in databases is to use some kind of logs, a lot of databases have their own WAL implementation. RisingWave also leverages concept of log stores for the same. &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/tree/main/src/stream/src/common/log_store_impl&quot;&gt;These&lt;/a&gt; are the current log stores implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Memory Log Store&lt;/li&gt;
&lt;li&gt;KV Log Store&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now our doubt was what if RisingWave compute node crashes before commit happens. LogStores implements &lt;code&gt;LogReader&lt;/code&gt;. &lt;code&gt;LogReader&lt;/code&gt; abstraction shows what &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/blob/1a6eb0001c806c547de129d4cf66035ec66e4fe1/src/connector/src/sink/log_store.rs#L158C22-L179&quot;&gt;all methods&lt;/a&gt; does it provide, namely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next_item&lt;/code&gt; , read next item in log&lt;/li&gt;
&lt;li&gt;&lt;code&gt;truncate&lt;/code&gt; , increments read offset in log&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rewind&lt;/code&gt; , decrements read offset in log&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These methods are used along side RisingWave&apos;s internal global clock to make sure no data is lost. Hierarchy of internal clock looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;barriers&lt;/code&gt; every configurable ms, configurable using &lt;code&gt;barrier_interval_ms&lt;/code&gt; in system params&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkpoints&lt;/code&gt; every N barriers, configurable using &lt;code&gt;checkpoint_frequency&lt;/code&gt; system param&lt;/li&gt;
&lt;li&gt;&lt;code&gt;commits&lt;/code&gt; every N checkpoints, configurable for iceberg sink using &lt;code&gt;commit_checkpoint_interval&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we can keep reading data async on every barrier using &lt;code&gt;next_item&lt;/code&gt; and keep &lt;code&gt;truncate&lt;/code&gt;ing on every commit. This would ensure we lose no data for different types of sinks.&lt;/p&gt;
&lt;p&gt;Let&apos;s see what happens for iceberg sink:&lt;/p&gt;
&lt;p&gt;Firstly, how &lt;code&gt;LogReader&lt;/code&gt; relates to our iceberg writer. &lt;code&gt;LogReader&lt;/code&gt; is used by &lt;code&gt;LogSinker&lt;/code&gt; , in our case &lt;code&gt;DecoupleCheckpointLogSinker&lt;/code&gt; &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/blob/5dcc141cf86b5b41e7e6965ac7ec840c73aad247/src/connector/src/sink/decouple_checkpoint_log_sink.rs#L80&quot;&gt;here&lt;/a&gt; and that finally calls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For writing:  &lt;code&gt;write_batch&lt;/code&gt; &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/blob/5dcc141cf86b5b41e7e6965ac7ec840c73aad247/src/connector/src/sink/decouple_checkpoint_log_sink.rs#L138&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;For committing: &lt;code&gt;commit&lt;/code&gt; &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/blob/1a6eb0001c806c547de129d4cf66035ec66e4fe1/src/meta/src/manager/sink_coordination/coordinator_worker.rs#L272&quot;&gt;here&lt;/a&gt;, this follows central clock of barriers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, &lt;code&gt;DecoupleCheckpointLogSinker&lt;/code&gt; also listens to central clock of barrier and writes data files to object store on each barrier &lt;a href=&quot;https://github.com/risingwavelabs/risingwave/blob/1a6eb0001c806c547de129d4cf66035ec66e4fe1/src/connector/src/sink/iceberg/mod.rs#L980-L1090&quot;&gt;here&lt;/a&gt; (i.e. call &lt;code&gt;close&lt;/code&gt; method on &lt;code&gt;data file writer&lt;/code&gt;), but it actually commits the result on every N checkpoints.&lt;/p&gt;
&lt;p&gt;So technically, if barrier and checkpoint values are not same and a compute node crashes between two checkpoints, we would have written data files to object store, but it would not be committed i.e. no metadata files. So these would fall under table maintenance job of &lt;code&gt;orphan files&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This can be mitigated by simply setting &lt;code&gt;checkpoint_frequency&lt;/code&gt;  to 1 i.e. trigger at every barrier and also &lt;code&gt;commit_checkpoint_interval&lt;/code&gt; to 1 i.e. &lt;code&gt;commit&lt;/code&gt; on every barrier/checkpoint.&lt;/p&gt;
&lt;p&gt;Now, how to increase batching size? That can be done by configuring &lt;code&gt;barrier_interval_ms&lt;/code&gt; . Though this could be a bad idea cause barriers are used internally for a lot of other things, they are like &lt;code&gt;ticks&lt;/code&gt; in minecraft engine. So making everything slower for batching can make us lose other system internal state/data leaving system in weird non-recoverable condition.&lt;/p&gt;
</content:encoded></item><item><title>Lets write a Brainfuck Interpreter: Optimizations</title><link>https://fknil.pages.dev/blog/brainfuck-jit-interpreter-2/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/brainfuck-jit-interpreter-2/</guid><pubDate>Sun, 26 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In last part, we wrote a naive implementation of brainfuck, which is pain-stakingly slow, let&apos;s try to optimize it, we will majorly discuss two major
optimizations in this blog. We will end up with really nice speedups at the end, so buckle up and let&apos;s go!&lt;/p&gt;
&lt;h2&gt;First optimization&lt;/h2&gt;
&lt;p&gt;One of the best things about implementing brainfuck is it&apos;s implementation is simple and straightforward and hence one can find optimization opportunities realtively easily. We don&apos;t try to plot a flamegraph, cause we know most of the time is spent in &lt;code&gt;exec&lt;/code&gt; function, that&apos;s where we execute all of our operations, so any optimizations done in that flow would give us direct noticeable speedups.&lt;/p&gt;
&lt;p&gt;Let&apos;s look at implementations of our operands again, this is the core loop: https://github.com/feniljain/brenphuk/blob/6b00f84be79c00679dc28ba917b853ff2e18beea/interpreter.c#L66-L142 right now. There&apos;s not much to see, implementations for &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt; are pretty simple and one liner even :P . So let&apos;s have a look at multi-liners i.e. loop implementations, here most hot path would definitely will be finding it&apos;s corresponding loop operand, let&apos;s say we have a program like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[:::[::]:::[::[::]::]]
^1  ^2     ^3 ^4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;:&lt;/code&gt; here means any random operand), we have 4 loops in total, 1 being the parent loops of all, containing 2 and 3 as their immediate child loop and finally 4 inside 3. Let&apos;s say 1 repeats 5 times. In a single interation of loop1 we will be finding end of loop2 once, which would make this find operation happen 5 times. Now let&apos;s say loop3 executes 10 times, for loop4 we will execute find operation 10 * 5 = 50 times, this is wasted computation. We can do this computation once and store it for whole execution of program.&lt;/p&gt;
&lt;p&gt;So do we make a kind of caching mechanism to store just for the inner loops? Technically we also have to jump for outer loops, so we do need jumping index for them too, but only once for most parent loop, and fewer times for depth one loops. What if we precompute all bracket locations? We as such do it while executing, maybe do it before execution starts, and then reference them to jump easily around. Let&apos;s give it a try and see our benchmark results.&lt;/p&gt;
&lt;p&gt;We make an array as big as program size and fill it in with -1 values, at exact index of loop operands we will fill in it&apos;s corresponding loop operands index. So we create two arrays: &lt;code&gt;open_brackets_loc&lt;/code&gt; and &lt;code&gt;close_brackets_loc&lt;/code&gt;. Now just before entering the core loop of &lt;code&gt;exec&lt;/code&gt; we call a new function called &lt;code&gt;fill_brackets_loc&lt;/code&gt;, this takes in program and it&apos;s length and calculates all brackets location along with filling them in our arrays. Implementation is simple, we find a &lt;code&gt;[&lt;/code&gt; and maintain a counter till we find corresponding &lt;code&gt;]&lt;/code&gt;, same as what we did in last blogpost, but we will only do it once this time, at the very start. Code looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void fill_brackets_loc(char *prog, int prog_len) {
  int i = 0, next_open_bracket_loc = -1;

  while (i &amp;lt; prog_len) {
    switch (prog[i]) {
    case &apos;[&apos;: {
      int brackets_depth = 0;
      for (int j = i; j &amp;lt; prog_len; j++) {
        if (prog[j] == &apos;[&apos;) { // found a new loop start operand
          if (next_open_bracket_loc == -1 &amp;amp;&amp;amp; j != i) {
            next_open_bracket_loc = j;
          }
          brackets_depth++; // increase the counter
        } else if (prog[j] == &apos;]&apos;) { // found a new loop end operand
          brackets_depth--; // decrease the counter
        }

        if (brackets_depth == 0) {
          open_brackets_loc[i] = j; // filling in our arrays
          close_brackets_loc[j] = i;
          break;
        }
      }

      if (brackets_depth != 0) {
        ABORT(&quot;brackets mismatch&quot;); // oops didn&apos;t find corresponding loop operand
      }

      break;
    }
    default:
      break;
    }

    if (next_open_bracket_loc != -1) {
      i = next_open_bracket_loc;
      next_open_bracket_loc = -1;
    } else {
      i++;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can even do one better by also storing each &lt;code&gt;[]&lt;/code&gt; identified when transversing nested loops. But for now, this works :P&lt;/p&gt;
&lt;p&gt;Now our &lt;code&gt;[&lt;/code&gt; handler in &lt;code&gt;exec&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;[&apos;:
  if (tape[pointer] == 0) {
    int idx = open_brackets_loc[i];
    if (idx == -1) {
      DBG_PRINTF(&quot;[: got bracket_loc as -1 for i: %d&quot;, i);
      ABORT(&quot;invalid state&quot;);
    }
    i = idx;
    continue;
  }

  break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We directly look up the location of corresponding loop operanding and jump!&lt;/p&gt;
&lt;p&gt;same for &lt;code&gt;]&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;]&apos;: {
  if (tape[pointer] != 0) {
    int idx = close_brackets_loc[i];
    if (idx == -1) {
      DBG_PRINTF(&quot;]: got bracket_loc as -1 for i: %d&quot;, i);
      ABORT(&quot;invalid state&quot;);
    }
    i = idx;
    continue;
  }

 break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running our benchmarks now gives us:
Factor: ~7s
Mandelbrot: ~22s&lt;/p&gt;
&lt;p&gt;That&apos;s some big gains from a simple observation! But wait we have more :)&lt;/p&gt;
&lt;h2&gt;Second optimization&lt;/h2&gt;
&lt;p&gt;Before this occurs, I made a small change to our core &lt;code&gt;exec&lt;/code&gt;, instead of using characters I am using enum variants for identifying each character, it&apos;s essentially the same thing as before just different representation. For the coversion between character operations and enum variants I wrote a simple &lt;code&gt;parse&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Op_type {
  INVALID = 0,
  FWD,
  BWD,
  INCREMENT,
  DECREMENT,
  OUTPUT,
  INPUT,
  JMP_IF_ZERO,
  JMP_IF_NOT_ZERO,
};

void parse(char *prog, int prog_len) {
  int i = 0;

  while (i &amp;lt; prog_len) {
    enum Op_type op_type = INVALID;
    switch (prog[i]) {
    case &apos;&amp;gt;&apos;:
      op_type = FWD;
    case &apos;&amp;lt;&apos;:
      if (op_type == INVALID)
        op_type = BWD;
    case &apos;+&apos;:
      if (op_type == INVALID)
        op_type = INCREMENT;
    case &apos;-&apos;: {
      if (op_type == INVALID)
        op_type = DECREMENT;
      break;
    }
    case &apos;.&apos;:
      op_type = OUTPUT;
      break;
    case &apos;,&apos;:
      op_type = INPUT;
      break;
    case &apos;[&apos;:
      op_type = JMP_IF_ZERO;
      break;
    case &apos;]&apos;:
      op_type = JMP_IF_NOT_ZERO;
      break;
    default:
      break;
    }

    if (op_type == INVALID) {
      i++;
      continue; // this can happen when there are comments which are supposed to
                // be ignored
    }
      i++;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now an interesting optimization I have seen done in Bytecode Interpreters is combining instructions when they occur together way too often. This could happen with same or different instructions too. I learnt about this first time while completing (Crafting interpreters)[https://craftinginterpreters.com/] an amazing book by Bob Nystorm. So let&apos;s try to find if it is possible to combine any instructions in our case. We add an array with size of &lt;code&gt;[number of instructions][number of instructions]&lt;/code&gt;. This is because we want to check how each instruction relates with other ones. At the end of parse function we add this code to make it record op_assoc:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ops[++ops_len] = op;
if (ops_len &amp;gt; 0) {
    // This logic simply tries to unite op_assoc[1][5]
    // and op_assoc[5][1] into one single field
    int op_type_1 = (int)ops[ops_len].op_type;
    int op_type_2 = (int)ops[ops_len - 1].op_type;
    if (op_type_1 &amp;gt;= op_type_2) {
      op_assoc[op_type_2][op_type_1]++;
    } else {
      op_assoc[op_type_1][op_type_2]++;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We try to unite results of form &lt;code&gt;op_assoc[i][j]&lt;/code&gt; and &lt;code&gt;op_assoc[j][i]&lt;/code&gt; into one field &lt;code&gt;op_assoc[i][j]&lt;/code&gt;, cause we don&apos;t want to see associativity of &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;[&lt;/code&gt; and &lt;code&gt;[&lt;/code&gt; and &lt;code&gt;+&lt;/code&gt; as separate results. With this done, let&apos;s try to get output of it for mandelbrot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DEBUG: op_assoc[1][1]: 3506
DEBUG: op_assoc[1][3]: 438
DEBUG: op_assoc[1][4]: 337
DEBUG: op_assoc[1][5]: 3
DEBUG: op_assoc[1][7]: 498
DEBUG: op_assoc[1][8]: 568
DEBUG: op_assoc[2][2]: 3604
DEBUG: op_assoc[2][3]: 386
DEBUG: op_assoc[2][4]: 246
DEBUG: op_assoc[2][5]: 3
DEBUG: op_assoc[2][7]: 362
DEBUG: op_assoc[2][8]: 521
DEBUG: op_assoc[3][3]: 224
DEBUG: op_assoc[3][7]: 30
DEBUG: op_assoc[3][8]: 86
DEBUG: op_assoc[4][4]: 2
DEBUG: op_assoc[4][7]: 462
DEBUG: op_assoc[4][8]: 133
DEBUG: op_assoc[5][7]: 1
DEBUG: op_assoc[5][8]: 1
DEBUG: op_assoc[7][7]: 10
DEBUG: op_assoc[8][8]: 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Highest oens are (2, 2), (1, 1), so repeating instructions, specifically &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, these should be easy to club. Let&apos;s do just that, we will add a &lt;code&gt;repeat&lt;/code&gt; field for each operation which will store how many times does the operation repeat. After this we can make exec function increment values by &lt;code&gt;repeat&lt;/code&gt;&apos;s value instead of just 1, after this change our &lt;code&gt;exec&lt;/code&gt; function looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int exec(char *prog, int prog_len) {
  DBG_PRINT(prog);
  int i = 0, val;

  parse(prog, prog_len);
  // print_op_assoc(); // This is for checking which all ops occur together
  fill_brackets_loc();

  while (i &amp;lt;= ops_len) {
    // start = clock();
    switch (ops[i].op_type) {
    case FWD:
      pointer += ops[i].repeat; // We increment by `repeat` now
      break;
    case BWD:
      pointer -= ops[i].repeat; // We increment by `repeat` now
      break;
    case INCREMENT:
      val = (int)tape[pointer];
      val += ops[i].repeat; // We increment by `repeat` now
      tape[pointer] = (char)val;
      break;
    case DECREMENT:
      val = (int)tape[pointer];
      val -= ops[i].repeat; // We increment by `repeat` now
      tape[pointer] = (char)val;
      break;
    case OUTPUT:
      printf(&quot;%c&quot;, tape[pointer]);
      break;
    case INPUT: {
      char ch = (char)getchar();
      tape[pointer] = ch;
      break;
    }
    case JMP_IF_ZERO:
      if (tape[pointer] == 0) {
        int idx = open_brackets_loc[i];
        if (idx == -1) {
          DBG_PRINTF(&quot;[: got bracket_loc as -1 for i: %d&quot;, i);
          ABORT(&quot;invalid state&quot;);
        }
        i = idx;
        continue;
      }

      break;
    case JMP_IF_NOT_ZERO: {
      if (tape[pointer] != 0) {
        int idx = close_brackets_loc[i];
        if (idx == -1) {
          DBG_PRINTF(&quot;]: got bracket_loc as -1 for i: %d&quot;, i);
          ABORT(&quot;invalid state&quot;);
        }
        i = idx;
        continue;
      }

      break;
    }
    case INVALID:
      ABORT(&quot;INVALID shouln&apos;t have leakded till here, there&apos;s a bug in parsing &quot;
            &quot;code&quot;);
    default:
      break;
    }

    i++;
  }

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple and easy, let&apos;s benchmark this change:&lt;/p&gt;
&lt;p&gt;Factor: ~2.16s
Mandelbrot: ~5.9s&lt;/p&gt;
&lt;p&gt;And we get another round of massive speedups! Whole code is available at: https://github.com/feniljain/brenphuk/tree/attempt_3&lt;/p&gt;
&lt;p&gt;This is where halt our efforts for optimizations, next we are going to learn about JITs from systems perspective, how do we leverage kernel APIs to achieve JITting.&lt;/p&gt;
</content:encoded></item><item><title>Lets write a Brainfuck Interpreter: Naive Implementation</title><link>https://fknil.pages.dev/blog/brainfuck-jit-interpreter-1/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/brainfuck-jit-interpreter-1/</guid><pubDate>Tue, 21 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is a series where we will slowly climb up to building a JIT for a brainfuck compiler. This is the first blog in the series covering the language and a naive implementation. We try to understand everything from first principles, so buckle up and let&apos;s get started!&lt;/p&gt;
&lt;h2&gt;Understanding Brainfuck Language Operators and Spec&lt;/h2&gt;
&lt;p&gt;Brainfuck is a super simple language which takes the idea of a turing machine and implements it as a programming language. That means we have a tape, an array of cell where each cell contains a number, and we just move around on it operating on numbers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      ----------------------------------------
Tape: |10||65||0||0||45||14||0||0||0||0||0||0|
      ----------------------------------------
                          ^
                          |
      Pointer -------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s look at all the operators:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; : move right to next cell on tape&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&lt;/code&gt; : move left to previous cell on tape&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; : increment the value of current cell&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-&lt;/code&gt; : decrement the value of current cell&lt;/li&gt;
&lt;li&gt;&lt;code&gt;,&lt;/code&gt; : take input from user and store it in current cell&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt; : output value stored in current cell to user&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[&lt;/code&gt; : jump to matching &lt;code&gt;]&lt;/code&gt;, if value is zero&lt;/li&gt;
&lt;li&gt;&lt;code&gt;]&lt;/code&gt; : jump to matching &lt;code&gt;[&lt;/code&gt;, if value is not zero&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that&apos;s it, this is the whole language, suprisingly simple right xD&lt;/p&gt;
&lt;p&gt;There are a few properties we haven&apos;t discussed yet, they are more like implementation details, for e.g. how long should the tape be?
what to do if you cross the max size of tape? what should be initial value of cell? These things are outlined in compelte detail in this spec:
https://github.com/sunjay/brainfuck/blob/master/brainfuck.md&lt;/p&gt;
&lt;h2&gt;Small Brainfuck Programs which we will use in tests of our interpreter&lt;/h2&gt;
&lt;p&gt;As we know about all operators let&apos;s try getting our hands dirty and write few small programs, this also would give us an additional benefit of having test
cases ready for our interpreter. We can build incrementally harder programs and use them for our interpreter, this way we also add a nice incremental debugging test suite.&lt;/p&gt;
&lt;p&gt;Super simple program: +++ , it just adds 1 to first cell thrice, so our tape would look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |3||0||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next one: ++-, add 1 to first cell twice, and then subtract 1 from it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |2||0||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s use &lt;code&gt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&lt;/code&gt; operators now: ++&amp;gt;+&amp;lt;-, this program first adds 1 twice to first cell, then shift to second cell, adds 1 over there, comes back to first cell and does a subtract operation. Our tape is now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |1||1||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Moving to next operators &lt;code&gt;,&lt;/code&gt; and &lt;code&gt;.&lt;/code&gt;: ,+., this program takes input from user, stores it in first cell, adds one to it and outputs it. Let&apos;s say we pass 65 when prompted for input, our tape would look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |66||0||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and our output would be: &lt;code&gt;B&lt;/code&gt;, we print ascii representations of numbers stored in cell, that&apos;s also how we get &lt;code&gt;hello world&lt;/code&gt; too later down the road xD&lt;/p&gt;
&lt;p&gt;Side Tip: Can&apos;t remember what ascii code represents what character? Don&apos;t worry there&apos;s a man page for it, just run: &lt;code&gt;man ascii&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;Now comes the l👀ps: [++], this programmm: does nothing :P . &lt;code&gt;[&lt;/code&gt; operator says jump to corresponding &lt;code&gt;]&lt;/code&gt; when current cell is zero, and by default all cell
values are zero, so our program jumped to last operator of program and exited.&lt;/p&gt;
&lt;p&gt;Okay, let&apos;s do something serious this time: +++[-] . It&apos;s a simple program, we first increment first cell to value three, next we start a loop, this time it won&apos;t jump cause we have value 3 in there, in first iteration it will decrement value by one, i.e. to 2. we then have &lt;code&gt;]&lt;/code&gt; which jumps to corresponding &lt;code&gt;[&lt;/code&gt; if cell has non-zero value, so it goes back to &lt;code&gt;[&lt;/code&gt; and second iteration starts where decrement happens and again jump happens, this continues till zero and at that point &lt;code&gt;]&lt;/code&gt; sees a zero value at it&apos;s cell and exits the program.&lt;/p&gt;
&lt;p&gt;After exec of &lt;code&gt;+++&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |3||0||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After first iteration, we exec 4th, 5th and 6th operator in program here:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[&lt;/code&gt; -&amp;gt; value is 3, don&apos;t jump, go to next instruction
&lt;code&gt;-&lt;/code&gt; -&amp;gt; decrement value to 2
&lt;code&gt;]&lt;/code&gt; -&amp;gt; value is 2, non-zero, jump to &lt;code&gt;[&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |2||0||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After second iteration, we again execute 4th, 5th and 6th operator:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[&lt;/code&gt; -&amp;gt; value is 2, don&apos;t jump, go to next instruction
&lt;code&gt;-&lt;/code&gt; -&amp;gt; decrement value to 1
&lt;code&gt;]&lt;/code&gt; -&amp;gt; value is 1, non-zero, jump to &lt;code&gt;[&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |1||0||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After third iteration, we again execute 4th, 5th and 6th operator:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[&lt;/code&gt; -&amp;gt; value is 1, don&apos;t jump, go to next instruction
&lt;code&gt;-&lt;/code&gt; -&amp;gt; decrement value to 0
&lt;code&gt;]&lt;/code&gt; -&amp;gt; value is 0, don&apos;t jump, go to next instruction i.e. program end&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |0||0||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This seems tedious to do it by hand right? No probs some humble person on internet made this for us:
https://arkark.github.io/brainfuck-online-simulator/&lt;/p&gt;
&lt;p&gt;Visualization of brainfuck programs, really helpful for debugging!&lt;/p&gt;
&lt;p&gt;Okay, now let&apos;s use loops, cell movement, etc together: &amp;gt;+++++++++[&amp;lt;++++++&amp;gt;-]&amp;lt;...&amp;gt;++++++++++. , at this point you should try to do some brain job and figure it out on your own.&lt;/p&gt;
&lt;p&gt;Final state of tape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ----------....
 Tape: |54||10||0||
       ----------....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Having difficulties understanding? Try the online playground I linked above and iterate slowly on each operator, you should be able to figure it out.
(hopefully :P just kidding xD)&lt;/p&gt;
&lt;p&gt;Okay, one last program and we are done, I just couldn&apos;t skip this program:&lt;/p&gt;
&lt;p&gt;++++++++[&amp;gt;++++[&amp;gt;++&amp;gt;+++&amp;gt;+++&amp;gt;+&amp;lt;&amp;lt;&amp;lt;&amp;lt;-]&amp;gt;+&amp;gt;+&amp;gt;-&amp;gt;&amp;gt;+[&amp;lt;]&amp;lt;-]&amp;gt;&amp;gt;.&amp;gt;---.+++++++..+++.&amp;gt;&amp;gt;.&amp;lt;-.&amp;lt;.+++.------.--------.&amp;gt;&amp;gt;+.&amp;gt;++.&lt;/p&gt;
&lt;p&gt;this is
is
is
is:&lt;/p&gt;
&lt;p&gt;&quot;hello world&quot;&lt;/p&gt;
&lt;p&gt;lessggoo we did it! We have reached hello world finally. It&apos;s a good exercise to think about execution here too, I think spec itself does a good job at trying
to explain it, so it&apos;s better to give that a try: https://github.com/sunjay/brainfuck/blob/master/brainfuck.md#hello-world-example&lt;/p&gt;
&lt;h2&gt;C build system setup&lt;/h2&gt;
&lt;p&gt;Okay, enough brain jog, time to get hands dirty with actual interpreter implmentation, but before we start I want to clear up few things. First, we are going to implement this interpreter in C, and I am not that good at writing idiomatic C code, so if you find some weird way of doing things, that&apos;s just me being noob :P . And next thing is, we will be using meson as our build system. You can change the build system as per your convinience, I am not doing some rocket science with it, so shouldn&apos;t be hard to port from any to any.&lt;/p&gt;
&lt;p&gt;I am going to dump the whole file at once, it&apos;s not much and easy to understand:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;project(&apos;brenphuk&apos;, &apos;c&apos;,
  version : &apos;0.1&apos;,
  default_options : [&apos;warning_level=3&apos;, &apos;default_library=static&apos;])

readline_dep = dependency(&apos;readline&apos;).as_system()

# source: https://github.com/tiernemi/meson-sample-project/blob/master/meson.build
# This adds the clang format file to the build directory
configure_file(input : &apos;.clang-format&apos;,
               output : &apos;.clang-format&apos;,
	       copy: true)

run_target(&apos;format&apos;,
  command : [&apos;clang-format&apos;,&apos;-i&apos;,&apos;-style=file&apos;, [&apos;../interpreter.c&apos;]])

run_command(&apos;clang-format&apos;,&apos;-i&apos;,&apos;-style=file&apos;, &apos;interpreter.c&apos;, check: true)

executable(&apos;brenphuk&apos;,
           &apos;interpreter.c&apos;,
           install : true,
		   c_args: [&apos;-Werror&apos;, &apos;-Wall&apos;, &apos;-Wextra&apos;, &apos;-Wshadow&apos;, &apos;-Wconversion&apos;,
					&apos;-Wcast-align&apos;, &apos;-Wunused&apos;, &apos;-Wpointer-arith&apos;, &apos;-Wold-style-cast&apos;,
					&apos;-Wundef&apos;, &apos;-Winit-self&apos;, &apos;-Wredundant-decls&apos;, &apos;-Wmissing-include-dirs&apos;,
					&apos;-Wswitch-default&apos;, &apos;-Wswitch-enum&apos;, &apos;-Wfloat-equal&apos;, &apos;-Wformat-security&apos;,
					&apos;-Wpedantic&apos;,
					&apos;-g&apos;],
           dependencies : [readline_dep],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We set our project name as brenphuk, set &lt;code&gt;readline&lt;/code&gt; as a dependency we need, we will be using that for REPL mode. Then we set up some formatting commands, code should look good always :) . Finally we define what our executable will be called and what files to use to make it, with some c-args, addded a bunch of them just for more strictness and help me not make mistakes, finally we pass &lt;code&gt;readline&lt;/code&gt; dependency to executable to be built together.&lt;/p&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;For main impl, what we want to do is, take input from user, go over character by character and perform operation specified by operand mentioned on that index. Let&apos;s call this core function of ours as &lt;code&gt;exec&lt;/code&gt;, which will accept an &lt;code&gt;engine&lt;/code&gt; struct, this struct contains our actual tape and pointer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
  char tape[TAPE_SIZE];
  int pointer;
} engine;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Along with engine, it will also accept a string, the program itself given as input from user.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int exec(engine *eng, char *prog) {
  size_t prog_len = strlen(prog);
  size_t i = 0;
  while (i &amp;lt; prog_len) {
    switch (prog[i]) {
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s start adding impl of operations now, for &lt;code&gt;&amp;lt;&lt;/code&gt;, we just want to increment engine-&amp;gt;pointer, so it&apos;s simple as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;&amp;gt;&apos;:
  eng-&amp;gt;pointer++;
  break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and it&apos;s opposite &lt;code&gt;&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;&amp;lt;&apos;:
  eng-&amp;gt;pointer--;
  break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly for &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt;, we want to increment value in tape on the index &lt;code&gt;pointer&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;+&apos;:
  eng-&amp;gt;tape[eng-&amp;gt;pointer]++;
  break;
case &apos;-&apos;:
  eng-&amp;gt;tape[eng-&amp;gt;pointer]--;
  break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For &lt;code&gt;,&lt;/code&gt;, we want to take input and store it in currently pointed cell&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;,&apos;: {
  char ch;
  scanf(&quot;%c&quot;, &amp;amp;ch);
  eng-&amp;gt;tape[eng-&amp;gt;pointer] = ch;
  break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and for &lt;code&gt;.&lt;/code&gt;, we want to output currently pointed cell&apos;s value&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;.&apos;:
  printf(&quot;%c&quot;, eng-&amp;gt;tape[eng-&amp;gt;pointer]);
  break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now comes the slightly more interesting ones, loop constructs. For &lt;code&gt;[&lt;/code&gt; and &lt;code&gt;]&lt;/code&gt;, we want to jump to corresponding loop operand on certain condition.
On finding a zero on &lt;code&gt;[&lt;/code&gt; operand, we have to find corresponding &lt;code&gt;]&lt;/code&gt; operand, as the loops can be nested we have to keep a count of loop constructs we
have seen, so we maintain a counter, and increment it whenever we see a &lt;code&gt;[&lt;/code&gt;, and decrement when we see a &lt;code&gt;]&lt;/code&gt;. When we complete at the same number we started
our counter with, we have reached corresponding &lt;code&gt;]&lt;/code&gt;. Impl for &lt;code&gt;[&lt;/code&gt;, will look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;[&apos;: {
  if (eng-&amp;gt;tape[eng-&amp;gt;pointer] == 0) {
    int brackets_depth = 0;
    while (i &amp;lt; prog_len) {
      if (prog[i] == &apos;[&apos;) {
        brackets_depth++;
      } else if (prog[i] == &apos;]&apos;) {
        brackets_depth--;
      }

      if (!brackets_depth) {
        break;
      }

      i++;
    }

    if (brackets_depth != 0) {
      ABORT(&quot;could not find matching closing square bracket&quot;);
    }
  }

  break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;here we use &lt;code&gt;bracket_depth&lt;/code&gt; to keep track of the counter we discussed above, we start &lt;code&gt;brackets_depth&lt;/code&gt; as 0, so at the end of transversing whole program if &lt;code&gt;brackets_depth&lt;/code&gt; is not zero, we print out &lt;code&gt;brackets mismatch error&lt;/code&gt;. Slightly, different impl for &lt;code&gt;]&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &apos;]&apos;: {
  if (eng-&amp;gt;tape[eng-&amp;gt;pointer] != 0) {
    int brackets_depth = 0;
    while (i &amp;gt; 0) {
      if (prog[i] == &apos;[&apos;) {
        brackets_depth--;
      } else if (prog[i] == &apos;]&apos;) {
        brackets_depth++;
      }

      if (!brackets_depth) {
        break;
      }

      i--;
    }

    if (brackets_depth != 0) {
      ABORT(&quot;could not find matching opening square bracket&quot;);
    }
  }

  break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s it, ofc we do need supporting code in main and things around repl, I am not covering them in blog cause they are mostly irrelevant and easy to do,
still just to whole thing together, you can check: https://github.com/feniljain/brenphuk/tree/attempt_1&lt;/p&gt;
&lt;p&gt;This implementation also contains a benchmark suite, this will be helpful when comparing results of our different approaches in sections further.
Benchmark prorgams are present in https://github.com/feniljain/brenphuk/tree/attempt_1/programs , when we run above program with -O3, we get these execution times:&lt;/p&gt;
&lt;p&gt;Factor: ~24-25s
Mandelbrot: ~69-70s&lt;/p&gt;
&lt;h2&gt;Resources:&lt;/h2&gt;
&lt;p&gt;I have collected few brainfuck resources to help here: https://github.com/feniljain/knowledge-base/blob/main/programming-languages/brainfuck/README.md&lt;/p&gt;
</content:encoded></item><item><title>Faster shell boot times</title><link>https://fknil.pages.dev/blog/faster-shell-boot/</link><guid isPermaLink="true">https://fknil.pages.dev/blog/faster-shell-boot/</guid><pubDate>Fri, 19 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Optimizing My Shell Startup Times&lt;/h2&gt;
&lt;p&gt;I was going through Thorsten&apos;s latest blog about faster startup times, and he talks about shell startup times, here is the direct link for curious: https://registerspill.thorstenball.com/p/how-fast-is-your-shell . This made me wonder how fast is my shell config, and work on my shell load times to at least get them in a bareable range.&lt;/p&gt;
&lt;p&gt;So to start we find how to measure our load times, well Thorsten covers it nicely with this simple one liner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;time zsh -i -c exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and one data point is not reliable, so let&apos;s run it 10 times:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i in $(seq 1 10); do time $SHELL -i -c exit; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I felt, looking at 10 results wasn&apos;t as good, so here&apos;s a script which gives you average for one of the columns:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/zsh

for i in $(seq 1 10); do
  2&amp;gt;&amp;amp;1 time $SHELL -i -c exit
done | awk &apos;{sum += $11} END {print &quot;Average:&quot;, sum/NR}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Note this averages on total)&lt;/p&gt;
&lt;p&gt;Cool, this looks good, well I hoped I could say the same for my shell startup times 😬. They were in order of ~600ms, that is very bad, super heavy leaded shoes in terms of original article :(
Thankfully it also links to some amazing articles, most notably: https://htr3n.github.io/2018/07/faster-zsh/. This is an amazing article which I referred through whole of my process, I did not end up implementing all the tricks because I wanted to keep all my config in .zshrc and also not much long.&lt;/p&gt;
&lt;p&gt;First tip is about profiling zsh, and that is done using zprof, we can enable it by pasting following line in .zshrc:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zmodload zsh/zprof
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or running &lt;code&gt;zmodload zprof&lt;/code&gt; directly in shell (note the difference of &lt;code&gt;zsh/&lt;/code&gt;). This gives us places where zsh spends most time in, and hence our data points to start optimizing. But before diving straight into checking profiling output, I decided to checkout few low hanging/no-brainer things I can sort myself out. This was easier to do considering I don&apos;t maintiain my .zshrc as avidly.&lt;/p&gt;
&lt;p&gt;To start, I had to figure out what all config files does zsh even consider, thankfully htr3n&apos;s article covers sequences of config files loaded and hence also listing them. So I started checking those files and found out unused nix, orbstack, etc. exports. First low hanging fruits spotted! Next moved onto .zshrc and cleaned up unused/old/irrelevant exports.&lt;/p&gt;
&lt;p&gt;Few of the time taking processes are, &lt;code&gt;eval&lt;/code&gt; calculation and &lt;code&gt;subshell&lt;/code&gt; spawning, if you are a mac user and have something like: &lt;code&gt;brew --prefix&lt;/code&gt; in your config, that is an indication of subshell spawning. To optimize this, we can run given command manually and then paste the output in place of subshell spawning code. One thing to note is, this is a double-edge sword, as any changes in install locations from brew in future would cause breakages for us, so do think about it before applying. Well I applied them and this instantly causes my startup time to reduce by half i.e. we reach 300ms territory.&lt;/p&gt;
&lt;p&gt;For our next target, article mentions version managers like rvm and nvm are super heavy and contribute a lot to startup times. That&apos;s easy, just stop using them? Right? Nope, we introduce some indirection. The godly trick which solves and breaks almost everything in computers. In this case, we define an additional function with same name as version manager, let&apos;s say &lt;code&gt;nvm&lt;/code&gt; and move all the nvm required load code in there, and finally call &lt;code&gt;nvm&lt;/code&gt; at last. In my case it ended up looking like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function nvm() {
    export NVM_DIR=&quot;$([ -z &quot;${XDG_CONFIG_HOME-}&quot; ] &amp;amp;&amp;amp; printf %s &quot;${HOME}/.nvm&quot; || printf %s &quot;${XDG_CONFIG_HOME}/nvm&quot;)&quot;
    [ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; \. &quot;$NVM_DIR/nvm.sh&quot;
    if [[ -e ~/.nvm/alias/default ]]; then
      PATH=&quot;${PATH}:${HOME}.nvm/versions/node/$(cat ~/.nvm/alias/default)/bin&quot;
    fi
    [ -s &quot;$NVM_DIR/bash_completion&quot; ] &amp;amp;&amp;amp; \. &quot;$NVM_DIR/bash_completion&quot;  # This loads nvm bash_completion

    # invoke the real nvm function now
    nvm &quot;$@&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What this results in is a neat trick, where whenever we call nvm for the first time, this function gets called and hence things gets loaded for the first time. So it does not block and increase load times on startup. This reduced my startup times to ~100ms!&lt;/p&gt;
&lt;p&gt;Some amazing progress for small and straightforward changes.&lt;/p&gt;
&lt;p&gt;Now for another small win, we disable auto-updates using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DISABLE_AUTO_UPDATE=&quot;true&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This brought us to ~80ms. Next I removed &lt;code&gt;battery&lt;/code&gt; and &lt;code&gt;git&lt;/code&gt; plugins, this shaved off 10ms more, so ~70ms. This is also a good time to talk about those fancy shells, if you someone who has a fancy shell which shows git version, nvm version, battery level, neighbour&apos;s mom&apos;s number, then you will suffer from relatively higher load times always, cause of the many computations shell needs to do in background to fill up those fancy prompts. I personally moved off them much before and now use simple &lt;code&gt;robbyrussell&lt;/code&gt; theme, which is super minimal and does not get in my way or try to occupy more space than necessary.&lt;/p&gt;
&lt;p&gt;After this, according to zprof most of zsh&apos;s time is spent on autocompletions, and in turn compinit, etc. components, there are some really nice tricks to optimize these components further, for e.g. compinit tries to read a cache files everytime it is invoked, people have observered that&apos;s not necessary and can be reduced to once per day, this gave some more small wins. After a while there comes a plateau in optimizing, where you are in a land in which moving the needle to your favour becomes increasingly harder and harder, this is that territory. Well maybe that&apos;s not quite true, cause one of the big optimizations which can be done is removing oh-my-zsh, I tried that and it caused my load times to go down to ~20ms. The thing is I don&apos;t want to move off omz yet. I tried to find some alternatives but wasn&apos;t as happy, honestly didn&apos;t spend much time looking too, so if I do find a better solution will update this article! Just as a side note one of the resource I am planning to check out was this gist: https://gist.github.com/laggardkernel/4a4c4986ccdcaf47b91e8227f9868ded, it was linked in comments of same article (aint it a gold mine xD).&lt;/p&gt;
&lt;p&gt;Another small thing which can help in speeding up very first shell load (i.e. after a fresh boot), is to remove ASL logs, these are apple logs which on some systems can grow to be huge. One can prune them easily using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /private/var/log/asl/
ls *.asl
sudo rm !$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read more about its benefits/losses in this article: https://osxdaily.com/2010/05/06/speed-up-a-slow-terminal-by-clearing-log-files/&lt;/p&gt;
&lt;p&gt;I tried a few more things, like replacing hack mentioned in this article: https://coderwall.com/p/sladaq/faster-zsh-in-large-git-repository, to reduce git branch computation time, tho it wasn&apos;t that bad for me, so I skipped this one also especially because this required changing a omz lib file. Well at last this was the place where I stopped trying to do any more complex more code optimizations and rested my case for this time. (we will pick it up again for sure!)&lt;/p&gt;
&lt;p&gt;If you would like to go through all of my changes together, this is the commit: https://github.com/feniljain/dotfiles/commit/202e22baee2164e4c38e18eb88db7a1b920f84c6#diff-d30bab601f4597c635d0bd4915f3475c4c22170a538d6781cd086bdfe100961fL5&lt;/p&gt;
</content:encoded></item></channel></rss>