Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

MySQL query performance meten met Runkit

Het vorige artikel ging over operator overloading, een extensie die ik had gevonden via een weblog item. In het zelfde item wordt ook gesproken over Runkit, dat overigens van dezelfde auteur komt.

Met Runkit kun je functies, klassen, constanten, methodes, kortom, alles wat tot voorkort statisch was, wijzigen.That's right: functie definities runtime wijzigen. In dit artikeltje gebruik ik runkit gebruiken om de mysql query performance te meten van een bestaand script, zonder dit script zelf aan te passen.

Installatie

Allereerst zul je runkit moeten installeren. Runkit is te downloaden of direct te installeren via PECL:

CODE:
  1. $ pecl install runkit-beta

Voeg vervolgens de juiste extension declaratie toe aan php.ini:

CODE:
  1. extension="runkit.so"
  2. runkit.override_internal = "1"

Deze 2e configuratie optie stelt je in staat om interne PHP functies te overschrijven. Uit veiligheids overwegingen is dit standaard uitgeschakeld. Als laatste even Apache herstarten als dat nodig is, en klaar is klara.

De volgende stap is dan het schrijven van een script dat de performance kan meten. Het doel is om op simpele wijze en zonder de daadwerkelijke code van het script te wijzigen een overzicht te krijgen van de queries die worden uitgevoerd en hoe lang deze duurden.

Het voorbeeld script

Als voorbeeld zullen we een zeer simpel script gebruiken:

PHP:
  1. $db  = mysqli_connect('localhost','test','test', 'scriptorama');
  2. $res = mysqli_query($db, "SELECT * FROM guestbook");
  3.  
  4. while ($row = mysqli_fetch_assoc($res)) {
  5.     echo '<div class="message">';
  6.     echo '<h1>', htmlspecialchars($row['title']), '</h1>';
  7.     echo '<p>', htmlspecialchars($row['inhoud']), '</p>';
  8.     echo '</div>';
  9. }

Opmerking: Ik gebruik MySQLi omdat ik MySQL 5.0 draai op mijn PC, maar je kunt dit trucje ook uithalen met de gewone mysql functies. Daarbij maak ik geen gebruik van de object georienteerde mogelijkheden van MySQLi omdat Runkit daar helaas een probleem van lijkt te maken: ik mag geen interne klassen wijzigen.

Wat we nu willen doen is het naadloos vervangen van mysqli_query() met een alternatieve functie die precies gaat bijhouden welke query hoelang duurde. Nu biedt runkit een stuk of wat functies waar we dat mee kunnen doen:

  • runkit_function_add
  • runkit_function_copy
  • runkit_function_redefine
  • runkit_function_remove
  • runkit_function_rename

Zoals je ziet hebben we genoeg mogelijkheden om de mysqli_query() functie te vervangen met iets dat de executie tijd meet. Mijn eerste gedachte was om runkit_function_rename() om mysqli_query() te hernoemen, zodat we hem nog wel kunnen gebruiken, en vervolgens runkit_function_add() te gebruiken om een nieuwe implementatie van mysqli_query() te leveren. Het lijkt alleen zo dat PHP toch een tikje gevoelig is voor de minder-subtiele ingrepen die runkit doet:

CODE:
  1. $ php test.php
  2. Segmentation fault

Maar na enig spelen ( en enige memory leaks later ), kreeg ik toch het gewenste resultaat door runkit_function_copy() en runkit_function_redefine() te gebruiken. Deze laatste functie verwacht 3 argumenten: de functienaam, de verwachtte argumenten als string en de functie code, ook als string:

PHP:
  1. $functieStr = '
  2.   $begin = array_sum(explode( " ", microtime()));
  3.   $res = mysqli_query_old($link, $query);
  4.   $end = array_sum(explode( " ", microtime()));
  5.   printf(
  6.     "[%s] duurde %0.2f<br />\n",
  7.     $query,
  8.     ($end - $begin)
  9.   );
  10.   return $res;
  11. ';
  12.  
  13. runkit_function_copy('mysqli_query',       'mysqli_query_old');
  14. runkit_function_redefine('mysqli_query', '$link, $query', $functieStr);

Als we deze code nu in DebugRunkit.php plaatsen en deze includeren in onze originele code:

PHP:
  1. require 'DebugRunkit.php';
  2.  
  3. $db  = mysqli_connect('localhost','test','test', 'scriptorama');
  4. $res = mysqli_query($db, "SELECT * FROM guestbook");
  5.  
  6. while ($row = mysqli_fetch_assoc($res)) {
  7.   echo '<div class="message">';
  8.   echo '<h1>', htmlspecialchars($row['title']), '</h1>';
  9.   echo '<p>', htmlspecialchars($row['inhoud']), '</p>';
  10.   echo '</div>';
  11. }

.. krijgen we, zonder dat we de originele code hebben hoeven wijzigen (we hebben alleen iets toegevoegd), de execution tijd kado:

CODE:
  1. $ php test.php
  2. [SELECT * FROM guestbook] duurde 0.00&lt;br /&gt;
  3. &lt;div class="message"&gt;&lt;h1&gt;test&lt;/h1&gt;&lt;p&gt;test&lt;/p&gt;&lt;/div&gt;

Conclusie

Met (deze functies van) Runkit kun je leuke trucjes uithalen die, volgens mij, voornamelijk een plek hebben bij het testen, debuggen of optimaliseren van php code. Je zou bijvoorbeeld ook de mail() functie kunnen overriden op jouw testserver, zodat je nooit meer perongeluk 1000 klanten een mailtje stuurt. Kortom: het lijkt mij voornamelijk nuttig voor developer werkzaamheden.

Helaas is de extensie niet geheel stabiel en/of vrij van memory leaks, maar met enigzins prutten moet het toch mogelijk zijn om je doel te behalen.

Reageer ook!

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>