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