An interactive race condition visualizer for Go.
The Go race detector tells you that a race happened and which lines are involved — but the output is a wall of text. When multiple goroutines are racing across several functions, reconstructing the timeline mentally is slow and error-prone.
racevis runs your tests with the race detector, captures the Go scheduler trace, and renders an animated ECG-style timeline showing exactly which goroutines collided, on which memory address, and at what moment in time. It also suggests idiomatic Go fixes for each race.
racevis runs two passes against your package and joins them into a visual timeline:
Your Go package
│
├── go test -race ./... ← Pass 1: race detection
│ │
│ ▼
│ WARNING: DATA RACE blocks
│ │
│ ▼
│ parser/race.go ← state machine parser → []RaceEvent
│
├── go test -trace=file.out . ← Pass 2: scheduler trace
│ │
│ ▼
│ go tool trace -d=parsed
│ │
│ ▼
│ parser/trace.go ← event parser → []SchedulerEvent
│
└── source/reader.go ← reads source files → []Snippet
│
▼
correlator.Correlate() ← joins by goroutine ID
│
▼
Timeline JSON ──────────▶ browser (http://localhost:7777)
Pass 1 instruments every memory access. When two goroutines touch the same address without synchronization, the race detector emits a WARNING: DATA RACE block — racevis parses this into structured RaceEvent objects with full stack traces.
Pass 2 captures every goroutine state transition (running → waiting → runnable) with nanosecond timestamps. The correlator joins both datasets on goroutine ID to place red collision zones at the exact moment the race occurred on the timeline.
racevis is not a general test explorer. The UI renders a test group only when:
- a race event stack involved that
TestXxx, or - the runtime trace has a goroutine creation stack containing that
TestXxx.
Synchronous tests with no race event and no trace-attributed goroutine lanes are intentionally omitted. This keeps the UI focused on concurrency evidence.
Each goroutine is a horizontal line. A pulse moves left to right as time advances. When two goroutines touch the same memory address simultaneously, the line turns red at that exact moment. Hover a collision zone for a plain-English explanation. Click it to open the source panel.
Browse all detected races as structured cards — each showing the two goroutines involved, their access type (READ/WRITE), the memory address they contested, and a suggested fix.
Opens when you click a collision zone or race card. Shows the racing lines of code side by side with the exact race line highlighted in red, plus copy-paste ready fix suggestions.
go install github.com/bramakrishna16/racevis@latestOr build from source:
git clone https://github.com/bramakrishna16/racevis
cd racevis
go build -o racevis .
mv racevis /usr/local/bin/ # optional: add to PATHRequirements: Go 1.22 or later. Your project must have tests that exercise concurrent code.
go run .
# Open http://localhost:7777racevis -target ./path/to/packageracevis -target ./path/to/package -run TestMyRaceracevis -target ./path/to/package -count 3racevis -target ./path/to/package -watch
# Browser updates automatically when you save a .go fileracevis -target ./path/to/package -report report.html
open report.html # works offline, no server neededracevis -target ./path/to/package -json timeline.json
racevis -target ./path/to/package -no-server # prints to stdoutracevis -target ./path/to/package -dwarf
# Shows "variable counter" instead of "0x00c000112198" in the UI-target Go package directory to analyze (default: bundled demo)
-run Run only tests matching this regex, e.g. -run TestMyRace
-count Number of race detector passes (default 1, higher catches more races)
-watch Re-analyze on every .go file change, auto-refresh browser
-report Write a self-contained HTML report to this file
-json Write timeline JSON to this file
-no-server Print timeline JSON to stdout
-port Web UI port (default 7777, tries next available if taken)
-dwarf Resolve memory addresses to variable names (experimental)
The left panel lists visualized tests, not every Go test. A test appears when it was involved in a race event or spawned goroutines visible in the runtime trace. The dropdown at the top lets you filter by a single visualized test.
Each race card shows:
- Which goroutines were involved and what operation each performed
- The memory address that was contested
- A suggested fix with copy-paste ready code (click the ▸ triangle to expand)
- Green line — goroutine running or runnable on a CPU core
- Flat baseline — goroutine blocked (channel, mutex, sleep, syscall)
- Red vertical markers — collision zone boundaries
- ⚡ #N label — which race event this collision belongs to
Each card shows one race: the two goroutines, their access type (READ/WRITE), the function and file:line for each side, and the fix suggestion. Click any card to open the source panel.
Shows the racing lines of code — the goroutine that wrote on the left, the one that read on the right. The exact racing line is highlighted in red. Expand the suggested fix block for copy-paste ready Go code.
racevis can only find races in code paths exercised by your tests. A minimal concurrent test is enough:
func TestMyConcurrentCode(t *testing.T) {
svc := NewMyService()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
svc.DoSomething()
}()
}
wg.Wait()
}The test doesn't need assertions — its purpose is to drive concurrent execution so the race detector can observe it. Run racevis -count 3 to repeat multiple times and catch races that only manifest under specific scheduling.
| Pattern | Detected by | Fix |
|---|---|---|
counter++ / counter-- |
++ or -- on racing line |
sync/atomic |
| Concurrent map access | runtime.mapassign in stack |
sync.RWMutex |
| Lazy singleton init | == nil check on racing line |
sync.Once |
| Loop variable capture | Closure over loop variable | Pass by value: go func(i int){}(i) |
| Struct field access | .field = on racing line |
sync.Mutex embedded in struct |
| Slice append | append( on racing line |
Mutex or channel-based fan-in |
racevis/
├── main.go Entry point — CLI flags, orchestration, analysis pipeline
├── dwarf.go Optional DWARF variable name resolution (-dwarf flag)
├── runner/ Executes go test and go tool trace, handles retry logic
├── parser/
│ ├── race.go State machine parser for race detector output
│ └── trace.go Parser for Go scheduler trace events
├── correlator/ Joins race events + trace into a Timeline struct
├── source/ Reads source files, extracts snippets around race sites
├── server/ HTTP server: /api/timeline, /api/events (SSE), static UI
├── ui/index.html Single-file vanilla JS frontend (embedded into binary)
├── testdata/racy/ Demo package with intentional race conditions
│ ├── main.go Four classic race patterns (counter, map, init, closure)
│ ├── main_test.go Test entry points for the classic patterns
│ └── broker_test.go Realistic task-queue races and a safe comparison
├── docs/
│ ├── DESIGN.md Full architecture document
│ ├── assets/ Screenshots and demo GIF
│ └── adr/ 15 Architecture Decision Records
└── scripts/
└── install-hooks.sh Installs the pre-commit lint hook
The binary is fully self-contained — ui/index.html is embedded at compile time via go:embed, so it works from any directory with no assets on disk.
See CONTRIBUTING.md for setup, testing, and coding conventions.
git clone https://github.com/bramakrishna16/racevis
cd racevis
go test ./... # run all unit tests
bash scripts/install-hooks.sh # install pre-commit lint hook
go run . # run against the bundled demo- The left panel is not a full test list. Tests with no race event and no trace-visible goroutine creation stack are omitted by design.
- Collision zone placement is approximate. The race detector fires after the fact — racevis places the red zone at the nearest overlapping running windows of the two goroutines. See ADR-006.
-tracecovers the root package only.go test -tracedoes not support./.... Goroutines from sub-packages appear only if invoked from the root package's tests. See ADR-008.- DWARF resolution is best-effort. For maps, slices, and interfaces, only the container variable name is shown. Stack-allocated variables may not be resolvable if inlined or optimized away.
- Race detection is probabilistic. Different runs may catch different races. Use
-count Nto increase coverage.
MIT


