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

Conversation

fpabl0
Copy link
Member

@fpabl0 fpabl0 commented Mar 19, 2021

Description:

Potentially fix #348, #735, #2069.

Checklist:

  • Tests included.
  • Lint and formatter run with no errors.
  • Tests all pass.
  • Call cache.Clean on mobile too. Update mobile driver, canvas and windows to have the same desktop changes.
  • Find a better way to clean textures cache.
  • Move svg_cache to cache package.
  • Decision about minSize cache on mobile driver (needs refactor).
  • Remove the mutex (added in this PR) for texture cache as it is not needed (texture cache is only manipulated in one goroutine).
  • Solve texture cache cleaning for multiple windows.

@fpabl0
Copy link
Member Author

fpabl0 commented Mar 19, 2021

Oh I forgot that ticker.Reset does not exist in Go 1.12, I will update later.

@Jacalz
Copy link
Member

Jacalz commented Mar 20, 2021

Oh I forgot that ticker.Reset does not exist in Go 1.12, I will update later.

Just add a comment about it (after you update the code) so that we can change it when we update the minimum Go version 🙂

…utine, added cache.NotifyCanvasRefresh, fix the way textures are freed, fix memory leak on window.Close
@fpabl0
Copy link
Member Author

fpabl0 commented Mar 21, 2021

@andydotxyz @Jacalz it is not finished yet, but I would like to know your thoughts/suggestions about the current changes :)

Basically, in the latest changes I have removed the go routine used to clean the cache in favor of using the paint events as the ticker.

Some of my doubts are:

  1. Should we destroy expired renderers independently of a canvas refresh action? (this is the current behavior with this PR)
  2. Texture cache map does not need a mutex because its functions are always called from the same go routine, however as you can see now the texture cache map saves a reference for the canvas which the primitive belongs to, by doing it we could use that map (only for primitives) inside CanvasForObject instead of the canvases cache map, this action would allow us to reduce the canvases cache map (because it would not contain primitives anymore) and delete more quickly them when they are destroyed (if we use canvases cache map we would need to wait them for expiration), however this implies to add a mutex to the textures cache map (CanvasForObject can be called from anywhere), what do you think? Should we keep all objects in canvases cache or just the ones that are not primitives?

@andydotxyz
Copy link
Member

  1. This is a good question. I think it was put that way for thinking it was faster. But other approaches probably OK :)
  2. I'm not sure about the underlying thought here. Many of our primitives don't translate to graphics primitives (yet?). We need to rasterise some elements (like rectangles with borders, and lines) so that's why the cache exists. Does that impact your thoughts?

@fpabl0
Copy link
Member Author

fpabl0 commented Mar 22, 2021

This is a good question. I think it was put that way for thinking it was faster. But other approaches probably OK :)

Indeed it is faster. Current PR use both of them (destroy on refresh and destroy when expired). However destroy on refresh will not destroy objects that are out of scope (because they no longer belonged to the object tree).

I'm not sure about the underlying thought here. Many of our primitives don't translate to graphics primitives (yet?). We need to rasterise some elements (like rectangles with borders, and lines) so that's why the cache exists. Does that impact your thoughts?

Hmm for primitives I mean the objects created with canvas.New... (Rectangle, Image, Text and others) they all are textures for gl and so they are saved in the textures cache map. Basically this is the functionality of every cache map:

  • rendereres map => widgets associated with their renderers
  • textures map => canvas.New... objects associated with their texture.
  • canvases map => all objects (widgets, containers and canvas.New... objects)

Textures are created and destroyed really fast. The destroy action will delete immediately the texture from the textures cache map but not from the canvases map (we would need to wait them to expire to release that memory).

So if we change the map data to:

  • rendereres map => widgets associated with their renderers
  • textures map => canvas.New... objects associated with their texture and canvas.
  • canvases map => all objects (only widgets and containers)

