2012-10-15 13 views
13

Ho cercato molto sul mio problema di prestazioni e ho provato un sacco di cose diverse, ma non riesco a farlo funzionare abbastanza velocemente. Ecco il mio problema per la sua forma più semplice:CodePrimo caricamento 1 genitore collegato a 25.000 bambini è lento

Sto usando l'entità framework 5 e voglio essere in grado di caricare le istanze secondarie di un genitore quando l'utente seleziona quel genitore, quindi non devo tirare l'intero Banca dati. Tuttavia ho avuto problemi di prestazioni con il caricamento pigro dei bambini. Penso che il problema sia il cablaggio delle proprietà di navigazione tra il genitore ed i bambini. Sto anche pensando che dev'essere qualcosa che ho sbagliato perché credo che sia un caso semplice.

Così ho installato un programma per testare un singolo carico pigro per isolare il problema.

Ecco il test:

ho creato una classe genitore e un bambino POCO POCO Class. Il genitore ha n Bambini e Bambino ha 1 genitore. C'è solo un genitore nel database di SQL Server e 25.000 bambini per quel genitore single. Ho provato diversi metodi per caricare questi dati. Ogni volta che carico i figli e il genitore nello stesso DbContext, ci vuole molto tempo. Ma se li carico in diversi DbContexts, si carica molto velocemente. Tuttavia, voglio che tali istanze siano nello stesso DbContext.

Qui è la mia messa a punto di test e tutto il necessario per replicare:

pocos:

public class Parent 
{ 
    public int ParentId { get; set; } 

    public string Name { get; set; } 

    public virtual List<Child> Childs { get; set; } 
} 

public class Child 
{ 
    public int ChildId { get; set; } 

    public int ParentId { get; set; } 

    public string Name { get; set; } 

    public virtual Parent Parent { get; set; } 
} 

DbContext:

public class Entities : DbContext 
{ 
    public DbSet<Parent> Parents { get; set; } 

    public DbSet<Child> Childs { get; set; } 
} 

TSQL script per creare il Datab ase e dei dati:

USE [master] 
GO 

IF EXISTS(SELECT name FROM sys.databases 
    WHERE name = 'PerformanceParentChild') 
    alter database [PerformanceParentChild] set single_user with rollback immediate 
    DROP DATABASE [PerformanceParentChild] 
GO 

CREATE DATABASE [PerformanceParentChild] 
GO 
USE [PerformanceParentChild] 
GO 
BEGIN TRAN T1; 
SET NOCOUNT ON 

CREATE TABLE [dbo].[Parents] 
(
    [ParentId] [int] CONSTRAINT PK_Parents PRIMARY KEY, 
    [Name] [nvarchar](200) NULL 
) 
GO 

CREATE TABLE [dbo].[Children] 
(
    [ChildId] [int] CONSTRAINT PK_Children PRIMARY KEY, 
    [ParentId] [int] NOT NULL, 
    [Name] [nvarchar](200) NULL 
) 
GO 

INSERT INTO Parents (ParentId, Name) 
VALUES (1, 'Parent') 

DECLARE @nbChildren int; 
DECLARE @childId int; 

SET @nbChildren = 25000; 
SET @childId = 0; 

WHILE @childId < @nbChildren 
BEGIN 
    SET @childId = @childId + 1; 
    INSERT INTO [dbo].[Children] (ChildId, ParentId, Name) 
    VALUES (@childId, 1, 'Child #' + convert(nvarchar(5), @childId)) 
END 

CREATE NONCLUSTERED INDEX [IX_ParentId] ON [dbo].[Children] 
(
    [ParentId] ASC 
) 
GO 

ALTER TABLE [dbo].[Children] ADD CONSTRAINT [FK_Children.Parents_ParentId] FOREIGN KEY([ParentId]) 
REFERENCES [dbo].[Parents] ([ParentId]) 
GO 

COMMIT TRAN T1; 

App.config che contiene la stringa di connessione:

<?xml version="1.0" encoding="utf-8"?> 
<configuration> 
    <connectionStrings> 
    <add 
     name="Entities" 
     providerName="System.Data.SqlClient" 
     connectionString="Server=localhost;Database=PerformanceParentChild;Trusted_Connection=true;"/> 
    </connectionStrings> 
</configuration> 

class Test console:

class Program 
{ 
    static void Main(string[] args) 
    { 
     List<Parent> parents; 
     List<Child> children; 

     Entities entities; 
     DateTime before; 
     TimeSpan childrenLoadElapsed; 
     TimeSpan parentLoadElapsed; 

     using (entities = new Entities()) 
     { 
      before = DateTime.Now; 
      parents = entities.Parents.ToList(); 
      parentLoadElapsed = DateTime.Now - before; 
      System.Diagnostics.Debug.WriteLine("Load only the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds"); 
     } 

     using (entities = new Entities()) 
     { 
      before = DateTime.Now; 
      children = entities.Childs.ToList(); 
      childrenLoadElapsed = DateTime.Now - before; 
      System.Diagnostics.Debug.WriteLine("Load only the children from DbSet:" + childrenLoadElapsed.TotalSeconds + " seconds"); 
     } 

     using (entities = new Entities()) 
     { 
      before = DateTime.Now; 
      parents = entities.Parents.ToList(); 
      parentLoadElapsed = DateTime.Now - before; 

      before = DateTime.Now; 
      children = entities.Childs.ToList(); 
      childrenLoadElapsed = DateTime.Now - before; 
      System.Diagnostics.Debug.WriteLine("Load the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds" + 
               ", then load the children from DbSet:" + childrenLoadElapsed.TotalSeconds + " seconds"); 
     } 

     using (entities = new Entities()) 
     { 
      before = DateTime.Now; 
      children = entities.Childs.ToList(); 
      childrenLoadElapsed = DateTime.Now - before; 

      before = DateTime.Now; 
      parents = entities.Parents.ToList(); 
      parentLoadElapsed = DateTime.Now - before; 


      System.Diagnostics.Debug.WriteLine("Load the children from DbSet:" + childrenLoadElapsed.TotalSeconds + " seconds" + 
               ", then load the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds"); 
     } 

     using (entities = new Entities()) 
     { 
      before = DateTime.Now; 
      parents = entities.Parents.ToList(); 
      parentLoadElapsed = DateTime.Now - before; 

      before = DateTime.Now; 
      children = parents[0].Childs; 
      childrenLoadElapsed = DateTime.Now - before; 
      System.Diagnostics.Debug.WriteLine("Load the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds" + 
               ", then load the children from Parent's lazy loaded navigation property:" + childrenLoadElapsed.TotalSeconds + " seconds"); 
     } 

     using (entities = new Entities()) 
     { 
      before = DateTime.Now; 
      parents = entities.Parents.Include(p => p.Childs).ToList(); 
      parentLoadElapsed = DateTime.Now - before; 
      System.Diagnostics.Debug.WriteLine("Load the parent from DbSet and children from include:" + parentLoadElapsed.TotalSeconds + " seconds"); 

     } 

     using (entities = new Entities()) 
     { 
      entities.Configuration.ProxyCreationEnabled = false; 
      entities.Configuration.AutoDetectChangesEnabled = false; 
      entities.Configuration.LazyLoadingEnabled = false; 
      entities.Configuration.ValidateOnSaveEnabled = false; 

      before = DateTime.Now; 
      parents = entities.Parents.Include(p => p.Childs).ToList(); 
      parentLoadElapsed = DateTime.Now - before; 
      System.Diagnostics.Debug.WriteLine("Load the parent from DbSet and children from include:" + parentLoadElapsed.TotalSeconds + " seconds with everything turned off"); 

     } 

    } 
} 

Ecco i risultati di tali test di:

Caricare solo il genitore da DbSet: 0,972 secondi

Carica solo i bambini da DbSet: 0,714 sec ondi

carico del genitore da DbSet: 0,001 secondi, quindi caricare i bambini da DbSet: 8,6026 secondi

Caricare i bambini da DbSet: 0,6864 secondi, quindi caricare il genitore da DbSet: 7,5816159 secondi

carico del genitore da DbSet: 0 secondi, quindi caricare i bambini dalla proprietà di navigazione caricato pigro del genitore: secondi 8,5644549

carico del genitore da DbSet e bambini da includono: secondi 8,6428788

Loa d il genitore da DbSet e bambini da includono: secondi 9,1416586 con tutto spenti

Analisi

Ogni volta che il genitore ed i bambini sono nella stessa DbContext, ci vuole molto tempo (9 secondi) collegare tutto. Ho anche provato a disattivare tutto, dalla creazione del proxy al caricamento lento, ma senza alcun risultato. Qualcuno può aiutarmi ?

+0

+1: Ottima domanda e analisi! – Slauma

risposta

5

Ho risposto in precedenza a similar question. La mia risposta precedente contiene la teoria che risponde a questo problema ma con la tua domanda dettagliata posso direttamente indicare dove si trova il problema. Per prima cosa eseguiamo uno dei casi problematici con Performance Profiler. Questo è il risultato di dotTrace quando si utilizza la modalità tracciamento:

enter image description here

rapporti di fissaggio viene eseguito in loop. Ciò significa che per 25.000 record avete 25.000 iterazioni, ma ciascuna di queste iterazioni chiama internamente CheckIfNavigationPropertyContainsEntity su EntityCollection:

internal override bool CheckIfNavigationPropertyContainsEntity(IEntityWrapper wrapper) 
{ 
    if (base.TargetAccessor.HasProperty) 
    { 
     object navigationPropertyValue = base.WrappedOwner.GetNavigationPropertyValue(this); 
     if (navigationPropertyValue != null) 
     { 
      if (!(navigationPropertyValue is IEnumerable)) 
      { 
       throw new EntityException(Strings.ObjectStateEntry_UnableToEnumerateCollection(base.TargetAccessor.PropertyName, base.WrappedOwner.Entity.GetType().FullName)); 
      } 
      foreach (object obj3 in navigationPropertyValue as IEnumerable) 
      { 
       if (object.Equals(obj3, wrapper.Entity)) 
       { 
        return true; 
       } 
      } 
     } 
    } 
    return false; 
} 

