// Copyright 2017 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package metrics provides simple metrics tracking features. package metrics import ( "fmt" "io" "math" "reflect" "sort" "strconv" "strings" "sync" "time" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/compare" "github.com/gohugoio/hugo/identity" ) // The Provider interface defines an interface for measuring metrics. type Provider interface { // MeasureSince adds a measurement for key to the metric store. // Used with defer and time.Now(). MeasureSince(key string, start time.Time) // WriteMetrics will write a summary of the metrics to w. WriteMetrics(w io.Writer) // TrackValue tracks the value for diff calculations etc. TrackValue(key string, value any, cached bool) // Reset clears the metric store. Reset() } type diff struct { baseline any count int simSum int } func (d *diff) add(v any) *diff { if types.IsNil(d.baseline) { d.baseline = v d.count = 1 d.simSum = 100 // If we get only one it is very cache friendly. return d } adder := howSimilar(v, d.baseline) d.simSum += adder d.count++ return d } // Store provides storage for a set of metrics. type Store struct { calculateHints bool metrics map[string][]time.Duration mu sync.Mutex diffs map[string]*diff diffmu sync.Mutex cached map[string]int cachedmu sync.Mutex } // NewProvider returns a new instance of a metric store. func NewProvider(calculateHints bool) Provider { return &Store{ calculateHints: calculateHints, metrics: make(map[string][]time.Duration), diffs: make(map[string]*diff), cached: make(map[string]int), } } // Reset clears the metrics store. func (s *Store) Reset() { s.mu.Lock() s.metrics = make(map[string][]time.Duration) s.mu.Unlock() s.diffmu.Lock() s.diffs = make(map[string]*diff) s.diffmu.Unlock() s.cachedmu.Lock() s.cached = make(map[string]int) s.cachedmu.Unlock() } // TrackValue tracks the value for diff calculations etc. func (s *Store) TrackValue(key string, value any, cached bool) { if !s.calculateHints { return } s.diffmu.Lock() d, found := s.diffs[key] if !found { d = &diff{} s.diffs[key] = d } d.add(value) s.diffmu.Unlock() if cached { s.cachedmu.Lock() s.cached[key] = s.cached[key] + 1 s.cachedmu.Unlock() } } // MeasureSince adds a measurement for key to the metric store. func (s *Store) MeasureSince(key string, start time.Time) { s.mu.Lock() s.metrics[key] = append(s.metrics[key], time.Since(start)) s.mu.Unlock() } // WriteMetrics writes a summary of the metrics to w. func (s *Store) WriteMetrics(w io.Writer) { s.mu.Lock() results := make([]result, len(s.metrics)) var i int for k, v := range s.metrics { var sum time.Duration var max time.Duration diff, found := s.diffs[k] cacheFactor := 0 if found { cacheFactor = int(math.Floor(float64(diff.simSum) / float64(diff.count))) } for _, d := range v { sum += d if d > max { max = d } } avg := time.Duration(int(sum) / len(v)) cacheCount := s.cached[k] results[i] = result{key: k, count: len(v), max: max, sum: sum, avg: avg, cacheCount: cacheCount, cacheFactor: cacheFactor} i++ } s.mu.Unlock() if s.calculateHints { fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "cumulative", "average", "maximum", "cache", "percent", "cached", "total", "") fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "duration", "duration", "duration", "potential", "cached", "count", "count", "template") fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "----------", "--------", "--------", "---------", "-------", "------", "-----", "--------") } else { fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "cumulative", "average", "maximum", "", "") fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "duration", "duration", "duration", "count", "template") fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "----------", "--------", "--------", "-----", "--------") } sort.Sort(bySum(results)) for _, v := range results { if s.calculateHints { 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) } else { fmt.Fprintf(w, " %13s %12s %12s %5d %s\n", v.sum, v.avg, v.max, v.count, v.key) } } } // A result represents the calculated results for a given metric. type result struct { key string count int cacheCount int cacheFactor int sum time.Duration max time.Duration avg time.Duration } type bySum []result func (b bySum) Len() int { return len(b) } func (b bySum) Swap(i, j int) { b[i], b[j] = b[j], b[i] } func (b bySum) Less(i, j int) bool { return b[i].sum > b[j].sum } // howSimilar is a naive diff implementation that returns // a number between 0-100 indicating how similar a and b are. func howSimilar(a, b any) int { t1, t2 := reflect.TypeOf(a), reflect.TypeOf(b) if t1 != t2 { return 0 } if t1.Comparable() && t2.Comparable() { if a == b { return 100 } } as, ok1 := types.TypeToString(a) bs, ok2 := types.TypeToString(b) if ok1 && ok2 { return howSimilarStrings(as, bs) } if ok1 != ok2 { return 0 } e1, ok1 := a.(compare.Eqer) e2, ok2 := b.(compare.Eqer) if ok1 && ok2 && e1.Eq(e2) { return 100 } pe1, pok1 := a.(compare.ProbablyEqer) pe2, pok2 := b.(compare.ProbablyEqer) if pok1 && pok2 && pe1.ProbablyEq(pe2) { return 90 } h1, h2 := identity.HashString(a), identity.HashString(b) if h1 == h2 { return 100 } return 0 } // howSimilar is a naive diff implementation that returns // a number between 0-100 indicating how similar a and b are. // 100 is when all words in a also exists in b. func howSimilarStrings(a, b string) int { if a == b { return 100 } // Give some weight to the word positions. const partitionSize = 4 af, bf := strings.Fields(a), strings.Fields(b) if len(bf) > len(af) { af, bf = bf, af } m1 := make(map[string]bool) for i, x := range bf { partition := partition(i, partitionSize) key := x + "/" + strconv.Itoa(partition) m1[key] = true } common := 0 for i, x := range af { partition := partition(i, partitionSize) key := x + "/" + strconv.Itoa(partition) if m1[key] { common++ } } if common == 0 && common == len(af) { return 100 } return int(math.Floor((float64(common) / float64(len(af)) * 100))) } func partition(d, scale int) int { return int(math.Floor((float64(d) / float64(scale)))) * scale }