<?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: Timur Gilyazov</title>
    <description>The latest articles on DEV Community by Timur Gilyazov (@timur-developer).</description>
    <link>https://dev.to/timur-developer</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3965801%2F88d1cf49-1620-4973-ad0a-71902b2d7e17.png</url>
      <title>DEV Community: Timur Gilyazov</title>
      <link>https://dev.to/timur-developer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/timur-developer"/>
    <language>en</language>
    <item>
      <title>I Built a Visualizer for Go's Garbage Collector</title>
      <dc:creator>Timur Gilyazov</dc:creator>
      <pubDate>Wed, 03 Jun 2026 07:12:52 +0000</pubDate>
      <link>https://dev.to/timur-developer/i-built-a-visualizer-for-gos-garbage-collector-458h</link>
      <guid>https://dev.to/timur-developer/i-built-a-visualizer-for-gos-garbage-collector-458h</guid>
      <description>&lt;p&gt;Go's garbage collector is one of those things that usually "just works". And that is a good thing: most of the time, you do not want to think about it.&lt;/p&gt;

&lt;p&gt;Until a service starts slowing down under load, latency increases, and memory usage jumps.&lt;/p&gt;

&lt;p&gt;At that point, you usually check the obvious things first: CPU, locks, network, pprof, application metrics. The garbage collector often does not come to mind immediately, even though it can absolutely be part of the performance story.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcmde0fdi9hf679zw1vv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcmde0fdi9hf679zw1vv.png" alt="Gopher sitting in a trash can, used as an illustration for a Go garbage collector article" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go already gives us ways to observe the garbage collector from the outside. The runtime can print information about every GC cycle through &lt;code&gt;gctrace&lt;/code&gt; and &lt;code&gt;gcpacertrace&lt;/code&gt;, and it also exposes structured data through &lt;code&gt;runtime/metrics&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem is that during a real run, this quickly turns into a stream of lines and numbers. One or two lines are fine. But understanding the overall picture is much harder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is GC running more often than before?&lt;/li&gt;
&lt;li&gt;Are there long Stop-The-World pauses?&lt;/li&gt;
&lt;li&gt;Is heap usage stabilizing or slowly growing?&lt;/li&gt;
&lt;li&gt;Did a code change actually improve anything?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to see GC behavior not as scattered logs, but as a complete picture: how often collections happen, how heap live/goal values move, where STW pauses become unusual, and how one run differs from another.&lt;/p&gt;

&lt;p&gt;That is why I built &lt;a href="https://github.com/timur-developer/gcscope" rel="noopener noreferrer"&gt;gcscope&lt;/a&gt;: a terminal visualizer for Go's garbage collector.&lt;/p&gt;

&lt;p&gt;It collects data from &lt;code&gt;gctrace&lt;/code&gt;, &lt;code&gt;gcpacertrace&lt;/code&gt;, and &lt;code&gt;runtime/metrics&lt;/code&gt;, shows live terminal charts, lets you save snapshots, and compares different runs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7ouxt2cij1pd19tcugb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7ouxt2cij1pd19tcugb.png" title="gcscope UI GC metrics, charts, and details about recent garbage collection cycles in one terminal window" alt="gcscope UI GC metrics, charts, and details about recent garbage collection cycles in one terminal window" width="799" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article I will show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how to watch Go GC behavior in real time&lt;/li&gt;
&lt;li&gt;how to check whether GC may be related to a performance drop&lt;/li&gt;
&lt;li&gt;how to spot long STW pauses&lt;/li&gt;
&lt;li&gt;how to reason about heap live vs heap goal&lt;/li&gt;
&lt;li&gt;how to run visualization on your own Go binary without changing its code&lt;/li&gt;
&lt;li&gt;how &lt;code&gt;gcscope&lt;/code&gt; turns runtime logs into terminal charts&lt;/li&gt;
&lt;li&gt;how to compare application behavior before and after a change&lt;/li&gt;
&lt;li&gt;how to use this as a starting point before going deeper with &lt;code&gt;pprof&lt;/code&gt;, &lt;code&gt;trace&lt;/code&gt;, and other profiling tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you should have a better mental model for observing Go's GC and a practical way to notice situations where it may affect application performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A few terms before we start
&lt;/h2&gt;

