Performance Tuning with XHProf
Dan van Kley
Computerologist
Salesforce Pardot
dvankley@salesforce.com
Who I Am
PHP Convert
Recovering .NET developer
Backend Engineer
This isn’t a Javascript conference anyway
Lover of Automation
Automate all the things
Very Lazy and Slightly Paranoid
Not much of a web presence, just send me an email.
Who Pardot Is
ATL Represent!
We’re right by Lenox, come by and play some ping pong
B2B Marketing Automation Division of Salesforce
Odds are you don’t care about that part
Primary App in PHP
• ~600k authenticated requests/day
• ~20M API requests/day
• ~125M public endpoint requests/day
• ~175M background task executions/day
Recent PHP 7 Migration
It is great and you should use it
It’s not French
Why Performance Matters
User Experience
Page load times are critical to user satisfaction
Resource Efficiency
Do more with less, save $$$
Larger Workloads
Scale up without more hardware
Understand Your Stack
Know what your function calls are actually doing
Agenda
XHProf Basics
Performance Tips
Scenarios and Examples
Hopefully we can get to lunch early
XHProf Basics
Getting off the ground
What XHProf Is
PHP Extension
Written in C and linked into the runtime to provide low level instrumentation.
Hierarchical Profiler
Profile is a call hierarchy, not a strict “graph”. Callgraph does a better job of modeling.
Simple Frontend
Ships with a basic frontend, many other options available as well. Choose your own adventure!
Measurements per Function:
• Number of times called
• Inclusive/exclusive memory usage
• Inclusive/exclusive execution time
How to Install XHProf
Original Project Abandoned
Facebook runs HHVM now so they don’t care about
vanilla PHP profiling anymore
PHP 5.x
Original or Phacility’s fork
PHP 7.x
Older versions may work, if not consider Tideways
or longxinH forks
Installation
You will need to compile forks from source.
Homebrew/PECL has the old Facebook version.
How to Install XHProf
Configuration
• Include the XHProf extension in your php.ini
• Verify it loads with phpinfo() or php –i
• Configure xhprof.output_dir
Set up your Frontend
• Facebook’s original UI is simple and works well,
that’s what I’m using
• Original UI is a simple PHP app contained in
xhprof_html, just point your server at it
• There are tons of other frontend options
How to Run XHProf
Hard Coding
Add xhprof_enable() and xhprof_disable() to
specific code paths, this helps focus the profile.
Custom Triggered Script
Use auto_prepend_file config directive with some
sort of custom trigger
Read the Documentation
How you store the profile run results depends on
the frontend you chose and how you configured it
Where to Profile
Manual Performance Tests
Run your application through performance testing locally or in a staging environment
Automated Performance Tests
Next level. Set up automated tests on a fixed hardware platform and alert on performance regressions.
Do it Live!
Add a custom trigger script to a non-customer-facing production machine and profile real data
How to Read a Profile
Call Hierarchy
• Main display is a sortable list of all function calls
• Click on functions to drill down to their children
Fields
• Calls
• Wall time
• Memory usage
• Inclusive vs exclusive
Comparison
Vanilla UI has a feature to compare two different
runs
How to Read a Callgraph
Time Performance
Critical path graph can be helpful, but be careful
Percentage Time
Helps judge overall shape of execution path
Call Count Performance
Validate your assumptions
Big Red Box
Will often make your problem very obvious
Performance Tips
Array Operations
Use Arrays as Hash Sets
• Use array_flip() and isset()/array_key_exists() instead of in_array()
• array_intersect_key() instead of array_intersect()
• Use array union instead of array_merge()
Avoid Nested Iteration
Acceptable on bounded data sets, but be careful. Unbounded data sets will catch you by surprise.
Avoid Unnecessary Repetition
Don’t count() every iteration of a loop. It’s O(1) but it’s still not worth it.
Use Generators/Iterators
Avoid processing huge data sets at once. Process manageable chunks iteratively.
Managing Memory
Be Conservative
Don’t create objects or variables you don’t need. Obvious, I know.
Know Your Stack
Understand the basics of your libraries. They’re not magical black boxes.
Give Yourself Room
PHP’s default memory limit is pretty low. Don’t go nuts, but 128M is not enough for many workloads.
Copy-On-Write
Know how and why PHP copies array structs.
Managing Memory
Avoid Circular References
PHP’s garbage collector is not good with them. Sometimes they’re unavoidable, but be aware.
Avoid Long-Running Processes
PHP is built for short processes and tends to leak memory, so keep it short and fast.
gc_disable()
It worked for Composer!
Avoid Global/Class State
To keep active variable references to a minimum and because it’s a good idea in general
Database Queries (And Other External Calls)
Know Your ORM/DB Driver
It all boils down to SQL queries. Hydration is important, remember to clear the object graph regularly.
Know Your Database
You don’t have to be a DBA, but know the basics: indexes, transactions, prepared statements, easy/lazy
fetching of relations. Consider sorting/aggregation in PHP over the DB.
Batch Queries
Minimize network/transaction overhead by grouping queries in batches.
Caching
Can be complex, but is often very helpful. Have a healthy respect for it.
Other Stuff
Run PHP 7
No drawbacks and huge improvements
When in Doubt, Test
Profiling’s easy once you have it set up
Keep Digging
Don’t be afraid to look at the source
Try Other Tools
Other XHProf frontends, New Relic, Symfony
profiler, Blackfire, Xdebug profiler, etc.
Scenarios and Examples
Getting our hands dirty
Sample Application
Symfony 3 with Doctrine 2
Basic “link shortener” app, find it on Github
Schema
Users have many Links have many LinkClicks
Scale
50 users, 12k Links, 5M LinkClicks
Versions
MySQL 5.7, PHP 7.0
Get Clicks For Today In Order
Problem
Using the DB, get all clicks from today in order.
Assume the timestamp column is not and cannot be
indexed.
Straight Database Query
• Simplest solution
• SELECT * FROM link_click WHERE created_at >
DATE_SUB(CURDATE(), INTERVAL 1 DAY)
ORDER BY created_at DESC;
• Bad performance if not indexed
• What do we expect to see?
Get Clicks For Today In Order
Results
• Filesorts are pretty much always bad
• Alternatives?
Revised Query with PHP Aggregation
• Lose the ORDER BY
• Do sorting on the PHP side
Expectations
Roughly what do we expect to see?
Get Clicks For Today In Order
Results
• Much worse.
• Why?
• What can we do?
Change Hydration Method
• We expect sorting to take a while
• See a lot of time spent calling Doctrine accessors
and performing hydration
• Switch to scalar hydration
Get Clicks For Today In Order
Results
• Decent speedup
• Profile looks more as we expect
• Victory! (mostly)
Next Steps?
• DB index is still the best solution if possible
• Depending on when we do this operation,
consider caching or an alternate data store if we
can’t add an index
• Do we need to fetch the entire day at once?
Consider grabbing smaller time windows.
Get Clicks By User
Problem
Excluding DB interactions this time, given an array
of clicks, links, and user ids, group the clicks by user
id. Note that LinkClick does not have a direct
user_id reference. DB JOIN is the best solution, but
ignore that
Naïve Approach
For each link click, find the user id from the array of
links and shove the click into an associative array
Get Clicks By User
Result
• Terrible
• What’s the O(n) of this?
• What can we improve?
Avoid User Id Lookup
• One pass through Links to set up a mapping from
link_id to user_id to make lookups O(1)
• Should be much less terrible
Get Clicks By User
Result
• Much better
• Is this good enough?
• What else can we do?
Further Improvements
• Avoid in_array() call
• Avoid expensive ORM calls
Get Clicks By User
Result
• Not much better, but no drawbacks
• Would be more significant if there were a larger
number of user ids
• Doctrine 2’s scalar hydrator doesn’t include
foreign keys, would need more digging
• Victory?
Questions
And maybe answers, or maybe ignorance and snark
Thank Y u
Please review me on https://joind.in/talk/f45f2

