Jednym z największych braków w licznikach wydajności jest brak informacji na temat aktualnego zużycia pamięci. Jednym z wymagań certyfikacyjnych aplikacji dla Windows Phone 7 jest limit 90 MB zużycia pamięci przez aplikację w przypadku telefonów z pamięcią mniejszą niż 256 MB Na chwilę obecną nie ma na rynku telefonów spełniających ten warunek. Teoretycznie nie powinniśmy się tym przejmować. Natomiast zgodnie z informacjami jakie podał Microsoft następna generacja systemu operacyjnego Windows Phone Tango powinna już pozwolić na opracowanie telefonów nisko budżetowych z mniejszą ilością pamięci.

Od tego momentu Microsoft pewnie zacznie sprawdzać stopień zużycia pamięci przez aplikacje na telefonach. Ja proponuję zrobić to szybciej, tak aby być już teraz gotowym na nadejście nowych urządzeń. Obecnie dostępne telefony z Windows Phone 7.5 przydzielają aplikacji od około 200 MB do 350 MB pamięci. Po przekroczeniu tej wartości program jest zamykany przez system operacyjny z wyjątkiem Out of memory exception.

W sytuacji, gdy nasza aplikacja zachowuje się w sposób stabilny i wiemy, że użytkownik swoim działaniem nie jest w stanie zwiększyć zużycia pamięci, jesteśmy w lepszej sytuacji. Musimy tylko w trakcie tworzenia aplikacji sprawdzić czy zachowuje się ona stabilnie. W tym celu można wykorzystać Memory profiler, który jest dostępny z poziomu Visual Studio. Przykład jego działania przedstawia poniższy wykres:

Wykres zużycia pamięci

Nie jest to rozwiązanie wygodne. Informacje o zużywanej pamięci uzyskuje się dopiero po zakończeniu działania programu. O wiele lepszym rozwiązaniem byłoby uzyskiwanie tej informacji na bieżąco. W tym celu proponuję dodać do programu informacje o wykorzystywanej pamięci. Takie informacje można wyświetlać koło liczników systemowych.

Pomiar zużycia pamięci - Koncepcja

W przypadku tego rozwianie proponuję uwzględnić trzy wskaźniki:

  • Current – informacja o obecnie zużywanej pamięci,
  • Available – informacja o ilości pamięci dostępnej dla programu,
  • Peak – maksymalna ilość pamięci zużywanej przez aplikację.

Każda z tych wartości zostanie wyrażona w kb. Dodatkowo wskaźniki Current i Peak powinny zmieniać kolor w zależności od wskazywanej pamięci. Graniczną wartością powinno być 90 MB – limit pamięci dla aplikacji, który występuje w przypadku urządzeń z mniejszą ilością pamięci RAM niż 256 MB. Poziom ten stanowi odwołanie dla dwóch wartości ostrzegawczych:

  • pierwszy poziom – 67,5 MB – wskaźniki zmieniają kolor na żółty,
  • drugi poziom – 81 MB – wskaźniki zmieniają kolor na czerwony.

Poziomy są tak dobrane, aby pełniły rolę znaków ostrzegawczych. W przypadku urządzeń z małą ilością pamięci Microsoft nie zaleca przekraczania 60 MB zużycia pamięci.

Od Mango Microsoft znacznie uprościł sposób pobierania informacji o używanej pamięci, jak również o innych parametrach urządzania. Służy do tego klasa DeviceStatus.

/// <summary>
/// Gets the current memory usage, in bytes.
/// </summary>
/// <value>Current memory usage</value>
private static long CurrentMemoryUsage
{
  get { return DeviceStatus.ApplicationCurrentMemoryUsage; }
}

/// <summary>
/// Gets the peak memory usage, in bytes.
/// </summary>
/// <value>Peak memory usage</value>
private static long PeakMemoryUsage
{
  get { return DeviceStatus.ApplicationPeakMemoryUsage; }
}

/// <summary>
/// Gets the application memory usage limit in bytes.
/// </summary>
private static long ApplicationMemoryUsageLimit
{
  get { return DeviceStatus.ApplicationMemoryUsageLimit; }
}

Całościową implementację zawiera poniższa klasa:

/// <summary>
/// Class that is responsible for memory measurements and display indicators
/// </summary>
public static class MemoryDiagnoster
{
  /// <summary>
  /// Popup control
  /// </summary>
  private static Popup popup;

  /// <summary>
  /// Text block with current memory usage value
  /// </summary>
  private static TextBlock currentMemoryBlock;

  /// <summary>
  /// Text block with peak memory usage value
  /// </summary>
  private static TextBlock peakMemoryBlock;

  /// <summary>
  /// Dispatcher timer
  /// </summary>
  private static DispatcherTimer dispatcherTimer;

  /// <summary>
  /// Decides if garbage collector should be run before each memory reading
  /// </summary>
  private static bool forceGarbageColector;

  /// <summary>
  /// Definition of 90MB memory limit for application on devices with less than 256 RAM
  /// </summary>
  private const long MAX_LIMIT_OF_MEMORY = 90 * 1024 * 1024;

  // Colors definitions
  private static readonly SolidColorBrush redBrush = new SolidColorBrush(Colors.Red);
  private static readonly SolidColorBrush yellowBrush = new SolidColorBrush(Colors.Yellow);
  private static readonly SolidColorBrush greenBrush = new SolidColorBrush(Colors.Green);

  /// <summary>
  /// Definition of last colour used by current memory indicator
  /// </summary>
  private static SolidColorBrush lastCurrentBrush;

  /// <summary>
  /// Definition of last colour used by peak memory indicator
  /// </summary>
  private static SolidColorBrush lastPeakBrush;

  /// <summary>
  /// Gets the current memory usage, in bytes.
  /// </summary>
  /// <value>Current memory usage</value>
  private static long CurrentMemoryUsage
  {
    get { return DeviceStatus.ApplicationCurrentMemoryUsage; }
  }

  /// <summary>
  /// Gets the peak memory usage, in bytes.
  /// </summary>
  /// <value>Peak memory usage</value>
  private static long PeakMemoryUsage
  {
    get { return DeviceStatus.ApplicationPeakMemoryUsage; }
  }

  /// <summary>
  /// Gets the application memory usage limit in bytes.
  /// </summary>
  private static long ApplicationMemoryUsageLimit
  {
    get { return DeviceStatus.ApplicationMemoryUsageLimit; }
  }

  /// <summary>
  /// Starts the memory monitoring process
  /// </summary>
  /// <param name="timespan">The timespan between following memory measurement.</param>
  /// <param name="forceGarbageColector">
  /// if set to <c>true</c> garbage collector is force to run before memory measurement.
  ///</param>
  public static void StartMemoryMonitoring(TimeSpan timespan, bool forceGarbageColector = false)
  {
    if (dispatcherTimer != null)
    {
      throw new InvalidOperationException("Process is already running.");
    }

    lastCurrentBrush = greenBrush;
    lastPeakBrush = greenBrush;
    MemoryDiagnoster.forceGarbageColector = forceGarbageColector;
    ShowControl();
    StartTimer(timespan);
  }

  /// <summary>
  /// Stops the timer and hides the counter
  /// </summary>
  public static void StartMemoryMonitoring()
  {
    HideControl();
    StopTimer();
  }

  /// <summary>
  /// Shows the control with results of memory measurements.
  /// </summary>
  private static void ShowControl()
  {
    popup = new Popup();
    double fontSize = (double)Application.Current.Resources["PhoneFontSizeSmall"] - 2;
    Brush foreground = (Brush)Application.Current.Resources["PhoneForegroundBrush"];
    StackPanel stackPanel = new StackPanel
      {
        Orientation = Orientation.Horizontal,
        Background = (Brush)Application.Current.Resources["PhoneSemitransparentBrush"]
      };
    currentMemoryBlock = new TextBlock
      {
        Text = "---",
        FontSize = fontSize,
        Foreground = lastCurrentBrush
      };
    peakMemoryBlock = new TextBlock
      {
        Text = "---",
        FontSize = fontSize,
        Foreground = lastPeakBrush
      };
    stackPanel.Children.Add(new TextBlock
                              {
                                Text = "Current: ",
                                FontSize = fontSize,
                                Foreground = foreground
                              });
    stackPanel.Children.Add(currentMemoryBlock);
    stackPanel.Children.Add(new TextBlock
                              {
                                Text = " / " + string.Format("{0:N0}",
                                ApplicationMemoryUsageLimit / 1024) ,
                                FontSize = fontSize,
                                Foreground = foreground
                              });
    stackPanel.Children.Add(new TextBlock
                              {
                                Text = " kb Peak: ",
                                FontSize = fontSize,
                                Foreground = foreground
                              });
    stackPanel.Children.Add(peakMemoryBlock);
    stackPanel.Children.Add(new TextBlock
                              {
                                Text = " kb",
                                FontSize = fontSize,
                                Foreground = foreground
                              });
    stackPanel.RenderTransform = new CompositeTransform
                                   {
                                     Rotation = 90,
                                     TranslateX = 480,
                                     TranslateY = 405,
                                     CenterX = 0,
                                     CenterY = 0
                                   };
    popup.Child = stackPanel;
    popup.IsOpen = true;
  }