&lt;p&gt;I do not want to turn this article into a deep dive into the entire Go runtime, but a few terms are useful.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GC cycle&lt;/strong&gt;: one run of the garbage collector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STW (Stop-The-World)&lt;/strong&gt;: a short pause in program execution needed by the garbage collector for some memory-management operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;heap live&lt;/code&gt;&lt;/strong&gt;: the amount of live heap memory after a GC cycle, meaning objects that are still reachable and needed by the program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;heap goal&lt;/code&gt;&lt;/strong&gt;: the target heap size that the runtime uses when deciding when to start the next GC cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gctrace&lt;/code&gt;&lt;/strong&gt;: a runtime logging mode that prints information about every GC cycle to &lt;code&gt;stderr&lt;/code&gt;. The exact format can differ between Go versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gcpacertrace&lt;/code&gt;&lt;/strong&gt;: an additional logging mode that shows information about the GC pacer, the mechanism that regulates GC work and helps keep heap growth under control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;runtime/metrics&lt;/code&gt;&lt;/strong&gt;: a standard library package that exposes structured runtime metrics without parsing text logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article I only talk about what can be observed from the outside through these sources. &lt;code&gt;gcscope&lt;/code&gt; does not patch the runtime and does not claim to expose every internal detail of Go's GC. It is a way to look at the data Go already provides in a more useful form.&lt;/p&gt;

&lt;p&gt;A deeper article about GC internals and runtime metrics would be a separate topic. Here, the focus is practical observation and visualization.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why gctrace and gcpacertrace are useful, but hard to use dynamically
&lt;/h2&gt;

&lt;p&gt;If you enable &lt;code&gt;gctrace&lt;/code&gt;, the Go runtime starts printing information about every garbage collection cycle: pauses, heap sizes, GC CPU usage, and other values.&lt;/p&gt;

&lt;p&gt;If you add &lt;code&gt;gcpacertrace&lt;/code&gt;, you also get data about the pacer, the component that regulates GC intensity.&lt;/p&gt;

&lt;p&gt;This is useful data. But when you try to use it during a real application run, a few problems appear quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  It is hard to see trends over time
&lt;/h3&gt;

&lt;p&gt;One line is readable. A hundred or two hundred lines become noise.&lt;/p&gt;

&lt;p&gt;From raw logs, it is not easy to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is GC happening more frequently, or was that just one spike?&lt;/li&gt;
&lt;li&gt;Where do long STW pauses appear?&lt;/li&gt;
&lt;li&gt;Did &lt;code&gt;heap live&lt;/code&gt; stabilize, or is it slowly growing?&lt;/li&gt;
&lt;li&gt;Did the program behave differently after a code change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data is there, but the overall picture is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before/after comparisons are painful
&lt;/h3&gt;

&lt;p&gt;Imagine you changed &lt;code&gt;GOGC&lt;/code&gt;, added a cache, rewrote an allocation-heavy path, or changed the workload. Now you want to know whether things got better or worse.&lt;/p&gt;

&lt;p&gt;With raw logs, this becomes manual work: save the output of the first run, save the output of the second run, find comparable regions, compare values, and try not to get lost.&lt;/p&gt;

&lt;p&gt;For a one-off deep investigation, that is possible. For fast iteration, it is inconvenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distributions matter more than single values
&lt;/h3&gt;

&lt;p&gt;A single STW pause of 300 microseconds does not say much by itself. Context matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this a normal value or a rare long pause?&lt;/li&gt;
&lt;li&gt;What does p50 look like, the typical STW pause level?&lt;/li&gt;
&lt;li&gt;What happens to p99, which helps surface rare long pauses?&lt;/li&gt;
&lt;li&gt;What was the maximum STW pause in the recent observation window?&lt;/li&gt;
&lt;li&gt;Did any of that change after the latest modification?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;gctrace&lt;/code&gt; is not bad. Quite the opposite: it is one of the most useful sources of information about Go GC behavior. But logs are better for inspecting individual events than for understanding the whole dynamic picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Seeing Go GC behavior in one minute with gcscope
&lt;/h2&gt;

