Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial work on dynamic cache cleaning #5546

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
123 changes: 99 additions & 24 deletions internal/cache/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,107 @@ import (
)

var (
ValidDuration = 1 * time.Minute
cleanTaskInterval = ValidDuration / 2
ValidDuration = 2 * time.Minute

lastClean time.Time
skippedCleanWithCanvasRefresh = false

framecounter uint64 = 1

// testing purpose only
timeNow = time.Now
)

func init() {
if t, err := time.ParseDuration(os.Getenv("FYNE_CACHE")); err == nil {
ValidDuration = t
cleanTaskInterval = ValidDuration / 2
}
}

// Clean run cache clean task, it should be called on paint events.
func Clean(canvasRefreshed bool) {
now := timeNow()
// do not run clean task too fast
if now.Sub(lastClean) < 10*time.Second {
if canvasRefreshed {
skippedCleanWithCanvasRefresh = true
}
return
// IncrementFrameCounter increments the current frame counter
// that is used to track which cached objects were accessed in the last frame.
// It should be called at the end of each iteration of the main loop.
func IncrementFrameCounter() {
framecounter += 1
}

// ShouldClean returns whether the clean tasks (CleanTextTextures and Clean)
// should be invoked during the current iteration of the main loop.
func ShouldClean() bool {
return shouldCleanTextTextures ||
shouldCleanObjectTextures ||
shouldCleanRenderers ||
shouldCleanCanvases ||
shouldCleanFontSizeCache ||
shouldFullClean()
}

// ShouldClean returns whether the clean tasks (CleanTextTextures and Clean)
// should be invoked during the current iteration of the main loop,
// AND the clean task will include a clean of the CanvasForObject map.
// If so, the driver should be sure to mark all objects -
// both visible and invisible, as alive before invoking the clean tasks.
func ShouldCleanCanvases() bool {
return shouldCleanCanvases || shouldFullClean()
}

// CleanTextures runs the per-canvas cache clean text for the texture caches.
// It should be run with a current GL context for each existing canvas
// during the main run loop if ShouldClean() returns true.
// Within the same iteration, Clean should be run once (not per-window)
// after CleanTextures has been called for all canvases
// to clean the non-texture caches and mark the texture caches clean as complete.
func CleanTextures(canvas fyne.Canvas, texFree func(fyne.CanvasObject)) {
full := shouldFullClean()
if full || shouldCleanTextTextures {
cleanTextTextureCache(canvas)
}
if skippedCleanWithCanvasRefresh {
skippedCleanWithCanvasRefresh = false
canvasRefreshed = true
if (full || shouldCleanObjectTextures) && texFree != nil {
rangeExpiredTexturesFor(canvas, texFree)
}
if !canvasRefreshed && now.Sub(lastClean) < cleanTaskInterval {
return
}

// Clean runs the non-texture cache clean task, if ShouldClean() returns true,
// it should be run during the main loop, after having called
// CleanTextures for each canvas.
func Clean() {
now := time.Now()
full := shouldFullClean()

if full {
destroyExpiredSvgs(now)
destroyExpiredFontMetrics(now)
}
destroyExpiredSvgs(now)
destroyExpiredFontMetrics(now)
if canvasRefreshed {
// Destroy renderers on canvas refresh to avoid flickering screen.
if full || shouldCleanRenderers {
destroyExpiredRenderers(now)
// canvases cache should be invalidated only on canvas refresh, otherwise there wouldn't
// be a way to recover them later
rendererCacheLastCleanSize = renderers.Len()
shouldCleanRenderers = false
}
if full || shouldCleanCanvases {
destroyExpiredCanvases(now)
canvasCacheLastCleanSize = canvases.Len()
shouldCleanCanvases = false
}
if full || shouldCleanFontSizeCache {
destroyExpiredFontMetrics(now)
fontSizeCacheLastCleanSize = fontSizeCache.Len()
shouldCleanFontSizeCache = false
}

// CleanTextures should have been called for each canvas
// update the sizes of the texture caches
if full || shouldCleanTextTextures {
textTextureLastCleanSize = textTextures.Len()
shouldCleanTextTextures = false
}
if full || shouldCleanObjectTextures {
objectTexturesLastCleanSize = objectTextures.Len()
shouldCleanObjectTextures = false
}

if full {
lastClean = now
}
lastClean = timeNow()
}

// CleanCanvas performs a complete remove of all the objects that belong to the specified
Expand Down Expand Up @@ -107,10 +164,28 @@ func destroyExpiredRenderers(now time.Time) {
})
}

func shouldFullClean() bool {
return lastClean.Add(ValidDuration).Before(timeNow())
}

type expiringCache struct {
expires time.Time
}

type frameCounterCache struct {
lastAccessedFrame uint64
}

// setAlive updates expiration time.
func (c *frameCounterCache) setAlive() {
c.lastAccessedFrame = framecounter
}

// isExpired check if the cache data is expired.
func (c *frameCounterCache) isExpired(now time.Time) bool {
return c.lastAccessedFrame < framecounter
}

// isExpired check if the cache data is expired.
func (c *expiringCache) isExpired(now time.Time) bool {
return c.expires.Before(now)
Expand Down
26 changes: 13 additions & 13 deletions internal/cache/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ func TestCacheClean(t *testing.T) {

t.Run("no_expired_objects", func(t *testing.T) {
lastClean = tm.createTime(10, 20)
Clean(false)
Clean()
assert.Equal(t, svgs.Len(), 40)
assert.Equal(t, 40, renderers.Len())
assert.Equal(t, 40, canvases.Len())
assert.Zero(t, destroyedRenderersCnt)
assert.Equal(t, tm.now, lastClean)

tm.setTime(10, 30)
Clean(true)
Clean()
assert.Equal(t, svgs.Len(), 40)
assert.Equal(t, 40, renderers.Len())
assert.Equal(t, 40, canvases.Len())
Expand All @@ -55,20 +55,20 @@ func TestCacheClean(t *testing.T) {
// when no canvas refresh and has been transcurred less than
// cleanTaskInterval duration, no clean task should occur.
tm.setTime(10, 42)
Clean(false)
Clean()
assert.Less(t, lastClean.UnixNano(), tm.now.UnixNano())

Clean(true)
Clean()
assert.Equal(t, tm.now, lastClean)

// when canvas refresh the clean task is only executed if it has been
// transcurred more than 10 seconds since the lastClean.
tm.setTime(10, 45)
Clean(true)
Clean()
assert.Less(t, lastClean.UnixNano(), tm.now.UnixNano())

tm.setTime(10, 53)
Clean(true)
Clean()
assert.Equal(t, tm.now, lastClean)

assert.Equal(t, svgs.Len(), 40)
Expand All @@ -80,14 +80,14 @@ func TestCacheClean(t *testing.T) {
t.Run("clean_no_canvas_refresh", func(t *testing.T) {
lastClean = tm.createTime(10, 11)
tm.setTime(11, 12)
Clean(false)
Clean()
assert.Equal(t, svgs.Len(), 20)
assert.Equal(t, renderers.Len(), 40)
assert.Equal(t, canvases.Len(), 40)
assert.Zero(t, destroyedRenderersCnt)

tm.setTime(11, 42)
Clean(false)
Clean()
assert.Equal(t, svgs.Len(), 0)
assert.Equal(t, renderers.Len(), 40)
assert.Equal(t, canvases.Len(), 40)
Expand All @@ -97,14 +97,14 @@ func TestCacheClean(t *testing.T) {
t.Run("clean_canvas_refresh", func(t *testing.T) {
lastClean = tm.createTime(10, 11)
tm.setTime(11, 11)
Clean(true)
Clean()
assert.Equal(t, svgs.Len(), 0)
assert.Equal(t, 20, renderers.Len())
assert.Equal(t, 20, canvases.Len())
assert.Equal(t, 20, destroyedRenderersCnt)

tm.setTime(11, 22)
Clean(true)
Clean()
assert.Equal(t, svgs.Len(), 0)
assert.Equal(t, 0, renderers.Len())
assert.Equal(t, 0, canvases.Len())
Expand All @@ -116,19 +116,19 @@ func TestCacheClean(t *testing.T) {
lastClean = tm.createTime(13, 10)
tm.setTime(13, 10)
assert.False(t, skippedCleanWithCanvasRefresh)
Clean(true)
Clean()
assert.Equal(t, tm.now, lastClean)

Renderer(&dummyWidget{})

tm.setTime(13, 15)
Clean(true)
Clean()
assert.True(t, skippedCleanWithCanvasRefresh)
assert.Less(t, lastClean.UnixNano(), tm.now.UnixNano())
assert.Equal(t, 1, renderers.Len())

tm.setTime(14, 21)
Clean(false)
Clean()
assert.False(t, skippedCleanWithCanvasRefresh)
assert.Equal(t, tm.now, lastClean)
assert.Equal(t, 0, renderers.Len())
Expand Down
32 changes: 25 additions & 7 deletions internal/cache/canvases.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import (
"fyne.io/fyne/v2/internal/async"
)

var canvases async.Map[fyne.CanvasObject, *canvasInfo]
var (
canvases async.Map[fyne.CanvasObject, *canvasInfo]
canvasCacheLastCleanSize int
shouldCleanCanvases bool
)

// GetCanvasForObject returns the canvas for the specified object.
func GetCanvasForObject(obj fyne.CanvasObject) fyne.Canvas {
Expand All @@ -20,16 +24,30 @@ func GetCanvasForObject(obj fyne.CanvasObject) fyne.Canvas {
// SetCanvasForObject sets the canvas for the specified object.
// The passed function will be called if the item was not previously attached to this canvas
func SetCanvasForObject(obj fyne.CanvasObject, c fyne.Canvas, setup func()) {
cinfo := &canvasInfo{canvas: c}
cinfo.setAlive()

old, found := canvases.LoadOrStore(obj, cinfo)
if (!found || old.canvas != c) && setup != nil {
old, found := canvases.Load(obj)
needSetup := false
if found {
old.setAlive()
if old.canvas != c {
old.canvas = c
needSetup = setup != nil
}
} else {
needSetup = setup != nil
cinfo := &canvasInfo{canvas: c}
cinfo.setAlive()
canvases.Store(obj, cinfo)
}
if needSetup {
setup()
}

if canvases.Len() > 2*canvasCacheLastCleanSize {
shouldCleanCanvases = true
}
}

type canvasInfo struct {
expiringCache
frameCounterCache
canvas fyne.Canvas
}
11 changes: 9 additions & 2 deletions internal/cache/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import (
"fyne.io/fyne/v2/internal/async"
)

var fontSizeCache async.Map[fontSizeEntry, *fontMetric]
var (
fontSizeCache async.Map[fontSizeEntry, *fontMetric]
fontSizeCacheLastCleanSize int
shouldCleanFontSizeCache bool
)

type fontMetric struct {
expiringCache
frameCounterCache
size fyne.Size
baseLine float32
}
Expand Down Expand Up @@ -55,6 +59,9 @@ func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source
metric := &fontMetric{size: size, baseLine: base}
metric.setAlive()
fontSizeCache.Store(ent, metric)
if fontSizeCache.Len() > 2*fontSizeCacheLastCleanSize {
shouldCleanFontSizeCache = true
}
}

// destroyExpiredFontMetrics destroys expired fontSizeCache entries
Expand Down
Loading
Loading