base.go 5.5 KB

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