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 }