loading up the forgejo repo on tangled to test page performance
at forgejo 352 lines 10 kB view raw
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}