Performance Tuning with XHProf

  • 1.
    Performance Tuning withXHProf Dan van Kley Computerologist Salesforce Pardot dvankley@salesforce.com
  • 3.
    Who I Am PHPConvert Recovering .NET developer Backend Engineer This isn’t a Javascript conference anyway Lover of Automation Automate all the things Very Lazy and Slightly Paranoid Not much of a web presence, just send me an email.
  • 4.
    Who Pardot Is ATLRepresent! We’re right by Lenox, come by and play some ping pong B2B Marketing Automation Division of Salesforce Odds are you don’t care about that part Primary App in PHP • ~600k authenticated requests/day • ~20M API requests/day • ~125M public endpoint requests/day • ~175M background task executions/day Recent PHP 7 Migration It is great and you should use it It’s not French
  • 5.
    Why Performance Matters UserExperience Page load times are critical to user satisfaction Resource Efficiency Do more with less, save $$$ Larger Workloads Scale up without more hardware Understand Your Stack Know what your function calls are actually doing
  • 6.
    Agenda XHProf Basics Performance Tips Scenariosand Examples Hopefully we can get to lunch early
  • 7.
  • 8.
    What XHProf Is PHPExtension Written in C and linked into the runtime to provide low level instrumentation. Hierarchical Profiler Profile is a call hierarchy, not a strict “graph”. Callgraph does a better job of modeling. Simple Frontend Ships with a basic frontend, many other options available as well. Choose your own adventure! Measurements per Function: • Number of times called • Inclusive/exclusive memory usage • Inclusive/exclusive execution time
  • 9.
    How to InstallXHProf Original Project Abandoned Facebook runs HHVM now so they don’t care about vanilla PHP profiling anymore PHP 5.x Original or Phacility’s fork PHP 7.x Older versions may work, if not consider Tideways or longxinH forks Installation You will need to compile forks from source. Homebrew/PECL has the old Facebook version.
  • 10.
    How to InstallXHProf Configuration • Include the XHProf extension in your php.ini • Verify it loads with phpinfo() or php –i • Configure xhprof.output_dir Set up your Frontend • Facebook’s original UI is simple and works well, that’s what I’m using • Original UI is a simple PHP app contained in xhprof_html, just point your server at it • There are tons of other frontend options
  • 11.
    How to RunXHProf Hard Coding Add xhprof_enable() and xhprof_disable() to specific code paths, this helps focus the profile. Custom Triggered Script Use auto_prepend_file config directive with some sort of custom trigger Read the Documentation How you store the profile run results depends on the frontend you chose and how you configured it
  • 12.
    Where to Profile ManualPerformance Tests Run your application through performance testing locally or in a staging environment Automated Performance Tests Next level. Set up automated tests on a fixed hardware platform and alert on performance regressions. Do it Live! Add a custom trigger script to a non-customer-facing production machine and profile real data
  • 13.
    How to Reada Profile Call Hierarchy • Main display is a sortable list of all function calls • Click on functions to drill down to their children Fields • Calls • Wall time • Memory usage • Inclusive vs exclusive Comparison Vanilla UI has a feature to compare two different runs
  • 14.
    How to Reada Callgraph Time Performance Critical path graph can be helpful, but be careful Percentage Time Helps judge overall shape of execution path Call Count Performance Validate your assumptions Big Red Box Will often make your problem very obvious
  • 15.
  • 16.
    Array Operations Use Arraysas Hash Sets • Use array_flip() and isset()/array_key_exists() instead of in_array() • array_intersect_key() instead of array_intersect() • Use array union instead of array_merge() Avoid Nested Iteration Acceptable on bounded data sets, but be careful. Unbounded data sets will catch you by surprise. Avoid Unnecessary Repetition Don’t count() every iteration of a loop. It’s O(1) but it’s still not worth it. Use Generators/Iterators Avoid processing huge data sets at once. Process manageable chunks iteratively.
  • 17.
    Managing Memory Be Conservative Don’tcreate objects or variables you don’t need. Obvious, I know. Know Your Stack Understand the basics of your libraries. They’re not magical black boxes. Give Yourself Room PHP’s default memory limit is pretty low. Don’t go nuts, but 128M is not enough for many workloads. Copy-On-Write Know how and why PHP copies array structs.
  • 18.
    Managing Memory Avoid CircularReferences PHP’s garbage collector is not good with them. Sometimes they’re unavoidable, but be aware. Avoid Long-Running Processes PHP is built for short processes and tends to leak memory, so keep it short and fast. gc_disable() It worked for Composer! Avoid Global/Class State To keep active variable references to a minimum and because it’s a good idea in general
  • 19.
    Database Queries (AndOther External Calls) Know Your ORM/DB Driver It all boils down to SQL queries. Hydration is important, remember to clear the object graph regularly. Know Your Database You don’t have to be a DBA, but know the basics: indexes, transactions, prepared statements, easy/lazy fetching of relations. Consider sorting/aggregation in PHP over the DB. Batch Queries Minimize network/transaction overhead by grouping queries in batches. Caching Can be complex, but is often very helpful. Have a healthy respect for it.
  • 20.
    Other Stuff Run PHP7 No drawbacks and huge improvements When in Doubt, Test Profiling’s easy once you have it set up Keep Digging Don’t be afraid to look at the source Try Other Tools Other XHProf frontends, New Relic, Symfony profiler, Blackfire, Xdebug profiler, etc.
  • 21.
  • 22.
    Sample Application Symfony 3with Doctrine 2 Basic “link shortener” app, find it on Github Schema Users have many Links have many LinkClicks Scale 50 users, 12k Links, 5M LinkClicks Versions MySQL 5.7, PHP 7.0
  • 23.
    Get Clicks ForToday In Order Problem Using the DB, get all clicks from today in order. Assume the timestamp column is not and cannot be indexed. Straight Database Query • Simplest solution • SELECT * FROM link_click WHERE created_at > DATE_SUB(CURDATE(), INTERVAL 1 DAY) ORDER BY created_at DESC; • Bad performance if not indexed • What do we expect to see?
  • 28.
    Get Clicks ForToday In Order Results • Filesorts are pretty much always bad • Alternatives? Revised Query with PHP Aggregation • Lose the ORDER BY • Do sorting on the PHP side Expectations Roughly what do we expect to see?
  • 32.
    Get Clicks ForToday In Order Results • Much worse. • Why? • What can we do? Change Hydration Method • We expect sorting to take a while • See a lot of time spent calling Doctrine accessors and performing hydration • Switch to scalar hydration
  • 35.
    Get Clicks ForToday In Order Results • Decent speedup • Profile looks more as we expect • Victory! (mostly) Next Steps? • DB index is still the best solution if possible • Depending on when we do this operation, consider caching or an alternate data store if we can’t add an index • Do we need to fetch the entire day at once? Consider grabbing smaller time windows.
  • 36.
    Get Clicks ByUser Problem Excluding DB interactions this time, given an array of clicks, links, and user ids, group the clicks by user id. Note that LinkClick does not have a direct user_id reference. DB JOIN is the best solution, but ignore that Naïve Approach For each link click, find the user id from the array of links and shove the click into an associative array
  • 39.
    Get Clicks ByUser Result • Terrible • What’s the O(n) of this? • What can we improve? Avoid User Id Lookup • One pass through Links to set up a mapping from link_id to user_id to make lookups O(1) • Should be much less terrible
  • 43.
    Get Clicks ByUser Result • Much better • Is this good enough? • What else can we do? Further Improvements • Avoid in_array() call • Avoid expensive ORM calls
  • 44.
    Get Clicks ByUser Result • Not much better, but no drawbacks • Would be more significant if there were a larger number of user ids • Doctrine 2’s scalar hydrator doesn’t include foreign keys, would need more digging • Victory?
  • 45.
    Questions And maybe answers,or maybe ignorance and snark
  • 46.
    Thank Y u Pleasereview me on https://joind.in/talk/f45f2

Editor's Notes

  • #6 - USer conversion rates
  • #25 The key here is ”Using filesort”, which means the index isn’t being used to satisfy the ORDER BY. This has a pretty major detrimental impact on performance. What can we do to get around this?
  • #26 This is the first run so this is just a baseline, but we can see that the actual PDOStatement::execute (the DB call) takes a substantial amount of exclusive time. We also note how much time is spent on the ORM itself.
  • #27 Same information as the previous slide, presented differently. Note the emphasis on the critical path and the two longest running functions.
  • #30 Ok, we got rid of the “using filesort,” the DB query should be much faster.
  • #31 The DB query is marginally faster… but the overall time is much, much longer. We expected and improvement and instead we got a regression. What went wrong?
  • #32 Sorting client side is taking a while, maybe this wasn’t such a great idea. But we also see a ton of work being done by the Doctrine hydrator, as well as the Doctrine accessor on the records.
  • #34 Things are much better. Actual DB query time is still in the same range, but we’ve trimmed a massive amount of ORM overhead. Saving DB load may be worth it
  • #35 This is more what we expect, the DB query and sorting using up most of our execution time.
  • #38 Even as a first run we can see this is pretty terrible at 141 seconds. What was our expectation? How do the numbers compare (100k clicks) What’s the culprit here? Notice the ridiculous number of function calls? What complexity is that?
  • #39 Tons of function calls taking forever.
  • #41 Ok, this is more like it. We’re down to O(n), which is way more reasonable. This doesn’t look too bad.
  • #42 Still spending a while on accessors, querying links, and in_array.
  • #43 Aww yiss