base.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. package cache
  2. import (
  3. "os"
  4. "sync/atomic"
  5. "time"
  6. "fyne.io/fyne/v2"
  7. )
  8. var (
  9. cacheDuration = 1 * time.Minute
  10. cleanTaskInterval = cacheDuration / 2
  11. expiredObjects = make([]fyne.CanvasObject, 0, 50)
  12. lastClean time.Time
  13. skippedCleanWithCanvasRefresh = false
  14. // testing purpose only
  15. timeNow func() time.Time = time.Now
  16. )
  17. func init() {
  18. if t, err := time.ParseDuration(os.Getenv("FYNE_CACHE")); err == nil {
  19. cacheDuration = t
  20. cleanTaskInterval = cacheDuration / 2
  21. }
  22. }
  23. // Clean run cache clean task, it should be called on paint events.
  24. func Clean(canvasRefreshed bool) {
  25. now := timeNow()
  26. // do not run clean task too fast
  27. if now.Sub(lastClean) < 10*time.Second {
  28. if canvasRefreshed {
  29. skippedCleanWithCanvasRefresh = true
  30. }
  31. return
  32. }
  33. if skippedCleanWithCanvasRefresh {
  34. skippedCleanWithCanvasRefresh = false
  35. canvasRefreshed = true
  36. }
  37. if !canvasRefreshed && now.Sub(lastClean) < cleanTaskInterval {
  38. return
  39. }
  40. destroyExpiredSvgs(now)
  41. if canvasRefreshed {
  42. // Destroy renderers on canvas refresh to avoid flickering screen.
  43. destroyExpiredRenderers(now)
  44. // canvases cache should be invalidated only on canvas refresh, otherwise there wouldn't
  45. // be a way to recover them later
  46. destroyExpiredCanvases(now)
  47. }
  48. lastClean = timeNow()
  49. }
  50. // CleanCanvas performs a complete remove of all the objects that belong to the specified
  51. // canvas. Usually used to free all objects from a closing windows.
  52. func CleanCanvas(canvas fyne.Canvas) {
  53. deletingObjs := make([]fyne.CanvasObject, 0, 50)
  54. canvasesLock.RLock()
  55. for obj, cinfo := range canvases {
  56. if cinfo.canvas == canvas {
  57. deletingObjs = append(deletingObjs, obj)
  58. }
  59. }
  60. canvasesLock.RUnlock()
  61. if len(deletingObjs) == 0 {
  62. return
  63. }
  64. canvasesLock.Lock()
  65. for _, dobj := range deletingObjs {
  66. delete(canvases, dobj)
  67. }
  68. canvasesLock.Unlock()
  69. renderersLock.Lock()
  70. for _, dobj := range deletingObjs {
  71. wid, ok := dobj.(fyne.Widget)
  72. if !ok {
  73. continue
  74. }
  75. winfo, ok := renderers[wid]
  76. if !ok {
  77. continue
  78. }
  79. winfo.renderer.Destroy()
  80. delete(renderers, wid)
  81. }
  82. renderersLock.Unlock()
  83. }
  84. // CleanCanvases runs cache clean tasks for canvases that are being refreshed. This is called on paint events.
  85. func CleanCanvases(refreshingCanvases []fyne.Canvas) {
  86. now := timeNow()
  87. // do not run clean task too fast
  88. if now.Sub(lastClean) < 10*time.Second {
  89. return
  90. }
  91. if now.Sub(lastClean) < cleanTaskInterval {
  92. return
  93. }
  94. destroyExpiredSvgs(now)
  95. deletingObjs := make([]fyne.CanvasObject, 0, 50)
  96. canvasesLock.RLock()
  97. for obj, cinfo := range canvases {
  98. if cinfo.isExpired(now) && matchesACanvas(cinfo, refreshingCanvases) {
  99. deletingObjs = append(deletingObjs, obj)
  100. }
  101. }
  102. canvasesLock.RUnlock()
  103. if len(deletingObjs) == 0 {
  104. return
  105. }
  106. canvasesLock.Lock()
  107. for _, dobj := range deletingObjs {
  108. delete(canvases, dobj)
  109. }
  110. canvasesLock.Unlock()
  111. renderersLock.Lock()
  112. for _, dobj := range deletingObjs {
  113. wid, ok := dobj.(fyne.Widget)
  114. if !ok {
  115. continue
  116. }
  117. rinfo, ok := renderers[wid]
  118. if !ok {
  119. continue
  120. }
  121. if rinfo.isExpired(now) {
  122. rinfo.renderer.Destroy()
  123. delete(renderers, wid)
  124. }
  125. }
  126. renderersLock.Unlock()
  127. lastClean = timeNow()
  128. }
  129. // ResetThemeCaches clears all the svg and text size cache maps
  130. func ResetThemeCaches() {
  131. svgs.Range(func(key, value interface{}) bool {
  132. svgs.Delete(key)
  133. return true
  134. })
  135. fontSizeLock.Lock()
  136. fontSizeCache = map[fontSizeEntry]fontMetric{}
  137. fontSizeLock.Unlock()
  138. }
  139. // destroyExpiredCanvases deletes objects from the canvases cache.
  140. func destroyExpiredCanvases(now time.Time) {
  141. expiredObjects = expiredObjects[:0]
  142. canvasesLock.RLock()
  143. for obj, cinfo := range canvases {
  144. if cinfo.isExpired(now) {
  145. expiredObjects = append(expiredObjects, obj)
  146. }
  147. }
  148. canvasesLock.RUnlock()
  149. if len(expiredObjects) > 0 {
  150. canvasesLock.Lock()
  151. for i, exp := range expiredObjects {
  152. delete(canvases, exp)
  153. expiredObjects[i] = nil
  154. }
  155. canvasesLock.Unlock()
  156. }
  157. }
  158. // destroyExpiredRenderers deletes the renderer from the cache and calls
  159. // renderer.Destroy()
  160. func destroyExpiredRenderers(now time.Time) {
  161. expiredObjects = expiredObjects[:0]
  162. renderersLock.RLock()
  163. for wid, rinfo := range renderers {
  164. if rinfo.isExpired(now) {
  165. rinfo.renderer.Destroy()
  166. expiredObjects = append(expiredObjects, wid)
  167. }
  168. }
  169. renderersLock.RUnlock()
  170. if len(expiredObjects) > 0 {
  171. renderersLock.Lock()
  172. for i, exp := range expiredObjects {
  173. delete(renderers, exp.(fyne.Widget))
  174. expiredObjects[i] = nil
  175. }
  176. renderersLock.Unlock()
  177. }
  178. }
  179. // matchesACanvas returns true if the canvas represented by the canvasInfo object matches one of
  180. // the canvases passed in in 'canvases', otherwise false is returned.
  181. func matchesACanvas(cinfo *canvasInfo, canvases []fyne.Canvas) bool {
  182. canvas := cinfo.canvas
  183. for _, obj := range canvases {
  184. if obj == canvas {
  185. return true
  186. }
  187. }
  188. return false
  189. }
  190. type expiringCache struct {
  191. expires atomic.Value // time.Time
  192. }
  193. // isExpired check if the cache data is expired.
  194. func (c *expiringCache) isExpired(now time.Time) bool {
  195. t := c.expires.Load()
  196. if t == nil {
  197. return (time.Time{}).Before(now)
  198. }
  199. return t.(time.Time).Before(now)
  200. }
  201. // setAlive updates expiration time.
  202. func (c *expiringCache) setAlive() {
  203. c.expires.Store(timeNow().Add(cacheDuration))
  204. }
  205. type expiringCacheNoLock struct {
  206. expires time.Time
  207. }
  208. // isExpired check if the cache data is expired.
  209. func (c *expiringCacheNoLock) isExpired(now time.Time) bool {
  210. return c.expires.Before(now)
  211. }
  212. // setAlive updates expiration time.
  213. func (c *expiringCacheNoLock) setAlive() {
  214. t := timeNow().Add(cacheDuration)
  215. c.expires = t
  216. }