4

Even after a very high score of Google PageSpeed(97) & Yahoo! YSlow(92) the PHP generated thumbnails don't seem to be coming passively from an old cache: they seem to be generated every time again...and again... freshly baked consuming lots of waisted time.

This question will focus only & specifically on how to solve the CACHE problem of the PHP Code that generates the thumbs:

Just have a look at these tiny puny little thumbnails measuring only 3 ~ 5 kb each!

Waterfall in detail: http://www.webpagetest.org/result/110328_AM_8T00/1/details/

Any & all suggestons are +1 help to me and warmly welcome, for I have grown quite desperate on this issue for the last months. Thanx a Thousand!

Using or not Modrewrite does not influence speed: both are same. I use these rewrite conditions: RewriteCond %{REQUEST_URI} ^/IMG-.*$ & RewriteCond %{REQUEST_FILENAME} !-f

Both the original default URL as well as the beautified rewritten URL produce the same delays!! So let us not point the fault to the lightning fast Apache: its the PHP Cache / headers that are somehow wrongly coded...

enter image description here


Warning by webpagetest.org: Leverage browser caching of static assets: 69/100

FAILED - (No max-age or expires): http://aster.nu/imgcpu?src=aster_bg/124.jpg&w=1400&h=100&c=p


After each refresh, you will see either of these two warnings appear on random at REDbot.org enter image description here enter image description here


Relevant Portions of The Code:

// Script is directly called
if(isset($_GET['src']) && (isset($_GET['w']) || isset($_GET['h']) || isset($_GET['m']) || isset($_GET['f']) || isset($_GET['q']))){
    $ImageProcessor = new ImageProcessor(true);
    $ImageProcessor->Load($_GET['src'], true);
    $ImageProcessor->EnableCache("/var/www/vhosts/blabla.org/httpdocs/tmp/", 345600);
    $ImageProcessor->Parse($quality);
}

/* Images processing class
 * - create image thumbnails on the fly
 * - Can be used with direct url imgcpu.php?src=
 * - Cache images for efficiency 
 */
class ImageProcessor
{
    private $_image_path;      # Origninal image path
    protected $_image_name;    # Image name   string
    private $_image_type;      # Image type  int    
    protected $_mime;          # Image mime type  string    
    private $_direct_call = false;   # Is it a direct url call?  boolean        
    private $_image_resource;  # Image resource   var Resource      
    private $_cache_folder;    # Cache folder strig
    private $_cache_ttl;        # Cache time to live  int
    private $_cache = false;    # Cache on   boolean
    private $_cache_skip = false;   # Cache skip   var boolean

    private function cleanUrl($image){   # Cleanup url
        $cimage = str_replace("\\", "/", $image);
        return $cimage;
    }   

    /** Get image resource
     * @access private, @param string $image, @param string $extension, @return resource  */
    private function GetImageResource($image, $extension){
        switch($extension){
            case "jpg":
                @ini_set('gd.jpeg_ignore_warning', 1);
                $resource = imagecreatefromjpeg($image);
                break;
        }
        return $resource;
    }


    /* Save image to cache folder
     * @access private, @return void  */
    private function cacheImage($name, $content){

        # Write content file
        $path = $this->_cache_folder . $name;
        $fh = fopen($path, 'w') or die("can't open file");
        fwrite($fh, $content);
        fclose($fh);

        # Delete expired images
        foreach (glob($this->_cache_folder . "*") as $filename) {
            if(filemtime($filename) < (time() - $this->_cache_ttl)){
                unlink( $filename );
            }
        }
    }

    /* Get an image from cache
     * @access public, @param string $name, @return void */
    private function cachedImage($name){
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    }

    /* Get name of the cache file
     * @access private, @return string  */
    private function generateCacheName(){
        $get = implode("-", $_GET);
        return md5($this->_resize_mode . $this->_image_path . $this->_old_width . $this->_old_height . $this->_new_width . $this->_new_height . $get) . "." . $this->_extension;
    }

    /* Check if a cache file is expired
     * @access private,  @return bool  */
    private function cacheExpired(){
        $path = $this->_cache_folder . $this->generateCacheName();
        if(file_exists($path)){
            $filetime = filemtime($path);
            return $filetime < (time() - $this->_cache_ttl);
        }else{
            return true;
        }
    }

