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