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 )