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

Re-fix #2449 #2477

Merged
merged 9 commits into from Sep 30, 2021
49 changes: 39 additions & 10 deletions app/preferences.go
Expand Up @@ -14,21 +14,37 @@ import (
type preferences struct {
*internal.InMemoryPreferences

prefLock sync.RWMutex
ignoreChange bool
prefLock sync.RWMutex
// Normally, any update of preferences is immediately written to disk,
nullst marked this conversation as resolved.
Show resolved Hide resolved
// but the initial operation of loading the preferences file performs many separate
// changes. To avoid "load->changes!->write" pattern (rewriting preferences file
// after each loading), saving file to disk is disabled
// during the loading progress. Access guarded by prefLock.
loadingInProgress bool
// If an application changes its preferences 1000 times per second, we don't want to
// rewrite the preferences file after every update. Instead, a time-out mechanism is
// implemented in resetSuspend(), limiting the number of file operations per second.
suspendChange bool
numSuspendedChanges int

app *fyneApp
}

// Declare conformity with Preferences interface
var _ fyne.Preferences = (*preferences)(nil)

func (p *preferences) resetIgnore() {
func (p *preferences) resetSuspend() {
go func() {
time.Sleep(time.Millisecond * 100) // writes are not always atomic. 10ms worked, 100 is safer.
p.prefLock.Lock()
p.ignoreChange = false
p.suspendChange = false
changes := p.numSuspendedChanges
p.numSuspendedChanges = 0
p.prefLock.Unlock()

if changes > 0 {
p.InMemoryPreferences.FireChange()
}
}()
}

Expand All @@ -38,9 +54,9 @@ func (p *preferences) save() error {

func (p *preferences) saveToFile(path string) error {
p.prefLock.Lock()
p.ignoreChange = true
p.suspendChange = true
p.prefLock.Unlock()
defer p.resetIgnore()
defer p.resetSuspend()
err := os.MkdirAll(filepath.Dir(path), 0700)
if err != nil { // this is not an exists error according to docs
return err
Expand Down Expand Up @@ -95,9 +111,18 @@ func (p *preferences) loadFromFile(path string) (err error) {
}()
decode := json.NewDecoder(file)

p.prefLock.Lock()
p.loadingInProgress = true
p.prefLock.Unlock()

p.InMemoryPreferences.WriteValues(func(values map[string]interface{}) {
err = decode.Decode(&values)
})

p.prefLock.Lock()
p.loadingInProgress = false
p.prefLock.Unlock()

return err
}

Expand All @@ -112,10 +137,14 @@ func newPreferences(app *fyneApp) *preferences {
}

p.AddChangeListener(func() {
p.prefLock.RLock()
shouldIgnoreChange := p.ignoreChange
p.prefLock.RUnlock()
if shouldIgnoreChange { // callback after loading, no need to save
p.prefLock.Lock()
shouldIgnoreChange := p.suspendChange || p.loadingInProgress
if p.suspendChange {
p.numSuspendedChanges++
}
p.prefLock.Unlock()

if shouldIgnoreChange { // callback after loading file, or too many updates in a row
return
}

Expand Down
2 changes: 1 addition & 1 deletion app/preferences_other.go
Expand Up @@ -17,7 +17,7 @@ func (a *fyneApp) storageRoot() string {
func (p *preferences) watch() {
watchFile(p.storagePath(), func() {
p.prefLock.RLock()
shouldIgnoreChange := p.ignoreChange
shouldIgnoreChange := p.suspendChange
p.prefLock.RUnlock()
if shouldIgnoreChange {
return
Expand Down
7 changes: 4 additions & 3 deletions internal/preferences.go
Expand Up @@ -41,7 +41,7 @@ func (p *InMemoryPreferences) WriteValues(fn func(map[string]interface{})) {
fn(p.values)
p.lock.Unlock()

p.fireChange()
p.FireChange()
}

func (p *InMemoryPreferences) set(key string, value interface{}) {
Expand All @@ -50,7 +50,7 @@ func (p *InMemoryPreferences) set(key string, value interface{}) {
p.values[key] = value
p.lock.Unlock()

p.fireChange()
p.FireChange()
}

func (p *InMemoryPreferences) get(key string) (interface{}, bool) {
Expand All @@ -68,7 +68,8 @@ func (p *InMemoryPreferences) remove(key string) {
delete(p.values, key)
}

func (p *InMemoryPreferences) fireChange() {
// FireChange causes InMemoryPreferences to activate all its .changeListeners
nullst marked this conversation as resolved.
Show resolved Hide resolved
func (p *InMemoryPreferences) FireChange() {
p.lock.RLock()
defer p.lock.RUnlock()

Expand Down