mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-04 21:32:51 -05:00
4ef9baf5bd
Note that this is backed by a LRU cache (which we soon shall see more usage of), so if you're a heavy user of cached partials it may be evicted and refreshed if needed. But in most cases every partial is only invoked once. This commit also adds a timeout (the global `timeout` config option) to make infinite recursion in partials easier to reason about. ``` name old time/op new time/op delta IncludeCached-10 8.92ms ± 0% 8.48ms ± 1% -4.87% (p=0.016 n=4+5) name old alloc/op new alloc/op delta IncludeCached-10 6.65MB ± 0% 5.17MB ± 0% -22.32% (p=0.002 n=6+6) name old allocs/op new allocs/op delta IncludeCached-10 117k ± 0% 71k ± 0% -39.44% (p=0.002 n=6+6) ``` Closes #4086 Updates #9588
293 lines
7 KiB
Go
293 lines
7 KiB
Go
// 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
|
|
}
|