Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Behaviors cause memoy leaks #1329

Closed
bwoester opened this issue Sep 3, 2012 · 20 comments
Closed

Behaviors cause memoy leaks #1329

bwoester opened this issue Sep 3, 2012 · 20 comments
Labels

Comments

@bwoester
Copy link

bwoester commented Sep 3, 2012

Basically, whenever you attach a CBehavior to a CComponent, this prevents the CComponent instance from being destroyed. As far as I can tell, this happens because you create a cyclic dependency: The component holds a reference to the attached behavior, the behavior holds a reference to its owner.

So this bug is by design and I have no idea if it can be solved. But maybe someone has an idea about it.

To reproduce:

// application.config.main
    'log'=>array(
      'routes'=>array(
        array(
          'class' => 'CWebLogRoute',
          'levels'=>'warning',
          'categories'=>'test.*',
        ),
    ),
// SiteController.php

class HeavyWeightComponent extends CComponent
{
  public $data = '';
  public function __construct() {
    $this->data = str_repeat( '1', 10*1024 ); // allocate 10k mem
  }
}

class SiteController extends Controller
{
  private function createHeavyWeightComponents( $attachBehavior )
  {
    Yii::log( 'Enter local function scope', CLogger::LEVEL_WARNING, 'test' );

    if ($attachBehavior) {
      Yii::log('Creating 100 heavy weight CComponents with attached behavior', CLogger::LEVEL_WARNING, 'test');
    } else {
      Yii::log('Creating 100 heavy weight CComponents', CLogger::LEVEL_WARNING, 'test');
    }

    for ($i = 0; $i < 100; $i++)
    {
      $c = Yii::createComponent('HeavyWeightComponent');

      if ($attachBehavior) {
        $c->attachBehavior( 'test', 'CBehavior' );
      }
    }

    Yii::log( 'Leaving local function scope', CLogger::LEVEL_WARNING, 'test' );
  }

  private function getHumanReadableMemoryUsage()
  {
    return $this->formatBytes( memory_get_usage() );
  }

  private function formatBytes( $size, $sizes = array('Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB') )
  {
    if ($size == 0) {
      return('n/a');
    }

    return (round($size/pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $sizes[$i]);
  }

  public function actionIndex()
  {
    $memoryUsage = $this->getHumanReadableMemoryUsage();
    Yii::log("Memory usage: '$memoryUsage'", CLogger::LEVEL_WARNING, 'test');

    for ($i=0; $i<10; $i++)
    {
      $this->createHeavyWeightComponents( false );
      $memoryUsage = $this->getHumanReadableMemoryUsage();
      Yii::log("Memory usage: '$memoryUsage'", CLogger::LEVEL_WARNING, 'test');
    }

    for ($i=0; $i<5; $i++)
    {
      $this->createHeavyWeightComponents( true );
      $memoryUsage = $this->getHumanReadableMemoryUsage();
      Yii::log("Memory usage: '$memoryUsage'", CLogger::LEVEL_WARNING, 'test');
    }

    // renders the view file 'protected/views/site/index.php'
    // using the default layout 'protected/views/layouts/main.php'
    $this->render('index');
  }
}

Run the action and you will see that normally, the heavy weight components will be cleaned up when leaving the local function scope (memory increases a little bit, maybe it's the log messages). But whenever a behavior is attached to the components, the memory won't be cleaned up.

@samdark
Copy link
Member

samdark commented Sep 3, 2012

Yes, that's confirmed issue. Would be great if it will be solved but I think it's not critical currently since Yii primary use is web applications where memory is cleaned up after each request-response cycle.

@klimov-paul
Copy link
Member

You can use "CComponent::detachBehaviors()" to strip the object from behaviors and ensure garbage collection.

@klimov-paul
Copy link
Member

PHP core team declares, that PHP 5.3 solves the problem of cycle references:

http://www.php.net/manual/en/features.gc.collecting-cycles.php

@bwoester, can you confirm, this problem is actual for PHP 5.3?

@bwoester
Copy link
Author

bwoester commented Sep 4, 2012

PHP 5.3 will detect the cyclic dependency. I didn't notice it when writing the issue, because the cycle detection doesn't run permanently. But you can trigger it manually using gc_enable() / gc_collect_cycles() / dump mem_usage in the test to verify.

So I think this issue won't affect many people.

@klimov-paul
Copy link
Member

@samdark, I think the Yii documentation related to the behavior mechanism should be updated: the warning about possible memoy leaks should be added as well as an instructions, how avoid it.

@cebe
Copy link
Member

cebe commented Sep 5, 2012

@klimov-paul I think we should simply fix it by implementing __destruct() haven't looked into this in detail but does not look too complicated to me right now.

@klimov-paul
Copy link
Member

@cebe, just how implementing __destruct() can solve the memory leaks?
Magic method __destruct() invoked in case the object instance is destroyed by the PHP memory garbage collector.
This issue says the objects are NOT destroyed by garbage collector in case they have behaviors attached.
PHP does NOT provide any option to manually destroy any object in the memory.

Before making anything rush, you should make yourself familiar with "cycle references" problem and garabage collection mechanism in PHP.

@cebe
Copy link
Member

cebe commented Sep 5, 2012

@klimov-paul I know well about cycle references. As said I haven't really looked into this, so if it can not be easily resolved as you said it should be documented of course.

@klimov-paul
Copy link
Member

Sorry, no offence.

@bwoester
Copy link
Author

bwoester commented Sep 5, 2012

I think there isn't much you can do to solve or avoid this. Best will be to use PHP >= 5.3 and to ensure circular reference collector is enabled.

I played with destructors, but as klimov-paul mentioned it doesn't help, because it will only be invoked when the the refcount reaches 0.

I tried attaching owners to behaviors via reference, but the refcount will still be increased by php.

I found some suggestions to invoke tear-down methods manually to clean up references (CComponent::detachBehaviors could be used as/in such a tear-down method). But this is not only cumbersome, it also required you to know about internas of the attached behaviors. For example if one of them passed its owner to some other component, the owner must not be destroyed. And as long as it won't be destroyed, you don't want its behaviors to be detached...

Whatever I come up with, it feels like running in circles. Maybe there is no solution as long as php doesn't provide a way to query and/or manually modify the refcount.

@qiangxue
Copy link
Member

qiangxue commented Sep 5, 2012

Agree with @bwoester. We had this issue logged before, and we ended closing it without doing anything.

We have to keep a reference to the behavior in the owner object so that we can support the access to behavior methods/properties via the owner. On the other hand, we also need the reference to the owner in the behavior in case the behavior needs to access something in the owner. So this cyclic reference seems unavoidable.

I'm closing this issue for the reason as described by bwoester.

@qiangxue qiangxue closed this as completed Sep 5, 2012
@cebe
Copy link
Member

cebe commented Sep 6, 2012

@klimov-paul didn't take it as serious as it might have sounded, everything is fine :)

@sisoje
Copy link

sisoje commented Feb 6, 2013

i use some old YII version with php 5.3 and it still leaks. i tried calling GC and still leaks

@samdark
Copy link
Member

samdark commented Feb 6, 2013

@sisoje the obvious suggestion is to update Yii.

@pohnean
Copy link

pohnean commented May 29, 2013

any idea when this will be fixed? i'm also facing this issue now (using PHP 5.3). Many of my models use the CTimeStampBehavior and a custom SoftDeleteBahavior.... With 128Mb of RAM allocated to php, I'm only able to generate 400+ instances of my ActiveRecord. This means I can't display >400 items in a single page in GridView without using SqlDataProvider. I also have background php scripts that loop many times. It's going to be painful to have to rewrite all the code in my models in SQL (not to mention code duplication). please help!

@klimov-paul
Copy link
Member

@pohnean, this issue can not be resolved in framework.

I'm only able to generate 400+ instances of my ActiveRecord

I am sorry but can not be resolve anyhow: PHP consumes a lot of memory per each object, when you have 400 models, each having 2 behaviors, means you have 1200 objects - no wonder you approach memory limit. All I can suggest is reducing page size - is it really necessary to show 400 records at the same page?

I also have background php scripts that loop many times

You should try to invoke "detachBehaviors()" for the models.
Or use following code:

gc_enable(); // Enable Garbage Collector
var_dump(gc_enabled()); // true
var_dump(gc_collect_cycles()); // # of elements cleaned up
gc_disable(); // Disable Garbage Collector

@cebe
Copy link
Member

cebe commented May 29, 2013

@pohnean you may want to use CDataProviderIterator for this: https://github.com/yiisoft/yii/blob/master/framework/web/CDataProviderIterator.php

@pohnean
Copy link

pohnean commented May 31, 2013

@cebe Thanks for the feedback, i'll try CDataProviderIterator... @klimov-paul The function i'm using it in is a background process. I'm actually creating the variable repeatly using the same variable, and not storing it in an array... (see example below)

example:
for ($i = 0; $i < 400; $i++) {
$model = new Model(); // this model has behavior attached
$model->doSomething();
unset($model);
}

This rightfully it should not take up additional memory if memory doesn't leak.... unless my understanding of PHP is mistaken...

@AnatolyRugalev
Copy link
Contributor

@pohnean, this happens because of objects linking recursion (model links to behavior and behavior links to model as its owner). Try to call $model->detachBehaviors() before unset($model).

This may happen when you have two-side parent-child links in objects. GC doesn't destroy them properly. Looks like this bug

@klimov-paul
Copy link
Member

Try to update your code to the following:

gc_enable(); // Enable Garbage Collector
for ($i = 0; $i < 400; $i++) {
    $model = new Model(); // this model has behavior attached
    $model->doSomething();
    unset($model);
    gc_collect_cycles(); // Ensure cycle references GC
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants