UWP application settings performance

Development WinUI

7 years ago

Universal Windows Platform includes the ApplicationData API, that provides easy way to store and retrieve application and user settings. If you use it to read settings very often however, you might run into performance problems. How to deal with them?

Settings performance

Retrieving stored settings with the ApplicationData is very simple:

var storedValue = ApplicationData.Current.LocalSettings.Values["key"];

Given a key you can retrieve your setting like it was a simple dictionary. This way it might seem as if this is an in-memory dictionary that gets populated on app startup and is magically persisted behind the scenes. Unfortunately it seems this assumption is not true.

Benchmark

I have created a simple benchmark to illustrate the problem (check it out here on my GitHub). The sample retrieves a setting in two ways. Directly using the ApplicationData API :

private bool PerformReadWithoutCache()
{
   return (bool)ApplicationData.Current.LocalSettings.Values["test"];
}

And indirectly by caching the value first time it is retrieved:

private bool PerformReadWithCache()
{
   if (_cachedValue == null)
   {
      _cachedValue = (bool)ApplicationData.Current.LocalSettings.Values["test"];
   }
   return _cachedValue.Value;
}

The benchmarks runs the read for both cases 100 000 times in a row. Invariably, the results look like this: This is a major difference! Direct access through ApplicationData was almost 1 500 times slower than the cached version! Apparently the caching of ApplicationData API is either only partial or it actually goes through the file on disk to read the value each time it is accessed (although I doubt this, because then the API would surely be asynchronous). In either case direct access is definitely not ideal for performance intensive applications. Update: As Petr Hudeček who first encountered this problem in our software project pointed out, the slowdown is probably caused by the fact that reading the value from ApplicationData many times in a row causes multiple memory allocations in the background and which then leads to many garbage collections that are, as we know, very performance expensive.

Settings service with caching

Because we developers love to keep things encapsulated, we would like to bundle the caching functionality with access to ApplicationData so that we don't have to think about it twice when developing our apps. Because UWP settings support two different locations for the settings, we will first define a helpful enumeration:

internal enum SettingLocality
{
   Local,
   Roamed
}

You may note that there already is a ApplicationDataLocality enum in the UWP API, but I decided against using that because it contains additional values that we will not support like Temporary or SharedLocal store. Now we can start writing the Settings service itself. First we declare a storage field, that will hold the cached settings, that we have already loaded into memory:

private readonly Dictionary<string, object> _settingCache = new Dictionary<string, object>();

To retrieve a setting, we first check if we actually need to read it from ApplicationData or we have a cached copy we can use.

public T GetSetting<T>(
    string key,
    Func<T> defaultValueBuilder,
    SettingLocality locality = SettingLocality.Local,
    bool forceResetCache = false)
{
    object result = null;
    if (forceResetCache || !_settingCache.TryGetValue(key, out result))
    {
        var container = locality == SettingLocality.Roamed ?
        ApplicationData.Current.RoamingSettings :
        ApplicationData.Current.LocalSettings;
        _settingCache[key] = RetrieveSettingFromApplicationData(key, defaultValueBuilder, container);
    }
    return (T)_settingCache[key];
}

There is a little bit more to this code:

  • We give give the method a Func<T> that will create a default value if the setting is not stored yet in ApplicationData

  • We use the SettingLocality enum to decide if the setting should be local or roamed

  • The forceResetCache parameter makes it possible to force read from ApplicationData , which might be useful for example when roaming settings are newly synced

    The inner RetrieveSettingFromApplicationData method simply tries to read the setting and returns the default value if not found. We have also included a check to ensure the stored value is actually of the right type.

private T RetrieveSettingFromApplicationData<T>(
   string key,
   Func<T> defaultValueBuilder,
   ApplicationDataContainer container)
{
    object result = null;
    if (container.Values.TryGetValue(key, out result))
    {
        //get existing
        try
        {
            return (T)result;
        }
        catch
        {
            //invalid value for the given type, remove
            container.Values.Remove(key);
        }
    }
    return defaultValueBuilder();
}

Now finally we have to store the setting, and that is extremely easy in comparison:

public void SetSetting<T>(
   string key,
   T value,
   SettingLocality locality = SettingLocality.Local)
{
    var container = locality == SettingLocality.Roamed ?
       ApplicationData.Current.RoamingSettings : 
       ApplicationData.Current.LocalSettings;
    container.Values[key] = value;
    //ensure cache is invalidated
    _settingCache.Remove(key);
}

We just store the value and invalidate the cache so that the next access will in fact read the new value.

Source code

The complete code for the settings service is available on my GitHub.

Summary

We have found out that the ApplicationData API is very convenient but also quite slow if we need to use it very often. For some applications it might even become a performance bottleneck. Fortunately, it is quite easy to avoid this issue using caching.