github.com/haraldrudell/parl@v0.4.176/pruntime/stack-r.go (about)

     1  /*
     2  © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package pruntime
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/haraldrudell/parl/pruntime/pruntimelib"
    15  )
    16  
    17  // Stackr is a parl-free [pdebug.Stack]
    18  //   - Go stack traces are created by [runtime.Stack] and is a byte slice
    19  //   - [debug.Stack] repeatedly calls [runtime.Stack] with an increased
    20  //     buffer size that is eventually returned
    21  //   - [debug.PrintStack] writes the byte stream to [os.Stderr]
    22  //   - interning large strings is a temporary memory leak.
    23  //     Converting the entire byte-slice stack-trace to string
    24  //     retains the memory for as long as there is a reference to any one character.
    25  //     This leads to megabytes of memory leaks
    26  type StackR struct {
    27  	// ThreadID is a unqique ID associated with this thread.
    28  	// typically numeric string “1”…
    29  	//	- [constraints.Ordered] [fmt.Stringer] [ThreadID.IsValid]
    30  	ThreadID uint64
    31  	// Status is typically word “running”
    32  	Status string
    33  	// isMainThread indicates if this is the thread that launched main.main
    34  	//	- if false, the stack trace is for a goroutine,
    35  	//		a thread directly or indirectly launched by the main thread
    36  	isMainThread bool
    37  	// Frames is a list of code locations for this thread.
    38  	//	- [0] is the most recent code location, typically the invoker of [pdebug.Stack]
    39  	//	- last is the function starting this thread or in the Go runtime
    40  	//	- Frame.Args is invocation values like "(0x14000113040)"
    41  	frames []Frame
    42  	// goFunction is the function used in a go statement
    43  	//	- if isMain is true, it is the zero-value
    44  	goFunction CodeLocation
    45  	// Creator is the code location of the go statement launching
    46  	// this thread
    47  	//	- FuncName is "main.main()" for main thread
    48  	Creator CodeLocation
    49  	// possible ID of creating goroutine
    50  	CreatorID uint64
    51  	// creator goroutine reference “in goroutine 1”
    52  	GoroutineRef string
    53  }
    54  
    55  var _ Stack = &StackR{}
    56  
    57  // NewStack populates a Stack object with the current thread
    58  // and its stack using debug.Stack
    59  func NewStack(skipFrames int) (stack Stack) {
    60  	var err error
    61  	if skipFrames < 0 {
    62  		skipFrames = 0
    63  	}
    64  	// result of parsing to be returned
    65  	var s StackR
    66  
    67  	// [pruntime.StackTrace] returns a stack trace with final newline:
    68  	// goroutine␠18␠[running]:
    69  	// github.com/haraldrudell/parl/pruntime.StackTrace()
    70  	// ␉/opt/sw/parl/pruntime/stack-trace.go:24␠+0x50
    71  	// github.com/haraldrudell/parl/pruntime.TestStackTrace(0x14000122820)
    72  	// ␉/opt/sw/parl/pruntime/stack-trace_test.go:14␠+0x20
    73  	// testing.tRunner(0x14000122820,␠0x104c204c8)
    74  	// ␉/opt/homebrew/Cellar/go/1.21.4/libexec/src/testing/testing.go:1595␠+0xe8
    75  	// created␠by␠testing.(*T).Run␠in␠goroutine␠1
    76  	// ␉/opt/homebrew/Cellar/go/1.21.4/libexec/src/testing/testing.go:1648␠+0x33c
    77  
    78  	// trace is array of byte-slice lines: removed final newline and split on newline
    79  	//	- trace[0] is status line “goroutine␠18␠[running]:”
    80  	//	- trace[1…2] is [pruntime.StackTrace] frame
    81  	//	- trace[3…4] is [pdebug.Stack] frame
    82  	//	- final newline is removed, so last line is non-empty
    83  	//	- created by is 2 optional lines at end
    84  	//	- if created by is present, the two preceding lines is the goroutine function
    85  	//	- [bytes.TrimSuffix]: no allocations
    86  	//	- [bytes.Split] allocates the [][]byte slice index
    87  	//	- each conversion to string causes an allocation
    88  	var trace = bytes.Split(bytes.TrimSuffix(StackTrace(), []byte{'\n'}), []byte{'\n'})
    89  	var nonEmptyLineCount = len(trace)
    90  	var skipAtStart = runtStatus + runtPdebugStack + runtPruntimeStackTrace
    91  	var skipAtEnd = 0
    92  
    93  	// parse possible “created by” line-pair at end
    94  	//	- goroutine creator may be in the two last text lines of the stack trace
    95  	if nonEmptyLineCount-skipAtStart >= runtCreator {
    96  		// the index in trace of created-by line
    97  		var creatorIndex = nonEmptyLineCount - runtCreator
    98  		// temporary creator code location
    99  		var creator CodeLocation
   100  		var goroutineRef string
   101  		// determine s.isMainThread
   102  		creator.FuncName, goroutineRef, s.isMainThread = pruntimelib.ParseCreatedLine(trace[creatorIndex])
   103  		// if a goroutine, store creator
   104  		if !s.isMainThread {
   105  			s.GoroutineRef = goroutineRef
   106  			creator.File, creator.Line = pruntimelib.ParseFileLine(trace[creatorIndex+runtFileLineOffset])
   107  			s.Creator = creator
   108  			// “in goroutine 1”
   109  			if index := strings.LastIndex(goroutineRef, "\x20"); index != -1 {
   110  				var i, _ = strconv.ParseUint(goroutineRef[index+1:], 10, 64)
   111  				if i > 0 {
   112  					s.CreatorID = i
   113  				}
   114  			}
   115  			skipAtEnd += runtCreator
   116  		}
   117  
   118  		// if not main thread, store goroutine function
   119  		if !s.isMainThread && creatorIndex >= skipAtStart+runtGoFunction {
   120  			// the trace index for goroutine function
   121  			var goIndex = creatorIndex - runtGoFunction
   122  			s.goFunction.FuncName, _ = pruntimelib.ParseFuncLine(trace[goIndex])
   123  			s.goFunction.File, s.goFunction.Line = pruntimelib.ParseFileLine(trace[goIndex+runtFileLineOffset])
   124  		}
   125  	}
   126  
   127  	// check trace length: must be at least one frame available
   128  	var minLines = skipAtStart + skipAtEnd + // skip lines at beginning and end
   129  		runtLinesPerFrame // one frame available
   130  	if nonEmptyLineCount < minLines || nonEmptyLineCount&1 == 0 {
   131  		panic(fmt.Errorf("pdebug.Stack trace less than %d[%d–%d] lines or even: %d\nTRACE: %s%s",
   132  			minLines, skipAtStart, skipAtEnd, len(trace),
   133  			string(bytes.Join(trace, []byte{'\n'})),
   134  			"\n",
   135  		))
   136  	}
   137  
   138  	// check skipFrames
   139  	var maxSkipFrames = (nonEmptyLineCount - minLines) / runtLinesPerFrame
   140  	if skipFrames > maxSkipFrames {
   141  		panic(fmt.Errorf("pruntime.Stack bad skipFrames: %d trace-length: %d[%d–%d] max-skipFrames: %d\nTRACE: %s%s",
   142  			skipFrames, nonEmptyLineCount, skipAtStart, skipAtEnd, maxSkipFrames,
   143  			string(bytes.Join(trace, []byte{'\n'})),
   144  			"\n",
   145  		))
   146  	}
   147  	skipAtStart += skipFrames * runtLinesPerFrame // remove frames from skipFrames
   148  	var skipIndex = nonEmptyLineCount - skipAtEnd // limit index at end
   149  
   150  	// parse first line: s.ID s.Status
   151  	var threadID uint64
   152  	var status string
   153  	if threadID, status, err = pruntimelib.ParseFirstLine(trace[0]); err != nil {
   154  		panic(err)
   155  	}
   156  	s.ThreadID = threadID
   157  	s.Status = status
   158  	//s.SetID(threadID, status)
   159  
   160  	var frameCount = (skipIndex - skipAtStart) / runtLinesPerFrame
   161  	if frameCount > 0 {
   162  		// extract the desired stack frames into s.Frames
   163  		// stack:
   164  		//  first line
   165  		//  two lines of runtime/debug.Stack()
   166  		//  two lines of goid.NewStack()
   167  		//  additional frame line-pairs
   168  		//  two lines of goroutine Creator
   169  		var frames = make([]Frame, frameCount)
   170  		var frameStructs = make([]FrameR, frameCount)
   171  		for i, frameIndex := skipAtStart, 0; i < skipIndex; i += runtLinesPerFrame {
   172  			var frame = &frameStructs[frameIndex]
   173  
   174  			// parse function line
   175  			frame.CodeLocation.FuncName, frame.args = pruntimelib.ParseFuncLine(trace[i])
   176  
   177  			// parse file line
   178  			frame.CodeLocation.File, frame.CodeLocation.Line = pruntimelib.ParseFileLine(trace[i+1])
   179  			frames[frameIndex] = frame
   180  			frameIndex++
   181  		}
   182  		s.frames = frames
   183  	}
   184  	stack = &s
   185  
   186  	return
   187  }
   188  
   189  // A list of code locations for this thread
   190  //   - index [0] is the most recent code location, typically the invoker requesting the stack trace
   191  //   - includes invocation argument values
   192  func (s *StackR) Frames() (frames []Frame) { return s.frames }
   193  
   194  // the goroutine function used to launch this thread
   195  //   - if IsMain is true, zero-value. Check using GoFunction().IsSet()
   196  //   - never nil
   197  func (s *StackR) GoFunction() (goFunction *CodeLocation) { return &s.goFunction }
   198  
   199  // true if the thread is the main thread
   200  //   - false for a launched goroutine
   201  func (s *StackR) IsMain() (isMainThread bool) { return s.isMainThread }
   202  
   203  // Shorts lists short code locations for all stack frames, most recent first:
   204  // Shorts("prepend") →
   205  //
   206  //	prepend Thread ID: 1
   207  //	prepend main.someFunction()-pruntime.go:84
   208  //	prepend main.main()-pruntime.go:52
   209  func (s *StackR) Shorts(prepend string) (shorts string) {
   210  	if prepend != "" {
   211  		prepend += "\x20"
   212  	}
   213  	sL := []string{
   214  		prepend + "Thread ID: " + strconv.FormatUint(s.ThreadID, 10),
   215  	}
   216  	for _, frame := range s.frames {
   217  		sL = append(sL, prepend+frame.Loc().Short())
   218  	}
   219  	if s.Creator.IsSet() {
   220  		var s3 = prepend + "creator: " + s.Creator.Short()
   221  		if s.GoroutineRef != "" {
   222  			s3 += "\x20" + s.GoroutineRef
   223  		}
   224  		sL = append(sL, s3)
   225  	}
   226  	return strings.Join(sL, "\n")
   227  }
   228  
   229  func (s *StackR) Dump() (s2 string) {
   230  	if s == nil {
   231  		return "<nil>"
   232  	}
   233  	var f = make([]string, len(s.frames))
   234  	for i, frame := range s.frames {
   235  		f[i] = fmt.Sprintf("%d: %s Args: %q", i+1, frame.Loc().Dump(), frame.Args())
   236  	}
   237  	return fmt.Sprintf("threadID %d status %q isMain %t frames %d[\n%s\n]\ngoFunction:\n%s\ncreator: ref: %q\n%s",
   238  		s.ThreadID, s.Status, s.IsMain(),
   239  		len(s.frames), strings.Join(f, "\n"),
   240  		s.goFunction.Dump(),
   241  		s.GoroutineRef,
   242  		s.Creator.Dump(),
   243  	)
   244  }
   245  
   246  // String is a multi-line stack trace, most recent code location first:
   247  //
   248  //	ID: 18 IsMain: false status: running␤
   249  //	main.someFunction({0x100dd2616, 0x19})␤
   250  //	␠␠pruntime.go:64␤
   251  //	cre: main.main-pruntime.go:53␤
   252  func (s *StackR) String() (s2 string) {
   253  
   254  	// convert frames to string slice
   255  	var frames = make([]string, len(s.frames))
   256  	for i, frame := range s.frames {
   257  		frames[i] = frame.String()
   258  	}
   259  
   260  	// parent information for goroutine
   261  	var parentS string
   262  	if !s.isMainThread {
   263  		parentS = fmt.Sprintf("\nParent-ID: %d go: %s",
   264  			s.CreatorID,
   265  			s.Creator,
   266  		)
   267  	}
   268  
   269  	return fmt.Sprintf("ID: %d status: ‘%s’\n%s%s",
   270  		s.ThreadID, s.Status,
   271  		strings.Join(frames, "\n"),
   272  		parentS,
   273  	)
   274  }
   275  
   276  const (
   277  	// each stack frame is two lines:
   278  	//	- function line
   279  	//	- filename line with leading tab, line number and byte-offset
   280  	runtLinesPerFrame = 2
   281  	// the filename line is 1 line after function line
   282  	runtFileLineOffset = 1
   283  	// runtStatus is a single line at the beginning of the stack trace
   284  	runtStatus = 1
   285  	// runtPdebugStack are lines for the [pdebug.Stack] stack frame
   286  	runtPdebugStack = runtLinesPerFrame
   287  	// runtPruntimeStackTrace are lines for the [pruntime.StackTrace] stack frame
   288  	runtPruntimeStackTrace = runtLinesPerFrame
   289  	// runtCreator are 2 optional lines at the end of the stack trace
   290  	runtCreator = runtLinesPerFrame
   291  	// runtGoFunction are 2 optional lines that precedes runtCreator
   292  	runtGoFunction = runtLinesPerFrame
   293  )