github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/timings/state.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2020 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 timings
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"time"
    26  
    27  	"github.com/snapcore/snapd/logger"
    28  )
    29  
    30  // TimingJSON and rootTimingsJSON aid in marshalling of flattened timings into state.
    31  type TimingJSON struct {
    32  	Level    int           `json:"level,omitempty"`
    33  	Label    string        `json:"label,omitempty"`
    34  	Summary  string        `json:"summary,omitempty"`
    35  	Duration time.Duration `json:"duration"`
    36  }
    37  
    38  type rootTimingsJSON struct {
    39  	Tags          map[string]string `json:"tags,omitempty"`
    40  	NestedTimings []*TimingJSON     `json:"timings,omitempty"`
    41  	// start time of the first timing
    42  	StartTime time.Time `json:"start-time"`
    43  	// the most recent stop time of all timings
    44  	StopTime time.Time `json:"stop-time"`
    45  }
    46  
    47  // TimingsInfo holds a set of related nested timings and the tags set when they were captured.
    48  type TimingsInfo struct {
    49  	Tags          map[string]string
    50  	NestedTimings []*TimingJSON
    51  	Duration      time.Duration
    52  }
    53  
    54  // Maximum number of timings to keep in state. It can be changed only while holding state lock.
    55  var MaxTimings = 100
    56  
    57  // Duration threshold - timings below the threshold will not be saved in the state.
    58  // It can be changed only while holding state lock.
    59  var DurationThreshold = 5 * time.Millisecond
    60  
    61  var timeDuration = func(start, end time.Time) time.Duration {
    62  	return end.Sub(start)
    63  }
    64  
    65  // flatten flattens nested measurements into a single list within rootTimingJson.NestedTimings
    66  // and calculates total duration.
    67  func (t *Timings) flatten() interface{} {
    68  	var hasChangeID, hasTaskID bool
    69  	if t.tags != nil {
    70  		_, hasChangeID = t.tags["change-id"]
    71  		_, hasTaskID = t.tags["task-id"]
    72  	}
    73  
    74  	// ensure timings which created a change, have the corresponding
    75  	// change-id tag, but no task-id
    76  	isEnsureWithChange := hasChangeID && !hasTaskID
    77  
    78  	if len(t.timings) == 0 && !isEnsureWithChange {
    79  		return nil
    80  	}
    81  
    82  	data := &rootTimingsJSON{
    83  		Tags: t.tags,
    84  	}
    85  	if len(t.timings) > 0 {
    86  		var maxStopTime time.Time
    87  		flattenRecursive(data, t.timings, 0, &maxStopTime)
    88  		if len(data.NestedTimings) == 0 && !hasChangeID {
    89  			return nil
    90  		}
    91  		data.StartTime = t.timings[0].start
    92  		data.StopTime = maxStopTime
    93  	}
    94  
    95  	return data
    96  }
    97  
    98  func flattenRecursive(data *rootTimingsJSON, timings []*Span, nestLevel int, maxStopTime *time.Time) {
    99  	for _, tm := range timings {
   100  		dur := timeDuration(tm.start, tm.stop)
   101  		if dur >= DurationThreshold {
   102  			data.NestedTimings = append(data.NestedTimings, &TimingJSON{
   103  				Level:    nestLevel,
   104  				Label:    tm.label,
   105  				Summary:  tm.summary,
   106  				Duration: dur,
   107  			})
   108  		}
   109  		if tm.stop.After(*maxStopTime) {
   110  			*maxStopTime = tm.stop
   111  		}
   112  		if len(tm.timings) > 0 {
   113  			flattenRecursive(data, tm.timings, nestLevel+1, maxStopTime)
   114  		}
   115  	}
   116  }
   117  
   118  // A GetSaver helps storing Timings (ignoring their details).
   119  type GetSaver interface {
   120  	// GetMaybeTimings gets the saved timings.
   121  	// It will not return an error if none were saved yet.
   122  	GetMaybeTimings(timings interface{}) error
   123  	// SaveTimings saves the given timings.
   124  	SaveTimings(timings interface{})
   125  }
   126  
   127  // Save appends Timings data to a timings list in the GetSaver (usually
   128  // state.State) and purges old timings, ensuring that up to MaxTimings
   129  // are kept. Timings are only stored if their duration is greater than
   130  // or equal to DurationThreshold.  If GetSaver is a state.State, it's
   131  // responsibility of the caller to lock the state before calling this
   132  // function.
   133  func (t *Timings) Save(s GetSaver) {
   134  	var stateTimings []*json.RawMessage
   135  	if err := s.GetMaybeTimings(&stateTimings); err != nil {
   136  		logger.Noticef("could not get timings data from the state: %v", err)
   137  		return
   138  	}
   139  
   140  	data := t.flatten()
   141  	if data == nil {
   142  		return
   143  	}
   144  	serialized, err := json.Marshal(data)
   145  	if err != nil {
   146  		logger.Noticef("could not marshal timings: %v", err)
   147  		return
   148  	}
   149  	entryJSON := json.RawMessage(serialized)
   150  
   151  	stateTimings = append(stateTimings, &entryJSON)
   152  	if len(stateTimings) > MaxTimings {
   153  		stateTimings = stateTimings[len(stateTimings)-MaxTimings:]
   154  	}
   155  	s.SaveTimings(stateTimings)
   156  }
   157  
   158  // Get returns timings for which filter predicate is true and filters
   159  // out nested timings whose level is greater than maxLevel.
   160  // Negative maxLevel value disables filtering by level.
   161  // If GetSaver is a state.State, it's responsibility of the caller to
   162  // lock the state before calling this function.
   163  func Get(s GetSaver, maxLevel int, filter func(tags map[string]string) bool) ([]*TimingsInfo, error) {
   164  	var stateTimings []rootTimingsJSON
   165  	if err := s.GetMaybeTimings(&stateTimings); err != nil {
   166  		return nil, fmt.Errorf("could not get timings data from the state: %v", err)
   167  	}
   168  
   169  	var result []*TimingsInfo
   170  	for _, tm := range stateTimings {
   171  		if !filter(tm.Tags) {
   172  			continue
   173  		}
   174  		res := &TimingsInfo{
   175  			Tags:     tm.Tags,
   176  			Duration: timeDuration(tm.StartTime, tm.StopTime),
   177  		}
   178  		// negative maxLevel means no level filtering, take all nested timings
   179  		if maxLevel < 0 {
   180  			res.NestedTimings = tm.NestedTimings // there is always at least one nested timing - guaranteed by Save()
   181  			result = append(result, res)
   182  			continue
   183  		}
   184  		for _, nested := range tm.NestedTimings {
   185  			if nested.Level <= maxLevel {
   186  				res.NestedTimings = append(res.NestedTimings, nested)
   187  			}
   188  		}
   189  		// maxLevel is >=0 here, so we always have at least level 0 timings when the loop finishes
   190  		result = append(result, res)
   191  	}
   192  	return result, nil
   193  }