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