So canvases map will not longer hold texture objects, and by doing so, we would just need to delete the texture from the textures cache map.

Then CanvasForObject should only check the type of the object. If it is a widget or container, it will use the canvases map, otherwise it will use the textures map to get its canvas.

@andydotxyz
Copy link
Member

However destroy on refresh will not destroy objects that are out of scope (because they no longer belonged to the object tree).

Good point!

So if we change the map data to:

  • rendereres map => widgets associated with their renderers
  • textures map => canvas.New... objects associated with their texture and canvas.
  • canvases map => all objects (only widgets and containers)

So canvases map will not longer hold texture objects, and by doing so, we would just need to delete the texture from the textures cache map.

I don't think this is wise from what I have read - the texture cache refers to the current rendering of an object. If this object is changed and Refresh() called then the object should remain in the canvases map, but it's texture needs to be invalidated.

@fpabl0
Copy link
Member Author

fpabl0 commented Mar 23, 2021

I don't think this is wise from what I have read - the texture cache refers to the current rendering of an object. If this object is changed and Refresh() called then the object should remain in the canvases map, but it's texture needs to be invalidated.

On yes, you are right, I missed that fact. Thanks :)

…o theme changes and remove 10ms delay on every paint event
@fpabl0
Copy link
Member Author

fpabl0 commented Mar 25, 2021

@andydotxyz I have moved the svg_cache to the cache package and removed the 10 ms delay in mobile driver. Could you a do quick review in internal/driver/gomobile/driver.go if you are agreed with the changes? I think that change probably solve the theme change issue on mobile.

@fpabl0
Copy link
Member Author

fpabl0 commented Apr 9, 2021

@andydotxyz Ok this is almost done. Desktop side is ready (maybe better naming for the added cache functions is needed), however on mobile side I think a refactorization is needed specially to rid off the minSizeCache and use renderCacheNode-canvasesCache solution used in desktop (or viceversa, I am not sure what is the better solution, maybe was it discussed before?)

So should I add TODO comments and leave this PR as it is now? Or should this PR wait until mobile driver is refactored (mainly just add the renderCacheNode and queueEvent method)?

@andydotxyz
Copy link
Member

Or should this PR wait until mobile driver is refactored (mainly just add the renderCacheNode and queueEvent method)?

It would make it a lot easier to apply this in a shared-code manner if the mobile driver had the equivalent of 7461574 applied.

I am nervous that the drivers are moving further apart instead of coming together, so let's not make the differences and performance gap greater yet.

@fpabl0
Copy link
Member Author

fpabl0 commented Apr 12, 2021

It would make it a lot easier to apply this in a shared-code manner if the mobile driver had the equivalent of 7461574 applied.

@andydotxyz Totally agreed. So should I open another PR as the initial step to try sharing code between the two drivers? Then this PR should wait until that PR is landed. Is that ok?

@andydotxyz
Copy link
Member

@andydotxyz Totally agreed. So should I open another PR as the initial step to try sharing code between the two drivers? Then this PR should wait until that PR is landed. Is that ok?

If you're OK doing that I think it sounds like a great idea

internal/cache/base.go Outdated Show resolved Hide resolved
@fpabl0 fpabl0 marked this pull request as ready for review May 13, 2021 20:24
internal/cache/widget.go Outdated Show resolved Hide resolved
@fpabl0
Copy link
Member Author

fpabl0 commented Jun 2, 2021

Yes, it is :)

Copy link
Member

@Jacalz Jacalz left a comment

Choose a reason for hiding this comment

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

This is looking really good. Just have a few suggestions for changes :)

internal/cache/texture_common.go Outdated Show resolved Hide resolved
internal/cache/base.go Outdated Show resolved Hide resolved
internal/cache/base.go Show resolved Hide resolved
internal/cache/base.go Outdated Show resolved Hide resolved
Co-authored-by: Jacob <Jacalz@users.noreply.github.com>
@fyne-io fyne-io deleted a comment from ddkwork Jun 3, 2021
Jacalz
Jacalz previously approved these changes Jun 3, 2021
Copy link
Member

@Jacalz Jacalz left a comment

Choose a reason for hiding this comment

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

Thanks for this. It is certainly a good improvement. I checked the memory usage before and after and and it certainly does seem to be much better at releasing memory. I still see the memory slowly climbing without me doing anything inside the application, but I guess that's another issue :)

@fpabl0
Copy link
Member Author

fpabl0 commented Jun 3, 2021

I still see the memory slowly climbing without me doing anything inside the application

Hmm the memory (after some time) should be constant (without increasing) 🤔. Could you share the code you are using to test this?

I think I should add some test cases, I will try to add them later today.

@Jacalz
Copy link
Member

Jacalz commented Jun 3, 2021

I still see the memory slowly climbing without me doing anything inside the application

Hmm the memory (after some time) should be constant (without increasing) 🤔. Could you share the code you are using to test this?

I was just using fyne_demo inside the branch that this PR is based on (on Linux). I think it started at 22Mb and then it slowly climbed to 29Mb. I don't know if this was before or after the time it should have stabilised in.

Copy link
Member

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

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

Great work, but it's not quite in-keeping with our code style.
You've included a lot of comments, some that are blocks to separate methods, and some that are telling what a line of code does - both seem superfluous.
If code needs a comment then it generally should be more cleanly written.

Additionally cache/base.go adds a lot of new code but I don't think that any tests cover it.

cleanTaskInterval = cacheDuration / 2

canvasRefreshCh = make(chan struct{}, 1)
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?

internal/cache/base.go Show resolved Hide resolved
internal/cache/base.go Outdated Show resolved Hide resolved
internal/cache/base.go Outdated Show resolved Hide resolved
internal/cache/base.go Outdated Show resolved Hide resolved
internal/cache/base.go Outdated Show resolved Hide resolved
internal/cache/base.go Outdated Show resolved Hide resolved
internal/driver/glfw/window.go Outdated Show resolved Hide resolved
internal/driver/glfw/window.go Outdated Show resolved Hide resolved
@fpabl0
Copy link
Member Author

fpabl0 commented Jun 4, 2021

I was just using fyne_demo inside the branch that this PR is based on (on Linux). I think it started at 22Mb and then it slowly climbed to 29Mb. I don't know if this was before or after the time it should have stabilised in.

Hmm in fyne_demo, is a bit difficult to test this, because if you go to the screens that already have memory leaks, then the memory will never be stable

@fpabl0
Copy link
Member Author

fpabl0 commented Jun 4, 2021

Additionally cache/base.go adds a lot of new code but I don't think that any tests cover it.

Yes, you are right, I will try to add them later today.

@fpabl0
Copy link
Member Author

fpabl0 commented Jun 4, 2021

@andydotxyz I have worked in some of your suggestions, waiting for your review now. If everything goes fine, I will start writing the tests.

@fpabl0 fpabl0 requested a review from andydotxyz June 4, 2021 20:28
…() for glfw driver as suggested in PR review
@andydotxyz
Copy link
Member

Looking good thanks. If tests can be added things should be good.

@fpabl0
Copy link
Member Author

fpabl0 commented Jun 14, 2021

Looking good thanks. If tests can be added things should be good.

Great :), I'll try to add them tonight.

Copy link
Member

@Jacalz Jacalz left a comment

Choose a reason for hiding this comment

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

There are most likely more things we can do to improve the memory usage but this is a great step in the right direction. Nice work! 👍

@andydotxyz
Copy link
Member

Yes, nice work thanks.

@fpabl0 fpabl0 merged commit f14ac77 into fyne-io:develop Jun 18, 2021
@fpabl0 fpabl0 deleted the fix/cache-renderer-memleak branch June 18, 2021 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants