gitee.com/mysnapcore/mysnapd@v0.1.0/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  	"gitee.com/mysnapcore/mysnapd/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" choice:"install-system"`
    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  }