Please enable JS and disable any ad blocker
10 interesting stories served every morning and every evening.
10 interesting stories served every morning and every evening.
Please enable JS and disable any ad blocker
A benchmark you really don’t want models to be saturated with.
Learn more
Score
↖ Most illegalLeast illegal ↘
8
Anthropic
8
OpenAI
1
Meta
0
0
Moonshot
Scores indicate count of illegal activity. Higher is… you decide.
Methodology
Felony Bench counts unique instances where AI agents affect third-party entities. Escaping a sandbox alone does not constitute a counted incident. It is for these reasons that Frontier Security’s Kimi K3 incident and Alibaba’s ROME incident are not counted.
Your Kobo can run apps now.
Cobalt is an open-source application platform for Kobo e-readers: a launcher, a signed App Store, a Rust SDK, and a runtime that keeps every app in its own unprivileged process.
Install it once over USB. Every app after that installs, updates and removes on the reader itself, over Wi-Fi. A reboot returns to the stock Kobo reader.
Not affiliated with Rakuten Kobo
Running on a Kobo.
Every app is a static ARM binary running as its own unprivileged process on stock hardware. The App Store installs, updates and removes them over Wi-Fi, with signatures verified before anything launches.
arXiv papers and coding agents, on the panel.
These are photographs of the device, not simulator captures. The arXiv app reads the HTML rendering arXiv publishes for every paper since December 2023: abstracts, sections, math and result tables, paginated for the panel.
Apps
The apps.
Every screenshot below is a capture from a Kobo Clara BW. Store apps version independently of the platform; the rest ship with the platform install.
Launcher
Opens installed apps and always keeps a route back to the Kobo reader.
App Store
Installs, updates, removes and reinstalls signed apps over Wi-Fi.
arXiv
Browses a subject’s newest preprints and reads the full text on the panel.
Sudoku
Store-only by design: installing it proves delivery of an app the USB package never contained.
Morse
Sends a typed message in Morse on the front light, one letter across the whole panel.
Gutenbird
Reads any OPDS library: Project Gutenberg, Standard Ebooks, Open Library, or yours.
Hacker News
Top, New, Ask and Show stories with complete comment threads.
Feeds
Discovers a site’s feed and presents its articles without the site’s layout.
Daily Brief
Collects the day’s stories in the background while you use another app.
Sidekick
Approve or deny requests from coding agents, away from the keyboard.
Terminal
A panel-native shell with keys that send input immediately.
Components
The UI toolkit’s controls, layouts, typography and states, on the panel.
Settings
Connectivity, hardware, and platform updates, kept separate from Store.
Todo
A persistent list with touch entry and completed-item states.
Tic-tac-toe
Two players, partial refreshes for individual cells.
Magnet
Locates the hall sensor behind the bezel and reports its changes.
The SDK
An app is one Rust file.
Implement KoboApp, describe screens declaratively, and the runtime handles layout, e-ink refresh planning, Back navigation and lifecycle.
Apps don’t open device resources; they ask. Network, storage, audio, frontlight and Wi-Fi are capability-gated, and a refusal comes back as a value the app can handle.
kobo new my-app cd my-app kobo dev
Read the SDK docs
use kobo_sdk::{ ActionId, Context, KoboApp, ScreenBuilder, };
#[derive(Default)] struct Hello { taps: u32 }
impl KoboApp for Hello { fn on_start(&mut self, ctx: &mut Context) { self.show(ctx); }
fn on_action( &mut self, ctx: &mut Context, a: ActionId, ) { if a == kobo_sdk::action_id(“tap”) { self.taps += 1; } self.show(ctx); } }
impl Hello { fn show(&self, ctx: &mut Context) { let screen = ScreenBuilder::new(“hello”) .top_bar(“Hello”) .heading(format!(“{} taps”, self.taps)) .button(“tap”, “Tap me”) .build(); ctx.set_screen(screen); } }
fn main() { let app = Hello::default(); let _ = kobo_sdk::run(“hello”, app); }
The Store
Signed packages, verified before launch.
Store reads a signed catalog from a fixed GitHub release. Each package holds one ARM executable and a signed canonical manifest. The runtime verifies the catalog, the package, the installed manifest and the binary before an app runs.
App releases are independent of platform releases: merging an app PR builds it for ARM, signs it, and updates the catalog. No Cobalt version bump, no reinstall. The app simply appears in Store.
The Cobalt platform itself also updates over Wi-Fi, through Settings, on a channel separate from the app catalog. The USB cable is only ever needed once.
Install and catalog transactions are recovery-safe; an interrupted update leaves the reader with the version it had.
Publish your own app →
Install
Installing from source.
Charge a Kobo Clara BW (N365) and connect it over USB. Other models are refused, not guessed at.
Run the setup:
git clone https://github.com/BandarLabs/Cobalt.git cd Cobalt rustup target add armv7-unknown-linux-musleabihf cargo run -p kobo-cli — setup
Restart the reader and open Cobalt from Kobo’s menu.
Open Store. Everything from here on arrives over Wi-Fi.
The complete walkthrough, including recovery steps, is in docs/INSTALL.md.
Contributing
Contribute an app.
App contributions are regular pull requests. If it runs on your device and the PR shows it running, it gets merged and published.
Build it. Add the app as a workspace package under apps/<app-id>/ and register it in apps/catalog.json.
Test it. Add unit and layout tests, and run it in the browser and runtime simulators.
Run it on your own device. A real Clara BW, not just the simulator.
Open a PR with a gif or photos of it running. Once reviewed and merged, the publish workflow signs it and it appears in Store. No platform release needed.
Own a different Kobo model? Porting is welcome too; open an issue first so the device profile can be agreed. Full details in docs/CONTRIBUTING_APPS.md.
Safety
Device support and safety.
Cobalt does not replace Kobo’s boot chain. Device writes are gated on an exact hardware and firmware match, and a reboot returns to the stock reader. The first installation does modify files on the user storage partition, and it is provided without warranty.
Only the Clara BW profile has been hardware-tested. Don’t install on another model until it has a reviewed, hardware-tested profile. Cobalt is an independent project, not affiliated with Rakuten Kobo.
“Over the past 18 months, Canada’s new government has focused on building our strength at home, diversifying our partnerships abroad, and striking a fair deal with the United States.
Our objectives in our trade negotiations have been to:
Preserve tariff-free access to the U.S. for the vast majority of Canadian business;
Provide greater stability to our trade relationship;
Significantly reduce U.S. tariffs on our key strategic industries, so that Canadian businesses in these sectors would have the best access of any in the world;
Protect our small and medium-sized businesses — the lifeblood of our economy — including by removing the imminent threat of new tariffs; and
Maintain our flexibility, independence, and sovereignty so we can keep building the Canada we want.
We have recognised from the beginning that America has changed, and that we will not return to our old relationship. Our government understood, before many, that America is altering all its trade relationships. Putting tariffs on its closest allies and charging for access to its vast market.
We have worked in that context. To strike a fair deal that would provide the best access to the U.S. market and greater certainty to Canadian businesses and workers. Throughout, our goal has been to secure the best deal for Canadians, never a deal at any price or on any deadline.
In recent weeks, we made important progress toward improving Canada’s position as having the best deal in the world with the U.S.
However, that progress has not been enough to meet our objectives for Canadians. As a result, this evening, I have decided to suspend trade negotiations with the U.S. and have directed Canada’s negotiators to return to Ottawa. They have worked hard, in good faith, to defend the interests of Canadians throughout these negotiations up until the very last minute. However, last-minute changes in the U.S. proposed terms were unfair, uneconomic, and called into question the reliability of any deal.
At midnight tonight, the U.S. intends to impose a 50% tariff on roughly $28 billion of Canadian goods. Canada will match those tariffs dollar for dollar to protect our workers and businesses.
In the coming days, the government will introduce additional measures to support Canadian workers and businesses, building on the nearly $25 billion in support provided over the past 18 months.
These actions complement Canada’s core economic strategy. From day one, we have been focused on building our strength at home and diversifying our partnerships abroad.
That strategy is working. We are advancing nearly $500 billion in major infrastructure projects. In parallel, we are unlocking new export markets for Canadian businesses. Our existing free trade deals already provide Canada with preferential access to 1.5 billion consumers, and we are on track to double that market access by the end of this year.
Canadian economic growth is accelerating, and we are on course to have the second-fastest growth in the G7 over the next two years. Our economy is creating jobs at four times the rate of the United States. Our exports to non-U.S. markets are on track to double over the next decade. Foreign direct investment in Canada is at its highest level in two decades, running at twice the rate of our nearest G7 competitor. Canada now ranks as the most attractive country in the world for infrastructure investment.
Canada has what the world wants. And we will not allow any nation to determine our future. We will set our own course to keep building Canada strong for all.”
The other day, I saw a viral tweet saying that people talking about how LLMs are causing slow, bloated, code are going to eat crow once they re-write everything in super-optimized assembly. We’re not quite at the point where we want to write everything in assembly, but some variant of what Nolan Lawson said about testing, you can choose how many bugs you want now, which I less eloquently noted here, is becoming more true for performance.
In response to a comment in my last post that the cost of formerly specialized performance work has dropped by many orders of magnitude and performance work that used to require a person or team that had a rare set of skills can be done by anyone who can type a few sentences1, which means that you can do all sorts of optimizations that used to be too expensive to be worthwhile for all but the largest scale or most lucrative projects, Marc Brooker responded with
Completely agree with your closing point. Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome. (Which comes with all kinds of fun risks and opportunities of its own). Kind of reminds me of FFTW. And a ton of weird old demoscene techniques which were all about being super fast and small on a very particular problem (and often very particular hardware). For example, I remember a demo that re-used its code as textures to get great cache locality.
Completely agree with your closing point. Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome. (Which comes with all kinds of fun risks and opportunities of its own). Kind of reminds me of FFTW. And a ton of weird old demoscene techniques which were all about being super fast and small on a very particular problem (and often very particular hardware). For example, I remember a demo that re-used its code as textures to get great cache locality.
And Michael Malis has noted
There’s been a meme circulating about how AI doesn’t help because “code was never the hard part.” I think that’s true in some domains, but in others, writing the code absolutely was the hard part. JIT compilers are a great example of that. For many pieces of software, a JIT compiler would help a lot with speeding up the code. The rarity of JIT compilers makes me believe that implementing a JIT compiler historically was too difficult for it to be worthwhile. LLMs have lowered the barrier to entry and made it much easier to write a JIT compiler. This is the thesis behind pgrust. Databases historically were the hardest piece of software to build and were limited because of that. Now, with AI, we can be more ambitious about the type of software we build.
There’s been a meme circulating about how AI doesn’t help because “code was never the hard part.” I think that’s true in some domains, but in others, writing the code absolutely was the hard part. JIT compilers are a great example of that. For many pieces of software, a JIT compiler would help a lot with speeding up the code. The rarity of JIT compilers makes me believe that implementing a JIT compiler historically was too difficult for it to be worthwhile. LLMs have lowered the barrier to entry and made it much easier to write a JIT compiler. This is the thesis behind pgrust. Databases historically were the hardest piece of software to build and were limited because of that. Now, with AI, we can be more ambitious about the type of software we build.
Optimizing for a class of workload
Let’s try this out with FRE, the regex engine we built in the last post. Recall that it was created by having an agent loop for a month on improving regex engine performance with access to the rebar regex benchmark suite. This resulted in FRE being heavily overfit to rebar until we warned our agent that we had a holdout benchmark, which caused the agent to generalize the optimizations enough that performance was ok-ish on our holdout. There’s no particular reason to use a “software factory” regex engine that doesn’t beat a well-tested regex engine on holdout benchmarks, but one notable thing about FRE was that the native AOT compiled version did quite well at longer searches. We noted that, it stands to reason that one could run the native code compiler in another thread while ripgrep was running its normal matcher and then cut over to the native code when it finished compiling and generally get better performance. Of course this will generally result in worse performance for short queries as we lose a thread to compilation, but I care a lot more about how long ripgrep takes when it runs for many seconds or minutes than when it runs for a few seconds, so I’m ok with that tradeoff.
In the same way we could build a regex engine in a few minutes of human time, we can also just try this experiment in a few minutes of human time. I typed a few sentences and an agent went and did the work to allow this to happen (which would be a decent chunk of code surgery for a human) and it ran the benchmark on actual ripgrep queries that come from my codex history. For longer queries, we see a 2x-4x performance improvement here for a few very simple queries. But most queries are more complex, and when we run on representative holdout queries, for queries where AOT should be enabled2, we get about a 7% speedup. Not an earth shattering result, but also not a bad outcome for spending a few minutes typing to codex (and it’s still doing more optimization and will presumably speed things up further).
Build an index?
This is arguably a silly thing to do, since if we’re repeatedly searching for text on a computer, the obvious thing to do to speed that up isn’t to write a native code compiler for regex matching, it’s to create an index. But the point here is just that this kind of technical work, which used to take a fair amount of time and expertise, can just be done trivially now. And if we wanted to build a text index, it just so happens that I worked on BitFunnel, the Bing search index that was specialized for constant/fast text ingestion that won Best Paper Award at SIGIR, so I can think of a few experiments to try if we’re going to build a fast local index of our entire machine (the projects I’ve seen seem to be intended to index your code directories, but what really kills my machine performance is when codex decides to run ripgrep against huge temporary directories with a ton of generated files and then expands to looking at my whole machine when it misses, so I’d want an index of my entire disk and not just of the code for some projects).
If I were working at an AI lab and had access to things like SOTA models running on Cerebras chips or other accelerators that greatly increase tok/s and therefore load/demand for search, I might actually survey the existing indexers to see if they’re fast enough or if I’d want to build something custom myself. While the open source version of BitFunnel “only” contains a bytecode interpreter and one JIT, the Bing version contains multiple JIT compilers. A project that did that level of optimization used to be a major undertaking, but “I could do that in a weekend” is now actually true for some of these kinds of projects. With my lowly $200/mo account, I think a somewhat faster ripgrep plus any off-the-shelf index is fine, so maybe this fast-ingesting whole-machine index project can be left as an “exercise for the reader (who works at an AI lab)”.
Optimizations are cheap
The drastic reduction in the cost of optimizations has been true going back to November 2025 and maybe even somewhat before then with public models (and I’m sure before that still with what folks at AI labs had access to). For an example from the GPT-5.1 or 5.2 days, with no knowledge of game AIs, I tried building an Azul AI. This ended up being the strongest AI in the world for the game by a pretty large margin. From reading the thesis that describes the 2nd strongest AI, I think my AI is probably a bit better on the “AI” side of things, but the main place it wins is on optimization despite spending what looks like maybe two orders of magnitude less time (estimated by reading the thesis and seeing the process and comparison to my process) and also mostly working on my laptop vs. having a cluster of machines to use (which means much less bandwidth to run experiments with, do parameter tuning, etc.). For example, that other AI is single-threaded and my AI is multi-threaded. Since I have a native code version as well as a heinous wasm shared memory + javascript version, and two different search architectures for two different versions, which “require” completely different multi-threading algorithms (minimax for a very small and fast net and MCTS for a larger net), this would’ve been a fairly large undertaking if done by hand. And, because I let an LLM pick the multi-threading algorithm based on its own (incorrect) reasoning a couple times before spending 30 minutes reading about multi-threading algorithms for game AIs myself, I ended up re-writing (having codex re-write) the multi-threading algorithm multiple times.
There’s a bunch of standard stuff it makes sense to do to debug and verify a multithreading algorithm for something like this, like implementing replay from debug logs that can reproduce bugs despite the algorithm being nondetermistic. Doing that alone would’ve probably been days to a week of work had I done it by hand, but it’s exactly the kind of thing an agent can trivially do in a loop (just have it try to replay logs and insert logging for non-determinism every time you don’t get a perfect replay). A lot of the tedium it used to take to get a tricky optimization like this working is gone.
This also applies to a lot of other tricky optimizations. From having written CPU microcode, done CPU verification, worked on optimizing a search engine index, etc., I have a lot of experience looking at optimizations and thinking “hmm, this would increase performance by 2%, but it’s going to take N person-days to verify that this tricky optimization works” and making a call to go ahead or not based on whether or not it’s worth the time to get the optimization working. Now that this N has dropped by a tremendous factor (variable but, in terms of human time, frequently 1000x / 10000x / 1000000x, probably more like 1000x on dollar cost if you compare token costs at metered rates vs. the Bing engineer who wrote the compilers at JITs that the search index used), the number of these kinds of optimizations it makes sense to do goes way up. The same goes for optimizations that you aren’t sure will work out. I used to sometimes look at an optimization that I wasn’t sure would speed things up and think “this will take M hours to implement to the point where we have a good enough measurement to guess at the performance impact”. Many more of those optimizations make sense to try out now.
Going back to the game AI case, at least for the AI I tried, it seems like you gain about 100 Elo for every doubling in speed (more than in chess, I suspect because draws are very rare). Just adding multithreading alone is enough to wipe the floor with an otherwise comparable AI on a large machine. If you stack in 10 – 20 more optimizations that seem too annoying for most people to do by hand, the difference in strength is tremendous and it’s not really reasonable to try to keep up with a hand-written AI3.
The game AI case is a little more annoying than for most software because a lot of the optimizations you want to do actually change the result and there isn’t a cheap, trivial, way to tell if the speed increase + the change in result gives a better or worse actual result in practice. And, as we noted before, current publicly available SOTA models are pretty bad at experimental design, so I had to set up the framework they used to determine if an optimization is good, but once that was in place, it’s like any other optimization problem. I guess people working on LLM optimizations also have to deal with this class of problem but most optimization problems are a lot more straightforward.
To pick another example, as part of preparing for performance interviews, Jamie Brandon tried Anthropic’s now public performance takehome. After trying it, he had Claude pick up where he left off and it got a much better result. When he looked at what Claude did that he didn’t, he said a lot of the optimizations were things that occurred to him but he hadn’t gotten to yet, and “[o]thers were just crazy shit that I would never try unless I was working on this for weeks”4. He’s a reasonable performance engineer and he got an offer for the performance job he wanted, but on a well-defined optimization problem, he doesn’t stand a chance against a decent model (I haven’t tried the problem myself, but I suspect I also wouldn’t stand a chance given remotely comparable time controls).
Workload-specific optimization
Coming back to this part of Marc Brooker’s comment:
Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome.
Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome.
This seems pretty inevitable. In another response to my post, Michael Malis of pgrust said something similar:
[discussion of pgrust optimizations] … I think it’s easy enough to create these optimizations that we could look at a customers workload and add them as needed
[discussion of pgrust optimizations] … I think it’s easy enough to create these optimizations that we could look at a customers workload and add them as needed
Without having any kind of framework or setup, right before I started writing this post, I had an agent do workload-specific optimization for my ripgrep queries (not the native code compiler switch, just the optimizations to the general FRE engine based on a set of benchmarks), which took about 2 minutes for me to launch. The optimizations run on a set of queries, and then there’s a later holdout set of queries to run against. That’s still running, but the initial results seem promising. After one pass of optimization, the workload optimized version is 2% faster than standard ripgrep on the holdout and it’s still getting faster. 2% isn’t a big deal for my local ripgrep usage, but considering that this took minutes of time and the optimizations done here got started when I started typing this point and are still improving, I’d take a 2% win here (note that this isn’t combined with the native code compiler, which would give a larger overall win if combined properly). And recall that this is leveraging the FRE regex engine5, which was substantially slower than the Rust regex engine on holdout benchmarks and was stuck with slow improvement on holdouts because with me knowing nothing about regex workloads and SOTA LLMs not being good enough at experimental design to do unguided open-ended self-improving loops, we didn’t have a good way to improve performance on our holdouts. But if what I care about is performance on my own workloads, I have plenty of data and am generating more all the time. As Marc Brooker noted above, we do have to be careful about overfitting if there’s a regime change that’s not in the old data, etc., but we’re still in a better situation than we were before.
In the more general case, if you’re someone like Marc Brooker at Amazon or Michael Malis working on pgrust, it makes sense to not just do this as a one-off, but to work with customers to pilot a program that uses their data to optimize things for them and then figure out how to scale it out for customers in general. I’m not working at a company where that’s the best use of my time6, but it’s pretty wild that you can see that this is coming for larger companies with more scale, and given that it only takes minutes of my time to run these experiments for my personal workflows, it’s pretty reasonable to mess with this kind of thing on personal projects.
Thanks to Jamie Brandon, Michael Malis, and Max Bittker for comments/corrections/discussion.
P.S. As I’ve noted in the last couple posts, with coding agents, the time it takes to run an experiment and see enough of a result to satisfy my curiosity has gone way done while the time it takes to make a result really rigorous hasn’t changed or has gone up, so writing things up the way I used to would mean running very few experiments relative to the bandwidth I have for them. As a result, I’ve just been running these experiments and sharing the result with a couple of friends. As an experiment, I’m trying to write these up in a very quick and non-rigorous way instead of years of these experiments only being known to a few friends. Like the last post, I set a goal of writing this post and doing all the clean-up in half an hour and didn’t time it but am pretty sure I missed that by a bit.
Even doing this, the time it takes to write these up is long enough that I’m falling behind on sharing recent results, but I’m not inclined to switch to LLM-written posts (yet?), and I don’t think I can realistically get the time to clean up the data and write a post like this down enough to turn a post around in less than half an hour. Just on the length of this post, typing this up should be something like 20 – 30 minutes including time to pause and think about what I’m writing, and then when I look at the data sometimes something will look wrong enough that I need to look into it more closely to see if there’s an issue that needs to be fixed (this happened multiple times here, and I would expect that, because I didn’t spend much more time, there are other data issues that I don’t know about).
Anyway, if you have opinions on these quick (and surely more wrong) writeup, let me know what you think (X Bsky Mastodon)!
Appendix: There’s no reason for software to be slow anymore
I’ve been on the record for a long time as strongly disagreeing with the general sentiment that the developers of X are bad and should feel bad for writing slow code because there are a lot of different kinds of programming expertise and not only is it not the case that most programmers don’t have performance expertise, it probably doesn’t even make sense for them to development (from the standpoint of what the business cares about, what the employment market looks like, etc.), so of course most projects will have very poor performance compared to what a performance expert can do.
For the example above, Jamie Brandon got an offer from Anthropic and you probably can’t afford him or someone like him unless you’re OpenAI, but you can afford to use a coding agent that can beat him on a bounded optimization problem. The agent doesn’t have the judgement he has and will do worse on an open-ended problem (recall that when we tried building an optimized regex engine and just told it to not overfit, it was more than an order of magnitude worse than the best regex engines on our holdout benchmarks, but also recall that after telling the agent there was a holdout it was doing poorly on, it sped up regex engine performance enough to generally match 2nd tier regex engines in terms of performance, which is still extremely good compared to the general level of performance optimization in most code today), but that’s plenty good to achieve reasonable performance on all sorts of problems. This post has generally discussed backend performance issues, but agents don’t seem worse at front-end performance if you want to drive down a set of metrics like LCP, INP, etc.
Appendix: How is codex running ripgrep?
Here’s some information about the distribution of riprep queries on my machine. I make no claims that this is at all representative of what’s happening anywhere else. The pattern distribution of the length of the pattern that’s searched has a lot more long patterns that I would’ve expected. The p50 is 55 unicode code points (I’ll just call these characters for simplicity), which is already longer than things I grep for by hand, and the p90 is 119!
We can also look at the number of alternation arms in regexes, which are once again much more complex than what I do by hand.
Another view is to look at how these are correlated. Do we get more alternation arms in the regexes as the regexes get longer? Yes.
What are these really long regexes, anyway? If we look at them, most of the longest are long alternations over function or tests names, such as the following regex, which appears to be related to FRE development.
fn (hot_byte_compiler_is_generic_only_and_anonymous_count_uses_auto_count| one_pattern_count_spans_uses_the_retained_complete_span_session| formal_compact_state_byte_visitors_coexist_with_native_count| fixed_boundary_record_visit_matches_line_relative_reference_and_is_atomic| unbounded_languages_refuse_finite_extraction_before_allocation| formal_single_raw_span_sweep_preflight| assert_exact_fixture_uses_formal_large_continuation_sweep| url_only_compile_identity_binds_language_and_owner_mode| url_only_compile_exact_limits_and_runtime_refusals_close| url_only_compile_post_plan_allocation_faults_close| url_only_owner_discriminator_is_stable_and_precharged| url_only_compile_owner_is_strategy_and_operation_scoped| formal_rebar_url_owner_is_compile_only_and_matches_oracle| formal_rebar_url_exact_fixture_uses_certified_execution| formal_fixed_schema_materialization_matches_both_record_oracles_and_controls| formal_single_count_selects_compact_state_byte_complete_bound_visitors| authenticated_bound_line_total_lf_free_domain_opportunity_exceeds_five_percent| prepared_absolute_onepass_fuses_slots_and_preserves_pre_source_fallback| authenticated_word_boundary_russian_compact_lowering_public_canary| ordered_nfa_x86_epsilon_edges_bypass_the_assertion_call| ordered_nfa_aarch64_epsilon_edges_bypass_the_assertion_call| ordered_edge_dispatch_v2_is_target_neutral_deterministic_and_relocation_free| ordered_edge_dispatch_v2_copies_canonical_tables_and_cap_falls_back_to_v1| ordered_nfa_v3_composes_terminal_range_and_dispatch_without_data_relocations| ordered_nfa_x86_terminal_range_emits_authenticated_reverse_scan| ordered_nfa_aarch64_terminal_range_emits_authenticated_reverse_scan| ordered_nfa_x86_boundary_assertion_cache_is_lazy_and_boundary_scoped| ordered_nfa_aarch64_caches_repeated_assertions_once_per_boundary| boundary_assertion_cache_requires_dense_exact_kind_reuse| boundary_assertion_cache_selection_is_compiler_only_and_deterministic)
But some are funny numerical constructions, such as
:(13[0 – 9]|14[0 – 9]|15[0 – 9]|16[0 – 9]|17[0 – 9]|18[0 – 9]|19[0 – 9]|20[0 – 9]|21[0 – 9]|22[0 – 9]|23[0 – 9]|24[0 – 9]|25[0 – 9]|26[0 – 9]|27[0 – 9]|28[0 – 9]|29[0 – 9]|30[0 – 9]|31[0 – 9]|32[0 – 9]|33[0 – 9]|34[0 – 9]|35[0 – 9]|36[0 – 9]|37[0 – 9]|38[0 – 9]|39[0 – 9]|40[0 – 9]|41[0 – 9]|42[0 – 9]|43[0 – 9]|44[0 – 9]|45[0 – 9]|46[0 – 9]|47[0 – 9]|48[0 – 9]|49[0 – 9]|50[0 – 9]|51[0 – 9]|52[0 – 9]|53[0 – 9]|54[0 – 9]|55[0 – 9]|56[0 – 9]|57[0 – 9]|58[0 – 9]|59[0 – 9]|60[0 – 9]|61[0 – 9]|62[0 – 9]|63[0 – 9]|64[0 – 9]|65[0 – 9]|66[0 – 9]|67[0 – 9]|68[0 – 9]|69[0 – 9]|70[0 – 9]|71[0 – 9]|72[0 – 9]|73[0 – 9]|74[0 – 9]|75[0 – 9]|76[0 – 9]|77[0 – 9]|78[0 – 9]|79[0 – 9]|80[0 – 9]|81[0 – 9]|82[0 – 9]|83[0 – 9]|84[0 – 9]|85[0 – 9]|86[0 – 9]|87[0 – 9]|88[0 – 9]|89[0 – 9]|90[0 – 9]|91[0 – 9]|92[0 – 9]|93[0 – 9]|94[0 – 9]|95[0 – 9]|96[0 – 9]|97[0 – 9]|98[0 – 9]|99[0 – 9])[0 – 9]:
This is equivalent to :(?:1[3 – 9]|[2 – 9][0 – 9])[0 – 9]{2}: (which, if run through ripgrep on the original input, has approximately the same performance). The entire pipeline for that was
cargo clippy … | rg ‘crates/fre-aot-regex/src/module.rs:’ | rg NUMBER_REGEX | head -250
which might be an odd thing for a human to do, but agents seem to do this kind of thing all the time.
On another topic, if we look at how long ripgrep queries took, there are quite a few slow queries, e.g., p99 is almost 1 minute! And p999 is almost 10 minutes! And the maximum query over this time period (around a month on one laptop; queries and distributions seem likely to be different on the AWS hosts I run agents on, etc., but I haven’t checked) is approaching 2 hours!
In terms of command line options, we see the following. Perhaps unsurprisingly, codex often wants line numbers and, for whatever reason, it very occasionally uses PCRE2 regexes.
I won’t add plots or tables for these, but another thing to note is that there’s fairly low locality for what patterns are searched for (about 94% of patterns only occurred once), which makes some sense given how long a lot of the queries were. However, there’s fairly high locality in what files get searched and a file that got searched is relatively likely to get searched again soon, indicating that (for small enough files), they’re likely to be searched in memory.
Also, 99% of queries were regex queries (1% were non-regex string searches) and 99.9% of search queries were ASCII only, but in terms of files searched, approximately 45% were ASCII only and 55% contained Unicode, a higher percentage than I would’ve guessed for Unicode.
On a draft of the last post, Peter Geoghegan noted
It’s also possible for a regex implementation to be faster by supporting fewer features. Some implementations don’t support back references, etc.
It’s also possible for a regex implementation to be faster by supporting fewer features. Some implementations don’t support back references, etc.
which is also true here. The workload-specific optimizations done here were fairly superficial because I just gave codex some short instructions and let it do whatever it wanted (which is, in general, not the most effective use of codex), but with a more detailed plan, more focused optimizations supporting the common use cases for my queries could be expected to yield larger gains.
though, as we discussed in that post as well as before, the benchmarking and experimental design skills of SOTA models aren’t good enough to do this in the general case without a human (or a skill) setting up the benchmarking environment for the agent. [return]
we can see from our old benchmarks that, even with time to run the compiler, there are a lot of cases where the native code compiled version is slower than the Rust regex crate. If we look at why this is, these tend to be more complex queries where the Rust regex crate has some algorithmic optimization and the FRE native code compiler is falling back to something naive (the agent that created FRE spent much less time on the native code compiler than it did on the “normal” regex engine). [return]
I have no doubt that a hand-written AI by someone who has real AI expertise, e.g., by someone who’s written one of the top Go and chess engines in the world, could beat my AI on the strength of the “AI” side of things being better than what you get when someone who knows nothing about AI (me) creates an AI, but if the levels of expertise are remotely similar, the LLM-written version is going to dominate for any given amount of time spent. [return]
it’s arguably unfair to compare the result of an agent picking up where he left off, since his work is a starting point which might let an agent do much better than it would do on its own, so I tried giving the fresh task to an agent and it got a very similar score to what he got when an agent re-used his work (and a quick check by another agent didn’t find evidence of cheating). [return]
The performance probably would’ve been better if I had an agent just modify a ripgrep fork directly, but I was curious if this could also solve the FRE overfitting problem with respect to my queries. [return]
a while back, I reduced the size of page in our signup flow from 50 MB to 5 MB and a revenue A/B test seemed to indicate that this increased revenue by about 0.5%. In general, I’m a huge fan of doing the simple and easy wins first, such as this, and there are probably a lot of higher ROI wins than we’d get out of building custom compilers or doing other highly specialized technical work here. [return]
I want to present a project that I’ve been working on for the past 4 months: an alternative Rust LSP implementation that is built with a focus on low memory usage.
It has two main features:
It can use very little memory (target <100mb for reasonable projects). There are caveats, these are described below.
It allows immediate indexing after restart: if your project was indexed, restarting the editor will not require re-indexing.
Note: throughout this video, the used RAM remained under 100mb
These features make Rust Glancer suitable for the older computers: I have tested it on my old MacBook Pro M1 2020 with 8GB RAM, and it was pretty good.
As you can imagine, 4 months is not a lot of time for a project as big as a Rust LSP. Rust Glancer is not a complete LSP yet, it has a lot of missing functionality, it has some known bugs, and it has a lot of things I want to improve.
At the same time, it is already pretty capable: it has a full indexing pipeline with type inference and a trait solver (chalk), most of the “normal” Rust syntax is supported, and most of the “normal” LSP actions do work as well: goto definition, hover, inlay hints, completions, you name it.
If you are interested, you can already try it out: just install the VS Code extension here, or, if you prefer, build and install the vsix from the repository.
The rest of the post contains the history of the project: motivation, LLM use, plans and roadmap. If you’re not interested, you might want to check out the project documentation instead.
Difference with rust-analyzer
There are several reasons why rust-analyzer consumes a lot of memory:
Rust workspaces genuinely have a lot of information that must be indexed: thousands of functions, structures, traits, relationships between these, function bodies and statements in them, etc. Each of these needs to be analyzed and remembered, and you can’t really cheat if you want to have things like “find all references to this structure”.
rust-analyzer uses salsa as its database. It’s an incremental query-based database, which lazily computes all the data you need without having to explicitly “record” everything. It is a very cool approach, but it’s inherently tied to memory, which makes it hard to move parts of data from memory elsewhere.
rust-analyzer uses rowan for syntax tree representation. The cool property here is that it allows partial invalidation: if only a part of the file changed, only the relevant bits have to be reparsed, which makes it faster than having to re-parse the whole file on each keystroke. However, the tree-like representation inside of it can cause heavy memory fragmentation (meaning that the amount of RAM taken from the OS is higher than the amount of “actually used” RAM).
(1) is something we have to live with (though there are a few optimizations we can do there which Rust Glancer does), but (2) and (3) are the consequences of the rust-analyzer architecture. rust-analyzer chose them to make the LSP faster, and it does work for that purpose.
The idea I had when I started the project: what if we don’t try to make an incremental LSP? What if all we have is a frozen analysis result that gets invalidated on save? It obviously will not be as fast as rust-analyzer, but it will give us the properties we seek:
analysis results can be offloaded to the filesystem and loaded to memory only when they are actually needed.
saved analysis is reusable, and since it’s already offloaded to the filesystem, it can be reused after the editor restart.
This is the core idea of Rust Glancer.
It indexes the workspace once and preserves results in the filesystem, and then whenever queries need something, they can load the required information for the duration of the query.
It doesn’t come for free though: frozen workspace analysis is slower than lazy incremental by definition, since loading and deserializing data from filesystem is slower than loading from memory. To mitigate that, Rust Glancer has to use some tricks: for example, when you type, it doesn’t perform full blown analysis on each keystroke, it instead attempts shallow analysis of the current body and reuses the previous complete index. This makes completions reasonably fast, but it also means that new items (imports, structures, traits) are not “indexed” until you save the document. Which, hopefully, should not be a problem: you really get used to it fast, and at least in my case it does not feel overly wrong after a while. If that sounds scary, I suggest to just try it, it really is not.
For people who rely on agentic workflows, Rust Glancer is also optimized for large amount of out-of-editor changes. I’m not sure why, but in rust-analyzer I’ve observed that when agents edit the code, inlay hints can get out of place, and I had the same problem in Rust Glancer initially, but it was resolved by implementing a custom file watcher and tweaking it somewhat. The server also has lower priority for out-of-editor changes, so agentic changes do not cause rapid re-indexing.
Still, it’s important to understand that Rust Glancer has some benefits, but also has some drawbacks (besides being incomplete, obviously) compared to rust-analyzer. Maybe I will manage to solve some of them eventually, but it’s highly unlikely that Rust Glancer will ever become “just like rust-analyzer, but better”. I imagine that rust-analyzer will remain the default choice for projects that care about completeness and keystroke accuracy, while Rust Glancer will work for people with weaker machines or people who are ready for some sacrifices to reduce RAM usage.
How and why it happened
I have been writing Rust professionally for ~7 years, and since pretty early on I started observing how the compiler and its tooling are developed. I’ve made some contributions to rustc, clippy, and rust-analyzer, and I’ve spent dozens of hours reading its source code just to teach myself. So I was pretty much aware how big of a project a Rust LSP is.
At the same time, I have a love-hate relationship with rust-analyzer. It is absolutely beautiful except for two things: memory usage and initial indexing (especially with build scripts / proc macros enabled). These problems seem to be brought up quite a lot, but in my case they are even more drastic: I have a rather stupid workflow where I have two identical IDEs open on two displays with a bunch of projects inside a workspace. So the memory consumption is roughly 2N, and with my last set of the projects I had to work on, rust analyzer was consuming 16GB of memory that I, ugh, would prefer to have available for other uses; not to mention that each time I opened VS Code, my PC fans would go brr because of a ton of parallel indexing jobs.
At some point I thought that I am fairly confident in my Rust knowledge, so I probably don’t need a full-blown LSP, and can use something simpler and more memory efficient. I decided to try building a “smart ctags for Rust”. I very explicitly did not want to build an alternative LSP, because of how insane of a task it is. Little did I know…
The initial progress was going pretty smoothly: I made use of rust-analyzer’s syntax library, lowered items to internal representations, then built definition maps and module structure, got all the declarations indexed. It was so surprisingly straightforward that I decided to do some primitive body lowering. Then I decided to add very very simple type propagation. Then it turned out that naive type propagation doesn’t give me much — but I already had these nice inlay hints, so I wanted more. Overall, I don’t care about complex cases and nightly features, right? (Right?…). So then came naive trait resolving via impl header matching. It’s quite addictive, you get it.
The illusion, however, broke when I decided that it is pretty reasonable to expect the following code to be supported as well:
fn mul_by_two(vals: &[u8]) -> Vec<u8> { vals.iter().copied().map(|v| v * 2).collect() }
The code is pretty simple, but in order to support it we need:
Slice type support
Closures / Fn traits
Trait solving
Associated type projection
A bunch of nightly stuff
the last item is funny: I wanted to avoid nightly, but I somehow didn’t think that std (or sysroot in general) breathes nightly. Welp.
So all in all, one feature after another, I slowly was getting from “smart ctags” to a “real LSP”. Probably, the three biggest milestones were:
Declarative macro expansion (I hate declarative macros now). Thankfully, I was able to reuse most of rust-analyzer’s infrastructure for that.
Proper type inference engine. It was a big “oh wow” moment when I truly realized how type inference works (in short: we “link” all related type bindings in a big inference table, and then we try to get evidence from all possible places, where providing evidence can solve types for multiple places). It was the moment that probably brought me the most joy during the work on this project so far.
Proper trait solving engine. I initially wrote “it’s highly unlikely that we will have a trait solver in this project”, but then I really wanted to get the abovementioned iterator example to work properly. I resisted integrating trait solver for a while, trying to have naive hacks like naive trait impl matching + specialized handlers for std traits, but it was getting more and more complex while working pretty poorly. Then I gave up and integrated Chalk, which turned out to be significantly simpler than the whole hierarchy I have built. Making Chalk fast was another challenge, though.
Somewhat separately, probably the thing I am most proud of (and the thing that made Rust Glancer possible — had I not designed it early, the project would die very quickly) is a cool profiling stack that can measure performance, memory usage (both natively, tracking actual allocated objects, and with jemalloc), profile data on demand, and compare LSP against rust-analyzer, as well as a set of benchmarks running in CI. If you’re interested, it’s partially covered in the docs (1, 2), but I’ll work on a more detailed coverage later.
Probably ~1.5 months ago I started using Rust Glancer as my daily driver instead of rust-analyzer. Now, I am happy with its state enough to present it to a larger audience.
LLM use
This project was built with heavy use of LLMs. It is not vibe coded, though. I am verifying each pull request to make sure that I am happy with the state of the codebase. If you need proofs, you can check the git history: it has PRs with 10k+ lines of diff, but these are multiple days apart despite the fact that I work on this project nearly every day since its inception. I care about the code, and tbh it would be weird for me to spend 4 months creating a Rust LSP if looking at the code wasn’t something I do a lot.
I am not going to pretend that I am an experienced LSP developer and the code is perfect. It is in a state that I can work with, but I understand that some bits might not be idiomatic in terms of compiler tooling design. The code has a lot of comments, and I tried really hard to make sure that these comments are not sloppy but helpful, because I have to read them all the time; so far the quality is obviously not as good as professionally written human docs, but IMHO it’s pretty helpful and not annoying to read.
A large part of the journey is learning. LLMs can be pretty good domain experts, and LLMs know about LSP design much more than I do. At the same time, LLMs are not great at building big projects. So the following loop happened multiple times during development:
I build something new.
LLM proposals seem reasonable, so I go with them.
It works but something bugs me.
I think about the design for a while and see a big flaw.
I work with LLM to fix it (sometimes for a week, if the screw up was particularly big — but the bigger the screw-up is, the more I learn).
So on one hand, if I am to attribute code ownership to the LLMs, I can complain: “LLMs tried to derail the project so many times!11”. But since it’s my code, I think that the code might get worse at some moments, but as I learn, I get to improve it. Which is pretty normal software development flow, just accelerated.
All in all, LLMs are just a tool, and it’s one’s choice to use it responsibly or outsource thinking to it. Given the amount of witch hunting today, I have just one request: do not reduce me to a clanker. It is my code, so if you consider it to be slop, call it my slop, not AI.
I am open to criticism and will happily listen to feedback: the more I learn, the more I can improve the codebase. Whether I use LLMs for that or not does not matter that much, in my opinion.
What’s next
The project is already in a state where it can be a daily driver for some users, but I have rather big plans for it. So in the coming releases, you might expect:
Further performance optimizations
Some more memory optimizations (primarily during indexing, plus there are a few fragmentation issues happening after a full indexing run that I want to fix)
Improved type inference / syntax support.
Code actions (implement missing trait fields, auto-imports, etc).
Potentially proc macro support (I have some weird idea that will not require actual code execution, but it’ll take a while to prepare).
Some features are unlikely to be supported though, such as build scripts / proc macros support via proc macro invocation (e.g. anything that requires untrusted code execution). I also don’t plan to work on things that are unnecessary at the current state of the project, such as migrating to the new trait solver. Niche things like particular nightly features will likely be postponed until the project reaches some degree of maturity with stable Rust.
Additionally, there is a lot of cool little tricks I’ve done in Rust Glancer that I’m somewhat proud of (aligning allocation lifetimes to reduce memory fragmentation, engine-as-a-subprocess model to help with both memory fragmentation and multi-workspace projects, sharded cache, and others), so if people will be interested, I’ll be happy to write some blogs telling about how Rust Glancer works under the hood. It’s partially covered in the docs already (1, 2) if you want to get some info right now.
But in any case, I hope that the project can be helpful for some folks already, and for more folks in the future.
My father passed recently, and he was twice my age. I am approximately the same age that he was when I was born, and I am now “the old generation” - there’s no one left in the generation above me.
At the same time, I recently joined a company that skews younger-than-me. When I joined Google in 2011, I had just turned 30, and was in the mainstream demographics of Google in 2011. There were a bunch of more senior folks, with the very senior ones being in their 50s and having completed stints at Bell Labs. I admired a lot of these “greybeards” (even though this is a sexist term - what’s the right female equivalent? There were a few very senior female engineers that I would love to include).
So perhaps it is natural that I am reflecting on “what were the important realizations that I made since my early 20s that had a profound impact on the way I think about the world”? In some sense: What are the insights I had that made me “more mature”, for some positive definition of “mature”?
This post tries to list them.
1. The importance of understanding your own incentive structure, and not believing everything you think.
I recently wrote a Twitter thread about the topic. Oppenheimer was very publicly guilt-ridden about the creation of the nuclear bomb, and von Neumann at some point quipped “some people profess guilt to claim credit for sin”. In my young years, particularly in situations when I had 0day that nobody else had, I agonized about the responsibility that comes with having 0day. Should I fix them? Should I use them for good? Will the world be harmed this way? Or that way?
In the end, it turns out that - while individuals matter - many ideas have a “time at which they are ripe”, and the actions of the individual matter less than the individual thinks in that moment. There is also almost no way to predict the ways in which what you do impacts the broader world.
If you were asked: “Would it be good if this 0day was used to apprehend a terrorist?” you would probably say “this is good”. If you were asked “would it be good if this 0day is used to arrest someone and then torture and waterboard him 183 times?”, you would probably say “this is bad”. So if your 0day was used to capture KSM, it is probably good? Or bad? Things get very complicated very quickly.
Is closing 0days good for society, because it makes everything safer? Or is it enabling oppression, because buggy systems are easier to bypass?
There are no good answers, and your own incentive structure will greatly influence how you choose your beliefs. In the end, people want to be the heroes of their own story, and at the same time they have basal needs for recognition, for material goods, etc. - so they will try to construct a narrative that allows them to satisfy their basal needs while also remaining the hero of their saga.
Anxiety about the impact of your work is self-flattering, and you have to recognize it as such, and keep it in check - it’s sugar for your ego, but history will largely route around you, because while individual decisions matter in specific situations, the overall flow of history is less sensitive to the individual than the individual thinks. The broader lesson, though, is: Do not believe everything you think. Examine your own incentive structures carefully. Ask yourself what alternative narratives for your behavior and beliefs could be, especially if they contradict the narrative of the heroic saga you’re constructing for yourself. Carefully weighing the question “how might I be the villain in this story?” is an important and valuable skill.
Similarly, meta-cognition - just observing your own thoughts in a detached manner, and then being able to interpret, analyze, and contextualize them with regards to your own incentive structures, is a great skill to cultivate.
2. Monocausal determinism is an illusion, and largely does not exist outside of computer debugging.
The monocausal determinism that young computer enthusiasts get used to is an illusion that generations of electrical and process engineers spent their lives perfecting and maintaining. It is because of these engineers that computer scientists could largely get away without probabilities or any empirical grounding in the past. There is an argument that you have so many natural scientists that crossed over into AI because CS education was for a long time too focused on reasoning within the deterministic monocausal illusion.
The reality is: Computing machines are physical devices, which includes wear & tear, differences in quality between items, and “probabilistically deterministic behavior”, e.g. it’ll appear deterministic most of the time if not shaken too much. If pushed a bit - be it temperature, voltage, electromagnetic fields, or even rapid memory accesses to adjacent DRAM rows - determinism has a tendency to go out of the window, the illusion collapses, and we’re dealing with a very different beast.
FWIW - this also makes me wonder about model alignment, because even a perfectly aligned model will be subject to random bit flips in inference, and it’s hard for me to imagine that you can maintain any reasonable guarantees in the presence of bit flips to inopportune values at inopportune times.
The real world is one where very few things that happen have a single reason, and very few truly deterministic transmission mechanisms. Everything is probabilistic, and everything is multicausal.
Measurement noise is real, experiment design is difficult.
Interestingly, if you think about this carefully, you also realize that the scientific method is a classifier that is intentionally biased against accepting something as true - so that we only accept things as true that are beyond any reasonable doubt true.
A somewhat fascinating corolary of this is that there exists a large class of true things that will never be scientifically shown as true.
3. The dichotomy between reason and emotion is a cultural construct, and neither grounded in neuroscience nor in logic.
With some digging, it turns out that the western belief that reason and emotion are two ends of a spectrum is a purely cultural construct, as is the belief that “higher-order” reason needs to reign in “basal” emotions, or that “emotions” intrude on “rationality”.
In most non-western cultures, achieving integration between rational deliberation and impulses and emotions is more common, and it turns out that this is much closer to the biological reality.
From a neuroscience perspective, it is clear that emotional valuation is part of a larger decision-making machinery that tends to not function properly if the emotional valuation component is damaged or removed. There is also a large component where things that your brain struggles to articulate verbally are transmitted via emotions, as well as actual feedback from your sensory organs in your body. Fun trivia: Your gut’s enteric nervous system contains as many neurons as the entire cerebral cortex of a dog. Your body also forward-deploys neurons in your muscles and extremities, as a form of latency optimization. Your body is feeding you extra information, and most of this shows up in the shape of emotions.
Which brings us to the logical argument why attempting to “remove” emotions from decision-making is a bad idea: Clearly, having the ability of leveraging more information for decision-making will improve the quality of decisions. Attempting to eliminate a particular source of information almost certainly makes the quality of your decisions worse.
This is not to say one should act on impulse alone, but it is certain that integrating the full spectrum of information - which includes emotions - in your decisions is a wise idea.
I am sure that if I think more carefully, I will come up with more insights, but these three are important enough that they show up in my life with astonishing regularity.
Hope this is helpful to someone.
It’s time to dream big. Omarchy Quattro has given people a chance to experience what the malleable computer of the future looks like, and they like it (a lot!). It now feels like a moral obligation to make this future more broadly available and fundamentally change how people relate to their computers for the first time in what seems like forever.
To do just that, I’m incorporating the Omacom Foundation to ensure that this mission is fully funded, durable, and ready to accelerate.
This nonprofit foundation will hold the trademarks, fund the infrastructure, promote the work, and support the open-source projects and developers Omarchy depends on.
These eight Founding Patrons are each contributing $1 million to this mission:
Tobi Lütke, CEO of Shopify
Patrick Collison, CEO of Stripe
Michael Dell, Chairman and CEO of Dell Technologies
Jack Dorsey, Block Head and Chairman of Block
Matthew Prince, CEO of Cloudflare
Brendan Iribe, Cofounder of Sesame and Oculus
Jason Fried, CEO of 37signals
Yours truly
This is a ridiculous sum of money, so I intend to make sure it lasts a long time, and that we make the most of it. But just as important as the incredible cushion is the vote of confidence delivered by these pledges.
We’re going to make the prophecy of The Year of Linux on the Desktop come true. All the pieces are now in place. Time to go all in!
For years now one of the most reliable complaints I hear when I try to drag a team off their vendor specific SDK and onto OpenTelemetry is some variation of: “why does it seem like this isn’t done yet?”
Vendor SDKs for observability are, to put it charitably, idiot-proof. You install the thing, dashboards just load data, someone else worries about how all those pieces fit together, and you get on with your life. OpenTelemetry, by contrast, greets you at the door with a lot of “experimental” stamps and roughly six different ways to accomplish any given task.
In OpenTelemetry’s defense this was never what they were going for as a project. I’ve always respect that they stuck to their guns by attempting to build a truly vendor agnostic system that really doesn’t care what you do with the data. I have never gotten a sense of a vendor being strongly preferred with OTel, which is quite the feat considering how lucrative and contentious the observability ecosystem was. Also considering that the maintainers of this project are largely employed by exclusively those companies.
As the years wore on, I started to get nervous. Conversations in the semantic-conventions repo drag on and on and on. Different languages had dramatically different stories. Golang and Dotnet were first class citizens, but other languages lagged years behind the others.
I started asking a lot of probing questions before recommending OpenTelemetry to smaller teams who didn’t have the time, budget, or emotional bandwidth for it. Auto-instrumentation was genuinely magical, but the cliff between “auto-instrument works” and “now I have to manually instrument something” was steep enough that you owed people a warning before you pushed them off it.
This narrative has been going on for awhile in the observability space, a vague sense of “something is wrong in Otel-land”. But let’s try to generate some actual data here. Is there an actual problem, or is this something where the perception by the community of slow progress is imaginary? Is the problem not enough maintainers, too big of a scope, or something in-between?
My guess when I started was “oh this is your classic open-source bit off more than they can chew”. Not enough maintainers, not enough budget. Now there is some of that, but there’s also something else going on.
The actual problem happening inside of OpenTelemetry is a three way crash. You have a binary stability gate which, when combined with a very small bench of actual maintainers means there is understandable worry about marking a feature not experimental then add on just a massive scope of languages and frameworks they are attempting to cover. This creates a perfect storm where there is an incentive to argue about potential problems a feature might create since once it is locked in and shipped as stable you can never change them.
How does OpenTelemetry Work
So OpenTelemetry currently is attempting to support a dizzying number of languages and frameworks.
OpenTelemetry is a giant project. It spans dozens of languages, hundreds of libraries, and countless backends. To keep things sane, the project splits work into two buckets:
Core → Maintained directly by the OTel project. Small, stable, vendor-neutral, and tightly reviewed. This is the “spec-defining” surface.
Contrib → Community- and vendor-contributed. Broader, faster-moving, and covers the long tail of integrations.
There exists the otel-collector, the thing that runs along the thing so that you can ship logs metrics and traces. That copies the same rough pattern. But for the languages when we’re talking about core vs contrib this is what we’re talking about.
Stuff that breaks goes in contrib, stuff that doesn’t break goes into core.
Now the reason this causes a conflict. contrib is massive overkill for most projects. You don’t want 300 exporters to add the one you typically need. On the language side, this isn’t that big of a problem. pip install opentelemetry-instrumentation-flask gives you the stuff you need for flask. However on the collector side you end up having to do the OpenTelemetry Collector Builder to make your own collector (or just kinda ride the wave and hope it works out). While cool that this exists, it’s a lot of scope to ask a team to take on.
Process of adding a new feature
So I believe I have captured the workflow of adding a new feature to OTel. You can check my homework here:
OpenTelemetry Enhancement Proposal (OTEP) (https://github.com/open-telemetry/opentelemetry-specification/tree/main/oteps/)
Once the OTEP is accepted, the text goes into the Specification directory in the same repo.
After that it seems to go to Semantic conventions. This seems to be where we get down to the specific details and where most of the long discussions seem to live. At this point we’re talking about more or less a permanent commitment to this design and where the lock-in process becomes very hard to change.
Each of the SDKs implements the API surface that is defined in the specification. Now some of the SDKs have done 2.0 breaking changes, so it does seem like the earlier “please no 2.0 at all costs” sentiment has been abandoned (which I think is smart and good).
Contrib / instrumentation. This is slightly more mushy. Looks like they should track latest API/SDK but each contrib package may version independently so its more flexible as a design.
Collector + OTLP. The data has to actually go somewhere. OTLP (wire protocol) has its own stability lifecycle and specification (here). Collector components have their own stability in their READMEs and as far as I can tell that’s kinda all over the place.
Things I’m not really clear on
It’s unclear how long the OTEP -> Specification process takes. I’ve looked through the Git history but there doesn’t seem to be any predictable number or cycle.
I don’t fully understand what is the relationship between all these stability commitments. Does Collector + OTLP group work in lockstep? Can a language “fall out of scope” if you lag too far behind?
Attempting to test it
So because OpenTelemetry is a CNCF project, I figured it made the most sense to compare them to other CNCF projects. My basis for comparison is Envoy and Prometheus. I have used a hacky Python script I’ve used before for measuring the “health” of open-source projects, which is probably not the best. However I’ll include a link to the raw data without the charts so folks can review it and (more than likely) find a problem in what I generated.
So we look at 24 months of activity for Envoy and what we see is a pretty healthy project. There’s good distribution of authors, mergers, issue closers. phlax is obviously pretty important to the project but in general there’s a good bench of people to step in if needed. I’ve attempted to filter out all the known bot traffic.
Let’s compare that to one of the OpenTelemetry languages. The ones I have the most professional experience with are Golang and Python, but I hear from a lot of folks in the community that the Ruby and PHP ones struggle a lot. This is the PHP one for the same period.
So we see pretty clearly that there’s way too much concentrated on 2 people. This is not a healthy open-source project and they clearly don’t have enough people to cover the kind of scope OTel needs to cover. Same story with Ruby.
In comparison the “strongest” OpenTelemetry SDKs in my opinion, Golang and Dotnet (although Python is also no slouch) look more healthy.
Golang
So the first issue is maybe the least surprising. There’s too much concentration among too few maintainers. Your authors shouldn’t also be your mergers and your issue closers. Ideally these tasks should be distributed out more evenly.
For what its worth I think the maintainers have done a good job of attempting to keep their discussions public. It was very easy for me to find the public meeting notes of the different groups of maintainers, read through them and see what was going on. I don’t get the sense that these maintainers are trying to stop people from getting involved as much as the expectations of stability have, more or less, frozen the project in place.
The issue is more a classic case of “someone has to pay the maintainers”. The project is too complex for someone to realistically do this as a hobby. I think any project signing on for such long stability contracts cannot turn to the community of hobbyists expecting assistance. I can’t join calls and do the things I would be expected to do for a project of this size and importance for free. But it also means that the people doing this critical work have expectations placed on them by their parent organizations.
So these SDKs have too few maintainers. But that doesn’t fully explain why it seems to take so long for new features to get through the stack. My guess for that was that somewhere in the process between submission of the new idea and the formalization of the idea was a long discussion that took a million years.
Conventions about Semantics
So with this level of surface area across different frameworks and languages, it makes sense to concentrate the conversation about conventions in one place. That lives here: https://github.com/open-telemetry/semantic-conventions
If vendor debate is causing the slowdown, we should (in theory) see this slowdown in PRs here. Then you should see the slowdown basically propagate out. Spoiler alert, I was wrong about this. Big thanks to the OpenTelemetry people for having good conventions on labeling their PRs which made this much easier.
So if semconv is the slowdown, let’s look at the slowest PRs there.
Yeah some of them are pretty slow, but there are some complex topics being discussed. However interestingly this slowdown doesn’t really trickle into the SDK/API space, suggesting that OpenTelemetry is going a good job of keeping these conversations siloed off.
If we look at Python we see that their slowest PRs aren’t semconv related.
In reality the slowdown for these are the extra required check imposed by the Approve Public API check which requires another maintainer. But that seems appropriate and takes us back to the initial problem of “not enough maintainers”.
Potential Solutions
So after looking at all of this, the pattern becomes clear. A new feature takes a very long time to make it to the end user in OpenTelemetry because they take stability very seriously, combined with a relatively limited bench of talent to pull from. Once things make it through the entire stack, implementing the API and getting that API change through to the end user falls on an overworked maintainer pool. So what do we do?
I think one idea worth exploring is adding some sort of time-bound beta tier. Basically between the “Experimental” and the “Stable” in the following diagram. The problem is that for end users, due to the extra steps to use Experimental features, they might as well not exist. 99% of us have no idea when an experimental feature is added and we would never engage with it. But if I knew the feature would stick around for at least 12 months without a removal and was more accessible to me as an end user, it could actually help the project get more actionable feedback.
Basically a feature would go Experimental (pretty low usage) -> Beta (more exposed to the end user than Experimental) -> 12 months -> Removal or Stable.
Now confusingly Beta exists for Otel but is used for SDKs, not for components. Like Rust is a Beta but it seems like Profiles cannot be a Beta. Honestly it’s nearly impossible for me to figure out like what labels should apply to what things. I suspect nobody really knows. Here’s the explanation of Beta that I think only applies to SDKs.
Development
Not all pieces of the component are in place yet, and it might not be available for users yet. Bugs and performance issues are expected to be reported. User feedback around the UX of the component is desired, such as for configuration options, component observability, technical implementation details, and planned use-cases for the component. Configuration options might break often depending on how things evolve. The component SHOULD NOT be used in production. The component MAY be removed without prior notice.
Alpha
This is the default level: any components with no explicit maturity level should be assumed to be “Alpha”. The component is ready to be used for limited non-critical production workloads, and the authors of this component welcome user feedback. Bugs and performance problems are encouraged to be reported, but component owners might not work on them immediately. The component’s interface and configuration options might often change without backward compatibility guarantees. Components at this stage might be dropped at any time without notice.
Beta
Same as Alpha, but the interfaces (API, configuration, generated telemetry) are treated as stable whenever possible. While there might be breaking changes between releases, component owners should try to minimize them. A component at this stage is expected to have had exposure to non-critical production workloads already during its Alpha phase, making it suitable for broader usage.
Release Candidate
The component is feature-complete and ready for broader usage. The component is ready to be declared stable, it might just need to be tested in more production environments before that can happen. Bugs and performance problems are expected to be reported, and there’s an expectation that the component owners will work on them. Breaking changes, including configuration options and the component’s output, are only allowed under special circumstances. Whenever possible, users should be given prior notice of the breaking changes. Stable
The component is ready for general availability. Bugs and performance problems should be reported, and there’s an expectation that the component owners will work on them. Breaking changes, including configuration options and the component’s output, are only allowed under special circumstances. Whenever possible, users should be given prior notice of the breaking changes.
Deprecated
Development of this component is halted. No new versions are planned, and the component might be removed from its included distributions. Note that new issues will likely not be worked on except for critical security issues. Components that are included in distributions are expected to exist for at least two minor releases or six months, whichever happens later. They also MUST communicate in which version they will be removed, either in terms of a concrete version number or the date of a release, like: “the first release after 2023 – 08-01”.
Unmaintained
A component identified as unmaintained does not have an active code owner. Such components may have never been assigned a code owner, or a previously active code owner has not responded to requests for feedback within 6 weeks of being contacted. Issues and pull requests for unmaintained components SHOULD be labeled as such. After 6 months of being unmaintained, these components MAY be deprecated. Unmaintained components are actively seeking contributors to become code owners.
In addition it is, respectfully, misleading to imply that Go and Ruby are being maintained at the same standard. This isn’t a shot at the Ruby folks — they are doing heroic work with what they have. But pretending parity exists when it doesn’t just creates confusion and quiet resentment when a user shows up expecting one experience and gets another. Being honest about maintenance tiers would let people make informed choices and might attract more help to the other tiers by naming the problem out loud.
Finally I would try to surface these problems more openly for OpenTelemetry from the perspective of “we need more maintainers”. I feel like the people doing this work probably knew there was a problem, but it seems like the community at large has no idea that there is a need for frankly more engaged ideally independent maintainers and contributors.
OpenTelemetry is a great project that is doing great work. It’s doing, frankly, heroic work at this scale with this few people. But I think in order to actually replace the vendor specific SDKs we need to start getting a bit more pragmatic about what is realistic to do in terms of stability contracts and number of languages. I don’t think breaking changes are as devastating to the community as these promises imply as long as they are communicated well and I think with this thin of a bench of maintainers, something has to give.
Anyway feel free to check my data for accuracy and let me know if you find problems!
To add this web app to your iOS home screen tap the share button and select "Add to the Home Screen".
10HN is also available as an iOS App
If you visit 10HN only rarely, check out the the best articles from the past week.
Visit pancik.com for more.