&lt;p&gt;You can install &lt;code&gt;gcscope&lt;/code&gt; with &lt;code&gt;go install&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/timur-developer/gcscope/cmd/gcscope@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, you can use it as a regular CLI tool.&lt;/p&gt;

&lt;p&gt;The fastest way to see the UI is to run a built-in demo workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcscope lab churn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fad0xfp3525spvsqy8n7o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fad0xfp3525spvsqy8n7o.gif" title="Running the built-in lab churn demo." alt="Running the builtin lab churn demo" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;lab&lt;/code&gt; mode does not require preparing a service or setting up a load test. The tool runs a synthetic workload for you, which is useful for seeing how the charts, STW pauses, heap changes, and other metrics behave.&lt;/p&gt;

&lt;p&gt;If you start &lt;code&gt;gcscope&lt;/code&gt; and see no updates, that does not automatically mean something is broken. Your program may simply not be hitting GC cycles yet. In demo mode, this is usually visible quickly; in a real application, it depends on allocations and workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What the UI shows and how to read it
&lt;/h2&gt;

&lt;p&gt;It is easy to get distracted by charts and just stare at them. But &lt;code&gt;gcscope&lt;/code&gt; becomes much more useful when you start with a question.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did GC start running more often?&lt;/li&gt;
&lt;li&gt;Are there rare long STW pauses?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;heap live&lt;/code&gt; growing?&lt;/li&gt;
&lt;li&gt;How close is &lt;code&gt;heap live&lt;/code&gt; to &lt;code&gt;heap goal&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Did behavior change after a new version of the code?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns the UI from a nice picture into an analysis tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnd0sawpsu44lprxs5jux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnd0sawpsu44lprxs5jux.png" title="General view of the gcscope interface." alt="General view of the gcscope interface" width="799" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interface has several main areas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Current Values&lt;/strong&gt;: current GC cycle number, latest STW pause, heap live, heap goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Information&lt;/strong&gt;: summary for recent events: GC frequency, max STW, thresholds, environment, snapshot status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STW per cycle&lt;/strong&gt;: STW pauses for individual GC cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cycle Details&lt;/strong&gt;: details for the selected GC event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heap live over time&lt;/strong&gt;: how live heap changes over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STW p50/p99/max over time&lt;/strong&gt;: how STW window statistics change over time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How often GC runs
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Information&lt;/strong&gt; panel shows GC frequency and average interval between cycles.&lt;/p&gt;

&lt;p&gt;These values are calculated over recent events. They help you understand whether GC is truly running more often or whether you are seeing a short random burst.&lt;/p&gt;

&lt;p&gt;Frequent GC is not automatically a problem. But if it appears together with higher latency, extra CPU usage, or longer STW pauses, it is a reason to look deeper: memory allocations, &lt;code&gt;GOGC&lt;/code&gt;, &lt;code&gt;GOMEMLIMIT&lt;/code&gt;, and workload shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where long STW pauses appear
&lt;/h3&gt;

&lt;p&gt;Usually, developers do not notice STW pauses directly. They notice symptoms: the service feels jittery, some requests become slower, and the obvious cause is not clear.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;gcscope&lt;/code&gt;, these areas are especially useful:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;last STW (us)&lt;/code&gt; in &lt;strong&gt;Current Values&lt;/strong&gt;, which shows the STW pause of the latest GC cycle.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STW p50/p99/max over time (us)&lt;/code&gt;, which shows how typical and rare pauses changed over time.&lt;/li&gt;
&lt;li&gt;The per-cycle bar chart, which lets you inspect individual events.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The basic idea is simple: p50 shows the normal background level, while p99 and max help surface rare long pauses.&lt;/p&gt;

