From 23fc6a32454277b52e53087a2de21318208ac2e4 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Wed, 8 Jan 2025 12:55:44 +0100 Subject: [PATCH 1/7] Add a generic sync.Map wrapper/helper Adding useful methods and adding some type safety on top. --- app/settings.go | 8 ++-- data/binding/binding.go | 7 +-- data/binding/binding_test.go | 17 ++------ data/binding/gen.go | 4 +- data/binding/listbinding_test.go | 8 ++-- data/binding/pref_helper.go | 34 ++++----------- data/binding/preference.go | 16 +++---- data/binding/treebinding_test.go | 8 ++-- internal/async/map.go | 74 ++++++++++++++++++++++++++++++++ internal/cache/base.go | 10 +---- internal/cache/base_test.go | 16 +++---- internal/cache/svg.go | 15 +++---- internal/cache/svg_test.go | 8 ++-- internal/cache/text.go | 24 ++++------- internal/cache/text_test.go | 4 +- internal/cache/theme.go | 17 ++++---- internal/painter/font.go | 19 +++----- 17 files changed, 159 insertions(+), 130 deletions(-) create mode 100644 internal/async/map.go diff --git a/app/settings.go b/app/settings.go index 80124ec325..3ee1cb205a 100644 --- a/app/settings.go +++ b/app/settings.go @@ -8,6 +8,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/build" "fyne.io/fyne/v2/theme" ) @@ -37,8 +38,8 @@ type settings struct { themeSpecified bool variant fyne.ThemeVariant - changeListeners sync.Map // map[chan fyne.Settings]bool - watcher any // normally *fsnotify.Watcher or nil - avoid import in this file + changeListeners async.Map[chan fyne.Settings, bool] + watcher any // normally *fsnotify.Watcher or nil - avoid import in this file schema SettingsSchema } @@ -116,8 +117,7 @@ func (s *settings) AddChangeListener(listener chan fyne.Settings) { } func (s *settings) apply() { - s.changeListeners.Range(func(key, _ any) bool { - listener := key.(chan fyne.Settings) + s.changeListeners.Range(func(listener chan fyne.Settings, _ bool) bool { select { case listener <- s: default: diff --git a/data/binding/binding.go b/data/binding/binding.go index 4b428e8a8d..37917f5b9f 100644 --- a/data/binding/binding.go +++ b/data/binding/binding.go @@ -9,6 +9,7 @@ import ( "sync" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) var ( @@ -57,7 +58,7 @@ func (l *listener) DataChanged() { } type base struct { - listeners sync.Map // map[DataListener]bool + listeners async.Map[DataListener, bool] lock sync.RWMutex } @@ -75,8 +76,8 @@ func (b *base) RemoveListener(l DataListener) { func (b *base) trigger() { var listeners []DataListener - b.listeners.Range(func(key, _ any) bool { - listeners = append(listeners, key.(DataListener)) + b.listeners.Range(func(listener DataListener, _ bool) bool { + listeners = append(listeners, listener) return true }) diff --git a/data/binding/binding_test.go b/data/binding/binding_test.go index 4ae02d6236..9f5fbe2f00 100644 --- a/data/binding/binding_test.go +++ b/data/binding/binding_test.go @@ -1,34 +1,25 @@ package binding import ( - "sync" "testing" "github.com/stretchr/testify/assert" ) -func syncMapLen(m *sync.Map) (n int) { - m.Range(func(_, _ any) bool { - n++ - return true - }) - return -} - type simpleItem struct { base } func TestBase_AddListener(t *testing.T) { data := &simpleItem{} - assert.Equal(t, 0, syncMapLen(&data.listeners)) + assert.Equal(t, 0, data.listeners.Len()) called := false fn := NewDataListener(func() { called = true }) data.AddListener(fn) - assert.Equal(t, 1, syncMapLen(&data.listeners)) + assert.Equal(t, 1, data.listeners.Len()) assert.True(t, called) } @@ -40,9 +31,9 @@ func TestBase_RemoveListener(t *testing.T) { data := &simpleItem{} data.listeners.Store(fn, true) - assert.Equal(t, 1, syncMapLen(&data.listeners)) + assert.Equal(t, 1, data.listeners.Len()) data.RemoveListener(fn) - assert.Equal(t, 0, syncMapLen(&data.listeners)) + assert.Equal(t, 0, data.listeners.Len()) data.trigger() assert.False(t, called) diff --git a/data/binding/gen.go b/data/binding/gen.go index fce5d11ef4..d1bebd2870 100644 --- a/data/binding/gen.go +++ b/data/binding/gen.go @@ -135,7 +135,7 @@ type prefBound{{ .Name }} struct { func BindPreference{{ .Name }}(key string, p fyne.Preferences) {{ .Name }} { binds := prefBinds.getBindings(p) if binds != nil { - if listen := binds.getItem(key); listen != nil { + if listen, ok := binds.Load(key); listen != nil && ok { if l, ok := listen.({{ .Name }}); ok { return l } @@ -145,7 +145,7 @@ func BindPreference{{ .Name }}(key string, p fyne.Preferences) {{ .Name }} { listen := &prefBound{{ .Name }}{key: key, p: p} binds = prefBinds.ensurePreferencesAttached(p) - binds.setItem(key, listen) + binds.Store(key, listen) return listen } diff --git a/data/binding/listbinding_test.go b/data/binding/listbinding_test.go index 24a656f11a..a67081e0f4 100644 --- a/data/binding/listbinding_test.go +++ b/data/binding/listbinding_test.go @@ -12,14 +12,14 @@ type simpleList struct { func TestListBase_AddListener(t *testing.T) { data := &simpleList{} - assert.Equal(t, 0, syncMapLen(&data.listeners)) + assert.Equal(t, 0, data.listeners.Len()) called := false fn := NewDataListener(func() { called = true }) data.AddListener(fn) - assert.Equal(t, 1, syncMapLen(&data.listeners)) + assert.Equal(t, 1, data.listeners.Len()) data.trigger() assert.True(t, called) @@ -57,9 +57,9 @@ func TestListBase_RemoveListener(t *testing.T) { data := &simpleList{} data.listeners.Store(fn, true) - assert.Equal(t, 1, syncMapLen(&data.listeners)) + assert.Equal(t, 1, data.listeners.Len()) data.RemoveListener(fn) - assert.Equal(t, 0, syncMapLen(&data.listeners)) + assert.Equal(t, 0, data.listeners.Len()) data.trigger() assert.False(t, called) diff --git a/data/binding/pref_helper.go b/data/binding/pref_helper.go index e37c367d3a..4589274375 100644 --- a/data/binding/pref_helper.go +++ b/data/binding/pref_helper.go @@ -1,9 +1,8 @@ package binding import ( - "sync" - "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) type preferenceItem interface { @@ -11,32 +10,20 @@ type preferenceItem interface { } type preferenceBindings struct { - items sync.Map // map[string]preferenceItem -} - -func (b *preferenceBindings) getItem(key string) preferenceItem { - val, loaded := b.items.Load(key) - if !loaded { - return nil - } - return val.(preferenceItem) + async.Map[string, preferenceItem] } func (b *preferenceBindings) list() []preferenceItem { ret := []preferenceItem{} - b.items.Range(func(_, val any) bool { - ret = append(ret, val.(preferenceItem)) + b.Range(func(_ string, item preferenceItem) bool { + ret = append(ret, item) return true }) return ret } -func (b *preferenceBindings) setItem(key string, item preferenceItem) { - b.items.Store(key, item) -} - type preferencesMap struct { - prefs sync.Map // map[fyne.Preferences]*preferenceBindings + prefs async.Map[fyne.Preferences, *preferenceBindings] appPrefs fyne.Preferences // the main application prefs, to check if it changed... } @@ -48,11 +35,11 @@ func newPreferencesMap() *preferencesMap { func (m *preferencesMap) ensurePreferencesAttached(p fyne.Preferences) *preferenceBindings { binds, loaded := m.prefs.LoadOrStore(p, &preferenceBindings{}) if loaded { - return binds.(*preferenceBindings) + return binds } p.AddChangeListener(func() { m.preferencesChanged(fyne.CurrentApp().Preferences()) }) - return binds.(*preferenceBindings) + return binds } func (m *preferencesMap) getBindings(p fyne.Preferences) *preferenceBindings { @@ -63,11 +50,8 @@ func (m *preferencesMap) getBindings(p fyne.Preferences) *preferenceBindings { m.migratePreferences(m.appPrefs, p) } } - binds, loaded := m.prefs.Load(p) - if !loaded { - return nil - } - return binds.(*preferenceBindings) + binds, _ := m.prefs.Load(p) + return binds } func (m *preferencesMap) preferencesChanged(p fyne.Preferences) { diff --git a/data/binding/preference.go b/data/binding/preference.go index 81b0b7c1be..47444fa526 100644 --- a/data/binding/preference.go +++ b/data/binding/preference.go @@ -25,7 +25,7 @@ type prefBoundBool struct { func BindPreferenceBool(key string, p fyne.Preferences) Bool { binds := prefBinds.getBindings(p) if binds != nil { - if listen := binds.getItem(key); listen != nil { + if listen, ok := binds.Load(key); listen != nil && ok { if l, ok := listen.(Bool); ok { return l } @@ -35,7 +35,7 @@ func BindPreferenceBool(key string, p fyne.Preferences) Bool { listen := &prefBoundBool{key: key, p: p} binds = prefBinds.ensurePreferencesAttached(p) - binds.setItem(key, listen) + binds.Store(key, listen) return listen } @@ -80,7 +80,7 @@ type prefBoundFloat struct { func BindPreferenceFloat(key string, p fyne.Preferences) Float { binds := prefBinds.getBindings(p) if binds != nil { - if listen := binds.getItem(key); listen != nil { + if listen, ok := binds.Load(key); listen != nil && ok { if l, ok := listen.(Float); ok { return l } @@ -90,7 +90,7 @@ func BindPreferenceFloat(key string, p fyne.Preferences) Float { listen := &prefBoundFloat{key: key, p: p} binds = prefBinds.ensurePreferencesAttached(p) - binds.setItem(key, listen) + binds.Store(key, listen) return listen } @@ -135,7 +135,7 @@ type prefBoundInt struct { func BindPreferenceInt(key string, p fyne.Preferences) Int { binds := prefBinds.getBindings(p) if binds != nil { - if listen := binds.getItem(key); listen != nil { + if listen, ok := binds.Load(key); listen != nil && ok { if l, ok := listen.(Int); ok { return l } @@ -145,7 +145,7 @@ func BindPreferenceInt(key string, p fyne.Preferences) Int { listen := &prefBoundInt{key: key, p: p} binds = prefBinds.ensurePreferencesAttached(p) - binds.setItem(key, listen) + binds.Store(key, listen) return listen } @@ -190,7 +190,7 @@ type prefBoundString struct { func BindPreferenceString(key string, p fyne.Preferences) String { binds := prefBinds.getBindings(p) if binds != nil { - if listen := binds.getItem(key); listen != nil { + if listen, ok := binds.Load(key); listen != nil && ok { if l, ok := listen.(String); ok { return l } @@ -200,7 +200,7 @@ func BindPreferenceString(key string, p fyne.Preferences) String { listen := &prefBoundString{key: key, p: p} binds = prefBinds.ensurePreferencesAttached(p) - binds.setItem(key, listen) + binds.Store(key, listen) return listen } diff --git a/data/binding/treebinding_test.go b/data/binding/treebinding_test.go index af624b38b5..eba1739224 100644 --- a/data/binding/treebinding_test.go +++ b/data/binding/treebinding_test.go @@ -8,14 +8,14 @@ import ( func TestTreeBase_AddListener(t *testing.T) { data := newSimpleTree() - assert.Equal(t, 0, syncMapLen(&data.listeners)) + assert.Equal(t, 0, data.listeners.Len()) called := false fn := NewDataListener(func() { called = true }) data.AddListener(fn) - assert.Equal(t, 1, syncMapLen(&data.listeners)) + assert.Equal(t, 1, data.listeners.Len()) data.trigger() assert.True(t, called) @@ -54,9 +54,9 @@ func TestTreeBase_RemoveListener(t *testing.T) { data := newSimpleTree() data.listeners.Store(fn, true) - assert.Equal(t, 1, syncMapLen(&data.listeners)) + assert.Equal(t, 1, data.listeners.Len()) data.RemoveListener(fn) - assert.Equal(t, 0, syncMapLen(&data.listeners)) + assert.Equal(t, 0, data.listeners.Len()) data.trigger() assert.False(t, called) diff --git a/internal/async/map.go b/internal/async/map.go new file mode 100644 index 0000000000..67c3f8ffe5 --- /dev/null +++ b/internal/async/map.go @@ -0,0 +1,74 @@ +package async + +import "sync" + +// Map is a generic wrapper around [sync.Map]. +type Map[K, V any] struct { + sync.Map +} + +// Clear deletes all the entries, resulting in an empty Map. +func (m *Map[K, V]) Clear() { + // TODO: Use m.Map.Clear() when Go 1.23 is base. + m.Map.Range(func(key, _ any) bool { + m.Map.Delete(key) + return true + }) +} + +// Delete deletes the value for a key. +func (m *Map[K, V]) Delete(key K) { + m.Map.Delete(key) +} + +// Len returns the length of the map. It is O(n) over the number of items. +func (m *Map[K, V]) Len() (count int) { + m.Map.Range(func(_, _ any) bool { + count++ + return true + }) + return count +} + +// Load returns the value stored in the map for a key, or nil if no value is present. +// The ok result indicates whether value was found in the map. +func (m *Map[K, V]) Load(key K) (value V, ok bool) { + val, ok := m.Map.Load(key) + if val == nil { + return *new(V), ok + } + return val.(V), ok +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + val, loaded := m.Map.LoadAndDelete(key) + if val == nil { + return *new(V), loaded + } + return val.(V), loaded +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + act, loaded := m.Map.LoadOrStore(key, value) + if act == nil { + return *new(V), loaded + } + return act.(V), loaded +} + +// Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration. +func (m *Map[K, V]) Range(f func(key K, value V) bool) { + m.Map.Range(func(key, value any) bool { + return f(key.(K), value.(V)) + }) +} + +// Store sets the value for a key. +func (m *Map[K, V]) Store(key K, value V) { + m.Map.Store(key, value) +} diff --git a/internal/cache/base.go b/internal/cache/base.go index 98d7a692e1..f46118f5b9 100644 --- a/internal/cache/base.go +++ b/internal/cache/base.go @@ -149,14 +149,8 @@ func CleanCanvases(refreshingCanvases []fyne.Canvas) { // ResetThemeCaches clears all the svg and text size cache maps func ResetThemeCaches() { - svgs.Range(func(key, value any) bool { - svgs.Delete(key) - return true - }) - - fontSizeLock.Lock() - fontSizeCache = map[fontSizeEntry]*fontMetric{} - fontSizeLock.Unlock() + svgs.Clear() + fontSizeCache.Clear() } // destroyExpiredCanvases deletes objects from the canvases cache. diff --git a/internal/cache/base_test.go b/internal/cache/base_test.go index f87ad62767..79089a66ff 100644 --- a/internal/cache/base_test.go +++ b/internal/cache/base_test.go @@ -36,7 +36,7 @@ func TestCacheClean(t *testing.T) { t.Run("no_expired_objects", func(t *testing.T) { lastClean = tm.createTime(10, 20) Clean(false) - assert.Equal(t, syncMapLen(svgs), 40) + assert.Equal(t, syncMapLen(&svgs.Map), 40) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -44,7 +44,7 @@ func TestCacheClean(t *testing.T) { tm.setTime(10, 30) Clean(true) - assert.Equal(t, syncMapLen(svgs), 40) + assert.Equal(t, syncMapLen(&svgs.Map), 40) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -72,7 +72,7 @@ func TestCacheClean(t *testing.T) { Clean(true) assert.Equal(t, tm.now, lastClean) - assert.Equal(t, syncMapLen(svgs), 40) + assert.Equal(t, syncMapLen(&svgs.Map), 40) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -82,14 +82,14 @@ func TestCacheClean(t *testing.T) { lastClean = tm.createTime(10, 11) tm.setTime(11, 12) Clean(false) - assert.Equal(t, syncMapLen(svgs), 20) + assert.Equal(t, syncMapLen(&svgs.Map), 20) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) tm.setTime(11, 42) Clean(false) - assert.Equal(t, syncMapLen(svgs), 0) + assert.Equal(t, syncMapLen(&svgs.Map), 0) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -99,14 +99,14 @@ func TestCacheClean(t *testing.T) { lastClean = tm.createTime(10, 11) tm.setTime(11, 11) Clean(true) - assert.Equal(t, syncMapLen(svgs), 0) + assert.Equal(t, syncMapLen(&svgs.Map), 0) assert.Len(t, renderers, 20) assert.Len(t, canvases, 20) assert.Equal(t, 20, destroyedRenderersCnt) tm.setTime(11, 22) Clean(true) - assert.Equal(t, syncMapLen(svgs), 0) + assert.Equal(t, syncMapLen(&svgs.Map), 0) assert.Len(t, renderers, 0) assert.Len(t, canvases, 0) assert.Equal(t, 40, destroyedRenderersCnt) @@ -266,7 +266,7 @@ func (t *timeMock) setTime(min, sec int) { func testClearAll() { skippedCleanWithCanvasRefresh = false canvases = make(map[fyne.CanvasObject]*canvasInfo, 1024) - svgs.Range(func(key, _ any) bool { + svgs.Range(func(key string, _ *svgInfo) bool { svgs.Delete(key) return true }) diff --git a/internal/cache/svg.go b/internal/cache/svg.go index b21fb711cb..8f163ac444 100644 --- a/internal/cache/svg.go +++ b/internal/cache/svg.go @@ -2,21 +2,21 @@ package cache import ( "image" - "sync" "time" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) -var svgs = &sync.Map{} // make(map[string]*svgInfo) +var svgs async.Map[string, *svgInfo] // GetSvg gets svg image from cache if it exists. func GetSvg(name string, o fyne.CanvasObject, w int, h int) *image.NRGBA { - sinfo, ok := svgs.Load(overriddenName(name, o)) - if !ok || sinfo == nil { + svginfo, ok := svgs.Load(overriddenName(name, o)) + if !ok || svginfo == nil { return nil } - svginfo := sinfo.(*svgInfo) + if svginfo.w != w || svginfo.h != h { return nil } @@ -46,8 +46,7 @@ type svgInfo struct { // destroyExpiredSvgs destroys expired svgs cache data. func destroyExpiredSvgs(now time.Time) { - svgs.Range(func(key, value any) bool { - sinfo := value.(*svgInfo) + svgs.Range(func(key string, sinfo *svgInfo) bool { if sinfo.isExpired(now) { svgs.Delete(key) } @@ -58,7 +57,7 @@ func destroyExpiredSvgs(now time.Time) { func overriddenName(name string, o fyne.CanvasObject) string { if o != nil { // for overridden themes get the cache key right if over, ok := overrides.Load(o); ok { - return over.(*overrideScope).cacheID + name + return over.cacheID + name } } diff --git a/internal/cache/svg_test.go b/internal/cache/svg_test.go index 5296ae0427..e9804ba5be 100644 --- a/internal/cache/svg_test.go +++ b/internal/cache/svg_test.go @@ -20,7 +20,7 @@ func syncMapLen(m *sync.Map) (n int) { func TestSvgCacheGet(t *testing.T) { ResetThemeCaches() img := addToCache("empty.svg", "", 25, 25) - assert.Equal(t, 1, syncMapLen(svgs)) + assert.Equal(t, 1, syncMapLen(&svgs.Map)) newImg := GetSvg("empty.svg", nil, 25, 25) assert.Equal(t, img, newImg) @@ -34,7 +34,7 @@ func TestSvgCacheGet(t *testing.T) { func TestSvgCacheGet_File(t *testing.T) { ResetThemeCaches() img := addFileToCache("testdata/stroke.svg", 25, 25) - assert.Equal(t, 1, syncMapLen(svgs)) + assert.Equal(t, 1, syncMapLen(&svgs.Map)) newImg := GetSvg("testdata/stroke.svg", nil, 25, 25) assert.Equal(t, img, newImg) @@ -48,10 +48,10 @@ func TestSvgCacheGet_File(t *testing.T) { func TestSvgCacheReset(t *testing.T) { ResetThemeCaches() _ = addToCache("empty.svg", "", 25, 25) - assert.Equal(t, 1, syncMapLen(svgs)) + assert.Equal(t, 1, syncMapLen(&svgs.Map)) ResetThemeCaches() - assert.Equal(t, 0, syncMapLen(svgs)) + assert.Equal(t, 0, syncMapLen(&svgs.Map)) } func addFileToCache(path string, w, h int) image.Image { diff --git a/internal/cache/text.go b/internal/cache/text.go index 17f546c2a7..d9eec25ad5 100644 --- a/internal/cache/text.go +++ b/internal/cache/text.go @@ -2,16 +2,13 @@ package cache import ( "image/color" - "sync" "time" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) -var ( - fontSizeCache = map[fontSizeEntry]*fontMetric{} - fontSizeLock = sync.RWMutex{} -) +var fontSizeCache async.Map[fontSizeEntry, *fontMetric] type fontMetric struct { expiringCache @@ -40,9 +37,7 @@ func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source name = source.Name() } ent := fontSizeEntry{text, fontSize, style, name} - fontSizeLock.RLock() - ret, ok := fontSizeCache[ent] - fontSizeLock.RUnlock() + ret, ok := fontSizeCache.Load(ent) if !ok { return fyne.Size{Width: 0, Height: 0}, 0 } @@ -59,18 +54,15 @@ func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source ent := fontSizeEntry{text, fontSize, style, name} metric := &fontMetric{size: size, baseLine: base} metric.setAlive() - fontSizeLock.Lock() - fontSizeCache[ent] = metric - fontSizeLock.Unlock() + fontSizeCache.Store(ent, metric) } // destroyExpiredFontMetrics destroys expired fontSizeCache entries func destroyExpiredFontMetrics(now time.Time) { - fontSizeLock.Lock() - for k, v := range fontSizeCache { + fontSizeCache.Range(func(k fontSizeEntry, v *fontMetric) bool { if v.isExpired(now) { - delete(fontSizeCache, k) + fontSizeCache.Delete(k) } - } - fontSizeLock.Unlock() + return true + }) } diff --git a/internal/cache/text_test.go b/internal/cache/text_test.go index af727204fe..b597d65173 100644 --- a/internal/cache/text_test.go +++ b/internal/cache/text_test.go @@ -9,14 +9,14 @@ import ( func TestTextCacheGet(t *testing.T) { ResetThemeCaches() - assert.Equal(t, 0, len(fontSizeCache)) + assert.Equal(t, 0, fontSizeCache.Len()) bound, base := GetFontMetrics("hi", 10, fyne.TextStyle{}, nil) assert.True(t, bound.IsZero()) assert.Equal(t, float32(0), base) SetFontMetrics("hi", 10, fyne.TextStyle{}, nil, fyne.NewSize(10, 10), 8) - assert.Equal(t, 1, len(fontSizeCache)) + assert.Equal(t, 1, fontSizeCache.Len()) bound, base = GetFontMetrics("hi", 10, fyne.TextStyle{}, nil) assert.Equal(t, fyne.NewSize(10, 10), bound) diff --git a/internal/cache/theme.go b/internal/cache/theme.go index 6a50ec2a67..ef06ab5784 100644 --- a/internal/cache/theme.go +++ b/internal/cache/theme.go @@ -2,15 +2,15 @@ package cache import ( "strconv" - "sync" "sync/atomic" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) var ( - overrides = &sync.Map{} // map[fyne.Widget]*overrideScope - overrideCount = atomic.Uint32{} + overrides async.Map[fyne.CanvasObject, *overrideScope] + overrideCount atomic.Uint32 ) type overrideScope struct { @@ -30,32 +30,31 @@ func OverrideTheme(o fyne.CanvasObject, th fyne.Theme) { } func OverrideThemeMatchingScope(o, parent fyne.CanvasObject) bool { - data, ok := overrides.Load(parent) + scope, ok := overrides.Load(parent) if !ok { // not overridden in parent return false } - scope := data.(*overrideScope) overrideTheme(o, scope) return true } func WidgetScopeID(o fyne.CanvasObject) string { - data, ok := overrides.Load(o) + scope, ok := overrides.Load(o) if !ok { return "" } - return data.(*overrideScope).cacheID + return scope.cacheID } func WidgetTheme(o fyne.CanvasObject) fyne.Theme { - data, ok := overrides.Load(o) + scope, ok := overrides.Load(o) if !ok { return nil } - return data.(*overrideScope).th + return scope.th } func overrideContainer(c *fyne.Container, s *overrideScope) { diff --git a/internal/painter/font.go b/internal/painter/font.go index 2c371a182b..2c2517ca1f 100644 --- a/internal/painter/font.go +++ b/internal/painter/font.go @@ -17,6 +17,7 @@ import ( "golang.org/x/image/math/fixed" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/theme" @@ -111,7 +112,7 @@ func CachedFontFace(style fyne.TextStyle, source fyne.Resource, o fyne.CanvasObj val = &FontCacheItem{Fonts: faces} fontCustomCache.Store(source, val) } - return val.(*FontCacheItem) + return val } scope := "" @@ -157,19 +158,13 @@ func CachedFontFace(style fyne.TextStyle, source fyne.Resource, o fyne.CanvasObj fontCache.Store(cacheID{style: style, scope: scope}, val) } - return val.(*FontCacheItem) + return val } // ClearFontCache is used to remove cached fonts in the case that we wish to re-load Font faces func ClearFontCache() { - fontCache.Range(func(key, _ any) bool { - fontCache.Delete(key) - return true - }) - fontCustomCache.Range(func(key, _ any) bool { - fontCustomCache.Delete(key) - return true - }) + fontCache.Clear() + fontCustomCache.Clear() } // DrawString draws a string into an image. @@ -350,8 +345,8 @@ type cacheID struct { scope string } -var fontCache = &sync.Map{} // map[cacheID]*FontCacheItem -var fontCustomCache = &sync.Map{} // map[string]*FontCacheItem for custom resources +var fontCache async.Map[cacheID, *FontCacheItem] +var fontCustomCache async.Map[fyne.Resource, *FontCacheItem] // for custom resources type noopLogger struct{} From a586a11a642e2f1bd2d357a36efae1049f33c95e Mon Sep 17 00:00:00 2001 From: Jacalz Date: Wed, 8 Jan 2025 13:00:30 +0100 Subject: [PATCH 2/7] Remove one syncMapLen function --- internal/cache/base_test.go | 14 +++++++------- internal/cache/svg_test.go | 17 ++++------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/internal/cache/base_test.go b/internal/cache/base_test.go index 79089a66ff..1bac1ce68e 100644 --- a/internal/cache/base_test.go +++ b/internal/cache/base_test.go @@ -36,7 +36,7 @@ func TestCacheClean(t *testing.T) { t.Run("no_expired_objects", func(t *testing.T) { lastClean = tm.createTime(10, 20) Clean(false) - assert.Equal(t, syncMapLen(&svgs.Map), 40) + assert.Equal(t, svgs.Len(), 40) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -44,7 +44,7 @@ func TestCacheClean(t *testing.T) { tm.setTime(10, 30) Clean(true) - assert.Equal(t, syncMapLen(&svgs.Map), 40) + assert.Equal(t, svgs.Len(), 40) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -72,7 +72,7 @@ func TestCacheClean(t *testing.T) { Clean(true) assert.Equal(t, tm.now, lastClean) - assert.Equal(t, syncMapLen(&svgs.Map), 40) + assert.Equal(t, svgs.Len(), 40) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -82,14 +82,14 @@ func TestCacheClean(t *testing.T) { lastClean = tm.createTime(10, 11) tm.setTime(11, 12) Clean(false) - assert.Equal(t, syncMapLen(&svgs.Map), 20) + assert.Equal(t, svgs.Len(), 20) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) tm.setTime(11, 42) Clean(false) - assert.Equal(t, syncMapLen(&svgs.Map), 0) + assert.Equal(t, svgs.Len(), 0) assert.Len(t, renderers, 40) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) @@ -99,14 +99,14 @@ func TestCacheClean(t *testing.T) { lastClean = tm.createTime(10, 11) tm.setTime(11, 11) Clean(true) - assert.Equal(t, syncMapLen(&svgs.Map), 0) + assert.Equal(t, svgs.Len(), 0) assert.Len(t, renderers, 20) assert.Len(t, canvases, 20) assert.Equal(t, 20, destroyedRenderersCnt) tm.setTime(11, 22) Clean(true) - assert.Equal(t, syncMapLen(&svgs.Map), 0) + assert.Equal(t, svgs.Len(), 0) assert.Len(t, renderers, 0) assert.Len(t, canvases, 0) assert.Equal(t, 40, destroyedRenderersCnt) diff --git a/internal/cache/svg_test.go b/internal/cache/svg_test.go index e9804ba5be..dc461b9e79 100644 --- a/internal/cache/svg_test.go +++ b/internal/cache/svg_test.go @@ -2,25 +2,16 @@ package cache import ( "image" - "sync" "testing" "fyne.io/fyne/v2" "github.com/stretchr/testify/assert" ) -func syncMapLen(m *sync.Map) (n int) { - m.Range(func(_, _ any) bool { - n++ - return true - }) - return -} - func TestSvgCacheGet(t *testing.T) { ResetThemeCaches() img := addToCache("empty.svg", "", 25, 25) - assert.Equal(t, 1, syncMapLen(&svgs.Map)) + assert.Equal(t, 1, svgs.Len()) newImg := GetSvg("empty.svg", nil, 25, 25) assert.Equal(t, img, newImg) @@ -34,7 +25,7 @@ func TestSvgCacheGet(t *testing.T) { func TestSvgCacheGet_File(t *testing.T) { ResetThemeCaches() img := addFileToCache("testdata/stroke.svg", 25, 25) - assert.Equal(t, 1, syncMapLen(&svgs.Map)) + assert.Equal(t, 1, svgs.Len()) newImg := GetSvg("testdata/stroke.svg", nil, 25, 25) assert.Equal(t, img, newImg) @@ -48,10 +39,10 @@ func TestSvgCacheGet_File(t *testing.T) { func TestSvgCacheReset(t *testing.T) { ResetThemeCaches() _ = addToCache("empty.svg", "", 25, 25) - assert.Equal(t, 1, syncMapLen(&svgs.Map)) + assert.Equal(t, 1, svgs.Len()) ResetThemeCaches() - assert.Equal(t, 0, syncMapLen(&svgs.Map)) + assert.Equal(t, 0, svgs.Len()) } func addFileToCache(path string, w, h int) image.Image { From 298cc19c7c638aedfe5254489bbed0752c483e94 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Wed, 8 Jan 2025 17:15:18 +0100 Subject: [PATCH 3/7] Use faster Clear() on Go 1.23 and newer --- internal/async/map.go | 9 --------- internal/async/map_clear.go | 12 ++++++++++++ internal/async/map_clear_go1.23.go | 8 ++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 internal/async/map_clear.go create mode 100644 internal/async/map_clear_go1.23.go diff --git a/internal/async/map.go b/internal/async/map.go index 67c3f8ffe5..5f95661ca5 100644 --- a/internal/async/map.go +++ b/internal/async/map.go @@ -7,15 +7,6 @@ type Map[K, V any] struct { sync.Map } -// Clear deletes all the entries, resulting in an empty Map. -func (m *Map[K, V]) Clear() { - // TODO: Use m.Map.Clear() when Go 1.23 is base. - m.Map.Range(func(key, _ any) bool { - m.Map.Delete(key) - return true - }) -} - // Delete deletes the value for a key. func (m *Map[K, V]) Delete(key K) { m.Map.Delete(key) diff --git a/internal/async/map_clear.go b/internal/async/map_clear.go new file mode 100644 index 0000000000..92cce64694 --- /dev/null +++ b/internal/async/map_clear.go @@ -0,0 +1,12 @@ +//go:build !go1.23 + +package async + +// Clear deletes all the entries, resulting in an empty Map. +// This is O(n) over the number of entries when not using Go 1.23 or newer. +func (m *Map[K, V]) Clear() { + m.Map.Range(func(key, _ any) bool { + m.Map.Delete(key) + return true + }) +} diff --git a/internal/async/map_clear_go1.23.go b/internal/async/map_clear_go1.23.go new file mode 100644 index 0000000000..c5b01c0424 --- /dev/null +++ b/internal/async/map_clear_go1.23.go @@ -0,0 +1,8 @@ +//go:build go1.23 + +package async + +// Clear deletes all the entries, resulting in an empty Map. +func (m *Map[K, V]) Clear() { + m.Map.Clear() // More efficient than O(n) range and delete in older Go. +} From 86505c0c2261e72433dbceb129c533521660583a Mon Sep 17 00:00:00 2001 From: Jacalz Date: Wed, 8 Jan 2025 17:52:09 +0100 Subject: [PATCH 4/7] Add tests for map wrapper --- internal/async/map_test.go | 100 +++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 internal/async/map_test.go diff --git a/internal/async/map_test.go b/internal/async/map_test.go new file mode 100644 index 0000000000..7190201990 --- /dev/null +++ b/internal/async/map_test.go @@ -0,0 +1,100 @@ +package async_test + +import ( + "testing" + + "fyne.io/fyne/v2/internal/async" + "github.com/stretchr/testify/assert" +) + +func TestMap_LoadAndStore(t *testing.T) { + m1 := async.Map[string, int]{} + + m1.Store("1", 1) + assert.Equal(t, 1, m1.Len()) + + num, ok := m1.Load("1") + assert.Equal(t, 1, num) + assert.True(t, ok) + + num, ok = m1.Load("2") + assert.Equal(t, 0, num) + assert.False(t, ok) + + m2 := async.Map[int, *string]{} + + str := "example" + m2.Store(0, &str) + assert.Equal(t, 1, m2.Len()) + + strptr, ok := m2.Load(0) + assert.Equal(t, str, *strptr) + assert.True(t, ok) + + m2.Store(1, nil) + assert.Equal(t, 2, m2.Len()) + + strptr, ok = m2.Load(1) + assert.True(t, strptr == nil) + assert.True(t, ok) + + strptr, ok = m2.Load(3) + assert.True(t, strptr == nil) + assert.False(t, ok) +} + +func TestMap_ClearAndDelete(t *testing.T) { + m := async.Map[int, *string]{} + + str := "example" + m.Store(10, &str) + assert.Equal(t, 1, m.Len()) + + m.Store(11, nil) + assert.Equal(t, 2, m.Len()) + + sum := 0 + m.Range(func(key int, value *string) bool { + sum += key + return true + }) + assert.Equal(t, 21, sum) + + m.Delete(10) + assert.Equal(t, 1, m.Len()) +} + +func TestMap_CombinedLoad(t *testing.T) { + m := async.Map[int, *string]{} + + str := "1" + actual, ok := m.LoadOrStore(1, &str) + assert.Equal(t, &str, actual) + assert.False(t, ok) + + actual, ok = m.LoadOrStore(1, nil) + assert.Equal(t, &str, actual) + assert.True(t, ok) + + m.Store(1, nil) + actual, ok = m.LoadOrStore(1, nil) + assert.True(t, actual == nil) + assert.True(t, ok) + + actual, ok = m.LoadOrStore(2, nil) + assert.True(t, actual == nil) + assert.False(t, ok) + + actual, ok = m.LoadAndDelete(1) + assert.True(t, actual == nil) + assert.True(t, ok) + + actual, ok = m.LoadAndDelete(1) + assert.True(t, actual == nil) + assert.False(t, ok) + + m.Store(1, &str) + actual, ok = m.LoadAndDelete(1) + assert.Equal(t, &str, actual) + assert.True(t, ok) +} From d49900e826279d0006b84e974363ef02ef5fb589 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Thu, 9 Jan 2025 10:02:16 +0100 Subject: [PATCH 5/7] Separate texture caches into two maps --- internal/cache/texture_common.go | 84 +++++++++++++++----------------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/internal/cache/texture_common.go b/internal/cache/texture_common.go index c8e8dea7e4..a363e36c2c 100644 --- a/internal/cache/texture_common.go +++ b/internal/cache/texture_common.go @@ -1,34 +1,36 @@ package cache import ( - "sync" - "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) -var textures = sync.Map{} // map[fyne.CanvasObject]*textureInfo +var ( + textTextures async.Map[FontCacheEntry, *textureInfo] + objectTextures async.Map[fyne.CanvasObject, *textureInfo] +) // DeleteTexture deletes the texture from the cache map. func DeleteTexture(obj fyne.CanvasObject) { - textures.Delete(obj) + objectTextures.Delete(obj) } // GetTextTexture gets cached texture for a text run. func GetTextTexture(ent FontCacheEntry) (TextureType, bool) { - return load(ent) + texInfo, ok := textTextures.Load(ent) + if texInfo == nil || !ok { + return NoTexture, false + } + texInfo.setAlive() + return texInfo.texture, true } // GetTexture gets cached texture. func GetTexture(obj fyne.CanvasObject) (TextureType, bool) { - return load(obj) -} - -func load(obj any) (TextureType, bool) { - t, ok := textures.Load(obj) - if t == nil || !ok { + texInfo, ok := objectTextures.Load(obj) + if texInfo == nil || !ok { return NoTexture, false } - texInfo := t.(*textureInfo) texInfo.setAlive() return texInfo.texture, true } @@ -39,19 +41,17 @@ func load(obj any) (TextureType, bool) { // gl context to ensure textures are deleted from gl. func RangeExpiredTexturesFor(canvas fyne.Canvas, f func(fyne.CanvasObject)) { now := timeNow() - textures.Range(func(key, value any) bool { - if _, ok := key.(FontCacheEntry); ok { - tinfo := value.(*textureInfo) - - // just free text directly when that string/style combo is done - if tinfo.isExpired(now) && tinfo.canvas == canvas { - textures.Delete(key) - tinfo.textFree() - } - return true + textTextures.Range(func(key FontCacheEntry, tinfo *textureInfo) bool { + // Just free text directly when that string/style combo is done. + if tinfo.isExpired(now) && tinfo.canvas == canvas { + textTextures.Delete(key) + tinfo.textFree() } - obj, tinfo := key.(fyne.CanvasObject), value.(*textureInfo) + return true + }) + + objectTextures.Range(func(obj fyne.CanvasObject, tinfo *textureInfo) bool { if tinfo.isExpired(now) && tinfo.canvas == canvas { f(obj) } @@ -65,12 +65,8 @@ func RangeExpiredTexturesFor(canvas fyne.Canvas, f func(fyne.CanvasObject)) { // Note: If this is used to free textures, then it should be called inside a current // gl context to ensure textures are deleted from gl. func RangeTexturesFor(canvas fyne.Canvas, f func(fyne.CanvasObject)) { - textures.Range(func(key, value any) bool { - if _, ok := key.(FontCacheEntry); ok { - return true // do nothing, text cache lives outside the scope of an object - } - - obj, tinfo := key.(fyne.CanvasObject), value.(*textureInfo) + // Do nothing for texture cache, it lives outside the scope of an object. + objectTextures.Range(func(obj fyne.CanvasObject, tinfo *textureInfo) bool { if tinfo.canvas == canvas { f(obj) } @@ -80,13 +76,10 @@ func RangeTexturesFor(canvas fyne.Canvas, f func(fyne.CanvasObject)) { // DeleteTextTexturesFor deletes all text textures for the given canvas. func DeleteTextTexturesFor(canvas fyne.Canvas) { - textures.Range(func(key, value any) bool { - if _, ok := key.(FontCacheEntry); ok { - tinfo := value.(*textureInfo) - if tinfo.canvas == canvas { - textures.Delete(key) - tinfo.textFree() - } + textTextures.Range(func(key FontCacheEntry, tinfo *textureInfo) bool { + if tinfo.canvas == canvas { + textTextures.Delete(key) + tinfo.textFree() } return true }) @@ -94,22 +87,21 @@ func DeleteTextTexturesFor(canvas fyne.Canvas) { // SetTextTexture sets cached texture for a text run. func SetTextTexture(ent FontCacheEntry, texture TextureType, canvas fyne.Canvas, free func()) { - store(ent, texture, canvas, free) + tinfo := prepareTexture(ent, texture, canvas, free) + textTextures.Store(ent, tinfo) } // SetTexture sets cached texture. func SetTexture(obj fyne.CanvasObject, texture TextureType, canvas fyne.Canvas) { - store(obj, texture, canvas, nil) + tinfo := prepareTexture(obj, texture, canvas, nil) + objectTextures.Store(obj, tinfo) } -func store(obj any, texture TextureType, canvas fyne.Canvas, free func()) { - texInfo := &textureInfo{texture: texture} - if free != nil { - texInfo.textFree = free - } - texInfo.canvas = canvas - texInfo.setAlive() - textures.Store(obj, texInfo) +func prepareTexture(obj any, texture TextureType, canvas fyne.Canvas, free func()) *textureInfo { + tinfo := &textureInfo{texture: texture, textFree: free} + tinfo.canvas = canvas + tinfo.setAlive() + return tinfo } // textureCacheBase defines base texture cache object. From 53db8dd6b46b9200a1525913e9ed8cf7741cc507 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Thu, 9 Jan 2025 10:04:19 +0100 Subject: [PATCH 6/7] Fix test clear --- internal/cache/base_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cache/base_test.go b/internal/cache/base_test.go index 1bac1ce68e..ccfecff430 100644 --- a/internal/cache/base_test.go +++ b/internal/cache/base_test.go @@ -3,7 +3,6 @@ package cache import ( "fmt" "os" - "sync" "testing" "time" @@ -270,7 +269,8 @@ func testClearAll() { svgs.Delete(key) return true }) - textures = sync.Map{} + textTextures.Clear() + objectTextures.Clear() renderers = map[fyne.Widget]*rendererInfo{} timeNow = time.Now } From dac93f7087fd1edb35e37ccec2109e2ec199c71a Mon Sep 17 00:00:00 2001 From: Jacalz Date: Thu, 9 Jan 2025 10:18:01 +0100 Subject: [PATCH 7/7] Store renderers using async.Map --- internal/cache/base.go | 19 ++++++------------- internal/cache/base_test.go | 26 +++++++++++++------------- internal/cache/widget.go | 26 ++++++-------------------- 3 files changed, 25 insertions(+), 46 deletions(-) diff --git a/internal/cache/base.go b/internal/cache/base.go index f46118f5b9..bffe6cb952 100644 --- a/internal/cache/base.go +++ b/internal/cache/base.go @@ -77,21 +77,18 @@ func CleanCanvas(canvas fyne.Canvas) { } canvasesLock.Unlock() - renderersLock.Lock() for _, dobj := range deletingObjs { wid, ok := dobj.(fyne.Widget) if !ok { continue } - rinfo, ok := renderers[wid] + rinfo, ok := renderers.LoadAndDelete(wid) if !ok { continue } rinfo.renderer.Destroy() overrides.Delete(wid) - delete(renderers, wid) } - renderersLock.Unlock() } // CleanCanvases runs cache clean tasks for canvases that are being refreshed. This is called on paint events. @@ -129,21 +126,18 @@ func CleanCanvases(refreshingCanvases []fyne.Canvas) { } canvasesLock.Unlock() - renderersLock.Lock() for _, dobj := range deletingObjs { wid, ok := dobj.(fyne.Widget) if !ok { continue } - rinfo, ok := renderers[wid] + rinfo, ok := renderers.LoadAndDelete(wid) if !ok || !rinfo.isExpired(now) { continue } rinfo.renderer.Destroy() overrides.Delete(wid) - delete(renderers, wid) } - renderersLock.Unlock() lastClean = timeNow() } @@ -167,15 +161,14 @@ func destroyExpiredCanvases(now time.Time) { // destroyExpiredRenderers deletes the renderer from the cache and calls // renderer.Destroy() func destroyExpiredRenderers(now time.Time) { - renderersLock.Lock() - for wid, rinfo := range renderers { + renderers.Range(func(wid fyne.Widget, rinfo *rendererInfo) bool { if rinfo.isExpired(now) { rinfo.renderer.Destroy() overrides.Delete(wid) - delete(renderers, wid) + renderers.Delete(wid) } - } - renderersLock.Unlock() + return true + }) } // matchesACanvas returns true if the canvas represented by the canvasInfo object matches one of diff --git a/internal/cache/base_test.go b/internal/cache/base_test.go index ccfecff430..3dc99ef4e6 100644 --- a/internal/cache/base_test.go +++ b/internal/cache/base_test.go @@ -36,7 +36,7 @@ func TestCacheClean(t *testing.T) { lastClean = tm.createTime(10, 20) Clean(false) assert.Equal(t, svgs.Len(), 40) - assert.Len(t, renderers, 40) + assert.Equal(t, 40, renderers.Len()) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) assert.Equal(t, tm.now, lastClean) @@ -44,7 +44,7 @@ func TestCacheClean(t *testing.T) { tm.setTime(10, 30) Clean(true) assert.Equal(t, svgs.Len(), 40) - assert.Len(t, renderers, 40) + assert.Equal(t, 40, renderers.Len()) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) assert.Equal(t, tm.now, lastClean) @@ -72,7 +72,7 @@ func TestCacheClean(t *testing.T) { assert.Equal(t, tm.now, lastClean) assert.Equal(t, svgs.Len(), 40) - assert.Len(t, renderers, 40) + assert.Equal(t, 40, renderers.Len()) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) }) @@ -82,14 +82,14 @@ func TestCacheClean(t *testing.T) { tm.setTime(11, 12) Clean(false) assert.Equal(t, svgs.Len(), 20) - assert.Len(t, renderers, 40) + assert.Equal(t, 40, renderers.Len()) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) tm.setTime(11, 42) Clean(false) assert.Equal(t, svgs.Len(), 0) - assert.Len(t, renderers, 40) + assert.Equal(t, 40, renderers.Len()) assert.Len(t, canvases, 40) assert.Zero(t, destroyedRenderersCnt) }) @@ -99,14 +99,14 @@ func TestCacheClean(t *testing.T) { tm.setTime(11, 11) Clean(true) assert.Equal(t, svgs.Len(), 0) - assert.Len(t, renderers, 20) + assert.Equal(t, 20, renderers.Len()) assert.Len(t, canvases, 20) assert.Equal(t, 20, destroyedRenderersCnt) tm.setTime(11, 22) Clean(true) assert.Equal(t, svgs.Len(), 0) - assert.Len(t, renderers, 0) + assert.Equal(t, 0, renderers.Len()) assert.Len(t, canvases, 0) assert.Equal(t, 40, destroyedRenderersCnt) }) @@ -125,13 +125,13 @@ func TestCacheClean(t *testing.T) { Clean(true) assert.True(t, skippedCleanWithCanvasRefresh) assert.Less(t, lastClean.UnixNano(), tm.now.UnixNano()) - assert.Len(t, renderers, 1) + assert.Equal(t, 1, renderers.Len()) tm.setTime(14, 21) Clean(false) assert.False(t, skippedCleanWithCanvasRefresh) assert.Equal(t, tm.now, lastClean) - assert.Len(t, renderers, 0) + assert.Equal(t, 0, renderers.Len()) }) } @@ -158,11 +158,11 @@ func TestCleanCanvas(t *testing.T) { SetCanvasForObject(dwidget, dcanvas2, nil) } - assert.Len(t, renderers, 42) + assert.Equal(t, 42, renderers.Len()) assert.Len(t, canvases, 42) CleanCanvas(dcanvas1) - assert.Len(t, renderers, 22) + assert.Equal(t, 22, renderers.Len()) assert.Len(t, canvases, 22) assert.Equal(t, 20, destroyedRenderersCnt) for _, cinfo := range canvases { @@ -170,7 +170,7 @@ func TestCleanCanvas(t *testing.T) { } CleanCanvas(dcanvas2) - assert.Len(t, renderers, 0) + assert.Equal(t, 0, renderers.Len()) assert.Len(t, canvases, 0) assert.Equal(t, 42, destroyedRenderersCnt) } @@ -271,6 +271,6 @@ func testClearAll() { }) textTextures.Clear() objectTextures.Clear() - renderers = map[fyne.Widget]*rendererInfo{} + renderers.Clear() timeNow = time.Now } diff --git a/internal/cache/widget.go b/internal/cache/widget.go index 130901076a..a4de5d3ef3 100644 --- a/internal/cache/widget.go +++ b/internal/cache/widget.go @@ -1,13 +1,11 @@ package cache import ( - "sync" - "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) -var renderersLock sync.RWMutex -var renderers = map[fyne.Widget]*rendererInfo{} +var renderers async.Map[fyne.Widget, *rendererInfo] type isBaseWidget interface { ExtendBaseWidget(fyne.Widget) @@ -26,14 +24,10 @@ func Renderer(wid fyne.Widget) fyne.WidgetRenderer { } } - renderersLock.RLock() - rinfo, ok := renderers[wid] - renderersLock.RUnlock() + rinfo, ok := renderers.Load(wid) if !ok { rinfo = &rendererInfo{renderer: wid.CreateRenderer()} - renderersLock.Lock() - renderers[wid] = rinfo - renderersLock.Unlock() + renderers.Store(wid, rinfo) } if rinfo == nil { @@ -48,9 +42,7 @@ func Renderer(wid fyne.Widget) fyne.WidgetRenderer { // DestroyRenderer frees a render implementation for a widget. // This is typically for internal use only. func DestroyRenderer(wid fyne.Widget) { - renderersLock.RLock() - rinfo, ok := renderers[wid] - renderersLock.RUnlock() + rinfo, ok := renderers.LoadAndDelete(wid) if !ok { return } @@ -58,18 +50,12 @@ func DestroyRenderer(wid fyne.Widget) { rinfo.renderer.Destroy() } overrides.Delete(wid) - - renderersLock.Lock() - delete(renderers, wid) - renderersLock.Unlock() } // IsRendered returns true of the widget currently has a renderer. // One will be created the first time a widget is shown but may be removed after it is hidden. func IsRendered(wid fyne.Widget) bool { - renderersLock.RLock() - _, found := renderers[wid] - renderersLock.RUnlock() + _, found := renderers.Load(wid) return found }