github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/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/common/types"
    29  	"github.com/gohugoio/hugo/compare"
    30  	"github.com/gohugoio/hugo/identity"
    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 := identity.HashString(a), identity.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  }