23

I'm trying to pack my web application (Symfony 2 project) with Phar. I've successfully packed Silex, a micro framework with hundred of files in a reasonable time (1-2 minutes).

The problem is on my development machine (i7 4770k, 16GB, SSD Raid 0, project on a RAM disk) creating the archive is really slow, it takes ~1 sec for each file. I really need to find out a way to speed up things.

Single iteration of reading/loading the file is slow. I'm adding files using:

function addFile(Phar $phar, SplFileInfo $file)
{
    $root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
    $path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
    $phar->addFromString($path, file_get_contents($file));
}

$phar = new Phar(/* ... */);
$phar->startBuffering();

// ...    
foreach ($files as $file) {
    addFile($phar, $file);
}

// ...
$phar->setStub(/* ... */);
$phar->stopBuffering();

How can I speed up reading/adding files? Could be my OS (Windows) the problem?

EDIT: disabling buffering didn't solve the problem. Same speed adding from strings:

// This is VERY fast (~ 1 sec to read all 3000+ files)
$strings = array();
foreach ($files as $file) {
    $root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
    $path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');

    $strings[$path] = file_get_contents($file->getRealPath());
}

// This is SLOW
foreach ($strings as $local => $content) {
    $phar->addFromString($local, $content);
}

EDIT: full quick&dirty script (may help) app/build:

#!/usr/bin/env php
<?php

set_time_limit(0);

require __DIR__.'/../vendor/autoload.php';

use Symfony\Component\Finder\Finder;
use Symfony\Component\Console\Input\ArgvInput;

$input = new ArgvInput();
$env = $input->getParameterOption(array('--env', '-e'), 'dev');

function addFile(Phar $phar, SplFileInfo $file)
{
    $root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
    $path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
    $phar->addFromString($path, file_get_contents($file));
}

$phar = new Phar(__DIR__ . "/../symfony.phar", 0, "symfony.phar");
$phar->startBuffering();

$envexclude = array_diff(array('dev', 'prod', 'test'), array($env));

// App
$app = (new Finder())
    ->files()
    ->notPath('/cache/')
    ->notPath('/logs/')
    ->notName('build')
    ->notname('/._('.implode('|', $envexclude).')\.yml$/')
    ->in(__DIR__);

// Vendor
$vendor = (new Finder())
    ->files()
    ->ignoreVCS(true)
    ->name('*.{php,twig,xlf,xsd,xml}')
    ->notPath('/tests/i')
    ->notPath('/docs/i')
    ->in(__DIR__.'/../vendor');

// Src
$src = (new Finder())
    ->files()
    ->notPath('/tests/i')
    ->in(__DIR__.'/../src');

// Web
$web = (new Finder())
    ->files()
    ->in(__DIR__.'/../web')
    ->notname('/._('.implode('|', $envexclude).')\.php$/');

$all = array_merge(
    iterator_to_array($app),
    iterator_to_array($src),
    iterator_to_array($vendor),
    iterator_to_array($web)
);

$c = count($all);
$i = 1;
$strings = array();
foreach ($all as $file) {
    addFile($phar, $file);
    echo "Done $i/$c\r\n";
    $i++;
}

$stub = <<<'STUB'
Phar::webPhar(null, "/web/app_phar.php", null, array(), function ($path) {
    return '/web/app_phar.php'.$path;
});

__HALT_COMPILER();
STUB;

$phar->setStub($stub);
$phar->stopBuffering();
gremo
  • 47,186
  • 75
  • 257
  • 421
  • Out of interest (and maybe to get some context), why do you need to speed this up? If you are creating a distributable phar file, could you just automate this in some way, so when your master changes, it kicks off a build process on a build? – halfer May 30 '14 at 22:09
  • @halfer thank you for you comment. Yes, I'll automate the distribution later. But right now, I need to check that the project can actually run (i.e. loading of templates, static assets, performances, etc) so it's a trial/error process ATM. – gremo May 30 '14 at 22:14
  • This is a shot in the dark, but assuming that this is all going through PHP, did you ever stop & maybe look at the PHP memory settings that can be adjusted to improve performance? – Giacomo1968 May 30 '14 at 22:14
  • @JakeGould do you mean memory limits? I didn't get any errors about memory, but if tuning can help I'll do it. Just point me to the right direction, thanks. – gremo May 30 '14 at 22:21
  • 1
    Have you checked out the process in your task manager? Is there high load on the cpu or RAM? – Dan May 30 '14 at 22:22
  • 1
    I think Jake is referring to http://de2.php.net/manual/en/ini.core.php#ini.memory-limit – Dan May 30 '14 at 22:24
  • @Dan really low cpu/memory usage. As I can see from the task manager, php.exe process is getting 1% of CPU, memory ~22MB. CPU is in idle all the time. – gremo May 30 '14 at 22:24
  • @gremo In general, your memory & CPU limits might seem light, but perhaps the processes that Symphony needs to create a PHAR are not making the best use of them. In general, having all the RAM & CPU power means nothing unless a system is properly configured. – Giacomo1968 May 30 '14 at 22:26
  • 1
    Doesn't sound like your machine is the bottleneck then. Take a look at the php settings as Jake suggested. – Dan May 30 '14 at 22:26
  • 1
    Look at this as well: http://www.phing.info/trac/ticket/782 – Giacomo1968 May 30 '14 at 22:27
  • How much RAM is allocated in the console php.ini? Try setting it to 128 or 256M. – halfer May 30 '14 at 22:27
  • Also, https://groups.google.com/forum/#!topic/maven-for-php/dlir72QenKA – Giacomo1968 May 30 '14 at 22:28
  • Symfony is not part of the process, my build.php file is just reading/writing to the phar. Current memory limit is 128MB (default), increasing didn't help. – gremo May 30 '14 at 22:29
  • 3
    Yeah, memory limit is just a limit, not an allocation. Increasing it will make no difference unless your script is dying with an error that it couldn't allocate more memory. – IMSoP May 30 '14 at 23:03
  • 1
    If you're just moving the files, why open them and read them to a string? – VikingBlooded Jun 10 '14 at 16:02
  • You could try to start the process and set the priority to realtime afterwards. – Eun Dec 01 '14 at 15:23
  • Just for the sake of the test, can you compare with the time it takes to zip up your project directory (e.g. with Windows built-in compressor)? (I have seen unexpected behaviours with SSD. Maybe ruling out any problem at OS/HW level is useful, or maybe not). – RandomSeed Dec 02 '14 at 09:02
  • Symfony standard edition + Acme bundle + your script = 3510 files in 10 minutes 39 seconds. PHP version 5.5.7 i7-920 Samsing EVO 840. If you also have a samsung SSD try applying a firmware update, for fix performance bug. http://www.anandtech.com/show/8617/samsung-releases-firmware-update-to-fix-the-ssd-840-evo-read-performance-bug – Flip Dec 03 '14 at 20:54
  • Finally, @gremo could you build your Symfony app and make it be runnable on a web-server? I'm interested in the process, because I'm using Box-Project to package one of my apps, and it takes hours... (and generates a 50MB phar archive) – Alex Rock Aug 06 '15 at 15:01
  • @AlexPierstoval yes, but isn't yet complete. There are many config files to change in order to make it work. If you need to try it out and make some tests I would suggest to use box and split "vendor.phar" and "app.phar" modifing the autoloader as needed. Then you only need to update app.phar. – gremo Aug 06 '15 at 15:37
  • Actually, I could package everything in the phar archive, and I've set up a proper Kernel and HttpCache class for this "portable" app so the cache and logs are always located "outside" the phar file. But right now, I'm experiencing many problems with Annotations (any annotation, not only Doctrine), because paths are not well resolved when looking for the file, because it uses "realpath", and realpath always return false when inside a phar archive... – Alex Rock Aug 07 '15 at 07:24

9 Answers9

11

Try using Phar::addFile($file) instead of Phar::addFromString(file_get_contents($file))

i.e.

function addFile(Phar $phar, SplFileInfo $file)
{
    $root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
    $path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
    //$phar->addFromString($path, file_get_contents($file));
    $phar->addFile($file,$path);
}
FuzzyTree
  • 32,014
  • 3
  • 54
  • 85
9

Phar::addFromString() or Phar:addFromFile() is incredibly slow. Like @sectus said Phar::buildFromDirectory() is a lot faster. But as an easy alternative with little changes to your code you could use Phar::buildFromIterator().

Example:

$all = $app->append($vendor)->append($src)->append($web);
$phar->buildFromIterator($all, dirname(__DIR__));

instead of:

$all = array_merge(
    iterator_to_array($app),
    iterator_to_array($src),
    iterator_to_array($vendor),
    iterator_to_array($web)
);

$c = count($all);
$i = 1;
$strings = array();
foreach ($all as $file) {
    addFile($phar, $file);
    echo "Done $i/$c\r\n";
    $i++;
}

$ time app/build

real    0m4.459s
user    0m2.895s
sys     0m1.108s

Takes < 5 seconds on my quite slow ubuntu machine.

Community
  • 1
  • 1
hnesk
  • 650
  • 4
  • 10
4

I'd suggest to dig in php config.

First recomendation - is to disable open_basedir if it is enabled. As i understand php internals, when u try to access any file location with php, it is a must for php to check if file location matches allowed directory-tree. So if there are many files this operation will be preformed for each file, and it can slow the proccess down significantly. On the other hand if open_basedir is disabled, check is never done.

http://www.php.net/manual/en/ini.core.php#ini.open-basedir

Second - is to check realpath_cache_size and realpath_cache_ttl.

As written in php description

Determines the size of the realpath cache to be used by PHP. This value should be increased on systems where PHP opens many files, to reflect the quantity of the file operations performed.

http://www.php.net/manual/en/ini.core.php#ini.realpath-cache-size

I hope this will help u to speed up ur operations.

fperet
  • 436
  • 8
  • 21
