Výkonnost nastavení UWP aplikací

Development WinUI

7 years ago

Univerzální platforma Windows obsahuje ApplicationData API, které poskytuje snadnou cestu pro ukládání a čtení aplikačních a uživatelských nastavení. Pokud jej však ve vaší aplikaci potřebujete používat velmi často, můžete narazit na výkonnostní problémy. Jak s nimi naložit?

Výkonnost nastavení

Čtení uložených nastavení pomocí ApplicationData je velmi jednoduché:

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

Pomocí klíče můžete přečíst nastavení jako z obyčejného slovníku. Z tohoto důvodu to může vypadat, že ve skutečnosti přistupujeme pouze ke slovníku, který je plně uložen v paměti a nějakým magickým způsobem je automaticky persistován na pevný disk na pozadí. Bohužel to vypadá, že tato doměnka není úplně správná.

Benchmark

Pro ilustraci problému jsem vytvořil jednoduchý benchmark (můžete si jej prohlédnout zde na mém GitHubu). Tato aplikace provádí čtení nastavení dvěma způsoby: Přímo pomocí ApplicationData API :

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

A nepřímo cachováním hodnoty po prvním přečtení nastavení:

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

Benchmark toto čtení provádí oběma způsoby 100 000 krát za sebou. Bez výjimky vypadá výsledek podobně jako na následujícím obrázku: Rozdíl mezi oběma přístupy je markantní! Přímý přístup přes ApplicationData byl v tomto případě téměř 1 500krát pomalejší než verze s cachováním! Vypadá to, že ApplicationData API cachuje pouze částečně nebo ve skutečnosti čte nastavení přímo z disku (ačkoliv o tomto pochybuji, protože rozhraní by pak bylo pravděpodobně asynchronní). V každém případě není přímý přístup bez cachování ideální pro výkonnostně intenzivní aplikace. Aktualizace: Jak podotkl Petr Hudeček, který na problém narazil první při práci na našem softwarovém projektu, nízká rychlost je pravděpdobně způsobena tím, že čtení hodnoty nastavení z ApplicationData způsobuje alokace paměti na pozadí a to v mnoha opakováních vede na mnoho cyklů garbage collection, které jsou, jak víme, velmi drahé z hlediska výkonu.

Settings service s cachováním

Protože my vývojáři máme rádi zapouzdřování, chtěli bychom určitě zabalit caching s přístupem k ApplicationData tak, abychom nad touto logikou nemuseli přemýšlet při vývoji aplikace. Protože UWP nastavení podporují dvě různé lokace nastavení, nejprve definujeme užitečný výčtový typ:

internal enum SettingLocality
{
   Local,
   Roamed
}

Můžete si všimnout, že již v UWP API existuje typ ApplicationDataLocality. Rozhodl jsem se ale tento typ nepoužívat, protože obsahuje další hodnoty, které pro nastavení nejsou podporována jako Temporary či SharedLocal . Nyní můžeme začít psát samotnou Settings service. Nejprve deklarujeme slovník, který bude obsahovat cachovaná nastavení, která již byla načtena do paměti:

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

Pro načtení nastevní nejprve zkontrolujeme, zda jej musíme přečíst z ApplicationData nebo již máme k dispozici cachovanou verzi.

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];
}

K tomuto kódu uvedeme ještě několik poznámek:

  • Pomocí parametru typu Func<T> můžeme vytvořit výchozí hodnotu pokud nastavení v ApplicationData není uloženo

  • Používáme výčtový typ SettingLocality abychom určili, zda je nastavení lokální nebo podporuje roaming

  • Parametr forceResetCache umožňuje vynutit čtení z ApplicationData , což může být užitečné například když dojde k synchronizaci roaming settings

    Metoda RetrieveSettingFromApplicationData jednoduše přečte nastavení a vrátí výchozí hodnotu pokud není nalezeno. Také jsme zahrnuli kontrolu, zda uložená hodnota je skutečně správného typu.

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();
}

Nakonec ještě vytvoříme metodu pro uložení nastavení. Ta je v porovnání s předchozími velmi jednoduchá:

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);
}

Hodnotu pouze uložíme a provedeme invalidaci cache, aby příští přístup četl novou hodnotu.

Zdrojový kód

Celý kód Settings service je dostupný na mém GitHubu.

Shrnutí

Ukázali jsme si, že ApplicationData API je velmi praktické, ale může být zároveň pomalé pokud jej potřebujeme používat velmi často. Pro některé aplikace se tak může stát úzkým hrdlem výkonu. Naštěstí je poměrně jednoduché se problému vyhnout pomocí cachování.