metrics.go (7209B)
1 // Copyright 2017 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // Package metrics provides simple metrics tracking features. 15 package metrics 16 17 import ( 18 "fmt" 19 "io" 20 "math" 21 "reflect" 22 "sort" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/gohugoio/hugo/common/types" 29 "github.com/gohugoio/hugo/compare" 30 "github.com/gohugoio/hugo/helpers" 31 ) 32 33 // The Provider interface defines an interface for measuring metrics. 34 type Provider interface { 35 // MeasureSince adds a measurement for key to the metric store. 36 // Used with defer and time.Now(). 37 MeasureSince(key string, start time.Time) 38 39 // WriteMetrics will write a summary of the metrics to w. 40 WriteMetrics(w io.Writer) 41 42 // TrackValue tracks the value for diff calculations etc. 43 TrackValue(key string, value any, cached bool) 44 45 // Reset clears the metric store. 46 Reset() 47 } 48 49 type diff struct { 50 baseline any 51 count int 52 simSum int 53 } 54 55 func (d *diff) add(v any) *diff { 56 if types.IsNil(d.baseline) { 57 d.baseline = v 58 d.count = 1 59 d.simSum = 100 // If we get only one it is very cache friendly. 60 return d 61 } 62 adder := howSimilar(v, d.baseline) 63 d.simSum += adder 64 d.count++ 65 66 return d 67 } 68 69 // Store provides storage for a set of metrics. 70 type Store struct { 71 calculateHints bool 72 metrics map[string][]time.Duration 73 mu sync.Mutex 74 diffs map[string]*diff 75 diffmu sync.Mutex 76 cached map[string]int 77 cachedmu sync.Mutex 78 } 79 80 // NewProvider returns a new instance of a metric store. 81 func NewProvider(calculateHints bool) Provider { 82 return &Store{ 83 calculateHints: calculateHints, 84 metrics: make(map[string][]time.Duration), 85 diffs: make(map[string]*diff), 86 cached: make(map[string]int), 87 } 88 } 89 90 // Reset clears the metrics store. 91 func (s *Store) Reset() { 92 s.mu.Lock() 93 s.metrics = make(map[string][]time.Duration) 94 s.mu.Unlock() 95 96 s.diffmu.Lock() 97 s.diffs = make(map[string]*diff) 98 s.diffmu.Unlock() 99 100 s.cachedmu.Lock() 101 s.cached = make(map[string]int) 102 s.cachedmu.Unlock() 103 } 104 105 // TrackValue tracks the value for diff calculations etc. 106 func (s *Store) TrackValue(key string, value any, cached bool) { 107 if !s.calculateHints { 108 return 109 } 110 111 s.diffmu.Lock() 112 d, found := s.diffs[key] 113 114 if !found { 115 d = &diff{} 116 s.diffs[key] = d 117 } 118 119 d.add(value) 120 s.diffmu.Unlock() 121 122 if cached { 123 s.cachedmu.Lock() 124 s.cached[key] = s.cached[key] + 1 125 s.cachedmu.Unlock() 126 } 127 } 128 129 // MeasureSince adds a measurement for key to the metric store. 130 func (s *Store) MeasureSince(key string, start time.Time) { 131 s.mu.Lock() 132 s.metrics[key] = append(s.metrics[key], time.Since(start)) 133 s.mu.Unlock() 134 } 135 136 // WriteMetrics writes a summary of the metrics to w. 137 func (s *Store) WriteMetrics(w io.Writer) { 138 s.mu.Lock() 139 140 results := make([]result, len(s.metrics)) 141 142 var i int 143 for k, v := range s.metrics { 144 var sum time.Duration 145 var max time.Duration 146 147 diff, found := s.diffs[k] 148 149 cacheFactor := 0 150 if found { 151 cacheFactor = int(math.Floor(float64(diff.simSum) / float64(diff.count))) 152 } 153 154 for _, d := range v { 155 sum += d 156 if d > max { 157 max = d 158 } 159 } 160 161 avg := time.Duration(int(sum) / len(v)) 162 cacheCount := s.cached[k] 163 164 results[i] = result{key: k, count: len(v), max: max, sum: sum, avg: avg, cacheCount: cacheCount, cacheFactor: cacheFactor} 165 i++ 166 } 167 168 s.mu.Unlock() 169 170 if s.calculateHints { 171 fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "cumulative", "average", "maximum", "cache", "percent", "cached", "total", "") 172 fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "duration", "duration", "duration", "potential", "cached", "count", "count", "template") 173 fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "----------", "--------", "--------", "---------", "-------", "------", "-----", "--------") 174 } else { 175 fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "cumulative", "average", "maximum", "", "") 176 fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "duration", "duration", "duration", "count", "template") 177 fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "----------", "--------", "--------", "-----", "--------") 178 179 } 180 181 sort.Sort(bySum(results)) 182 for _, v := range results { 183 if s.calculateHints { 184 fmt.Fprintf(w, " %13s %12s %12s %9d %7.f %6d %5d %s\n", v.sum, v.avg, v.max, v.cacheFactor, float64(v.cacheCount)/float64(v.count)*100, v.cacheCount, v.count, v.key) 185 } else { 186 fmt.Fprintf(w, " %13s %12s %12s %5d %s\n", v.sum, v.avg, v.max, v.count, v.key) 187 } 188 } 189 } 190 191 // A result represents the calculated results for a given metric. 192 type result struct { 193 key string 194 count int 195 cacheCount int 196 cacheFactor int 197 sum time.Duration 198 max time.Duration 199 avg time.Duration 200 } 201 202 type bySum []result 203 204 func (b bySum) Len() int { return len(b) } 205 func (b bySum) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 206 func (b bySum) Less(i, j int) bool { return b[i].sum > b[j].sum } 207 208 // howSimilar is a naive diff implementation that returns 209 // a number between 0-100 indicating how similar a and b are. 210 func howSimilar(a, b any) int { 211 t1, t2 := reflect.TypeOf(a), reflect.TypeOf(b) 212 if t1 != t2 { 213 return 0 214 } 215 216 if t1.Comparable() && t2.Comparable() { 217 if a == b { 218 return 100 219 } 220 } 221 222 as, ok1 := types.TypeToString(a) 223 bs, ok2 := types.TypeToString(b) 224 225 if ok1 && ok2 { 226 return howSimilarStrings(as, bs) 227 } 228 229 if ok1 != ok2 { 230 return 0 231 } 232 233 e1, ok1 := a.(compare.Eqer) 234 e2, ok2 := b.(compare.Eqer) 235 if ok1 && ok2 && e1.Eq(e2) { 236 return 100 237 } 238 239 pe1, pok1 := a.(compare.ProbablyEqer) 240 pe2, pok2 := b.(compare.ProbablyEqer) 241 if pok1 && pok2 && pe1.ProbablyEq(pe2) { 242 return 90 243 } 244 245 h1, h2 := helpers.HashString(a), helpers.HashString(b) 246 if h1 == h2 { 247 return 100 248 } 249 return 0 250 } 251 252 // howSimilar is a naive diff implementation that returns 253 // a number between 0-100 indicating how similar a and b are. 254 // 100 is when all words in a also exists in b. 255 func howSimilarStrings(a, b string) int { 256 if a == b { 257 return 100 258 } 259 260 // Give some weight to the word positions. 261 const partitionSize = 4 262 263 af, bf := strings.Fields(a), strings.Fields(b) 264 if len(bf) > len(af) { 265 af, bf = bf, af 266 } 267 268 m1 := make(map[string]bool) 269 for i, x := range bf { 270 partition := partition(i, partitionSize) 271 key := x + "/" + strconv.Itoa(partition) 272 m1[key] = true 273 } 274 275 common := 0 276 for i, x := range af { 277 partition := partition(i, partitionSize) 278 key := x + "/" + strconv.Itoa(partition) 279 if m1[key] { 280 common++ 281 } 282 } 283 284 if common == 0 && common == len(af) { 285 return 100 286 } 287 288 return int(math.Floor((float64(common) / float64(len(af)) * 100))) 289 } 290 291 func partition(d, scale int) int { 292 return int(math.Floor((float64(d) / float64(scale)))) * scale 293 }