惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
F
Full Disclosure
Cloudbric
Cloudbric
Blog — PlanetScale
Blog — PlanetScale
W
WeLiveSecurity
N
News and Events Feed by Topic
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
B
Blog
GbyAI
GbyAI
C
Check Point Blog
B
Blog RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Recorded Future
Recorded Future
The Last Watchdog
The Last Watchdog
N
News and Events Feed by Topic
T
The Blog of Author Tim Ferriss
O
OpenAI News
V
V2EX
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
IT之家
IT之家
WordPress大学
WordPress大学
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Security @ Cisco Blogs
C
Cisco Blogs
Security Latest
Security Latest
S
Security Affairs
V
Visual Studio Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Azure Blog
Microsoft Azure Blog
Last Week in AI
Last Week in AI
AWS News Blog
AWS News Blog
雷峰网
雷峰网
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
博客园_首页
U
Unit 42
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
Project Zero
Project Zero
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
L
LINUX DO - 热门话题
H
Hacker News: Front Page

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
I Built a Visualizer for Go's Garbage Collector
Timur Gilyazov · 2026-06-03 · via DEV Community

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.

Until a service starts slowing down under load, latency increases, and memory usage jumps.

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.

Gopher sitting in a trash can, used as an illustration for a Go garbage collector article

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

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:

  • Is GC running more often than before?
  • Are there long Stop-The-World pauses?
  • Is heap usage stabilizing or slowly growing?
  • Did a code change actually improve anything?

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.

That is why I built gcscope: a terminal visualizer for Go's garbage collector.

It collects data from gctrace, gcpacertrace, and runtime/metrics, shows live terminal charts, lets you save snapshots, and compares different runs.

gcscope UI GC metrics, charts, and details about recent garbage collection cycles in one terminal window

In this article I will show:

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

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.

1. A few terms before we start

I do not want to turn this article into a deep dive into the entire Go runtime, but a few terms are useful.

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

In this article I only talk about what can be observed from the outside through these sources. gcscope 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.

A deeper article about GC internals and runtime metrics would be a separate topic. Here, the focus is practical observation and visualization.

2. Why gctrace and gcpacertrace are useful, but hard to use dynamically

If you enable gctrace, the Go runtime starts printing information about every garbage collection cycle: pauses, heap sizes, GC CPU usage, and other values.

If you add gcpacertrace, you also get data about the pacer, the component that regulates GC intensity.

This is useful data. But when you try to use it during a real application run, a few problems appear quickly.

It is hard to see trends over time

One line is readable. A hundred or two hundred lines become noise.

From raw logs, it is not easy to answer questions like:

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

The data is there, but the overall picture is not.

Before/after comparisons are painful

Imagine you changed GOGC, added a cache, rewrote an allocation-heavy path, or changed the workload. Now you want to know whether things got better or worse.

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.

For a one-off deep investigation, that is possible. For fast iteration, it is inconvenient.

Distributions matter more than single values

A single STW pause of 300 microseconds does not say much by itself. Context matters:

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

gctrace 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.

3. Seeing Go GC behavior in one minute with gcscope

You can install gcscope with go install:

go install github.com/timur-developer/gcscope/cmd/gcscope@latest

After that, you can use it as a regular CLI tool.

The fastest way to see the UI is to run a built-in demo workload:

gcscope lab churn

Running the builtin lab churn demo

The lab 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.

If you start gcscope 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.

4. What the UI shows and how to read it

It is easy to get distracted by charts and just stare at them. But gcscope becomes much more useful when you start with a question.

For example:

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

This turns the UI from a nice picture into an analysis tool.

General view of the gcscope interface

The interface has several main areas:

  1. Current Values: current GC cycle number, latest STW pause, heap live, heap goal.
  2. Information: summary for recent events: GC frequency, max STW, thresholds, environment, snapshot status.
  3. STW per cycle: STW pauses for individual GC cycles.
  4. Cycle Details: details for the selected GC event.
  5. Heap live over time: how live heap changes over time.
  6. STW p50/p99/max over time: how STW window statistics change over time.

How often GC runs

The Information panel shows GC frequency and average interval between cycles.

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.

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, GOGC, GOMEMLIMIT, and workload shape.

Where long STW pauses appear

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.

In gcscope, these areas are especially useful:

  1. last STW (us) in Current Values, which shows the STW pause of the latest GC cycle.
  2. STW p50/p99/max over time (us), which shows how typical and rare pauses changed over time.
  3. The per-cycle bar chart, which lets you inspect individual events.

The basic idea is simple: p50 shows the normal background level, while p99 and max help surface rare long pauses.

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?

Heap behavior: heap live vs heap goal

Heap size is rarely interesting by itself. The dynamic behavior matters more:

  • Is heap live growing over time or stabilizing?
  • How close is live heap to heap goal?
  • How does this change under different workloads?
  • What happens after code or runtime-setting changes?

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

In gcscope, this is visible in Current Values and on the Heap live over time chart.

What changed between two runs

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

