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 }