2015-08-01 22 views
5

Eseguo un sistema di generazione. Datawise la descrizione semplificata sarebbe che ho Configurazioni e ogni configurazione ha 0 .. n Build. Ora le build producono artefatti e alcuni di questi sono memorizzati sul server. Quello che sto facendo è scrivere una sorta di regola, che somma tutti i byte prodotti per build di configurazione e controlla se questi sono troppo.Ottimizzazione delle routine LINQ

Il codice per la routine in questo momento è la seguente:

private void CalculateExtendedDiskUsage(IEnumerable<Configuration> allConfigurations) 
{ 
    var sw = new Stopwatch(); 
    sw.Start(); 
    // Lets take only confs that have been updated within last 7 days 
    var items = allConfigurations.AsParallel().Where(x => 
     x.artifact_cleanup_type != null && x.build_cleanup_type != null && 
     x.updated_date > DateTime.UtcNow.AddDays(-7) 
     ).ToList(); 

    using (var ctx = new LocalEntities()) 
    { 
     Debug.WriteLine("Context: " + sw.Elapsed); 
     var allBuilds = ctx.Builds; 
     var ruleResult = new List<Notification>(); 
     foreach (var configuration in items) 
     { 
      // all builds for current configuration 
      var configurationBuilds = allBuilds.Where(x => x.configuration_id == configuration.configuration_id) 
       .OrderByDescending(z => z.build_date); 
      Debug.WriteLine("Filter conf builds: " + sw.Elapsed); 

      // Since I don't know which builds/artifacts have been cleaned up, calculate it manually 
      if (configuration.build_cleanup_count != null) 
      { 
       var buildCleanupCount = "30"; // default 
       if (configuration.build_cleanup_type.Equals("ReserveBuildsByDays")) 
       { 
        var buildLastCleanupDate = DateTime.UtcNow.AddDays(-int.Parse(buildCleanupCount)); 
        configurationBuilds = configurationBuilds.Where(x => x.build_date > buildLastCleanupDate) 
          .OrderByDescending(z => z.build_date); 
       } 
       if (configuration.build_cleanup_type.Equals("ReserveBuildsByCount")) 
       { 
        var buildLastCleanupCount = int.Parse(buildCleanupCount); 
        configurationBuilds = 
         configurationBuilds.Take(buildLastCleanupCount).OrderByDescending(z => z.build_date); 
       } 
      } 

      if (configuration.artifact_cleanup_count != null) 
      { 
       // skipped, similar to previous block 
      } 

      Debug.WriteLine("Done cleanup: " + sw.Elapsed); 
      const int maxDiscAllocationPerConfiguration = 1000000000; // 1GB 
      // Sum all disc usage per configuration 
      var confDiscSizePerConfiguration = configurationBuilds 
       .GroupBy(c => new {c.configuration_id}) 
       .Where(c => (c.Sum(z => z.artifact_dir_size) > maxDiscAllocationPerConfiguration)) 
       .Select(groupedBuilds => 
        new 
        { 
         configurationId = groupedBuilds.FirstOrDefault().configuration_id, 
         configurationPath = groupedBuilds.FirstOrDefault().configuration_path, 
         Total = groupedBuilds.Sum(c => c.artifact_dir_size), 
         Average = groupedBuilds.Average(c => c.artifact_dir_size) 
        }).ToList(); 
      Debug.WriteLine("Done db query: " + sw.Elapsed); 

      ruleResult.AddRange(confDiscSizePerConfiguration.Select(iter => new Notification 
      { 
       ConfigurationId = iter.configurationId, 
       CreatedDate = DateTime.UtcNow, 
       RuleType = (int) RulesEnum.TooMuchDisc, 
       ConfigrationPath = iter.configurationPath 
      })); 
      Debug.WriteLine("Finished loop: " + sw.Elapsed); 
     } 
     // find owners and insert... 
    } 
} 

Questo fa esattamente quello che voglio, ma sto pensando se potevo farlo più velocemente. Currenly Vedo:

Context: 00:00:00.0609067 
// first round 
Filter conf builds: 00:00:00.0636291 
Done cleanup: 00:00:00.0644505 
Done db query: 00:00:00.3050122 
Finished loop: 00:00:00.3062711 
// avg round 
Filter conf builds: 00:00:00.0001707 
Done cleanup: 00:00:00.0006343 
Done db query: 00:00:00.0760567 
Finished loop: 00:00:00.0773370 

Il SQL generato da .ToList()looks very messy. (Tutto ciò che viene utilizzato in WHERE è coperto con un indice in DB)

sto testando con 200 configurazioni, quindi questo aggiunge fino a 00: 00: 18,6,326722 millions. Ho un totale di ~ 8k articoli che devono essere elaborati giornalmente (quindi l'intera routine richiede più di 10 minuti per essere completata).

Sono stato su Google in modo casuale su Internet e mi sembra che lo standard Entitiy Framework non sia molto buono con l'elaborazione parallela. Sapendo che ho ancora deciso di provare questo approcio a async/await (la prima volta che l'ho provato, mi dispiace per qualsiasi assurdità).

Fondamentalmente se sposto tutta l'elaborazione di applicazione come:

foreach (var configuration in items) 
    { 

     var confDiscSizePerConfiguration = await GetData(configuration, allBuilds); 

     ruleResult.AddRange(confDiscSizePerConfiguration.Select(iter => new Notification 
     { 
      ... skiped 
    } 

E:

private async Task<List<Tmp>> GetData(Configuration configuration, IQueryable<Build> allBuilds) 
{ 
     var configurationBuilds = allBuilds.Where(x => x.configuration_id == configuration.configuration_id) 
      .OrderByDescending(z => z.build_date); 
     //..skipped 
     var confDiscSizePerConfiguration = configurationBuilds 
      .GroupBy(c => new {c.configuration_id}) 
      .Where(c => (c.Sum(z => z.artifact_dir_size) > maxDiscAllocationPerConfiguration)) 
      .Select(groupedBuilds => 
       new Tmp 
       { 
        ConfigurationId = groupedBuilds.FirstOrDefault().configuration_id, 
        ConfigurationPath = groupedBuilds.FirstOrDefault().configuration_path, 
        Total = groupedBuilds.Sum(c => c.artifact_dir_size), 
        Average = groupedBuilds.Average(c => c.artifact_dir_size) 
       }).ToListAsync(); 
     return await confDiscSizePerConfiguration; 
    } 

Questo, per qualche ragione, gocce il tempo di esecuzione di 200 articoli da 18 -> 13 sec. Ad ogni modo, da quanto ho capito, dato che sono await ogni .ToListAsync(), è ancora processato in sequenza, è corretto?

Quindi la richiesta "non può elaborare in parallelo" inizia a uscire quando sostituisco lo foreach (var configuration in items) con Parallel.ForEach(items, async configuration =>. Questa modifica comporta:

Una seconda operazione è iniziata in questo contesto prima di una precedente operazione asincrona completata. Utilizzare 'attendi' per assicurarsi che tutte le operazioni asincrone siano state completate prima di chiamare un altro metodo in questo contesto. Non è garantito che i membri di istanza siano protetti da thread .

E 'stato un po' di confusione per me in un primo momento, come ho await praticamente in ogni luogo in cui il compilatore lo permette, ma forse i dati vengono seminate a digiunare.

Ho cercato di ovviare a questo essendo meno avido e ho aggiunto il new ParallelOptions {MaxDegreeOfParallelism = 4} a quel ciclo parallelo, l'ipotesi contadina era che la dimensione del pool di connessione predefinita fosse 100, tutto quello che voglio usare è 4, dovrebbe essere abbondante. Ma fallisce ancora.

Ho anche provato a creare nuovi DbContexts all'interno del metodo GetData, ma non riesce ancora.Se non ricordo male (non può provare ora), mi sono riuscito

collegamento sottostante per aprire

Quali possibilità ci sono per rendere questa routine andare più veloce?

+0

Profilo e vedere quali metodi/linee utilizzano più tempo. –

+0

sql-server ha una proprietà che regola il numero massimo di query eseguite in parallelo. Verificare questo insieme al carico del server per determinare il numero massimo di query parallele che è possibile eseguire prima che sql-server inizi a metterle in coda. SQL Server alla fine si rivelerà essere il collo di bottiglia. – Mark

+2

LINQ non sostituisce SQL e genera query difficili da leggere per scenari complessi. Il parallelismo NON migliorerà le prestazioni di una query errata, anzi lo degraderà, spesso a causa del blocco. Inoltre, le prestazioni SQL dipendono dall'avere gli indici * appropriati *. L'unica soluzione realistica è scrivere l'istruzione SQL che si desidera (probabilmente creare una vista) e creare indici appropriati nelle tabelle sottostanti. SQL Server dispone persino di un analizzatore per suggerire indici per query o carichi di lavoro specifici –

risposta

3

Prima di procedere in parallelo, vale la pena ottimizzare la query. Ecco alcuni suggerimenti che potrebbero migliorare i tuoi tempi:

1) Utilizzare Key quando si lavora con GroupBy. Questo potrebbe risolvere il problema della query SQL nidificata complessa & poiché in questo modo si ordina a Linq di utilizzare le stesse chiavi definite in GROUP BY e di non creare la sottoselezione.

 var confDiscSizePerConfiguration = configurationBuilds 
      .GroupBy(c => new { ConfigurationId = c.configuration_id, ConfigurationPath = c.configuration_path}) 
      .Where(c => (c.Sum(z => z.artifact_dir_size) > maxDiscAllocationPerConfiguration)) 
      .Select(groupedBuilds => 
       new 
       { 
        configurationId = groupedBuilds.Key.ConfigurationId, 
        configurationPath = groupedBuilds.Key.ConfigurationPath, 
        Total = groupedBuilds.Sum(c => c.artifact_dir_size), 
        Average = groupedBuilds.Average(c => c.artifact_dir_size) 
       }) 
      .ToList(); 

2) Sembra che tu sia stato morso dal problema N + 1. In parole semplici: si esegue una query SQL per ottenere tutte le configurazioni e N un'altra per ottenere informazioni sulla build. In totale sarebbero ~ 8k piccole query in cui sarebbero sufficienti 2 query più grandi. Se la memoria utilizzata non è un vincolo, recupera tutti i dati di costruzione in memoria e ottimizza la ricerca rapida utilizzando ToLookup.

var allBuilds = ctx.Builds.ToLookup(x=>x.configuration_id); 

Successivamente si può cercare costruisce da:

var configurationBuilds = allBuilds[configuration.configuration_id].OrderByDescending(z => z.build_date); 

3) Si sta facendo OrderBy su configurationBuilds più volte. Filtraggio non influisce ordine di registrazione, in modo da poter rimuovere in modo sicuro le chiamate in più per OrderBy:

... 
configurationBuilds = configurationBuilds.Where(x => x.build_date > buildLastCleanupDate); 
... 
configurationBuilds = configurationBuilds.Take(buildLastCleanupCount); 
... 

4) Non ha senso fare GroupBy come generazioni sono già filtrata per una singola configurazione.

