Diagnosticare i problemi di performance con MiniProfiler su ASP.NET Core MVC

di Moreno Gentili, in ASP.NET Core,

Per realizzare applicazioni che rendano i nostri utenti entusiasti, non dobbiamo solo fare in modo che il nostro codice "funzioni" ma dovremmo anche garantire buone performance e pagine che si caricano rapidamente. Oltretutto, se realizziamo un'applicazione che esige poche risorse dal server, aiuteremo il nostro committente a ridurre i costi di funzionamento.

MiniProfiler è uno strumento che può aiutarci a realizzare questi obiettivi, perché serve a "profilare", cioè esaminare il comportamento della nostra applicazione per produrre un report HTML, compatto ma informativo, che rende evidenti le possibili inefficienze della nostra applicazione.

Con esso possiamo tracciare sia i tempi di esecuzione delle nostre routine che il numero di query SQL che inviamo al database.

Installare MiniProfiler

Iniziamo installando i pacchetti di MiniProfiler con i seguenti comandi. Il primo pacchetto è strettamente necessario, mentre il secondo lo referenziamo solo se nella nostra applicazione stiamo usando Entity Framework Core.

dotnet add package MiniProfiler.AspNetCore.Mvc
dotnet add package MiniProfiler.EntityFrameworkCore

Poi rechiamoci nel metodo Configure della classe Startup e configuriamo il middleware di MiniProfiler.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
  app.UseMiniProfiler(); 
  
  //Qui usiamo altri middleware
}

Restiamo nella classe Startup e andiamo anche ad aggiungere i suoi servizi nel metodo ConfigureServices.

public void ConfigureServices(IServiceCollection services)
{
  services.AddMiniProfiler()
          .AddEntityFramework(); //Aggiungiamo questa riga solo se usiamo Entity Framework Core

  //Qui aggiungiamo altri servizi
}

MiniProfiler dispone di tante opzioni di configurazione che possiamo fornire al metodo AddMiniProfiler. Ad esempio possiamo decidere selettivamente quali richieste profilare e come vogliamo che siano mostrate le informazioni. Tutto ciò lo troviamo documentato all'indirizzo https://miniprofiler.com/dotnet/AspDotNetCore

Per completare l'installazione, apriamo la view /Views/Shared/_Layout.cshtml e inseriamo il tag helper di MiniProfiler, che verrà usato per sovraimporre il report HTML alle nostre view. Posizioniamolo subito prima della chiusura del tag .

<!DOCTYPE html>
<html>
  <head>
    <!-- Omissis -->
  </head>
  <body>
    <!-- Omissis -->
    <mini-profiler />
  </body>
</html>

Infine andiamo in /Views/_ViewImports.cshtml e registriamo il tag helper in modo che possa essere elaborato dal view engine Razor.

@addTagHelper *, MiniProfiler.AspNetCore.Mvc

Ora siamo finalmente pronti per iniziare a profilare la nostra applicazione ASP.NET Core.

Profilare le query LINQ di Entity Framework Core

Supponiamo di voler verificare il comportamento di una query LINQ che estre alcuni prodotti e da cui selezioniamo la categoria. Per profilare il codice, avvolgiamolo con l'istruzione MiniProfiler.Current.Step("Descrizione").

using (MiniProfiler.Current.Step("Recupero categorie"))
{
  var categories = new HashSet<Category>();
  
  //db è un riferimento all'istanza del nostro DbContext
  var products = await db.Products.Where(p => p.Amount > 1000)
    .ToListAsync();
  
  foreach (var product in products)
  {
    categories.Add(product.Category);
  }
}

Se abbiamo il lazy loading abilitato, questo codice produrrà numerose query al database: una per recuperare i prodotti e altre n per recuperare la categoria di ciascun prodotto. Questo è anche noto come problema Select n+1 e potrebbe passare inosservato se non prestassimo attenzione alle query inviate da Entity Framework Core. Con MiniProfiler, il problema risulta subito evidente perché nella ci mostra una linguetta nella parte alta a sinistra della pagina. Essa riporta il tempo di esecuzione con un punto esclamativo, a segnalare una situazione anomala. Lo possiamo cliccare per avere dettagli aggiuntivi e scoprire che in questo caso stiamo inviando ben 38 query SQL, come si nota dall'immagine.


Cliccando il 38 possiamo entrare nel dettaglio per scoprire quali sono le query in questione. Qui sono anche evidenti i momenti in cui apriamo e chiudiamo la connessione.


Comprendere il problema è solo il primo passo ma ci mette in condizione di pensare a una soluzione migliore, che in questo caso consiste nel modificare la query LINQ così.

var categories = await db.Products.Where(p => p.Amount > 1000)
  .Select(p => p.Category)
  .Distinct()
  .ToListAsync();

Profilare le query di ADO.NET

Anche usando ADO.NET possiamo sfruttare MiniProfiler per misurare il tempo di esecuzione delle query. Per attivare il profiler, in questo caso dobbiamo anche crearci un oggetto StackExchange.Profiling.Data.ProfiledDbConnection e passargli la connessione nel costruttore, come si vede nel seguente esempio.

using (MiniProfiler.Current.Step("Query ADO.NET"))
{
  using (var conn = new StackExchange.Profiling.Data.ProfiledDbConnection(
                      new SqlConnection(connString), MiniProfiler.Current))
  {          
    await conn.OpenAsync();
    using (var cmd = conn.CreateCommand())
    {
      cmd.CommandText = "SELECT * FROM Products, Categories WHERE Products.Amount > 1000";
      using (var reader = await cmd.ExecuteReaderAsync())
      {
        //Omissis
      }
    }
  }
}

In questo caso vediamo appunto che la query (una CROSS JOIN non corretta) sta richiedendo più di 1 secondo e quindi dovremmo valutare se abbiamo risultati migliori da una LEFT JOIN e dall'aggiunta di indici.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi