Optimising GraphicsMagick On A CentOS 5.9 Server

Teapot

Well-known member
I've got an odd problem at the moment regarding a heavy image generator application I've developed, which essentially composites up to 15 small images into a PNG background. The application in question is here. The simple problem is that it isn't scaling on the server, and quite frequently causes timeouts during peak times. The thing that I'm curious about is that I cannot produce the same sort of load on my (significantly less powerful) local machine, even when generating hundreds of images in sequence.

So I have two main questions:

Firstly, is there anything I can do to optimise the code that generates the cards? I'll post the code below and explain some of the optimisations I have already made:

PHP:
    public function generate($input)
    {
        $backgroundModel = $this->_getBackgroundModel();
        $characterModel = $this->_getCharacterModel();
        $badgeModel = $this->_getBadgeModel();
        $pokemonModel = $this->_getPokemonModel();

        $background = $backgroundModel->getBackgroundById($input['background']);
        $background = $backgroundModel->prepareBackground($background);

        $character = $characterModel->getCharacterById($input['character']);

        if ($input['badgesUsed']) {
            $badgeIds = explode(',', $input['badgesUsed']);
            $badges = array();
            foreach ($badgeIds as $badge => $badgeId) {
                array_push($badges, $badgeModel->getBadgeToRenderById($badgeId));
            }
        }

        if ($input['pokemonUsed']) {
            $pokemonIds = explode(',', $input['pokemonUsed']);
            $pokemon = array();
            foreach ($pokemonIds as $poke => $pokemonId) {
                array_push($pokemon, $pokemonModel->getPanelToRenderById($pokemonId));
            }
        }

        $images = array(
                'background' => XenForo_Helper_File::getExternalDataPath() . "/trainercardmaker/" .  $background['background_url'],
                'trainerSprite' => XenForo_Helper_File::getExternalDataPath() . "/trainercardmaker/" .  $character['character_url'],
                'pkmn' => XenForo_Helper_File::getExternalDataPath() . "/trainercardmaker/pokemon/000.png"
        );

        $trainerCard = new Gmagick($images['background']);
        $trainerSprite = new Gmagick($images['trainerSprite']);
        $pkmn = new Gmagick($images['pkmn']); // blank Pokémon sprite

        // If you somehow manage to enter more than 12 characters in the trainer name box, cut it down.
        $input['trainername'] = mb_substr($input['trainername'],0,12);

        // Do the same for the friend code.
        $input['fc1'] = substr($input['fc1'],0,4);
        $input['fc2'] = substr($input['fc2'],0,4);
        $input['fc3'] = substr($input['fc3'],0,4);

        $font = XenForo_Helper_File::getExternalDataPath()
            . "/trainercardmaker/EnvyCodeRBold.ttf";

        $friendCodeFont = new GmagickDraw();
        $friendCodeFont->setFont($font);
        $friendCodeFont->setFontSize(12);
        $friendCodeFont->setFillColor($background['background_params']['friendcodeColor']);

        $trainerNameFont = new GmagickDraw();
        $trainerNameFont->setFont($font);
        $trainerNameFont->setFontSize(15);
        $trainerNameFont->setFillColor($background['background_params']['trainernameColor']);

        /* Stick the trainer sprite onto the background */
        $trainerCard->compositeImage($trainerSprite, Gmagick::COMPOSITE_OVER, $background['background_params']['trainerX'], $background['background_params']['trainerY']);

        /* Stick the Pokémon sprites onto the background */
        $pokemonIndex = 1;
        if (isset($pokemon)) {
            foreach ($pokemon as $key => $value) {
                $value['filename'] = XenForo_Helper_File::getExternalDataPath() . "/trainercardmaker/pokemon/" .  $value['filename'];
                $value['pokemon_object'] = new Gmagick($value['filename']);
                $trainerCard->compositeImage($value['pokemon_object'], Gmagick::COMPOSITE_OVER, $background['background_params']['pokemon' . $pokemonIndex . 'X'], $background['background_params']['pokemon' . $pokemonIndex . 'Y']);
                $pokemonIndex++;
            }
        }

        /* Stick blank Pokémon sprites onto the background for any Pokémon not defined */
        for ($i=$pokemonIndex; $i < 7; $i++) {
            $trainerCard->compositeImage($pkmn, Gmagick::COMPOSITE_OVER, $background['background_params']['pokemon' . $i . 'X'], $background['background_params']['pokemon' . $i . 'Y']);
        }

        /* Stick the badge sprites onto the background */
        $badgeIndex = 1;
        if (isset($badges)) {
            foreach ($badges as $key => $value) {
                $value['badge_url_small'] = XenForo_Helper_File::getExternalDataPath() . "/trainercardmaker/" .  $value['badge_url_small'];
                $value['badge_object'] = new Gmagick($value['badge_url_small']);
                $trainerCard->compositeImage($value['badge_object'], Gmagick::COMPOSITE_OVER, $background['background_params']['badge' . $badgeIndex . 'X'], $background['background_params']['badge' . $badgeIndex . 'Y']);
                $badgeIndex++;
            }
        }

        $friendCodeOutput = $input['fc1'] . (empty($input['fc2']) ? '' : '-') . $input['fc2'] . (empty($input['fc3']) ? '' : '-') . $input['fc3'];

        /* Print out the Friend Code text */
        if ($friendCodeOutput) {
            $trainerCard->annotateImage(
                $friendCodeFont,
                $background['background_params']['friendcodeX'], // x
                $background['background_params']['friendcodeY'], // y
                0, // angle
                $friendCodeOutput
            );
        }

        /* Print out the trainer name text */
        if ($input['trainername']) {
            $trainerCard->annotateImage(
                $trainerNameFont,
                $background['background_params']['trainernameX'],
                $background['background_params']['trainernameY'],
                0,
                $input['trainername']
                ); 
        }

        $trainerCard->setImageFormat("png");
        return $trainerCard;
    }
Firstly, all the asset database entries are cached in Memcache - the URLs for the background, character, up to 6 Pokémon panels, and up to 8 small 32x32 badges are stored in memory, so shouldn't need database queries to fetch. Unfortunately, I don't know how to optimise the image generation side - up to 15 files are being loaded from a standard hard disk drive, and each are being individually composited onto the image. At the same time, randomly generating 400-500 cards in a row each only take 0.01 seconds each, according to a very unscientific test I did, and not nearly as much CPU activity as I'm seeing on the production server.

Which leads me to my second question: Is there anything I can change in the server to make it more performant in this situation?

The production server is a reasonably powerful server for our purposes (a decent 8-core Xeon, 8GB RAM), which currently exclusively runs the XenForo forum, running the latest cPanel on top of CentOS 5.9, with Apache 2.4, PHP 5.4, APC, and Memcache. Because we use cPanel, PHP uses the fcgi handler rather than php-fpm, and thus each process spawned unfortunately has its own, small APC cache. We're also using a standard MySQL 5.5 without any manual optimisations to speak of. Without the Trainer Card Maker application running, XenForo is handled easily with all the relevant performance options (minify CSS, cache BBCode output, news feeds, store templates as files, etcetera) enabled.

Thanks for reading :)
 
If it's just your site on the server, switch the PHP handler to DSO. I had a nightmare trying to run APC and fcgi, as each user could end up on any random APC instance.
 
If it's just your site on the server, switch the PHP handler to DSO. I had a nightmare trying to run APC and fcgi, as each user could end up on any random APC instance.
Unfortunately, we need to be able to add more users as needed, so FCGI is (as far as I can tell) the best option that works for us.

Thanks anyway, however. :)
 
Top Bottom