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  }