1// Copyright 2022 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package process
5
6import (
7 "bytes"
8 "fmt"
9 "runtime/pprof"
10 "sort"
11 "time"
12
13 "github.com/google/pprof/profile"
14)
15
16// StackEntry is an entry on a stacktrace
17type StackEntry struct {
18 Function string
19 File string
20 Line int
21}
22
23// Label represents a pprof label assigned to goroutine stack
24type Label struct {
25 Name string
26 Value string
27}
28
29// Stack is a stacktrace relating to a goroutine. (Multiple goroutines may have the same stacktrace)
30type Stack struct {
31 Count int64 // Number of goroutines with this stack trace
32 Description string
33 Labels []*Label `json:",omitempty"`
34 Entry []*StackEntry `json:",omitempty"`
35}
36
37// A Process is a combined representation of a Process and a Stacktrace for the goroutines associated with it
38type Process struct {
39 PID IDType
40 ParentPID IDType
41 Description string
42 Start time.Time
43 Type string
44
45 Children []*Process `json:",omitempty"`
46 Stacks []*Stack `json:",omitempty"`
47}
48
49// Processes gets the processes in a thread safe manner
50func (pm *Manager) Processes(flat, noSystem bool) ([]*Process, int) {
51 pm.mutex.Lock()
52 processCount := len(pm.processMap)
53 processes := make([]*Process, 0, len(pm.processMap))
54 if flat {
55 for _, process := range pm.processMap {
56 if noSystem && process.Type == SystemProcessType {
57 continue
58 }
59 processes = append(processes, process.toProcess())
60 }
61 } else {
62 // We need our own processMap
63 processMap := map[IDType]*Process{}
64 for _, internalProcess := range pm.processMap {
65 process, ok := processMap[internalProcess.PID]
66 if !ok {
67 process = internalProcess.toProcess()
68 processMap[process.PID] = process
69 }
70
71 // Check its parent
72 if process.ParentPID == "" {
73 processes = append(processes, process)
74 continue
75 }
76
77 internalParentProcess, ok := pm.processMap[internalProcess.ParentPID]
78 if ok {
79 parentProcess, ok := processMap[process.ParentPID]
80 if !ok {
81 parentProcess = internalParentProcess.toProcess()
82 processMap[parentProcess.PID] = parentProcess
83 }
84 parentProcess.Children = append(parentProcess.Children, process)
85 continue
86 }
87
88 processes = append(processes, process)
89 }
90 }
91 pm.mutex.Unlock()
92
93 if !flat && noSystem {
94 for i := 0; i < len(processes); i++ {
95 process := processes[i]
96 if process.Type != SystemProcessType {
97 continue
98 }
99 processes[len(processes)-1], processes[i] = processes[i], processes[len(processes)-1]
100 processes = append(processes[:len(processes)-1], process.Children...)
101 i--
102 }
103 }
104
105 // Sort by process' start time. Oldest process appears first.
106 sort.Slice(processes, func(i, j int) bool {
107 left, right := processes[i], processes[j]
108
109 return left.Start.Before(right.Start)
110 })
111
112 return processes, processCount
113}
114
115// ProcessStacktraces gets the processes and stacktraces in a thread safe manner
116func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int64, error) {
117 var stacks *profile.Profile
118 var err error
119
120 // We cannot use the pm.ProcessMap here because we will release the mutex ...
121 processMap := map[IDType]*Process{}
122 var processCount int
123
124 // Lock the manager
125 pm.mutex.Lock()
126 processCount = len(pm.processMap)
127
128 // Add a defer to unlock in case there is a panic
129 unlocked := false
130 defer func() {
131 if !unlocked {
132 pm.mutex.Unlock()
133 }
134 }()
135
136 processes := make([]*Process, 0, len(pm.processMap))
137 if flat {
138 for _, internalProcess := range pm.processMap {
139 process := internalProcess.toProcess()
140 processMap[process.PID] = process
141 if noSystem && internalProcess.Type == SystemProcessType {
142 continue
143 }
144 processes = append(processes, process)
145 }
146 } else {
147 for _, internalProcess := range pm.processMap {
148 process, ok := processMap[internalProcess.PID]
149 if !ok {
150 process = internalProcess.toProcess()
151 processMap[process.PID] = process
152 }
153
154 // Check its parent
155 if process.ParentPID == "" {
156 processes = append(processes, process)
157 continue
158 }
159
160 internalParentProcess, ok := pm.processMap[internalProcess.ParentPID]
161 if ok {
162 parentProcess, ok := processMap[process.ParentPID]
163 if !ok {
164 parentProcess = internalParentProcess.toProcess()
165 processMap[parentProcess.PID] = parentProcess
166 }
167 parentProcess.Children = append(parentProcess.Children, process)
168 continue
169 }
170
171 processes = append(processes, process)
172 }
173 }
174
175 // Now from within the lock we need to get the goroutines.
176 // Why? If we release the lock then between between filling the above map and getting
177 // the stacktraces another process could be created which would then look like a dead process below
178 var buf bytes.Buffer
179 if err := pprof.Lookup("goroutine").WriteTo(&buf, 0); err != nil {
180 return nil, 0, 0, err
181 }
182
183 stacks, err = profile.ParseData(buf.Bytes())
184 if err != nil {
185 return nil, 0, 0, err
186 }
187
188 // Unlock the mutex
189 pm.mutex.Unlock()
190 unlocked = true
191
192 goroutineCount := int64(0)
193
194 // Now walk through the "Sample" slice in the goroutines stack
195 for _, sample := range stacks.Sample {
196 // In the "goroutine" pprof profile each sample represents one or more goroutines
197 // with the same labels and stacktraces.
198
199 // We will represent each goroutine by a `Stack`
200 stack := &Stack{}
201
202 // Add the non-process associated labels from the goroutine sample to the Stack
203 for name, value := range sample.Label {
204 if name == DescriptionPProfLabel || name == PIDPProfLabel || (!flat && name == PPIDPProfLabel) || name == ProcessTypePProfLabel {
205 continue
206 }
207
208 // Labels from the "goroutine" pprof profile only have one value.
209 // This is because the underlying representation is a map[string]string
210 if len(value) != 1 {
211 // Unexpected...
212 return nil, 0, 0, fmt.Errorf("label: %s in goroutine stack with unexpected number of values: %v", name, value)
213 }
214
215 stack.Labels = append(stack.Labels, &Label{Name: name, Value: value[0]})
216 }
217
218 // The number of goroutines that this sample represents is the `stack.Value[0]`
219 stack.Count = sample.Value[0]
220 goroutineCount += stack.Count
221
222 // Now we want to associate this Stack with a Process.
223 var process *Process
224
225 // Try to get the PID from the goroutine labels
226 if pidvalue, ok := sample.Label[PIDPProfLabel]; ok && len(pidvalue) == 1 {
227 pid := IDType(pidvalue[0])
228
229 // Now try to get the process from our map
230 process, ok = processMap[pid]
231 if !ok && pid != "" {
232 // This means that no process has been found in the process map - but there was a process PID
233 // Therefore this goroutine belongs to a dead process and it has escaped control of the process as it
234 // should have died with the process context cancellation.
235
236 // We need to create a dead process holder for this process and label it appropriately
237
238 // get the parent PID
239 ppid := IDType("")
240 if value, ok := sample.Label[PPIDPProfLabel]; ok && len(value) == 1 {
241 ppid = IDType(value[0])
242 }
243
244 // format the description
245 description := "(dead process)"
246 if value, ok := sample.Label[DescriptionPProfLabel]; ok && len(value) == 1 {
247 description = value[0] + " " + description
248 }
249
250 // override the type of the process to "code" but add the old type as a label on the first stack
251 ptype := NoneProcessType
252 if value, ok := sample.Label[ProcessTypePProfLabel]; ok && len(value) == 1 {
253 stack.Labels = append(stack.Labels, &Label{Name: ProcessTypePProfLabel, Value: value[0]})
254 }
255 process = &Process{
256 PID: pid,
257 ParentPID: ppid,
258 Description: description,
259 Type: ptype,
260 }
261
262 // Now add the dead process back to the map and tree so we don't go back through this again.
263 processMap[process.PID] = process
264 added := false
265 if process.ParentPID != "" && !flat {
266 if parent, ok := processMap[process.ParentPID]; ok {
267 parent.Children = append(parent.Children, process)
268 added = true
269 }
270 }
271 if !added {
272 processes = append(processes, process)
273 }
274 }
275 }
276
277 if process == nil {
278 // This means that the sample we're looking has no PID label
279 var ok bool
280 process, ok = processMap[""]
281 if !ok {
282 // this is the first time we've come acrross an unassociated goroutine so create a "process" to hold them
283 process = &Process{
284 Description: "(unassociated)",
285 Type: NoneProcessType,
286 }
287 processMap[process.PID] = process
288 processes = append(processes, process)
289 }
290 }
291
292 // The sample.Location represents a stack trace for this goroutine,
293 // however each Location can represent multiple lines (mostly due to inlining)
294 // so we need to walk the lines too
295 for _, location := range sample.Location {
296 for _, line := range location.Line {
297 entry := &StackEntry{
298 Function: line.Function.Name,
299 File: line.Function.Filename,
300 Line: int(line.Line),
301 }
302 stack.Entry = append(stack.Entry, entry)
303 }
304 }
305
306 // Now we need a short-descriptive name to call the stack trace if when it is folded and
307 // assuming the stack trace has some lines we'll choose the bottom of the stack (i.e. the
308 // initial function that started the stack trace.) The top of the stack is unlikely to
309 // be very helpful as a lot of the time it will be runtime.select or some other call into
310 // a std library.
311 stack.Description = "(unknown)"
312 if len(stack.Entry) > 0 {
313 stack.Description = stack.Entry[len(stack.Entry)-1].Function
314 }
315
316 process.Stacks = append(process.Stacks, stack)
317 }
318
319 // restrict to not show system processes
320 if noSystem {
321 for i := 0; i < len(processes); i++ {
322 process := processes[i]
323 if process.Type != SystemProcessType && process.Type != NoneProcessType {
324 continue
325 }
326 processes[len(processes)-1], processes[i] = processes[i], processes[len(processes)-1]
327 processes = append(processes[:len(processes)-1], process.Children...)
328 i--
329 }
330 }
331
332 // Now finally re-sort the processes. Newest process appears first
333 after := func(processes []*Process) func(i, j int) bool {
334 return func(i, j int) bool {
335 left, right := processes[i], processes[j]
336 return left.Start.After(right.Start)
337 }
338 }
339 sort.Slice(processes, after(processes))
340 if !flat {
341 var sortChildren func(process *Process)
342
343 sortChildren = func(process *Process) {
344 sort.Slice(process.Children, after(process.Children))
345 for _, child := range process.Children {
346 sortChildren(child)
347 }
348 }
349 }
350
351 return processes, processCount, goroutineCount, err
352}