Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ Multiple variables can share the same offset when their lifetimes do not overlap

Spilling of unamed values, such as intermediate values of an expression does not appear in this output (at this this). Such spilled values can share stack slots with named variables and may use gaps in offsets that are shown.

### Inspecting in a web UI

The `web` (alias `w`) sub-command starts a local web server for exploring stack usage interactively.

```
$ stackwhere web {path to .o}
Serving stackwhere web UI on http://127.0.0.1:8080
```

Use `--source-dir` to limit which directories can be read by the `/source` endpoint. The flag is repeatable.

```
$ stackwhere web {path to .o} --source-dir ./bpf --source-dir ./common
```

If no `--source-dir` is set, stackwhere defaults to the directory containing the collection file.

## Tips for reducing stack usage

The limiting factor for a BPF program is the **peak** memory usage, some ideas to reduce peak memory usage are:
Expand Down
194 changes: 113 additions & 81 deletions cmd/stackwhere/lifetimes.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ type StackLifetime struct {
lifetime *Lifetime
}

type bpfFn struct {
fn *btf.Func
insns asm.Instructions
}

type lifetimesCmd struct {
// Dump visitor state, reachable writes, reachable reads, and discovered lifetimes
debug *bool
Expand All @@ -129,12 +134,10 @@ type lifetimesCmd struct {
drawBBB *bool
}

func (lc *lifetimesCmd) run(cmd *cobra.Command, args []string) error {
dbgWriter := cmd.OutOrStdout()

spec, err := ebpf.LoadCollectionSpec(args[0])
func loadCollectionFunctions(collectionPath string) (*ebpf.CollectionSpec, map[string]bpfFn, error) {
spec, err := ebpf.LoadCollectionSpec(collectionPath)
if err != nil {
return fmt.Errorf("load ebpf collection: %w", err)
return nil, nil, fmt.Errorf("load ebpf collection: %w", err)
}

fns := make(map[string]bpfFn)
Expand All @@ -159,65 +162,13 @@ func (lc *lifetimesCmd) run(cmd *cobra.Command, args []string) error {
}
}

fn := fns[args[1]]
if fn.fn == nil {
return fmt.Errorf("program %s not found in collection", args[1])
}

blocks, err := analyze.MakeBlocks(fn.insns)
if err != nil {
return fmt.Errorf("make blocks: %w", err)
}

if *lc.dumpInstructions || *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "=== Program instructions ===")
_, _ = fmt.Fprint(dbgWriter, blocks.Dump(fn.insns))
}

visitor := &visitor{
dbgWriter: io.Discard,
spec: spec,
insns: fn.insns,
reachableWrites: make(map[*analyze.Block][]rw),
inStates: make(map[*analyze.Block]state),
outStates: make(map[*analyze.Block]state),
seenReads: make(map[rw]struct{}),
seenWrites: make(map[rw]struct{}),
}
if *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "=== Visitor state ===")
visitor.dbgWriter = dbgWriter
}
visitor.visit(blocks[0], state{stack: make(map[int16]stackState)})

if visitor.inaccurate {
_, _ = fmt.Fprintln(dbgWriter, "WARN: analysis may be inaccurate due to unhandled instructions or unknown helper functions, see debug log for details")
}

if *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "\n=== Reachable writes per block ===")
for _, block := range blocks {
_, _ = fmt.Fprintf(dbgWriter, "%d:\n", block.ID)
for _, write := range visitor.reachableWrites[block] {
_, _ = fmt.Fprintf(dbgWriter, "\tBlock: %d, Offset: %d, RawIns: %d\n", write.Block.ID, write.Offset, write.RawIns)
}
}

_, _ = fmt.Fprintln(dbgWriter, "\n=== Reads in program ===")
for _, read := range visitor.reads {
_, _ = fmt.Fprintf(dbgWriter, "\tBlock: %d, Offset: %d, RawIns: %d\n", read.Block.ID, read.Offset, read.RawIns)
}
}
return spec, fns, nil
}

func computeStackLifetimes(blocks analyze.Blocks, insns asm.Instructions, reachableWrites map[*analyze.Block][]rw, reads []rw) []StackLifetime {
var results []StackLifetime

if *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "\n=== Lifetime discovery ===")
}
for _, read := range visitor.reads {
if *lc.debug {
_, _ = fmt.Fprintf(dbgWriter, "Finding lifetimes for %d at %d\n", read.Offset, read.RawIns)
}
for _, read := range reads {
lt := &Lifetime{}

visited := make(map[*analyze.Block]bool)
Expand All @@ -229,13 +180,12 @@ func (lc *lifetimesCmd) run(cmd *cobra.Command, args []string) error {
visited[block] = true

var reachableWritesForOffset []rw
for _, write := range visitor.reachableWrites[block] {
for _, write := range reachableWrites[block] {
if write.Offset == read.Offset {
reachableWritesForOffset = append(reachableWritesForOffset, write)
}
}

// No more reachable writes for this offset, no point in visiting further predecessors
if len(reachableWritesForOffset) == 0 {
return
}
Expand All @@ -250,30 +200,21 @@ func (lc *lifetimesCmd) run(cmd *cobra.Command, args []string) error {
return cmp.Compare(a.RawIns, b.RawIns)
})

// If there is a write in the current block
if len(writesInCur) > 0 {
// if the current block is the same block as the read, add a single interval from the write to the read
if block == read.Block {
for _, write := range slices.Backward(writesInCur) {
if write.RawIns < read.RawIns {
lt.Add(LTInterval(write.RawIns, read.RawIns, false))

// No need to explore further, we have found the last write before the read
return
}
}
} else {
lastWrite := writesInCur[len(writesInCur)-1]
// From last write to end of block
lt.Add(LTInterval(lastWrite.RawIns, insRawOff(block, fn.insns, block.End), false))
// From start of block to end of block for all blocks in the stack
lt.Add(LTInterval(lastWrite.RawIns, insRawOff(block, insns, block.End), false))
for _, b := range slices.Backward(stack) {
lt.Add(LTInterval(b.Raw, insRawOff(b, fn.insns, b.End), true))
lt.Add(LTInterval(b.Raw, insRawOff(b, insns, b.End), true))
}
// From start of block to read in the read block
lt.Add(LTInterval(read.Block.Raw, read.RawIns, true))

// No need to explore further, we have found the last write before the read
return
}
}
Expand All @@ -289,10 +230,6 @@ func (lc *lifetimesCmd) run(cmd *cobra.Command, args []string) error {
offset: read.Offset,
lifetime: lt,
})

if *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, lt)
}
}
}

Expand Down Expand Up @@ -340,7 +277,102 @@ func (lc *lifetimesCmd) run(cmd *cobra.Command, args []string) error {
slt.lifetime.Intervals = slices.Delete(slt.lifetime.Intervals, i, i+1)
}
}
if *lc.debug {
}

return results
}

func buildProgramLifetimeGraph(spec *ebpf.CollectionSpec, insns asm.Instructions, drawBBB bool) (string, error) {
if len(insns) == 0 {
return "", nil
}

blocks, err := analyze.MakeBlocks(insns)
if err != nil {
return "", fmt.Errorf("make blocks: %w", err)
}
if len(blocks) == 0 {
return "", nil
}

visitor := &visitor{
dbgWriter: io.Discard,
spec: spec,
insns: insns,
reachableWrites: make(map[*analyze.Block][]rw),
inStates: make(map[*analyze.Block]state),
outStates: make(map[*analyze.Block]state),
seenReads: make(map[rw]struct{}),
seenWrites: make(map[rw]struct{}),
}
visitor.visit(blocks[0], state{stack: make(map[int16]stackState)})

results := computeStackLifetimes(blocks, insns, visitor.reachableWrites, visitor.reads)
return graphLifetimes(results, blocks, insns, visitor.writes, visitor.reads, drawBBB), nil
}

func (lc *lifetimesCmd) run(cmd *cobra.Command, args []string) error {
dbgWriter := cmd.OutOrStdout()

spec, fns, err := loadCollectionFunctions(args[0])
if err != nil {
return err
}

fn := fns[args[1]]
if fn.fn == nil {
return fmt.Errorf("program %s not found in collection", args[1])
}

blocks, err := analyze.MakeBlocks(fn.insns)
if err != nil {
return fmt.Errorf("make blocks: %w", err)
}

if *lc.dumpInstructions || *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "=== Program instructions ===")
_, _ = fmt.Fprint(dbgWriter, blocks.Dump(fn.insns))
}

visitor := &visitor{
dbgWriter: io.Discard,
spec: spec,
insns: fn.insns,
reachableWrites: make(map[*analyze.Block][]rw),
inStates: make(map[*analyze.Block]state),
outStates: make(map[*analyze.Block]state),
seenReads: make(map[rw]struct{}),
seenWrites: make(map[rw]struct{}),
}
if *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "=== Visitor state ===")
visitor.dbgWriter = dbgWriter
}
visitor.visit(blocks[0], state{stack: make(map[int16]stackState)})

if visitor.inaccurate {
_, _ = fmt.Fprintln(dbgWriter, "WARN: analysis may be inaccurate due to unhandled instructions or unknown helper functions, see debug log for details")
}

if *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "\n=== Reachable writes per block ===")
for _, block := range blocks {
_, _ = fmt.Fprintf(dbgWriter, "%d:\n", block.ID)
for _, write := range visitor.reachableWrites[block] {
_, _ = fmt.Fprintf(dbgWriter, "\tBlock: %d, Offset: %d, RawIns: %d\n", write.Block.ID, write.Offset, write.RawIns)
}
}

_, _ = fmt.Fprintln(dbgWriter, "\n=== Reads in program ===")
for _, read := range visitor.reads {
_, _ = fmt.Fprintf(dbgWriter, "\tBlock: %d, Offset: %d, RawIns: %d\n", read.Block.ID, read.Offset, read.RawIns)
}
}

results := computeStackLifetimes(blocks, fn.insns, visitor.reachableWrites, visitor.reads)
if *lc.debug {
_, _ = fmt.Fprintln(dbgWriter, "\n=== Lifetime discovery ===")
for _, slt := range results {
_, _ = fmt.Fprintf(dbgWriter, "%d : %v\n", slt.offset, slt.lifetime)
}
}
Expand Down Expand Up @@ -1402,7 +1434,7 @@ func graphLifetimes(lifetimes []StackLifetime, blocks analyze.Blocks, insns asm.
}
cx := marginLeft + int(w.RawIns)*cellW + cellW/2
cy := marginTop + rowIdx*rowH + rowH/2
fmt.Fprintf(&sb, `<circle cx="%d" cy="%d" r="4" fill="black"/>`, cx, cy)
fmt.Fprintf(&sb, `<circle class="lifetime-dot lifetime-dot-write" data-raw="%d" cx="%d" cy="%d" r="4" fill="black"/>`, w.RawIns, cx, cy)
}

// Read markers: white circles with black stroke.
Expand All @@ -1413,7 +1445,7 @@ func graphLifetimes(lifetimes []StackLifetime, blocks analyze.Blocks, insns asm.
}
cx := marginLeft + int(r.RawIns)*cellW + cellW/2
cy := marginTop + rowIdx*rowH + rowH/2
fmt.Fprintf(&sb, `<circle cx="%d" cy="%d" r="4" fill="white" stroke="black" stroke-width="1.5"/>`, cx, cy)
fmt.Fprintf(&sb, `<circle class="lifetime-dot lifetime-dot-read" data-raw="%d" cx="%d" cy="%d" r="4" fill="white" stroke="black" stroke-width="1.5"/>`, r.RawIns, cx, cy)
}

// Border around the chart area.
Expand Down
Loading