Doctrine 2 - ORM
  1. Doctrine 2 - ORM
  2. DDC-1402

Huge performance leak in SingleTablePersister

    Details

    • Type: Bug Bug
    • Status: Resolved
    • Priority: Critical Critical
    • Resolution: Fixed
    • Affects Version/s: 2.1, 2.1.1, 2.1.2
    • Fix Version/s: 2.1.3
    • Component/s: ORM
    • Security Level: All
    • Labels:
      None

      Description

      This code :

      /**
       * @Entity
       * @InheritanceType("SINGLE_TABLE")
       * @DiscriminatorColumn(name="discr", type="string")
       * @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
       */
      class Person
      {
          /** @Id @Column(type="integer") */ private $id;
      }
      
      /**
       * @Entity
       */
      class Employee extends Person
      {
          /** @Column(type="integer") */ private $a;
          /** @Column(type="integer") */ private $b;
          /** @Column(type="integer") */ private $c;
          /** @Column(type="integer") */ private $d;
          /** @Column(type="integer") */ private $e;
          /** @Column(type="integer") */ private $f;
          /** @Column(type="integer") */ private $g;
          /** @Column(type="integer") */ private $h;
      }
      
      foreach (range(0, 20) as $i) {
          $time = microtime(true);
          foreach (range(1, 100) as $j) {
              $id = ($i * 100) + $j;
              $entityManager->find('Person', $id);
          }
          printf("%4d ==> %f\n", $id, microtime(true) - $time);
      }
      

      Will output:

       100 ==> 0.461275
       200 ==> 1.128404
       300 ==> 1.823122
       400 ==> 2.521054
       500 ==> 3.232034
       600 ==> 3.950081
       700 ==> 4.648849
       800 ==> 5.380236
       900 ==> 6.080108
      1000 ==> 6.807214
      1100 ==> 7.519942
      1200 ==> 8.238971
      1300 ==> 8.951686
      1400 ==> 9.648996
      1500 ==> 10.370053
      1600 ==> 11.069523
      1700 ==> 11.791530
      1800 ==> 12.481427
      1900 ==> 13.190570
      2000 ==> 13.902810
      2100 ==> 14.671100
      

      With the first and last SELECT queries as:

      SELECT t0.id AS id1, discr, t0.a AS a2, t0.b AS b3, t0.c AS c4, t0.d AS d5, t0.e AS e6, t0.f AS f7, t0.g AS g8, t0.h AS h9 FROM Person t0 WHERE t0.id = 1 AND t0.discr IN ('person', 'employee')
      
      ...
      
      SELECT t0.id AS id1, discr, t0.a AS a16794, t0.b AS b16795, t0.c AS c16796, t0.d AS d16797, t0.e AS e16798, t0.f AS f16799, t0.g AS g16800, t0.h AS h16801 FROM Person t0 WHERE t0.id = 2100 AND t0.discr IN ('person', 'employee')
      

      Notes:

      • Last 100 SELECT queries take more than 14 seconds to execute! (table is empty)
      • Filed as a bug because a real life scenario caused major performance leak and memory usage when attempting to do 6000 calls to find().

      Analysis:

      1. Calls to SingleTablePersister::_getSelectColumnListSQL() do not use BasicEntityPersister::$_selectColumnListSql
      2. This generates a lot of calls to _getSelectColumnSQL() (See last SELECT query with alias counter up to 16800)
      3. This generates a lot of calls to addFieldResult()
      4. And this fills up pretty quickly ResultSetMapping::$declaringClasses
      5. To finally put a huge burden on SimpleObjectHydrator::_prepare() where it iterates on $this->_rsm->declaringClasses and calls getClassMetadata() for each one!
      6. And all that, including _prepare(), is executed for each SELECT, even though none of them find any result (the table is empty).

      I fixed my project using the following patch:

      diff --git a/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php b/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php
      index f910a8e..78b27cb 100644
      --- a/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php
      +++ b/doctrine-orm/Doctrine/ORM/Persisters/SingleTablePersister.php
      @@ -41,6 +41,15 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
           /** {@inheritdoc} */
           protected function _getSelectColumnListSQL()
           {
      +        /** @see BasicEntityPersister::_getSelectColumnListSQL() */
      +        if ($this->_selectColumnListSql !== null) {
      +            return $this->_selectColumnListSql;
      +        }
      +
      +        #####
      +        #####
      +        #####
      +
               $columnList = parent::_getSelectColumnListSQL();
       
               // Append discriminator column
      @@ -74,7 +83,13 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
                   }
               }
       
      -        return $columnList;
      +        #####
      +        #####
      +        #####
      +
      +        /** @see BasicEntityPersister::_getSelectColumnListSQL() */
      +        $this->_selectColumnListSql = $columnList;
      +        return $this->_selectColumnListSql;
           }
       
           /** {@inheritdoc} */
      

      I do not know if this patch is safe for everybody.

      But, well, you can easily reproduce the problem and analyze the phenomenon using a profiler on the sample code provided.

      Thanks for this great piece of software. I hope this will help you find and fix the bug.

        Activity

        Hide
        Benjamin Eberlei added a comment -

        good catch, thank you very much!

        fixed and merged back into 2.1.x

        Show
        Benjamin Eberlei added a comment - good catch, thank you very much! fixed and merged back into 2.1.x

          People

          • Assignee:
            Benjamin Eberlei
            Reporter:
            Sylvain Bernier
          • Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved: