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  }