gcscope gives you two ways to approach that:

  • visually compare behavior in the UI
  • save snapshots and compare them with diff

I will return to snapshots and diff later.

Minimal controls

For a first run, you only need a few keys:

  • ?, h, or f1: open help
  • space: pause or resume live updates
  • left / right: move through history when paused
  • s: save a snapshot
  • q or ctrl+c: quit

Animated demo of gcscope showing help, layout switching, chart zooming, pause mode, event history navigation, and snapshot saving

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

5. run mode: observing your own application without changing code

For most situations, I would start with run mode.

It starts your Go binary under observation and reads data that the runtime writes to stderr through gctrace and gcpacertrace.

Example:

gcscope run ./path/to/your-binary

There are two important details.

First, target is a path to an already compiled binary, not a .go file. So build your application first:

# replace ./cmd/myapp with the path to your application's main package
go build -o ./myapp ./cmd/myapp

Then run it through gcscope:

gcscope run ./myapp

Second, if your application needs arguments, use -- as a separator:

gcscope run ./myapp -- --config ./config.yaml --port 8080

Everything after -- is passed to your program unchanged. gcscope uses the separator to distinguish its own arguments from the target application's arguments.

6. Architecture: from stderr to TUI

At a high level, gcscope 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.

For run mode, the path looks like this:

Go binary
  -> stderr (gctrace/gcpacertrace)
  -> parser
  -> GC events
  -> latest N events
  -> statistics and chart data
  -> terminal UI, snapshots, and run comparison

Why run can see GC at all

For run mode to observe the garbage collector, the target process must output gctrace and gcpacertrace data.

gcscope does this automatically by configuring GODEBUG and adding gctrace=1 and gcpacertrace=1.

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

Here is the relevant code that builds the final GODEBUG value:

Code: building the final GODEBUG value
// internal/source/runner/runner.go
func NormalizeGODEBUG(value string) string {
    parts := strings.Split(value, ",")
    out := make([]string, 0, len(parts)+2)
    foundGctrace := false
    foundGcpacer := false

    for _, part := range parts {
        part = strings.TrimSpace(part)
        if part == "" {
            continue
        }

        switch {
        case strings.HasPrefix(part, "gctrace="):
            if !foundGctrace {
                out = append(out, "gctrace=1")
                foundGctrace = true
            }
        case strings.HasPrefix(part, "gcpacertrace="):
            if !foundGcpacer {
                out = append(out, "gcpacertrace=1")
                foundGcpacer = true
            }
        default:
            out = append(out, part)
        }
    }

    if !foundGctrace {
        out = append(out, "gctrace=1")
    }
    if !foundGcpacer {
        out = append(out, "gcpacertrace=1")
    }

    return strings.Join(out, ",")
}

So the user starts their binary through gcscope, and the tool creates the conditions needed for the runtime to expose GC data.

Why a GC event is not just one log line

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

For a minimal prototype, that is enough. But limitations appear quickly.

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

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

This is the parser entry point that separates GC lines from pacer lines:

Code: parsing GC and pacer trace lines
// internal/source/runner/parser.go
func (p *Parser) ParseLine(line string) (*domain.GCEvent, error) {
    trimmed := strings.TrimSpace(line)
    if trimmed == "" {
        return nil, nil
    }
    if strings.HasPrefix(trimmed, "gc ") {
        return p.parseGCLine(trimmed)
    }
    if strings.HasPrefix(trimmed, "pacer:") {
        return nil, p.parsePacerLine(trimmed)
    }
    return nil, nil
}

func (p *Parser) Flush() *domain.GCEvent {
    if p.current == nil {
        return nil
    }
    event := p.current
    p.current = nil
    return event
}

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

So I did not bind the UI directly to gctrace strings. Regular expressions can exist inside the parser, but the parser should return proper GC events.

The model became simple:

logs and metrics -> GC events -> aggregates -> charts, snapshots, diff

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

How data reaches the UI

For the terminal interface, I use the message model from Bubble Tea, a Go library for building TUI applications.

Once a GC event is produced, gcscope sends it into the Bubble Tea model:

Code: sending GC events into the TUI model
// cmd/gcscope/lab.go, similar in run.go
go func() {
    for ev := range r.Events() {
        prog.Send(ui.GCEventMsg{
            Event: ev,
            At:    time.Now(),
        })
    }
}()

When the UI receives a GC event, it updates the sliding window, recalculates aggregates, and refreshes chart history:

Code: updating aggregates and chart history
// internal/ui/model_update.go
case GCEventMsg:
    m.lastUpdate = msg.At
    m.now = msg.At

    m.store.Add(msg.Event)                         // sliding window of recent events
    m.agg = domain.ComputeAggregates(m.store.Recent()) // aggregate calculation
    m.pushHistory(msg.At)                          // history for charts

    if !m.paused {
        m.cursor = m.currentWindowLen() - 1
    }
    return m, nil

The UI works with events, not raw log strings.

The UI works with a GCEvent model that contains the fields needed for charts, snapshots, and comparisons:

