La generazione di immagini dinamiche è un'esigenza piuttosto frequente che Asp.net MVC risolve in modo realmente triviale. Ovviamente le considerazioni che faremo valgono per qualsiasi file servito dinamicamente, per esempio per i Download.
È sufficiente creare il mapping del routing di asp.net e un controller in cui una action ritorna uno di questi oggetti:
- FileContentResult, tipicamente utilizzato per restituire un byte array, magari letto da un database
- FileStreamResult, comodo quando il file viene generato a partire da uno stream
- FilePathResult, nel caso in cui il file sia fisicamente presente su disco
- Una ActionResult custom, magari derivata da una delle classi sopra elencate (o dalla loro classe base FileResult)
Un esempio di controller che restituisce sempre lo stesso file può essere il seguente:
public class ImageDemoController : Controller
{
public ActionResult Get(string file)
{
return base.File(@"C:\Temp\image_22.png", "image/png");
}
}
Fin qui tutto bene, il codice è semplice e perfettamente funzionante.
Il problema
Poi passa il tempo e quando si arriva a verificare le performance del sito ci si accorge con il profiler di Internet Explorer 9 che le cose vanno piuttosto male:
I contenuti statici, ma solo alcuni contenuti dinamici, vengono serviti in tempi rapidi come è logico aspettarsi per quelle dimensioni.
La barra di colore giallo evidenzia che il tempo è tutto speso nella request (la response è in colore blu). Questo significa che quel tempo è l'intervallo passato tra la richiesta del browser e l'inizio del trasferimento. Perciò nulla è addebitabile agli script, al browser o alla banda.
Se nel codice del controller vi fosse della logica più corposa (come è auspicabile :D) potremmo pensare di aver scritto qualche sciocchezza che ruba quella montagna di tempo.
La diagnosi
Il codice della nostra demo è troppo triviale ma in casi reali vorremmo eseguire una profilazione. Potremmo quindi utilizzare Ants di RedGate ma purtroppo verrebbe fuori che il nostro codice è così performante da non valere neppure la pena di apparire sul profiler.
Una seconda opzione per la profilazione, molto utile per misurare le performance di una request è MvcMiniProfiler, realizzato dal team di StackOverflow e disponibile su NuGet:
Non fa magie ma ci rende semplice collezionare i dati di profilazione ottenuti tra la Application_BeginRequest e la Application_EndRequest nel global.asax.cs. Basta chiamare, rispettivamente, la MvcMiniProfiler.MiniProfiler.Start(); e la MvcMiniProfiler.MiniProfiler.Stop();
Evito di scendere nei particolari perché in questo caso scopriamo che effettivamente la nostra request impiega il tempo già visto nel profiler di Internet Explorer ma non ci fornisce alcuna indicazione utile per capire dove viene speso.
Riassumendo. Alcune request di immagini dinamiche hanno un alto costo globale ma che non dipende dal nostro codice. Appare ovvio che bisogna scendere di livello.
Andiamo perciò a vedere cosa succede in IIS. Per questa diagnosi è necessario utilizzare IIS "full" e non la versione express.
Dalla mmc di IIS, abilitiamo FREB (Failed Request Tracing)
E poi abilitiamo, via wizard, il log di tutte le richieste:
Questo step non ha ancora abilitato il logging ma lo ha solo configurato. Per "accendere" il log è necessario abilitarlo in questo modo:
Nota importante. Questa operazione eseguita su un server di produzione causa forti rallentamenti e la generazione di un altissimo numero di log. Sto perciò dando per scontato che venga fatto su una macchina di sviluppo o di test.
A questo punto è sufficiente un refresh della pagina incriminata e aprire la cartella dei log FREB appena configurata.
IIS genera automaticamente il file FREB .XSL che consente una visualizzazione "umana" dei log. Ogni request viene loggata in un file diverso.
Aprendo file per file, sulla destra si può immediatamente vedere quanto tempo è occorso, globalmente, per servire la request:
Una volta individuata una immagine dinamica lenta, si entra nei dettagli:
Ed espandendo la request con il tempo più alto si può finamente capire la causa:
La creazione della Session, che pure non utilizziamo, inficia inutilmente sulla performance di queste request.
Ricordiamoci di non abusare mai della Session e che, pure utilizzando Cache come lo stesso team di Asp.net raccomanda, la session è utile per avere il SessionId. Disabilitarla in modo globale a mio avviso deve essere una decisione molto ben ponderata.
La soluzione.
La soluzione è tanto triviale quanto il codice che abbiamo già scritto: disabilitare la Session solo per le request che sono servite da quel specifico controller:
[SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]
public class ImageDemoController : Controller
{
public ActionResult Get(string file)
{
return base.File(@"C:\Temp\image_22.png", "image/png");
}
}