Optimizing CakePHP 2.x Apps

52
Optimizing CakePHP Optimizing CakePHP 2.x Apps 2.x Apps CakeFest 2015 - 10 Years! Yay!

Transcript of Optimizing CakePHP 2.x Apps

Optimizing CakePHPOptimizing CakePHP2.x Apps2.x Apps

CakeFest 2015 - 10 Years! Yay!

Juan BassoJuan BassoDirector of ArchitectureCakePHP Core DeveloperWork for Zumba Fitness (yeah, that dancing stuff!)Obsessed by performance/optimization

@jrbasso

Which one is faster?Which one is faster?

<?php

for ($i = 0; $i < 1000; $i++) { // Do something}

<?php

$i = 1000;while ($i--) { // Do something}

Over 10M pageviews per month180+ countries, 15M people doing classes, 200K+ classesaround the worldTop 10k websites in US, top 25k worldwide (Alexa)Lots of custom and crazy rulesEcommerce, trainings, membership, certifications,conventions, classes and instructors searches,music/video streaming, e-learning, affiliate program, etc.Worldwide, so translations (8 languages)Backed with CakePHP 2.5

Also with MySQL, MongoDB, Memcached, Redis,ElasticSearch, Node.js, Varnish, AWS Lambda, etc.

Hosted in AWS

Why to optimize?Why to optimize?Increase visitor retention/engagement and loyalty [1]

Better ranking on Google Search (SEO)Reduce the response timeImprove page load timeMake the customer happier (better UX)Reduce network throughput in some types ofoptimization

Save customer money on bandwidth (mobilenetwork)

Helps the environment saving energy

COST!!!

[1] http://bit.ly/1ejSNig

Costs?Costs?Reduce resource usage (CPU/Memory/DiskIO)Reduce network throughputReduce requests queueingReduce number or size of instancesIncrease number of concurrent requests per instance

Consequences - SearchesConsequences - SearchesBing – A page that was 2 seconds slower resulted in a4.3% drop in revenue/user.

Google – A 400 millisecond delay caused a 0.59% dropin searches/user.

Yahoo! – A 400 milliseconds slowdown resulted in a 5-9% drop in full-page traffic.

Source: Velocity 2009 - http://bit.ly/1QZaBFP

Consequences - CompaniesConsequences - CompaniesShopzilla – Speeding up their site by 5 seconds increasedthe conversion rate 7-12%, doubled the number ofsessions from search engine marketing, and cut thenumber of required servers in half.

Mozilla – Shaving 2.2 seconds off their landing pagesincreased download conversions by 15.4%, which theyestimate will result in 60 million more Firefoxdownloads per year.

Netflix – Adopting a single optimization, gzipcompression, resulted in a 13-25% speedup and cut theiroutbound network traffic by 50%.

Source: Velocity 2009 - http://bit.ly/1QZaBFP

Study - 2014Study - 2014Slow web pages correlate to more than $3 billion in lostecommerce sales (US) every year

44% of online shoppers fear that slow checkout pagesmeans something went wrong with the transaction

A 2-second delay during a transaction results in ashopping cart abandonment rates of up to 87%

Source: http://bit.ly/1d2MbeT

Types of OptimizationsTypes of OptimizationsSearch Engine Optimization (SEO)Content OptimizationUX OptimizationPerformance Optimization (this talk)Mobile OptimizationEtc.

Performance OptimizationPerformance OptimizationBrowser level (CSS, Markup, JS)Environment level (CPU, Memory, PHP/Apache/Kernelconfigs)Application level (Algorithms, Loading Strategy, Caching,Coding Language, Frameworks)

Browser OptimizationsBrowser OptimizationsOne of the most important for the end user

Browser combine all final requestsLoad all JS/CSS, parse, renderGoogle 332ms BE, 778ms PL, 12 requests = 42.6% BEAmazon 1.28s BE, 6.5s PL, 289 requests = 19.7% BENike 157ms BE, 4.3s PL, 147 requests = 3.6% BE

Compile Assets

Minify (reduce size)Combine (reduce network requests)

CDNSplit requests in different hosts (parallel requests)Reduce quantity and size of cookiesAsync/Defer JS or JS at the bottom

Browser OptimizationsBrowser OptimizationsImage optimization

Loseless or Lossy optimizationFormat (jpg, png, webp)

HTTPS with HTTP 2.0/SPDYHTTP Cache headersVarnish

Environment OptimizationsEnvironment OptimizationsTune PHP configurations

Opcache sizeDisable profiling tools in production

Tune Apache configurations

Disable unnecessary modules in apacheBe careful with number of concurrent requestsDisable AllowOverride (.htaccess)

Kernel configurationsReduce I/O operationsHHVMFind where is the hardware bottleneck - test & monitor

Application OptimizationsApplication OptimizationsDon't give high priority for micro-optimizations, exceptfor big loops or core librariesLazy load things, but don't be too lazyLazy load sessionsReduce your bootstrapping (overhead)Fix PHP notices/warnings even with debug offAvoid try/catch inside loopsReduce I/O operationsPrefer memory over I/OUse AJAX for secondary content if that is slow

CakePHPCakePHPOptimizationsOptimizations

Finally I got here

CakePHP OptimizationsCakePHP OptimizationsInstall DebugKitRequest your page with debug level 2Analyze DebugKit resultsFind and fix the bottlenecks

Questions?Questions?

OpcacheOpcacheAPC for PHP 5.4

Check stat configuration to disable disk check

Built in opcache in PHP 5.5+Check the number of files and used memory

BootstrappingBootstrappingKeep it leanRemember that everything that you put on the bootstrapare executed on every request

Caching StrategyCaching StrategyAPCFileMemcache[d]RedisWincacheXcache

Caching Strategy - APCCaching Strategy - APCLocal (no network)Consume server memoryCache can get out of sync between serversHard to invalidate and keep in syncIdeal for content that doesn't change often (ie,_cake_core_ and _cake_model_ caches)

Caching Strategy - FileCaching Strategy - FileLocal (no network - unless using NFS)Use disk I/O - Slower than memory or networksometimesCache can get out of sync between servers (unless usingshared NFS)Ideal for local development

Caching Strategy - MemcacheCaching Strategy - MemcacheRequires networkCan be installed on the same serverCan retrieve multiple caches in parallel (ideal for loadingsubsets)Multiple servers can use the same memcacheCache is centralized, so no out of syncIdeal for caches of short periods of time (ie, inventory,etc)

Cache is easy to use!Cache is easy to use!class User extends AppModel {

public function getSomeList() { $cache = Cache::read('User.SomeList'); if ($cache) { return $cache; }

$data = $this->find(...); Cache::write('User.SomeList', $data); return $data }

}

Not easy? What about this?Not easy? What about this?class User extends AppModel {

public function getSomeList() { return Cache::remember('User.SomeList', function() { return $this->find(...); }); }

}

Thanks Mark Story!

Model in-memory CacheModel in-memory CacheJust valid during the current requestCan use a lot of memory if enabled for everything

class User extends AppModel { public $cacheQueries = true;

public function getSomeList() { return $this->find(...); }

}

Element CacheElement Cache// In your bootstrap/coreConfigure::write('weather', array( // ... 'duration' => '+5 minutes'));

// In your ctpecho $this->element('weather', array(), array( 'cache' => array( 'config' => 'weather' )));

// Elements/weather.ctp<?php// assuming Weather is a helper that use an external servicefor ($this->Weather->temperature('New York, NY, USA') as $temp) { // do wherever you have to do}

View CacheView Cache

// In your bootstrap/coreConfigure::write('Cache.check', true);

Configure::write('Dispatcher.filters', array( // ... 'CacheDispatcher'));

// Your controllerclass Users extends AppController { public $helpers = array('Cache'); public $cacheAction = array( 'wherever' => '1 hour' );

public function wherever() { $this->set('users', $this->User->getSomeList()); }

}

Removed in Cake 3.0Use Varnish or similar instead

View Cache (if using)View Cache (if using)

// In your AppModel.phpclass AppModel extends Model {

protected function _clearCache($type = null) { return true; }

}

Don't care about cache invalidation? Youcan save some inflections and I/O

Routing - OrderingRouting - OrderingCakePHP 2.x search the route sequentiallySort by most visited or most generated

// Home is more accessed and generated, so goes firstRouter::connect('/', array('controller' => 'StaticPages', 'action' => 'home'));

// About just few requests, goes on the bottomRouter::connect('/about', array('controller' => 'StaticPages', 'action' => 'about'));

// Cake default routesrequire CAKE . 'Config' . DS . 'routes.php';

Routing - Custom RoutesRouting - Custom RoutesMore routes, more processingCombine routes with regex

// BeforeRouter::connect('/about', array('controller' => 'StaticPages', 'action' => 'about'));Router::connect('/contactus', array('controller' => 'StaticPages', 'action' => 'contactus'));Router::connect('/docs', array('controller' => 'StaticPages', 'action' => 'docs'));Router::connect('/press', array('controller' => 'StaticPages', 'action' => 'press'));

// After$staticRoutes = implode('|', array('about', 'contactus', 'docs', 'press'));Router::connect( '/:action', array('controller' => 'StaticPages'), array('action' => $staticRoutes));

Routing - RedirectsRouting - RedirectsIf possible, use apache/nginx for redirectsAvoid load PHP + CakePHP for a simple task

Routing - Route ParsingRouting - Route ParsingCake converts Router::connect() into CakeRoute objectsCakeRoute lazy parse the route when need to matchParsing takes some time and can be significant with lotsof routesPossible solution: cache routes parsing!

Routing - Route ParsingRouting - Route Parsing// routes.php

$routesFile = __DIR__ . '/routes.connect.php';$routesHash = sha1_file($routesFile); $cacheFile = TMP . '/routes-' . $routesHash . '.php';if (file_exists($cacheFile)) { App::uses('PluginShortRoute', 'Routing/Route'); include $cacheFile;} else { include $routesFile; // Prepare for cache foreach (Router::$routes as $route) { $route->compile(); } $tmpCacheFile = TMP . '/routes-' . uniqid('tmp-', true) . '.php'; file_put_contents($tmpCacheFile, '<?php Router::$initialized = true; Router::$routes = ' . var_export(Router::$routes, true) . '; '); rename($tmpCacheFile, $cacheFile);} Router::connectNamed(true);

Thanks for Jose Gonzalez with the atomic file write

Routing - Route ParsingRouting - Route ParsingBootstrapping can be slower if using some custom routeLimitations:

Don't support conditional routesDon't support dynamic routesMay not be compatible with some pluginsBefore Cake 2.6 need to change the default route toimplement __set_static

Adapt to your applicationMore details on dereuromark/cakephp-shim

Routing - URL GeneratorRouting - URL GeneratorSlowCan use Matt Curry / Mark Scherer's plugin forcaching ( )3.0 has url alias which generate urls faster (thanks Mark Story!)

dereuromark/cakephp-url-cache

Lazy Load SessionsLazy Load SessionsSince Cake 2.5 the sessions are lazy loaded (Thanks ADmad!)

Make sure you don't write some unnecessary data tosession on every requestCake won't load PHP session if trying to read andsession cookie is not set

Use Controller $usesUse Controller $usesModels on $uses are lazy loadedClassRegistry::init() is more expensive than lazy loadingit if called more than once

RequestHandler Can BeRequestHandler Can BeDangerousDangerous

Without RequestHandler With RequestHandler

Use Caching HeadersUse Caching HeadersYou can set the last modified header based on your data

public function view($id = null) { $post = $this->Post->findById($id); if (!$post) { throw NotFoundException(); } $this->response->modified($post['Post']['modified']); if ($this->response->checkNotModified($this->request)) { // Don't call the view, just return an empty response with 304 header return $this->response; } // Render normally (the Last-Modified header will be sent) $this->set('post', $post);}

Use Caching HeadersUse Caching HeadersMaybe not even hitting your database

public function view($id = null) { $lastModified = Cache::read('Post.LM.' . $id); if ($lastModified) { $this->response->modified($lastModified); if ($this->response->checkNotModified($this->request)) { // Don't call the view, just return an empty response with 304 header return $this->response; } }

$post = $this->Post->findById($id); if (!$post) { throw NotFoundException(); } $this->response->modified($post['Post']['modified']); Cache::write('Post.LM.' . $id, $post['Post']['modified']); $this->set('post', $post);}

Use Caching HeadersUse Caching HeadersOr maybe not even receiving the request again

public function view($id = null) { $post = $this->Post->findById($id); if (!$post) { throw NotFoundException(); }

// Set Cache-Control to "public, max-age=3600" // that means the browser can store it for 1h // without request the server again $this->response->sharable(true, 3600); $this->set('post', $post);}

Model Recursive andModel Recursive andContainableContainable

Recursive can be villain while containable can be theheroRecursive will load all the associations, even if you don'tneed (not lazy loaded)Containable explicit define what associations to returnZumba's database crashed before because bad recursiveusage

Model Recursive andModel Recursive andContainableContainable

// Getting a list of 10 users$this->User->find('all', array('limit' => 10));

/*array( array( 'User' => array( 'id' => 1, 'name' => 'Dude 1' ), 'Role' => array( 'id' => 1, 'name' => 'Admin', 'User' => array( // A list of over a 1000 users ) ) ), // Other 9 users with a list of 1000 users on role)*/

Avoid Looping Small ElementsAvoid Looping Small ElementsElements are good, but in loops the overhead start to besignificantMove the loop to the element, if applicableUse element as template (some string replace)

Micro OptimizationsMicro OptimizationsDon't spend time on it unless working in a code usedfrequently (like a core library or framework) or inside abig loopWait, why do you have a big loop in the first case?!Xcode profiler should give good tips on where youshould focus

Compile Your AssetsCompile Your AssetsUse Mark Story's asset compress plugin( )Use gulp / gruntmarkstory/asset_compress

Some Untested IdeasSome Untested Ideas when you don't need reverse routing

Use Cake 3 ORM instead of Cake 2 models :)Fast route

Testing Your AppTesting Your AppDisable Cake debug modeDisable XDebugTools: siege, ab, JMeter, *cachegrindDon't compare the results from different benchmarkapps, they give different numbersKeep an eye on the 99% tooDifferent servers can behave differentlySometimes the best algorithm is not the best for yourapp

Response TimeResponse Time

Questions?Questions?Thank You