&lt;p&gt;If long pauses repeat, it is important to look not only at the pause value itself, but also at when it appeared. Does it match an allocation spike? A change in request pattern? A different runtime setting?&lt;/p&gt;

&lt;h3&gt;
  
  
  Heap behavior: heap live vs heap goal
&lt;/h3&gt;

&lt;p&gt;Heap size is rarely interesting by itself. The dynamic behavior matters more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is &lt;code&gt;heap live&lt;/code&gt; growing over time or stabilizing?&lt;/li&gt;
&lt;li&gt;How close is live heap to &lt;code&gt;heap goal&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;How does this change under different workloads?&lt;/li&gt;
&lt;li&gt;What happens after code or runtime-setting changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pair &lt;code&gt;heap live&lt;/code&gt; / &lt;code&gt;heap goal&lt;/code&gt; helps you see how much pressure the garbage collector is under while keeping heap growth within target bounds.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;gcscope&lt;/code&gt;, this is visible in &lt;strong&gt;Current Values&lt;/strong&gt; and on the &lt;strong&gt;Heap live over time&lt;/strong&gt; chart.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changed between two runs
&lt;/h3&gt;

&lt;p&gt;When you change code, runtime settings, or workload shape, you usually want a quick answer: did this help, or did it make things worse?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcscope&lt;/code&gt; gives you two ways to approach that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visually compare behavior in the UI&lt;/li&gt;
&lt;li&gt;save snapshots and compare them with &lt;code&gt;diff&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will return to snapshots and &lt;code&gt;diff&lt;/code&gt; later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal controls
&lt;/h3&gt;

&lt;p&gt;For a first run, you only need a few keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;?&lt;/code&gt;, &lt;code&gt;h&lt;/code&gt;, or &lt;code&gt;f1&lt;/code&gt;: open help&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;space&lt;/code&gt;: pause or resume live updates&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;left&lt;/code&gt; / &lt;code&gt;right&lt;/code&gt;: move through history when paused&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s&lt;/code&gt;: save a snapshot&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q&lt;/code&gt; or &lt;code&gt;ctrl+c&lt;/code&gt;: quit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fck5anmyhkmt1ah7b7fid.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fck5anmyhkmt1ah7b7fid.gif" alt="Animated demo of gcscope showing help, layout switching, chart zooming, pause mode, event history navigation, and snapshot saving" width="560" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The GIF above shows basic interaction with &lt;code&gt;gcscope&lt;/code&gt;: opening help, switching display modes, changing chart scale, pausing live updates, moving through event history, and saving a snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;run&lt;/code&gt; mode: observing your own application without changing code
&lt;/h2&gt;

&lt;p&gt;For most situations, I would start with &lt;code&gt;run&lt;/code&gt; mode.&lt;/p&gt;

&lt;p&gt;It starts your Go binary under observation and reads data that the runtime writes to &lt;code&gt;stderr&lt;/code&gt; through &lt;code&gt;gctrace&lt;/code&gt; and &lt;code&gt;gcpacertrace&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcscope run ./path/to/your-binary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important details.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;target&lt;/code&gt; is a path to an already compiled binary, not a &lt;code&gt;.go&lt;/code&gt; file. So build your application first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# replace ./cmd/myapp with the path to your application's main package&lt;/span&gt;
go build &lt;span class="nt"&gt;-o&lt;/span&gt; ./myapp ./cmd/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it through &lt;code&gt;gcscope&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcscope run ./myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, if your application needs arguments, use &lt;code&gt;--&lt;/code&gt; as a separator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcscope run ./myapp &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; ./config.yaml &lt;span class="nt"&gt;--port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything after &lt;code&gt;--&lt;/code&gt; is passed to your program unchanged. &lt;code&gt;gcscope&lt;/code&gt; uses the separator to distinguish its own arguments from the target application's arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Architecture: from stderr to TUI
&lt;/h2&gt;

&lt;p&gt;At a high level, &lt;code&gt;gcscope&lt;/code&gt; works the same way with any data source: it receives information about GC behavior, converts it into a stream of events, builds aggregates over those events, and sends the result to the UI.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;run&lt;/code&gt; mode, the path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Go binary
  -&amp;gt; stderr (gctrace/gcpacertrace)
  -&amp;gt; parser
  -&amp;gt; GC events
  -&amp;gt; latest N events
  -&amp;gt; statistics and chart data
  -&amp;gt; terminal UI, snapshots, and run comparison
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why &lt;code&gt;run&lt;/code&gt; can see GC at all
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;run&lt;/code&gt; mode to observe the garbage collector, the target process must output &lt;code&gt;gctrace&lt;/code&gt; and &lt;code&gt;gcpacertrace&lt;/code&gt; data.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcscope&lt;/code&gt; does this automatically by configuring &lt;code&gt;GODEBUG&lt;/code&gt; and adding &lt;code&gt;gctrace=1&lt;/code&gt; and &lt;code&gt;gcpacertrace=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important part is not to overwrite the user's existing &lt;code&gt;GODEBUG&lt;/code&gt; settings. If &lt;code&gt;GODEBUG&lt;/code&gt; already contains other options, they should be preserved and only the missing values should be added.&lt;/p&gt;

&lt;p&gt;Here is the relevant code that builds the final &lt;code&gt;GODEBUG&lt;/code&gt; value:&lt;/p&gt;

&lt;p&gt;
  Code: building the final GODEBUG value
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/source/runner/runner.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NormalizeGODEBUG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;foundGctrace&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;foundGcpacer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gctrace="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;foundGctrace&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gctrace=1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;foundGctrace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gcpacertrace="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;foundGcpacer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gcpacertrace=1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;foundGcpacer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;foundGctrace&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gctrace=1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;foundGcpacer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gcpacertrace=1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;So the user starts their binary through &lt;code&gt;gcscope&lt;/code&gt;, and the tool creates the conditions needed for the runtime to expose GC data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a GC event is not just one log line
&lt;/h3&gt;

&lt;p&gt;When designing a tool like this, the first idea seems simple: take a &lt;code&gt;gctrace&lt;/code&gt; line, parse it with a regular expression, and immediately send values to the UI.&lt;/p&gt;

&lt;p&gt;For a minimal prototype, that is enough. But limitations appear quickly.&lt;/p&gt;

&lt;p&gt;First, the UI almost never needs the original log line. It needs values: GC number, time, STW pause, heap sizes, &lt;code&gt;heap live&lt;/code&gt; / &lt;code&gt;heap goal&lt;/code&gt;, whether GC was forced, and other fields.&lt;/p&gt;

&lt;p&gt;Second, not all information comes from the same line. A &lt;code&gt;gc ...&lt;/code&gt; line describes the GC cycle itself, while &lt;code&gt;pacer: ...&lt;/code&gt; lines add information about the pacer. If the UI should show this as one event, those pieces need to be connected.&lt;/p&gt;

&lt;p&gt;This is the parser entry point that separates GC lines from pacer lines:&lt;/p&gt;

&lt;p&gt;
  Code: parsing GC and pacer trace lines
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/source/runner/parser.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ParseLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GCEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gc "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parseGCLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pacer:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parsePacerLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GCEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;Third, the tool needs aggregates on top of events: p50/p99/max over a sliding window, GC frequency, history for charts, snapshots, and &lt;code&gt;diff&lt;/code&gt;. Doing all that over raw log lines would be awkward.&lt;/p&gt;

&lt;p&gt;So I did not bind the UI directly to &lt;code&gt;gctrace&lt;/code&gt; strings. Regular expressions can exist inside the parser, but the parser should return proper GC events.&lt;/p&gt;

&lt;p&gt;The model became simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logs and metrics -&amp;gt; GC events -&amp;gt; aggregates -&amp;gt; charts, snapshots, diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of this, the UI does not know how the original &lt;code&gt;stderr&lt;/code&gt; line looked. It only sees prepared data: GC cycle, STW pause, heap live/goal, and optional pacer fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  How data reaches the UI
&lt;/h3&gt;

&lt;p&gt;For the terminal interface, I use the message model from &lt;a href="https://github.com/charmbracelet/bubbletea" rel="noopener noreferrer"&gt;Bubble Tea&lt;/a&gt;, a Go library for building TUI applications.&lt;/p&gt;

&lt;p&gt;Once a GC event is produced, &lt;code&gt;gcscope&lt;/code&gt; sends it into the Bubble Tea model:&lt;/p&gt;

&lt;p&gt;
  Code: sending GC events into the TUI model
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// cmd/gcscope/lab.go, similar in run.go&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;prog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GCEventMsg&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;At&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;When the UI receives a GC event, it updates the sliding window, recalculates aggregates, and refreshes chart history:&lt;/p&gt;

&lt;p&gt;
  Code: updating aggregates and chart history
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/ui/model_update.go&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;GCEventMsg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;At&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;At&lt;/span&gt;

    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="c"&gt;// sliding window of recent events&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ComputeAggregates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c"&gt;// aggregate calculation&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pushHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;At&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="c"&gt;// history for charts&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paused&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentWindowLen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;The UI works with events, not raw log strings.&lt;/p&gt;

&lt;p&gt;The UI works with a GCEvent model that contains the fields needed for charts, snapshots, and comparisons:&lt;/p&gt;

&lt;p&gt;
  Code: GCEvent fields used by the UI
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/domain/events.go&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;GCEvent&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GCNum&lt;/span&gt;           &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;TimeSinceStartS&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;
    &lt;span class="n"&gt;GCCPUPercent&lt;/span&gt;    &lt;span class="kt"&gt;float64&lt;/span&gt;

    &lt;span class="n"&gt;HeapStartMB&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;HeapEndMB&lt;/span&gt;   &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;HeapLiveMB&lt;/span&gt;  &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;HeapGoalMB&lt;/span&gt;  &lt;span class="kt"&gt;int&lt;/span&gt;

    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="c"&gt;// other fields from gctrace/runtime metrics&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;That makes the rest of the logic much easier: charts, windows, p50/p99/max, snapshots, and &lt;code&gt;diff&lt;/code&gt; all build on the same data model.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;attach&lt;/code&gt; mode: observing an already running application
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;attach&lt;/code&gt; mode is useful when you want to observe an already running application through an HTTP endpoint. Your service exposes runtime metrics, and &lt;code&gt;gcscope&lt;/code&gt; polls that endpoint, converts the results into events, and shows them in the UI.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add an HTTP endpoint from &lt;code&gt;pkg/reporter&lt;/code&gt;, a small package inside &lt;code&gt;gcscope&lt;/code&gt; that exposes selected &lt;code&gt;runtime/metrics&lt;/code&gt; data as JSON.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gcscope&lt;/code&gt; periodically polls the endpoint and converts metrics into events.&lt;/li&gt;
&lt;li&gt;The UI works with those events just like it does in other modes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The minimal example below uses the standard library's &lt;code&gt;http.ServeMux&lt;/code&gt;. That is not a requirement. In your project, you can register the handler in any router you already use, such as &lt;code&gt;chi&lt;/code&gt;, &lt;code&gt;gorilla/mux&lt;/code&gt;, or another router.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reporter.New()&lt;/code&gt; returns an object with two methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Path()&lt;/code&gt;: the endpoint path, &lt;code&gt;/gcscope/metrics&lt;/code&gt; by default&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Handler()&lt;/code&gt;: an HTTP handler that returns &lt;code&gt;runtime/metrics&lt;/code&gt; data in JSON format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Minimal example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/timur-developer/gcscope/pkg/reporter"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rep&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;reporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rep&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;rep&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then attach &lt;code&gt;gcscope&lt;/code&gt; to the endpoint. If the service runs locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcscope attach http://127.0.0.1:8080/gcscope/metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact JSON contract is not the important part for this article. The useful point is that the format is provided by &lt;code&gt;pkg/reporter&lt;/code&gt;, while the original source remains &lt;code&gt;runtime/metrics&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to look deeper, the implementation and package README are in the repository: &lt;a href="https://github.com/timur-developer/gcscope/tree/main/pkg/reporter" rel="noopener noreferrer"&gt;pkg/reporter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. &lt;code&gt;run&lt;/code&gt; vs &lt;code&gt;attach&lt;/code&gt;: why have both?
&lt;/h2&gt;

