[ Index ] |
|
Code source de Symfony 1.0.0 |
1 Chapter 18 - Performance 2 ======================== 3 4 If you expect your website will attract a crowd, performance and optimization issues should be a major factor during the development phase. Rest assured, performance has always been a chief concern among the core symfony developers. 5 6 While the advantages gained by accelerating the development process result in some overhead, the core symfony developers have always been cognizant of performance requirements. Accordingly, every class and every method have been closely inspected and optimized to be as fast as possible. The basic overhead, which you can measure by comparing the time to display a "hello, world" message with and without symfony, is minimal. As a result, the framework is scalable and reacts well to stress tests. And as the ultimate proof, some websites with extremely high traffic (that is, websites with millions of active subscribers and a lot of server-pressuring Ajax interactions) use symfony and are very satisfied with its performance. Check the list of websites developed with symfony in the wiki ([http://www.symfony-project.com/trac/wiki/ApplicationsDevelopedWithSymfony](http://www.symfony-project.com/trac/wiki/ApplicationsDevelopedWithSymfony)) for names. 7 8 But, of course, high-traffic websites often have the means to expand the server farm and upgrade hardware as they see fit. If you don't have the resources to do this, or if you want to be sure the full power of the framework is always at your disposal, there are a few tweaks that you can use to further speed up your symfony application. This chapter lists some of the recommended performance optimizations at all levels of the framework and they are mostly for advanced users. Some of them were already mentioned throughout the previous chapters, but you will find it useful to have them all in one place. 9 10 Tweaking the Server 11 ------------------- 12 13 A well-optimized application should rely on a well-optimized server. You should know the basics of server performance to make sure there is no bottleneck outside symfony. Here are a few things to check to make sure that your server isn't unnecessarily slow. 14 15 Having `magic_quotes_gpc` turned `on` in the `php.ini` slows down an application, because it tells PHP to escape all quotes in request parameters, but symfony will systematically unescape them afterwards, and the only consequence will be a loss of time--and quotes-escaping problems on some platforms. Therefore, turn this setting off if you have access to the PHP configuration. 16 17 The more recent PHP release you use, the better. PHP 5.2 is faster than PHP 5.1, and PHP 5.1 is a lot faster than PHP 5.0. So make sure you upgrade your PHP version to benefit from the latest performance improvements. 18 19 The use of a PHP accelerator (such as APC, XCache, or eAccelerator) is almost compulsory for a production server, because it can make PHP run an average 50% faster, with no tradeoff. Make sure you install one of the accelerator extensions to feel the real speed of PHP. 20 21 On the other hand, make sure you deactivate any debug utility, such as the Xdebug or APD extension, in your production server. 22 23 >**NOTE** 24 >You might be wondering about the overhead caused by the `mod_rewrite` extension: it is negligible. Of course, loading an image with rewriting rules is slower than loading an image without, but the slowdown is orders of magnitude below the execution of any PHP statement. 25 26 Some symfony developers like to use `syck`, which is a YAML parser packaged as a PHP extension, as an alternative to the symfony internal parser. It is faster, but symfony's caching system already minimizes the overhead of YAML parsing, so the benefit is nonexistent in a production environment. You should also be aware that `syck` isn't completely mature yet, and that it may cause errors when you use it. However, if you are interested, install the extension ([http://whytheluckystiff.net/syck/](http://whytheluckystiff.net/syck/)), and symfony will use it automatically. 27 28 >**TIP** 29 >When one server is not enough, you can still add another and use load balancing. As long as the `uploads/` directory is shared and you use database storage for sessions, a symfony project will react seamlessly in a load-balanced architecture. 30 31 Tweaking the Model 32 ------------------ 33 34 In symfony, the model layer has the reputation of being the slowest part. If benchmarks show that you have to optimize this layer, here are a few possible improvements. 35 36 ### Optimizing Propel Integration 37 38 Initializing the model layer (the core Propel classes) takes some time, because of the need to load a few classes and construct various objects. However, because of the way symfony integrates Propel, these initialization tasks occur only when an action actually needs the model--and as late as possible. The Propel classes will be initialized only when an object of your generated model is autoloaded. This means pages that don't use the model are not penalized by the model layer. 39 40 If your entire application doesn't require the use of the model layer, you can also save the initialization of the `sfDatabaseManager` by switching the whole layer off in your `settings.yml`: 41 42 all: 43 .settings: 44 use_database: off 45 46 The generated model classes (in `lib/model/om/`) are already optimized--they don't contain comments, and they benefit from the autoloading system. Relying on autoloading instead of manually including files means that classes are loaded only if it is really necessary. So in case one model class is not needed, having classes autoloaded will save execution time, while the alternative method of using `include` statements won't. As for the comments, they document the use of the generated methods but lengthen the model files--resulting in a minor overhead on slow disks. As the generated method names are pretty explicit, the comments are turned off by default. 47 48 These two enhancements are symfony-specific, but you can revert to the Propel defaults by changing two settings in your `propel.ini` file, as follows: 49 50 propel.builder.addIncludes = true # Add include statements in generated classes 51 # Instead of relying on the autoloading system 52 propel.builder.addComments = true # Add comments to generated classes 53 54 ### Limiting the Number of Objects to Hydrate 55 56 When you use a method of a peer class to retrieve objects, your query goes through the hydrating process (creating and populating objects based on the rows of the result of the query). For instance, to retrieve all the rows of the `article` table with Propel, you usually do the following: 57 58 [php] 59 $articles = ArticlePeer::doSelect(new Criteria()); 60 61 The resulting `$articles` variable is an array of objects of class `Article`. Each object has to be created and initialized, which takes time. This has one major consequence: Contrary to direct database queries, the speed of a Propel query is directly proportional to the number of results it returns. This means your model methods should be optimized to return only a given number of results. When you don't need all the results returned by a `Criteria`, you should limit it with the `setLimit()` and `setOffset()` methods. For instance, if you need only the rows 10 to 20 of a particular query, refine the `Criteria` as in Listing 18-1. 62 63 Listing 18-1 - Limiting the Number of Results Returned by a Criteria 64 65 [php] 66 $c = new Criteria(); 67 $c->setOffset(10); // Offset of the first record returned 68 $c->setLimit(10); // Number of records returned 69 $articles = ArticlePeer::doSelect($c); 70 71 This can be automated by the use of a pager. The `sfPropelPager` object automatically handles the offset and the limit of a Propel query to hydrate only the objects required for a given page. Refer to the API documentation for more information on this class. 72 73 ### Minimizing the Number of Queries with Joins 74 75 During application development, you should keep an eye on the number of database queries issued by each request. The web debug toolbar shows the number of queries for each page, and clicking the little database icon reveals the SQL code of these queries. If you see the number of queries rising abnormally, it is time to consider using a Join. 76 77 Before explaining the Join methods, let's review what happens when you loop over an array of objects and use a Propel getter to retrieve details about a related class, as in Listing 18-2. This example supposes that your schema describes an `article` table with a foreign key to an `author` table. 78 79 Listing 18-2 - Retrieving Details About a Related Class in a Loop 80 81 [php] 82 // In the action 83 $this->articles = ArticlePeer::doSelect(new Criteria()); 84 85 // Database query issued by doSelect() 86 SELECT article.id, article.title, article.author_id, ... 87 FROM article 88 89 // In the template 90 <ul> 91 <?php foreach ($articles as $article): ?> 92 <li><?php echo $article->getTitle() ?>, 93 written by <?php echo $article->getAuthor()->getName() ?></li> 94 <?php endforeach; ?> 95 </ul> 96 97 If the `$articles` array contains ten objects, the `getAuthor()` method will be called ten times, which in turn executes one database query each time it is called to hydrate one object of class `Author`, as in Listing 18-3. 98 99 Listing 18-3 - Foreign Key Getters Issue One Database Query 100 101 [php] 102 // In the template 103 $article->getAuthor() 104 105 // Database query issued by getAuthor() 106 SELECT author.id, author.name, ... 107 FROM author 108 WHERE author.id = ? // ? is article.author_id 109 110 So the page of Listing 18-2 will require a total of 11 queries: the one necessary to build the array of `Article` objects, plus the 10 queries to build one `Author` object at a time. This is a lot of queries to display only a list of articles and their author. 111 112 If you were using plain SQL, you would know how to reduce the number of queries to only one by retrieving the columns of the `article` table and those of the `author` table in the same query. That's exactly what the `doSelectJoinAuthor()` method of the `ArticlePeer` class does. It issues a slightly more complex query than a simple `doSelect()` call, but the additional columns in the result set allow Propel to hydrate both `Article` objects and the related `Author` objects. The code of Listing 18-4 displays exactly the same result as Listing 18-2, but it requires only one database query to do so rather than 11 and therefore is faster. 113 114 Listing 18-4 - Retrieving Details About Articles and Their Author in the Same Query 115 116 [php] 117 // In the action 118 $this->articles = ArticlePeer::doSelectJoinAuthor(new Criteria()); 119 120 // Database query issued by doSelectJoinAuthor() 121 SELECT article.id, article.title, article.author_id, ... 122 author.id, author.name, ... 123 FROM article, author 124 WHERE article.author_id = author.id 125 126 // In the template (unchanged) 127 <ul> 128 <?php foreach ($articles as $article): ?> 129 <li><?php echo $article->getTitle() ?>, 130 written by <?php echo $article->getAuthor()->getName() ?></li> 131 <?php endforeach; ?> 132 </ul> 133 134 There is no difference in the result returned by a `doSelect()` call and a `doSelectJoinXXX()` method; they both return the same array of objects (of class Article in the example). The difference appears when a foreign key getter is used on these objects afterwards. In the case of `doSelect()`, it issues a query, and one object is hydrated with the result; in the case of `doSelectJoinXXX()`, the foreign object already exists and no query is required, and the process is much faster. So if you know that you will need related objects, call a `doSelectJoinXXX()` method to reduce the number of database queries--and improve the page performance. 135 136 The `doSelectJoinAuthor()` method is automatically generated when you call a `propel-build-model` because of the relationship between the `article` and `author` tables. If there were other foreign keys in the article table structure--for instance, to a category table--the generated `BaseArticlePeer` class would have other Join methods, as shown in Listing 18-5. 137 138 Listing 18-5 - Example of Available `doSelect` Methods for an `ArticlePeer` Class 139 140 [php] 141 // Retrieve Article objects 142 doSelect() 143 144 // Retrieve Article objects and hydrate related Author objects 145 doSelectJoinAuthor() 146 147 // Retrieve Article objects and hydrate related Category objects 148 doSelectJoinCategory() 149 150 // Retrieve Article objects and hydrate related Author and Category objects 151 doSelectJoinAuthorAndCategory() 152 153 // Synonym of 154 doSelectJoinAll() 155 156 The peer classes also contain Join methods for `doCount()`. The classes with an i18n counterpart (see Chapter 13) provide a `doSelectWithI18n()` method, which behaves the same as Join methods but for i18n objects. To discover the available Join methods in your model classes, you should inspect the generated peer classes in `lib/model/om/`. If you don't find the Join method needed for your query (for instance, there is no automatically generated Join method for many-to-many relationships), you can build it yourself and extend your model. 157 158 >**TIP** 159 >Of course, a `doSelectJoinXXX()` call is a bit slower than a call to `doSelect()`, so it only improves the overall performance if you use the hydrated objects afterwards. 160 161 ### Avoid Using Temporary Arrays 162 163 When using Propel, objects are already hydrated, so there is no need to prepare a temporary array for the template. Developers not used to ORMs usually fall into this trap. They want to prepare an array of strings or integers, whereas the template can rely directly on an existing array of objects. For instance, imagine that a template displays the list of all the titles of the articles present in the database. A developer who doesn't use OOP would probably write code similar to what is shown in Listing 18-6. 164 165 Listing 18-6 - Preparing an Array in the Action Is Useless If You Already Have One 166 167 [php] 168 // In the action 169 $articles = ArticlePeer::doSelect(new Criteria()); 170 $titles = array(); 171 foreach ($articles as $article) 172 { 173 $titles[] = $article->getTitle(); 174 } 175 $this->titles = $titles; 176 177 // In the template 178 <ul> 179 <?php foreach ($titles as $title): ?> 180 <li><?php echo $title ?></li> 181 <?php endforeach; ?> 182 </ul> 183 184 The problem with this code is that the hydrating is already done by the `doSelect()` call (which takes time), making the `$titles` array superfluous, since you can write the same code as in Listing 18-7. So the time spent to build the `$titles` array could be gained to improve the application performance. 185 186 Listing 18-7 - Using an Array of Objects Exempts You from Creating a Temporary Array 187 188 [php] 189 // In the action 190 $this->articles = ArticlePeer::doSelect(new Criteria()); 191 192 // In the template 193 <ul> 194 <?php foreach ($articles as $article): ?> 195 <li><?php echo $article->getTitle() ?></li> 196 <?php endforeach; ?> 197 </ul> 198 199 If you feel that you really need to prepare a temporary array because some processing is necessary on objects, the right way to do so is to create a new method in your model class that directly returns this array. For instance, if you need an array of article titles and the number of comments for each article, the action and the template should look like Listing 18-8. 200 201 Listing 18-8 - Using a Custom Method to Prepare a Temporary Array 202 203 [php] 204 // In the action 205 $this->articles = ArticlePeer::getArticleTitlesWithNbComments(); 206 207 // In the template 208 <ul> 209 <?php foreach ($articles as $article): ?> 210 <li><?php echo $article[0] ?> (<?php echo $article[1] ?> comments)</li> 211 <?php endforeach; ?> 212 </ul> 213 214 It's up to you to build a fast-processing `getArticleTitlesWithNbComments()` method in the model--for instance, by bypassing the whole object-relational mapping and database abstraction layers. 215 216 ### Bypassing the ORM 217 218 When you don't really need objects but only a few columns from various tables, as in the previous example, you can create specific methods in your model that bypass completely the ORM layer. You can directly call the database with Creole, for instance, and return a custom-built array. Listing 18-9 illustrates this idea. 219 220 Listing 18-9 - Using Direct Creole Access for Optimized Model Methods, in `lib/model/ArticlePeer.php` 221 222 [php] 223 class ArticlePeer extends BaseArticlePeer 224 { 225 public static function getArticleTitlesWithNbComments() 226 { 227 $connection = Propel::getConnection(); 228 $query = 'SELECT %s as title, COUNT(%s) AS nb FROM %s LEFT JOIN %s ON %s = %sGROUP BY %s'; 229 $query = sprintf($query, 230 ArticlePeer::TITLE, CommentPeer::ID, 231 ArticlePeer::TABLE_NAME, CommentPeer::TABLE_NAME, 232 ArticlePeer::ID, CommentPeer::ARTICLE_ID, 233 ArticlePeer::ID 234 ); 235 $statement = $connection->prepareStatement($query); 236 $resultset = $statement->executeQuery(); 237 $results = array(); 238 while ($resultset->next()) 239 { 240 $results[] = array($resultset->getString('title'), $resultset->getInt('nb')); 241 } 242 243 return $results; 244 } 245 } 246 247 When you start building these sorts of methods, you may end up writing one custom method for each action, and lose the benefit of the layer separation--not to mention the fact that you lose database-independence. 248 249 >**TIP** 250 >If Propel doesn't suit you as a model layer, consider using other ORMs before writing your queries by hand. For instance, check the `sfDoctrine` plug-in for an interface with the PhpDoctrine ORM. In addition, you can use another database abstraction layer than Creole to access your database directly. As of PHP 5.1, PDO is bundled with PHP and provides a faster alternative to Creole. 251 252 ### Speeding Up the Database 253 254 There are many database-specific optimization techniques that can be applied regardless of whether you're using symfony. This section briefly outlines the most common database optimization strategies, but a good knowledge of database engines and administration is required to get the most out of your model layer. 255 256 >**TIP** 257 >Remember that the web debug toolbar displays the time taken by each query in a page, and that every tweak should be monitored to determine whether it really improves performance. 258 259 Table queries are often based on non-primary key columns. To improve the speed of such queries, you should define indexes in your database schema. To add a single column index, add the `index: true` property to the column definition, as in Listing 18-10. 260 261 Listing 18-10 - Adding a Single Column Index, in `config/schema.yml` 262 263 propel: 264 article: 265 id: 266 author_id: 267 title: { type: varchar(100), index: true } 268 269 You can use the alternative `index: unique` syntax to define a unique index instead of a classic one. You can also define multiple column indices in `schema.yml` (refer to Chapter 8 for more details about the indexing syntax). You should strongly consider doing this, because it is often a good way to speed up a complex query. 270 271 After adding an index to a schema, you should do the same in the database itself, either by issuing an `ADD INDEX` query directly in the database or by calling the `propel-build-all` command (which will not only rebuild the table structure, but also erase all the existing data). 272 273 >**TIP** 274 >Indexing tends to make `SELECT` queries faster, but `INSERT`, `UPDATE`, and `DELETE` queries are slower. Also, database engines use only one index per query, and they infer the index to be used for each query based on internal heuristics. Adding an index can sometimes be disappointing in terms of performance boost, so make sure you measure the improvements. 275 276 Unless specified otherwise, each request uses a single database connection in symfony, and the connection is closed at the end of the request. You can enable persistent database connections to use a pool of database connections that remain open between queries, by setting `persistent: true` in the `databases.yml` file, as shown in Listing 18-11. 277 278 Listing 18-11 - Enabling Persistent Database Connection Support, in `config/databases.yml` 279 280 prod: 281 propel: 282 class: sfPropelDatabase 283 param: 284 persistent: true 285 dsn: mysql://login:passwd@localhost/blog 286 287 This may or may not improve the overall database performance, depending on numerous factors. The documentation on the subject is abundant on the Internet. Make sure you benchmark your application performance before and after changing this setting to validate its interest. 288 289 >**SIDEBAR** 290 >MySQL-specific tips 291 > 292 >Many settings of the MySQL configuration, found in the my.cnf file, may alter database performance. Make sure you read the online documentation ([http://dev.mysql.com/doc/refman/5.0/en/option-files.html](http://dev.mysql.com/doc/refman/5.0/en/option-files.html)) on this subject. 293 > 294 >One of the tools provided by MySQL is the slow queries log. All SQL statements that take more than `long_query_time` seconds to execute (this is a setting that can be changed in the `my.cnf`) are logged in a file that is quite difficult to construe by hand, but that the `mysqldumpslow` command summarizes usefully. This is a great tool to detect the queries that require optimizations. 295 296 Tweaking the View 297 ----------------- 298 299 According to how you design and implement the view layer, you may notice small slowdowns or speedups. This section describes the alternatives and their tradeoffs. 300 301 ### Using the Fastest Code Fragment 302 303 If you don't use the caching system, you have to be aware that an `include_component()` is slightly slower than an `include_partial()`, which itself is slightly slower than a simple PHP `include`. This is because symfony instantiates a view to include a partial and an object of class `sfComponent` to include a component, which collectively add some minor overhead beyond what's required to include the file. 304 305 However, this overhead is insignificant, unless you include a lot of partials or components in a template. This may happen in lists or tables, and every time you call an `include_partial()` helper inside a `foreach` statement. When you notice that a large number of partial or component inclusions have a significant impact on your performance, you may consider caching (see Chapter 12), and if caching is not an option, then switch to simple `include` statements. 306 307 As for slots and component slots, the difference in performance is perceptible. The process time necessary to set and include a slot is negligible--it is equivalent to a variable instantiation. But component slots rely on a view configuration, and they require a few objects to be initiated to work. However, component slots can be cached independently from the calling templates, while slots are always cached within the template that includes them. 308 309 ### Speeding Up the Routing Process 310 311 As explained in Chapter 9, every call to a link helper in a template asks the routing system to process an internal URI into an external URL. This is done by finding a match between the URI and the patterns of the `routing.yml` file. Symfony does it quite simply: It tries to match the first rule with the given URI, and if it doesn't work, it tries with the following, and so on. As every test involves regular expressions, this is quite time consuming. 312 313 There is a simple workaround: Use the rule name instead of the module/action couple. This will tell symfony which rule to use, and the routing system won't lose time trying to match all previous rules. 314 315 In concrete terms, consider the following routing rule, defined in your `routing.yml` file: 316 317 article_by_id: 318 url: /article/:id 319 param: { module: article, action: read } 320 321 Then instead of outputting a hyperlink this way: 322 323 [php] 324 <?php echo link_to('my article', 'article/read?id='.$article->getId()) ?> 325 326 you should use the fastest version: 327 328 [php] 329 <?php echo link_to('my article', '@article_by_id?id='.$article->getId()) ?> 330 331 The difference starts being noticeable when a page includes a few dozen routed hyperlinks. 332 333 ### Skipping the Template 334 335 Usually, a response is composed of a set of headers and content. But some responses don't need content. For instance, some Ajax interactions need only a few pieces of data from the server in order to feed a JavaScript program that will update different parts of the page. For this kind of short response, a set of headers alone is faster to transmit. As discussed in Chapter 11, an action can return only a JSON header. Listing 18-12 reproduces an example from Chapter 11. 336 337 Listing 18-12 - Example Action Returning a JSON Header 338 339 [php] 340 public function executeRefresh() 341 { 342 $output = '<"title", "My basic letter"], ["name", "Mr Brown">'; 343 $this->getResponse()->setHttpHeader("X-JSON", '('.$output.')'); 344 345 return sfView::HEADER_ONLY; 346 } 347 348 This skips the template and the layout, and the response can be sent at once. As it contains only headers, it is more lightweight and will take less time to transmit to the user. 349 350 Chapter 6 explained another way to skip the template by returning content text directly from the action. This breaks the MVC separation, but it can increase the responsiveness of an action greatly. Check Listing 18-13 for an example. 351 352 Listing 18-13 - Example Action Returning Content Text Directly 353 354 [php] 355 public function executeFastAction() 356 { 357 return $this->renderText("<html><body>Hello, World!</body></html>"); 358 } 359 360 ### Restricting the Default Helpers 361 362 The standard helper groups (`Partial`, `Cache`, and `Form`) are loaded for every request. If you are sure that you won't use some of them, removing a helper group from the list of standard ones will save you the parsing of the helper file. In particular, the Form helper group, although included by default, is quite heavy and slows down pages with no forms just because of its size. So it might be a good idea to edit the `standard_helpers` setting in the `settings.yml` file to remove it: 363 364 all: 365 .settings: 366 standard_helpers: [Partial, Cache] # Form is removed 367 368 The tradeoff is that you must declare the `Form` helper group on each template using it with `use_helper('Form')`. 369 370 ### Compressing the Response 371 372 Symfony compresses the response before sending it to the user. This feature is based on the PHP zlib module. You can save a little CPU time for each request by deactivating it in the `settings.yml` file: 373 374 all: 375 .settings: 376 compressed: off 377 378 Be aware that the CPU gain will be balanced by the bandwidth loss, so the performance won't increase in all configurations with this change. 379 380 >**TIP** 381 >If you deactivate zip compression in PHP, you can enable it at the server level. Apache has a compression extension of its own. 382 383 Tweaking the Cache 384 ------------------ 385 386 Chapter 12 already described how to cache parts of a response or all of it. The response cache results in a major performance improvement, and it should be one of your first optimization considerations. If you want to make the most out of the cache system, read further, for this section unveils a few tricks you might not have thought of. 387 388 ### Clearing Selective Parts of the Cache 389 390 During application development, you have to clear the cache in various situations: 391 392 * When you create a new class: Adding a class to an autoloading directory (one of the project's `lib/` folders) is not enough to have symfony find it automatically. You must clear the autoloading configuration cache so that symfony browses again all the directories of the `autoload.yml` file and references the location of autoloadable classes--including the new ones. 393 * When you change the configuration in production: The configuration is parsed only during the first request in production. Further requests use the cached version instead. So a change in the configuration in the production environment (or any environment where `SF_DEBUG` is turned off) doesn't take effect until you clear the cached version of the file. 394 * When you modify a template in an environment where the template cache is enabled: The valid cached templates are always used instead of existing templates in production, so a template change is ignored until the template cache is cleared or outdated. 395 * When you update an application with the `sync` command: This case usually covers the three previous modifications. 396 397 The problem with clearing the whole cache is that the next request will take quite long to process, because the configuration cache needs to be regenerated. Besides, the templates that were not modified will be cleared from the cache as well, losing the benefit of previous requests. 398 399 That means it's a good idea to clear only the cache files that really need to be regenerated. Use the options of the `clear-cache` task to define a subset of cache files to clear, as demonstrated in Listing 18-14. 400 401 Listing 18-14 - Clearing Only Selective Parts of the Cache 402 403 // Clear only the cache of the myapp application 404 > symfony clear-cache myapp 405 406 // Clear only the HTML cache of the myapp application 407 > symfony clear-cache myapp template 408 409 // Clear only the configuration cache of the myapp application 410 > symfony clear-cache myapp config 411 412 You can also remove files by hand in the `cache/` directory, or clear template cache files selectively from the action with the `$cacheManager->remove()` method, as described in Chapter 12. 413 414 All these techniques will minimize the negative performance impact of any of the changes listed previously. 415 416 >**TIP** 417 >When you upgrade symfony, the cache is automatically cleared, without manual intervention (if you set the `check_symfony_version` parameter to `true` in `settings.yml`). 418 419 ### Generating Cached Pages 420 421 When you deploy a new application to production, the template cache is empty. You must wait for users to visit a page once for this page to be put in the cache. In critical deployments, the overhead of page processing is not acceptable, and the benefits of caching must be available as soon as the first request is issued. 422 423 The solution is to automatically browse the pages of your application in the staging environment (where the configuration is similar to the one in production) to have the template cache generated, then to transfer the application with the cache to production. 424 425 To browse the pages automatically, one option is to create a shell script that looks through a list of external URLs with a browser (curl for instance). But there is a better and faster solution: a symfony batch using the `sfBrowser` object, already discussed in Chapter 15. That's an internal browser written in PHP (and used by `sfTestBrowser` for functional tests). It takes an external URL and returns a response, but the interesting thing is that it triggers the template cache just like a regular browser. As it only initializes symfony once and doesn't pass by the HTTP transport layer, this method is a lot faster. 426 427 Listing 18-15 shows an example batch script used to generate template cache files in a staging environment. Launch it by calling `php batch/generate_cache.php`. 428 429 Listing 18-15 - Generating the Template Cache, in `batch/generate_cache.php` 430 431 [php] 432 <?php 433 434 define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/..')); 435 define('SF_APP', 'myapp'); 436 define('SF_ENVIRONMENT', 'staging'); 437 define('SF_DEBUG', false); 438 439 require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php'); 440 441 // Array of URLs to browse 442 $uris = array( 443 '/foo/index', 444 '/foo/bar/id/1', 445 '/foo/bar/id/2', 446 ... 447 ); 448 449 $b = new sfBrowser(); 450 foreach ($uris as $uri) 451 { 452 $b->get($uri); 453 } 454 455 ### Using a Database Storage System for Caching 456 457 The default storage system for the template cache in symfony is the file system: Fragments of HTML or serialized response objects are stored under the `cache/` directory of a project. Symfony proposes an alternative way to store cache: a SQLite database. Such a database is a simple file that PHP natively knows how to query very efficiently. 458 459 To tell symfony to use SQLite storage instead of file system storage for the template cache, open the `factories.yml` file and edit the `view_cache` entry as follows: 460 461 view_cache: 462 class: sfSQLiteCache 463 param: 464 database: %SF_TEMPLATE_CACHE_DIR%/cache.db 465 466 The benefits of using SQLite storage for the template cache are faster read and write operations when the number of cache elements is important. If your application makes heavy use of caching, the template cache files end up scattered in a deep file structure; in this case, switching to SQLite storage will increase performance. In addition, clearing the cache on file system storage may require a lot of files to be removed from the disk; this operation may last a few seconds, during which your application is unavailable. With a SQLite storage system, the cache clearing process results in a single file operation: the deletion of the SQLite database file. Whatever the number of cache elements currently stored, the operation is instantaneous. 467 468 ### Bypassing Symfony 469 470 Perhaps the best way to speed symfony up is to bypass it completely . . . this is said only partly in jest. Some pages don't change and don't need to be reprocessed by the framework at each request. The template cache is already here to speed up the delivery of such pages, but it still relies on symfony. 471 472 A couple of tricks described in Chapter 12 allow you to bypass symfony completely for some pages. The first one involves the use of HTTP 1.1 headers for asking the proxies and client browsers to cache the page themselves, so that they don't request it again the next time the page is needed. The second one is the super fast cache (automated by the `sfSuperCachePlugin` plug-in), which consists of storing a copy of the response in the `web/` directory and modifying the rewriting rules so that Apache first looks for a cached version before handing a request to symfony. 473 474 Both these methods are very effective, and even if they only apply to static pages, they will take the burden of handling these pages off from symfony, and the server will then be fully available to deal with complex requests. 475 476 ### Caching the Result of a Function Call 477 478 If a function doesn't rely on context-sensitive values nor on randomness, calling it twice with the same parameters should return the same result. That means the second call could very well be avoided if the result had been stored the first time. That's exactly what the `sfFunctionCache` class does. This class has a `call()` method, which expects a callable and a set of parameters. When called, this method creates an md5 hash with all its arguments and looks in the cache directory for a file named by this hash. If such a file is found, the function returns the result stored in the file. If not, the sfFunctionCache executes the function, stores the result in the cache, and returns it. So the second execution of Listing 18-16 will be faster than the first one. 479 480 Listing 18-16 - Caching the Result of a Function 481 482 [php] 483 $function_cache_dir = sfConfig::get('sf_cache_dir').'/function'; 484 $fc = new sfFunctionCache($function_cache_dir); 485 $result1 = $fc->call('cos', M_PI); 486 $result2 = $fc->call('preg_replace', '/\s\s+/', ' ', $input); 487 488 The `sfFunctionCache` constructor expects an absolute directory path as argument (the directory must exist prior to the object instantiation). The first argument of the `call()` method must be a callable, so it can be a function name, an array of a class name and static method name, or an array of an object name and public method name. As for the other arguments of the `call()` method, you can include as many as you need--they are all handed to the callable. 489 490 This object is especially useful for CPU-intensive functions, because the file I/O overhead exceeds the time required to process a simple function. It relies on the `sfFileCache` class, which is the component also used by the symfony template cache engine. Refer to the API documentation for more details. 491 492 >**CAUTION** 493 >The `clear-cache` task erases only the contents of the `cache/` file. If you store the function cache somewhere else, it will not be cleared automatically when you clear the cache through the command line. 494 495 ### Caching Data in the Server 496 497 PHP accelerators provide special functions to store data in memory so that you can reuse it across requests. The problem is that they all have a different syntax, and each has its own specific way of performing this task. Symfony provides a class called `sfProcessCache`, which abstracts all these differences and works with whatever accelerator you are using. See its syntax in Listing 18-17. 498 499 Listing 18-17 - Syntax of the `sfProcessCache` Methods 500 501 [php] 502 // Storing data in the process cache 503 sfProcessCache::set($name, $value, $lifetime); 504 505 // Retrieving data 506 $value = sfProcessCache::get($name); 507 508 // Checking if a piece of data exists in the process cache 509 $value_exists = sfProcessCache::has($name); 510 511 // Clear the process cache 512 sfProcessCache::clear(); 513 514 The `set()` method returns `false` if the caching didn't work. The cached value can be anything (a string, an array, an object); the `sfProcessCache` class will deal with the serialization. The `get()` method returns `null` if the required variable doesn't exist in the cache. 515 516 The methods of the `sfProcessCache` class work even if no accelerator is installed. Therefore, there is no risk in trying to retrieve data from the process cache, as long as you provide a fallback value. For instance, Listing 18-18 shows how to retrieve a configuration setting in the process cache. 517 518 Listing 18-18 - Using the Process Cache Safely 519 520 [php] 521 if (sfProcessCache::has('myapp_parameters')) 522 { 523 $params = sfProcessCache::get('myapp_parameters'); 524 } 525 else 526 { 527 $params = retrieve_parameters(); 528 } 529 530 >**TIP** 531 >If you want to go further into memory caching, make sure you take a look at the memcache extension for PHP. It can help decrease the database load on load-balanced applications, and PHP 5 provides an OO interface to it ([http://www.php.net/memcache/](http://www.php.net/memcache/)). 532 533 Deactivating the Unused Features 534 -------------------------------- 535 536 The default symfony configuration activates the most common features of a web application. However, if you happen to not need all of them, you should deactivate them to save the time their initialization takes on each request. 537 538 For instance, if your application doesn't use the session mechanism, or if you want to start the session handling by hand, you should turn the `auto_start` setting to `false` in the `storage` key of the `factories.yml` file, as in Listing 18-19. 539 540 Listing 18-19 - Turning Sessions Off, in `myapp/config/factories.yml` 541 542 all: 543 storage: 544 class: sfSessionStorage 545 param: 546 auto_start: false 547 548 The same applies for the database (as explained in the "Tweaking the Model" section earlier in this chapter) and output escaping feature (see Chapter 7). If your application makes no use of them, deactivate them for a small performance gain, this time in the `settings.yml` file (see Listing 18-20). 549 550 Listing 18-20 - Turning Features Off, in `myapp/config/settings.yml` 551 552 all: 553 .settings: 554 use_database: off # Database and model features 555 escaping_strategy: off # Output escaping feature 556 557 As for the security and the flash attribute features (see Chapter 6), you can deactivate them in the `filters.yml` file, as shown in Listing 18-21. 558 559 Listing 18-21 - Turning Features Off, in `myapp/config/filters.yml` 560 561 rendering: ~ 562 web_debug: ~ 563 security: 564 enabled: off 565 566 # generally, you will want to insert your own filters here 567 568 cache: ~ 569 common: ~ 570 flash: 571 enabled: off 572 573 execution: ~ 574 575 Some features are useful only in development, so you should not activate them in production. This is already the case by default, since the production environment in symfony is really optimized for performance. Among the performance-impacting development features, the `SF_DEBUG` mode is the most severe. As for the symfony logs, the feature is also turned off in production by default. 576 577 You may wonder how to get information about failed requests in production if logging is disabled, and argue that problems arise not only in development. Fortunately, symfony can use the `sfErrorLoggerPlugin` plug-in, which runs in the background in production and logs the details of 404 and 500 errors in a database. It is much faster than the file logging feature, because the plug-in methods are called only when a request fails, while the logging mechanism, once turned on, adds a nonnegligible overhead whatever the level. Check the installation instructions and manual at [http://www.symfony-project.com/trac/wiki/sfErrorLoggerPlugin](http://www.symfony-project.com/trac/wiki/sfErrorLoggerPlugin). 578 579 >**TIP** 580 >Make sure you regularly check the server error logs--they also contain very valuable information about 404 and 500 errors. 581 582 Optimizing Your Code 583 -------------------- 584 585 It's also possible to speed up your application by optimizing the code itself. This section offers some insight regarding how to do that. 586 587 ### Core Compilation 588 589 Loading ten files requires more I/O operations than loading one long file, especially on slow disks. Loading a very long file requires more resources than loading a smaller file--especially if a large share of the file content is of no use for the PHP parser, which is the case for comments. 590 591 So merging a large number of files and stripping out the comments they contain is an operation that improves performance. Symfony already does that optimization; it's called the core compilation. At the beginning of the first request (or after the cache is cleared), a symfony application concatenates all the core framework classes (`sfActions`, `sfRequest`, `sfView`, and so on) into one file, optimizes the file size by removing comments and double blanks, and saves it in the cache, in a file called `config_core_compile.yml.php`. Each subsequent request only loads this single optimized file instead of the 30 files that compose it. 592 593 If your application has classes that must always be loaded, and especially if they are big classes with lots of comments, it may be beneficial to add them to the core compile file. To do so, just add a `core_compile.yml` file in your application `config/` directory, and list in it the classes that you want to add, as in Listing 18-22. 594 595 Listing 18-22 - Adding Your Classes to the Core Compile File, in `myapp/config/core_compile.yml` 596 597 - %SF_ROOT_DIR%/lib/myClass.class.php 598 - %SF_ROOT_DIR%/apps/myapp/lib/myToolkit.class.php 599 - %SF_ROOT_DIR%/plugins/myPlugin/lib/myPluginCore.class.php 600 ... 601 602 ### The sfOptimizer Plug-In 603 604 Symfony also offers another optimization tool, called `sfOptimizer`. It applies various optimization strategies to the symfony and application code, which may further speed up the execution. 605 606 The symfony code counts many tests that rely on configuration parameters--and your application may also do so. For instance, if you take a look at the symfony classes, you will often see a test on the value of the `sf_logging_enabled` parameter before a call to the `sfLogger` object: 607 608 [php] 609 if (sfConfig::get('sf_logging_enabled')) 610 { 611 $this->getContext()->getLogger()->info('Been there'); 612 } 613 614 Even if the `sfConfig` registry is very well optimized, the number of calls to its `get()` method during the processing of each request is important--and it counts in the final performance. One of the `sfOptimizer` optimization strategies is to replace configuration constants by their value--as long as these constants are not subject to change at runtime. That's the case, for instance, with the `sf_logging_enabled` parameter; when it is defined as `false`, the `sfOptimizer` transforms the previous code into the following: 615 616 [php] 617 if (0) 618 { 619 $this->getContext()->getLogger()->info('Been there'); 620 } 621 622 And that's not all, because an evident test like the preceding one also gets optimized to an empty string. 623 624 To apply the optimizations, you must first install the plug-in from [http://www.symfony-project.com/trac/wiki/sfOptimizerPlugin](http://www.symfony-project.com/trac/wiki/sfOptimizerPlugin) and then call the `optimize` task, specifying an application and an environment: 625 626 > symfony optimize myapp prod 627 628 If you want to apply other optimization strategies to your code, the `sfOptimizer` plug-in might be a good starting place. 629 630 Summary 631 ------- 632 633 Symfony is already a very optimized framework and is able to handle high-traffic websites without a problem. But if you really need to optimize your application's performance, tweaking the configuration (whether the server configuration, the PHP configuration, or the application settings) will gain you a small boost. You should also follow good practices to write efficient model methods; and since the database is often a bottleneck in web applications, this point should require all your attention. Templates can also benefit from a few tricks, but the best boost will always come from caching. Finally, don't hesitate to look at existing plug-ins, since some of them provide innovative techniques to further speed up the delivery of web pages (`sfSuperCache`, `sfOptimizer`).
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
Généré le : Fri Mar 16 22:42:14 2007 | par Balluche grâce à PHPXref 0.7 |