forked from gohugoio/hugo
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve content map, memory cache and dependency resolution
TODO(bep) improve commit message. Hugo has always been a active user of in-memory caches, but before this commit we did nothing to control the memory usage. One failing example would be loading lots of big JSON data files and unmarshal them via `transform.Unmarshal`. This commit consolidates all these caches into one single LRU cache with an eviction strategy that also considers used vs. available memory. Hugo will try to limit its memory usage to 1/4 or total system memory, but this can be controlled with the `HUGO_MEMORYLIMIT` environment variable (a float value representing Gigabytes). A natural next step after this would be to use this cache for `.Content`. Fixes gohugoio#10386 Fixes gohugoio#8307 Fixes gohugoio#8498 Fixes gohugoio#8927 Fixes gohugoio#9192 Fixes gohugoio#9189 Fixes gohugoio#7425 Fixes gohugoio#7437 Fixes gohugoio#7436 Fixes gohugoio#7882 Updates gohugoio#7544 Fixes gohugoio#9224 Fixes gohugoio#9324 Fixes gohugoio#9352 Fixes gohugoio#9343 Fixes gohugoio#9171 Fixes gohugoio#10104 Fixes gohugoio#10380
- Loading branch information
Showing
256 changed files
with
14,113 additions
and
11,168 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"autoHide.autoHidePanel": false | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree" | ||
gobench --package ./hugolib --bench "BenchmarkSiteNew/Regular_Deep" -base v0.89.4 |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
// Copyright 2022 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 memcache | ||
|
||
import ( | ||
"context" | ||
"math" | ||
"runtime" | ||
"sync" | ||
"time" | ||
|
||
"github.com/bep/lazycache" | ||
"github.com/gohugoio/hugo/config" | ||
"github.com/gohugoio/hugo/identity" | ||
) | ||
|
||
const minMaxSize = 10 | ||
|
||
type Options struct { | ||
CheckInterval time.Duration | ||
MaxSize int | ||
MinMaxSize int | ||
Running bool | ||
} | ||
|
||
type OptionsPartition struct { | ||
ClearWhen ClearWhen | ||
|
||
// Weight is a number between 1 and 100 that indicates how, in general, how big this partition may get. | ||
Weight int | ||
} | ||
|
||
func (o OptionsPartition) WeightFraction() float64 { | ||
return float64(o.Weight) / 100 | ||
} | ||
|
||
func (o OptionsPartition) CalculateMaxSize(maxSizePerPartition int) int { | ||
return int(math.Floor(float64(maxSizePerPartition) * o.WeightFraction())) | ||
} | ||
|
||
type Cache struct { | ||
mu sync.RWMutex | ||
|
||
partitions map[string]PartitionManager | ||
opts Options | ||
|
||
stats *stats | ||
stopOnce sync.Once | ||
stop func() | ||
} | ||
|
||
func (c *Cache) ClearOn(when ClearWhen, changeset ...identity.Identity) { | ||
if when == 0 { | ||
panic("invalid ClearWhen") | ||
} | ||
|
||
// TODO1 | ||
|
||
} | ||
|
||
func calculateMaxSizePerPartition(maxItemsTotal, totalWeightQuantity, numPartitions int) int { | ||
if numPartitions == 0 { | ||
panic("numPartitions must be > 0") | ||
} | ||
if totalWeightQuantity == 0 { | ||
panic("totalWeightQuantity must be > 0") | ||
} | ||
|
||
avgWeight := float64(totalWeightQuantity) / float64(numPartitions) | ||
return int(math.Floor(float64(maxItemsTotal) / float64(numPartitions) * (100.0 / avgWeight))) | ||
} | ||
|
||
func (c *Cache) Stop() { | ||
c.stopOnce.Do(func() { | ||
c.stop() | ||
}) | ||
} | ||
|
||
func (c *Cache) adjustCurrentMaxSize() { | ||
if len(c.partitions) == 0 { | ||
return | ||
} | ||
var m runtime.MemStats | ||
runtime.ReadMemStats(&m) | ||
s := c.stats | ||
s.memstatsCurrent = m | ||
if s.availableMemory >= s.memstatsCurrent.Alloc { | ||
if s.adjustmentFactor <= 1.0 { | ||
s.adjustmentFactor += 0.1 | ||
} | ||
} else { | ||
s.adjustmentFactor -= 0.4 | ||
} | ||
|
||
if s.adjustmentFactor < 0.2 { | ||
s.adjustmentFactor = 0.2 | ||
} | ||
|
||
if !s.adjustCurrentMaxSize() { | ||
return | ||
} | ||
|
||
//fmt.Printf("\n\nAvailable = %v\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\nMaxSize = %d\n\n", helpers.FormatByteCount(s.availableMemory), helpers.FormatByteCount(m.Alloc), helpers.FormatByteCount(m.TotalAlloc), helpers.FormatByteCount(m.Sys), m.NumGC, c.stats.currentMaxSize) | ||
|
||
totalWeight := 0 | ||
for _, pm := range c.partitions { | ||
totalWeight += pm.getOptions().Weight | ||
} | ||
|
||
maxSizePerPartition := calculateMaxSizePerPartition(c.stats.currentMaxSize, totalWeight, len(c.partitions)) | ||
|
||
//fmt.Println("SCALE", s.adjustmentFactor, maxSizePerPartition) | ||
|
||
evicted := 0 | ||
for _, p := range c.partitions { | ||
evicted += p.adjustMaxSize(p.getOptions().CalculateMaxSize(maxSizePerPartition)) | ||
} | ||
|
||
// TODO1 | ||
//fmt.Println("Evicted", evicted, "items from cache") | ||
|
||
} | ||
|
||
func (c *Cache) start() func() { | ||
ticker := time.NewTicker(c.opts.CheckInterval) | ||
quit := make(chan struct{}) | ||
|
||
go func() { | ||
for { | ||
select { | ||
case <-ticker.C: | ||
c.adjustCurrentMaxSize() | ||
case <-quit: | ||
ticker.Stop() | ||
return | ||
} | ||
} | ||
}() | ||
|
||
return func() { | ||
close(quit) | ||
} | ||
} | ||
|
||
func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts OptionsPartition) *Partition[K, V] { | ||
if c == nil { | ||
panic("nil Cache") | ||
} | ||
if opts.Weight < 1 || opts.Weight > 100 { | ||
panic("invalid Weight, must be between 1 and 100") | ||
} | ||
|
||
c.mu.RLock() | ||
p, found := c.partitions[name] | ||
c.mu.RUnlock() | ||
if found { | ||
return p.(*Partition[K, V]) | ||
} | ||
|
||
c.mu.Lock() | ||
defer c.mu.Unlock() | ||
|
||
// Double check. | ||
p, found = c.partitions[name] | ||
if found { | ||
return p.(*Partition[K, V]) | ||
} | ||
|
||
// At this point, we don't now the the number of partitions or their configuration, but | ||
// this will be re-adjusted later. | ||
const numberOfPartitionsEstimate = 10 | ||
maxSize := opts.CalculateMaxSize(c.opts.MaxSize / numberOfPartitionsEstimate) | ||
|
||
// Create a new partition and cache it. | ||
partition := &Partition[K, V]{ | ||
c: lazycache.New[K, V](lazycache.Options{MaxEntries: maxSize}), | ||
maxSize: maxSize, | ||
opts: opts, | ||
} | ||
c.partitions[name] = partition | ||
|
||
return partition | ||
} | ||
|
||
func New(opts Options) *Cache { | ||
if opts.CheckInterval == 0 { | ||
opts.CheckInterval = time.Second * 2 | ||
} | ||
|
||
if opts.MaxSize == 0 { | ||
opts.MaxSize = 10000 | ||
} | ||
|
||
if opts.MinMaxSize == 0 { | ||
opts.MinMaxSize = 30 | ||
} | ||
|
||
stats := &stats{ | ||
configuredMaxSize: opts.MaxSize, | ||
configuredMinMaxSize: opts.MinMaxSize, | ||
currentMaxSize: opts.MaxSize, | ||
availableMemory: config.GetMemoryLimit(), | ||
} | ||
|
||
c := &Cache{ | ||
partitions: make(map[string]PartitionManager), | ||
opts: opts, | ||
stats: stats, | ||
} | ||
|
||
c.stop = c.start() | ||
|
||
return c | ||
} | ||
|
||
type Partition[K comparable, V any] struct { | ||
c *lazycache.Cache[K, V] | ||
|
||
opts OptionsPartition | ||
|
||
maxSize int | ||
} | ||
|
||
func (p *Partition[K, V]) GetOrCreate(ctx context.Context, key K, create func(key K) (V, error)) (V, error) { | ||
return p.c.GetOrCreate(key, create) | ||
|
||
//g.c.trackDependencyIfRunning(ctx, v) | ||
|
||
} | ||
|
||
// adjustMaxSize adjusts the max size of the and returns the number of items evicted. | ||
func (p *Partition[K, V]) adjustMaxSize(newMaxSize int) int { | ||
if newMaxSize < minMaxSize { | ||
newMaxSize = minMaxSize | ||
} | ||
p.maxSize = newMaxSize | ||
//fmt.Println("Adjusting max size of partition from", oldMaxSize, "to", newMaxSize) | ||
return p.c.Resize(newMaxSize) | ||
} | ||
|
||
func (p *Partition[K, V]) getMaxSize() int { | ||
return p.maxSize | ||
} | ||
|
||
func (p *Partition[K, V]) getOptions() OptionsPartition { | ||
return p.opts | ||
} | ||
|
||
func (p *Partition[K, V]) Clear() { | ||
// TODOD1 | ||
} | ||
|
||
func (p *Partition[K, V]) Get(ctx context.Context, key K) (V, bool) { | ||
return p.c.Get(key) | ||
// g.c.trackDependencyIfRunning(ctx, v) | ||
} | ||
|
||
type PartitionManager interface { | ||
Clear() | ||
|
||
adjustMaxSize(addend int) int | ||
getMaxSize() int | ||
getOptions() OptionsPartition | ||
} |
Oops, something went wrong.