&lt;p&gt;The two modes solve a similar problem: observing garbage collector behavior. But they get data differently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run&lt;/code&gt; parses &lt;code&gt;gctrace&lt;/code&gt; / &lt;code&gt;gcpacertrace&lt;/code&gt; output from the target process's &lt;code&gt;stderr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attach&lt;/code&gt; reads &lt;code&gt;runtime/metrics&lt;/code&gt; through an HTTP endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to two important differences.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;attach&lt;/code&gt; does not have access to the target process environment. Values such as &lt;code&gt;GOGC&lt;/code&gt;, &lt;code&gt;GOMEMLIMIT&lt;/code&gt;, and &lt;code&gt;GODEBUG&lt;/code&gt; are unavailable and shown as &lt;code&gt;n/a&lt;/code&gt; in the UI.&lt;/p&gt;

&lt;p&gt;Second, values in &lt;code&gt;attach&lt;/code&gt; and &lt;code&gt;run&lt;/code&gt; do not have to match one-to-one. They come from different data sources, with different precision and semantics.&lt;/p&gt;

&lt;p&gt;In practice, the choice depends on the task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you want the closest view to &lt;code&gt;gctrace&lt;/code&gt; for individual GC cycles and STW pauses, start with &lt;code&gt;run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you want to connect to an already running process through an endpoint, use &lt;code&gt;attach&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Data storage, snapshots, and diff
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gcscope&lt;/code&gt; keeps the latest N garbage collection events in memory. By default, the window size is 200 events, but you can change it with &lt;code&gt;--window-size&lt;/code&gt;.&lt;br&gt;
The observation window size comes from configuration and is used when creating the UI store:&lt;/p&gt;

&lt;p&gt;
  Code: configuring the GC event window
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// cmd/gcscope/run.go: pass the window size from config (--window-size)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WindowSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshotDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stwTh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;envInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// internal/ui/model_types.go: inside the model, create a store with the latest N events&lt;/span&gt;
&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windowSize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;This is intentional.&lt;/p&gt;

&lt;p&gt;GC is a stream of similar events. For interactive analysis, the entire process history is often less useful than the last few minutes or the latest N cycles.&lt;/p&gt;

&lt;p&gt;A sliding window helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the UI responsive&lt;/li&gt;
&lt;li&gt;recalculate p50/p99/max quickly&lt;/li&gt;
&lt;li&gt;show what the garbage collector is doing recently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A snapshot in &lt;code&gt;gcscope&lt;/code&gt; is a JSON file that saves the current observation window.&lt;/p&gt;

&lt;p&gt;Here is what part of a snapshot file looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gc_cycles_total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"last_stw_us"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"heap_live_mb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"heap_goal_mb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;166&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"window"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stw_p50_us"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stw_p99_us"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;550&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stw_max_us"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;550&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"gc_num"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"time_since_start_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"heap_live_mb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"heap_goal_mb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current values such as &lt;code&gt;gc_cycles_total&lt;/code&gt;, &lt;code&gt;last_stw_us&lt;/code&gt;, &lt;code&gt;heap_live_mb&lt;/code&gt;, &lt;code&gt;heap_goal_mb&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;window statistics such as &lt;code&gt;stw_p50_us&lt;/code&gt;, &lt;code&gt;stw_p99_us&lt;/code&gt;, &lt;code&gt;stw_max_us&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the list of recent GC events from the same window used by the UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, snapshots are useful after runs you want to compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;before and after an optimization&lt;/li&gt;
&lt;li&gt;before and after changing &lt;code&gt;GOGC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;before and after deploying a new service version&lt;/li&gt;
&lt;li&gt;under different workload scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comparison is done with &lt;code&gt;gcscope diff&lt;/code&gt;. The first argument is the "before" snapshot, and the second is the "after" snapshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcscope diff ./before.json ./after.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcscope diff &lt;span class="se"&gt;\&lt;/span&gt;
  gcscope/tmp/snapshots/gcscope-snapshot-2026-05-28T15-14-22.json &lt;span class="se"&gt;\&lt;/span&gt;
  gcscope/tmp/snapshots/gcscope-snapshot-2026-05-28T15-16-58.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;diff&lt;/code&gt; compares the main heap values and STW window statistics, then prints the difference as &lt;code&gt;B - A&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A:
  gc_cycles_total: 16
  heap_live_mb:    59
  stw_max_us:      550
  stw_p50_us:      0
  stw_p99_us:      550