Code: GCEvent fields used by the UI
// internal/domain/events.go
type GCEvent struct {
    GCNum           int
    TimeSinceStartS float64
    GCCPUPercent    float64

    HeapStartMB int
    HeapEndMB   int
    HeapLiveMB  int
    HeapGoalMB  int

    // ...
    // other fields from gctrace/runtime metrics
}

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

7. attach mode: observing an already running application

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

The flow is:

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

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

reporter.New() returns an object with two methods:

  • Path(): the endpoint path, /gcscope/metrics by default
  • Handler(): an HTTP handler that returns runtime/metrics data in JSON format

Minimal example:

package main

import (
    "log"
    "net/http"

    "github.com/timur-developer/gcscope/pkg/reporter"
)

func main() {
    rep := reporter.New()

    mux := http.NewServeMux()
    mux.Handle(rep.Path(), rep.Handler())

    log.Fatal(http.ListenAndServe(":8080", mux))
}

Then attach gcscope to the endpoint. If the service runs locally:

gcscope attach http://127.0.0.1:8080/gcscope/metrics

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

If you want to look deeper, the implementation and package README are in the repository: pkg/reporter.

8. run vs attach: why have both?

The two modes solve a similar problem: observing garbage collector behavior. But they get data differently.

  • run parses gctrace / gcpacertrace output from the target process's stderr
  • attach reads runtime/metrics through an HTTP endpoint

This leads to two important differences.

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

Second, values in attach and run do not have to match one-to-one. They come from different data sources, with different precision and semantics.

In practice, the choice depends on the task:

  • If you want the closest view to gctrace for individual GC cycles and STW pauses, start with run.
  • If you want to connect to an already running process through an endpoint, use attach.

9. Data storage, snapshots, and diff

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

Code: configuring the GC event window
// cmd/gcscope/run.go: pass the window size from config (--window-size)
model := ui.NewModel(ctx, cancel, cfg.WindowSize, snapshotDir, writer, stwTh, envInfo)

// internal/ui/model_types.go: inside the model, create a store with the latest N events
store: domain.NewStore(windowSize),

This is intentional.

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.

A sliding window helps:

  • keep the UI responsive
  • recalculate p50/p99/max quickly
  • show what the garbage collector is doing recently

A snapshot in gcscope is a JSON file that saves the current observation window.

Here is what part of a snapshot file looks like:

{
  "version": 1,
  "current": {
    "gc_cycles_total": 16,
    "last_stw_us": 0,
    "heap_live_mb": 59,
    "heap_goal_mb": 166
  },
  "window": {
    "stw_p50_us": 0,
    "stw_p99_us": 550,
    "stw_max_us": 550
  },
  "events": [
    {
      "gc_num": 1,
      "time_since_start_s": 0.08,
      "heap_live_mb": 3,
      "heap_goal_mb": 4
    }
  ]
}

It contains:

  • current values such as gc_cycles_total, last_stw_us, heap_live_mb, heap_goal_mb
  • window statistics such as stw_p50_us, stw_p99_us, stw_max_us
  • the list of recent GC events from the same window used by the UI

In practice, snapshots are useful after runs you want to compare:

  • before and after an optimization
  • before and after changing GOGC
  • before and after deploying a new service version
  • under different workload scenarios

Comparison is done with gcscope diff. The first argument is the "before" snapshot, and the second is the "after" snapshot.

gcscope diff ./before.json ./after.json

For example:

gcscope diff \
  gcscope/tmp/snapshots/gcscope-snapshot-2026-05-28T15-14-22.json \
  gcscope/tmp/snapshots/gcscope-snapshot-2026-05-28T15-16-58.json

diff compares the main heap values and STW window statistics, then prints the difference as B - A.

Example output:

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

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?

10. Where gcscope is especially useful

I see gcscope as a quick first step when investigating problems that might be related to GC.

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

gcscope helps check one hypothesis quickly: what was the garbage collector doing at that moment?

It shows GC behavior over time:

  • how often GC runs
  • what happens to the heap
  • whether long STW pauses appear
  • whether behavior changed after code or runtime-setting changes

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

11. Trying gcscope on your own project

The simplest way to try gcscope on your project is:

  1. Install gcscope.
  2. Run the built-in lab churn workload to understand the UI.
  3. Build your Go application or service as a binary.
  4. Run it with gcscope run under a realistic workload.
  5. Save a snapshot with s.
  6. Repeat the run after changing code or runtime settings and save a second snapshot.
  7. Compare both snapshots with gcscope diff.

Minimal command set:

go install github.com/timur-developer/gcscope/cmd/gcscope@latest

gcscope lab churn

# replace ./cmd/myapp with the path to your application's main package
go build -o ./myapp ./cmd/myapp

gcscope run ./myapp

gcscope diff ./before.json ./after.json

The project code, installation instructions, and documentation for gcscope are here:

https://github.com/timur-developer/gcscope

If the tool looks useful, I would really appreciate a GitHub star, feedback in GitHub Issues, or your thoughts in the comments.

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