    /* Lazy load the image resource needed for the caching to work
     * @return void */
    private function lazyLoad(){
        if(empty($this->_image_resource)){
            if($this->_cache && !$this->cacheExpired()){
                $this->_cache_skip = true;
                return;
            }
            $resource = $this->GetImageResource($this->_image_path, $this->_extension);
            $this->_image_resource = $resource;
        }    
    }

    /* Constructor
     * @access public, @param bool $direct_call, @return void */
    public function __construct($direct_call=false){

    # Check if GD extension is loaded
        if (!extension_loaded('gd') && !extension_loaded('gd2')) {
            $this->showError("GD is not loaded");
        }

        $this->_direct_call = $direct_call;
    }

    /* Resize
     * @param int $width, @param int $height, @param define $mode
     * @param bool $auto_orientation houd rekening met orientatie wanneer er een resize gebeurt */
    public function Resize($width=100, $height=100, $mode=RESIZE_STRETCH, $auto_orientation=false){

        // Validate resize mode
        $valid_modes = array("f", "p");
        }
                     // .... omitted .....

        // Set news size vars because these are used for the
        // cache name generation
                 // .... omitted .....          
        $this->_old_width = $width;
        $this->_old_height = $height;

        // Lazy load for the directurl cache to work
        $this->lazyLoad();
        if($this->_cache_skip) return true;

        // Create canvas for the new image
        $new_image = imagecreatetruecolor($width, $height);

        imagecopyresampled($new_image, $this->_image_resource, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);

             // .... omitted .....

        $this->_image_resource = $new_image;
    }

    /* Create image resource from path or url
     * @access public, @param string $location, @param bool $lazy_load, @return */
    public function Load($image,$lazy_load=false){

        // Cleanup image url
        $image = $this->cleanUrl($image);

        // Check if it is a valid image
        if(isset($mimes[$extension]) && ((!strstr($image, "http://") && file_exists($image)) || strstr($image, "http://")) ){

            // Urlencode if http
            if(strstr($image, "http://")){
                $image = str_replace(array('http%3A%2F%2F', '%2F'), array('http://', '/'), urlencode($image));
            }
            $image = str_replace("+", "%20", $image);

            $this->_extension = $extension;
            $this->_mime = $mimes[$extension];
            $this->_image_path = $image;
            $parts = explode("/", $image);
            $this->_image_name = str_replace("." . $this->_extension, "", end($parts));

            // Get image size
            list($width, $height, $type) = getimagesize($image);
            $this->_old_width = $width;
            $this->_old_height = $height;
            $this->_image_type = $type;
        }else{
            $this->showError("Wrong image type or file does not exists.");
        }
        if(!$lazy_load){
            $resource = $this->GetImageResource($image, $extension);
            $this->_image_resource = $resource;
        }           
    }

    /* Save image to computer
     * @access public, @param string $destination, @return void  */
    public function Save($destination, $quality=60){
        if($this->_extension == "png" || $this->_extension == "gif"){
            imagesavealpha($this->_image_resource, true); 
        }
        switch ($this->_extension) {
            case "jpg": imagejpeg($this->_image_resource,$destination, $quality);   break;
            case "gif": imagegif($this->_image_resource,$destination);      break;
            default: $this->showError('Failed to save image!');             break;
        }           
    }

    /* Print image to screen
     * @access public, @return void */
    public function Parse($quality=60){
        $name = $this->generateCacheName();
        $content = "";
        if(!$this->_cache || ($this->_cache && $this->cacheExpired())){
            ob_start();
            header ("Content-type: " . $this->_mime);
            if($this->_extension == "png" || $this->_extension == "gif"){
                imagesavealpha($this->_image_resource, true); 
            }

            switch ($this->_extension) {
                case "jpg": imagejpeg($this->_image_resource, "", $quality);    break;
                case "gif": imagegif($this->_image_resource);   break;
                default: $this->showError('Failed to save image!');             break;
            }

            $content = ob_get_contents();
            ob_end_clean();
        }else{

            if (isset ($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
                if (strtotime ($_SERVER['HTTP_IF_MODIFIED_SINCE']) < strtotime('now')) {
                    header ('HTTP/1.1 304 Not Modified');
                    die ();
                }
            }

            // change the modified headers
            $gmdate_expires = gmdate ('D, d M Y H:i:s', strtotime ('now +10 days')) . ' GMT';
            $gmdate_modified = gmdate ('D, d M Y H:i:s') . ' GMT';

            header ("Content-type: " . $this->_mime);
            header ('Accept-Ranges: bytes');
            header ('Last-Modified: ' . $gmdate_modified);
            header ('Cache-Control: max-age=864000, must-revalidate');
            header ('Expires: ' . $gmdate_expires);

            echo $this->cachedImage($name);
            exit();
        }

        // Save image content
        if(!empty($content) && $this->_cache){
            $this->cacheImage($name, $content);
        }

        // Destroy image
        $this->Destroy();

        echo $content;
        exit();
    }

    /* Destroy resources
     * @access public,  @return void */
    public function Destroy(){
        imagedestroy($this->_image_resource); 
    }


    /* Get image resources
     * @access public,  @return resource */
    public function GetResource(){
        return $this->_image_resource;
    }

    /* Set image resources
     * @access public, @param resource $image, @return resource */
    public function SetResource($image){
        $this->_image_resource = $image;
    }

    /* Enable caching
     * @access public, @param string $folder, @param int $ttl,   * @return void */
    public function EnableCache($folder="/var/www/vhosts/blabla.org/httpdocs/tmp/", $ttl=345600){
        if(!is_dir($folder)){
            $this->showError("Directory '" . $folder . "' does'nt exist");
        }else{
            $this->_cache           = true;
            $this->_cache_folder    = $folder;
            $this->_cache_ttl       = $ttl;
        }
        return false;
    }
}

