Skip to content

Commit

Permalink
Merge pull request #2101 from fpabl0/fix/cache-renderer-memleak
Browse files Browse the repository at this point in the history
Fix memory leaks in some cache maps
  • Loading branch information
fpabl0 committed Jun 18, 2021
2 parents da02125 + b018a2b commit f14ac77
Show file tree
Hide file tree
Showing 24 changed files with 773 additions and 172 deletions.
6 changes: 3 additions & 3 deletions cmd/fyne_settings/settings/appearance.go
Expand Up @@ -14,7 +14,7 @@ import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/internal/painter"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/tools/playground"
Expand Down Expand Up @@ -118,7 +118,7 @@ func (s *Settings) createPreview() image.Image {
th = theme.DarkTheme()
}

painter.SvgCacheReset() // reset icon cache
cache.ResetSvg() // reset icon cache
fyne.CurrentApp().Settings().(overrideTheme).OverrideTheme(th, s.fyneSettings.PrimaryColor)

empty := widget.NewLabel("")
Expand All @@ -136,7 +136,7 @@ func (s *Settings) createPreview() image.Image {
time.Sleep(canvas.DurationShort)
img := c.Capture()

painter.SvgCacheReset() // ensure we re-create the correct cached assets
cache.ResetSvg() // ensure we re-create the correct cached assets
fyne.CurrentApp().Settings().(overrideTheme).OverrideTheme(oldTheme, oldColor)
return img
}
Expand Down
190 changes: 190 additions & 0 deletions internal/cache/base.go
@@ -0,0 +1,190 @@
package cache

import (
"os"
"sync"
"time"

"fyne.io/fyne/v2"
)

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

expiredObjects = make([]fyne.CanvasObject, 0, 50)
lastClean time.Time
skippedCleanWithCanvasRefresh = false

// testing purpose only
timeNow func() time.Time = time.Now
)

func init() {
if t, err := time.ParseDuration(os.Getenv("FYNE_CACHE")); err == nil {
cacheDuration = t
cleanTaskInterval = cacheDuration / 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
}
if skippedCleanWithCanvasRefresh {
skippedCleanWithCanvasRefresh = false
canvasRefreshed = true
}
if !canvasRefreshed && now.Sub(lastClean) < cleanTaskInterval {
return
}
destroyExpiredSvgs(now)
if canvasRefreshed {
// Destroy renderers on canvas refresh to avoid flickering screen.
destroyExpiredRenderers(now)
// canvases cache should be invalidated only on canvas refresh, otherwise there wouldn't
// be a way to recover them later
destroyExpiredCanvases(now)
}
lastClean = timeNow()
}

// CleanCanvas performs a complete remove of all the objects that belong to the specified
// canvas. Usually used to free all objects from a closing windows.
func CleanCanvas(canvas fyne.Canvas) {
deletingObjs := make([]fyne.CanvasObject, 0, 50)

canvasesLock.RLock()
for obj, cinfo := range canvases {
if cinfo.canvas == canvas {
deletingObjs = append(deletingObjs, obj)
}
}
canvasesLock.RUnlock()
if len(deletingObjs) == 0 {
return
}

canvasesLock.Lock()
for _, dobj := range deletingObjs {
delete(canvases, dobj)
}
canvasesLock.Unlock()

renderersLock.Lock()
for _, dobj := range deletingObjs {
wid, ok := dobj.(fyne.Widget)
if !ok {
continue
}
winfo, ok := renderers[wid]
if !ok {
continue
}
winfo.renderer.Destroy()
delete(renderers, wid)
}
renderersLock.Unlock()
}

// destroyExpiredCanvases deletes objects from the canvases cache.
func destroyExpiredCanvases(now time.Time) {
expiredObjects = expiredObjects[:0]
canvasesLock.RLock()
for obj, cinfo := range canvases {
if cinfo.isExpired(now) {
expiredObjects = append(expiredObjects, obj)
}
}
canvasesLock.RUnlock()
if len(expiredObjects) > 0 {
canvasesLock.Lock()
for i, exp := range expiredObjects {
delete(canvases, exp)
expiredObjects[i] = nil
}
canvasesLock.Unlock()
}
}

// destroyExpiredRenderers deletes the renderer from the cache and calls
// renderer.Destroy()
func destroyExpiredRenderers(now time.Time) {
expiredObjects = expiredObjects[:0]
renderersLock.RLock()
for wid, rinfo := range renderers {
if rinfo.isExpired(now) {
rinfo.renderer.Destroy()
expiredObjects = append(expiredObjects, wid)
}
}
renderersLock.RUnlock()
if len(expiredObjects) > 0 {
renderersLock.Lock()
for i, exp := range expiredObjects {
delete(renderers, exp.(fyne.Widget))
expiredObjects[i] = nil
}
renderersLock.Unlock()
}
}

// destroyExpiredSvgs destroys expired svgs cache data.
func destroyExpiredSvgs(now time.Time) {
expiredSvgs := make([]string, 0, 20)
svgLock.RLock()
for s, sinfo := range svgs {
if sinfo.isExpired(now) {
expiredSvgs = append(expiredSvgs, s)
}
}
svgLock.RUnlock()
if len(expiredSvgs) > 0 {
svgLock.Lock()
for _, exp := range expiredSvgs {
delete(svgs, exp)
}
svgLock.Unlock()
}
}

type expiringCache struct {
expireLock sync.RWMutex
expires time.Time
}

// isExpired check if the cache data is expired.
func (c *expiringCache) isExpired(now time.Time) bool {
c.expireLock.RLock()
defer c.expireLock.RUnlock()
return c.expires.Before(now)
}

// setAlive updates expiration time.
func (c *expiringCache) setAlive() {
t := timeNow().Add(cacheDuration)
c.expireLock.Lock()
c.expires = t
c.expireLock.Unlock()
}

type expiringCacheNoLock struct {
expires time.Time
}

// isExpired check if the cache data is expired.
func (c *expiringCacheNoLock) isExpired(now time.Time) bool {
return c.expires.Before(now)
}

// setAlive updates expiration time.
func (c *expiringCacheNoLock) setAlive() {
t := timeNow().Add(cacheDuration)
c.expires = t
}

0 comments on commit f14ac77

Please sign in to comment.