summaryrefslogtreecommitdiffstats
path: root/modules/process/manager_stacktraces.go
blob: e2608931131183cb0d67df866b4b636ba34b98b4 (plain)
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package process

import (
	"fmt"
	"io"
	"runtime/pprof"
	"sort"
	"time"

	"github.com/google/pprof/profile"
)

// StackEntry is an entry on a stacktrace
type StackEntry struct {
	Function string
	File     string
	Line     int
}

// Label represents a pprof label assigned to goroutine stack
type Label struct {
	Name  string
	Value string
}

// Stack is a stacktrace relating to a goroutine. (Multiple goroutines may have the same stacktrace)
type Stack struct {
	Count       int64 // Number of goroutines with this stack trace
	Description string
	Labels      []*Label      `json:",omitempty"`
	Entry       []*StackEntry `json:",omitempty"`
}

// A Process is a combined representation of a Process and a Stacktrace for the goroutines associated with it
type Process struct {
	PID         IDType
	ParentPID   IDType
	Description string
	Start       time.Time
	Type        string

	Children []*Process `json:",omitempty"`
	Stacks   []*Stack   `json:",omitempty"`
}

// Processes gets the processes in a thread safe manner
func (pm *Manager) Processes(flat, noSystem bool) ([]*Process, int) {
	pm.mutex.Lock()
	processCount := len(pm.processMap)
	processes := make([]*Process, 0, len(pm.processMap))
	if flat {
		for _, process := range pm.processMap {
			if noSystem && process.Type == SystemProcessType {
				continue
			}
			processes = append(processes, process.toProcess())
		}
	} else {
		// We need our own processMap
		processMap := map[IDType]*Process{}
		for _, internalProcess := range pm.processMap {
			process, ok := processMap[internalProcess.PID]
			if !ok {
				process = internalProcess.toProcess()
				processMap[process.PID] = process
			}

			// Check its parent
			if process.ParentPID == "" {
				processes = append(processes, process)
				continue
			}

			internalParentProcess, ok := pm.processMap[internalProcess.ParentPID]
			if ok {
				parentProcess, ok := processMap[process.ParentPID]
				if !ok {
					parentProcess = internalParentProcess.toProcess()
					processMap[parentProcess.PID] = parentProcess
				}
				parentProcess.Children = append(parentProcess.Children, process)
				continue
			}

			processes = append(processes, process)
		}
	}
	pm.mutex.Unlock()

	if !flat && noSystem {
		for i := 0; i < len(processes); i++ {
			process := processes[i]
			if process.Type != SystemProcessType {
				continue
			}
			processes[len(processes)-1], processes[i] = processes[i], processes[len(processes)-1]
			processes = append(processes[:len(processes)-1], process.Children...)
			i--
		}
	}

	// Sort by process' start time. Oldest process appears first.
	sort.Slice(processes, func(i, j int) bool {
		left, right := processes[i], processes[j]

		return left.Start.Before(right.Start)
	})

	return processes, processCount
}

