- Add context cancellation to WidgetPool cleanup goroutine - Properly stop ticker when context is canceled - Add BackgroundContext() getter method to App for context access - Pass application context to NewWidgetPool for lifecycle management This fixes a resource leak where the cleanup goroutine would run indefinitely without any way to stop it, preventing proper cleanup on application shutdown.
113 lines
2.6 KiB
Go
113 lines
2.6 KiB
Go
package util
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
)
|
|
|
|
type WidgetType int
|
|
|
|
const (
|
|
WidgetTypeAlbumPageHeader WidgetType = iota
|
|
WidgetTypeArtistPageHeader
|
|
WidgetTypeGridView
|
|
WidgetTypePlaylistPageHeader
|
|
WidgetTypeTracklist
|
|
WidgetTypeCompactTracklist
|
|
WidgetTypeNowPlayingPage
|
|
WidgetTypeGroupedReleases
|
|
|
|
// keep at bottom
|
|
numWidgetTypes
|
|
)
|
|
|
|
const (
|
|
multipleItemsExpiry = 2 * time.Minute
|
|
singleItemExpiry = 5 * time.Minute
|
|
)
|
|
|
|
// A pool to share commonly-used widgets across pages to reduce
|
|
// creation of new widgets and memory allocations.
|
|
type WidgetPool struct {
|
|
mut sync.Mutex
|
|
pools [][]pooledWidget
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
type pooledWidget struct {
|
|
widget fyne.CanvasObject
|
|
releasedAt int64 // unixMillis
|
|
}
|
|
|
|
func NewWidgetPool(ctx context.Context) *WidgetPool {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
p := &WidgetPool{
|
|
pools: make([][]pooledWidget, numWidgetTypes),
|
|
cancel: cancel,
|
|
}
|
|
go func() {
|
|
t := time.NewTicker(2 * time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
p.cleanUpExpiredItems()
|
|
}
|
|
}
|
|
}()
|
|
return p
|
|
}
|
|
|
|
// Obtain obtains a widget of the given type from the pool, if one exists.
|
|
// Returns nil if there is no available widget.
|
|
func (w *WidgetPool) Obtain(typ WidgetType) fyne.CanvasObject {
|
|
w.mut.Lock()
|
|
defer w.mut.Unlock()
|
|
var widget fyne.CanvasObject
|
|
if l := len(w.pools[typ]); l > 0 {
|
|
i := l - 1
|
|
widget = w.pools[typ][i].widget
|
|
w.pools[typ][i].widget = nil
|
|
w.pools[typ] = w.pools[typ][:i]
|
|
}
|
|
return widget
|
|
}
|
|
|
|
// Release releases a widget into the pool.
|
|
// The widget must not be modified by the releaser after release,
|
|
// since it may be Obtained for a new use at any time.
|
|
func (w *WidgetPool) Release(typ WidgetType, wid fyne.CanvasObject) {
|
|
w.mut.Lock()
|
|
defer w.mut.Unlock()
|
|
w.pools[typ] = append(w.pools[typ], pooledWidget{
|
|
widget: wid,
|
|
releasedAt: time.Now().UnixMilli(),
|
|
})
|
|
}
|
|
|
|
func (w *WidgetPool) cleanUpExpiredItems() {
|
|
w.mut.Lock()
|
|
defer w.mut.Unlock()
|
|
for widTyp, pool := range w.pools {
|
|
newP := make([]pooledWidget, 0, len(pool))
|
|
l := len(pool)
|
|
for _, wid := range pool {
|
|
timeSinceRelease := time.Since(time.UnixMilli(wid.releasedAt))
|
|
if l > 1 && timeSinceRelease > multipleItemsExpiry {
|
|
l-- // let expire if >1 item in pool and released long enough ago
|
|
} else if l == 1 && timeSinceRelease > singleItemExpiry {
|
|
l-- // let expire if last item in pool and released long enough ago
|
|
} else {
|
|
newP = append(newP, wid) // not expired
|
|
}
|
|
}
|
|
// re-assign non-expired items back to this widget type pool
|
|
w.pools[widTyp] = newP
|
|
}
|
|
}
|