Sebbene un'applicazione Windows Store sia eseguita in un'ambiente sand-boxed "ad alte prestazioni", codice inefficiente, chiamate a servizi remoti o a librerie di terze parti che richiedono troppo tempo, calcoli complessi che impegnano a fondo la CPU, e così via, potrebbero determinare problemi di performance tali da pregiudicare la user experience.
Il profiling di un'applicazione Windows Store
Visual Studio 2013 mette a disposizione una serie di strumenti per il profiling di un'applicazione Windows Store che consentono di analizzare e registrare metriche durante l'esecuzione dell'applicazione, raccogliere dati dalla CPU a intervalli regolari, esaminare l'efficienza energetica dell'applicazione e analizzare la fluidità dell'interfaccia utente.
Occorre anche precisare che gli strumenti di profiling per applicazioni Windows Store soffrono anche di alcune limitazione rispetto alle tradizionali applicazioni .NET. Ad esempio, non è supportato l'utilizzo di performance counter (esposti nel framework .NET dal namespace System.Diagnostics
) per raccogliere informazioni dettagliate sulle performance di un'applicazione e sull'ambiente circostante, così come mancano gli strumenti per il concurrency profiling (e dunque per l'analisi di eventuali problemi nell'accesso a risorse condivise), per il profiling della memoria e delle operazioni di garbage collection, o per il monitoraggio delle chiamate a un database SQL Server ("Tier Interaction Profiling", o TIP).
Qualora si renda necessario raccogliere informazioni addizionali sulla performance di un'applicazione, è possibile ricorrere a toolkit esterni a Visual Studio, come il Windows Performance Toolkit (WPT), oggi parte del più ampio Windows Assessment and Deployment Kit. Questo tool contiene strumenti di analisi che consentono di raccogliere dati approfonditi sul sistema operativo e sulle applicazioni Windows. Se invece volete analizzare nel dettaglio l'uso della memoria da parte di un'app Windows Store, potete ricorrere al Microsoft NP .NET Profiler Tool. Tenete tuttavia presente che la discussione di questi strumenti, così come di ogni altro tool di terze parti, esula dagli obiettivi di questa guida.
Per vedere come funzionano gli strumenti di profiling disponibili in Visual Studio, creiamo un'applicazione di prova il cui unico obiettivo è quello di recuperare un elenco di nominativi da un servizio remoto (fittizio) e mostrarli a video.
Qui di seguito è riportato il codice XAML che potete usare come riferimento per la pagina di default della vostra app:
<Page
x:Class="Demo.Html.it.ProfilingSample.CS.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Demo.Html.it.ProfilingSample.CS"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<Button Content="Elenco clienti" Click="GetCustomerListButton_Click" Margin="20, 5"/>
<ListView x:Name="CustomerListView" DisplayMemberPath="Name" Margin="20, 5" />
</StackPanel>
</Grid>
</Page>
Nel code-behind della pagina, l'handler dell'evento di click sul pulsante recupera l'elenco dei "clienti" tramite una chiamata a un business layer (fake) e lo mette in binding con il controllo ListView
(il codice che segue ha finalità puramente illustrative e non dovrebbe mai essere usato in un'app reale, poiché congela la user interface fino a quando l'operazione non è conclusa; in questo caso, avremmo dovuto utilizzare il pattern asincrono async/await):
private void GetCustomerListButton_Click(object sender, RoutedEventArgs e)
{
var biz = new FakeBiz();
var customers = biz.GetCustomers();
CustomerListView.ItemsSource = customers;
}
La classe FakeBiz
contiene solo un metodo GetCustomer
, il quale recupera una collezione di client simulando una chiamata a un servizio remoto, rappresentato dal metodo SimulateRemoteServiceCall
(questo metodo contiene un loop vuoto che sospende l'esecuzione dell'operazione per alcuni secondi).
public class FakeBiz
{
public List GetCustomers()
{
return this.SimulateRemoteServiceCall();
}
private List SimulateRemoteServiceCall()
{
for (int i = 0; i < Int32.MaxValue; i++)
{
// dummy loop
}
return new List()
{
new Person() { Name = "Roberto Brunetti" },
new Person() { Name = "Vanni Boncinelli" },
new Person() { Name = "Luca Regnicoli" },
new Person() { Name = "Katia Egiziano" },
new Person() { Name = "Paolo Pialorsi" },
new Person() { Name = "Marco Russo" },
};
}
}
public class Person
{
public string Name { get; set; }
}
Per analizzare questo codice in Visual Studio, occorre cliccare su Debug Start Performance Analysis, come mostrato nella prossima immagine.
A questo punto apparirà una nuova finestra in cui potete selezionare il tipo di profiling da eseguire. Tre sono le opzioni possibili (in Visual Studio 2012 questa finestra non è presente, dal momento che solo la prima opzione è disponibile).
Opzione | Descrizione |
---|---|
CPU Sampling | permette di raccogliere informazioni dalla CPU a intervalli regolari, navigando lungo la catena di chiamate ai vari metodi (execution path) e valutando il costo di ciascuna di queste funzioni. |
Energy Consumption | consente di analizzare la quantità di energia consumata dall'applicazione, e dove avviene il maggiore consumo (CPU, rete, ecc.) |
XAML UI Responsiveness | raccoglie dati attinenti alla responsività, ossia al grado di fluidità, dell'interfaccia utente di un'applicazione |
Vediamo adesso questi tre strumenti più nel dettaglio utilizzando la nostra app come "cavia".
CPU Sampling
Selezionando questa opzione e premendo su Start, Visual Studio lancerà l'applicazione target e comincerà a registrare i dati provenienti dalla call stack. È possibile in qualunque momento interrompere l'attività di profiling tramite l'apposito pulsante di Stop. A questo punto, Visual Studio 2013 visualizzerà i dati raccolti tramite un Sample Profiling Report, mostrato nella prossima immagine:
Questo report contiene i dati raccolti durante l'esecuzione dell'applicazione. La parte superiore del report mostra un grafico in cui viene analizzato il consumo della CPU nel tempo (in secondi). È possibile ingrandire o isolare aree specifiche del grafico, ad esempio un picco di consumo della CPU. Subito sotto, la sezione Hot Path indica gli le chiamate più onerose in termini di utilizzo di CPU. Ciascun metodo è evidenziato con un'icona a forma di fiamma, proprio accanto al nome del metodo. Ciascuna funzione mostra due indicatori, denominati rispettivamente "Inclusive Samples" ed "Exclusive Samples". Il primo valore, detto anche "tempo inclusivo" (inclusive time), indica il tempo CPU impiegato per completare quella particolare funzione, incluso il tempo speso attendendo il completamento degli altri metodi invocati dal metodo stesso. Il secondo valore, o "tempo esclusivo" (exclusive time), è invece da intendersi "al netto" del tempo speso da una funzione per attendere il completamento di altre funzioni, e indica unicamente la quantità di tempo consumato per eseguire il codice interno al metodo stesso (escluse le chiamate a funzioni ulteriori).
Nel nostro esempio, tutti i metodi inclusi nell'Hot Path mostrano un tempo inclusivo compreso tra il 94,23 e il 94,25%, mentre il tempo esclusivo è pari a zero, con la sola eccezione del metodo SimulateRemoteServiceCall
, che riporta un valore di 94,21%. Questo significa che praticamente tutto il tempo CPU consumato dall'app è stato usato da questo metodo, e che tutte le altre funzioni non hanno fatto altro che attendere il completamento di questa chiamata.
La sezione Functions Doing Most Individual Work mostra invece il tempo CPU esclusivo consumato da ciascuna delle funzioni chiamate durante l'esecuzione dell'applicazione. Trattandosi di tempo esclusivo, si intende "al netto" del tempo passato ad aspettare il completamento di altri metodi.
Per avere maggiori informazioni su uno dei metodi elencati, è sufficiente cliccare sul nome del metodo, e Visual Studio passerà a una nuova vista di dettaglio, denominata Function Details, mostrata nella prossima immagine.
La parte superiore della vista mostra il tempo inclusivo allocato lungo l'execution path che include la funzione corrente. Le tre colonne mostrano le relazioni tra la funzione corrente (GetCustomers
), rappresentata dalla colonna centrale, la funzione chiamante (GetCustomers
), indicata nella colonna di sinistra, e una qualsiasi funzione chiamata a sua volta dal metodo corrente(in questo caso, SimulateRemoteServiceCall
), rappresentata dalla terza colonna. Nell'esempio, la colonna Current Function indica che durante l'esecuzione del codice interno al metodo GetCustomers
è stato consumato meno dello 0.1% del tempo CPU. La colonna Called Function, invece, mostra chiaramente che il 94,2% del tempo CPU è stato speso nell'esecuzione del metodo SimulateRemoteServiceCall
.
Particolarmente interessante è il pannello Function Code View nella parte inferiore della vista. Questa vista evidenzia in rosso i punti del codice che causano il collo di bottiglia. Se adesso cliccate sul metodo SimulateRemoteServiceCall
, la vista mostrerà le linee di codice all'origine del problema (nel nostro esempio, il ciclo for
), indicando per ciascuna di queste il tempo CPU speso.
Un'altra vista particolarmente utile, soprattutto se per la nostra app abbiamo adottato una soluzione multitier o utilizziamo librerie di terze parti, è la vista Modules Vew, la quale mostra i dati raccolti raggruppati per moduli. La prossima immagine ne mostra un esempio.
Energy consumption
Come si è accennato, lo strumento di analisi "Energy consumption" permette di stimare in modo preciso quanta energia la nostra app consuma, e per quali attività questa energia viene impiegata. La prossima immagine mostra un esempio di report sul consumo energetico della nostra app:
Gli elementi informativi principali sono tre.
- Nella sezione Estimated Power Usage, il grafico mostra il consumo di energia legato alla CPU, al display e alla rete. Questo grafico è particolarmente utile, poiché permette di evidenziare inaspettati picchi nel consumo di energia in uno di questi tre comparti.
- La sezione Resources (On/Off) specifica lo stato delle risorse di rete. Cliccando su una delle voci, si ottengono i dettagli relativi al trasferimento dati per ciascuna risorsa.
- Infine, nell'Estimated Energy Consumption Summary un grafico sintetizza il consumo di energia dell'app e viene indicata la durata prevista della batteria nel caso di uso continuativo dell'applicazione (poco più di 3 ore, nel nostro esempio).
XAML UI Responsiveness
Infine, scegliendo l'opzione XAML UI Responsiveness il profiler esaminerà la fluidità della UI dell'applicazione, raccogliendo dati sia dal composition thread che dal thread di UI. Una volta terminata la raccolta dei dati, Visual Studio elaborerà un report dettagliato, mostrato nella prossima immagine:
Nella parte superiore del report viene riportato il grafico relativo all'utilizzo del thread di UI. Il carico di lavoro è suddiviso, a seconda del tipo di operazione svolta, tra Parsing, Layout, App code e Xaml Other (quest'ultimo include altre attività svolte dal framework XAML per mostrare gli elementi a video): come si può vedere nell'immagine, la gran parte del carico di lavoro è imputabile all'esecuzione del codice applicativo (in particolare al metodo SimulateRemoteServiceCall
), piuttosto che al parsing e al rendering degli elementi di UI.
Il grafico denominato Visual Throughput riporta invece il frame rate (FPS) dei due thread coinvolti dalla user interface, ossia il thread di UI e il composition thread.
Infine, la metà inferiore della vista include due riquadri che riportano informazioni dettagliate sul parsing del codice XAML: Hot Elements e Parsing. Il primo mostra i controlli XAML che hanno richiesto più tempo per il parsing, mentre il secondo riporta il tempo necessario al parsing dei vari file XAML. La prossima immagine ne mostra un esempio:
Ulteriori strumenti di analisi per il codice XAML
Un'ulteriore opzione per profilare il codice XAML di un'app Windows Store è offerta dalla proprietà booleana EnableFrameRateCounter
esposta dalla classe DebugSettings
. Impostando questa proprietà a true
, è possibile osservare a schermo una serie di indicatori della performance dell'app "in tempo reale".
In Windows 8, questa proprietà doveva essere impostata manualmente nel codice, mentre nelle applicazioni Windows 8.1 adesso la proprietà fa parte del codice auto-generato da Visual Studio al momento della creazione di un nuovo progetto, ed e circondata da un blocco condizionale, per cui il contatore verrà mostrato ogni volta che l'applicazione viene lanciata in modalità Debug:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
#if DEBUG
if (System.Diagnostics.Debugger.IsAttached)
{
this.DebugSettings.EnableFrameRateCounter = true;
}
#endif
Frame rootFrame = Window.Current.Content as Frame;
(...)
}
Nel passaggio da Windows 8 a Windows 8.1, anche il tipo di informazioni mostrate a video è cambiato. La prossima immagine mostra il contatore in un'app Windows 8 dopo aver manualmente impostato la proprietà DebugSettings.EnableFrameRateCounter
a true
.
Le cifre mostrate nel contatore forniscono le seguenti indicazioni (da sinistra verso destra):
- il numero di fps (frame al secondo) del composition thread
- il numero di fps del thread di UI
- la memoria video utilizzata per renderizzare le texture
- il numero di superfici inviate alla GPU per essere disegnate
- il tempo (in millisecondi) impiegato dalla CPU per il composition thread
- il tempo speso dalla CPU per il thread di UI
In Windows 8.1, invece, i valori riportati sono solo 4 e sono mostrati in coppie ai due angoli superiori dello schermo. Nell'angolo in alto a sinistra sono riportati il numero di FPS e il tempo impiegato dalla CPU per il thread di UI, mentre nell'alto a destra sono indicati il numero di FPS e il tempo CPU per il composition thread.
Un'altra utile proprietà esposta sempre dalla classe DebugSettings
è IsOverdrawHeatMapEnabled
. Questa proprietà offre un aiuto visuale per individuare aree della user interface in cui gli oggetti vengono disegnati uno sopra l'altro (overdraw), appesantendo il carico di lavoro. Colori più scuri, indicano quantità maggiori di overdraw.
Questa particolare visualizzazione può risultare utile durante lo sviluppo di un'app per individuare aree, animazioni e altre operazioni intensive dal punto di vista del processore grafico, ed eventualmente porvi rimedio (ad esempio impostando la proprietà CacheMode
di un UIElement a BitmapCache
, in modo da evitare che questo elemento sia nuovamente renderizzato a ogni frame).
Il seguente snippet mostra come attivare questa particolare visualizzazione:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
#if DEBUG
if (System.Diagnostics.Debugger.IsAttached)
{
this.DebugSettings.EnableFrameRateCounter = true;
this.DebugSettings.IsOverdrawHeatMapEnabled = true;
}
#endif
Frame rootFrame = Window.Current.Content as Frame;
(...)
}
Tracing ed event logging nelle applicazioni Windows Store
Il namespace System.Diagnostics.Tracing
mette a disposizione tipi e metodi che consentono di creare eventi di "tracing" che descrivono lo stato corrente dell'applicazione o di un'operazione. Questi eventi sono quindi "catturati" dall'Event Tracing for Windows (ETW) per finalità di debugging, logging e analisi. Rispetto a una tradizionale applicazione .NET, WinRT supporta solo un sotto-insieme dei tipi disponibili. Ad esempio, in un'app Windows Store la classe TraceListener
(e le classe derivate, come DefaultTraceListener
, TextWriterTraceListener
, e EventLogTraceListener
) non sono supportate dal Windows Runtime.
La prima cosa da fare per sfruttare questo meccanismo è creare la propria implementazione della classe EventSource
, la quale consente di creare differenti tipologie di eventi di tracing. Il prossimo snippet mostra una semplice implementazione di questa classe.
public class MyCustomEventSource : EventSource
{
[Event(1, Level = EventLevel.LogAlways)]
public void WriteDebug(string message)
{
this.WriteEvent(1, message);
}
[Event(2, Level = EventLevel.Informational)]
public void WriteInfo(string message)
{
this.WriteEvent(2, message);
}
[Event(3, Level = EventLevel.Warning)]
public void WriteWarning(string message)
{
this.WriteEvent(3, message);
}
[Event(4, Level = EventLevel.Error)]
public void WriteError(string message)
{
this.WriteEvent(4, message);
}
[Event(5, Level = EventLevel.Critical)]
public void WriteCritical(string message)
{
this.WriteEvent(5, message);
}
}
Ciascun evento è marcato con un attributo denominato Event
che consente di specificare informazioni addizionali sull'evento, che potranno essere poi utilizzate dall'event listener (vedi più avanti) per filtrare gli eventi da tracciare. Le informazioni che possono essere specificate tramite questo attributo sono le seguenti:
Campo | Descrizione |
---|---|
EventId | rappresenta l'identificativo dell'evento |
Keywords | specifica le parole chiave da associare a un evento, così come definite nell'enum EventKeywords |
Level | Indica il livello dell'evento; può assumere uno dei valori definiti nell'enum EventLevel :
|
Message | specifica il messaggio associato all'evento |
Opcode | specifica il codice dell'operazione associato all'evento |
Task | specifica il task associato all'evento |
TypeId | quando implementato in una classe derivata, rappresenta l'identificativo dell'attributo |
Version | Indicata la versione dell'evento |
Una volta creata la propria implementazione della classe EventSource
, possiamo decidere di condividere questo oggetto attraverso tutta l'applicazione, oppure creare una nuova istanza ogni volta che serve (in questo caso occorre ricordarsi di operare la dispose dell'oggetto al termine delle operazioni). Il prossimo snippet mostra un semplice esempio di singleton che permette di riutilizzare più volte lo stesso oggetto:
public static class EventSourceFactory
{
private static MyCustomEventSource _eventSource;
public static MyCustomEventSource EventSource
{
get
{
if (_eventSource == null)
{
_eventSource = new MyCustomEventSource();
}
return _eventSource;
}
}
}
Una volta decisi gli eventi da tracciare, è necessario implementare un event listener. Dal momento che la class EventListener
è marcata come abstract
, dobbiamo derivare il nostro event listener custom e fornire l'implementazione del metodo astratto OnEventWritten
che riceverà la callback dell'evento. È anche possibile definire più event listener, ciascuno dei quali logicamente indipendente dagli altri, che possono essere utilizzati per tracciare differenti tipi di eventi: ad esempio, un listener potrebbe ascoltare solo gli eventi che corrispondono agli errori più gravi che richiedono di essere inviati immediatamente a un servizio remoto (magari sul cloud), mentre altri listener potrebbero essere dedicati a errori più comuni, magari per essere semplicemente loggati e ispezionati in un secondo momento. Il prossimo snippet mostra lo scheletro di due diverse implementazioni della classe astratta EventListener
che tracciano due differenti tipi di eventi.
public class RemoteEventListener : EventListener
{
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
// logga l'evento e lo invia a un servizio remoto
}
}
public class LocalStorageEventListener : EventListener
{
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
// logga l'evento nello storage locale
}
}
L'event handler OnEventWritten
viene chiamato ogni volta che il metodo WriteEvent
dell'oggetto EventSource
associato viene invocato. Il metodo OnEventWritten
riceve come parametro un'istanza della classe EventWrittenEventArgs
contenente tutte le informazioni relative all'evento stesso. Il seguente snippet mostra come un listener può filtrare gli eventi tracciati sulla base degli attributi definiti nell'oggetto di tipo EventSource
associato al listener stesso.
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
this.InitializeEventListener();
// codice omesso
}
private void InitializeEventListener()
{
EventListener remoteListener = new RemoteEventListener();
remoteListener.EnableEvents(EventSourceFactory.EventSource, EventLevel.Error | EventLevel.Critical);
EventListener storageListener = new LocalStorageEventListener();
storageListener.EnableEvents(EventSourceFactory.EventSource, EventLevel.Informational | EventLevel.Verbose | EventLevel.Warning);
}
Il primo listener accetterà solo gli evneti più critici, quelli cioè che richiedono di essere risolti nel più breve tempo possibile, mentre il secondo listener provvederà a loggare solo quegli eventi che non rappresentano errori in senso stretto, ma che ciò nonostante potrebbero comunque informazioni utili, e che per questo possiamo voler salvare nello storage applicativo per poi eventualmente ispezionarle in un secondo momento.
Per loggare gli eventi, è sufficiente invocare uno dei metodi Write*
esposti dalla classe EventSource
. Il prossimo snippet, ad esempio, utilizza il metodo WriteInfo
per loggare l'avvenuto acquisto di un'app sul Windows Store, mentre nel caso di errori a essere invocato è il metodo WriteCritical
:
private async void BuyButton_Click(object sender, RoutedEventArgs e)
{
try
{
await CurrentApp.RequestAppPurchaseAsync(false);
EventSourceFactory.EventSource.WriteInfo(String.Format("app acquistata il {0}", DateTime.Now));
}
catch (Exception ex)
{
// Impossibile acquistare l'app
EventSourceFactory.EventSource.WriteCritical(ex.Message);
}
}
Utilizzare i Quality Report del Windows Store per migliorare la qualità dell'applicazione
Per migliorare la qualità e monitorare le performance delle nostre app, oltre a sfruttare i dati raccolti tramite le API di WinRT viste in precedenza, è possibile contare anche su una serie di informazioni raccolte dal Windows Store dopo la pubblicazione dell'app. Il Windows Store, in particolare, espone due tipi di informazioni:
- Analytics: si riferisce ai dati raccolti direttamente dallo store, come informazioni sul numero di download e sul rating degli utenti. Questi dati possono aiutarci a migliorare le vendite della nostra app.
- Telemetry: fa riferimento ai dati raccolti durante l'esecuzione dell'applicazione sul device dell'utente. Questo tipo di dati fornisce informazioni su quante volte l'app è stata lanciata, per quanto tempo è stata utilizzata, se si sono verificati crash, episodi di hang (non responsività) dell'applicazione o eccezioni JavaScript. Dal punto di vista dello sviluppatore, la telemetria costituisce una preziosa fonte di informazioni che possono grandemente aiutare a migliorare la qualità e l'affidabilità di un'app.
A differenza dei dati analitici, la raccolta dei dati telemetrici può essere disabilitata nella sezione Profile della Windows Store Dashboard, come mostrato nella prossima figura.
Le informazioni analitiche relative all'applicazione vengono messe a disposizione tramite Adoption Report, i quali includono una serie di dati relative al trend dei download e ai feedback provenienti dagli utenti. Questi report includono anche informazioni sul numero di conversioni dell'app (da trial alla versione full) e degli acquisti tramite il meccanismo degli in-app purchase. Il prossimo snippet mostra un esempio di report in cui sono evidenziati i download dell'applicazione.
I dati telemetrici sono invece riassunti nei Quality Report, che misurano l'affidabilità dell'applicazione (secondo la documentazione ufficiale MSDN, i dati telemetrici sono estrapolati da un panel di circa 500 macchine selezionate in modo random). Questi report possono essere visualizzati nella sezione Quality della dashboard. Da qui, cliccando sul link Details è possibile accedere alle informazioni dettagliate circa i vari errori incontrati durante il ciclo di vita dell'app. La prossima immagine mostra un esempio di Quality Report.
Nella sezione Most Common Crashes è inoltre possibile trovare elencati i cinque più frequenti crash che hanno riguardato la nostra app, ciascuno identificato da un'etichetta. Ciascuna etichetta fornisce le seguenti informazioni: il problema che ha originato il crash, il codice errore e i simboli di debug. Accanto a ciascun errore si trova il link a un file .cab contenente il dump del processo associato a quel particolare errore. Un file di dump rappresenta uno snapshot dell'applicazione che mostra il processo in esecuzione e può essere analizzato utilizzando Visual Studio (per i dettagli sull'analisi dei file di dump si rinvia alla documentazione su MSDN).