3

You know, I have realized that Phar::buildFromDirectory is pretty fast.

$phar->buildFromDirectory('./src/', '/\.php$/');

But you need write more complicated regex. But you could call buildFromDirectory several times with different arguments.

Or create temporary folder and copy all files into from $all. Something like this

function myCopy($src, $dest)
{
    @mkdir(dirname($dest), 0755, true);
    copy($src, $dest);
}

foreach ($all as $file)
{
    //$phar->addFile($file);
    myCopy($file, './tmp/' . $file);
}

$phar->buildFromDirectory('./tmp/');
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
sectus
  • 15,605
  • 5
  • 55
  • 97
  • For anyone wondering about the `@` before the `mkdir`: It [supresses all errors](http://stackoverflow.com/questions/1032161/what-is-the-use-of-the-symbol-in-php). – Uwe Keim Dec 28 '16 at 08:13
1

I would suggest using Preloader to concatenate all your files into a single file and then simply add that single file to the phar.

Wing Lian
  • 2,387
  • 16
  • 14
1

I know you said that adding the filename from string did not improve performance, but perhaps a different way of loading the file names can improve performance along with using the filename from string. Composer is pretty fast, but I never timed it. Try loading the files by group of filetypes and adding them as groups separately.

It uses a class from Symfony which you may not want or would change.

use Symfony\Component\Finder\Finder;

$phar = new \Phar(/* ... */);
$phar->setSignatureAlgorithm(\Phar::SHA1);
$phar->startBuffering();
$finder = new Finder();

//add php files with filter
$finder->files()
        ->ignoreVCS(true)
        ->name('*.php')
        ->notName('Compiler.php')
        ->notName('ClassLoader.php')
        ->in(__DIR__.'/..')
    ;

foreach ($finder as $file) {
    $this->addFile($phar, $file);
    }

$this->addFile($phar, new \SplFileInfo(/* ... */), false);

$finder = new Finder();
$finder->files()
     ->name('*.json')
    ->in(__DIR__ . '/../../res')
    ;

foreach ($finder as $file) {
     $this->addFile($phar, $file, false);
    }

    $this->addFile($phar, new \SplFileInfo(/* ... */), false);

$phar->setStub($this->getStub());
$phar->stopBuffering();

Perhaps you can exclude a cache or log file using the Finer's filters if somehow its one large file that causes the long lag. Look at the composer link for full details on how its implemented.

George
  • 1,478
  • 17
  • 28
0

what about using class instead of passing phar into func? just a piece of code to understand.. or ever heard about memory limit of php.ini or other setting that can slow down things.

class XY {

private $phar;

function addFile(SplFileInfo $file)
    $root = realpath(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR);
    $path = strtr(str_replace($root, '', $file->getRealPath()), '\\', '/');
    $this->phar->addFromString($path, file_get_contents($file));
}
// other code here
}

Correct me if i am wrong but in this way instead of passing the phar into function, you will avoid "copying" of object. this way is like pointer.

Erik Kubica
  • 1,180
  • 3
  • 15
  • 39
  • 1
    This wont change anything, since PHP doesn't copy object method arguments anymore since version 5. http://php.net/manual/en/language.oop5.references.php . – hnesk Dec 06 '14 at 14:00
  • not only when you use the reference sign? "&" $a = &$b; then it is reference, but when $a = $b then $a is copy of $b ?? Sometimes English explanation is hard to understand for me. I have started to learn C and this makes me mad – Erik Kubica Dec 09 '14 at 12:25
  • No, what you describe is the PHP4 way. Take a look at the link I provided – hnesk Dec 09 '14 at 15:44
  • Yeah i have checked the docs, but the explanation is complicated for me. But your comments is enoungh to understand so thank you – Erik Kubica Dec 09 '14 at 19:28
0

you can create threads and reduce total time, of course Symfony has to support concurrent loading. it is not actually best answer for your question, but it can significantly decrease total load time.

class Loader extends Thread {
    public $phar;
    public $file;
    public $done;
    public function run() {
        $self->done = false;
        addFile($self->phar, $self->file);
        $self->done = true;
    }
}
$threads = array();
foreach ($files as $file) {
    $t = new Loader();
    $t->phar = $phar;
    $t->file = $file;
    addFile($phar, $file);
    $t->start();
    $threads[] = $t;
}
while(true){
  $finished = true;
  foreach ($t as $threads) {
     if ($t->done == false){
        $finished = false;
        sleep(1);
        break;
     }
  }
  if ($finished)
    break;
}

and, creating 3000 threads is not good idea. you might need to create well thread-worker logic.

Adem
  • 9,402
  • 9
  • 43
  • 58
-2

Try to disable GC like composer did

gc_disable();
Alexey B.
  • 11,965
  • 2
  • 49
  • 73
  • gc_disable() fixes all PHP code magically. No seriously the cyclic dependency garbage collector kicks in at 10.000 objects, and there is just one `new Phar` – Flip Dec 03 '14 at 14:51