// ProcessStacktraces gets the processes and stacktraces in a thread safe manner
func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int64, error) {
	var stacks *profile.Profile
	var err error

	// We cannot use the pm.ProcessMap here because we will release the mutex ...
	processMap := map[IDType]*Process{}
	var processCount int

	// Lock the manager
	pm.mutex.Lock()
	processCount = len(pm.processMap)

	// Add a defer to unlock in case there is a panic
	unlocked := false
	defer func() {
		if !unlocked {
			pm.mutex.Unlock()
		}
	}()

	processes := make([]*Process, 0, len(pm.processMap))
	if flat {
		for _, internalProcess := range pm.processMap {
			process := internalProcess.toProcess()
			processMap[process.PID] = process
			if noSystem && internalProcess.Type == SystemProcessType {
				continue
			}
			processes = append(processes, process)
		}
	} else {
		for _, internalProcess := range pm.processMap {
			process, ok := processMap[internalProcess.PID]
			if !ok {
				process = internalProcess.toProcess()
				processMap[process.PID] = process
			}

			// Check its parent
			if process.ParentPID == "" {
				processes = append(processes, process)
				continue
			}

			internalParentProcess, ok := pm.processMap[internalProcess.ParentPID]
			if ok {
				parentProcess, ok := processMap[process.ParentPID]
				if !ok {
					parentProcess = internalParentProcess.toProcess()
					processMap[parentProcess.PID] = parentProcess
				}
				parentProcess.Children = append(parentProcess.Children, process)
				continue
			}

			processes = append(processes, process)
		}
	}

	// Now from within the lock we need to get the goroutines.
	// Why? If we release the lock then between between filling the above map and getting
	// the stacktraces another process could be created which would then look like a dead process below
	reader, writer := io.Pipe()
	defer reader.Close()
	go func() {
		err := pprof.Lookup("goroutine").WriteTo(writer, 0)
		_ = writer.CloseWithError(err)
	}()
	stacks, err = profile.Parse(reader)
	if err != nil {
		return nil, 0, 0, err
	}

	// Unlock the mutex
	pm.mutex.Unlock()
	unlocked = true

	goroutineCount := int64(0)

	// Now walk through the "Sample" slice in the goroutines stack
	for _, sample := range stacks.Sample {
		// In the "goroutine" pprof profile each sample represents one or more goroutines
		// with the same labels and stacktraces.

		// We will represent each goroutine by a `Stack`
		stack := &Stack{}

		// Add the non-process associated labels from the goroutine sample to the Stack
		for name, value := range sample.Label {
			if name == DescriptionPProfLabel || name == PIDPProfLabel || (!flat && name == PPIDPProfLabel) || name == ProcessTypePProfLabel {
				continue
			}

			// Labels from the "goroutine" pprof profile only have one value.
			// This is because the underlying representation is a map[string]string
			if len(value) != 1 {
				// Unexpected...
				return nil, 0, 0, fmt.Errorf("label: %s in goroutine stack with unexpected number of values: %v", name, value)
			}

			stack.Labels = append(stack.Labels, &Label{Name: name, Value: value[0]})
		}

		// The number of goroutines that this sample represents is the `stack.Value[0]`
		stack.Count = sample.Value[0]
		goroutineCount += stack.Count

		// Now we want to associate this Stack with a Process.
		var process *Process

		// Try to get the PID from the goroutine labels
		if pidvalue, ok := sample.Label[PIDPProfLabel]; ok && len(pidvalue) == 1 {
			pid := IDType(pidvalue[0])

			// Now try to get the process from our map
			process, ok = processMap[pid]
			if !ok && pid != "" {
				// This means that no process has been found in the process map - but there was a process PID
				// Therefore this goroutine belongs to a dead process and it has escaped control of the process as it
				// should have died with the process context cancellation.

				// We need to create a dead process holder for this process and label it appropriately

				// get the parent PID
				ppid := IDType("")
				if value, ok := sample.Label[PPIDPProfLabel]; ok && len(value) == 1 {
					ppid = IDType(value[0])
				}

				// format the description
				description := "(dead process)"
				if value, ok := sample.Label[DescriptionPProfLabel]; ok && len(value) == 1 {
					description = value[0] + " " + description
				}

				// override the type of the process to "code" but add the old type as a label on the first stack
				ptype := NoneProcessType
				if value, ok := sample.Label[ProcessTypePProfLabel]; ok && len(value) == 1 {
					stack.Labels = append(stack.Labels, &Label{Name: ProcessTypePProfLabel, Value: value[0]})
				}
				process = &Process{
					PID:         pid,
					ParentPID:   ppid,
					Description: description,
					Type:        ptype,
				}

				// Now add the dead process back to the map and tree so we don't go back through this again.
				processMap[process.PID] = process
				added := false
				if process.ParentPID != "" && !flat {
					if parent, ok := processMap[process.ParentPID]; ok {
						parent.Children = append(parent.Children, process)
						added = true
					}
				}
				if !added {
					processes = append(processes, process)
				}
			}
		}

		if process == nil {
			// This means that the sample we're looking has no PID label
			var ok bool
			process, ok = processMap[""]
			if !ok {
				// this is the first time we've come acrross an unassociated goroutine so create a "process" to hold them
				process = &Process{
					Description: "(unassociated)",
					Type:        NoneProcessType,
				}
				processMap[process.PID] = process
				processes = append(processes, process)
			}
		}

		// The sample.Location represents a stack trace for this goroutine,
		// however each Location can represent multiple lines (mostly due to inlining)
		// so we need to walk the lines too
		for _, location := range sample.Location {
			for _, line := range location.Line {
				entry := &StackEntry{
					Function: line.Function.Name,
					File:     line.Function.Filename,
					Line:     int(line.Line),
				}
				stack.Entry = append(stack.Entry, entry)
			}
		}

		// Now we need a short-descriptive name to call the stack trace if when it is folded and
		// assuming the stack trace has some lines we'll choose the bottom of the stack (i.e. the
		// initial function that started the stack trace.) The top of the stack is unlikely to
		// be very helpful as a lot of the time it will be runtime.select or some other call into
		// a std library.
		stack.Description = "(unknown)"
		if len(stack.Entry) > 0 {
			stack.Description = stack.Entry[len(stack.Entry)-1].Function
		}

		process.Stacks = append(process.Stacks, stack)
	}

	// restrict to not show system processes
	if noSystem {
		for i := 0; i < len(processes); i++ {
			process := processes[i]
			if process.Type != SystemProcessType && process.Type != NoneProcessType {
				continue
			}
			processes[len(processes)-1], processes[i] = processes[i], processes[len(processes)-1]
			processes = append(processes[:len(processes)-1], process.Children...)
			i--
		}
	}

	// Now finally re-sort the processes. Newest process appears first
	after := func(processes []*Process) func(i, j int) bool {
		return func(i, j int) bool {
			left, right := processes[i], processes[j]
			return left.Start.After(right.Start)
		}
	}
	sort.Slice(processes, after(processes))
	if !flat {
		var sortChildren func(process *Process)

		sortChildren = func(process *Process) {
			sort.Slice(process.Children, after(process.Children))
			for _, child := range process.Children {
				sortChildren(child)
			}
		}
	}

	return processes, processCount, goroutineCount, err
}