Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Advanced PHP Programming

.pdf
Скачиваний:
71
Добавлен:
14.04.2015
Размер:
7.82 Mб
Скачать

438Chapter 18 Profiling

very suspicious. Calling mktime() and date() repeatedly seems strange. In fact, date() is called 217 times in this function. Looking back up to the exclusive trace in Figure 18.5, you can see that the date() function is called 240 times in total and accounts for 14.8% of the script’s execution time, so this might be a good place to optimize.

Figure 18.6 A call tree for the Serendipity index page.

Fortunately, the call tree tells you exactly where to look: serendipity_functions.inc.php, lines 245–261. Here is the offending code:

227

print (<TR CLASS=serendipity_calendar>);

228

for ($y=0; $y<7; $y++) {

229

// Be able to print borders nicely

230

$cellProp

= “”;

231if ($y==0) $cellProp = FirstInRow;

232if ($y==6) $cellProp = LastInRow;

233if ($x==4) $cellProp = LastRow;

234if ($x==4 && $y==6) $cellProp = LastInLastRow;

236// Start printing

237if (($x>0 || $y>=$firstDayWeekDay) && $currDay<=$nrOfDays) {

238if ($activeDays[$currDay] > 1) $cellProp.=Active;

239print(<TD CLASS=serendipity_calendarDay$cellProp>);

Profiling a Larger Application

439

240

241// Print day

242if ($serendipity[rewrite]==true)

243$link = $serendipity[serendipityHTTPPath].archives/.

244

date(Ymd, mktime(0,0,0, $month, $currDay, $year)).

245

.html;

246else

247$link = $serendipity[serendipityHTTPPath];;

248if (date(m) == $month &&

249date(Y) == $year &&

250date(j) == currDay) {

251echo <I>;

252}

253if ($activeDays[$currDay] > 1) {

254print (<A HREF=$link>);

255}

256print ($currDay);

257if ($activeDays[$currDay] > 1) print (</A>);

258if (date(m) == $month &&

259date(Y) == $year &&

260date(j) == $currDay) {

261echo </I>;

262}

263print(</TD>);

264$currDay++;

265}

266else {

267print <TD CLASS=serendipity_calendarBlankDay$cellProp>;

268print  </TD>;

269}

270}

271print (</TR>);

This is a piece of the serendipity_drawcalendar() function, which draws the calendar in the navigation bar. Looking at line 244, you can see that the date() call is dependent on $month, $currDay, and $year. $currDay is incremented on every iteration through the loop, so you cannot cleanly avoid this call.You can, however, replace it:

date(Ymd, mktime(0,0,0, $month, $currDay, $year))

This line makes a date string from $month, $currDay, and $year.You can avoid the date() and mktime() functions by simply formatting the string yourself:

sprintf(%4d%02d%02d:, $year, $month, $currDay)

However, the date calls on lines 248, 249, 250, 258, 259, and 260 are not dependent on any variables, so you can pull their calculation to outside the loop.When you do this, the top of the loop should precalculate the three date() results needed:

440 Chapter 18 Profiling

227 $date_m = date(m); 228 $date_Y = date(Y); 229 $date_j = date(j);

230 print (<TR CLASS=serendipity_calendar>); 231 for ($y=0; $y<7; $y++) {

232 /* ... */

Then lines 248–250 and 258–261 should both become this:

if ($date_m == $month &&

$date_Y == $year &&

$date_j == $currDay) {

Implementing this simple change reduces the number of date() calls from 240 to 38, improves the speed of serendipity_plugin_api::generate_plugins() by more than 20%, and reduces the overall execution time of the index page by 10%.That’s a significant gain for a nine-line change and 15 minutes’ worth of work.

This particular example is easy to categorize as simply being a case of programmer error. Putting an invariant function inside a loop is a common mistake for beginners; dismissing it is a mistake, though, for a number of reasons:

nExperienced programmers as well as beginners make these sorts of mistakes, especially in large loops where it is easy to forget where variables change.

nIn a team environment, it’s extremely easy for simple inefficiencies like these to crop up. For example, a relatively simple task (such as writing a calendar) may be dispatched to a junior developer, and a casual audit of the work might fail to turn up this sort of error.

nInefficiencies like these are almost never revealed by intuition. If you approach the code base from afar, it is unlikely that you’ll think that the calendar (largely an afterthought in the application design) is a bottleneck. Small features like these often contain subtle inefficiencies; 10% here, 15% there—they quickly add up to trouble in a performance-sensitive application.

Spotting General Inefficiencies

Profilers excel at spotting general inefficiencies. An example might include using a moderately expensive user function repeatedly when a built-in function might do or frequently using a function in a loop where a single built-in function would do the job. Unlike the analysis done earlier in this chapter, using the inclusive timings, mild but widespread issues are often better diagnosed by using exclusive time ordering.

My favorite example of this sort of “obvious” yet largely undetectable inefficiency occurred during the birth of APD. At the company where I was working, there were some functions to handle making binary data (specifically, encrypted user data) 8-bit safe so that they could be set into cookies. On every request to a page that required member

Spotting General Inefficiencies

441

credentials, the users’ cookie would be decrypted and used for both authentication and as a basic cache of their personal data. User sessions were to be timed out, so the cookie contained a timestamp that was reset on every request and used to ensure that the session was still valid.

This code had been in use for three years and was authored in the days of PHP3, when non-binary-safe data (for example, data containing nulls) was not correctly handled in the PHP cookie handling code—and before rawurlencode() was binary safe. The functions looked something like this:

function hexencode($data) {

$ascii = unpack(C*, $data); $retval = ‘’;

foreach ($ascii as $v) {

$retval .= sprintf(%02x, $v);

}

return $retval;

}

function hexdecode($data) {

$len = strlen($data); $retval = ‘’;

for($i=0; $i < $len; $i+= 2) { $retval .= pack(C, hexdec( substr($data, $i, 2)

)

);

}

return $retval;

}

On encoding, a string of binary data was broken down into its component characters with unpack().The component characters were then converted to their hexadecimal values and reassembled. Decoding affected the reverse. On the surface, these functions are pretty efficient—or at least as efficient as they can be when written in PHP.

When I was testing APD, I discovered to my dismay that these two functions consumed almost 30% of the execution time of every page on the site.The problem was that the user cookies were not small—they were about 1KB on average—and looping through an array of that size, appending to a string, is extremely slow in PHP. Because the functions were relatively optimal from a PHP perspective, we had a couple choices:

nFix the cookie encoding inside PHP itself to be binary safe.

nUse a built-in function that achieves a result similar to what we were looking for (for example, base64_encode()).

We ended up choosing the former option, and current releases of PHP have binary-safe cookie handling. However, the second option would have been just as good.

442 Chapter 18 Profiling

A simple fix resulted in a significant speedup.This was not a single script speedup, but a capacity increase of 30% across the board. As with all technical problems that have simple answers, the question from on top was “How did this happen?”The answer is multifaceted but simple, and the reason all high-traffic scripts should be profiled regularly:

nThe data had changed—When the code had been written (years before), user cookies had been much smaller (less than 100 bytes), and so the overhead was much lower.

nIt didn’t actually break anything—A 30% slowdown since inception is inher-

ently hard to track.The difference between 100ms and 130ms is impossible to spot with the human eye.When machines are running below capacity (as is common in many projects), these cumulative slowdowns do not affect traffic levels.

nIt looked efficient—The encoding functions are efficient, for code written in PHP.With more than 2,000 internal functions in PHP’s standard library, it is not hard to imagine failing to find base64_encode() when you are looking for a built-in hex-encoding function.

nThe code base was huge—With nearly a million lines of PHP code, the application code base was so large that a manual inspection of all the code was impossible.Worse still, with PHP lacking a hexencode() internal function, you need to have specific information about the context in which the userspace function is being used to suggest that base64_encode() will provide equivalent functionality.

Without a profiler, this issue would never have been caught.The code was too old and buried too deep to ever be found otherwise.

Note

There is an additional inefficiency in this cookie strategy. Resetting the user’s cookie on every access could guarantee that a user session was expired after exactly 15 minutes, but it required the cookie to be reencrypted and reset on every access. By changing the time expiration time window to a fuzzy one—between 15 and 20 minutes for expiration—you can change the cookie setting strategy so that it is reset only if it is already more than 5 minutes old. This will buy you a significant speedup as well.

Removing Superfluous Functionality

After you have identified and addressed any obvious bottlenecks that have transparent changes, you can also use APD to gather a list of features that are intrinsically expensive. Cutting the fat from an application is more common in adopted projects (for example, when you want to integrate a free Web log or Web mail system into a large application) than it is in projects that are completely home-grown, although even in the latter case, you occasionally need to remove bloat (for example, if you need to repurpose the application into a higher-traffic role).

Removing Superfluous Functionality

443

There are two ways to go about culling features.You can systematically go through a product’s feature list and remove those you do not want or need. (I like to think of this as top-down culling.) Or you can profile the code, identify features that are expensive, and then decide whether you want or need them (bottom-up culling).Top-down culling certainly has an advantage: It ensures that you do a thorough job of removing all the features you do not want.The bottom-up methodology has some benefits as well:

n It identifies features. In many projects, certain features are undocumented.

nIt provides incentive to determine which features are nice and which are necessary.

nIt supplies data for prioritizing pruning.

In general, I prefer using the bottom-up method when I am trying to gut a third-party application for use in a production setting, where I do not have a specific list of features I want to remove but am simply trying to improve its performance as much as necessary.

Let’s return to the Serendipity example.You can look for bloat by sorting a trace by inclusive times. Figure 18.7 shows a new trace (after the optimizations you made earlier), sorted by exclusive real time. In this trace, two things jump out: the define() functions and the preg_replace() calls.

Figure 18.7 A postoptimization profile.

444Chapter 18 Profiling

In general, I think it is unwise to make any statements about the efficiency of define(). The usual alternative to using define() is to utilize a global variable. Global variable declarations are part of the language syntax (as opposed to define(), which is a function), so the overhead of their declaration is not as easily visible through APD.The solution I would recommend is to implement constants by using const class constants. If you are running a compiler cache, these will be cached in the class definition, so they will not need to be reinstantiated on every request.

The preg_replace() calls demand more attention. By using a call tree (so you can be certain to find the instances of preg_replace() that are actually being called), you can narrow down the majority of the occurrences to this function:

function serendipity_emoticate($str) { global $serendipity;

foreach ($serendipity[smiles] as $key => $value) { $str = preg_replace(/([\t\ ]?).preg_quote($key,/).

([\t\ \!\.\)]?)/m, $1<img src=\$value\/>$2, $str);

}

return $str;

}

where $serendipity[smiles] is defined as

$serendipity[smiles] =

array(:(=> $serendipity[serendipityHTTPPath].pixel/cry_smile.gif,

:-) => $serendipity[serendipityHTTPPath].pixel/regular_smile.gif,

:-O => $serendipity[serendipityHTTPPath].pixel/embaressed_smile.gif,

:O=> $serendipity[serendipityHTTPPath].pixel/embaressed_smile.gif,

:-( => $serendipity[serendipityHTTPPath].pixel/sad_smile.gif,

:(=> $serendipity[serendipityHTTPPath].pixel/sad_smile.gif,

:)=> $serendipity[serendipityHTTPPath].pixel/regular_smile.gif,

8-) => $serendipity[serendipityHTTPPath].pixel/shades_smile.gif,

:-D => $serendipity[serendipityHTTPPath].pixel/teeth_smile.gif,

:D=> $serendipity[serendipityHTTPPath].pixel/teeth_smile.gif,

8)=> $serendipity[serendipityHTTPPath].pixel/shades_smile.gif,

:-P => $serendipity[serendipityHTTPPath].pixel/tounge_smile.gif,

;-) => $serendipity[serendipityHTTPPath].pixel/wink_smile.gif,

;)=> $serendipity[serendipityHTTPPath].pixel/wink_smile.gif,

:P=> $serendipity[serendipityHTTPPath].pixel/tounge_smile.gif, );

and here is the function that actually applies the markup, substituting images for the emoticons and allowing other shortcut markups:

function serendipity_markup_text($str, $entry_id = 0) {

global $serendipity;

Removing Superfluous Functionality

445

$ret = $str;

$ret = str_replace(\_, chr(1), $ret);

$ret = preg_replace(/#([[:alnum:]]+?)#/,&\1;,$ret); $ret = preg_replace(/\b_([\S ]+?)_\b/,<u>\1</u>,$ret); $ret = str_replace(chr(1), \_, $ret);

//bold

$ret = str_replace(\*,chr(1),$ret); $ret = str_replace(**,chr(2),$ret);

$ret = preg_replace(/(\S)\*(\S)/,\1. chr(1) . \2,$ret);

$ret = preg_replace(/\B\*([^*]+)\*\B/,<strong>\1</strong>,$ret); $ret = str_replace(chr(2),**,$ret);

$ret = str_replace(chr(1),\*,$ret);

// monospace font

$ret = str_replace(\%,chr(1),$ret);

$ret = preg_replace_callback(/%([\S ]+?)%/, serendipity_format_tt, $ret); $ret = str_replace(chr(1),%,$ret) ;

$ret = preg_replace(/\|([0-9a-fA-F]+?)\|([\S ]+?)\|/,

<font color=\1>\2</font>,$ret);

$ret = preg_replace(/\^([[:alnum:]]+?)\^/,<sup>\1</sup>,$ret); $ret = preg_replace(/\@([[:alnum:]]+?)\@/,<sub>\1</sub>,$ret); $ret = preg_replace(/([\\\])([*#_|^@%])/, \2, $ret);

if ($serendipity[track_exits]) { $serendipity[encodeExitsCallback_entry_id] = $entry_id;

$ret = preg_replace_callback(

#<a href=(\|)http://([^\”’]+)(\|)#im,

serendipity_encodeExitsCallback, $ret

);

}

return $ret;

}

The first function, serendipity_emoticate(), goes over a string and replaces each text emoticon—such as the smiley face :)—with a link to an actual picture.This is designed to allow users to enter entries with emoticons in them and have the Web log software automatically beautify them.This is done on entry display, which allows users to retheme their Web logs (including changing emoticons) without having to manually edit all their entries. Because there are 15 default emoticons, preg_replace() is run 15 times for every Web log entry displayed.

446 Chapter 18 Profiling

The second function, serendipity_markup_text(), implements certain common text typesetting conventions.This phrase:

*hello*

is replaced with this:

<strong>hello</strong>

Other similar replacements are made as well. Again, this is performed at display time so that you can add new text markups later without having to manually alter existing entries.This function runs nine preg_replace() and eight str_replace() calls on every entry.

Although these features are certainly neat, they can become expensive as traffic increases. Even with a single small entry, these calls constitute almost 15% of the script’s runtime. On my personal Web log, the speed increases I have garnered so far are already more than the log will probably ever need. But if you were adapting this to be a service to users on a high-traffic Web site, removing this overhead might be critical.

You have two choices for reducing the impact of these calls.The first is to simply remove them altogether. Emoticon support can be implemented with a JavaScript entry editor that knows ahead of time what the emoticons are and lets the user select from a menu.The text markup can also be removed, requiring users to write their text markup in HTML.

A second choice is to retain both of the functions but apply them to entries before they are saved so that the overhead is experienced only when the entry is created. Both of these methods remove the ability to change markups after the fact without modifying existing entries, which means you should only consider removing them if you need to.

A Third Method for Handling Expensive Markup

I once worked on a site where there was a library of regular expressions to remove profanity and malicious JavaScript/CSS from user-uploaded content (to prevent cross-site scripting attacks). Because users can be extremely…creative…in their slurs, the profanity list was a constantly evolving entity as new and unusual foul language was discovered by the customer service folks. The site was extremely high traffic, which meant that the sanitizing process could not be effectively applied at request time (it was simply too expensive), but the dynamic nature of the profanity list meant that we needed to be able to reapply new filter rules to existing entries. Unfortunately, the user population was large enough that actively applying the filter to all user records was not feasible either.

The solution we devised was to use two content tables and a cache-on-demand system. An unmodified copy of a user’s entry was stored in a master table. The first time it was requested, the current filter set was applied to it, and the result was stored in a cache table. When subsequent requests for a page came in, they checked the cache table first, and only on failure did they re-cache the entry. When the filter set was updated, the cache table was truncated, removing all its data. Any new page requests would immediately be re-cached—this time with the new filter. This caching table could easily have been replaced with a network file system if we had so desired.

Further Reading

447

The two-tier method provided almost all the performance gain of the modify-on-upload semantics. There was still a significant hit immediately after the rule-set was updated, but there was all the convenience of modify-on-request. The only downside to the method was that it required double the storage necessary to implement either of the straightforward methods (because the original and cached copies are stored separately). In this case, this was an excellent tradeoff.

Further Reading

There is not an abundance of information on profiling tools in PHP.The individual profilers mentioned in this chapter all have some information on their respective Web sites but there is no comprehensive discussion on the art of profiling.

In addition to PHP-level profilers, there are a plethora of lower-level profilers you can use to profile a system.These tools are extremely useful if you are trying to improve the performance of the PHP language itself, but they’re not terribly useful for improving an application’s performance.The problem is that it is almost impossible to directly connect lower-level (that is, engine-internal) C function calls or kernel system calls to actions you take in PHP code. Here are some excellent C profiling tools:

n gprof is the GNU profiler and is available on almost any system. It profiles C code well, but it can be difficult to interpret.

n valgrind, combined with its companion GUI kcachegrind, is an incredible memory debugger and profiler for Linux. If you write C code on Linux, you should learn to use valgrind.

n ooprofile is a kernel-level profiler for Linux. If you are doing low-level debugging where you need to profile an application’s system calls, ooprofile is a good tool for the job.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]