-
Notifications
You must be signed in to change notification settings - Fork 1
/
stack.go
209 lines (182 loc) · 5.75 KB
/
stack.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/*
© 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
ISC License
*/
package pdebug
import (
"fmt"
"runtime/debug"
"strings"
"github.com/haraldrudell/parl"
"github.com/haraldrudell/parl/pruntime"
)
const (
runtDebugAndStackFrames = 2
runtStatusAndCreatorLines = 3
runtLinesPerFrame = 2
// runtStatusLines is a single line at the beginning of the stack trace
runtStatusLines = 1
// runtDebugStackLines are lines for the debug.Stack stack frame
runtDebugStackLines = 2
// runtNewStackLines are lines for the goid.NewStack stack frame
runtNewStackLines = 2
// runtCreatorLines are 2 optional lines at the end of the stack trace
runtCreatorLines = 2
)
type Stack struct {
// ThreadID is a unqique ID associated with this thread.
// typically numeric string “1”…
// it can be used as a map key or converted to string
threadID parl.ThreadID
// Status is typically word “running”
status parl.ThreadStatus
// isMainThread indicates if this is the thread that launched main.main
isMainThread bool
// Frames is a list of code locations for this thread.
// [0] is the invoker of goid.NewStack().
// last is the function starting this thread.
// Frame.Args is invocation values like "(0x14000113040)".
frames []parl.Frame
// Creator is the code location of the go statement launching
// this thread.
// FuncName is "main.main()" for main thread
creator pruntime.CodeLocation
}
// NewStack populates a Stack object with the current thread
// and its stack using debug.Stack
func NewStack(skipFrames int) (stack parl.Stack) {
var err error
if skipFrames < 0 {
skipFrames = 0
}
var s Stack
// trace is a stack trace as a string with final newline removed split into lines
/*
goroutine 18 [running]:
runtime/debug.Stack()
/opt/homebrew/Cellar/go/1.18/libexec/src/runtime/debug/stack.go:24 +0x68
github.com/haraldrudell/parl/pruntime.NewStack()
…
created by testing.(*T).Run
/opt/homebrew/Cellar/go/1.18/libexec/src/testing/testing.go:1486 +0x300
*/
// line 1 is status line
// line 2 is debug.Stack frame
// created by is 2 optional lines at end
trace := strings.Split(strings.TrimSuffix(string(debug.Stack()), "\n"), "\n")
traceLen := len(trace)
skipAtStart := runtStatusLines + runtDebugStackLines + runtNewStackLines
skipAtEnd := 0
// parse possible "created by"
if traceLen >= runtCreatorLines {
// populate s.IsMainThread s.Creator
// last 2 lines
var isMainThread bool
var creator pruntime.CodeLocation
if creator.FuncName, isMainThread = ParseCreatedLine(trace[traceLen-2]); !isMainThread {
skipAtEnd += runtCreatorLines
creator.File, creator.Line = ParseFileLine(trace[traceLen-1])
}
s.SetCreator(&creator, isMainThread)
}
// check trace length: must be at least one frame available
minLines := skipAtStart + skipAtEnd + // skip lines at beginning and end
runtLinesPerFrame // one frame available
if traceLen < minLines || traceLen&1 == 0 {
panic(fmt.Errorf("pruntime.Stack trace less than %d[%d–%d] lines or even: %d\nTRACE: %s\n",
minLines, skipAtStart, skipAtEnd, len(trace),
strings.Join(trace, "\n"),
))
}
// check skipFrames
maxSkipFrames := (traceLen - minLines) / runtLinesPerFrame
if skipFrames > maxSkipFrames {
panic(fmt.Errorf("pruntime.Stack bad skipFrames: %d trace-length: %d[%d–%d] max-skipFrames: %d\nTRACE: %s\n",
skipFrames, traceLen, skipAtStart, skipAtEnd, maxSkipFrames,
strings.Join(trace, "\n"),
))
}
skipAtStart += skipFrames * runtLinesPerFrame // remove frames from skipFrames
skipIndex := traceLen - skipAtEnd // limit index at end
// parse first line: s.ID s.Status
var threadID parl.ThreadID
var status parl.ThreadStatus
if threadID, status, err = ParseFirstLine(trace[0]); err != nil {
panic(err)
}
s.SetID(threadID, status)
// extract the desired stack frames into s.Frames
// stack:
// first line
// two lines of runtime/debug.Stack()
// two lines of goid.NewStack()
// additional frame line-pairs
// two lines of goroutine Creator
var frames []parl.Frame
for i := skipAtStart; i < skipIndex; i += runtLinesPerFrame {
var frame Frame
// parse function line
frame.CodeLocation.FuncName, frame.args = ParseFuncLine(trace[i])
// parse file line
frame.CodeLocation.File, frame.CodeLocation.Line = ParseFileLine(trace[i+1])
frames = append(frames, &frame)
}
s.SetFrames(frames)
stack = &s
return
}
var _ parl.Stack = &Stack{}
func (s *Stack) ID() (threadID parl.ThreadID) {
return s.threadID
}
func (s *Stack) IsMain() (isMainThread bool) {
return s.isMainThread
}
func (s *Stack) Status() (status parl.ThreadStatus) {
return s.status
}
func (s *Stack) Creator() (creator *pruntime.CodeLocation) {
return &s.creator
}
func (s *Stack) Frames() (frames []parl.Frame) {
return s.frames
}
func (st *Stack) Shorts(prepend string) (s string) {
sL := []string{
prepend + "Thread ID: " + st.threadID.String(),
}
for _, frame := range st.frames {
sL = append(sL, prepend+frame.Loc().Short())
}
if st.creator.IsSet() {
sL = append(sL, prepend+"creator: "+st.creator.Short())
}
return strings.Join(sL, "\n")
}
func (st *Stack) SetID(threadID parl.ThreadID, status parl.ThreadStatus) {
st.threadID = threadID
st.status = status
}
func (st *Stack) SetCreator(creator *pruntime.CodeLocation, isMainThread bool) {
st.creator = *creator
st.isMainThread = isMainThread
}
func (st *Stack) SetFrames(frames []parl.Frame) {
st.frames = frames
}
func (st *Stack) String() (s string) {
sL := make([]string, len(st.frames))
for i, frame := range st.frames {
sL[i] = frame.String()
}
if s = strings.Join(sL, "\n"); s != "" {
s += "\n"
}
return fmt.Sprintf("ID: %s IsMain: %t status: %s\n"+
"%s"+
"cre: %s",
st.threadID, st.isMainThread, st.status,
s,
st.creator.Long(),
)
}