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 15 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
207 changes: 207 additions & 0 deletions internal/cache/base.go
@@ -0,0 +1,207 @@
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

canvasRefreshCh = make(chan struct{}, 1)
Jacalz marked this conversation as resolved.
Show resolved Hide resolved
expiredObjects = make([]fyne.CanvasObject, 0, 50)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be global, it is only used internally in two methods, both of which append and then clean out in a single run.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it global only to avoid having to recreate the slice on every clean task, in that way the same slice is reused on every task without the need to allocate memory again. Should I make it local?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making it global requires locking, surely being able to avoid that outweighs any time required in allocating memory?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not require locking because cache.Clean won't run asynchronously

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ok?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andydotxyz Any comment about this?

lastClean time.Time
)

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
}
}

// CleanTask run cache clean task, it should be called on paint events.
func CleanTask() {
fpabl0 marked this conversation as resolved.
Show resolved Hide resolved
now := time.Now()
// do not run clean task so fast
if now.Sub(lastClean) < 10*time.Second {
return
}
canvasRefresh := false
select {
case <-canvasRefreshCh:
canvasRefresh = true
default:
if now.Sub(lastClean) < cleanTaskInterval {
return
}
}
destroyExpiredSvgs(now)
if canvasRefresh {
// 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 = time.Now()
}

// ForceCleanFor forces a complete remove of all the objects that belong to the specified
// canvas. Usually used to free all objects from a closing windows.
func ForceCleanFor(canvas fyne.Canvas) {
fpabl0 marked this conversation as resolved.
Show resolved Hide resolved
deletingObjs := make([]fyne.CanvasObject, 0, 50)

// find all objects that belong to the specified canvas
canvasesLock.RLock()
for obj, cinfo := range canvases {
if cinfo.canvas == canvas {
deletingObjs = append(deletingObjs, obj)
}
}
canvasesLock.RUnlock()
if len(deletingObjs) == 0 {
return
}

// remove them from canvas cache
canvasesLock.Lock()
for _, dobj := range deletingObjs {
delete(canvases, dobj)
}
canvasesLock.Unlock()

// destroy their renderers and delete them from the renderer
// cache if they are widgets
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()
}

// NotifyCanvasRefresh notifies to the caches that a canvas refresh was triggered.
fpabl0 marked this conversation as resolved.
Show resolved Hide resolved
func NotifyCanvasRefresh() {
select {
case canvasRefreshCh <- struct{}{}:
default:
return
}
Jacalz marked this conversation as resolved.
Show resolved Hide resolved
}

// ===============================================================
fpabl0 marked this conversation as resolved.
Show resolved Hide resolved
// Private functions
// ===============================================================

// 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()
}
}

// ===============================================================
fpabl0 marked this conversation as resolved.
Show resolved Hide resolved
// Private types
// ===============================================================

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 := time.Now().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 := time.Now().Add(cacheDuration)
c.expires = t
}
36 changes: 36 additions & 0 deletions internal/cache/canvases.go
@@ -0,0 +1,36 @@
package cache

import (
"sync"

"fyne.io/fyne/v2"
)

var canvasesLock sync.RWMutex
var canvases = make(map[fyne.CanvasObject]*canvasInfo, 1024)

// GetCanvasForObject returns the canvas for the specified object.
func GetCanvasForObject(obj fyne.CanvasObject) fyne.Canvas {
canvasesLock.RLock()
cinfo, ok := canvases[obj]
canvasesLock.RUnlock()
if cinfo == nil || !ok {
return nil
}
cinfo.setAlive()
return cinfo.canvas
}

// SetCanvasForObject sets the canvas for the specified object.
func SetCanvasForObject(obj fyne.CanvasObject, canvas fyne.Canvas) {
cinfo := &canvasInfo{canvas: canvas}
cinfo.setAlive()
canvasesLock.Lock()
canvases[obj] = cinfo
canvasesLock.Unlock()
}

type canvasInfo struct {
expiringCache
canvas fyne.Canvas
}
47 changes: 47 additions & 0 deletions internal/cache/svg.go
@@ -0,0 +1,47 @@
package cache

import (
"image"
"sync"
)

var svgLock sync.RWMutex
var svgs = make(map[string]*svgInfo)

// GetSvg gets svg image from cache if it exists.
func GetSvg(name string, w int, h int) *image.NRGBA {
svgLock.RLock()
sinfo, ok := svgs[name]
svgLock.RUnlock()
if !ok || sinfo == nil || sinfo.w != w || sinfo.h != h {
return nil
}
sinfo.setAlive()
return sinfo.pix
}

// ResetSvg clears all the svg cache map
func ResetSvg() {
svgLock.Lock()
svgs = make(map[string]*svgInfo)
svgLock.Unlock()
}

// SetSvg sets a svg into the cache map.
func SetSvg(name string, pix *image.NRGBA, w int, h int) {
sinfo := &svgInfo{
pix: pix,
w: w,
h: h,
}
sinfo.setAlive()
svgLock.Lock()
svgs[name] = sinfo
svgLock.Unlock()
}

type svgInfo struct {
expiringCacheNoLock
pix *image.NRGBA
w, h int
}