numero di iterazioni del ciclo interno cresce man mano che gli elementi vengono aggiunti alla proprietà di navigazione. La matematica è nella mia risposta precedente - è una serie aritmetica in cui il numero totale di iterazioni del ciclo interno è 1/2 * (n^2 - n) => n^2 complessità. Il loop interno all'interno del loop esterno produce 312.487.500 iterazioni nel tuo caso, come anche gli spettacoli di tracciamento delle prestazioni.

Ho creato work item on EF CodePlex per questo problema.

+0

Questa è un'ottima risposta, grazie mille. Ti sono anche grato per aver creato un oggetto di lavoro su EF CodePlex. – CurlyFire

5

Questa non è una risposta in quanto non ho una soluzione per migliorare le prestazioni, ma la sezione dei commenti non ha spazio sufficiente per quanto segue. Voglio solo aggiungere alcuni test e osservazioni aggiuntivi.

Innanzitutto, è possibile riprodurre i tempi misurati quasi esattamente per tutti e sette i test. Ho usato EF 4.1 per il test.

Alcune cose interessanti da notare:

  • Da (il digiuno) prova di 2 vorrei concludere che materializzazione oggetto (convertendo le righe e le colonne restituite dal server di database in oggetti) non è lenta.

  • Ciò è confermato anche caricando le entità in prova 3 senza il rilevamento delle modifiche:

    parents = entities.Parents.AsNoTracking().ToList(); 
    // ... 
    children = entities.Childs.AsNoTracking().ToList(); 
    

    Questo codice corre veloce anche se 25001 oggetti devono essere materializzato così (ma non le relazioni tra le proprietà di navigazione saranno stabilito!).

  • Anche dal test (veloce) 2 concluderei che la creazione di istantanee di entità per il rilevamento delle modifiche non è lenta.

  • Nei test 3 e 4 le relazioni tra padre e 25000 bambini vengono corrette quando le entità vengono caricate dal database, ad es.EF aggiunge tutte le entità Child alla raccolta Childs del genitore e imposta lo Parent in ogni figlio sul genitore caricato. A quanto pare questo passo è lento, come già indovinato:

    Credo che il problema è il filo di proprietà di navigazione tra la Capogruppo e dei bambini.

    Soprattutto il lato raccolta del rapporto sembra essere il problema: se si commento la proprietà Childs navigazione nella classe Parent (il rapporto è ancora necessaria relazione uno-a-molti, allora) le prove 3 e 4 sono veloci, sebbene EF stabilisca ancora la proprietà Parent per tutte le 25000 entità Child.

    Non so perché riempire la raccolta di navigazione durante la risoluzione dei problemi è così lento. Se lo simuli manualmente in modo naif, in questo modo ...

    entities.Configuration.ProxyCreationEnabled = false; 
    
    children = entities.Childs.AsNoTracking().ToList(); 
    parents = entities.Parents.AsNoTracking().ToList(); 
    
    parents[0].Childs = new List<Child>(); 
    foreach (var c in children) 
    { 
        if (c.ParentId == parents[0].ParentId) 
        { 
         c.Parent = parents[0]; 
         parents[0].Childs.Add(c); 
        } 
    } 
    

    ... va veloce. Ovviamente la correzione delle relazioni internamente non funziona in questo modo semplice. Forse ha bisogno di essere controllato se la raccolta contiene già il bambino da testare:

    foreach (var c in children) 
    { 
        if (c.ParentId == parents[0].ParentId) 
        { 
         c.Parent = parents[0]; 
         if (!parents[0].Childs.Contains(c)) 
          parents[0].Childs.Add(c); 
        } 
    } 
    

    Questo è significativamente più lenta (circa 4 secondi).

In ogni caso, la correzione delle relazioni sembra essere il bottleneck delle prestazioni. Non so come migliorarlo se hai bisogno di cambiare tracciamento e correggere le relazioni tra le tue entità allegate.

+0

Grazie per avermi dato una mano! Modificherò il post con i tuoi commenti e analisi aggiuntive. Formerò anche le mie analisi come hai fatto tu, è molto più chiaro quale test è stato utilizzato per dedurre un punto. – CurlyFire

+0

Ho anche fatto un altro test mentre stavo aspettando un aiuto. Ho creato un livello di accesso ai dati NHibernate che utilizza gli stessi POCO e il codice di test di questo (tranne gli ultimi 2) e ogni test viene eseguito con il segno da 1 secondo. – CurlyFire

+0

@CurlyFire: NHibernate aggiusta automaticamente le relazioni? Ad esempio, nel test 3, NH imposta 'Child.Parent' e' Parent.Childs' per tutte le entità caricate sugli oggetti corretti? – Slauma

Problemi correlati