github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap/cmd_debug_timings.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 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 main 21 22 import ( 23 "fmt" 24 "io" 25 "sort" 26 "strings" 27 "time" 28 29 "github.com/jessevdk/go-flags" 30 31 "github.com/snapcore/snapd/i18n" 32 ) 33 34 type cmdChangeTimings struct { 35 changeIDMixin 36 EnsureTag string `long:"ensure" choice:"auto-refresh" choice:"become-operational" choice:"refresh-catalogs" choice:"refresh-hints" choice:"seed"` 37 All bool `long:"all"` 38 StartupTag string `long:"startup" choice:"load-state" choice:"ifacemgr"` 39 Verbose bool `long:"verbose"` 40 } 41 42 func init() { 43 addDebugCommand("timings", 44 i18n.G("Get the timings of the tasks of a change"), 45 i18n.G("The timings command displays details about the time each task runs."), 46 func() flags.Commander { 47 return &cmdChangeTimings{} 48 }, changeIDMixinOptDesc.also(map[string]string{ 49 "ensure": i18n.G("Show timings for a change related to the given Ensure activity (one of: auto-refresh, become-operational, refresh-catalogs, refresh-hints, seed)"), 50 "all": i18n.G("Show timings for all executions of the given Ensure or startup activity, not just the latest"), 51 "startup": i18n.G("Show timings for the startup of given subsystem (one of: load-state, ifacemgr)"), 52 // TRANSLATORS: This should not start with a lowercase letter. 53 "verbose": i18n.G("Show more information"), 54 }), changeIDMixinArgDesc) 55 } 56 57 type Timing struct { 58 Level int `json:"level,omitempty"` 59 Label string `json:"label,omitempty"` 60 Summary string `json:"summary,omitempty"` 61 Duration time.Duration `json:"duration,omitempty"` 62 } 63 64 func formatDuration(dur time.Duration) string { 65 return fmt.Sprintf("%dms", dur/time.Millisecond) 66 } 67 68 func printTiming(w io.Writer, verbose bool, nestLevel int, id, status, doingTimeStr, undoingTimeStr, label, summary string) { 69 // don't display id for nesting>1, instead show nesting indicator 70 if nestLevel > 0 { 71 id = strings.Repeat(" ", nestLevel) + "^" 72 } 73 // Duration formats to 17m14.342s or 2.038s or 970ms, so with 74 // 11 chars we can go up to 59m59.999s 75 if verbose { 76 fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\t%s\n", id, status, doingTimeStr, undoingTimeStr, label, strings.Repeat(" ", 2*nestLevel)+summary) 77 } else { 78 fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\n", id, status, doingTimeStr, undoingTimeStr, strings.Repeat(" ", 2*nestLevel)+summary) 79 } 80 } 81 82 func printTaskTiming(w io.Writer, t *Timing, verbose, doing bool) { 83 var doingTimeStr, undoingTimeStr string 84 if doing { 85 doingTimeStr = formatDuration(t.Duration) 86 undoingTimeStr = "-" 87 } else { 88 if doing { 89 doingTimeStr = "-" 90 undoingTimeStr = formatDuration(t.Duration) 91 } 92 } 93 printTiming(w, verbose, t.Level+1, "", "", doingTimeStr, undoingTimeStr, t.Label, t.Summary) 94 } 95 96 // sortTimingsTasks sorts tasks from changeTimings by lane and ready time with special treatment of lane 0 tasks: 97 // - tasks from lanes >0 are grouped by lanes and sorted by ready time. 98 // - tasks from lane 0 are sorted by ready time and inserted before and after other lanes based on the min/max 99 // ready times of non-zero lanes. 100 // - tasks from lane 0 with ready time between non-zero lane tasks are not really expected in our system and will 101 // appear after non-zero lane tasks. 102 func sortTimingsTasks(timings map[string]changeTimings) []string { 103 tasks := make([]string, 0, len(timings)) 104 105 var minReadyTime time.Time 106 // determine min ready time from all non-zero lane tasks 107 for taskID, taskData := range timings { 108 if taskData.Lane > 0 { 109 if minReadyTime.IsZero() { 110 minReadyTime = taskData.ReadyTime 111 } 112 if taskData.ReadyTime.Before(minReadyTime) { 113 minReadyTime = taskData.ReadyTime 114 } 115 } 116 tasks = append(tasks, taskID) 117 } 118 119 sort.Slice(tasks, func(i, j int) bool { 120 t1 := timings[tasks[i]] 121 t2 := timings[tasks[j]] 122 if t1.Lane != t2.Lane { 123 // if either t1 or t2 is from lane 0, then it comes before or after non-zero lane tasks 124 if t1.Lane == 0 { 125 return t1.ReadyTime.Before(minReadyTime) 126 } 127 if t2.Lane == 0 { 128 return !t2.ReadyTime.Before(minReadyTime) 129 } 130 // different lanes (but neither of them is 0), order by lane 131 return t1.Lane < t2.Lane 132 } 133 134 // same lane - order by ready time 135 return t1.ReadyTime.Before(t2.ReadyTime) 136 }) 137 138 return tasks 139 } 140 141 func (x *cmdChangeTimings) printChangeTimings(w io.Writer, timing *timingsData) error { 142 tasks := sortTimingsTasks(timing.ChangeTimings) 143 144 for _, taskID := range tasks { 145 chgTiming := timing.ChangeTimings[taskID] 146 doingTime := formatDuration(timing.ChangeTimings[taskID].DoingTime) 147 if chgTiming.DoingTime == 0 { 148 doingTime = "-" 149 } 150 undoingTime := formatDuration(timing.ChangeTimings[taskID].UndoingTime) 151 if chgTiming.UndoingTime == 0 { 152 undoingTime = "-" 153 } 154 155 printTiming(w, x.Verbose, 0, taskID, chgTiming.Status, doingTime, undoingTime, chgTiming.Kind, chgTiming.Summary) 156 for _, nested := range timing.ChangeTimings[taskID].DoingTimings { 157 showDoing := true 158 printTaskTiming(w, &nested, x.Verbose, showDoing) 159 } 160 for _, nested := range timing.ChangeTimings[taskID].UndoingTimings { 161 showDoing := false 162 printTaskTiming(w, &nested, x.Verbose, showDoing) 163 } 164 } 165 166 return nil 167 } 168 169 func (x *cmdChangeTimings) printEnsureTimings(w io.Writer, timings []*timingsData) error { 170 for _, td := range timings { 171 printTiming(w, x.Verbose, 0, x.EnsureTag, "", formatDuration(td.TotalDuration), "-", "", "") 172 for _, t := range td.EnsureTimings { 173 printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary) 174 } 175 176 // change is optional for ensure timings 177 if td.ChangeID != "" { 178 x.printChangeTimings(w, td) 179 } 180 } 181 return nil 182 } 183 184 func (x *cmdChangeTimings) printStartupTimings(w io.Writer, timings []*timingsData) error { 185 for _, td := range timings { 186 printTiming(w, x.Verbose, 0, x.StartupTag, "", formatDuration(td.TotalDuration), "-", "", "") 187 for _, t := range td.StartupTimings { 188 printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary) 189 } 190 } 191 return nil 192 } 193 194 type changeTimings struct { 195 Status string `json:"status,omitempty"` 196 Kind string `json:"kind,omitempty"` 197 Summary string `json:"summary,omitempty"` 198 Lane int `json:"lane,omitempty"` 199 ReadyTime time.Time `json:"ready-time,omitempty"` 200 DoingTime time.Duration `json:"doing-time,omitempty"` 201 UndoingTime time.Duration `json:"undoing-time,omitempty"` 202 DoingTimings []Timing `json:"doing-timings,omitempty"` 203 UndoingTimings []Timing `json:"undoing-timings,omitempty"` 204 } 205 206 type timingsData struct { 207 ChangeID string `json:"change-id"` 208 EnsureTimings []Timing `json:"ensure-timings,omitempty"` 209 StartupTimings []Timing `json:"startup-timings,omitempty"` 210 TotalDuration time.Duration `json:"total-duration,omitempty"` 211 // ChangeTimings are indexed by task id 212 ChangeTimings map[string]changeTimings `json:"change-timings,omitempty"` 213 } 214 215 func (x *cmdChangeTimings) checkConflictingFlags() error { 216 var i int 217 for _, opt := range []string{string(x.Positional.ID), x.StartupTag, x.EnsureTag} { 218 if opt != "" { 219 i++ 220 if i > 1 { 221 return fmt.Errorf("cannot use change id, 'startup' or 'ensure' together") 222 } 223 } 224 } 225 226 if x.All && (x.Positional.ID != "" || x.LastChangeType != "") { 227 return fmt.Errorf("cannot use 'all' with change id or 'last'") 228 } 229 return nil 230 } 231 232 func (x *cmdChangeTimings) Execute(args []string) error { 233 if len(args) > 0 { 234 return ErrExtraArgs 235 } 236 237 if err := x.checkConflictingFlags(); err != nil { 238 return err 239 } 240 241 var chgid string 242 var err error 243 244 if x.EnsureTag == "" && x.StartupTag == "" { 245 if x.Positional.ID == "" && x.LastChangeType == "" { 246 // GetChangeID() below checks for empty change ID / --last, check them early here to provide more helpful error message 247 return fmt.Errorf("please provide change ID or type with --last=<type>, or query for --ensure=<name> or --startup=<name>") 248 } 249 250 // GetChangeID takes care of --last=... if change ID was not specified by the user 251 chgid, err = x.GetChangeID() 252 if err != nil { 253 return err 254 } 255 } 256 257 // gather debug timings first 258 var timings []*timingsData 259 var allEnsures string 260 if x.All { 261 allEnsures = "true" 262 } else { 263 allEnsures = "false" 264 } 265 if err := x.client.DebugGet("change-timings", &timings, map[string]string{"change-id": chgid, "ensure": x.EnsureTag, "all": allEnsures, "startup": x.StartupTag}); err != nil { 266 return err 267 } 268 269 w := tabWriter() 270 if x.Verbose { 271 fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tLabel\tSummary\n", "Doing", "Undoing") 272 } else { 273 fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tSummary\n", "Doing", "Undoing") 274 } 275 276 // If a specific change was requested, we expect exactly one timingsData element. 277 // If "ensure" activity was requested, we may get multiple elements (for multiple executions of the ensure) 278 if chgid != "" && len(timings) > 0 { 279 x.printChangeTimings(w, timings[0]) 280 } 281 282 if x.EnsureTag != "" { 283 x.printEnsureTimings(w, timings) 284 } 285 286 if x.StartupTag != "" { 287 x.printStartupTimings(w, timings) 288 } 289 290 w.Flush() 291 fmt.Fprintln(Stdout) 292 293 return nil 294 }