  /// <summary>
  /// Hides the control.
  /// </summary>
  private static void HideControl()
  {
    popup.IsOpen = false;
    popup = null;
  }

  /// <summary>
  /// Starts the timer.
  /// </summary>
  /// <param name="timespan">The timespan.</param>
  private static void StartTimer(TimeSpan timespan)
  {
    dispatcherTimer = new DispatcherTimer {Interval = timespan};
    dispatcherTimer.Tick += DispatcherTimerTick;
    dispatcherTimer.Start();
  }

  /// <summary>
  /// Stops the timer.
  /// </summary>
  private static void StopTimer()
  {
    dispatcherTimer.Stop();
    dispatcherTimer = null;
  }

  /// <summary>
  /// Updates memory usage indicators on every tick.
  /// </summary>
  /// <param name="sender">The sender.</param>
  /// <param name="e">
  /// The <see cref="System.EventArgs"/> instance containing the event data.
  /// </param>
  private static void DispatcherTimerTick(object sender, EventArgs e)
  {
    if (forceGarbageColector)
    {
      GC.Collect();
    }

    long currentMemory = CurrentMemoryUsage;
    currentMemoryBlock.Text = string.Format("{0:N0}", currentMemory / 1024);
    SolidColorBrush currentMemoryBrush = GetBrushForUsage(currentMemory);
    if (currentMemoryBrush != lastCurrentBrush)
    {
      currentMemoryBlock.Foreground = currentMemoryBrush;
      lastCurrentBrush = currentMemoryBrush;
    }

    long peakMemory = PeakMemoryUsage;
    peakMemoryBlock.Text = string.Format("{0:N0}", peakMemory / 1024);
    SolidColorBrush peakMemoryBrush = GetBrushForUsage(peakMemory);
    if (peakMemoryBrush != lastPeakBrush)
    {
      peakMemoryBlock.Foreground = peakMemoryBrush;
      lastPeakBrush = peakMemoryBrush;
    }
  }

  /// <summary>
  /// Gets the proper brush colour which depends of memory usage.
  /// </summary>
  /// <param name="memoryUsage">The memory usage.</param>
  /// <returns>Colour for memory usage</returns>
  private static SolidColorBrush GetBrushForUsage(long memoryUsage)
  {
    double percent = memoryUsage / (double)MAX_LIMIT_OF_MEMORY;
    if (percent <= 0.75)
    {
      return greenBrush;
    }

    if (percent <= 0.90)
    {
      return yellowBrush;
    }

    return redBrush;
  }
}&#91;/code&#93;
<p>Aby uruchomić omawiane wskaźniki należy jeszcze tylko dodać jedną linijkę do kodu:</p>
[code lang="csharp"]MemoryDiagnoster.StartMemoryMonitoring(TimeSpan.FromMilliseconds(500), [use Garbage Collector]);

Po wykonaniu tej linii kodu powinny wyświetlić się informacje dotyczące pamięci na ekranie telefonu. Użytkownik może zmienić czas, który mija pomiędzy poszczególnymi pomiarami pamięci oraz wymusić uruchomienie procesu Garbage Collector co pomiar.

Wynik działania proponowanego rozwiązania przedstawia poniższy rysunek. Mierniki pamięci zostały zastosowane w programie Kursy Walut.

Pomiar zużycia pamięci - Kursy Walut