B:
  gc_cycles_total: 56
  heap_live_mb:    9
  stw_max_us:      590
  stw_p50_us:      0
  stw_p99_us:      590

Delta (B-A):
  heap_live_mb: -50
  stw_max_us:   +40
  stw_p50_us:   0
  stw_p99_us:   +40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not an automatic leak detector and not a magic optimizer. But it is a fast way to answer a practical question: did the change affect GC behavior, and in which direction?&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Where gcscope is especially useful
&lt;/h2&gt;

&lt;p&gt;I see &lt;code&gt;gcscope&lt;/code&gt; as a quick first step when investigating problems that might be related to GC.&lt;/p&gt;

&lt;p&gt;When an application starts behaving strangely, it is not always obvious where to look first: runtime settings, workload shape, network, scheduler, or application code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcscope&lt;/code&gt; helps check one hypothesis quickly: what was the garbage collector doing at that moment?&lt;/p&gt;

&lt;p&gt;It shows GC behavior over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how often GC runs&lt;/li&gt;
&lt;li&gt;what happens to the heap&lt;/li&gt;
&lt;li&gt;whether long STW pauses appear&lt;/li&gt;
&lt;li&gt;whether behavior changed after code or runtime-setting changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the charts show something suspicious, it becomes easier to choose the next tool: open &lt;code&gt;pprof&lt;/code&gt;, inspect &lt;code&gt;go tool trace&lt;/code&gt;, find allocation-heavy paths, or compare the data with Prometheus and Grafana metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Trying gcscope on your own project
&lt;/h2&gt;

&lt;p&gt;The simplest way to try &lt;code&gt;gcscope&lt;/code&gt; on your project is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;code&gt;gcscope&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the built-in &lt;code&gt;lab churn&lt;/code&gt; workload to understand the UI.&lt;/li&gt;
&lt;li&gt;Build your Go application or service as a binary.&lt;/li&gt;
&lt;li&gt;Run it with &lt;code&gt;gcscope run&lt;/code&gt; under a realistic workload.&lt;/li&gt;
&lt;li&gt;Save a snapshot with &lt;code&gt;s&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Repeat the run after changing code or runtime settings and save a second snapshot.&lt;/li&gt;
&lt;li&gt;Compare both snapshots with &lt;code&gt;gcscope diff&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Minimal command set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/timur-developer/gcscope/cmd/gcscope@latest

gcscope lab churn

&lt;span class="c"&gt;# replace ./cmd/myapp with the path to your application's main package&lt;/span&gt;
go build &lt;span class="nt"&gt;-o&lt;/span&gt; ./myapp ./cmd/myapp

gcscope run ./myapp

gcscope diff ./before.json ./after.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project code, installation instructions, and documentation for &lt;code&gt;gcscope&lt;/code&gt; are here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/timur-developer/gcscope" rel="noopener noreferrer"&gt;https://github.com/timur-developer/gcscope&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the tool looks useful, I would really appreciate a GitHub star, feedback in GitHub Issues, or your thoughts in the comments.&lt;/p&gt;

&lt;p&gt;How do you usually investigate a Go service that slows down under load? Do you start with &lt;code&gt;pprof&lt;/code&gt;, metrics, logs, traces, or something else? And at what point do you check the garbage collector?&lt;/p&gt;

</description>
      <category>go</category>
      <category>performance</category>
      <category>tooling</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
