🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsHeader and Footer not showing in Excel(14.09.2026 um 22:43 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)
🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsHeader and Footer not showing in Excel(14.09.2026 um 22:43 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)

🔧 Programmierung 🕛 vor 3 Monaten 19 Min Lesezeit
0

I Built a Visualizer for Go's Garbage Collector

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

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.



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





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.





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:




CODE
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:




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






Then run it through gcscope:




CODE
gcscope run ./myapp






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




CODE
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:




CODE
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




CODE
// 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




CODE
// 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:




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






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




CODE
// 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:




CODE
{
"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.




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






For example:




CODE
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:




CODE
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:




CODE
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?

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
The Gemini desktop app is now available for Windows
1 Quelle
Header and Footer not showing in Excel
1 Quelle
Burn Out, Or Fade Away
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten I Built a Visualizer for Go's Garbage Collector

Thematisch verwandte Begriffe: Built, Visualizer, Garbage, Collector · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...