UPDATE:

ho preso un passo ulteriore e ha creato il codice che avrebbe recuperare stessi risultati come il codice fornito con una singola richiesta. Dovrebbe essere più performante e usare meno memoria.

private void CalculateExtendedDiskUsage() 
{ 
    using (var ctx = new LocalEntities()) 
    { 
     var ruleResult = ctx.Configurations 
      .Where(x => x.build_cleanup_count != null && 
       (
        (x.build_cleanup_type == "ReserveBuildsByDays" && ctx.Builds.Where(y => y.configuration_id == x.configuration_id).Where(y => y.build_date > buildLastCleanupDate).Sum(y => y.artifact_dir_size) > maxDiscAllocationPerConfiguration) || 
        (x.build_cleanup_type == "ReserveBuildsByCount" && ctx.Builds.Where(y => y.configuration_id == x.configuration_id).OrderByDescending(y => y.build_date).Take(buildCleanupCount).Sum(y => y.artifact_dir_size) > maxDiscAllocationPerConfiguration) 
       ) 
      ) 
      .Select(x => new Notification 
      { 
       ConfigurationId = x.configuration_id, 
       ConfigrationPath = x.configuration_path 
       CreatedDate = DateTime.UtcNow, 
       RuleType = (int)RulesEnum.TooMuchDisc, 
      }) 
      .ToList(); 
    } 
} 
+0

"I grandi menti pensano allo stesso modo" – Caverna

0

Per prima cosa creare un nuovo contesto ogni parallel.foreach di voi intenzione di andare quella rotta. Ma devi scrivere una query che raccolga tutti i dati necessari in un solo viaggio. Per accelerare, ef u può anche disabilitare il rilevamento delle modifiche o i proxy nel contesto durante la lettura dei dati.

0

Ci sono un sacco di posti per ottimizzazioni ...

Ci sono luoghi dove si dovrebbe mettere .ToArray() per evitare di chiedere il tempo multiplo per server ...

ho fatto un sacco di refactoring , ma non sono in grado di controllare, a causa della mancanza di ulteriori informazioni.

Forse questo può portare a una soluzione migliore ...

