Skip to content

Commit

Permalink
feat: add heapview
Browse files Browse the repository at this point in the history
  • Loading branch information
burntcarrot committed Sep 27, 2023
0 parents commit a44851c
Show file tree
Hide file tree
Showing 12 changed files with 554 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Lint
on:
push:
pull_request:

permissions:
contents: read

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
heapdump*
heapview*
dist/*
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# heapview

A tiny, experimental heap dump viewer for Go heap dumps. (for heap dumps produced by `debug.WriteHeapDump()`)

Tested on Go 1.21.0.

## Installation

The easiest way to get started is to install `heapview` by downloading the [releases](https://github.com/burntcarrot/heapview/releases).

## Usage

```sh
heapview -file=<heapdump_path>
```

On running `heapview`, the server would serve the HTML view at `localhost:8080`:

![Records View](./static/records-view.png)

Graph view:

![Graph View](./static/graph-view.png)

## Future work

`heapview` is a small tool, but can be improved with the following features:

- a good, responsive Object Graph viewer, which could redirect to the record on interactions with the nodes
- a way to extract type information from the heap dumps
- an easier way to be in sync with the Go runtime changes

If you'd like to contribute to the following, please consider raising a pull request!

## Acknowledgements

- https://github.com/golang/go/wiki/heapdump15-through-heapdump17, which documents the current Go heap dump format. (and was the main reference while I was building [heaputil](https://github.com/burntcarrot/heaputil))
- https://github.com/golang/go/issues/16410, the Go heap dump viewer proposal
- https://github.com/adamroach/heapspurs, which aims to provide a set of utilities to play around with the Go heap dump.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/burntcarrot/heapview

go 1.21.0

require github.com/burntcarrot/heaputil v0.0.0-20230927162808-497024fb706a
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/burntcarrot/heaputil v0.0.0-20230927162808-497024fb706a h1:w1S7K4+qL19+czjhyWrgyc8QmaseY1f3mkP4YPcAdFM=
github.com/burntcarrot/heaputil v0.0.0-20230927162808-497024fb706a/go.mod h1:LwbcObA3AsK5SN1Bet7dQjOkxuKqJ/B0iM0Qn6VdxPk=
16 changes: 16 additions & 0 deletions goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
project_name: heapview

builds:
- id: "heapview"
binary: heapview
goos:
- linux
- darwin
- windows
- openbsd
goarch:
- amd64
- arm64
mod_timestamp: '{{ .CommitTimestamp }}'
env:
- CGO_ENABLED=0
132 changes: 132 additions & 0 deletions html.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package main

import (
"bufio"
"fmt"
"html/template"
"regexp"
"strings"

"github.com/burntcarrot/heaputil"
"github.com/burntcarrot/heaputil/record"
)

type templateData struct {
RecordTypes []RecordInfo
Records []heaputil.RecordData
GraphVizContent string
}

func GenerateHTML(records []heaputil.RecordData, graphContent string) (string, error) {
tmpl, err := template.ParseFiles("index.html")
if err != nil {
return "", err
}

data := templateData{
RecordTypes: GetUniqueRecordTypes(records),
Records: records,
GraphVizContent: graphContent,
}

var htmlBuilder strings.Builder
err = tmpl.Execute(&htmlBuilder, data)
if err != nil {
return "", err
}

return htmlBuilder.String(), nil
}

func GenerateGraph(rd *bufio.Reader) (string, error) {
err := record.ReadHeader(rd)
if err != nil {
return "", err
}

var dotContent strings.Builder

// Write DOT file header
dotContent.WriteString("digraph GoHeapDump {\n")

// Create the "heap" node as a cluster
dotContent.WriteString(" subgraph cluster_heap {\n")
dotContent.WriteString(" label=\"Heap\";\n")
dotContent.WriteString(" style=dotted;\n")

var dumpParams *record.DumpParamsRecord
counter := 0

for {
r, err := record.ReadRecord(rd)
if err != nil {
return dotContent.String(), err
}

_, isEOF := r.(*record.EOFRecord)
if isEOF {
break
}

dp, isDumpParams := r.(*record.DumpParamsRecord)
if isDumpParams {
dumpParams = dp
}

// Filter out objects. If the record isn't of the type Object, ignore.
_, isObj := r.(*record.ObjectRecord)
if !isObj {
continue
}

// Create a DOT node for each record
nodeName := fmt.Sprintf("Node%d", counter)
counter++
name, address := ParseNameAndAddress(r.Repr())
nodeLabel := fmt.Sprintf("[%s] %s", name, address)

// Write DOT node entry within the "heap" cluster
s := fmt.Sprintf(" %s [label=\"%s\"];\n", nodeName, nodeLabel)
dotContent.WriteString(s)

// Check if the record has pointers
p, isParent := r.(record.ParentGuard)
if isParent {
_, outgoing := record.ParsePointers(p, dumpParams)
for i := 0; i < len(outgoing); i++ {
if outgoing[i] != 0 {
childNodeName := fmt.Sprintf("Pointer0x%x", outgoing[i])

// Create an edge from the current record to the child record
s := fmt.Sprintf(" %s -> %s;\n", nodeName, childNodeName)
dotContent.WriteString(s)
}
}
}
}

// Close the "heap" cluster
dotContent.WriteString(" }\n")

// Write DOT file footer
dotContent.WriteString("}\n")

return dotContent.String(), nil
}

func ParseNameAndAddress(input string) (name, address string) {
// Define a regular expression pattern to match the desired format
// The pattern captures the node name (before " at address") and the address.
re := regexp.MustCompile(`^(.*?) at address (0x[0-9a-fA-F]+).*?$`)

// Find the submatches in the input string
matches := re.FindStringSubmatch(input)

// If there are no matches, return empty strings for both name and address
if len(matches) != 3 {
return "", ""
}

// The first submatch (matches[1]) contains the node name, and the second submatch (matches[2]) contains the address.
return matches[1], matches[2]
}
Loading

0 comments on commit a44851c

Please sign in to comment.