XHProf est un outil de profilage hiérarchique pour PHP. Il relève
38 | les appels au niveau des fonctions et mesure inclusivement et
39 | exclusivement des métriques telles que le temps écoulé
40 | la charge CPU ou l’usage de la mémoire. Un profil de fonction peut être divisé selon ses appelants, ou ses appelés. Le composant qui extrait les données brutes est écrit en C
41 | et implémenté telle une extension PHP Zend.
42 | xhprof. XHProf a une interface utilisateur simple en HTML, (écrite en PHP).
43 | L’interface permet de visualiser et de partager facilement le résultat des profilages dans un navigateur.
44 | Un rendu sous forme de graphique est également disponible.
45 |
46 |
Les rapports fournis par XHProf permettent souvent de mieux comprendre
47 | la structure du code qui est éxécuté.
48 | Le rendu hiérarchique des rapports permet par exemple de déterminer
49 | quelle chaîne d’appels mène à une fonction particulière.
50 |
51 |
XHProf propose également de comparer deux runs (résultat de profilage)
52 | pour analyser les différences ou aggréger les résultat de multiples runs afin
53 | d’analyser des données consolidées.
54 | Les comparaisons et aggrégations de données permettent surtout de visualiser des données plates
55 |
56 |
XHProf est un outil de profilage très léger. Pendant la phase de collecte
57 | Il garde une trace du nombre d’appels et des métriques inclusives viualisables en courbes dans le graphe d’appels dynamique d’un programme.
58 | Il calcule les métriques exclusives dans la phase de rapport.
59 | XHProf supporte les fonctions recursives en détectant les cycles dans la pile d’appels dès la capture des données et en utilisant un nom unique pour l’invocation principale.
60 |
61 |
La nature légère d’XHProf, ses performances et ses possibilités de consolidations de données
62 | en font un outil taillé pour les environnements de production [Voir les notes sur l’usage en production.]
64 |
65 |
66 |
67 |
68 |
XHProfLive (qui ne fait pas partie de ce kit open source), par exemple,
69 | est un système de monitoring de performance utilsé chez Facebook et qui est bâti sur XHProf.
70 | XHProfLive récupère en permanence les données de profilage en production en lançant XHProf sur un échantillon de pages
71 | XHProfLive aggrège ensuite les données suivant des critères tels que le temps, type de pages, et peut aider à répondre à tout type de questions comme :
72 | Quel est le profil de la pile d’appel pour une page spécifique ? Quel est le coût de la méthode "foo" dans toutes les pages, ou sur une page spécifique ? quelles fonctions ont régressé le plus depuis une heure, un jour pou un mois ? Quel est l’historique des tendances, des temps d’executions pour une page ou une fonction …
73 |
74 |
75 |
76 |
77 |
78 |
Développé à l’origine par Facebook, XHProf est maintenant open source depuis mars 2009.
Un résumé des appels de fonctions avec des informations telles que le nombre d’appels,
92 | inclusivement et exclusivement, les temps, la charge mémoire, et le temps processeur.
93 |
94 |
Pour chaque fonction, il fournit le détail des appels et le temps par
98 | parent (appelant) & enfant (appelé), tel que :
99 |
100 |
101 |
102 |
quelle fonctions appelle quelle fonction précisement et combien de fois ?
103 |
104 |
Quelles fonctions font un appel particulier ?
105 |
106 |
Le temps total passé dans une fonction appelé par un parent bien précis.
107 |
108 |
109 |
110 |
Comparateur de rapports
111 |
112 |
Vous pouvez comparer les données de deux appels à XHProf pour des raisons diverses;
113 | Pour voir ce qui cause une régression entre une version du code et une autre,
114 | Pour évaluer l’impact sur les performances d’une évolution dans le code …
115 |
116 |
Une comparaison de rapport prends deux runs en entrée et produit à la fois des informations différencielles au niveau de la fonction, mais aussi des informations hiérarchiques (séparation des différences par fonction parente/enfant) pour chaque fonction.
117 |
118 |
La vue tabulaire (copie d’écran) du rapport différentiel pointe les plus grosses améliorations et régressions.
120 |
121 |
Cliquer sur un nom de fonction dans la bue tabulaire du rapport différentiel, mène à la vue hiérarchique
122 | (ou vue parent/enfant) différentielle d’une fonction (copie d’écran). On peut ainsi avoir une séparation des différences par fonctions parent/enfant.
124 |
125 |
Les données du rapport peuvent également être visualisées sous forme de graphique.
129 | Cette vue permet de mettre en lumière les chemins crtiques du programme.
130 |
131 |
Profilage mémoire
132 |
133 |
Le mode profilage mémoire d’XHProf aide à isoler les fonctions qui occupent trop de mémoire.
134 |
135 |
On ne peut pas dire qu’XHProf trace exactement chaque opération
136 | d’allocation/libération de mémoire, en effet il utilise un schéma simplistique;
137 | Il trace les hausses et les baisse de besoin en mémoire allouée à PHP à chaque entré ou sortie de fonction.
138 | Il trace aussi les hausses et baisses de pics mémoire alloués à chaque fonction PHP.
139 |
140 |
XHProf trace les opération include, include_once, require and
141 | require_once comme si c’était des fonctions. Le nom du fichier inclus est utilisé pour nommer "fausses" fonctions.
143 |
144 |
145 |
146 |
147 |
Terminologie
148 |
149 |
150 |
Temps inclusive (ou temps du sous-ensemble):
151 | Inclus le temps passé dans la fonction et celui passé dans les fonctions descendantes (filles).
152 |
153 |
Temps exclusive (ou temps "propre"): Mesure le temps passé dans la fonction elle-même et n’inclus pas le temps passé dans les fonctions descendantes.
154 |
155 |
Wall Time: Temps passé ou temps ressenti.
156 |
157 |
CPU Time: Charge CPU sur les process utilisateur + charge CPU sur les process noyaux
158 |
159 |
160 |
Convention de nommage pour les fonctions spéciales
161 |
162 |
163 |
main(): Une fonction fictive qui est à la racine de la pile d’appel.
164 |
165 |
166 |
load::<filename>
167 | et run_init::<filename>:
168 |
169 |
XHProf trace les appels include/require comme des appels de fonction.
170 |
171 |
Par exemple, une inclusion include "lib/common.php"; va donner deux entrées pour XHProf :
172 |
173 |
174 |
175 |
load::lib/common.php - Cela représente le travail fait par l’interpréteur pour charger et compiler le fichier.
176 | [Note: Si vous utilisez un cache d’opcode PHP comme APC, alors la compilation intervient uniquement si le cahce est manquant dans APC.]
177 |
178 |
run_init::lib/common.php - Cela répresente le code exécuté au niveau du fichier, soit le résultat de l’inclusion.
179 |
180 |
181 |
182 |
foo@<n>: Implique un appel récursif de foo(), ou <n> représente le niveau de récursion.
183 | Cette récursion peut être directe comme foo() --> foo()), ou indirecte comme foo() --> goo() --> foo().
184 |
185 |
186 |
187 |
188 |
Limitations
189 |
190 |
Un vrai profileur hiérarchique trace toute la pile d’appel pour chaque donnée., et est capables de répondre aux questions comme : Quel était le coût du 3e appel de foo(), ou quel était le coût de bar() quand il était appelé par a()->b()->bar()?
191 |
192 |
193 |
194 |
XHProf garde une trace d’un seul niveau dans le contexte de l’appel et est seulement capable de répondre aux questions à propos
195 | d’une fonction qui regarde un niveau en dessus ou en dessous.
196 | Il appraît que dans la majorité des cas c’est bien suffisant.
197 |
198 |
199 |
Pour mieux comprendre, regaredez l’exemple suivant :
200 |
201 |
202 |
203 | Vous avez:
204 | 1 appel de a() --> c()
205 | 1 appel de b() --> c()
206 | 50 appels de c() --> d()
207 |
208 |
209 |
Quand XHProf peut vous dire que d() a été appelé par c() 50 fois, il ne peut pas vous dire
210 | combien d’appels dont dus à a() ou b().
211 | [On peut imaginer que c’est peut être 25 pour a() et 25 pour b(), mais ce n’est pas nécéssairement vrai.]
212 |
213 |
214 |
De toutes façons en pratique ce n’est pas vraiment une limitation.
215 |
L’extension se trouve dans le sous-répertoire "extension/".
220 |
221 |
222 |
223 |
Note: Le portage pour Windows n’est pas encore implémenté. Nous avons testé XHProf sur Linux/FreeBSD.
224 | [NDT : Il existe un fork avec un portage Windows sur Github]
225 |
226 |
La version 0.9.2 et les précédentes sont aussi censées fonctionner sur Mac
227 | OS. [Cela a été testé sur Mac OS 10.5.]
228 |
229 |
Note: XHProf utilise les insctructions RDTSC (time stamp counter)
230 | pour implémenter un compteur de temps vraiment bas niveau. C’est pourquoi actuellement xhprof fonctionne uniquement sur une architecture x86.
231 | Aussi tant que les valeurs de RDTSC ne pourront pas être synchronisées entre plusieurs CPUs,
232 | xhprof n’en utilisera qu’un seul lors du profilage.
233 |
234 |
Le timer XHProf bzasé sur RDTSC ne fonctionen pas parfaitement si la techno
235 | SpeedStep est activée. Cette technologie est disponible sur certains processeurs Intel.
236 | [Note: Les Macs ont typiquement cette fonctionnalité d’activée par défaut, il faut donc la désactiver pour utiliser XHProf.]
237 |
238 |
239 |
240 |
Les étapes suivantes sont prévues pour un environnement Linux/Unix.
241 |
242 |
243 |
244 | % cd <repertoire_source_xhprof>/extension/
245 | % phpize
246 | % ./configure --with-php-config=<chemin vers php-config>
247 | % make
248 | % make install
249 | % make test
250 |
251 |
252 |
253 |
php.ini file: Vous pouvez mettre à jour votre fichier
254 | php.ini file afin qu’il charge automatiquement votre extension en ajoutant le code suivant :
255 |
256 |
257 | [xhprof]
258 | extension=xhprof.so
259 | ;
260 | ; répertoire utilisé par l’implémentation par défaut de l’interface iXHProfRuns
261 | ; (nommée, XHProfRuns_Default class) pour stocker les runs XHProf.
262 | ;
263 | xhprof.output_dir=<repertoire_pour_stocker_les_runs_xhprof>
264 |
Note: Les données brutes contienent uniquement les métriques inclusives.
354 | Par exemple les données brutes du tableau de données temporelles represente les temps inclusifs en microsecondes.
355 | Les temps exclusifs sont calculés pour chaque fonction lors de la phase d’analyse et de rapport.
356 |
357 |
Note: Par défault suelemnt le nombre d’appel & et le temps passé sont profilés.
358 | Vous pouvez aussi profilerle temps CPU et/ou la charge mémoire. Remplacez,
359 |
360 |
361 | xhprof_enable();
362 |
363 | dans le programme précédent avec, par exemple :
364 |
Éviter les fonctions natives lors du profilage
431 |
432 |
Par défault les fonctions natives de PHP (comme strlen) sont profilées.
433 | Si vous ne voulez pas les profiler (pour simplifier le résultat et la taille des données brutes générées),
434 | Vous pouvez utiliser le drapeau XHPROF_FLAGS_NO_BUILTINS comme dans l’exemple ci-dessous :
435 |
436 |
437 | // ne pas profiler les fonctions natives
438 | xhprof_enable(XHPROF_FLAGS_NO_BUILTINS);
439 |
440 |
441 |
442 |
Ignorer des fonctions spécfiques lors du profilage (0.9.2 ou plus récent)
443 |
444 |
À partir de la version 0.9.2 d’XHProf, vous pouvez spécifier une liste de
445 | fonctions à ignorer pendant le profilage. Cela vous permet de ne pas prendre en compte par exemple
446 | des fonctions utilisées pour des appels indirects comme call_user_func et call_user_func_array.
447 | Ces fonctions intermédiaires compliquent inutilement la hirarchie des appels et rendent plus ardue l’interprétation des rapports en brouillant les relations parent/enfant.
448 |
449 |
Pour spécifier cette liste de fonctions à ignorer durant le profilage, il suffit d’utiliser le second paramètre (optionnel) de xhprof_enable.
450 | Par exemple,
451 |
452 |
453 |
454 |
455 | // temps passé en profilage; ignore les appels de call_user_func* pendant le profilage
456 | xhprof_enable(0,
457 | array('ignored_functions' => array('call_user_func',
458 | 'call_user_func_array')));
459 |
460 | or,
461 |
462 | // tempas pasé en profilage + profilage mémoire; ignore call_user_func* durant le profilage
463 | xhprof_enable(XHPROF_FLAGS_MEMORY,
464 | array('ignored_functions' => array('call_user_func',
465 | 'call_user_func_array')));
466 |
467 |
l’interface graphique d’XHProf est implémentée en PHP. Le code est divisé en deux sous-répertoires,
479 | xhprof_html/ and xhprof_lib/.
480 |
481 |
Le répertoire xhprof_html contient les 3 pages PHP principales.
482 |
483 |
484 |
index.php: Pour visualiser un run ou un différentiel entre deux runs.
485 |
callgraph.php: Pour visualiser sous la forme de graphique avec un rendu en image.
486 |
typeahead.php: Utilisé implicitement pour les fonctions de gestion de pile sur un rapport XHProf.
487 |
488 |
489 |
Le répertoire xhprof_lib contient le code pour l’analyse et l’affichage.
490 | (calcul sur les informations de profilage, calcul des différentiels, aggrégation de données, etc.).
491 |
492 |
Configuration du server web : Vous devez vous assurer que le répertoire
493 | xhprof_html/ est accessible depuis le serveur web, et qu’il est configuré pour éxécuter des scripts PHP.
494 |
495 |
Gérer les runs XHProf
496 |
497 |
Les clients web ont une certaine souplesse dans la manière de sauvegarder les données brutes fournies par XHProf.
498 | XHProf expose une interface utilisateur nommée iXHProfRuns (voir xhprof_lib/utils/xhprof_runs.php) que les clients peuvent implémenter.
499 | Cela permet aux clients de préciser comment afficher les donées des runs.
500 |
501 |
L’interface utilisateur d’XHProf fournit une implementation par défaut nommée,
502 | "XHProfRuns_Default" (aussi dans xhprof_lib/utils/xhprof_runs.php).
503 | L’implementation par d"faut stocke les runs dans le répertoire définit par le paramètre INI :
504 | xhprof.output_dir.
505 |
506 |
Un run XHProf doit être définit de manière unique par un espace de nom et un identifiant de run.
507 |
508 |
a) Sauver les données XHProf de façon persistente :
509 |
510 |
Soit si vous utilisez l’interface par défaut,
511 | XHProfRuns_Default qui implémente
512 | iXHProfRuns, Un run XHProf sauvegardé ressemble au code suivant :
513 |
514 |
515 |
516 | // début du profilage
517 | xhprof_enable();
518 |
519 | // lancement du programme
520 | ...
521 |
522 | // fin du profilage
523 | $xhprof_data = xhprof_disable();
524 |
525 | //
526 | // Sauvegarde du run XHProf
527 | // en utilisant l’implementation par défaut de iXHProfRuns.
528 | //
529 | include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_lib.php";
530 | include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_runs.php";
531 |
532 | $xhprof_runs = new XHProfRuns_Default();
533 |
534 | // sauvegarde du run avec l’espace de nom "xhprof_foo".
535 | //
536 | // **NOTE**:
537 | // par défault save_run() va automatiquement générer un identifiant de run
538 | // unique. [Vous pouvez surcharger cette donnée en passant l’identifiant en paramètre optionnel
539 | // à la méthode save_run().]
540 | //
541 | $run_id = $xhprof_runs->save_run($xhprof_data, "xhprof_foo");
542 |
543 | echo "---------------\n".
544 | "En partant du principe que vous avez parametré l’interface utilisateur http \n".
545 | "XHProf, vous pouvez visualiser les runs avec l’adresse : \n".
546 | "http://<adresse-interface-utilisateur-xhprof>/index.php?run=$run_id&source=xhprof_foo\n".
547 | "---------------\n";
548 |
549 |
550 |
551 |
La suite permet de sauvegarder le run sous forme d’un fichier dans le répertoire spécifié
552 | par le paramètre ini xhprof.output_dir. Le nom du fichier doit être de la forme
553 | 49bafaa3a3f66.xhprof_foo; Les deux parties du nom sont formées par l’identifiant du run
554 | ("49bafaa3a3f66") et l’espace de nom ("xhprof_foo"). [Si vous souhaitez créer un identifiant de run vous-même
555 | (comme une sequence de base de données, ou un timestamp), vous pouvez explicitementpasser l’identifiant
556 | du run à la méthode save_run.
557 |
558 |
b) En utilisant votre propre implementation d’iXHProfRuns
559 |
560 |
Si vous décidez de stocker différement les runs XHProf
561 | (soit dans un format compressé, dans une base de données,
562 | etc.), vous aurez besoin d’implémenter une classe qui implémente l’interface
563 | iXHProfRuns().
564 |
565 |
Vous devrez également modifier les 3 pages PHP d’entrée (index.php,
566 | callgraph.php, typeahead.php) dans le répertoire "xhprof_html/" pour utiliser la
567 | nouvelle interface au lieu de celle par défaut (XHProfRuns_Default),
568 | changez cette ligne dans les 3 fichier.
569 |
570 |
571 | $xhprof_runs_impl = new XHProfRuns_Default();
572 |
573 |
574 |
Vous aurez aussi besoin d’inclure le fichier qui implémente votre classe dans les fichiers cités.
575 |
576 |
Acceéder aux runs depuis l’interface utilisateur
577 |
578 |
a) Voir un rapport simple
579 |
580 |
Pour voir un rapport avec l’identifiant <run_id> et l’espace de nom
581 | <namespace> utilisez une url de la forme :
582 |
583 |
Vous pouvez aussi spécifier un ensemble de runspour lesquels vous souhaitez un rapport d’aggrégation.
604 |
605 |
Si vous avez trois runs XHProf avec les identifiants 1, 2 & 3 pour l’espace de noms
606 | "benchmark". Pour voir l’aggrégation de ces trois runs :
607 |
608 |
Aggrégations pondérées: En supposant que les trois runs
613 | correspondent à trois types de programmes p1.php, p2.php and p3.php
614 | qui occupent chacun respectivement 20%, 30% et 50%. Pour voir un rapport d’aggrégation
615 | pondéré par les poids des runs :
616 |
617 |
Quelques observations qui peuvent faire varier votre expérience :
626 |
627 |
628 |
629 |
Le timer CPU (getrusage) sur Linux peut avoir des dépassements de capacité. Il a également un rendu granuleux
630 | (Une précision à la milliseconde plutôt qu’à la microseconde) pour être efficace au niveau des méthodes.
631 | En conséquence, les valeurs rapportées en utilisant le mode XHPROF_FLAGS_CPU on tendance à être plus élevés.
632 |
633 |
Nous recommandons d’utiliser le mode de profilage "temps passé" + "memoire" en production.
634 | [Note: Le surplus de temps passé par le mode de profilage mémoire est non significatif.]
635 |
636 |
637 | // profilage du temps passé (par défault) + profilage mémoire
638 | xhprof_enable(XHPROF_FLAGS_MEMORY);
639 |
640 |
641 |
642 |
643 |
Profiler une plage aléatoire de pages/requêtes est efficace pour récupérer des données représentatives
644 | de votre environnement de production.
645 |
646 |
Pour profiler 1/10000 de vos requêtes, définissez le début du profilage avec un code dans l’esprit de celui-ci :
647 |
648 |
À la fin de la requête (ou dans une fonction de finalisation de la requête), vous pouvez faire quelque chose comme :
656 |
657 |
658 | if ($xhprof_on) {
659 | // fin du profilage
660 | $xhprof_data = xhprof_disable();
661 |
662 | // sauvegarde $xhprof_data quelquepart (base de données centralisée …)
663 | ...
664 | }
665 |
666 |
667 |
Vous pouvez alors récupérer et aggréger ces profilages par horaire
668 | (par exemple 5 minutes, par jour, par jour …), par page ou type de requête, ou n’importe quel
669 | paramètre utilisé par xhprof_aggregate_runs().
670 |
671 |
672 |
673 |
L’extension XHProf propose aussi un mode très léger d’échantillonage.
676 | L’intervalle est de 0,1 seconde. Les échantillons enregistrent l’ensemble des données.
677 | Ce mode peut être très utile pour avoir un impact le plus négligeable possible, et permettre
678 | Le mode sample peut être utile si vous désirez un moyen avec peu de dépassement de faire de la surveillance de performances et des diagnostics.
679 |
680 |
Les très pertinentes fonctions utilisées par l’extension pour utiliser le mode
681 | d’échantillonage sont xhprof_sample_enable() et xhprof_sample_disable().
682 |
683 |
[TBD: Documentation plus détaillée pour le mode d’échantillonage.]
684 |
685 |
Le fichier XHProf_lib/utils/xhprof_lib.php contient
688 | des librairies de fonctions additionellesqui peuvent être utilisées pour manipuler
689 | et aggréger les runs XHProf.
690 |
691 |
Par exemple:
692 |
693 |
694 |
695 |
696 |
xhprof_aggregate_runs():
697 | peut être utilisé pour aggréger de multiples runs XHProf runs dans un seul run.
698 | Cela peut être très utile pour fabriquer un outil de monitoring utilisant XHProf et à l’échelle voulue.
699 | [Par exemple, vous pouvez mixer des runs XHProf issus périodiquement
700 | d’échantillonage de la production pour générer des rapport journalier.]
701 |
702 |
xhprof_prune_run(): Aggréger une grande quantité de runs
703 | (particulièrement si ils correspondent à des zones différentes du programme) peut créer un rendu
704 | graphique beaucoup trop gros. Vous pouvez donc utiliser la fonction xhprof_prune_run
705 | élaguer les données à afficher. En supprimant des branches qui compte pour une partie négligeable du temps passé.
706 |
707 |
JQuery Javascript: Pour les bulles d’aides et les noms de fonctions de pile,
717 | nous utilisons la librairie Javascript, JQuery. JQuery est disponible sous les licences MIT et GPL
718 | (http://docs.jquery.com/Licensing). Ce code JQuery, utilisé par XHProf, se trouve dans le
719 | sous répertoire xhprof_html/jquery.
720 |
721 |
dot (utilitaire de génération d’image): Le fonctionnalité de rendu graphique
722 | ([View Callgraph]) est présente grâce à la présence de Graphviz "dot" dans votre path.
723 | "dot" est un utilitaire de dessin et de gén"ration d’image.
724 |
725 |
Le rendu HTML et l’interface de navigation pour consulter les résultat du profilage sont inspirés par un outil similaire
729 | qui existe pour les procédures stockées PL/SQL d’Oracle. Mais c’est là que la comparaison s’arrête;
730 | Le fonctionnement interne du profileur étant assez différent
731 |
732 | [NDT : Merci à Rudy Rigot (@rudyrigot) pour sa relecture attentive ]
733 |
XHProf is a hierarchical profiler for PHP. It reports
35 | function-level call counts and inclusive and
36 | exclusive metrics such as wall (elapsed)
37 | time, CPU time and memory usage. A function's profile can be broken
38 | down by callers or callees. The raw data collection component is
39 | implemented in C as a PHP Zend extension called
40 | xhprof. XHProf has a simple HTML based user
41 | interface (written in PHP). The browser based UI for viewing profiler
42 | results makes it easy to view results or to share results with peers.
43 | A callgraph image view is also supported.
44 |
45 |
XHProf reports can often be helpful in understanding the structure
46 | of the code being executed. The hierarchical nature of the reports can
47 | be used to determine, for example, what chain of calls led to a
48 | particular function getting called.
49 |
50 |
XHProf supports ability to compare two runs (a.k.a. "diff" reports)
51 | or aggregate data from multiple runs. Diff and aggregate reports, much
52 | like single run reports, offer "flat" as well as "hierarchical" views
53 | of the profile.
54 |
55 |
XHProf is a light-weight instrumentation based profiler. During the
56 | data collection phase, it keeps track of call counts and inclusive
57 | metrics for arcs in the dynamic callgraph of a program. It computes
58 | exclusive metrics in the reporting/post processing phase. XHProf
59 | handles recursive functions by detecting cycles in the callgraph at
60 | data collection time itself and avoiding the cycles by giving unique
61 | depth qualified names for the recursive invocations.
62 |
63 |
64 |
XHProf's light-weight nature and aggregation capabilities make it
65 | well suited for collecting "function-level" performance statistics
66 | from production environments. [See additional notes for use in production.]
68 |
69 |
70 |
71 |
72 |
XHProfLive (not part of the open source kit), for example, is a
73 | system-wide performance monitoring system in use at Facebook that is
74 | built on top of XHProf. XHProfLive continually gathers function-level
75 | profiler data from production tier by running a sample of page
76 | requests under XHProf. XHProfLive then aggregates the profile data
77 | corresponding to individual requests by various dimensions such as
78 | time, page type, and can help answer a variety of questions such as:
79 | What is the function-level profile for a specific page? How expensive
80 | is function "foo" across all pages, or on a specific page? What
81 | functions regressed most in the last hour/day/week? What is the
82 | historical trend for execution time of a page/function? and so on.
83 |
84 |
85 |
86 |
87 |
88 |
Originally developed at Facebook, XHProf was open sourced in Mar, 2009.
For each function, it provides a breakdown of calls and times per
108 | parent (caller) & child (callee), such as:
109 |
110 |
111 |
112 |
what functions call a particular function and how many times?
113 |
114 |
what functions does a particular function call?
115 |
116 |
The total time spent under a function when called from a particular parent.
117 |
118 |
119 |
120 |
Diff Reports
121 |
122 |
You may want to compare data from two XHProf runs for various
123 | reasons-- to figure out what's causing a regression between one
124 | version of the code base to another, to evaluate the performance
125 | improvement of a code change you are making, and so on.
126 |
127 |
A diff report takes two runs as input and provides both flat
128 | function-level diff information, and hierarchical information
129 | (breakdown of diff by parent/children functions) for each function.
130 |
131 |
The "flat" view (sample screenshot) in the diff report points out the top
133 | regressions & improvements.
134 |
135 |
Clicking on functions in the "flat" view of the diff report, leads
136 | to the "hierarchical" (or parent/child) diff view of a function (sample screenshot). We can get a
138 | breakdown of the diff by parent/children functions.
139 |
140 |
141 |
The profile data can also be viewed as a callgraph. The callgraph
145 | view highlights the critical path of the program.
146 |
147 |
148 |
Memory Profile
149 |
150 |
XHProf's memory profile mode helps track functions that
151 | allocate lots of memory.
152 |
153 |
It is worth clarifying that that XHProf doesn't strictly track each
154 | allocation/free operation. Rather it uses a more simplistic
155 | scheme. It tracks the increase/decrease in the amount of memory
156 | allocated to PHP between each function's entry and exit. It also
157 | tracks increase/decrease in the amount of peak memory allocated to
158 | PHP for each function.
159 |
160 |
XHProf tracks include, include_once, require and
161 | require_once operations as if they were functions. The name of
162 | the file being included is used to generate the name for these "fake" functions.
164 |
165 |
166 |
167 |
168 |
Terminology
169 |
170 |
171 |
Inclusive Time (or Subtree Time):
172 | Includes time spent in the function as well as in descendant functions
173 | called from a given function.
174 |
175 |
Exclusive Time/Self Time: Measures
176 | time spent in the function itself. Does not include time in descendant
177 | functions.
178 |
179 |
Wall Time: a.k.a. Elapsed time or wall clock time.
180 |
181 |
CPU Time: CPU time in user space + CPU time in kernel space
182 |
183 |
184 |
Naming convention for special functions
185 |
186 |
187 |
main(): a fictitious function that is at the root of the call graph.
188 |
189 |
190 |
load::<filename>
191 | and run_init::<filename>:
192 |
193 |
XHProf tracks PHP include/require operations as
194 | function calls.
195 |
196 |
For example, an include "lib/common.php"; operation will
197 | result in two XHProf function entries:
198 |
199 |
200 |
201 |
load::lib/common.php - This represents the work done by the
202 | interpreter to compile/load the file. [Note: If you are using a PHP
203 | opcode cache like APC, then the compile only happens on a cache miss
204 | in APC.]
205 |
206 |
run_init::lib/common.php - This represents
207 | initialization code executed at the file scope as a result of the
208 | include operation.
209 |
210 |
211 |
212 |
foo@<n>: Implies that this is a
213 | recursive invocation of foo(), where <n> represents
214 | the recursion depth. The recursion may be direct (such as due to
215 | foo() --> foo()), or indirect (such as
216 | due to foo() --> goo() --> foo()).
217 |
218 |
219 |
220 |
221 |
Limitations
222 |
223 |
True hierarchical profilers keep track of a full call stack at
224 | every data gathering point, and are later able to answer questions
225 | like: what was the cost of the 3rd invokation of foo()? or what was
226 | the cost of bar() when the call stack looked like
227 | a()->b()->bar()?
228 |
229 |
230 |
231 |
XHProf keeps track of only 1-level of calling context and is
232 | therefore only able to answer questions about a function looking
233 | either 1-level up or 1-level down. It turns out that in practice this
234 | is sufficient for most use cases.
235 |
236 |
237 |
To make this more concrete, take for instance the following
238 | example.
239 |
240 |
241 |
242 | Say you have:
243 | 1 call from a() --> c()
244 | 1 call from b() --> c()
245 | 50 calls from c() --> d()
246 |
247 |
248 |
While XHProf can tell you that d() was called from c() 50 times, it
249 | cannot tell you how many of those calls were triggered due to a()
250 | vs. b(). [We could speculate that perhaps 25 were due to a() and 25
251 | due to b(), but that's not necessarily true.]
252 |
253 |
254 |
In practice however, this isn't a very big limitation.
255 |
The extension lives in the "extension/" sub-directory.
260 |
261 |
262 |
263 |
Note: A windows port hasn't been implemented yet. We have
264 | tested xhprof on Linux/FreeBSD so far.
265 |
266 |
Version 0.9.2 and above of XHProf is also expected to work on Mac
267 | OS. [We have tested on Mac OS 10.5.]
268 |
269 |
Note: XHProf uses the RDTSC instruction (time stamp counter)
270 | to implement a really low overhead timer for elapsed time. So at the
271 | moment xhprof only works on x86 architecture.
272 | Also, since RDTSC values may not be synchronized across CPUs,
273 | xhprof binds the program to a single CPU during the
274 | profiling period.
275 |
276 |
XHProf's RDTSC based timer functionality doesn't work correctly if
277 | SpeedStep technology is turned on. This technology is available on
278 | some Intel processors. [Note: Mac desktops and laptops typically have
279 | SpeedStep turned on by default. To use XHProf, you'll need to disable
280 | SpeedStep.]
281 |
282 |
283 |
284 |
The steps
285 | below should work for Linux/Unix environments.
286 |
287 |
288 |
289 | % cd <xhprof_source_directory>/extension/
290 | % phpize
291 | % ./configure --with-php-config=<path to php-config>
292 | % make
293 | % make install
294 | % make test
295 |
296 |
297 |
298 |
php.ini file: You can update your
299 | php.ini file to automatically load your extension. Add the following
300 | to your php.ini file.
301 |
302 |
303 | [xhprof]
304 | extension=xhprof.so
305 | ;
306 | ; directory used by default implementation of the iXHProfRuns
307 | ; interface (namely, the XHProfRuns_Default class) for storing
308 | ; XHProf runs.
309 | ;
310 | xhprof.output_dir=<directory_for_storing_xhprof_runs>
311 |
Note: The raw data only contains "inclusive" metrics. For
401 | example, the wall time metric in the raw data represents inclusive
402 | time in microsecs. Exclusive times for any function are computed
403 | during the analysis/reporting phase.
404 |
405 |
Note: By default only call counts & elapsed time is profiled.
406 | You can optionally also profile CPU time and/or memory usage. Replace,
407 |
408 |
409 | xhprof_enable();
410 |
411 | in the above program with, for example:
412 |
Skipping builtin functions during profiling
479 |
480 |
By default PHP builtin functions (such as strlen) are
481 | profiled. If you do not want to profile builtin functions (to either
482 | reduce the overhead of profiling further or size of generated raw
483 | data), you can use the XHPROF_FLAGS_NO_BUILTINS
484 | flag as in for example:
485 |
486 |
487 | // do not profile builtin functions
488 | xhprof_enable(XHPROF_FLAGS_NO_BUILTINS);
489 |
490 |
491 |
492 |
Ignoring specific functions during profiling (0.9.2 or higher)
493 |
494 |
Starting with release 0.9.2 of xhprof, you can tell XHProf to
495 | ignore a specified list of functions during profiling. This allows you
496 | to ignore, for example, functions used for indirect function calls
497 | such as call_user_func and
498 | call_user_func_array. These intermediate functions
499 | unnecessarily complicate the call hierarchy and make the XHProf
500 | reports harder to interpret since they muddle the parent-child
501 | relationship for functions called indirectly.
502 |
503 |
To specify the list of functions to be ignored during profiling
504 | use the 2nd (optional) argument to xhprof_enable.
505 | For example,
506 |
507 |
The XHProf UI is implemented in PHP. The code resides in two
534 | subdirectories, xhprof_html/ and xhprof_lib/.
535 |
536 |
The xhprof_html directory contains the 3 top-level PHP pages.
537 |
538 |
539 |
index.php: For viewing a single run or diff report.
540 |
callgraph.php: For viewing a callgraph of a XHProf run as an image.
541 |
typeahead.php: Used implicitly for the function typeahead form
542 | on a XHProf report.
543 |
544 |
545 |
The xhprof_lib directory contains supporting code for
546 | display as well as analysis (computing flat profile info, computing
547 | diffs, aggregating data from multiple runs, etc.).
548 |
549 |
Web server config: You'll need to make sure that the
550 | xhprof_html/ directory is accessible from your web server, and that
551 | your web server is setup to serve PHP scripts.
552 |
553 |
Managing XHProf Runs
554 |
555 |
Clients have flexibility in how they save the XHProf raw data
556 | obtained from an XHProf run. The XHProf UI layer exposes an interface
557 | iXHProfRuns (see xhprof_lib/utils/xhprof_runs.php) that clients can
558 | implement. This allows the clients to tell the UI layer how to fetch
559 | the data corresponding to a XHProf run.
560 |
561 |
The XHProf UI libaries come with a default file based
562 | implementation of the iXHProfRuns interface, namely
563 | "XHProfRuns_Default" (also in xhprof_lib/utils/xhprof_runs.php).
564 | This default implementation stores runs in the directory specified by
565 | xhprof.output_dir INI parameter.
566 |
567 |
A XHProf run must be uniquely identified by a namespace and a run
568 | id.
569 |
570 |
571 |
572 |
a) Saving XHProf data persistently:
573 |
574 |
Assuming you are using the default implementation
575 | XHProfRuns_Default of the
576 | iXHProfRuns interface, a typical XHProf run
577 | followed by the save step might look something like:
578 |
579 |
580 |
581 | // start profiling
582 | xhprof_enable();
583 |
584 | // run program
585 | ....
586 |
587 | // stop profiler
588 | $xhprof_data = xhprof_disable();
589 |
590 | //
591 | // Saving the XHProf run
592 | // using the default implementation of iXHProfRuns.
593 | //
594 | include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_lib.php";
595 | include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_runs.php";
596 |
597 | $xhprof_runs = new XHProfRuns_Default();
598 |
599 | // Save the run under a namespace "xhprof_foo".
600 | //
601 | // **NOTE**:
602 | // By default save_run() will automatically generate a unique
603 | // run id for you. [You can override that behavior by passing
604 | // a run id (optional arg) to the save_run() method instead.]
605 | //
606 | $run_id = $xhprof_runs->save_run($xhprof_data, "xhprof_foo");
607 |
608 | echo "---------------\n".
609 | "Assuming you have set up the http based UI for \n".
610 | "XHProf at some address, you can view run at \n".
611 | "http://<xhprof-ui-address>/index.php?run=$run_id&source=xhprof_foo\n".
612 | "---------------\n";
613 |
614 |
615 |
616 |
The above should save the run as a file in the directory specified
617 | by the xhprof.output_dir INI parameter. The file's
618 | name might be something like
619 | 49bafaa3a3f66.xhprof_foo; the two parts being the
620 | run id ("49bafaa3a3f66") and the namespace ("xhprof_foo"). [If you
621 | want to create/assign run ids yourself (such as a database sequence
622 | number, or a timestamp), you can explicitly pass in the run id to the
623 | save_run method.
624 |
625 |
626 |
b) Using your own implementation of iXHProfRuns
627 |
628 |
If you decide you want your XHProf runs to be stored differently
629 | (either in a compressed format, in an alternate place such as DB,
630 | etc.) database, you'll need to implement a class that implements the
631 | iXHProfRuns() interface.
632 |
633 |
You'll also need to modify the 3 main PHP entry pages (index.php,
634 | callgraph.php, typeahead.php) in the "xhprof_html/" directory to use
635 | the new class instead of the default class XHProfRuns_Default.
636 | Change this line in the 3 files.
637 |
638 |
639 | $xhprof_runs_impl = new XHProfRuns_Default();
640 |
641 |
642 |
You'll also need to "include" the file that implements your class in
643 | the above files.
644 |
645 |
646 |
Accessing runs from UI
647 |
648 |
a) Viewing a Single Run Report
649 |
650 |
To view the report for run id say <run_id> and namespace
651 | <namespace> use a URL of the form:
652 |
653 |
Weighted aggregations: Further suppose that the above three runs
684 | correspond to three types of programs p1.php, p2.php and p3.php that
685 | typically occur in a mix of 20%, 30%, 50% respectively. To view an
686 | aggregate report that corresponds to a weighted average of these runs
687 | using:
688 |
689 |
Some observations/guidelines. Your mileage may vary:
699 |
700 |
701 |
702 |
CPU timer (getrusage) on Linux has high overheads. It is also
703 | coarse grained (millisec accuracy rather than microsec level) to be
704 | useful at function level. Therefore, the skew in reported numbers
705 | when using XHPROF_FLAGS_CPU mode tends to be higher.
706 |
707 |
We recommend using elapsed time + memory profiling mode in
708 | production. [Note: The additional overhead of memory profiling
709 | mode is really low.]
710 |
711 |
At the end of the request (or in a request shutdown function), you might
732 | then do something like:
733 |
734 |
735 | if ($xhprof_on) {
736 | // stop profiler
737 | $xhprof_data = xhprof_disable();
738 |
739 | // save $xhprof_data somewhere (say a central DB)
740 | ...
741 | }
742 |
743 |
744 |
You can then rollup/aggregate these individual profiles by time
745 | (e.g., 5 minutely/hourly/daily basis), page/request type,or other
746 | dimensions using xhprof_aggregate_runs().
747 |
748 |
749 |
750 |
751 |
The xhprof extension also provides a very light weight sampling
754 | mode. The sampling interval is 0.1 secs. Samples record the full
755 | function call stack. The sampling mode can be useful if an extremely
756 | low overhead means of doing performance monitoring and diagnostics is
757 | desired.
758 |
759 |
The relevant functions exposed by the extension for using the
760 | sampling mode are xhprof_sample_enable() and
761 | xhprof_sample_disable().
762 |
763 |
764 |
[TBD: more detailed documentation on sampling mode.]
765 |
766 |
767 |
The xhprof_lib/utils/xhprof_lib.php file contains
770 | additional library functions that can be used for manipulating/
771 | aggregating XHProf runs.
772 |
773 |
For example:
774 |
775 |
776 |
777 |
778 |
xhprof_aggregate_runs():
779 | can be used to aggregate multiple XHProf runs into a single run. This
780 | can be helpful for building a system-wide "function-level" performance
781 | monitoring tool using XHProf. [For example, you might to roll up
782 | XHProf runs sampled from production periodically to generate hourly,
783 | daily, reports.]
784 |
785 |
xhprof_prune_run(): Aggregating large number of
786 | XHProf runs (especially if they correspond to different types of
787 | programs) can result in the callgraph size becoming too large. You can
788 | use xhprof_prune_run function to prune the callgraph data
789 | by editing out subtrees that account for a very small portion of the
790 | total time.
791 |
792 |
JQuery Javascript: For tooltips and function name
803 | typeahead, we make use of JQuery's javascript libraries. JQuery is
804 | available under both a MIT and GPL licencse
805 | (http://docs.jquery.com/Licensing). The relevant JQuery code, used by
806 | XHProf, is in the xhprof_html/jquery subdirectory.
807 |
808 |
dot (image generation utility): The callgraph image
809 | visualization ([View Callgraph]) feature relies on the presence of
810 | Graphviz "dot" utility in your path. "dot" is a utility to
811 | draw/generate an image for a directed graph.
812 |
813 |
The HTML-based navigational interface for browsing profiler results
817 | is inspired by that of a similar tool that exists for Oracle's stored
818 | procedure language, PL/SQL. But that's where the similarity ends; the
819 | internals of the profiler itself are quite different.
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/docs/sample-callgraph-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KickPeach/kickPeach/ec8ae05c34e8adeaa02ac210ceaa5a24ad668503/public/xhprof/xhprof_html/docs/sample-callgraph-image.jpg
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/docs/sample-diff-report-flat-view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KickPeach/kickPeach/ec8ae05c34e8adeaa02ac210ceaa5a24ad668503/public/xhprof/xhprof_html/docs/sample-diff-report-flat-view.jpg
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/docs/sample-diff-report-parent-child-view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KickPeach/kickPeach/ec8ae05c34e8adeaa02ac210ceaa5a24ad668503/public/xhprof/xhprof_html/docs/sample-diff-report-parent-child-view.jpg
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/docs/sample-flat-view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KickPeach/kickPeach/ec8ae05c34e8adeaa02ac210ceaa5a24ad668503/public/xhprof/xhprof_html/docs/sample-flat-view.jpg
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/docs/sample-parent-child-view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KickPeach/kickPeach/ec8ae05c34e8adeaa02ac210ceaa5a24ad668503/public/xhprof/xhprof_html/docs/sample-parent-child-view.jpg
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/index.php:
--------------------------------------------------------------------------------
1 | array(XHPROF_STRING_PARAM, ''),
42 | 'wts' => array(XHPROF_STRING_PARAM, ''),
43 | 'symbol' => array(XHPROF_STRING_PARAM, ''),
44 | 'sort' => array(XHPROF_STRING_PARAM, 'wt'), // wall time
45 | 'run1' => array(XHPROF_STRING_PARAM, ''),
46 | 'run2' => array(XHPROF_STRING_PARAM, ''),
47 | 'source' => array(XHPROF_STRING_PARAM, 'xhprof'),
48 | 'all' => array(XHPROF_UINT_PARAM, 0),
49 | );
50 |
51 | // pull values of these params, and create named globals for each param
52 | xhprof_param_init($params);
53 |
54 | /* reset params to be a array of variable names to values
55 | by the end of this page, param should only contain values that need
56 | to be preserved for the next page. unset all unwanted keys in $params.
57 | */
58 | foreach ($params as $k => $v) {
59 | $params[$k] = $$k;
60 |
61 | // unset key from params that are using default values. So URLs aren't
62 | // ridiculously long.
63 | if ($params[$k] == $v[1]) {
64 | unset($params[$k]);
65 | }
66 | }
67 |
68 | echo "";
69 |
70 | echo "XHProf: Hierarchical Profiler Report";
71 | xhprof_include_js_css();
72 | echo "";
73 |
74 | echo "";
75 |
76 | $vbar = ' class="vbar"';
77 | $vwbar = ' class="vwbar"';
78 | $vwlbar = ' class="vwlbar"';
79 | $vbbar = ' class="vbbar"';
80 | $vrbar = ' class="vrbar"';
81 | $vgbar = ' class="vgbar"';
82 |
83 | $xhprof_runs_impl = new XHProfRuns_Default();
84 |
85 | displayXHProfReport($xhprof_runs_impl, $params, $source, $run, $wts,
86 | $symbol, $sort, $run1, $run2);
87 |
88 |
89 | echo "";
90 | echo "";
91 |
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/jquery/indicator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KickPeach/kickPeach/ec8ae05c34e8adeaa02ac210ceaa5a24ad668503/public/xhprof/xhprof_html/jquery/indicator.gif
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/jquery/jquery.autocomplete.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Autocomplete - jQuery plugin 1.0.2
3 | *
4 | * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
5 | *
6 | * Dual licensed under the MIT and GPL licenses:
7 | * http://www.opensource.org/licenses/mit-license.php
8 | * http://www.gnu.org/licenses/gpl.html
9 | *
10 | * Revision: $Id$
11 | *
12 | */
13 |
14 | .ac_results {
15 | padding: 0px;
16 | border: 1px solid black;
17 | background-color: white;
18 | overflow: hidden;
19 | z-index: 99999;
20 | }
21 |
22 | .ac_results ul {
23 | width: 100%;
24 | list-style-position: outside;
25 | list-style: none;
26 | padding: 0;
27 | margin: 0;
28 | }
29 |
30 | .ac_results li {
31 | margin: 0px;
32 | padding: 2px 5px;
33 | cursor: default;
34 | display: block;
35 | /*
36 | if width will be 100% horizontal scrollbar will apear
37 | when scroll mode will be used
38 | */
39 | /*width: 100%;*/
40 | font: menu;
41 | font-size: 12px;
42 | /*
43 | it is very important, if line-height not setted or setted
44 | in relative units scroll will be broken in firefox
45 | */
46 | line-height: 16px;
47 | overflow: hidden;
48 | }
49 |
50 | .ac_loading {
51 | background: white url('indicator.gif') right center no-repeat;
52 | }
53 |
54 | .ac_odd {
55 | background-color: #eee;
56 | }
57 |
58 | .ac_over {
59 | background-color: #0A246A;
60 | color: white;
61 | }
62 |
--------------------------------------------------------------------------------
/public/xhprof/xhprof_html/jquery/jquery.autocomplete.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Autocomplete - jQuery plugin 1.0.2
3 | *
4 | * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
5 | *
6 | * Dual licensed under the MIT and GPL licenses:
7 | * http://www.opensource.org/licenses/mit-license.php
8 | * http://www.gnu.org/licenses/gpl.html
9 | *
10 | * Revision: $Id: jquery.autocomplete.js,v 1.1.1.1 2009-03-17 18:35:18 kannan Exp $
11 | *
12 | */
13 |
14 | ;(function($) {
15 |
16 | $.fn.extend({
17 | autocomplete: function(urlOrData, options) {
18 | var isUrl = typeof urlOrData == "string";
19 | options = $.extend({}, $.Autocompleter.defaults, {
20 | url: isUrl ? urlOrData : null,
21 | data: isUrl ? null : urlOrData,
22 | delay: isUrl ? $.Autocompleter.defaults.delay : 10,
23 | max: options && !options.scroll ? 10 : 150
24 | }, options);
25 |
26 | // if highlight is set to false, replace it with a do-nothing function
27 | options.highlight = options.highlight || function(value) { return value; };
28 |
29 | // if the formatMatch option is not specified, then use formatItem for backwards compatibility
30 | options.formatMatch = options.formatMatch || options.formatItem;
31 |
32 | return this.each(function() {
33 | new $.Autocompleter(this, options);
34 | });
35 | },
36 | result: function(handler) {
37 | return this.bind("result", handler);
38 | },
39 | search: function(handler) {
40 | return this.trigger("search", [handler]);
41 | },
42 | flushCache: function() {
43 | return this.trigger("flushCache");
44 | },
45 | setOptions: function(options){
46 | return this.trigger("setOptions", [options]);
47 | },
48 | unautocomplete: function() {
49 | return this.trigger("unautocomplete");
50 | }
51 | });
52 |
53 | $.Autocompleter = function(input, options) {
54 |
55 | var KEY = {
56 | UP: 38,
57 | DOWN: 40,
58 | DEL: 46,
59 | TAB: 9,
60 | RETURN: 13,
61 | ESC: 27,
62 | COMMA: 188,
63 | PAGEUP: 33,
64 | PAGEDOWN: 34,
65 | BACKSPACE: 8
66 | };
67 |
68 | // Create $ object for input element
69 | var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
70 |
71 | var timeout;
72 | var previousValue = "";
73 | var cache = $.Autocompleter.Cache(options);
74 | var hasFocus = 0;
75 | var lastKeyPressCode;
76 | var config = {
77 | mouseDownOnSelect: false
78 | };
79 | var select = $.Autocompleter.Select(options, input, selectCurrent, config);
80 |
81 | var blockSubmit;
82 |
83 | // prevent form submit in opera when selecting with return key
84 | $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
85 | if (blockSubmit) {
86 | blockSubmit = false;
87 | return false;
88 | }
89 | });
90 |
91 | // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
92 | $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
93 | // track last key pressed
94 | lastKeyPressCode = event.keyCode;
95 | switch(event.keyCode) {
96 |
97 | case KEY.UP:
98 | event.preventDefault();
99 | if ( select.visible() ) {
100 | select.prev();
101 | } else {
102 | onChange(0, true);
103 | }
104 | break;
105 |
106 | case KEY.DOWN:
107 | event.preventDefault();
108 | if ( select.visible() ) {
109 | select.next();
110 | } else {
111 | onChange(0, true);
112 | }
113 | break;
114 |
115 | case KEY.PAGEUP:
116 | event.preventDefault();
117 | if ( select.visible() ) {
118 | select.pageUp();
119 | } else {
120 | onChange(0, true);
121 | }
122 | break;
123 |
124 | case KEY.PAGEDOWN:
125 | event.preventDefault();
126 | if ( select.visible() ) {
127 | select.pageDown();
128 | } else {
129 | onChange(0, true);
130 | }
131 | break;
132 |
133 | // matches also semicolon
134 | case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
135 | case KEY.TAB:
136 | case KEY.RETURN:
137 | if( selectCurrent() ) {
138 | // stop default to prevent a form submit, Opera needs special handling
139 | event.preventDefault();
140 | blockSubmit = true;
141 | return false;
142 | }
143 | break;
144 |
145 | case KEY.ESC:
146 | select.hide();
147 | break;
148 |
149 | default:
150 | clearTimeout(timeout);
151 | timeout = setTimeout(onChange, options.delay);
152 | break;
153 | }
154 | }).focus(function(){
155 | // track whether the field has focus, we shouldn't process any
156 | // results if the field no longer has focus
157 | hasFocus++;
158 | }).blur(function() {
159 | hasFocus = 0;
160 | if (!config.mouseDownOnSelect) {
161 | hideResults();
162 | }
163 | }).click(function() {
164 | // show select when clicking in a focused field
165 | if ( hasFocus++ > 1 && !select.visible() ) {
166 | onChange(0, true);
167 | }
168 | }).bind("search", function() {
169 | // TODO why not just specifying both arguments?
170 | var fn = (arguments.length > 1) ? arguments[1] : null;
171 | function findValueCallback(q, data) {
172 | var result;
173 | if( data && data.length ) {
174 | for (var i=0; i < data.length; i++) {
175 | if( data[i].result.toLowerCase() == q.toLowerCase() ) {
176 | result = data[i];
177 | break;
178 | }
179 | }
180 | }
181 | if( typeof fn == "function" ) fn(result);
182 | else $input.trigger("result", result && [result.data, result.value]);
183 | }
184 | $.each(trimWords($input.val()), function(i, value) {
185 | request(value, findValueCallback, findValueCallback);
186 | });
187 | }).bind("flushCache", function() {
188 | cache.flush();
189 | }).bind("setOptions", function() {
190 | $.extend(options, arguments[1]);
191 | // if we've updated the data, repopulate
192 | if ( "data" in arguments[1] )
193 | cache.populate();
194 | }).bind("unautocomplete", function() {
195 | select.unbind();
196 | $input.unbind();
197 | $(input.form).unbind(".autocomplete");
198 | });
199 |
200 |
201 | function selectCurrent() {
202 | var selected = select.selected();
203 | if( !selected )
204 | return false;
205 |
206 | var v = selected.result;
207 | previousValue = v;
208 |
209 | if ( options.multiple ) {
210 | var words = trimWords($input.val());
211 | if ( words.length > 1 ) {
212 | v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
213 | }
214 | v += options.multipleSeparator;
215 | }
216 |
217 | $input.val(v);
218 | hideResultsNow();
219 | $input.trigger("result", [selected.data, selected.value]);
220 | return true;
221 | }
222 |
223 | function onChange(crap, skipPrevCheck) {
224 | if( lastKeyPressCode == KEY.DEL ) {
225 | select.hide();
226 | return;
227 | }
228 |
229 | var currentValue = $input.val();
230 |
231 | if ( !skipPrevCheck && currentValue == previousValue )
232 | return;
233 |
234 | previousValue = currentValue;
235 |
236 | currentValue = lastWord(currentValue);
237 | if ( currentValue.length >= options.minChars) {
238 | $input.addClass(options.loadingClass);
239 | if (!options.matchCase)
240 | currentValue = currentValue.toLowerCase();
241 | request(currentValue, receiveData, hideResultsNow);
242 | } else {
243 | stopLoading();
244 | select.hide();
245 | }
246 | };
247 |
248 | function trimWords(value) {
249 | if ( !value ) {
250 | return [""];
251 | }
252 | var words = value.split( options.multipleSeparator );
253 | var result = [];
254 | $.each(words, function(i, value) {
255 | if ( $.trim(value) )
256 | result[i] = $.trim(value);
257 | });
258 | return result;
259 | }
260 |
261 | function lastWord(value) {
262 | if ( !options.multiple )
263 | return value;
264 | var words = trimWords(value);
265 | return words[words.length - 1];
266 | }
267 |
268 | // fills in the input box w/the first match (assumed to be the best match)
269 | // q: the term entered
270 | // sValue: the first matching result
271 | function autoFill(q, sValue){
272 | // autofill in the complete box w/the first match as long as the user hasn't entered in more data
273 | // if the last user key pressed was backspace, don't autofill
274 | if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
275 | // fill in the value (keep the case the user has typed)
276 | $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
277 | // select the portion of the value not typed by the user (so the next character will erase)
278 | $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
279 | }
280 | };
281 |
282 | function hideResults() {
283 | clearTimeout(timeout);
284 | timeout = setTimeout(hideResultsNow, 200);
285 | };
286 |
287 | function hideResultsNow() {
288 | var wasVisible = select.visible();
289 | select.hide();
290 | clearTimeout(timeout);
291 | stopLoading();
292 | if (options.mustMatch) {
293 | // call search and run callback
294 | $input.search(
295 | function (result){
296 | // if no value found, clear the input box
297 | if( !result ) {
298 | if (options.multiple) {
299 | var words = trimWords($input.val()).slice(0, -1);
300 | $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
301 | }
302 | else
303 | $input.val( "" );
304 | }
305 | }
306 | );
307 | }
308 | if (wasVisible)
309 | // position cursor at end of input field
310 | $.Autocompleter.Selection(input, input.value.length, input.value.length);
311 | };
312 |
313 | function receiveData(q, data) {
314 | if ( data && data.length && hasFocus ) {
315 | stopLoading();
316 | select.display(data, q);
317 | autoFill(q, data[0].value);
318 | select.show();
319 | } else {
320 | hideResultsNow();
321 | }
322 | };
323 |
324 | function request(term, success, failure) {
325 | if (!options.matchCase)
326 | term = term.toLowerCase();
327 | var data = cache.load(term);
328 | // recieve the cached data
329 | if (data && data.length) {
330 | success(term, data);
331 | // if an AJAX url has been supplied, try loading the data now
332 | } else if( (typeof options.url == "string") && (options.url.length > 0) ){
333 |
334 | var extraParams = {
335 | timestamp: +new Date()
336 | };
337 | $.each(options.extraParams, function(key, param) {
338 | extraParams[key] = typeof param == "function" ? param() : param;
339 | });
340 |
341 | $.ajax({
342 | // try to leverage ajaxQueue plugin to abort previous requests
343 | mode: "abort",
344 | // limit abortion to this input
345 | port: "autocomplete" + input.name,
346 | dataType: options.dataType,
347 | url: options.url,
348 | data: $.extend({
349 | q: lastWord(term),
350 | limit: options.max
351 | }, extraParams),
352 | success: function(data) {
353 | var parsed = options.parse && options.parse(data) || parse(data);
354 | cache.add(term, parsed);
355 | success(term, parsed);
356 | }
357 | });
358 | } else {
359 | // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
360 | select.emptyList();
361 | failure(term);
362 | }
363 | };
364 |
365 | function parse(data) {
366 | var parsed = [];
367 | var rows = data.split("\n");
368 | for (var i=0; i < rows.length; i++) {
369 | var row = $.trim(rows[i]);
370 | if (row) {
371 | row = row.split("|");
372 | parsed[parsed.length] = {
373 | data: row,
374 | value: row[0],
375 | result: options.formatResult && options.formatResult(row, row[0]) || row[0]
376 | };
377 | }
378 | }
379 | return parsed;
380 | };
381 |
382 | function stopLoading() {
383 | $input.removeClass(options.loadingClass);
384 | };
385 |
386 | };
387 |
388 | $.Autocompleter.defaults = {
389 | inputClass: "ac_input",
390 | resultsClass: "ac_results",
391 | loadingClass: "ac_loading",
392 | minChars: 1,
393 | delay: 400,
394 | matchCase: false,
395 | matchSubset: true,
396 | matchContains: false,
397 | cacheLength: 10,
398 | max: 100,
399 | mustMatch: false,
400 | extraParams: {},
401 | selectFirst: true,
402 | formatItem: function(row) { return row[0]; },
403 | formatMatch: null,
404 | autoFill: false,
405 | width: 0,
406 | multiple: false,
407 | multipleSeparator: ", ",
408 | highlight: function(value, term) {
409 | return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1");
410 | },
411 | scroll: true,
412 | scrollHeight: 180
413 | };
414 |
415 | $.Autocompleter.Cache = function(options) {
416 |
417 | var data = {};
418 | var length = 0;
419 |
420 | function matchSubset(s, sub) {
421 | if (!options.matchCase)
422 | s = s.toLowerCase();
423 | var i = s.indexOf(sub);
424 | if (i == -1) return false;
425 | return i == 0 || options.matchContains;
426 | };
427 |
428 | function add(q, value) {
429 | if (length > options.cacheLength){
430 | flush();
431 | }
432 | if (!data[q]){
433 | length++;
434 | }
435 | data[q] = value;
436 | }
437 |
438 | function populate(){
439 | if( !options.data ) return false;
440 | // track the matches
441 | var stMatchSets = {},
442 | nullData = 0;
443 |
444 | // no url was specified, we need to adjust the cache length to make sure it fits the local data store
445 | if( !options.url ) options.cacheLength = 1;
446 |
447 | // track all options for minChars = 0
448 | stMatchSets[""] = [];
449 |
450 | // loop through the array and create a lookup structure
451 | for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
452 | var rawValue = options.data[i];
453 | // if rawValue is a string, make an array otherwise just reference the array
454 | rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
455 |
456 | var value = options.formatMatch(rawValue, i+1, options.data.length);
457 | if ( value === false )
458 | continue;
459 |
460 | var firstChar = value.charAt(0).toLowerCase();
461 | // if no lookup array for this character exists, look it up now
462 | if( !stMatchSets[firstChar] )
463 | stMatchSets[firstChar] = [];
464 |
465 | // if the match is a string
466 | var row = {
467 | value: value,
468 | data: rawValue,
469 | result: options.formatResult && options.formatResult(rawValue) || value
470 | };
471 |
472 | // push the current match into the set list
473 | stMatchSets[firstChar].push(row);
474 |
475 | // keep track of minChars zero items
476 | if ( nullData++ < options.max ) {
477 | stMatchSets[""].push(row);
478 | }
479 | };
480 |
481 | // add the data items to the cache
482 | $.each(stMatchSets, function(i, value) {
483 | // increase the cache size
484 | options.cacheLength++;
485 | // add to the cache
486 | add(i, value);
487 | });
488 | }
489 |
490 | // populate any existing data
491 | setTimeout(populate, 25);
492 |
493 | function flush(){
494 | data = {};
495 | length = 0;
496 | }
497 |
498 | return {
499 | flush: flush,
500 | add: add,
501 | populate: populate,
502 | load: function(q) {
503 | if (!options.cacheLength || !length)
504 | return null;
505 | /*
506 | * if dealing w/local data and matchContains than we must make sure
507 | * to loop through all the data collections looking for matches
508 | */
509 | if( !options.url && options.matchContains ){
510 | // track all matches
511 | var csub = [];
512 | // loop through all the data grids for matches
513 | for( var k in data ){
514 | // don't search through the stMatchSets[""] (minChars: 0) cache
515 | // this prevents duplicates
516 | if( k.length > 0 ){
517 | var c = data[k];
518 | $.each(c, function(i, x) {
519 | // if we've got a match, add it to the array
520 | if (matchSubset(x.value, q)) {
521 | csub.push(x);
522 | }
523 | });
524 | }
525 | }
526 | return csub;
527 | } else
528 | // if the exact item exists, use it
529 | if (data[q]){
530 | return data[q];
531 | } else
532 | if (options.matchSubset) {
533 | for (var i = q.length - 1; i >= options.minChars; i--) {
534 | var c = data[q.substr(0, i)];
535 | if (c) {
536 | var csub = [];
537 | $.each(c, function(i, x) {
538 | if (matchSubset(x.value, q)) {
539 | csub[csub.length] = x;
540 | }
541 | });
542 | return csub;
543 | }
544 | }
545 | }
546 | return null;
547 | }
548 | };
549 | };
550 |
551 | $.Autocompleter.Select = function (options, input, select, config) {
552 | var CLASSES = {
553 | ACTIVE: "ac_over"
554 | };
555 |
556 | var listItems,
557 | active = -1,
558 | data,
559 | term = "",
560 | needsInit = true,
561 | element,
562 | list;
563 |
564 | // Create results
565 | function init() {
566 | if (!needsInit)
567 | return;
568 | element = $("")
569 | .hide()
570 | .addClass(options.resultsClass)
571 | .css("position", "absolute")
572 | .appendTo(document.body);
573 |
574 | list = $("