privato CalculateExtendedDiskUsage void (IEnumerable allConfigurations) {var sw = new Cronometro(); sw.Inizio();

 using (var ctx = new LocalEntities()) 
     { 
      Debug.WriteLine("Context: " + sw.Elapsed); 

      var allBuilds = ctx.Builds; 
      var ruleResult = GetRulesResult(sw, allConfigurations, allBuilds); // Clean Code!!! 

      // find owners and insert... 
     } 
    } 

    private static IEnumerable<Notification> GetRulesResult(Stopwatch sw, IEnumerable<Configuration> allConfigurations, ICollection<Configuration> allBuilds) 
    { 
     // Lets take only confs that have been updated within last 7 days 
     var ruleResult = allConfigurations 
      .AsParallel() // Check if you really need this right here... 
      .Where(IsConfigElegible) // Clean Code!!! 
      .SelectMany(x => CreateNotifications(sw, allBuilds, x)) 
      .ToArray(); 

     Debug.WriteLine("Finished loop: " + sw.Elapsed); 

     return ruleResult; 
    } 
    private static bool IsConfigElegible(Configuration x) 
    { 
     return x.artifact_cleanup_type != null && 
       x.build_cleanup_type != null && 
       x.updated_date > DateTime.UtcNow.AddDays(-7); 
    } 
    private static IEnumerable<Notification> CreateNotifications(Stopwatch sw, IEnumerable<Configuration> allBuilds, Configuration configuration) 
    { 
     // all builds for current configuration 
     var configurationBuilds = allBuilds 
      .Where(x => x.configuration_id == configuration.configuration_id); 
     // .OrderByDescending(z => z.build_date); <<< You should order only when needed (most at the end) 

     Debug.WriteLine("Filter conf builds: " + sw.Elapsed); 

     configurationBuilds = BuildCleanup(configuration, configurationBuilds); // Clean Code!!! 
     configurationBuilds = ArtifactCleanup(configuration, configurationBuilds); // Clean Code!!! 
     Debug.WriteLine("Done cleanup: " + sw.Elapsed); 

     const int maxDiscAllocationPerConfiguration = 1000000000; // 1GB 
     // Sum all disc usage per configuration 
     var confDiscSizePerConfiguration = configurationBuilds 
      .OrderByDescending(z => z.build_date) // I think that you can put this even later (or not to have anyway) 
      .GroupBy(c => c.configuration_id) // No need to create a new object, just use the property 
      .Where(c => (c.Sum(z => z.artifact_dir_size) > maxDiscAllocationPerConfiguration)) 
      .Select(CreateSumPerConfiguration); 
     Debug.WriteLine("Done db query: " + sw.Elapsed); 

     // Extracting to variable to be able to return it as function result 
     var notifications = confDiscSizePerConfiguration 
      .Select(CreateNotification); 

     return notifications; 
    } 

    private static IEnumerable<Configuration> BuildCleanup(Configuration configuration, IEnumerable<Configuration> builds) 
    { 
     // Since I don't know which builds/artifacts have been cleaned up, calculate it manually 
     if (configuration.build_cleanup_count == null) return builds; 

     const int buildCleanupCount = 30; // Why 'string' if you always need as integer? 
     builds = GetDiscartBelow(configuration, buildCleanupCount, builds); // Clean Code (almost) 
     builds = GetDiscartAbove(configuration, buildCleanupCount, builds); // Clean Code (almost) 

     return builds; 
    } 
    private static IEnumerable<Configuration> ArtifactCleanup(Configuration configuration, IEnumerable<Configuration> configurationBuilds) 
    { 
     if (configuration.artifact_cleanup_count != null) 
     { 
      // skipped, similar to previous block 
     } 

     return configurationBuilds; 
    } 
    private static SumPerConfiguration CreateSumPerConfiguration(IGrouping<object, Configuration> groupedBuilds) 
    { 
     var configuration = groupedBuilds.First(); 

     return new SumPerConfiguration 
     { 
      configurationId = configuration.configuration_id, 
      configurationPath = configuration.configuration_path, 
      Total = groupedBuilds.Sum(c => c.artifact_dir_size), 
      Average = groupedBuilds.Average(c => c.artifact_dir_size) 
     }; 
    } 
    private static IEnumerable<Configuration> GetDiscartBelow(Configuration configuration, 
     int buildCleanupCount, 
     IEnumerable<Configuration> configurationBuilds) 
    { 
     if (!configuration.build_cleanup_type.Equals("ReserveBuildsByDays")) 
      return configurationBuilds; 

     var buildLastCleanupDate = DateTime.UtcNow.AddDays(-buildCleanupCount); 
     var result = configurationBuilds 
      .Where(x => x.build_date > buildLastCleanupDate); 

     return result; 
    } 
    private static IEnumerable<Configuration> GetDiscartAbove(Configuration configuration, 
     int buildLastCleanupCount, 
     IEnumerable<Configuration> configurationBuilds) 
    { 
     if (!configuration.build_cleanup_type.Equals("ReserveBuildsByCount")) 
      return configurationBuilds; 

     var result = configurationBuilds 
      .Take(buildLastCleanupCount); 

     return result; 
    } 

    private static Notification CreateNotification(SumPerConfiguration iter) 
    { 
     return new Notification 
     { 
      ConfigurationId = iter.configurationId, 
      CreatedDate = DateTime.UtcNow, 
      RuleType = (int)RulesEnum.TooMuchDisc, 
      ConfigrationPath = iter.configurationPath 
     }; 
    } 
} 

internal class SumPerConfiguration { 
    public object configurationId { get; set; } // 
    public object configurationPath { get; set; } // I did use 'object' cause I don't know your type data 
    public int Total { get; set; } 
    public double Average { get; set; } 
} 
Problemi correlati