The original author granted me permission for placing parts of code in here for solving this issue.


hakre
  • 193,403
  • 52
  • 435
  • 836
Sam
  • 15,254
  • 25
  • 90
  • 145
  • Are those images running through a PHP front controller of some sort? – Pekka Mar 11 '11 at 20:27
  • This just seems like a repost of your previous question. – drewish Mar 11 '11 at 20:29
  • 1
    @drewish: the answer on the previous question doesn't really address *this* particular bit of it. – Matchu Mar 11 '11 at 20:32
  • The one he links to in the first paragraph: http://stackoverflow.com/questions/4810806/cached-php-generated-thumbnails-load-slow-how-to-find-problem-solution-from-wat – drewish Mar 11 '11 at 20:34
  • 1
    I don't see why can't he just post some code. Having us try to debug it via some—admittedly cool—screenshots is pretty crazy. I'm sure the code is doing something stupid like resizing the images on the fly instead of writing them out to files and just sending the cached files. – drewish Mar 11 '11 at 20:36
  • I pointed my bowser at http://asterdesign.com/en/aster and it doesn't seem that bad. I wonder which browser he's using to do the benchmarking. – drewish Mar 11 '11 at 20:40
  • 1
    @drewish: I got .5s-1s load times on the images in question, almost 100% of which was spent on the server. Not awful, but much more than necessary. – Matchu Mar 11 '11 at 20:42
  • @Matshu, Indeed can you believe it? I've posted the most optimistic, fastest results I could get! Normally the load much slower than the graph, as you pointed out. – Sam Mar 11 '11 at 20:57
  • @drewish, give the author a bit time sothat he can react wether its okay or not, I really hope he agrees since HE WILL BENEFIT TOO, and I can put some code here ( which I don't understand myself anyways). OR: are you saying that when I pay for code to implement on my site I automatically have some more rights than I know? Then again it seems only I am having this problem since he did not tell me that other had same problem. – Sam Mar 11 '11 at 21:01
  • @Pekka, what do you mean by `PHP front controller`? do you mean a frame work like a CMS/cake/drupal/wordpress/ajax/joomla? Nope! I have heard of these words but none of them are on my site. I just bought various scripts/custom made for me and implementen on my site. I have a pretty oldfashion so called `FLATFILE` site. Just like flat tires but a bit more modern and evolved... – Sam Mar 11 '11 at 21:06
  • @Sam so those requests are genuine requests to static JPG files? There is no mod_rewrite or other magic at play? I am not seeing any thumbnail generator working in your image, can you point out where that happens? – Pekka Mar 11 '11 at 21:09
  • @Pekka, glad you asked. There is a mod_rewrite (though im beginner, I found the tremmendous power of apache irresistable) and so renamed (below water) all `imgcpu.php?src=foo/foo.jpg&w=100&h=100` to `IMG-foo/foo_w100_h100.jpg` making it easier for me + making the statistics aware of the correct filetype. A month ago I already hesitated this could be due to the mod rewrite and so renamed all files on the home page back to their old `imgcpu.php?src=` without them needing anything from htaccess, and... EXACT same delays/behaviour. Curious of your reply @Pekka, thanks in advance for your vision. – Sam Mar 12 '11 at 00:00

5 Answers5

5

If I'm understanding the question correctly, this is entirely to be expected. Image manipulation is slow.

The yellow is your browser sending the request. The green is your browser waiting on the server to actually create the thumbnail, which takes a very significant amount of time, no matter what library the server is using. The blue is the server sending the response, which, unlike the previous steps, is affected by filesize.

There's not much to be done about the inherent slowness of image manipulation. It would be wise to cache these thumbnails so that they are only generated once and are then served statically. That way, very few of your users will ever have to sit through that green delay, and your server will be happy, too.

EDIT: If the issue is that the files exist at those URLs, but your RewriteRule is kicking in anyway, bear in mind that, by default, rules run without checking if the file exists.

Use the following condition above your RewriteRule to make sure the file exists.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule # ...etc...
Matchu
  • 83,922
  • 18
  • 153
  • 160
  • +1. @Matchu, Sorry I forgot to mention: the thumbs ARE CACHED in their separate directory and when I check in the ftp they are generated there... so that seems to work... Does this information help you for the answer? Let me guesse: you want to see the source code... Will try to get permission to upload it here, temporarily. – Sam Mar 11 '11 at 21:04
  • 1
    But there is no thumbnail generation at play here, is there? Those are static images, are they not? – Pekka Mar 11 '11 at 21:08
  • 1
    @Pekka: title contains "PHP thumbnail image generator," so that's where I'd put my money for where that load time is coming from. @Sam, if you're not able to get clearance to post the source, try some standard debugging techniques: put some `echo` statements here and there to see where the code ends up going and why. It's clearly not hitting the cached copy. – Matchu Mar 12 '11 at 01:17
  • 1
    @Sam: how does the script work? Are the URLs given where the images are supposed to live? Bear in mind that `mod_rewrite` will, by default, kick in without checking if the file exists. Use `RewriteCond %{REQUEST_FILENAME} !-f` before the `RewriteRule` to make the rule only apply if the file doesn't exist. – Matchu Mar 12 '11 at 01:20
  • I added that rewrite rule but nothing helps I now have two conditions before the rewrite rule: `RewriteCond %{REQUEST_FILENAME} !-f` then `RewriteCond %{REQUEST_URI} ^IMG.*$` once again @Matchu: after numerous tests where I deleted the entire rewrite rule and just had the default imgcpu?src=bla.jpg$ etc in place, and had the EXACT same delays! so i can guarantee mod rewrite is adding no (measurable) delays to this problem... Ps see my updates question with more new stuff. What do you reccon mate? thank you's in advance! – Sam Mar 14 '11 at 18:31
  • 1
    @Sam: well, we can be reasonably certain that PHP is serving up that file, since that's the only thing that would really cause that issue… the request is *somehow* getting redirected to PHP. (Unless your server specifically has it disabled, you'll probably see an `X-Powered-By` header from that image request.) The question, then, is why PHP is getting that request, even if the rewrite rule is off. Sometimes PHP files like `images.php` also handle requests to `images` or `images/anything`. Is that possibly the issue here? – Matchu Mar 15 '11 at 01:01
  • @Matchu, thanks for your comments, please see an entirely rewritten question. Thanks to your code I have good and bad news: Good news: APACHE IS AWESOME Bad news: your suggestion as well as other improvements make the apache rewritten url NOT SLOWER than the original url, so, the problem lies in the PHP headers not being sent correctly to fetch the old already saved cache jpg but generate and creates a fresh new JPG on every request... THATS THE DELAY what we see in the time to first byte – Sam Mar 26 '11 at 18:23
