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?
Profilo e vedere quali metodi/linee utilizzano più tempo. –
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
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 –