Skip to content

[Process] Unset callback after stop to free memory #16798

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

Closed
wants to merge 3 commits into from
Closed

[Process] Unset callback after stop to free memory #16798

wants to merge 3 commits into from

Conversation

Slamdunk
Copy link
Contributor

@Slamdunk Slamdunk commented Dec 2, 2015

Q A
Bug fix? yes
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets -
License MIT
Doc PR -

As of yet, a Process instance can't free memory.
The built-in callback has a self-reference to the intance not cleared after instance destruction.
I've tried to change both buildCallback and https://github.com/symfony/process/blob/v2.7.6/Process.php#L1373 usage to avoid self-referencing, but it can't be done without breaking BC, and the memory issue is still there.

A simple test script (that obiously can't be embedded in unit-tests)

for ($inc = 0; $inc < 10000; $inc++) {
    $process = new Symfony\Component\Process\Process('echo 1');
    $process->mustRun();
    $process->stop();
    unset($process);

    echo (memory_get_usage(true) / 1000000) . ' MB ' . $inc . PHP_EOL;
}

Before:

1.572864 MB 0
5.505024 MB 9999

After:

1.572864 MB 0
1.572864 MB 9999

Ok, in this simple scenario 4 MB of RAM is nothing, but in our business app full of IOC where unset-ting is hard, our RAM explodes.

After stop(), the callback is no longer necessary.

To be clear, the garbage collector in the previous example was already active.
Deactivating it manually, which somewhere is needed to avoid particluar segfaults, obiously leads to worse scenarios:

Before:

 gc_disable();
 1.572864 MB 0
29.360128 MB 9999

After:

 gc_disable();
 1.572864 MB 0
 1.572864 MB 9999

@@ -801,6 +801,8 @@ public function stop($timeout = 10, $signal = null)
$this->close();
}

$this->callback = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment explaining why it is done

@stof
Copy link
Member

stof commented Dec 2, 2015

this should also be reset at the end of a normal run (i.e. without the user stopping the process), as the callback won't be necessary anymore either (basically, the same benchmark without the call to ->stop())

@stof
Copy link
Member

stof commented Dec 2, 2015

And I think this can break the call to getOutput or getErrorOutput in the current implementation

@Slamdunk
Copy link
Contributor Author

Slamdunk commented Dec 2, 2015

@stof unsetting the callback automatically after run breaks getOutput and getErrorOutput, that's why I put it in stop, a method called only on __destruct or manually in particular cases.

I am sure that it does not break anything where I put it because:

  1. No test fails (the one failing in Windows is unreleated)
  2. The stop calls updateStatus, which calls readPipes and so all the Pipes are already filled. After that, every internal call to readPipes will result in an empty foreach, unless you restart the process with another callback build

My tests on unsetting the callback in any other context other then stop, like __destruct, has led to ineffective memory freeing, or breaking something.

@nicolas-grekas
Copy link
Member

I think the callback should also be reset in wait (and thus in run also since wait is used in run).

@Slamdunk
Copy link
Contributor Author

Slamdunk commented Dec 3, 2015

@nicolas-grekas only after pipes are close you can be sure to unset the callback, and the pipes are closed only in stop: https://github.com/symfony/process/blob/v3.0.0/Process.php#L1356

@nicolas-grekas
Copy link
Member

The line you linked doesn't proves anything about what stop does that wait doesn't do. And in fact, as far as I read correctly, wait waits for isRunning to be false, which means pipes are closed. Or did I read too quickly?

@Slamdunk
Copy link
Contributor Author

Slamdunk commented Dec 3, 2015

@nicolas-grekas your assumptions are based on the ability of PipesInterface::readAndWrite $close flag to close the Pipes. What I read in the interface (https://github.com/symfony/process/blob/v3.0.0/Pipes/PipesInterface.php#L43) is "Whether to close pipes if they've reached EOF" which means is not mandatory to close the pipes with the flag activated.
In fact on Windows, the pipes may not be closed even with the flag set to true: https://github.com/symfony/process/blob/v3.0.0/Pipes/WindowsPipes.php#L132 .
The only way to be sure pipes are closed, is to call PipesInterface::close, which you can be sure is always called if and only if the Process::stop is called because of this and this together.

@nicolas-grekas
Copy link
Member

@Slamdunk let me give you my understanding of the code:

readPipes is not part of this closing procedure (it happens earlier in the call chain)

@nicolas-grekas
Copy link
Member

Btw, this means that resetting the callback should be done only in the close method!

@Slamdunk
Copy link
Contributor Author

Slamdunk commented Dec 3, 2015

@nicolas-grekas 👍

@nicolas-grekas
Copy link
Member

👍 for merging on 2.3

@stof
Copy link
Member

stof commented Dec 5, 2015

👍

@stof
Copy link
Member

stof commented Dec 5, 2015

Thank you @Slamdunk.

stof added a commit that referenced this pull request Dec 5, 2015
…dunk)

This PR was submitted for the master branch but it was merged into the 2.3 branch instead (closes #16798).

Discussion
----------

[Process] Unset callback after stop to free memory

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

As of yet, a `Process` instance can't free memory.
The built-in callback has a self-reference to the intance not cleared after instance destruction.
I've tried to change both `buildCallback` and https://github.com/symfony/process/blob/v2.7.6/Process.php#L1373 usage to avoid self-referencing, but it can't be done without breaking BC, and the memory issue is still there.

A simple test script (that obiously can't be embedded in unit-tests)
```php
for ($inc = 0; $inc < 10000; $inc++) {
    $process = new Symfony\Component\Process\Process('echo 1');
    $process->mustRun();
    $process->stop();
    unset($process);

    echo (memory_get_usage(true) / 1000000) . ' MB ' . $inc . PHP_EOL;
}
```
Before:
```
1.572864 MB 0
5.505024 MB 9999
```
After:
```
1.572864 MB 0
1.572864 MB 9999
```
Ok, in this simple scenario 4 MB of RAM is nothing, but in our business app full of IOC where `unset`-ting is hard, our RAM explodes.

After `stop()`, the callback is no longer necessary.

To be clear, the garbage collector in the previous example was already active.
Deactivating it manually, which somewhere is needed to avoid particluar segfaults, obiously leads to worse scenarios:

Before:
```
 gc_disable();
 1.572864 MB 0
29.360128 MB 9999
```
After:
```
 gc_disable();
 1.572864 MB 0
 1.572864 MB 9999
```

Commits
-------

ec93b9a [Process] Unset callback after stop to free memory
@stof stof closed this Dec 5, 2015
@Slamdunk Slamdunk deleted the process/unset-callback branch December 7, 2015 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants