<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SACHIN INDWAR</title>
    <description>The latest articles on DEV Community by SACHIN INDWAR (@sachin-680).</description>
    <link>https://dev.to/sachin-680</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2225598%2F8cdbbcd6-8dfd-46e0-b399-2c6bd14bded9.jpg</url>
      <title>DEV Community: SACHIN INDWAR</title>
      <link>https://dev.to/sachin-680</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sachin-680"/>
    <language>en</language>
    <item>
      <title>Tracing a 3 Memory Blow-Up in Grafana's Time Comparison</title>
      <dc:creator>SACHIN INDWAR</dc:creator>
      <pubDate>Fri, 07 Aug 2026 15:15:46 +0000</pubDate>
      <link>https://dev.to/sachin-680/tracing-a-3x-memory-blow-up-in-grafanas-time-comparison-2jpn</link>
      <guid>https://dev.to/sachin-680/tracing-a-3x-memory-blow-up-in-grafanas-time-comparison-2jpn</guid>
      <description>&lt;p&gt;While contributing to Grafana, I picked up a memory issue in the &lt;strong&gt;Time Comparison&lt;/strong&gt; feature — a follow-up to &lt;a href="https://github.com/grafana/grafana/pull/129681" rel="noopener noreferrer"&gt;earlier performance work&lt;/a&gt; I had done in the same area.&lt;/p&gt;

&lt;p&gt;A comparison panel was consuming significantly more memory than expected. The interesting part: the extra memory wasn't coming from real data.&lt;/p&gt;

&lt;p&gt;This post covers how I traced it to the root cause and fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Time Comparison overlays an earlier period onto the current one — for example, this week vs. last week. The comparison data is fetched from the earlier window and &lt;strong&gt;shifted forward&lt;/strong&gt; before rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query → DataFrame → Prepare frame → Shift → Render
                         │
                         └─ Gap filling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important detail: &lt;strong&gt;gap filling ran before the comparison frame was shifted.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I reproduced the issue with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Series&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window&lt;/td&gt;
&lt;td&gt;6h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interval&lt;/td&gt;
&lt;td&gt;20s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare offset&lt;/td&gt;
&lt;td&gt;24h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single-period panel contained roughly &lt;strong&gt;540,000 points&lt;/strong&gt;, so a comparison panel should be about &lt;strong&gt;2× the baseline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead, the compare frame contained &lt;strong&gt;3,240,500 points — ~6× the baseline&lt;/strong&gt; — and consumed &lt;strong&gt;76.4 MB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The question was: &lt;strong&gt;where did the extra points come from?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Investigation
&lt;/h2&gt;

&lt;p&gt;I first verified the baseline to rule out the query returning unexpected data. It was correct.&lt;/p&gt;

&lt;p&gt;Then I used a reproducible browser harness and a &lt;strong&gt;heap snapshot&lt;/strong&gt; to inspect the extra memory. Most of it was &lt;strong&gt;null rows introduced during gap filling&lt;/strong&gt; — not real samples, not copies.&lt;/p&gt;

&lt;p&gt;Following the frame through the preparation pipeline revealed why. When gap filling ran, the compare frame still represented data &lt;strong&gt;24 hours in the past&lt;/strong&gt;, but the gap-filler was using the &lt;strong&gt;current time range&lt;/strong&gt; as its reference:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Compare frame                         Current range
   [===== 6h =====]                      [===== 6h =====]
          └───────────────  24h  ───────────────┘
                gap-filler reads this offset as one gap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At a 20-second interval, 24 hours is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;24 × 60 × 60 / 20 = 4,320 intervals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So up to &lt;strong&gt;4,320 null positions per series&lt;/strong&gt; were introduced purely because the frame hadn't been shifted yet. The frame was then shifted forward, leaving most of that padding outside the visible range — where nothing draws it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;The issue was &lt;strong&gt;the reference range gap filling used&lt;/strong&gt;. It evaluated the compare frame against the &lt;em&gt;current&lt;/em&gt; range instead of the range the frame actually represented:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before                         After
──────                         ─────
Compare frame                  Compare frame
     ↓                              ↓
Gap-fill vs current range      Use frame's effective range
     ↓                              ↓
Shift by 24h                   Gap-fill
                                    ↓
                               Shift by 24h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;I changed the gap-filling logic to account for the compare frame's own offset when determining its effective time range. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;genuine gaps are still filled&lt;/li&gt;
&lt;li&gt;non-comparison frames are unchanged&lt;/li&gt;
&lt;li&gt;the unnecessary null padding is eliminated&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compare points&lt;/td&gt;
&lt;td&gt;3,240,500&lt;/td&gt;
&lt;td&gt;1,081,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare heap&lt;/td&gt;
&lt;td&gt;76.4 MB&lt;/td&gt;
&lt;td&gt;22.8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12× dashboard heap&lt;/td&gt;
&lt;td&gt;822.5 MB&lt;/td&gt;
&lt;td&gt;225.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The compare frame went from roughly &lt;strong&gt;6× to 2× the baseline&lt;/strong&gt; — consistent with the actual amount of data being rendered. I also verified it in the browser: the reproduced panel dropped from &lt;strong&gt;144.2 MB to 84.4 MB&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Performance bugs can be data-flow bugs.&lt;/strong&gt; The problem wasn't an expensive algorithm — it was a correct operation running against the wrong state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Profile before optimizing.&lt;/strong&gt; The heap snapshot showed the extra memory was synthetic null data, which immediately narrowed the search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Large-codebase bugs often live between components.&lt;/strong&gt; This one emerged from the interaction between time comparison, frame preparation, gap filling, and time shifting — not from a single isolated function.&lt;/p&gt;
&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;This reinforced a lesson I keep coming back to while contributing to large open source projects:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When debugging performance, follow the data — not just the allocations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix is up as a pull request against Grafana:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/grafana/grafana/pull/129934" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        TimeComparison: Stop padding compare frames across the compare offset
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#129934&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/sAchin-680" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F118827645%3Fv%3D4" alt="sAchin-680 avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/sAchin-680" rel="noopener noreferrer"&gt;sAchin-680&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/grafana/grafana/pull/129934" rel="noopener noreferrer"&gt;&lt;time&gt;Aug 03, 2026&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;Follow-up to the memory investigation in #129681.&lt;/p&gt;
&lt;p&gt;Compare frames are gap-filled while still sitting in their own earlier window - the panel only shifts them onto the current range afterwards. Measuring them against the unshifted current range reads the whole compare offset as a gap, so each frame gets padded with a null row per interval across it. Those rows then travel forward with the shift and land outside the visible range, where nothing can see them.&lt;/p&gt;
&lt;p&gt;The fix measures a compare frame against the window it actually covers. Genuine gaps inside a frame still fill, and non-compare frames are untouched.&lt;/p&gt;
&lt;p&gt;Measured with @drew08t's benchmark from #129806 (500 series, 6h window, 20s interval, 24h offset):&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Prepared points&lt;/th&gt;
&lt;th&gt;Heap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compare — before&lt;/td&gt;
&lt;td&gt;3,240,500&lt;/td&gt;
&lt;td&gt;76.4 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare — after&lt;/td&gt;
&lt;td&gt;1,081,000&lt;/td&gt;
&lt;td&gt;22.8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard 12x compare — before&lt;/td&gt;
&lt;td&gt;38,886,000&lt;/td&gt;
&lt;td&gt;822.5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard 12x compare — after&lt;/td&gt;
&lt;td&gt;12,972,000&lt;/td&gt;
&lt;td&gt;225.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Compare now reports 2.00x the points of a single-period baseline instead of 6.00x — two periods of 500 series should cost exactly twice one period, and it now does.&lt;/p&gt;
&lt;p&gt;Also confirmed in a browser on the four-panel harness, toggling the fix in one session: the compare panel goes from 144.2 MB to 84.4 MB, with compare frames back to their true 1080 points. It now sits alongside an equivalent 1000-series non-compare panel (81.7 MB), the remaining ~3 MB being genuine compare overhead.&lt;/p&gt;
&lt;p&gt;Note the benchmark in #129806 is still a draft, so those numbers aren't reproducible from main until it lands.&lt;/p&gt;
&lt;p&gt;Ref #125104&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/grafana/grafana/pull/129934" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>opensource</category>
      <category>debugging</category>
      <category>performance</category>
      <category>grafana</category>
    </item>
  </channel>
</rss>
