code.gitea.io/gitea@v1.19.3/modules/process/manager_stacktraces.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package process 5 6 import ( 7 "fmt" 8 "io" 9 "runtime/pprof" 10 "sort" 11 "time" 12 13 "github.com/google/pprof/profile" 14 ) 15 16 // StackEntry is an entry on a stacktrace 17 type StackEntry struct { 18 Function string 19 File string 20 Line int 21 } 22 23 // Label represents a pprof label assigned to goroutine stack 24 type 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) 30 type 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 38 type 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 50 func (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 116 func (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 reader, writer := io.Pipe() 179 defer reader.Close() 180 go func() { 181 err := pprof.Lookup("goroutine").WriteTo(writer, 0) 182 _ = writer.CloseWithError(err) 183 }() 184 stacks, err = profile.Parse(reader) 185 if err != nil { 186 return nil, 0, 0, err 187 } 188 189 // Unlock the mutex 190 pm.mutex.Unlock() 191 unlocked = true 192 193 goroutineCount := int64(0) 194 195 // Now walk through the "Sample" slice in the goroutines stack 196 for _, sample := range stacks.Sample { 197 // In the "goroutine" pprof profile each sample represents one or more goroutines 198 // with the same labels and stacktraces. 199 200 // We will represent each goroutine by a `Stack` 201 stack := &Stack{} 202 203 // Add the non-process associated labels from the goroutine sample to the Stack 204 for name, value := range sample.Label { 205 if name == DescriptionPProfLabel || name == PIDPProfLabel || (!flat && name == PPIDPProfLabel) || name == ProcessTypePProfLabel { 206 continue 207 } 208 209 // Labels from the "goroutine" pprof profile only have one value. 210 // This is because the underlying representation is a map[string]string 211 if len(value) != 1 { 212 // Unexpected... 213 return nil, 0, 0, fmt.Errorf("label: %s in goroutine stack with unexpected number of values: %v", name, value) 214 } 215 216 stack.Labels = append(stack.Labels, &Label{Name: name, Value: value[0]}) 217 } 218 219 // The number of goroutines that this sample represents is the `stack.Value[0]` 220 stack.Count = sample.Value[0] 221 goroutineCount += stack.Count 222 223 // Now we want to associate this Stack with a Process. 224 var process *Process 225 226 // Try to get the PID from the goroutine labels 227 if pidvalue, ok := sample.Label[PIDPProfLabel]; ok && len(pidvalue) == 1 { 228 pid := IDType(pidvalue[0]) 229 230 // Now try to get the process from our map 231 process, ok = processMap[pid] 232 if !ok && pid != "" { 233 // This means that no process has been found in the process map - but there was a process PID 234 // Therefore this goroutine belongs to a dead process and it has escaped control of the process as it 235 // should have died with the process context cancellation. 236 237 // We need to create a dead process holder for this process and label it appropriately 238 239 // get the parent PID 240 ppid := IDType("") 241 if value, ok := sample.Label[PPIDPProfLabel]; ok && len(value) == 1 { 242 ppid = IDType(value[0]) 243 } 244 245 // format the description 246 description := "(dead process)" 247 if value, ok := sample.Label[DescriptionPProfLabel]; ok && len(value) == 1 { 248 description = value[0] + " " + description 249 } 250 251 // override the type of the process to "code" but add the old type as a label on the first stack 252 ptype := NoneProcessType 253 if value, ok := sample.Label[ProcessTypePProfLabel]; ok && len(value) == 1 { 254 stack.Labels = append(stack.Labels, &Label{Name: ProcessTypePProfLabel, Value: value[0]}) 255 } 256 process = &Process{ 257 PID: pid, 258 ParentPID: ppid, 259 Description: description, 260 Type: ptype, 261 } 262 263 // Now add the dead process back to the map and tree so we don't go back through this again. 264 processMap[process.PID] = process 265 added := false 266 if process.ParentPID != "" && !flat { 267 if parent, ok := processMap[process.ParentPID]; ok { 268 parent.Children = append(parent.Children, process) 269 added = true 270 } 271 } 272 if !added { 273 processes = append(processes, process) 274 } 275 } 276 } 277 278 if process == nil { 279 // This means that the sample we're looking has no PID label 280 var ok bool 281 process, ok = processMap[""] 282 if !ok { 283 // this is the first time we've come acrross an unassociated goroutine so create a "process" to hold them 284 process = &Process{ 285 Description: "(unassociated)", 286 Type: NoneProcessType, 287 } 288 processMap[process.PID] = process 289 processes = append(processes, process) 290 } 291 } 292 293 // The sample.Location represents a stack trace for this goroutine, 294 // however each Location can represent multiple lines (mostly due to inlining) 295 // so we need to walk the lines too 296 for _, location := range sample.Location { 297 for _, line := range location.Line { 298 entry := &StackEntry{ 299 Function: line.Function.Name, 300 File: line.Function.Filename, 301 Line: int(line.Line), 302 } 303 stack.Entry = append(stack.Entry, entry) 304 } 305 } 306 307 // Now we need a short-descriptive name to call the stack trace if when it is folded and 308 // assuming the stack trace has some lines we'll choose the bottom of the stack (i.e. the 309 // initial function that started the stack trace.) The top of the stack is unlikely to 310 // be very helpful as a lot of the time it will be runtime.select or some other call into 311 // a std library. 312 stack.Description = "(unknown)" 313 if len(stack.Entry) > 0 { 314 stack.Description = stack.Entry[len(stack.Entry)-1].Function 315 } 316 317 process.Stacks = append(process.Stacks, stack) 318 } 319 320 // restrict to not show system processes 321 if noSystem { 322 for i := 0; i < len(processes); i++ { 323 process := processes[i] 324 if process.Type != SystemProcessType && process.Type != NoneProcessType { 325 continue 326 } 327 processes[len(processes)-1], processes[i] = processes[i], processes[len(processes)-1] 328 processes = append(processes[:len(processes)-1], process.Children...) 329 i-- 330 } 331 } 332 333 // Now finally re-sort the processes. Newest process appears first 334 after := func(processes []*Process) func(i, j int) bool { 335 return func(i, j int) bool { 336 left, right := processes[i], processes[j] 337 return left.Start.After(right.Start) 338 } 339 } 340 sort.Slice(processes, after(processes)) 341 if !flat { 342 343 var sortChildren func(process *Process) 344 345 sortChildren = func(process *Process) { 346 sort.Slice(process.Children, after(process.Children)) 347 for _, child := range process.Children { 348 sortChildren(child) 349 } 350 } 351 } 352 353 return processes, processCount, goroutineCount, err 354 }