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  }