3

imgcpu.php?src=foo/foo.jpg&w=100&h=100

so imgcpu.php is running for every image request?

In that case, if you are worried about performance,:

  • the script needs to do some caching of the thumbnails it creates. If it resizes stuff on every request, that's your problem right there.

  • the script needs to send some caching headers to the browser - a pure PHP script won't do that, and will be refreshed on every page load

  • a session_start() call inside the PHP script could lead to concurrency issues because of session locking.

you will need to show some PHP code. Maybe in a separate question, though.

Pekka
  • 442,112
  • 142
  • 972
  • 1,088
  • +1 Dear @Pekka I updated my scores: 97 Page Speed and 93 YSlow, incredibly high to my knowledge. http://gtmetrix.com/reports/asterdesign.com/f4NK8SwG So as you can see all except the thumbnails are perfect. I have the feeling it indeed resizes upon every request. Though thumbnails *are* saved in the cache folder I checked there are as many jpgs there with scrambled-eggs-file-names like 8237928379 The code author hasnt replied yet, hope soon... to be continued. – Sam Mar 14 '11 at 06:23
  • @Pekka, I finally got permission to put parts of the code online! Yppie! What do you recommend:, is there a place like http://jsfiddle.net but for PHP code sothat I can paste it there and give a link for testing? Or should I put them just in my question? thanks – Sam Mar 14 '11 at 15:24
  • @Sam putting it in a fiddle may be the best idea, the question is so crowded already :) – Pekka Mar 14 '11 at 15:41
  • @Pekka, whats a fiddle and how do I put it in a fiddle? link? thank you. – Sam Mar 14 '11 at 15:43
  • @Sam I mean http://jsfiddle.net. There is also http://ideone.com which can run *some* PHP code, but it's unlikely to work with a complex resizing script. – Pekka Mar 14 '11 at 15:44
  • @Pekka got some problems with fiddle so finally decided to paste portions of the code just in here: StackOverflow has neater ways to indent to my taste. See updated lower part of question. – Sam Mar 14 '11 at 18:27
  • @Pekka, see an entirely rewritten question: your bulleted lists are spot one, The problem is, now I know more, the SECOND bullet: the script dos NOT seem to send the correct caching headers, although the HEADERS seem elaborately built even I see a line with content not changed `304` header in the code... What do you recommend me doing ( after the rewritten question) – Sam Mar 26 '11 at 18:21
  • @Sam mmm, that's a lot of code... Hard to tell why the correct code block is not reached. Doing some test outputs to find out which way the script goes (or using a debugger) may be the best way to go. – Pekka Mar 26 '11 at 23:11
