<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/feed.css?v=208f42b55cff" type="text/css"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:site="https://lalitm.com/feed/ns#"><channel><title>Lalit Maganti (Tag: Algorithms)</title><link>https://lalitm.com/tags/algorithms/</link><description>Recent content tagged Algorithms on Lalit Maganti</description><site:notice>This is a feed.
Feeds let you subscribe to updates from this site using a feed reader. Copy this page's URL from your address bar and paste it into your reader.
New to feeds? Read: https://aboutfeeds.com</site:notice><docs>https://aboutfeeds.com</docs><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 03 Feb 2026 21:07:04 +0000</lastBuildDate><atom:link href="https://lalitm.com/tags/algorithms/index.xml" rel="self" type="application/rss+xml"/><item><title>Rendering 100k trace events faster with exponential search</title><link>https://lalitm.com/post/exponential-search/</link><pubDate>Tue, 03 Feb 2026 21:07:04 +0000</pubDate><guid>https://lalitm.com/post/exponential-search/</guid><description>We’ve recently been looking into optimizing rendering performance of the Perfetto UI on large traces. We discovered that there was some inefficiency in our data fetching logic, especially when you’re very zoomed out.
In this case, there can be a lot of slices (spans) which are so small that they take less than one pixel of width. So for each pixel, we need to figure out “what is the event which we should draw for this pixel”. Over time we’ve come to the conclusion that the best thing to draw is the slice with the largest duration in that pixel.
We can break this into two sub-problems:
What is the range of events which correspond to each pixel? What is the event with the maximum duration for that pixel? We’re going to focus on 1) in this post as that’s where the slowdown was. 2) is fascinating but also surprisingly orthogonal. If you’re interested, I would suggest reading this excellent post from Tristan Hume explaining the basic algorithm we use.</description><content:encoded>&lt;p&gt;We&amp;rsquo;ve recently been looking into optimizing rendering performance of the Perfetto UI on large traces. We discovered that there was some inefficiency in our data fetching logic, especially when you&amp;rsquo;re very zoomed out.&lt;/p&gt;
&lt;p&gt;In this case, there can be a lot of slices (spans) which are so small that they take less than one pixel of width. So for each pixel, we need to figure out &amp;ldquo;what is the event which we should draw for this pixel&amp;rdquo;. Over time we&amp;rsquo;ve come to the conclusion that the best thing to draw is the slice with the largest duration in that pixel.&lt;/p&gt;
&lt;p&gt;We can break this into two sub-problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What is the range of events which correspond to each pixel?&lt;/li&gt;
&lt;li&gt;What is the event with the maximum duration for that pixel?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We&amp;rsquo;re going to focus on 1) in this post as that&amp;rsquo;s where the slowdown was. 2) is fascinating but also surprisingly orthogonal. If you&amp;rsquo;re interested, I would suggest reading &lt;a href="https://thume.ca/2021/03/14/iforests/"&gt;this excellent post&lt;/a&gt; from Tristan Hume explaining the basic algorithm we use.&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s formalize our setup for 1): we have a sorted array containing N (where N is O(100k)) int64 timestamps. The timestamps come directly from the Perfetto trace file so we cannot make any assumptions on values or the distribution. But generally speaking, traces tend to have a mix of dense clusters of events (i.e. lot of activity) and large gaps between these clusters (i.e. not much going on). So it makes sense to optimize for this sort of data pattern.&lt;/p&gt;
&lt;p&gt;We then need to find the indexes corresponding to the &amp;ldquo;lower bound&amp;rdquo; of M regularly-spaced timestamps (A, A+S, A+2S&amp;hellip; B), where each corresponds to a pixel boundary and S corresponds to the &amp;ldquo;time in the trace represented by one pixel&amp;rdquo;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Data: | | | · · | | | | | | | | | | · · · | | · · · · | | | | | | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; dense sparse dense
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Queries: ↓ ↓ ↓ ↓ ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A A+S A+2S A+3S B
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │ │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ ▼ ▼ ▼ ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Result: lb(A) lb(A+S) lb(A+2S) lb(A+3S) lb(B)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The obvious solution, and what we&amp;rsquo;ve been doing for the last few years, is to just binary search each of the M values independently giving us O(M × log N). This is very fast when N is reasonable but when we get into O(100k) on high resolution screens (meaning M is also relatively large), you end up taking several milliseconds to do these queries.&lt;/p&gt;
&lt;p&gt;My first approach was to use &amp;ldquo;static search trees&amp;rdquo; to speed up the searches; ever since I had come across &lt;a href="https://curiouscoding.nl/posts/static-search-tree/"&gt;this amazing post&lt;/a&gt; last year, I had been itching to find a place to use them. They worked great and definitely made things faster, but it was a &lt;em&gt;lot&lt;/em&gt; of code and required hand-written AVX and a deep understanding, which I knew would be difficult to get through review. And I had this nagging feeling there was something much simpler I could do.&lt;/p&gt;
&lt;p&gt;Eventually I had a key realization: I wasn&amp;rsquo;t using the fact that the &lt;em&gt;queries themselves are sorted&lt;/em&gt;! By using this, we can do &lt;em&gt;much&lt;/em&gt; better and achieve O(M + log(N)) performance instead!&lt;/p&gt;
&lt;p&gt;How? Using a cool technique called &lt;a href="https://en.wikipedia.org/wiki/Exponential_search"&gt;exponential search&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;You start by binary searching for A just like before giving you an index I. But for every timestamp after that, instead of binary searching, you do something which looks like a hybrid between linear search and binary search.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Found A at index i, now searching for A+S:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Index: i i+1 i+2 i+3 i+4 i+5 i+6 i+7 i+8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Data: [A] [ ] [ ] [ ] [ ] [A+S] [ ] [ ] [ ]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↑ ↑ ↑ ↑ ↑
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; start +1 +2 +4 +8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;A+S &amp;lt;A+S &amp;lt;A+S &amp;gt;A+S
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; overshot!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └───────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; binary search here
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Take A + S: at I+1, I+2, I+4, I+8&amp;hellip;, you check if the timestamp at that position is greater than A + S. If not, you continue to the next one. If it is, then you stop and instead binary search between the previous position you checked and the current one.&lt;/p&gt;
&lt;p&gt;This is much faster than just binary searching both theoretically &lt;em&gt;and&lt;/em&gt; practically. Theoretically, notice how we scan the same memory &lt;strong&gt;at most twice&lt;/strong&gt;; once for the exponential scan, once in the binary search afterwards. This is unlike the &amp;ldquo;binary search everything&amp;rdquo; approach where the same piece of memory could be touched M times, if the data layout was pathological. This is where the O(M × log N) -&amp;gt; O(M + log N) improvement comes from.&lt;/p&gt;
&lt;p&gt;Note that this &lt;em&gt;also&lt;/em&gt; applies to static search trees which also have O(M × log N) complexity; exponential search turned out to beat them too!&lt;/p&gt;
&lt;p&gt;Practically, the initial exponential scan is great for CPUs which love scanning through memory linearly instead of jumping around randomly like binary search does. By significantly reducing the binary search range, you significantly reduce the chance of cache misses.&lt;/p&gt;
&lt;p&gt;A final micro-optimization you can do is, when the number of indices you are binary searching is fewer than 16 (empirically chosen), you can just replace with a linear scan instead. At those sizes, you find that the branching and looping from binary search is actually &lt;em&gt;slower&lt;/em&gt; than brute force checking everything (especially with the auto-vectorizer helping you out!).&lt;/p&gt;
&lt;p&gt;We switched to exponential search in &lt;a href="https://github.com/google/perfetto/pull/4648"&gt;this Perfetto PR&lt;/a&gt; and in our microbenchmark (on real trace data!), we saw an improvement from 1.5ms to 180us - a speedup of 8x! This, along with a bunch of other improvements we made, should help make the Perfetto UI faster and smoother on large traces!&lt;/p&gt;</content:encoded></item></channel></rss>