Skip to content

Commit

Permalink
callstack: Use PC instead of runtime.Frame
Browse files Browse the repository at this point in the history
This is really just to make Frame resolution lazy
  • Loading branch information
thatguystone committed Jan 30, 2024
1 parent 9247e7e commit d4bd801
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 38 deletions.
97 changes: 79 additions & 18 deletions callstack/frame.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,96 @@
package callstack

import (
"fmt"
"path/filepath"
"runtime"

"github.com/thatguystone/cog/assert"
"strings"
)

// Frame wraps [runtime.Frame] with extra functionality
type Frame struct {
runtime.Frame
}
// Raw program counter wrapper
type PC uintptr

// Self gets the Frame of the caller
func Self() Frame {
// Self gets the PC of the caller
func Self() PC {
return Caller(1)
}

// Caller gets the Frame of the caller after skipping the given number of frames
func Caller(skip int) Frame {
// Caller gets the PC of the caller after skipping the given number of frames
func Caller(skip int) PC {
var pcs [1]uintptr
n := runtime.Callers(skip+2, pcs[:]) // runtime.Callers + Self()
assert.True(n > 0)
runtime.Callers(skip+2, pcs[:]) // runtime.Callers + Self()
return PC(pcs[0])
}

func (pc PC) Frame() Frame {
pcs := [1]uintptr{uintptr(pc)}
frames := runtime.CallersFrames(pcs[:])
frame, _ := frames.Next()
return Frame{frame}
}

// Frame wraps [runtime.Frame] with extra functionality
type Frame struct {
f runtime.Frame
}

// PC gets the raw program counter
func (frame Frame) PC() uintptr {
return frame.f.PC
}

// Func gets the fully-qualified name of the function
func (frame Frame) Func() string {
name := frame.f.Function
if name == "" {
return "???"
}

return name
}

// FuncName gets the non-qualified name of the function.
func (fr Frame) FuncName() string {
_, funcName := fr.PkgAndFunc()
func (frame Frame) FuncName() string {
_, funcName := frame.PkgAndFunc()
return funcName
}

// PkgPath gets the name of the package the frame belongs to
func (fr Frame) PkgPath() string {
pkgPath, _ := fr.PkgAndFunc()
func (frame Frame) PkgPath() string {
pkgPath, _ := frame.PkgAndFunc()
return pkgPath
}

func (fr Frame) PkgAndFunc() (pkgPath string, funcName string) {
// File gets the path and file name of this Frame
func (frame Frame) File() string {
file := frame.f.File
if file == "" {
return "???"
}

return file
}

// FileName gets the file name of this Frame
func (frame Frame) FileName() string {
file := frame.File()
return filepath.Base(file)
}

// Line gets the line number of this Frame
func (frame Frame) Line() int {
return frame.f.Line
}

// PkgAndFunc gets the name of the package this frame belongs to and the
// non-qualified name of the function in the package.
func (frame Frame) PkgAndFunc() (pkgPath string, funcName string) {
name := frame.f.Function
if name == "" {
return "???", "???"
}

// Borrowed from [runtime.funcpkgpath]
name := fr.Function
i := len(name) - 1
for ; i > 0; i-- {
if name[i] == '/' {
Expand All @@ -55,3 +104,15 @@ func (fr Frame) PkgAndFunc() (pkgPath string, funcName string) {
}
return name[:i], name[i+1:]
}

func (frame Frame) append(b *strings.Builder) {
fmt.Fprintf(b, "%s()\n", frame.Func())
fmt.Fprintf(b, "\t%s:%d\n", frame.File(), frame.Line())
}

// String implements [fmt.Stringer]
func (frame Frame) String() string {
var b strings.Builder
frame.append(&b)
return b.String()
}
50 changes: 41 additions & 9 deletions callstack/frame_test.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,70 @@
package callstack

import (
"strings"
"testing"

"github.com/thatguystone/cog/check"
)

const pkgPath = "github.com/thatguystone/cog/callstack"
const (
pkgPath = "github.com/thatguystone/cog/callstack"
fileName = "frame_test.go"
)

func TestSelfFunc(t *testing.T) {
c := check.NewT(t)

fr := Self()
fr := Self().Frame()

const funcName = "TestSelfFunc"
c.Equal(fr.Function, pkgPath+"."+funcName)
c.Equal(fr.FuncName(), funcName)
c.NotEqual(fr.PC(), 0)
c.Equal(fr.PkgPath(), pkgPath)
c.Equal(fr.Func(), pkgPath+"."+funcName)
c.Equal(fr.FuncName(), funcName)
c.True(strings.Contains(fr.File(), fileName))
c.Equal(fr.FileName(), fileName)
c.NotEqual(fr.Line(), 0)
}

func TestSelfMethod(t *testing.T) {
c := check.NewT(t)

fr := testSelf{}.getFrame()
fr := testSelf{}.getPC().Frame()

const funcName = "testSelf.getFrame"
c.Equal(fr.Function, pkgPath+"."+funcName)
c.Equal(fr.FuncName(), funcName)
const funcName = "testSelf.getPC"
c.NotEqual(fr.PC(), 0)
c.Equal(fr.PkgPath(), pkgPath)
c.Equal(fr.Func(), pkgPath+"."+funcName)
c.Equal(fr.FuncName(), funcName)
c.True(strings.Contains(fr.File(), fileName))
c.Equal(fr.FileName(), fileName)
c.NotEqual(fr.Line(), 0)
}

func TestPCZero(t *testing.T) {
c := check.NewT(t)

var pc PC
fr := pc.Frame()
c.Equal(fr.PkgPath(), "???")
c.Equal(fr.Func(), "???")
c.Equal(fr.FuncName(), "???")
c.Equal(fr.File(), "???")
c.Equal(fr.FileName(), "???")
c.Equal(fr.Line(), 0)
}

func TestFrameString(t *testing.T) {
c := check.NewT(t)

str := Self().Frame().String()
c.True(strings.Contains(str, fileName))
}

type testSelf struct{}

func (testSelf) getFrame() Frame {
func (testSelf) getPC() PC {
return Self()
}

Expand Down
9 changes: 3 additions & 6 deletions callstack/stack.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package callstack

import (
"fmt"
"iter"
"runtime"
"strings"
Expand Down Expand Up @@ -56,7 +55,7 @@ func (st Stack) All() iter.Seq[Frame] {
for {
frame, more := frames.Next()
if frame != (runtime.Frame{}) {
if !yield(Frame{frame}) {
if !yield(Frame{f: frame}) {
return
}
}
Expand All @@ -71,10 +70,8 @@ func (st Stack) All() iter.Seq[Frame] {
// String implements [fmt.Stringer]
func (st Stack) String() string {
var b strings.Builder

for f := range st.All() {
fmt.Fprintf(&b, "%s()\n", f.Function)
fmt.Fprintf(&b, "\t%s:%d\n", f.File, f.Line)
for frame := range st.All() {
frame.append(&b)
}

return strings.TrimSpace(b.String())
Expand Down
4 changes: 2 additions & 2 deletions callstack/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ func TestGet(t *testing.T) {
funcName := pkgName + ".TestGet"

st := Get()
c.Equal(st.Slice()[0].Function, funcName)
c.Equal(st.Slice()[0].Func(), funcName)
c.True(strings.Contains(st.String(), funcName))

const depth = 129
expectDepth := len(st.Slice()) + depth

frames := recurse(depth, Get).Slice()
c.Equalf(len(frames), expectDepth, "%s", st)
c.Equal(frames[depth].Function, funcName)
c.Equal(frames[depth].Func(), funcName)
}

func TestStackIters(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions coverr/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ func (trk *Tracker) Err() error {

for f := range callstack.GetSkip(1).All() {
// Avoid allocations for speed
buf := strconv.AppendUint(h.buf, uint64(f.PC), 10)
buf := strconv.AppendUint(h.buf, uint64(f.PC()), 10)
buf = append(buf, '-')
buf = append(buf, f.File...)
buf = append(buf, f.File()...)
buf = append(buf, ':')
buf = strconv.AppendInt(buf, int64(f.Line), 10)
buf = strconv.AppendInt(buf, int64(f.Line()), 10)
buf = append(buf, '\n')

_, err := h.h.Write(buf)
Expand Down

0 comments on commit d4bd801

Please sign in to comment.