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

Fix memory leaks in some cache maps #2101

Merged
merged 24 commits into from Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5ec29d8
fix memory leak in widget renderers, textures and canvases caches
fpabl0 Mar 19, 2021
1b8916c
fix the way textures are destroyed from cache
fpabl0 Mar 19, 2021
c1c52b6
remove texture cache mutex
fpabl0 Mar 21, 2021
32b0d45
use draw events to run the clean task instead of creating a new go ro…
fpabl0 Mar 21, 2021
801d271
move svg_cache to cache package, improve mobile driver by listening t…
fpabl0 Mar 25, 2021
7157855
revert mobile driver changes
fpabl0 Mar 25, 2021
34ee183
Merge branch 'develop' into fix/cache-renderer-memleak
fpabl0 Apr 9, 2021
29ae75f
remove repeated code
fpabl0 Apr 9, 2021
36842d9
apply clearing cache tasks to mobile driver
fpabl0 Apr 9, 2021
378dc5d
Merge branch 'develop' into fix/cache-renderer-memleak
fpabl0 May 12, 2021
e97fd54
Merge branch 'develop' into fix/cache-renderer-memleak
fpabl0 May 13, 2021
3223933
added cache logic to common driver and canvas, free resources when go…
fpabl0 May 13, 2021
07a7d30
group variables
fpabl0 May 13, 2021
16c8d56
Merge branch 'develop' into fix/cache-renderer-memleak
fpabl0 May 29, 2021
2a3abfb
destroy widget renderers only when canvas refresh and improve Destroy…
fpabl0 May 29, 2021
d0041e5
Update comment in internal/cache/texture_common.go
fpabl0 Jun 3, 2021
8764345
Merge branch 'develop' into fix/cache-renderer-memleak
fpabl0 Jun 4, 2021
6fc2bfc
rename CleanTask to Clean and ForceCleanFor to CleanCanvas as suggest…
fpabl0 Jun 4, 2021
ad7f974
remove unnecessary comments
fpabl0 Jun 4, 2021
342712a
remove NotifyCanvasRefresh logic
fpabl0 Jun 4, 2021
569a354
call cache.CleanCanvas in window.destroy() instead of in window.Close…
fpabl0 Jun 9, 2021
1d35466
Merge branch 'develop' into fix/cache-renderer-memleak
fpabl0 Jun 15, 2021
2c65c07
added tests
fpabl0 Jun 15, 2021
b018a2b
Merge branch 'develop' into fix/cache-renderer-memleak
fpabl0 Jun 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Jacalz marked this conversation as resolved.
Show resolved Hide resolved
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 {
fpabl0 marked this conversation as resolved.
Show resolved Hide resolved
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
}