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 }