github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/osutil/strace/timing.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package strace
    21  
    22  import (
    23  	"bufio"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"regexp"
    28  	"strconv"
    29  )
    30  
    31  // ExeRuntime is the runtime of an individual executable
    32  type ExeRuntime struct {
    33  	Exe string
    34  	// FIXME: move to time.Duration
    35  	TotalSec float64
    36  }
    37  
    38  // ExecveTiming measures the execve calls timings under strace. This is
    39  // useful for performance analysis. It keeps the N slowest samples.
    40  type ExecveTiming struct {
    41  	TotalTime   float64
    42  	exeRuntimes []ExeRuntime
    43  
    44  	nSlowestSamples int
    45  }
    46  
    47  // NewExecveTiming returns a new ExecveTiming struct that keeps
    48  // the given amount of the slowest exec samples.
    49  func NewExecveTiming(nSlowestSamples int) *ExecveTiming {
    50  	return &ExecveTiming{nSlowestSamples: nSlowestSamples}
    51  }
    52  
    53  func (stt *ExecveTiming) addExeRuntime(exe string, totalSec float64) {
    54  	stt.exeRuntimes = append(stt.exeRuntimes, ExeRuntime{
    55  		Exe:      exe,
    56  		TotalSec: totalSec,
    57  	})
    58  	stt.prune()
    59  }
    60  
    61  // prune() ensures the number of exeRuntimes stays with the nSlowestSamples
    62  // limit
    63  func (stt *ExecveTiming) prune() {
    64  	for len(stt.exeRuntimes) > stt.nSlowestSamples {
    65  		fastest := 0
    66  		for idx, rt := range stt.exeRuntimes {
    67  			if rt.TotalSec < stt.exeRuntimes[fastest].TotalSec {
    68  				fastest = idx
    69  			}
    70  		}
    71  		// delete fastest element
    72  		stt.exeRuntimes = append(stt.exeRuntimes[:fastest], stt.exeRuntimes[fastest+1:]...)
    73  	}
    74  }
    75  
    76  func (stt *ExecveTiming) Display(w io.Writer) {
    77  	if len(stt.exeRuntimes) == 0 {
    78  		return
    79  	}
    80  	fmt.Fprintf(w, "Slowest %d exec calls during snap run:\n", len(stt.exeRuntimes))
    81  	for _, rt := range stt.exeRuntimes {
    82  		fmt.Fprintf(w, "  %2.3fs %s\n", rt.TotalSec, rt.Exe)
    83  	}
    84  	fmt.Fprintf(w, "Total time: %2.3fs\n", stt.TotalTime)
    85  }
    86  
    87  type exeStart struct {
    88  	start float64
    89  	exe   string
    90  }
    91  
    92  type pidTracker struct {
    93  	pidToExeStart map[string]exeStart
    94  }
    95  
    96  func newPidTracker() *pidTracker {
    97  	return &pidTracker{
    98  		pidToExeStart: make(map[string]exeStart),
    99  	}
   100  
   101  }
   102  
   103  func (pt *pidTracker) Get(pid string) (startTime float64, exe string) {
   104  	if exeStart, ok := pt.pidToExeStart[pid]; ok {
   105  		return exeStart.start, exeStart.exe
   106  	}
   107  	return 0, ""
   108  }
   109  
   110  func (pt *pidTracker) Add(pid string, startTime float64, exe string) {
   111  	pt.pidToExeStart[pid] = exeStart{start: startTime, exe: exe}
   112  }
   113  
   114  func (pt *pidTracker) Del(pid string) {
   115  	delete(pt.pidToExeStart, pid)
   116  }
   117  
   118  // lines look like:
   119  // PID   TIME              SYSCALL
   120  // 17363 1542815326.700248 execve("/snap/brave/44/usr/bin/update-mime-database", ["update-mime-database", "/home/egon/snap/brave/44/.local/"...], 0x1566008 /* 69 vars */) = 0
   121  var execveRE = regexp.MustCompile(`([0-9]+)\ +([0-9.]+) execve\(\"([^"]+)\"`)
   122  
   123  // lines look like:
   124  // PID   TIME              SYSCALL
   125  // 14157 1542875582.816782 execveat(3, "", ["snap-update-ns", "--from-snap-confine", "test-snapd-tools"], 0x7ffce7dd6160 /* 0 vars */, AT_EMPTY_PATH) = 0
   126  var execveatRE = regexp.MustCompile(`([0-9]+)\ +([0-9.]+) execveat\(.*\["([^"]+)"`)
   127  
   128  // lines look like (both SIGTERM and SIGCHLD need to be handled):
   129  // PID   TIME                  SIGNAL
   130  // 17559 1542815330.242750 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=17643, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
   131  var sigChldTermRE = regexp.MustCompile(`[0-9]+\ +([0-9.]+).*SIG(CHLD|TERM)\ {.*si_pid=([0-9]+),`)
   132  
   133  func handleExecMatch(trace *ExecveTiming, pt *pidTracker, match []string) error {
   134  	if len(match) == 0 {
   135  		return nil
   136  	}
   137  	// the pid of the process that does the execve{,at}()
   138  	pid := match[1]
   139  	execStart, err := strconv.ParseFloat(match[2], 64)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	exe := match[3]
   144  	// deal with subsequent execve()
   145  	if start, exe := pt.Get(pid); exe != "" {
   146  		trace.addExeRuntime(exe, execStart-start)
   147  	}
   148  	pt.Add(pid, execStart, exe)
   149  	return nil
   150  }
   151  
   152  func handleSignalMatch(trace *ExecveTiming, pt *pidTracker, match []string) error {
   153  	if len(match) == 0 {
   154  		return nil
   155  	}
   156  	sigTime, err := strconv.ParseFloat(match[1], 64)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	sigPid := match[3]
   161  	if start, exe := pt.Get(sigPid); exe != "" {
   162  		trace.addExeRuntime(exe, sigTime-start)
   163  		pt.Del(sigPid)
   164  	}
   165  	return nil
   166  }
   167  
   168  func TraceExecveTimings(straceLog string, nSlowest int) (*ExecveTiming, error) {
   169  	slog, err := os.Open(straceLog)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	defer slog.Close()
   174  
   175  	// pidTracker maps the "pid" string to the executable
   176  	pidTracker := newPidTracker()
   177  
   178  	var line string
   179  	var start, end, tmp float64
   180  	trace := NewExecveTiming(nSlowest)
   181  	r := bufio.NewScanner(slog)
   182  	for r.Scan() {
   183  		line = r.Text()
   184  		if start == 0.0 {
   185  			if _, err := fmt.Sscanf(line, "%f %f ", &tmp, &start); err != nil {
   186  				return nil, fmt.Errorf("cannot parse start of exec profile: %s", err)
   187  			}
   188  		}
   189  		// handleExecMatch looks for execve{,at}() calls and
   190  		// uses the pidTracker to keep track of execution of
   191  		// things. Because of fork() we may see many pids and
   192  		// within each pid we can see multiple execve{,at}()
   193  		// calls.
   194  		// An example of pids/exec transitions:
   195  		// $ snap run --trace-exec test-snapd-sh -c "/bin/true"
   196  		//    pid 20817 execve("snap-confine")
   197  		//    pid 20817 execve("snap-exec")
   198  		//    pid 20817 execve("/snap/test-snapd-sh/x2/bin/sh")
   199  		//    pid 20817 execve("/bin/sh")
   200  		//    pid 2023  execve("/bin/true")
   201  		match := execveRE.FindStringSubmatch(line)
   202  		if err := handleExecMatch(trace, pidTracker, match); err != nil {
   203  			return nil, err
   204  		}
   205  		match = execveatRE.FindStringSubmatch(line)
   206  		if err := handleExecMatch(trace, pidTracker, match); err != nil {
   207  			return nil, err
   208  		}
   209  		// handleSignalMatch looks for SIG{CHLD,TERM} signals and
   210  		// maps them via the pidTracker to the execve{,at}() calls
   211  		// of the terminating PID to calculate the total time of
   212  		// a execve{,at}() call.
   213  		match = sigChldTermRE.FindStringSubmatch(line)
   214  		if err := handleSignalMatch(trace, pidTracker, match); err != nil {
   215  			return nil, err
   216  		}
   217  	}
   218  	if _, err := fmt.Sscanf(line, "%f %f", &tmp, &end); err != nil {
   219  		return nil, fmt.Errorf("cannot parse end of exec profile: %s", err)
   220  	}
   221  	trace.TotalTime = end - start
   222  
   223  	if r.Err() != nil {
   224  		return nil, r.Err()
   225  	}
   226  
   227  	return trace, nil
   228  }