3

Apache can serve up files from your hard disk a lot faster than PHP can, and it appears that you're doing the latter to handle caching:

 /**
     * Get an image from cache
     * 
     * @access public
     * @param string $name
     * @return void
     */
    private function cachedImage($name){
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    }

There's a better way of doing what that function is doing (passthru), but the best option is to setup a regex that'll only rewrite a request to your thumbnailing script if the file doesn't already exist:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^images/.*$ - [NC,L]
RewriteRule ^images/(.*)$ /imgcpu.php/$1 [NC,L]

And then introduce logic to parse the request to an image and format it accordingly. For example, you could say thumbs should be named after the original file and have the W x H dimensions appended like "stackoverflow_logo_100x100.jpg".

Make sense?


Per request (in the comment), the description of the "s", "l", and "d" flags is as follows (quoting the docs):

'-d' (is directory) Treats the TestString as a pathname and tests whether or not it exists, and is a directory.

'-s' (is regular file, with size) Treats the TestString as a pathname and tests whether or not it exists, and is a regular file with size greater than zero.

'-l' (is symbolic link) Treats the TestString as a pathname and tests whether or not it exists, and is a symbolic link.

Community
  • 1
  • 1
coreyward
  • 77,547
  • 20
  • 137
  • 166
  • +1 and Thanks @coreyward, could you please comment on what the `-s` `-l` `-d` do each? (ps I have completely rewritten my question as I have more information now) Thanks! – Sam Mar 26 '11 at 18:25
  • @Sam I've added a description of the flags in my answer. – coreyward Mar 26 '11 at 18:48
  • +1, this is the best way to go as it saves you from starting a PHP process on every request. – Pekka Mar 27 '11 at 11:24
2

Your checking your HTTP_IF_MODIFIED_SINCE header and cache AFTER you generate the image so the image is getting generated and cached every time you load the page. You would get a considerable decrease in time if you move these checks closer to the start of execution, before you start processing the image.

James
  • 2,609
  • 3
  • 20
  • 27
0

Matchu gave you answer why. If you want to fix it, save the created thumbnails, so they are not recreated on each request. I use simple 404 page that catches request to thumbnails that haven't been created, that script figures out the required dimensions and file from url - eg /thumbs/100x100/cat.png means create 100x100 thumbnail from /images/cat.png.

Marek
  • 7,337
  • 1
  • 22
  • 33