Merge "Remove require_once from some tests by adding classes to TestsAutoLoader"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 13 Nov 2014 13:03:55 +0000 (13:03 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 13 Nov 2014 13:03:55 +0000 (13:03 +0000)
32 files changed:
RELEASE-NOTES-1.25
includes/AutoLoader.php
includes/api/ApiQuerySearch.php
includes/api/i18n/de.json
includes/api/i18n/fr.json
includes/api/i18n/zh-hans.json
includes/installer/i18n/ckb.json
includes/libs/RunningStat.php
includes/libs/Xhprof.php [new file with mode: 0644]
includes/media/MediaTransformOutput.php
includes/parser/Parser.php
includes/profiler/Profiler.php
includes/profiler/ProfilerSimpleDB.php
includes/profiler/ProfilerSimpleUDP.php
includes/profiler/ProfilerStandard.php
includes/profiler/ProfilerStub.php
includes/profiler/ProfilerXhprof.php [new file with mode: 0644]
includes/profiler/SectionProfiler.php [new file with mode: 0644]
languages/classes/LanguageKk.php
languages/i18n/be-tarask.json
languages/i18n/diq.json
languages/i18n/fr.json
languages/i18n/kiu.json
languages/i18n/pms.json
languages/i18n/pt.json
languages/i18n/ru.json
languages/i18n/rup.json
languages/i18n/scn.json
languages/i18n/tt-cyrl.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
tests/phpunit/includes/libs/XhprofTest.php [new file with mode: 0644]

index b649c2b..7ab8772 100644 (file)
@@ -150,6 +150,9 @@ MediaWiki supports over 350 languages. Many localisations are updated
 regularly. Below only new and removed languages are listed, as well as
 changes to languages because of Bugzilla reports.
 
+* (bug 64440) Kazakh (kk) wikis should no longer forcefully reset the user's
+  interface language to kk where unexpected.
+
 === Other changes in 1.25 ===
 * The skin autodiscovery mechanism, deprecated in MediaWiki 1.23, has been
   removed. See https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery for
index af86c99..92eb4c8 100644 (file)
@@ -699,6 +699,7 @@ $wgAutoloadLocalClasses = array(
        'SwiftVirtualRESTService' => 'includes/libs/virtualrest/SwiftVirtualRESTService.php',
        'VirtualRESTService' => 'includes/libs/virtualrest/VirtualRESTService.php',
        'VirtualRESTServiceClient' => 'includes/libs/virtualrest/VirtualRESTServiceClient.php',
+       'Xhprof' => 'includes/libs/Xhprof.php',
        'XmlTypeCheck' => 'includes/libs/XmlTypeCheck.php',
 
        # includes/libs/cdb
@@ -885,7 +886,9 @@ $wgAutoloadLocalClasses = array(
        'ProfilerSimpleUDP' => 'includes/profiler/ProfilerSimpleUDP.php',
        'ProfilerStandard' => 'includes/profiler/ProfilerStandard.php',
        'ProfilerStub' => 'includes/profiler/ProfilerStub.php',
+       'ProfilerXhprof' => 'includes/profiler/ProfilerXhprof.php',
        'ProfileSection' => 'includes/profiler/ProfileSection.php',
+       'SectionProfiler' => 'includes/profiler/SectionProfiler.php',
        'TransactionProfiler' => 'includes/profiler/TransactionProfiler.php',
 
        # includes/rcfeed
index cc9f9aa..0f33d07 100644 (file)
@@ -116,19 +116,21 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                        $this->dieUsage( $matches->getWikiText(), 'search-error' );
                }
 
-               $apiResult = $this->getResult();
-               // Add search meta data to result
-               if ( isset( $searchInfo['totalhits'] ) ) {
-                       $totalhits = $matches->getTotalHits();
-                       if ( $totalhits !== null ) {
+               if ( $resultPageSet === null ) {
+                       $apiResult = $this->getResult();
+                       // Add search meta data to result
+                       if ( isset( $searchInfo['totalhits'] ) ) {
+                               $totalhits = $matches->getTotalHits();
+                               if ( $totalhits !== null ) {
+                                       $apiResult->addValue( array( 'query', 'searchinfo' ),
+                                               'totalhits', $totalhits );
+                               }
+                       }
+                       if ( isset( $searchInfo['suggestion'] ) && $matches->hasSuggestion() ) {
                                $apiResult->addValue( array( 'query', 'searchinfo' ),
-                                       'totalhits', $totalhits );
+                                       'suggestion', $matches->getSuggestionQuery() );
                        }
                }
-               if ( isset( $searchInfo['suggestion'] ) && $matches->hasSuggestion() ) {
-                       $apiResult->addValue( array( 'query', 'searchinfo' ),
-                               'suggestion', $matches->getSuggestionQuery() );
-               }
 
                // Add the search results to the result
                $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
@@ -151,7 +153,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                        }
 
                        $title = $result->getTitle();
-                       if ( is_null( $resultPageSet ) ) {
+                       if ( $resultPageSet === null ) {
                                $vals = array();
                                ApiQueryBase::addTitleInfo( $vals, $title );
 
@@ -207,7 +209,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                        $hasInterwikiResults = true;
 
                        // Include number of results if requested
-                       if ( isset( $searchInfo['totalhits'] ) ) {
+                       if ( $resultPageSet === null && isset( $searchInfo['totalhits'] ) ) {
                                $totalhits = $matches->getTotalHits();
                                if ( $totalhits !== null ) {
                                        $apiResult->addValue( array( 'query', 'interwikisearchinfo' ),
@@ -218,30 +220,35 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                        $result = $matches->next();
                        while ( $result ) {
                                $title = $result->getTitle();
-                               $vals = array(
-                                       'namespace' => $result->getInterwikiNamespaceText(),
-                                       'title' => $title->getText(),
-                                       'url' => $title->getFullUrl(),
-                               );
-
-                               // Add item to results and see whether it fits
-                               $fit = $apiResult->addValue(
-                                       array( 'query', 'interwiki' . $this->getModuleName(), $result->getInterwikiPrefix()  ),
-                                       null,
-                                       $vals
-                               );
 
-                               if ( !$fit ) {
-                                       // We hit the limit. We can't really provide any meaningful
-                                       // pagination info so just bail out
-                                       break;
+                               if ( $resultPageSet === null ) {
+                                       $vals = array(
+                                               'namespace' => $result->getInterwikiNamespaceText(),
+                                               'title' => $title->getText(),
+                                               'url' => $title->getFullUrl(),
+                                       );
+
+                                       // Add item to results and see whether it fits
+                                       $fit = $apiResult->addValue(
+                                               array( 'query', 'interwiki' . $this->getModuleName(), $result->getInterwikiPrefix()  ),
+                                               null,
+                                               $vals
+                                       );
+
+                                       if ( !$fit ) {
+                                               // We hit the limit. We can't really provide any meaningful
+                                               // pagination info so just bail out
+                                               break;
+                                       }
+                               } else {
+                                       $titles[] = $title;
                                }
 
                                $result = $matches->next();
                        }
                }
 
-               if ( is_null( $resultPageSet ) ) {
+               if ( $resultPageSet === null ) {
                        $apiResult->setIndexedTagName_internal( array(
                                'query', $this->getModuleName()
                        ), 'p' );
index acef17d..70d32e5 100644 (file)
        "apihelp-purge-param-forcelinkupdate": "Aktualisiert die Linktabellen.",
        "apihelp-query+allcategories-description": "Alle Kategorien aufzählen.",
        "apihelp-query+allcategories-param-limit": "Wie viele Kategorien zurückgegeben werden sollen.",
+       "apihelp-query+alldeletedrevisions-param-namespace": "Nur Seiten in diesem Namensraum auflisten.",
        "apihelp-query+allfileusages-param-limit": "Wie viele Gesamtobjekte zurückgegeben werden sollen.",
        "apihelp-query+allfileusages-example-unique": "Einheitliche Dateititel auflisten",
        "apihelp-query+allfileusages-example-generator": "Seiten abrufen, die die Dateien enthalten",
        "apihelp-query+allredirects-example-generator": "Seiten abrufen, die die Weiterleitungen enthalten",
        "apihelp-query+alltransclusions-param-namespace": "Der aufzulistende Namensraum.",
        "apihelp-query+alltransclusions-example-unique": "Einheitlich eingebundene Titel auflisten",
+       "apihelp-query+allusers-param-limit": "Wie viele Benutzernamen insgesamt zurückgegeben werden sollen.",
        "apihelp-query+allusers-example-Y": "Benutzer ab Y auflisten",
        "apihelp-query+backlinks-description": "Alle Seiten finden, die auf die angegebene Seite verlinken.",
        "apihelp-query+backlinks-example-simple": "Links auf [[Hauptseite]] anzeigen",
        "apihelp-query+deletedrevs-param-from": "Auflistung bei diesem Titel beginnen.",
        "apihelp-query+deletedrevs-param-to": "Auflistung bei diesem Titel beenden.",
        "apihelp-query+extlinks-param-limit": "Wie viele Links zurückgegeben werden sollen.",
+       "apihelp-query+exturlusage-param-limit": "Wie viele Seiten zurückgegeben werden sollen.",
+       "apihelp-query+filearchive-example-simple": "Eine Liste aller gelöschten Dateien auflisten",
        "apihelp-query+imageinfo-param-limit": "Wie viele Dateiversionen pro Datei zurückgegeben werden sollen.",
        "apihelp-query+imageinfo-param-start": "Zeitstempel, von dem die Liste beginnen soll.",
        "apihelp-query+imageinfo-param-end": "Zeitstempel, an dem die Liste enden soll.",
index 2077c85..9044f02 100644 (file)
@@ -2,7 +2,8 @@
        "@metadata": {
                "authors": [
                        "Gomoko",
-                       "Windes"
+                       "Windes",
+                       "Orlodrim"
                ]
        },
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [https://www.mediawiki.org/wiki/API:Main_page Documentation]\n* [https://www.mediawiki.org/wiki/API:FAQ FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://bugzilla.wikimedia.org/buglist.cgi?component=API&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts Bogues et demandes]\n</div>\n<strong>État :</strong> Toutes les fonctionnalités affichées sur cette page devraient fonctionner, mais l’API est encore en  cours de développement, et peut changer à tout moment. Inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Demandes erronées :</strong> Qaund des demandes erronés sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error » et à la fois la valeur de l’entête et le code d’erreur retourné prendront la même valeur. Pour plus d’information, voyez https://www.mediawiki.org/wiki/API:Errors_and_warnings.",
        "apihelp-query+imageusage-example-simple": "Afficher les pages utilisant [[:File:Albert Einstein Head.jpg]]",
        "apihelp-query+imageusage-example-generator": "Obtenir des informations sur les pages utilisant [[:File:Albert Einstein Head.jpg]]",
        "apihelp-query+info-description": "Obtenir les informations de base sur la page.",
+       "apihelp-query+info-param-prop": "Quelles propriétés supplémentaires récupérer :\n;protection:Liste de niveau de protection de chaque page.\n;talkid:L’ID de la page de discussion pour chaque page qui n’est pas une page de discussion.\n;watched:Liste de l’état de suivi de chaque page.\n;watchers:Le nombre d’observateurs, si c&est autorisé.\n;notificationtimestamp:L’horodatage de notification de la liste de suivi de chaque page.\n;subjectid:L’ID de la page parente de chaque page de discussion.\n;url:Fournit une URL complète, une URL de modification, et l’URL canonique pour chaque page.\n;readable:Si l’utilisateur peut lire cette page.\n;preload:Fournit le texte renvoyé par EditFormPreloadText.\n;displaytitle:Fournit la manière dont le titre de la page est vraiment affiché.",
+       "apihelp-query+info-param-token": "Utiliser plutôt [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
+       "apihelp-query+info-example-simple": "Obtenir des informations sur [[Main Page]]",
+       "apihelp-query+info-example-protection": "Obtenir des informations générale et de protection sur [[Main Page]]",
+       "apihelp-query+iwbacklinks-description": "Trouver toutes les pages qui ont un lien vers le lien interwiki indiqué.\n\nPeut être utilisé pour trouver tous les liens avec un préfixe, ou tous les liens vers un titre (avec un préfixe donné). N’utiliser aucun paramètre revient en pratique à « tous les liens interwiki ».",
+       "apihelp-query+iwbacklinks-param-prefix": "Préfixe pour l’interwiki.",
+       "apihelp-query+iwbacklinks-param-title": "Lien interwiki à rechercher. Doit être utilisé avec $1blprefix.",
+       "apihelp-query+iwbacklinks-param-limit": "Combien de pages renvoyer.",
+       "apihelp-query+iwbacklinks-param-prop": "Quelles propriétés obtenir :\n;iwprefix:Ajoute le préfixe de l’interwiki.\n;iwtitle:Ajoute le titre de l’interwiki.",
+       "apihelp-query+iwbacklinks-param-dir": "La direction dans laquelle lister.",
+       "apihelp-query+iwbacklinks-example-simple": "Obtenir les pages ayant un lien vers [[wikibooks:Test]]",
+       "apihelp-query+iwbacklinks-example-generator": "Obtenir des informations sur les pages ayant un lien vers [[wikibooks:Test]]",
+       "apihelp-query+iwlinks-description": "Renvoie tous les liens interwiki des pages indiquées.",
+       "apihelp-query+iwlinks-param-url": "S&il faut obtenir l’URL complète (impossible à utiliser avec $1prop).",
+       "apihelp-query+iwlinks-param-prop": "Quelles propriétés supplémentaires obtenir pour chaque lien interlangue :\n;url:Ajoute l’URL complète.",
+       "apihelp-query+iwlinks-param-limit": "Combien de liens interwiki renvoyer.",
+       "apihelp-query+iwlinks-param-prefix": "Renvoyer uniquement les liens interwiki avec ce préfixe.",
+       "apihelp-query+iwlinks-param-title": "Lien interwiki à rechercher. Doit être utilisé avec $1prefix.",
+       "apihelp-query+iwlinks-param-dir": "La direction dans laquelle lister.",
+       "apihelp-query+iwlinks-example-simple": "Obtenir les liens interwiki de [[Main Page]]",
+       "apihelp-query+langbacklinks-description": "Trouver toutes les pages qui ont un lien vers le lien de langue indiqué.\n\nPeut être utilisé pour trouver tous les liens avec un code de langue, ou tous les liens vers un titre (avec une langue donnée). N’utiliser aucun paramètre revient à « tous les liens de langue ».\n\nNotez que cela peut ne pas prendre en compte les liens de langue ajoutés par les extensions.",
+       "apihelp-query+langbacklinks-param-lang": "Langue pour le lien de langue.",
+       "apihelp-query+langbacklinks-param-title": "Lien de langue à rechercher. Doit être utilisé avec $1lang.",
+       "apihelp-query+langbacklinks-param-limit": "Combien de pages renvoyer au total.",
+       "apihelp-query+langbacklinks-param-prop": "Quelles propriétés obtenir :\n;lllang:Ajoute le code de langue du lien de langue.\n;lltitle:Ajoute le titre du lien de langue.",
+       "apihelp-query+langbacklinks-param-dir": "La direction dans laquelle lister.",
+       "apihelp-query+langbacklinks-example-simple": "Obtenir les pages avec un lien avec [[:fr:Test]]",
+       "apihelp-query+langbacklinks-example-generator": "Obtenir des informations sur les pages ayant un lien vers [[:fr:Test]]",
+       "apihelp-query+langlinks-description": "Renvoie tous les liens inter-langue des pages fournies.",
+       "apihelp-query+langlinks-param-limit": "Combien de liens de langue renvoyer.",
+       "apihelp-query+langlinks-param-url": "S’il faut récupérer l’URL complète (impossible à utiliser avec $1prop).",
+       "apihelp-query+langlinks-param-prop": "Quelles propriétés supplémentaires obtenir pour chaque lien inter-langue :\n;url:Ajoute l’URL complète.\n;langname:Ajoute le nom localisé de la langue (au mieux). Utiliser $1inlanguagecode pour contrôler la langue.\n;autonym:Ajoute le nom natif de la langue.",
+       "apihelp-query+langlinks-param-lang": "Renvoyer uniquement les liens de langue avec ce code de langue.",
+       "apihelp-query+langlinks-param-title": "Lien à rechercher. Doit être utilisé avec $1lang.",
+       "apihelp-query+langlinks-param-dir": "La direction à lister.",
+       "apihelp-query+langlinks-param-inlanguagecode": "Code de langue pour les noms de langue localisés.",
+       "apihelp-query+langlinks-example-simple": "Obtenir les liens inter-langue de [[Main Page]]",
+       "apihelp-query+links-description": "Renvoie tous les liens des pages fournies.",
+       "apihelp-query+links-param-namespace": "Afficher les liens uniquement dans ces espaces de nom.",
+       "apihelp-query+links-param-limit": "Combien de liens renvoyer.",
+       "apihelp-query+links-param-titles": "Lister uniquement les liens de ces titres. Utile pour vérifier si une certaine page a un lien vers un titre donné.",
+       "apihelp-query+links-param-dir": "La direction dans laquelle lister.",
+       "apihelp-query+links-example-simple": "Obtenir les liens de [[Main Page]]",
+       "apihelp-query+links-example-generator": "Obtenir des informations sur tous les liens de page dans [[Main Page]]",
+       "apihelp-query+links-example-namespaces": "Obtenir les liens de [[Main Page]] dans les espaces de nom Utilisateur et Modèle",
+       "apihelp-query+linkshere-description": "Trouver toutes les pages ayant un lien vers les pages données.",
+       "apihelp-query+linkshere-param-prop": "Quelles propriétés obtenir :\n;pageid:ID de chaque page.\n;title:Titre de chaque page.\n;redirect:Marquage si la page est une redirection.",
+       "apihelp-query+linkshere-param-namespace": "Inclure uniquement les pages dans ces espaces de nom.",
+       "apihelp-query+linkshere-param-limit": "Combien renvoyer.",
+       "apihelp-query+linkshere-param-show": "Afficher uniquement les éléments qui correspondent à ces critères :\n;redirect:Afficher uniquement les redirections.\n;!redirects:Afficher uniquement les non-redirections.",
+       "apihelp-query+linkshere-example-simple": "Obtenir une liste des pages liées à  [[Main Page]]",
+       "apihelp-query+linkshere-example-generator": "Obtenir des informations sur les pages liées à [[Main Page]]",
+       "apihelp-query+logevents-description": "Obtenir des événements des journaux.",
+       "apihelp-query+logevents-param-prop": "Quelles propriétés obtenir :\n;ids:Ajoute l’ID de l’événement tracé.\n;title:Ajoute le titre de la page pour l’événement tracé.\n;type:Ajoute le type de l’événement tracé.\n;user:Ajoute l’utilisateur responsable de l’événement tracé.\n;userid:Ajoute l’ID de l’utilisateur responsable de l’événement tracé.\n;timestamp:Ajoute l’horodatage de l’événement.\n;comment:Ajoute le commentaire de l’événement.\n;parsedcomment:Ajoute le commentaire analysé de l’événement.\n;details:Liste les détails supplémentaires sur l’événement.\n;tags:Liste les balises de l’événement.",
+       "apihelp-query+logevents-param-type": "Filtrer les entrées du journal à ce seul type.",
+       "apihelp-query+logevents-param-action": "Filtrer les actions du journal à cette seule action. Écrase $1type. Des actions avec une astérisque de la forme « action/* » sont autorisées pour spécifier n’importe quelle chaîne à la place de l’astérisque.",
+       "apihelp-query+logevents-param-start": "L’horodatage auquel démarrer l’énumération.",
+       "apihelp-query+logevents-param-end": "L’horodatage auquel arrêter l’énumération.",
        "apihelp-format-example-generic": "Mettre en forme le résultat de la requête dans le format $1",
        "apihelp-dbg-description": "Extraire les données au format de var_export() de PHP.",
        "apihelp-dbgfm-description": "Extraire les données au format de var_export() de PHP (affiché proprement en HTML).",
        "apihelp-yamlfm-description": "Extraire les données YAML (affiché proprement en HTML).",
        "api-format-title": "Résultat de l’API de MédiaWiki",
        "api-format-prettyprint-header": "Vous regardez la représentation HTML du format $1. HTML est utile pour le débogage, mais inapproprié pour être utilisé dans une application.\n\nSpécifiez le paramètre format pour modifier le format de sortie. Pour voir la représentation non HTML du format $1, mettez format=$2.\n\nVoyez la [https://www.mediawiki.org/wiki/API documentation complète], ou l’ [[Special:ApiHelp/main|aide de l’API]] pour plus d’information.",
-       "api-help-title": "Aide de l’API de MédiaWiki",
+       "api-help-title": "Aide de l’API de MediaWiki",
        "api-help-lead": "Ceci est une page d’aide de l’API de MédiaWiki générée automatiquement.\n\nDocumentation et exemples : https://www.mediawiki.org/wiki/API",
        "api-help-main-header": "Module principal",
        "api-help-flag-deprecated": "Ce module est obsolète.",
index 411a1f3..9c1465c 100644 (file)
        "apihelp-upload-param-watch": "监视页面。",
        "apihelp-upload-param-ignorewarnings": "忽略任何警告。",
        "apihelp-upload-param-file": "文件内容。",
+       "apihelp-upload-param-stash": "如果设置,服务器将不会添加文件至存储库并暂时藏匿。",
        "apihelp-upload-param-chunk": "大块内容。",
        "apihelp-upload-example-url": "从URL上传",
        "apihelp-userrights-param-user": "用户名。",
index d0db084..202edef 100644 (file)
@@ -6,11 +6,13 @@
                        "Muhammed taha"
                ]
        },
+       "config-desc": "دامەزرێنەرەکە بۆ میدیاویکی",
+       "config-title": "دامەزرانی میدیاویکی $1",
        "config-information": "زانیاری",
        "config-your-language": "زمانەکەت:",
        "config-wiki-language": "زمانی ویکی:",
        "config-back": "→ گەڕانەوە",
-       "config-continue": "بەردەوامبوون ←",
+       "config-continue": "بەردەوام بە ←",
        "config-page-language": "زمان",
        "config-page-welcome": "بەخێربێیت بۆ میدیاویکی!",
        "config-page-dbconnect": "پەیوەندی دەکات بەبنکەی زانیارییەکان",
        "config-page-upgradedoc": "نوێدەکرێتەوە",
        "config-page-existingwiki": "ویکی پێشوو",
        "config-restart": "بەڵێ، دەستی پێ بکەرەوە",
-       "config-env-php": "PHP $1 دابەزێندرا.",
-       "config-env-php-toolow": "PHP $1 دابەزێندرا.\nھەرچۆنێک بێت میدیاویکی پێویستی بە PHP $2 یان بەرزتر ھەیە.",
+       "config-env-php": "PHP $1 دامەزراوە.",
+       "config-apc": "[http://www.php.net/apc APC] دامەزراوە",
+       "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] دامەزراوە",
+       "config-db-type": "جۆری داتابەیس:",
+       "config-db-host": "خانەخوێی داتابەیس:",
        "config-db-name": "ناوی بنکەدراوە:",
+       "config-db-install-account": "ھەژماری بەکارھێنەری بۆ دامەزراندن",
        "config-db-username": "ناوی بەکارھێنەری بنکەدراوە:",
        "config-db-password": "تێپەڕوشەی بنکەدراوە",
        "config-site-name": "ناوی ویکی:",
        "config-ns-generic": "پرۆژە",
+       "config-admin-name": "ناوی بەکارھێنەرییەکەت:",
        "config-admin-password": "تێپەڕوشە:",
+       "config-admin-password-confirm": "دووبارە تێپەڕوشە:",
        "config-admin-email": "ناونیشانی ئیمەیل:",
+       "config-profile-wiki": "ویکیی کراوە",
+       "config-profile-no-anon": "دروستکردنی ھەژمارە پێویستە",
+       "config-profile-fishbowl": "تەنھا دەستکاریکەری ڕێگەپێدراوە",
+       "config-license-pd": "پاوانی گشتی",
+       "config-email-settings": "ڕێکخستنەکانی ئیمەیڵ",
        "config-install-step-done": "کرا",
        "config-help": "یارمەتی",
        "mainpagetext": "'''میدیاویکی بە سەرکەوتوویی دامەزرا.'''",
index dda5254..f09d101 100644 (file)
@@ -126,7 +126,7 @@ class RunningStat implements Countable {
        }
 
        /**
-        * Get the estimated stanard deviation.
+        * Get the estimated standard deviation.
         *
         * The standard deviation of a statistical population is the square root of
         * its variance. It shows shows how much variation from the mean exists. In
diff --git a/includes/libs/Xhprof.php b/includes/libs/Xhprof.php
new file mode 100644 (file)
index 0000000..1ad01cc
--- /dev/null
@@ -0,0 +1,443 @@
+<?php
+/**
+ * @section LICENSE
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Convenience class for working with XHProf
+ * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
+ * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
+ *
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.25
+ */
+class Xhprof {
+
+       /**
+        * @var array $config
+        */
+       protected $config;
+
+       /**
+        * Hierarchical profiling data returned by xhprof.
+        * @var array $hieraData
+        */
+       protected $hieraData;
+
+       /**
+        * Per-function inclusive data.
+        * @var array $inclusive
+        */
+       protected $inclusive;
+
+       /**
+        * Per-function inclusive and exclusive data.
+        * @var array $complete
+        */
+       protected $complete;
+
+       /**
+        * Configuration data can contain:
+        * - flags:   Optional flags to add additional information to the
+        *            profiling data collected.
+        *            (XHPROF_FLAGS_NO_BUILTINS, XHPROF_FLAGS_CPU,
+        *            XHPROF_FLAGS_MEMORY)
+        * - exclude: Array of function names to exclude from profiling.
+        * - include: Array of function names to include in profiling.
+        * - sort:    Key to sort per-function reports on.
+        *
+        * Note: When running under HHVM, xhprof will always behave as though the
+        * XHPROF_FLAGS_NO_BUILTINS flag has been used unless the
+        * Eval.JitEnableRenameFunction option is enabled for the HHVM process.
+        *
+        * @param array $config
+        */
+       public function __construct( array $config = array() ) {
+               $this->config = array_merge(
+                       array(
+                               'flags' => 0,
+                               'exclude' => array(),
+                               'include' => null,
+                               'sort' => 'wt',
+                       ),
+                       $config
+               );
+
+               xhprof_enable( $this->config['flags'], array(
+                       'ignored_functions' => $this->config['exclude']
+               ) );
+       }
+
+       /**
+        * Stop collecting profiling data.
+        *
+        * Only the first invocation of this method will effect the internal
+        * object state. Subsequent calls will return the data collected by the
+        * initial call.
+        *
+        * @return array Collected profiling data (possibly cached)
+        */
+       public function stop() {
+               if ( $this->hieraData === null ) {
+                       $this->hieraData = $this->pruneData( xhprof_disable() );
+               }
+               return $this->hieraData;
+       }
+
+       /**
+        * Load raw data from a prior run for analysis.
+        * Stops any existing data collection and clears internal caches.
+        *
+        * Any 'include' filters configured for this Xhprof instance will be
+        * enforced on the data as it is loaded. 'exclude' filters will however
+        * not be enforced as they are an XHProf intrinsic behavior.
+        *
+        * @param array $data
+        * @see getRawData()
+        */
+       public function loadRawData( array $data ) {
+               $this->stop();
+               $this->inclusive = null;
+               $this->complete = null;
+               $this->hieraData = $this->pruneData( $data );
+       }
+
+       /**
+        * Get raw data collected by xhprof.
+        *
+        * If data collection has not been stopped yet this method will halt
+        * collection to gather the profiling data.
+        *
+        * Each key in the returned array is an edge label for the call graph in
+        * the form "caller==>callee". There is once special case edge labled
+        * simply "main()" which represents the global scope entry point of the
+        * application.
+        *
+        * XHProf will collect different data depending on the flags that are used:
+        * - ct:    Number of matching events seen.
+        * - wt:    Inclusive elapsed wall time for this event in microseconds.
+        * - cpu:   Inclusive elapsed cpu time for this event in microseconds.
+        *          (XHPROF_FLAGS_CPU)
+        * - mu:    Delta of memory usage from start to end of callee in bytes.
+        *          (XHPROF_FLAGS_MEMORY)
+        * - pmu:   Delta of peak memory usage from start to end of callee in
+        *          bytes. (XHPROF_FLAGS_MEMORY)
+        * - alloc: Delta of amount memory requested from malloc() by the callee,
+        *          in bytes. (XHPROF_FLAGS_MALLOC)
+        * - free:  Delta of amount of memory passed to free() by the callee, in
+        *          bytes. (XHPROF_FLAGS_MALLOC)
+        *
+        * @return array
+        * @see stop()
+        * @see getInclusiveMetrics()
+        * @see getCompleteMetrics()
+        */
+       public function getRawData() {
+               return $this->stop();
+       }
+
+       /**
+        * Convert an xhprof data key into an array of ['parent', 'child']
+        * function names.
+        *
+        * The resulting array is left padded with nulls, so a key
+        * with no parent (eg 'main()') will return [null, 'function'].
+        *
+        * @return array
+        */
+       public static function splitKey( $key ) {
+               return array_pad( explode( '==>', $key, 2 ), -2, null );
+       }
+
+       /**
+        * Remove data for functions that are not included in the 'include'
+        * configuration array.
+        *
+        * @param array $data Raw xhprof data
+        * @return array
+        */
+       protected function pruneData( $data ) {
+               if ( !$this->config['include'] ) {
+                       return $data;
+               }
+
+               $want = array_fill_keys( $this->config['include'], true );
+               $want['main()'] = true;
+
+               $keep = array();
+               foreach ( $data as $key => $stats ) {
+                       list( $parent, $child ) = self::splitKey( $key );
+                       if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
+                               $keep[$key] = $stats;
+                       }
+               }
+               return $keep;
+       }
+
+       /**
+        * Get the inclusive metrics for each function call. Inclusive metrics
+        * for given function include the metrics for all functions that were
+        * called from that function during the measurement period.
+        *
+        * If data collection has not been stopped yet this method will halt
+        * collection to gather the profiling data.
+        *
+        * See getRawData() for a description of the metric that are returned for
+        * each funcition call. The values for the wt, cpu, mu and pmu metrics are
+        * arrays with these values:
+        * - total: Cumulative value
+        * - min: Minimum value
+        * - mean: Mean (average) value
+        * - max: Maximum value
+        * - variance: Variance (spread) of the values
+        *
+        * @return array
+        * @see getRawData()
+        * @see getCompleteMetrics()
+        */
+       public function getInclusiveMetrics() {
+               if ( $this->inclusive === null ) {
+                       // Make sure we have data to work with
+                       $this->stop();
+
+                       $main = $this->hieraData['main()'];
+                       $hasCpu = isset( $main['cpu'] );
+                       $hasMu = isset( $main['mu'] );
+                       $hasAlloc = isset( $main['alloc'] );
+
+                       $this->inclusive = array();
+                       foreach ( $this->hieraData as $key => $stats ) {
+                               list( $parent, $child ) = self::splitKey( $key );
+                               if ( !isset( $this->inclusive[$child] ) ) {
+                                       $this->inclusive[$child] = array(
+                                               'ct' => 0,
+                                               'wt' => new RunningStat(),
+                                       );
+                                       if ( $hasCpu ) {
+                                               $this->inclusive[$child]['cpu'] = new RunningStat();
+                                       }
+                                       if ( $hasMu ) {
+                                               $this->inclusive[$child]['mu'] = new RunningStat();
+                                               $this->inclusive[$child]['pmu'] = new RunningStat();
+                                       }
+                                       if ( $hasAlloc ) {
+                                               $this->inclusive[$child]['alloc'] = new RunningStat();
+                                               $this->inclusive[$child]['free'] = new RunningStat();
+                                       }
+                               }
+
+                               $this->inclusive[$child]['ct'] += $stats['ct'];
+                               foreach ( $stats as $stat => $value ) {
+                                       if ( $stat === 'ct' ) {
+                                               continue;
+                                       }
+
+                                       if ( !isset( $this->inclusive[$child][$stat] ) ) {
+                                               // Ignore unknown stats
+                                               continue;
+                                       }
+
+                                       for ( $i = 0; $i < $stats['ct']; $i++ ) {
+                                               $this->inclusive[$child][$stat]->push(
+                                                       $value / $stats['ct']
+                                               );
+                                       }
+                               }
+                       }
+
+                       // Convert RunningStat instances to static arrays and add
+                       // percentage stats.
+                       foreach ( $this->inclusive as $func => $stats ) {
+                               foreach ( $stats as $name => $value ) {
+                                       if ( $value instanceof RunningStat ) {
+                                               $total = $value->m1 * $value->n;
+                                               $this->inclusive[$func][$name] = array(
+                                                       'total' => $total,
+                                                       'min' => $value->min,
+                                                       'mean' => $value->m1,
+                                                       'max' => $value->max,
+                                                       'variance' => $value->m2,
+                                                       'percent' => 100 * $total / $main[$name],
+                                               );
+                                       }
+                               }
+                       }
+
+                       uasort( $this->inclusive, self::makeSortFunction(
+                               $this->config['sort'], 'total'
+                       ) );
+               }
+               return $this->inclusive;
+       }
+
+       /**
+        * Get the inclusive and exclusive metrics for each function call.
+        *
+        * If data collection has not been stopped yet this method will halt
+        * collection to gather the profiling data.
+        *
+        * In addition to the normal data contained in the inclusive metrics, the
+        * metrics have an additional 'exclusive' measurement which is the total
+        * minus the totals of all child function calls.
+        *
+        * @return array
+        * @see getRawData()
+        * @see getInclusiveMetrics()
+        */
+       public function getCompleteMetrics() {
+               if ( $this->complete === null ) {
+                       // Start with inclusive data
+                       $this->complete = $this->getInclusiveMetrics();
+
+                       foreach ( $this->complete as $func => $stats ) {
+                               foreach ( $stats as $stat => $value ) {
+                                       if ( $stat === 'ct' ) {
+                                               continue;
+                                       }
+                                       // Initialize exclusive data with inclusive totals
+                                       $this->complete[$func][$stat]['exclusive'] = $value['total'];
+                               }
+                               // Add sapce for call tree information to be filled in later
+                               $this->complete[$func]['calls'] = array();
+                               $this->complete[$func]['subcalls'] = array();
+                       }
+
+                       foreach( $this->hieraData as $key => $stats ) {
+                               list( $parent, $child ) = self::splitKey( $key );
+                               if ( $parent !== null ) {
+                                       // Track call tree information
+                                       $this->complete[$child]['calls'][$parent] = $stats;
+                                       $this->complete[$parent]['subcalls'][$child] = $stats;
+                               }
+
+                               if ( isset( $this->complete[$parent] ) ) {
+                                       // Deduct child inclusive data from exclusive data
+                                       foreach ( $stats as $stat => $value ) {
+                                               if ( $stat === 'ct' ) {
+                                                       continue;
+                                               }
+
+                                               if ( !isset( $this->complete[$parent][$stat] ) ) {
+                                                       // Ignore unknown stats
+                                                       continue;
+                                               }
+
+                                               $this->complete[$parent][$stat]['exclusive'] -= $value;
+                                       }
+                               }
+                       }
+
+                       uasort( $this->complete, self::makeSortFunction(
+                               $this->config['sort'], 'exclusive'
+                       ) );
+               }
+               return $this->complete;
+       }
+
+       /**
+        * Get a list of all callers of a given function.
+        *
+        * @param string $function Function name
+        * @return array
+        * @see getEdges()
+        */
+       public function getCallers( $function ) {
+               $edges = $this->getCompleteMetrics();
+               if ( isset( $edges[$function]['calls'] ) ) {
+                       return array_keys( $edges[$function]['calls'] );
+               } else {
+                       return array();
+               }
+       }
+
+       /**
+        * Get a list of all callees from a given function.
+        *
+        * @param string $function Function name
+        * @return array
+        * @see getEdges()
+        */
+       public function getCallees( $function ) {
+               $edges = $this->getCompleteMetrics();
+               if ( isset( $edges[$function]['subcalls'] ) ) {
+                       return array_keys( $edges[$function]['subcalls'] );
+               } else {
+                       return array();
+               }
+       }
+
+       /**
+        * Find the critical path for the given metric.
+        *
+        * @param string $metric Metric to find critical path for
+        * @return array
+        */
+       public function getCriticalPath( $metric = 'wt' ) {
+               $this->stop();
+               $func = 'main()';
+               $path = array(
+                       $func => $this->hieraData[$func],
+               );
+               while ( $func ) {
+                       $callees = $this->getCallees( $func );
+                       $maxCallee = null;
+                       $maxCall = null;
+                       foreach ( $callees as $callee ) {
+                               $call = "{$func}==>{$callee}";
+                               if ( $maxCall === null ||
+                                       $this->hieraData[$call][$metric] >
+                                               $this->hieraData[$maxCall][$metric]
+                               ) {
+                                       $maxCallee = $callee;
+                                       $maxCall = $call;
+                               }
+                       }
+                       if ( $maxCall !== null ) {
+                               $path[$maxCall] = $this->hieraData[$maxCall];
+                       }
+                       $func = $maxCallee;
+               }
+               return $path;
+       }
+
+       /**
+        * Make a closure to use as a sort function. The resulting function will
+        * sort by descending numeric values (largest value first).
+        *
+        * @param string $key Data key to sort on
+        * @param string $sub Sub key to sort array values on
+        * @return Closure
+        */
+       public static function makeSortFunction( $key, $sub ) {
+               return function ( $a, $b ) use ( $key, $sub ) {
+                       if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
+                               // Descending sort: larger values will be first in result.
+                               // Assumes all values are numeric.
+                               // Values for 'main()' will not have sub keys
+                               $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
+                               $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
+                               return $valB - $valA;
+                       } else {
+                               // Sort datum with the key before those without
+                               return isset( $a[$key] ) ? -1 : 1;
+                       }
+               };
+       }
+}
index bc9e917..d9327fb 100644 (file)
@@ -348,9 +348,9 @@ class ThumbnailImage extends MediaTransformOutput {
                        throw new MWException( __METHOD__ . ' called in the old style' );
                }
 
-               $alt = empty( $options['alt'] ) ? '' : $options['alt'];
+               $alt = isset( $options['alt'] ) ? $options['alt'] : '';
 
-               $query = empty( $options['desc-query'] ) ? '' : $options['desc-query'];
+               $query = isset( $options['desc-query'] ) ? $options['desc-query'] : '';
 
                if ( !empty( $options['custom-url-link'] ) ) {
                        $linkAttribs = array( 'href' => $options['custom-url-link'] );
index cd804b5..d310836 100644 (file)
@@ -224,6 +224,9 @@ class Parser {
         */
        public $mInParse = false;
 
+       /** @var SectionProfiler */
+       protected $mProfiler;
+
        /**
         * @param array $conf
         */
@@ -365,6 +368,8 @@ class Parser {
                        $this->mPreprocessor = null;
                }
 
+               $this->mProfiler = new SectionProfiler();
+
                wfRunHooks( 'ParserClearState', array( &$this ) );
                wfProfileOut( __METHOD__ );
        }
@@ -526,6 +531,19 @@ class Parser {
                        $limitReport = str_replace( array( '-', '&' ), array( '‐', '&amp;' ), $limitReport );
                        $text .= "\n<!-- \n$limitReport-->\n";
 
+                       // Add on template profiling data
+                       $dataByFunc = $this->mProfiler->getFunctionStats();
+                       uasort( $dataByFunc, function( $a, $b ) {
+                               return $a['elapsed'] < $b['elapsed']; // descending order
+                       } );
+                       $profileReport = "Top template expansion time report (%,ms,calls,template)\n";
+                       foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
+                               $profileReport .= sprintf( "%6.2f%% %3.6f %6d - %s\n",
+                                       $item['percent'], $item['elapsed'], $item['calls'],
+                                       htmlspecialchars($item['name'] ) );
+                       }
+                       $text .= "\n<!-- \n$profileReport-->\n";
+
                        if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
                                wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
                                        $this->mTitle->getPrefixedDBkey() );
@@ -3473,7 +3491,7 @@ class Parser {
                $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
                wfProfileOut( __METHOD__ . '-setup' );
 
-               $titleProfileIn = null; // profile templates
+               $profileSection = null; // profile templates
 
                # SUBST
                wfProfileIn( __METHOD__ . '-modifiers' );
@@ -3594,11 +3612,7 @@ class Parser {
 
                # Load from database
                if ( !$found && $title ) {
-                       if ( !Profiler::instance()->isPersistent() ) {
-                               # Too many unique items can kill profiling DBs/collectors
-                               $titleProfileIn = __METHOD__ . "-title-" . $title->getPrefixedDBkey();
-                               wfProfileIn( $titleProfileIn ); // template in
-                       }
+                       $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
                        wfProfileIn( __METHOD__ . '-loadtpl' );
                        if ( !$title->isExternal() ) {
                                if ( $title->isSpecialPage()
@@ -3680,8 +3694,8 @@ class Parser {
                # Recover the source wikitext and return it
                if ( !$found ) {
                        $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
-                       if ( $titleProfileIn ) {
-                               wfProfileOut( $titleProfileIn ); // template out
+                       if ( $profileSection ) {
+                               $this->mProfiler->scopedProfileOut( $profileSection );
                        }
                        wfProfileOut( __METHOD__ );
                        return array( 'object' => $text );
@@ -3707,8 +3721,8 @@ class Parser {
                        $isLocalObj = false;
                }
 
-               if ( $titleProfileIn ) {
-                       wfProfileOut( $titleProfileIn ); // template out
+               if ( $profileSection ) {
+                       $this->mProfiler->scopedProfileOut( $profileSection );
                }
 
                # Replace raw HTML by a placeholder
index 4c12b10..078b66b 100644 (file)
@@ -80,18 +80,6 @@ abstract class Profiler {
         */
        abstract public function isStub();
 
-       /**
-        * Return whether this profiler stores data
-        *
-        * Called by Parser::braceSubstitution. If true, the parser will not
-        * generate per-title profiling sections, to avoid overloading the
-        * profiling data collector.
-        *
-        * @see Profiler::logData()
-        * @return bool
-        */
-       abstract public function isPersistent();
-
        /**
         * @param string $id
         */
@@ -159,6 +147,8 @@ abstract class Profiler {
        abstract public function getOutput();
 
        /**
+        * Get data for the debugging toolbar.
+        *
         * @return array
         */
        abstract public function getRawData();
index 3625db6..911b926 100644 (file)
  * @ingroup Profiler
  */
 class ProfilerSimpleDB extends ProfilerStandard {
-       public function isPersistent() {
-               return true;
-       }
-
        /**
         * Log the whole profiling data into the database.
         */
index 2671376..ad16a18 100644 (file)
  * @ingroup Profiler
  */
 class ProfilerSimpleUDP extends ProfilerStandard {
-       public function isPersistent() {
-               return true;
-       }
-
        public function logData() {
                global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgUDPProfilerFormatString;
 
index 4825f7a..b873806 100644 (file)
@@ -47,11 +47,14 @@ class ProfilerStandard extends Profiler {
 
        /**
         * @param array $params
+        *   - initTotal : set to false to omit -total/-setup entries (which use request start time)
         */
        public function __construct( array $params ) {
                parent::__construct( $params );
 
-               $this->addInitialStack();
+               if ( !isset( $params['initTotal'] ) || $params['initTotal'] ) {
+                       $this->addInitialStack();
+               }
        }
 
        /**
@@ -63,16 +66,6 @@ class ProfilerStandard extends Profiler {
                return false;
        }
 
-       /**
-        * Return whether this profiler stores data
-        *
-        * @see Profiler::logData()
-        * @return bool
-        */
-       public function isPersistent() {
-               return false;
-       }
-
        /**
         * Add the inital item in the stack.
         */
index e81f579..43e2193 100644 (file)
@@ -31,10 +31,6 @@ class ProfilerStub extends Profiler {
                return true;
        }
 
-       public function isPersistent() {
-               return false;
-       }
-
        public function profileIn( $fn ) {
        }
 
diff --git a/includes/profiler/ProfilerXhprof.php b/includes/profiler/ProfilerXhprof.php
new file mode 100644 (file)
index 0000000..1e83e27
--- /dev/null
@@ -0,0 +1,474 @@
+<?php
+/**
+ * @section LICENSE
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Profiler wrapper for XHProf extension.
+ *
+ * Mimics the output of ProfilerSimpleText, ProfilerSimpleUDP or
+ * ProfilerSimpleTrace but using data collected via the XHProf PHP extension.
+ * This Profiler also produces data compatable with the debugging toolbar.
+ *
+ * To mimic ProfilerSimpleText results:
+ * @code
+ * $wgProfiler['class'] = 'ProfilerXhprof';
+ * $wgProfiler['flags'] = XHPROF_FLAGS_NO_BUILTINS;
+ * $wgProfiler['log'] = 'text';
+ * $wgProfiler['visible'] = true;
+ * @endcode
+ *
+ * To mimic ProfilerSimpleUDP results:
+ * @code
+ * $wgProfiler['class'] = 'ProfilerXhprof';
+ * $wgProfiler['flags'] = XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS;
+ * $wgProfiler['log'] = 'udpprofile';
+ * @endcode
+ *
+ * Similar to ProfilerSimpleTrace results, report the most expensive path
+ * through the application:
+ * @code
+ * $wgProfiler['class'] = 'ProfilerXhprof';
+ * $wgProfiler['flags'] = XHPROF_FLAGS_NO_BUILTINS;
+ * $wgProfiler['log'] = 'critpath';
+ * $wgProfiler['visible'] = true;
+ * @endcode
+ *
+ * Rather than obeying wfProfileIn() and wfProfileOut() calls placed in the
+ * application code, ProfilerXhprof profiles all functions using the XHProf
+ * PHP extenstion. For PHP5 users, this extension can be installed via PECL or
+ * your operating system's package manager. XHProf support is built into HHVM.
+ *
+ * To restrict the functions for which profiling data is collected, you can
+ * use either a whitelist ($wgProfiler['include']) or a blacklist
+ * ($wgProfiler['exclude']) containing an array of function names. The
+ * blacklist funcitonality is built into HHVM and will completely exclude the
+ * named functions from profiling collection. The whitelist is implemented by
+ * Xhprof class and will filter the data collected by XHProf before reporting.
+ * See documentation for the Xhprof class and the XHProf extension for
+ * additional information.
+ *
+ * Data reported to debug toolbar via getRawData() can be restricted by
+ * setting $wgProfiler['toolbarCutoff'] to a minumum cumulative wall clock
+ * percentage. Functions in the call graph which contribute less than this
+ * percentage to the total wall clock time measured will be excluded from the
+ * data sent to debug toolbar. The default cutoff is 0.1 (1/1000th of the
+ * total time measured).
+ *
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @ingroup Profiler
+ * @see Xhprof
+ * @see https://php.net/xhprof
+ * @see https://github.com/facebook/hhvm/blob/master/hphp/doc/profiling.md
+ */
+class ProfilerXhprof extends Profiler {
+
+       /**
+        * @var Xhprof $xhprof
+        */
+       protected $xhprof;
+
+       /**
+        * Type of report to send when logData() is called.
+        * @var string $logType
+        */
+       protected $logType;
+
+       /**
+        * Should profile report sent to in page content be visible?
+        * @var bool $visible
+        */
+       protected $visible;
+
+       /**
+        * Minimum wall time precentage for a fucntion to be reported to the debug
+        * toolbar via getRawData().
+        * @var float $toolbarCutoff
+        */
+       protected $toolbarCutoff;
+
+       /**
+        * @param array $params
+        * @see Xhprof::__construct()
+        */
+       public function __construct( array $params = array() ) {
+               $params = array_merge(
+                       array(
+                               'log' => 'text',
+                               'visible' => false,
+                               'toolbarCutoff' => 0.1,
+                       ),
+                       $params
+               );
+               parent::__construct( $params );
+               $this->logType = $params['log'];
+               $this->visible = $params['visible'];
+               $this->xhprof = new Xhprof( $params );
+       }
+
+       public function isStub() {
+               return false;
+       }
+
+       /**
+        * No-op for xhprof profiling.
+        *
+        * Use the 'include' configuration key instead if you need to constrain
+        * the functions that are profiled.
+        *
+        * @param string $functionname
+        */
+       public function profileIn( $functionname ) {
+               global $wgDebugFunctionEntry;
+               if ( $wgDebugFunctionEntry ) {
+                       $this->debug( "Entering {$functionname}" );
+               }
+       }
+
+       /**
+        * No-op for xhprof profiling.
+        *
+        * Use the 'include' configuration key instead if you need to constrain
+        * the functions that are profiled.
+        *
+        * @param string $functionname
+        */
+       public function profileOut( $functionname ) {
+               global $wgDebugFunctionEntry;
+               if ( $wgDebugFunctionEntry ) {
+                       $this->debug( "Exiting {$functionname}" );
+               }
+       }
+
+       /**
+        * No-op for xhprof profiling.
+        */
+       public function close() {
+       }
+
+       /**
+        * Get data for the debugging toolbar.
+        *
+        * @return array
+        * @see https://www.mediawiki.org/wiki/Debugging_toolbar
+        */
+       public function getRawData() {
+               $metrics = $this->xhprof->getCompleteMetrics();
+               $endEpoch = $this->getTime();
+               $profile = array();
+
+               foreach ( $metrics as $fname => $stats ) {
+                       if ( $stats['wt']['percent'] < $this->toolbarCutoff ) {
+                               // Ignore functions which are not significant contributors
+                               // to the total elapsed time.
+                               continue;
+                       }
+
+                       $record = array(
+                               'name' => $fname,
+                               'calls' => $stats['ct'],
+                               'elapsed' => $stats['wt']['total'] / 1000,
+                               'percent' => $stats['wt']['percent'],
+                               'memory' => isset( $stats['mu'] ) ? $stats['mu']['total'] : 0,
+                               'min' => $stats['wt']['min'] / 1000,
+                               'max' => $stats['wt']['max'] / 1000,
+                               'overhead' => array_reduce(
+                                       $stats['subcalls'],
+                                       function( $carry, $item ) {
+                                               return $carry + $item['ct'];
+                                       },
+                                       0
+                               ),
+                               'periods' => array(),
+                       );
+
+                       // We are making up periods based on the call segments we have.
+                       // What we don't have is the start time for each call (which
+                       // actually may be a collection of multiple calls from the
+                       // caller). We will pretend that all the calls happen sequentially
+                       // and finish at the end of the end of the request.
+                       foreach ( $stats['calls'] as $call ) {
+                               $avgElapsed = $call['wt'] / 1000 / $call['ct'];
+                               $avgMem = isset( $call['mu'] ) ? $call['mu'] / $call['ct'] : 0;
+                               $start = $endEpoch - $avgElapsed;
+                               for ( $i = 0; $i < $call['ct']; $i++ ) {
+                                       $callStart = $start + ( $avgElapsed * $i );
+                                       $record['periods'][] = array(
+                                               'start' => $callStart,
+                                               'end' => $callStart + $avgElapsed,
+                                               'memory' => $avgMem,
+                                               'subcalls' => 0,
+                                       );
+                               }
+                       }
+
+                       $profile[] = $record;
+               }
+               return $profile;
+       }
+
+       /**
+        * Log the data to some store or even the page output.
+        */
+       public function logData() {
+               if ( $this->logType === 'text' ) {
+                       $this->logText();
+               } elseif ( $this->logType === 'udpprofile' ) {
+                       $this->logToUdpprofile();
+               } elseif ( $this->logType === 'critpath' ){
+                       $this->logCriticalPath();
+               } else {
+                       wfLogWarning(
+                               "Unknown ProfilerXhprof log type '{$this->logType}'"
+                       );
+               }
+       }
+
+       /**
+        * Write a brief profile report to stdout.
+        */
+       protected function logText() {
+               if ( $this->templated ) {
+                       $ct = $this->getContentType();
+                       if ( $ct === 'text/html' && $this->visible ) {
+                               $prefix = '<pre>';
+                               $suffix = '</pre>';
+                       } elseif ( $ct === 'text/javascript' || $ct === 'text/css' ) {
+                               $prefix = "\n/*";
+                               $suffix = "*/\n";
+                       } else {
+                               $prefix = "<!--";
+                               $suffix = "-->\n";
+                       }
+
+                       print $this->getSummaryReport( $prefix, $suffix );
+               }
+       }
+
+       /**
+        * Send collected information to a udpprofile daemon.
+        *
+        * @see http://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile
+        * @see $wgUDPProfilerHost
+        * @see $wgUDPProfilerPort
+        * @see $wgUDPProfilerFormatString
+        */
+       protected function logToUdpprofile() {
+               global $wgUDPProfilerHost, $wgUDPProfilerPort;
+               global $wgUDPProfilerFormatString;
+
+               if ( !function_exists( 'socket_create' ) ) {
+                       return;
+               }
+
+               $metrics = $this->xhprof->getInclusiveMetrics();
+
+               $sock = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
+               $buffer = '';
+               $bufferSize = 0;
+               foreach ( $metrics as $func => $data ) {
+                       $line = sprintf( $wgUDPProfilerFormatString,
+                               $this->getProfileID(),
+                               $data['ct'],
+                               isset( $data['cpu'] ) ? $data['cpu']['total'] : 0,
+                               isset( $data['cpu'] ) ? $data['cpu']['variance'] : 0,
+                               $data['wt']['total'] / 1000,
+                               $data['wt']['variance'],
+                               $func,
+                               isset( $data['mu'] ) ? $data['mu']['total'] : 0
+                       );
+                       $lineLength = strlen( $line );
+                       if ( $lineLength + $bufferSize > 1400 ) {
+                               // Line would exceed max packet size, send packet before
+                               // appending to buffer.
+                               socket_sendto( $sock, $buffer, $bufferSize, 0,
+                                       $wgUDPProfilerHost, $wgUDPProfilerPort
+                               );
+                               $buffer = '';
+                               $bufferSize = 0;
+                       }
+                       $buffer .= $line;
+                       $bufferSize += $lineLength;
+               }
+               // Send final buffer
+               socket_sendto( $sock, $buffer, $bufferSize, 0x100 /* EOF */,
+                       $wgUDPProfilerHost, $wgUDPProfilerPort
+               );
+       }
+
+       /**
+        * Write a critical path report to stdout.
+        */
+       protected function logCriticalPath() {
+               if ( $this->templated ) {
+                       $ct = $this->getContentType();
+                       if ( $ct === 'text/html' && $this->visible ) {
+                               $prefix = '<pre>Critical path:';
+                               $suffix = '</pre>';
+                       } elseif ( $ct === 'text/javascript' || $ct === 'text/css' ) {
+                               $prefix = "\n/* Critical path:";
+                               $suffix = "*/\n";
+                       } else {
+                               $prefix = "<!-- Critical path:";
+                               $suffix = "-->\n";
+                       }
+
+                       print $this->getCriticalPathReport( $prefix, $suffix );
+               }
+       }
+
+       /**
+        * Get the content type of the current request.
+        * @return string
+        */
+       protected function getContentType() {
+               foreach ( headers_list() as $header ) {
+                       if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) {
+                               return $m[1];
+                       }
+               }
+               return 'application/octet-stream';
+       }
+
+       /**
+        * Returns a profiling output to be stored in debug file
+        *
+        * @return string
+        */
+       public function getOutput() {
+               return $this->getFunctionReport();
+       }
+
+       /**
+        * Get a report of profiled functions sorted by inclusive wall clock time
+        * in descending order.
+        *
+        * Each line of the report includes this data:
+        * - Function name
+        * - Number of times function was called
+        * - Total wall clock time spent in function in microseconds
+        * - Minimum wall clock time spent in function in microseconds
+        * - Average wall clock time spent in function in microseconds
+        * - Maximum wall clock time spent in function in microseconds
+        * - Percentage of total wall clock time spent in function
+        * - Total delta of memory usage from start to end of function in bytes
+        *
+        * @return string
+        */
+       protected function getFunctionReport() {
+               $data = $this->xhprof->getInclusiveMetrics();
+               uasort( $data, Xhprof::makeSortFunction( 'wt', 'total' ) );
+
+               $width = 140;
+               $nameWidth = $width - 65;
+               $format = "%-{$nameWidth}s %6d %9d %9d %9d %9d %7.3f%% %9d";
+               $out = array();
+               $out[] = sprintf( "%-{$nameWidth}s %6s %9s %9s %9s %9s %7s %9s",
+                       'Name', 'Calls', 'Total', 'Min', 'Each', 'Max', '%', 'Mem'
+               );
+               foreach ( $data as $func => $stats ) {
+                       $out[] = sprintf( $format,
+                               $func,
+                               $stats['ct'],
+                               $stats['wt']['total'],
+                               $stats['wt']['min'],
+                               $stats['wt']['mean'],
+                               $stats['wt']['max'],
+                               $stats['wt']['percent'],
+                               isset( $stats['mu'] ) ? $stats['mu']['total'] : 0
+                       );
+               }
+               return implode( "\n", $out );
+       }
+
+       /**
+        * Get a brief report of profiled functions sorted by inclusive wall clock
+        * time in descending order.
+        *
+        * Each line of the report includes this data:
+        * - Percentage of total wall clock time spent in function
+        * - Total wall clock time spent in function in seconds
+        * - Number of times function was called
+        * - Function name
+        *
+        * @param string $header Header text to prepend to report
+        * @param string $footer Footer text to append to report
+        * @return string
+        */
+       protected function getSummaryReport( $header = '', $footer = '' ) {
+               $data = $this->xhprof->getInclusiveMetrics();
+               uasort( $data, Xhprof::makeSortFunction( 'wt', 'total' ) );
+
+               $format = '%6.2f%% %3.6f %6d - %s';
+               $out = array( $header );
+               foreach ( $data as $func => $stats ) {
+                       $out[] = sprintf( $format,
+                               $stats['wt']['percent'],
+                               $stats['wt']['total'] / 1e6,
+                               $stats['ct'],
+                               $func
+                       );
+               }
+               $out[] = $footer;
+               return implode( "\n", $out );
+       }
+
+       /**
+        * Get a brief report of the most costly code path by wall clock time.
+        *
+        * Each line of the report includes this data:
+        * - Total wall clock time spent in function in seconds
+        * - Function name
+        *
+        * @param string $header Header text to prepend to report
+        * @param string $footer Footer text to append to report
+        * @return string
+        */
+       protected function getCriticalPathReport( $header = '', $footer = '' ) {
+               $data = $this->xhprof->getCriticalPath();
+
+               $out = array( $header );
+               $out[] = sprintf( "%7s %9s %9s %6s %4s",
+                       'Time%', 'Time', 'Mem', 'Calls', 'Name'
+               );
+
+               $format = '%6.2f%% %9.6f %9d %6d %s%s';
+               $total = null;
+               $nest = 0;
+               foreach ( $data as $key => $stats ) {
+                       list( $parent, $child ) = Xhprof::splitKey( $key );
+                       if ( $total === null ) {
+                               $total = $stats;
+                       }
+                       $out[] = sprintf( $format,
+                               100 * $stats['wt'] / $total['wt'],
+                               $stats['wt'] / 1e6,
+                               isset( $stats['mu'] ) ? $stats['mu'] : 0,
+                               $stats['ct'],
+                               //$nest ? str_repeat( ' ', $nest - 1 ) . '┗ ' : '',
+                               $nest ? str_repeat( ' ', $nest - 1 ) . '└─' : '',
+                               $child
+                       );
+                       $nest++;
+               }
+               $out[] = $footer;
+               return implode( "\n", $out );
+       }
+}
diff --git a/includes/profiler/SectionProfiler.php b/includes/profiler/SectionProfiler.php
new file mode 100644 (file)
index 0000000..a418d30
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+/**
+ * Arbitrary section name based PHP profiling.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Profiler
+ * @author Aaron Schulz
+ */
+
+/**
+ * Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle
+ *
+ * @TODO: refactor implementation by moving Profiler code to here when non-automatic
+ * profiler support is dropped.
+ *
+ * @since 1.25
+ */
+class SectionProfiler {
+       /** @var ProfilerStandard */
+       protected $profiler;
+
+       public function __construct() {
+               // This does *not* care about PHP request start time
+               $this->profiler = new ProfilerStandard( array( 'initTotal' => false ) );
+       }
+
+       /**
+        * @param string $section
+        * @return ScopedCallback
+        */
+       public function scopedProfileIn( $section ) {
+               $profiler = $this->profiler;
+               $sc = new ScopedCallback( function() use ( $profiler, $section ) {
+                       $profiler->profileOut( $section );
+               } );
+               $profiler->profileIn( $section );
+
+               return $sc;
+       }
+
+       /**
+        * @param ScopedCallback $section
+        */
+       public function scopedProfileOut( ScopedCallback &$section ) {
+               $section = null;
+       }
+
+       /**
+        * Get the raw and collated breakdown data for each method
+        *
+        * The percent time for each time is based on the current "total" time
+        * used is based on all methods so far. This method can therefore be
+        * called several times in between several profiling calls without the
+        * delays in usage of the profiler skewing the results. A "-total" entry
+        * is always included in the results.
+        *
+        * @return array List of method entries arrays, each having:
+        *   - name     : method name
+        *   - calls    : the number of method calls
+        *   - elapsed  : real time ellapsed (ms)
+        *   - percent  : percent real time
+        *   - memory   : memory used (bytes)
+        *   - min      : min real time of all calls (ms)
+        *   - max      : max real time of all calls (ms)
+        */
+       public function getFunctionStats() {
+               $data = $this->profiler->getRawData();
+
+               $memoryTotal = 0;
+               $elapsedTotal = 0;
+               foreach ( $data as $item ) {
+                       $memoryTotal += $item['memory'];
+                       $elapsedTotal += $item['elapsed'];
+               }
+
+               foreach ( $data as &$item ) {
+                       $item['percent'] = $item['elapsed'] / $elapsedTotal * 100;
+               }
+               unset( $item );
+
+               $data[] = array(
+                       'name' => '-total',
+                       'calls' => 1,
+                       'elapsed' => $elapsedTotal,
+                       'percent' => 100,
+                       'memory' => $memoryTotal,
+                       'min' => $elapsedTotal,
+                       'max' => $elapsedTotal
+               );
+
+               return $data;
+       }
+}
index 1a09818..39e62f5 100644 (file)
@@ -279,27 +279,22 @@ class KkConverter extends LanguageConverter {
         * @return string
         */
        function translate( $text, $toVariant ) {
-               global $wgLanguageCode;
                $text = parent::translate( $text, $toVariant );
 
                switch ( $toVariant ) {
                        case 'kk-cyrl':
                        case 'kk-kz':
                                $letters = KK_L_UC . KK_L_LC . 'ʺʹ#0123456789';
-                               $wgLanguageCode = 'kk';
                                break;
                        case 'kk-latn':
                        case 'kk-tr':
                                $letters = KK_C_UC . KK_C_LC . '№0123456789';
-                               $wgLanguageCode = 'kk-Latn';
                                break;
                        case 'kk-arab':
                        case 'kk-cn':
                                $letters = KK_C_UC . KK_C_LC . /*KK_L_UC.KK_L_LC.'ʺʹ'.*/',;\?%\*№0123456789';
-                               $wgLanguageCode = 'kk-Arab';
                                break;
                        default:
-                               $wgLanguageCode = 'kk';
                                return $text;
                }
                // disable conversion variables like $1, $2...
index 622494c..56e6cbc 100644 (file)
        "filedeleteerror": "Немагчыма выдаліць файл «$1».",
        "directorycreateerror": "Немагчыма стварыць дырэкторыю «$1».",
        "directoryreadonlyerror": "Тэчка «$1» толькі для чытаньня.",
+       "directorynotreadableerror": "Тэчка «$1» не чытаецца.",
        "filenotfound": "Немагчыма знайсьці файл «$1».",
        "unexpected": "Нечаканае значэньне: «$1»=«$2».",
        "formerror": "Памылка: не атрымалася адаслаць зьвесткі формы",
index 95472a5..f36a000 100644 (file)
        "november-date": "Tışrino Peyên $1",
        "december-date": "Kanun $1",
        "pagecategories": "{{PLURAL:$1|Kategoriye|Kategoriy}}",
-       "category_header": "Pelê ke kategoriya \"$1\" miyan derê",
+       "category_header": "Pelê ke kategoriya \"$1\" derê",
        "subcategories": "Kategoriyê bınêni",
-       "category-media-header": "Dosyeyê ke kategoriya \"$1\" miyan derê",
+       "category-media-header": "Dosyeyê ke kategoriya \"$1\" derê",
        "category-empty": "''Ena kategoriye de hewna qet nuştey ya zi medya çıniyê.''",
        "hidden-categories": "{{PLURAL:$1|Kategoriya nımıtiye|Kategoriyê nımıtey}}",
        "hidden-category-category": "Kategoriyê nımıtey",
index 15eeca8..0930132 100644 (file)
        "filerenameerror": "Impossible de renommer le fichier « $1 » en « $2 ».",
        "filedeleteerror": "Impossible de supprimer le fichier « $1 ».",
        "directorycreateerror": "Impossible de créer le dossier « $1 ».",
+       "directoryreadonlyerror": "Le répertoire « $1 » est en lecture seule.",
+       "directorynotreadableerror": "Le répertoire « $1 » n’est pas lisible.",
        "filenotfound": "Impossible de trouver le fichier « $1 ».",
        "unexpected": "Valeur inattendue : « $1 » = « $2 ».",
        "formerror": "Erreur : Impossible de soumettre le formulaire.",
index f422a47..dd14b35 100644 (file)
        "nov": "Teş",
        "dec": "Gağ",
        "pagecategories": "{{PLURAL:$1|Kategoriye|Kategoriy}}",
-       "category_header": "Pelê ke kategoriya \"$1\" miyan derê",
+       "category_header": "Pelê ke kategoriya \"$1\" derê",
        "subcategories": "Kategoriyê bınêni",
        "category-media-header": "Medyawa ke kategoriya \"$1\" dera",
        "category-empty": "''Na kategoriye de hona qet nustey ya ki medya çinê.''",
index 47e8040..5d1bffb 100644 (file)
        "filerenameerror": "A l'é pa podusse cangeje nòm a l'archivi «$1» an «$2».",
        "filedeleteerror": "A l'é pa podusse scancelé l'archivi «$1».",
        "directorycreateerror": "A l'é pa podusse creé ël dossié «$1».",
+       "directoryreadonlyerror": "Ël dossié «$1» a l'é mach an letura.",
+       "directorynotreadableerror": "Ël dossié «$1» as peul nen les-se.",
        "filenotfound": "A l'é pa trovasse l'archivi «$1».",
        "unexpected": "Valor che i së spetavo pa: «$1»=«$2».",
        "formerror": "Eror: A l'é nen podusse mandé ël formolari.",
index 4747ded..fc640b9 100644 (file)
        "filerenameerror": "Não foi possível alterar o nome do ficheiro \"$1\" para \"$2\".",
        "filedeleteerror": "Não foi possível eliminar o ficheiro \"$1\".",
        "directorycreateerror": "Não foi possível criar o diretório \"$1\".",
+       "directoryreadonlyerror": "O diretório \"$1\" é apenas de leitura.",
+       "directorynotreadableerror": "O diretório \"$1\" não é legível.",
        "filenotfound": "Não foi possível encontrar o ficheiro \"$1\".",
        "unexpected": "Valor não esperado: \"$1\"=\"$2\".",
        "formerror": "Erro: Não foi possível enviar o formulário",
index 984ed55..dd90bc9 100644 (file)
        "filerenameerror": "Невозможно переименовать файл «$1» в «$2».",
        "filedeleteerror": "Невозможно удалить файл «$1».",
        "directorycreateerror": "Невозможно создать директорию «$1».",
+       "directoryreadonlyerror": "Каталог «$1» доступен только для чтения.",
+       "directorynotreadableerror": "Каталог «$1» не читается.",
        "filenotfound": "Невозможно найти файл «$1».",
        "unexpected": "Неподходящее значение: «$1»=«$2».",
        "formerror": "Ошибка: невозможно передать данные формы",
index 3969358..7892473 100644 (file)
@@ -16,7 +16,7 @@
        "tog-showtoolbar": "Aratâ bara di halati trâ alâxiri",
        "tog-editondblclick": "Alâxeaști frândzâli pri-tu click duplo",
        "tog-editsectiononrightclick": "Activeadzâ alâxirea a secțiuniloru pri-tu click ndreapta pi titlu a secțiunâľei",
-       "tog-watchcreations": "Adavgâ frândzâli pi cari li adar și fișierele pi cari li ncari la lista a ńia di frândzâ avinati",
+       "tog-watchcreations": "Adavgâ frândzâli pi cari li adaru și fișierili pi cari li ancarcu la lista a ńia di frândzâ avinati",
        "tog-watchdefault": "Adavgâ frândzâli și fișierili pi cari li alâxescu la lista a ńia di avinari",
        "tog-watchmoves": "Adavgâ frândzâli și fișierili pi cari li dau numâ noao la lista a ńia di avinari",
        "tog-watchdeletion": "Adavgâ frândzâli și fișierili pi cari li aștergu la lista a ńia di avinari",
        "readonly": "Baza di dati easti blocatâ (ncľisâ) la nyrâpseari",
        "missingarticle-rev": "(versiuniľea#: $1)",
        "missingarticle-diff": "(Dif: $1, $2)",
+       "internalerror": "Sfalmâ di nuntru",
+       "internalerror_info": "Sfalmâ di nuntru: $1",
+       "filecopyerror": "Fișierlu \"$1\" nu putu s-hibâ copiatu \"$2\".",
+       "filerenameerror": "Fișierlu \"$1\" nu putu s-hibâ mutatu \"$2\".",
+       "filedeleteerror": "Fișierlu \"$1\" nu si putu s-hibâ aștersu.",
+       "directorycreateerror": "Nu s-poati si s-facâ directorlu \"$1\".",
+       "directoryreadonlyerror": "Directorlu \"$1\" easti mași trâ adghivâsiri.",
+       "directorynotreadableerror": "Directorlu \"$1\" nu s-poati si s-adghivâseascâ.",
+       "filenotfound": "Fișierlu \"$1\" nu si putu s-hibâ aflatu.",
        "badtitle": "Titlu alatusu",
        "viewsource": "Vez-u textu",
        "viewsource-title": "Vedz ivurlu trâ $1",
        "userlogin-yourname-ph": "Bagâ-u numa a ta di utilizatoru",
        "createacct-another-username-ph": "Bagâ-u numa di utilizatoru",
        "yourpassword": "Zboru cľeae:",
-       "userlogin-yourpassword": "Zboarâ acrifo (parolâ)",
+       "userlogin-yourpassword": "Zboru acrifo (parolâ)",
        "userlogin-yourpassword-ph": "Bagâ-u parola (zboru acrifo)",
        "createacct-yourpassword-ph": "Bagâ-u parola (zboru acrifo)",
        "yourpasswordagain": "Bagâ-u cľeae diznou:",
        "gotaccountlink": "Leagâ-ti",
        "userlogin-resetlink": "U agârșii parola i numa di utilizatoru?",
        "userlogin-resetpassword-link": "U agârșii parola?",
+       "userlogin-helplink2": "Agiutoru la ligari",
+       "userlogin-createanother": "Adrari contu nou",
        "createacct-emailrequired": "Adresâ di carti electronicâ",
        "createacct-emailoptional": "Adresâ di carti electronicâ (opționalu)",
        "createacct-email-ph": "Bagâ-u adresa a ta di carti electronicâ",
        "createacct-another-email-ph": "Bagâ-u adresa di carti electronicâ",
+       "createaccountmail": "Ufiliseaști unâ parolâ (zboru acrifo) pirastica și u pitreați la adresa di e-mail cari u dzâț tini",
        "createacct-realname": "Numa realâ (opționalu)",
        "createaccountreason": "Furńie:",
        "createacct-reason": "Furńie",
+       "createacct-reason-ph": "Câ ți feci altu contu",
+       "createacct-captcha": "Duchimie (provâ) di securitati",
+       "createacct-imgcaptcha-ph": "Bagâ lu textu pi cari lu vedz disuprâ",
+       "createacct-submit": "Adrari contu-ț",
+       "createacct-another-submit": "Adrari altu contu",
+       "createacct-benefit-heading": "{{SITENAME}} s-feasi cu oamińi ca tini.",
+       "createacct-benefit-body1": "{{PLURAL:$1|alâxiri|alâxiri|di alâxiri}}",
+       "createacct-benefit-body2": "{{PLURAL:$1|frândzâ|frândzâ|de frândzâ}}",
+       "createacct-benefit-body3": "{{PLURAL:$1|contribuitoru proaspitu|contribuitori proaspiț|di contribuitori proaspiț}}",
+       "badretype": "Zboarâli acrifo pi cari lâ bâgaș nu suntu unâ.",
+       "userexists": "Numa di utilizatoru pi cari u bâgaș ari nica aoa. Ti oru, ľea altâ numâ.",
+       "loginerror": "Sfalmâ di ligari",
+       "createacct-error": "Sfalmâ la adrarea a contlui",
+       "createaccounterror": "Nu s-putu si s-facâ contu: $1",
+       "loginsuccesstitle": "Ligarea s-bitisi ghini",
+       "loginsuccess": "'''Ti ligai la {{SITENAME}} ca „$1”.'''",
+       "loginlanguagelabel": "Limbâ: $1",
+       "pt-login": "Leagâ-ti",
+       "pt-login-button": "Leagâ-ti",
+       "pt-createaccount": "Fă contu (isape)",
+       "pt-userlogout": "Dizleagâ-ti",
+       "bold_sample": "Scriari groasâ (bold)",
+       "bold_tip": "Scriari groasâ (bold)",
+       "italic_sample": "Scriari aplicatâ (italic)",
+       "italic_tip": "Scriari aplicatâ (italic)",
+       "link_sample": "Titlu a ligâturiľei",
+       "link_tip": "Ligâturâ di nuntru",
+       "extlink_sample": "http://www.example.com titlu a ligâturiľei",
        "summary": "Rezumatu:",
        "minoredit": "Aestâ easti unâ alâxiri minorâ (ńicâ)",
        "watchthis": "Mutrea-u frândzâ aestâ",
index 662f189..a03a4cb 100644 (file)
        "edit-gone-missing": "Mpussìbbili aggiurnari la pàggina.\nPari ca fu scancillata.",
        "edit-conflict": "Cunflittu di edizzioni.",
        "edit-no-change": "La mudifica fu ignurata pirchì nu foru appurtati canci ntô testu.",
+       "postedit-confirmation-created": "La pàggina fu criata.",
        "postedit-confirmation-saved": "Lu canciamentu fu sarbatu.",
        "edit-already-exists": "Mpussìbbili criari na pàggina nova.\nEsisti ggià.",
        "expensive-parserfunction-warning": "Accura: Sta pàggina cunteni troppi chiamati ê parser functions.\n\nAvissi a èssiri menu di $2, ô mumentu ci {{PLURAL:$1|nn'è $1|nni sunnu $1}}.",
index 8dcc5d8..879e8ef 100644 (file)
        "edit-no-change": "Текстта үзгәешләр ясалмау сәбәпле, сезнең үзгәртү кире кагыла.",
        "edit-already-exists": "Яңа бит төзеп булмый.\nУл инде бар.",
        "editwarning-warning": "Башка биткә күчү вакытында бу мәкаләгә керткән үзгәрешләр югалырга мөмкин.\nӘгәрдә сез теркәлгән булсагыз, бу искәрмәне сез «Көйләнмәләрем» өлешендә үзгәртә аласыз.",
+       "duplicate-args-category": "Калыпны чакыруда кабатлап торган аргументларны кулланган битләр",
        "expensive-parserfunction-warning": "'''Игътибар:''' бу биттә хәтерне еш кулланучы функцияләр артык күп.\n\nЧикләү: $2 {{PLURAL:$2|1=куллану}}, бу очракта {{PLURAL:$1|$1 тапкыр}} башкарырга рөхсәт ителә.",
        "expensive-parserfunction-category": "Хәтерне еш кулланучы функцияләр күп булган битләр",
        "post-expand-template-inclusion-warning": "'''Игътибар:''' Кулланылучы үрнәкләр артык зур.\nКайберләре кабызылмаячак.",
index 39a573a..4cb1ff4 100644 (file)
        "content-model-javascript": "JavaScript",
        "content-model-css": "CSS",
        "duplicate-args-category": "调用重复模板参数的页面",
-       "duplicate-args-category-desc": "页面包含使用重复参数的模板调用,例如<code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code>或<code><nowiki>{{foo|bar|1=baz}}</nowiki></code>。",
+       "duplicate-args-category-desc": "页面包含调用了重复参数的模板,例如<code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code>或<code><nowiki>{{foo|bar|1=baz}}</nowiki></code>。",
        "expensive-parserfunction-warning": "<strong>警告:</strong>这个页面有太多高昂的语法功能调用。\n\n它应该少过$2次呼叫,现在有$1次呼叫。",
        "expensive-parserfunction-category": "页面中有太多耗费的语法功能呼叫",
        "post-expand-template-inclusion-warning": "'''警告:'''包含模板大小过大。\n一些模板将不会包含。",
index 60dea73..9545173 100644 (file)
        "projectpage": "檢視專案頁面",
        "imagepage": "檢視檔案頁面",
        "mediawikipage": "檢視訊息頁面",
-       "templatepage": "檢視樣頁面",
+       "templatepage": "檢視樣頁面",
        "viewhelppage": "檢視說明頁面",
        "categorypage": "檢視分類頁面",
        "viewtalkpage": "檢視討論頁面",
        "nstab-project": "專案頁面",
        "nstab-image": "檔案",
        "nstab-mediawiki": "訊息",
-       "nstab-template": "樣",
+       "nstab-template": "樣",
        "nstab-help": "說明頁面",
        "nstab-category": "分類",
        "nosuchaction": "無此動作",
        "cascadeprotectedwarning": "<strong>警告:</strong>本頁已經被保護,只有擁有管理員權限的使用者才可編輯,此頁面被下列頁面引用因此連鎖保護:",
        "titleprotectedwarning": "<strong>警告:本頁面已被保護,需要 [[Special:ListGroupRights|特殊權限]] 方可建立。</strong>\n以下提供最近的日誌以便參考:",
        "templatesused": "此頁面使用了以下{{PLURAL:$1|樣版}}:",
-       "templatesusedpreview": "此預覽使用了以下{{PLURAL:$1|樣}}:",
+       "templatesusedpreview": "此預覽使用了以下{{PLURAL:$1|樣}}:",
        "templatesusedsection": "此頁面使用了以下{{PLURAL:$1|樣版}}:",
        "template-protected": "(受保護)",
        "template-semiprotected": "(受半保護)",
        "duplicate-args-category-desc": "該頁面包含重複使用參數的樣版呼叫,如 <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> 或 <code><nowiki>{{foo|bar|1=baz}}</nowiki>。",
        "expensive-parserfunction-warning": "<strong>警告:</strong>此頁面使用了太多消耗系統資源的解析函數。\n\n使用次數應小於 $2 次,但目前使用了 $1 次。",
        "expensive-parserfunction-category": "使用了太多消耗系統資源的分析函數的頁面",
-       "post-expand-template-inclusion-warning": "<strong>警告:</strong>引用樣板後大小超出限制。\n部份樣內容將不會被使用。",
+       "post-expand-template-inclusion-warning": "<strong>警告:</strong>引用樣板後大小超出限制。\n部份樣內容將不會被使用。",
        "post-expand-template-inclusion-category": "引用樣板後大小超出限制的頁面",
        "post-expand-template-argument-warning": "<strong>警告:</strong>此頁面有一個以上的樣版參數過長。\n過長的參數會被直接忽略。",
-       "post-expand-template-argument-category": "樣參數有部份被忽略的頁面",
-       "parser-template-loop-warning": "偵測到樣遞迴:[[$1]]",
+       "post-expand-template-argument-category": "樣參數有部份被忽略的頁面",
+       "parser-template-loop-warning": "偵測到樣遞迴:[[$1]]",
        "parser-template-recursion-depth-warning": "超出樣版遞迴深度限制 ($1)",
        "language-converter-depth-warning": "已超出語言轉換器深度限制 ($1)",
        "node-count-exceeded-category": "節點數量超出限制的頁面",
        "listduplicatedfiles": "重複檔案清單",
        "listduplicatedfiles-summary": "此清單中包含最新版本的檔案與其他檔案重複的清單,本清單只顯示本地檔案。",
        "listduplicatedfiles-entry": "[[:File:$1|$1]] 有[[$3|其他 $2 個重複檔案]]。",
-       "unusedtemplates": "未使用的樣",
+       "unusedtemplates": "未使用的樣",
        "unusedtemplatestext": "此頁面列出所有於 {{ns:template}} 命名空間下未被其他頁面引用的樣版。\n在刪除前,仍需檢查是否有連結這些樣版的其他頁面。",
        "unusedtemplateswlh": "其他連結",
        "randompage": "隨機頁面",
        "linksearch-pat": "搜尋關鍵字:",
        "linksearch-ns": "命名空間:",
        "linksearch-ok": "搜尋",
-       "linksearch-text": "可使用萬用字元如 \"*.wikipedia.org\"。\n萬用字元必須使用在最上層網域,例如 \"*.org\".<br />\n支援的{{PLURAL:$2|通訊協定}}有:<code>$1</code> (若未指定則預設使用 http:// 通訊協定)。",
+       "linksearch-text": "可使用萬用字元如 *.wikipedia.org。\n萬用字元必須使用在最上層網域,例如 *.org 。<br />\n支援的{{PLURAL:$2|通訊協定}}有:<code>$1</code> (若未指定則預設使用 http:// 通訊協定)。",
        "linksearch-line": "$1 由 $2 所連結",
        "linksearch-error": "萬用字元僅可在主機名稱的開頭使用。",
        "listusersfrom": "顯示使用者開始自:",
        "unblock": "解除封鎖使用者",
        "blockip": "封鎖{{GENDER:$1|使用者}}",
        "blockip-legend": "封鎖使用者",
-       "blockiptext": "填寫以下單據可封鎖特定 IP 位址或使用者名稱的存取權限。\n這個動作應用來避免破壞行為,可根據 [[{{MediaWiki:Policy-url}}|管理政策]]。\n請在下方填寫一個具體的原因 (例如:引述一段破壞頁面的事實)。",
+       "blockiptext": "填寫以下表單可封鎖特定 IP 位址或使用者名稱的存取權限。\n這個動作應用來避免破壞行為,可根據 [[{{MediaWiki:Policy-url}}|管理政策]]。\n請在下方填寫一個具體的原因 (例如:引述一段破壞頁面的事實)。",
        "ipaddressorusername": "IP 位址或使用者名稱:",
        "ipbexpiry": "期限:",
        "ipbreason": "原因:",
        "export-addnstext": "使用命名空間新增頁面:",
        "export-addns": "新增",
        "export-download": "儲存為檔案",
-       "export-templates": "包含樣",
+       "export-templates": "包含樣",
        "export-pagelinks": "包含連結的頁面深度:",
        "allmessages": "系統訊息",
        "allmessagesname": "名稱",
        "import-interwiki-sourcewiki": "來源 Wiki:",
        "import-interwiki-sourcepage": "來源頁面:",
        "import-interwiki-history": "複製此頁的所有歷史修訂",
-       "import-interwiki-templates": "包含所有樣",
+       "import-interwiki-templates": "包含所有樣",
        "import-interwiki-submit": "匯入",
        "import-interwiki-namespace": "目標命名空間:",
        "import-interwiki-rootpage": "目標根頁面 (選填):",
        "tooltip-ca-nstab-project": "檢視專案頁面",
        "tooltip-ca-nstab-image": "檢視檔案頁面",
        "tooltip-ca-nstab-mediawiki": "檢視系統訊息",
-       "tooltip-ca-nstab-template": "檢視樣",
+       "tooltip-ca-nstab-template": "檢視樣",
        "tooltip-ca-nstab-help": "檢視說明頁面",
        "tooltip-ca-nstab-category": "檢視分類頁面",
        "tooltip-minoredit": "標記為小修訂",
        "confirmemail_success": "您的電子郵件已經被確認。您現在可以[[Special:UserLogin|登入]]並使用此網站了。",
        "confirmemail_loggedin": "已確認您的電子郵件位址。",
        "confirmemail_subject": "{{SITENAME}} 電子郵件位址確認",
-       "confirmemail_body": "不明人士 (可能是您自己,來自 IP 位址 $1)  已在 {{SITENAME}} 註冊了一個帳號 \"$2\" 並使用了此電子郵件位址。\n\n請確認這個帳號是屬於您的,並使用瀏覽器開啟下方連結以啟用在 {{SITENAME}} 上的電子郵件功能:\n\n$3\n\n若您 *未* 註冊此帳號,\n請開啟下方連結取消電子郵件確認:\n\n$5\n\n此確認代碼會於 $4 過期。",
+       "confirmemail_body": "不明人士(可能是您自己,來自 IP 位址 $1 )已在 {{SITENAME}} 註冊了一個帳號 \"$2\" 並使用了此電子郵件位址。\n\n請確認這個帳號是屬於您的,並使用瀏覽器開啟下方連結以啟用在 {{SITENAME}} 上的電子郵件功能:\n\n$3\n\n若您 *未* 註冊此帳號,\n請開啟下方連結取消電子郵件確認:\n\n$5\n\n此確認代碼會於 $4 過期。",
        "confirmemail_body_changed": "不明人士 (可能是您自己,來自 IP 位址 $1)  已將在 {{SITENAME}} 帳號 \"$2\" 的電子郵件位址更改至此。\n\n請確認這個帳號是屬於您的,並使用瀏覽器開啟下方連結以啟用在 {{SITENAME}} 上的電子郵件功能:\n\n$3\n\n若您 *未* 註冊此帳號,\n請開啟下方連結取消電子郵件確認:\n\n$5\n\n此確認代碼會於 $4 過期。",
-       "confirmemail_body_set": "不明人士 (可能是您自己,來自 IP 位址 $1)  已將在 {{SITENAME}} 帳號 \"$2\" 的電子郵件位址設定至此。\n\n請確認這個帳號是屬於您的,並使用瀏覽器開啟下方連結以啟用在 {{SITENAME}} 上的電子郵件功能:\n\n$3\n\n若您 *未* 註冊此帳號,\n請開啟下方連結取消電子郵件確認:\n\n$5\n\n此確認代碼會於 $4 過期。",
+       "confirmemail_body_set": "不明人士(可能是您自己,來自 IP 位址 $1 )已將在 {{SITENAME}} 帳號 \"$2\" 的電子郵件位址設定至此。\n\n請確認這個帳號是屬於您的,並使用瀏覽器開啟下方連結以啟用在 {{SITENAME}} 上的電子郵件功能:\n\n$3\n\n若您 *未* 註冊此帳號,\n請開啟下方連結取消電子郵件確認:\n\n$5\n\n此確認代碼會於 $4 過期。",
        "confirmemail_invalidated": "已取消電子郵件位址確認",
        "invalidateemail": "取消電子郵件確認",
        "scarytranscludedisabled": "[Interwiki 轉換代碼不可用]",
-       "scarytranscludefailed": "[樣 $1 讀取失敗]",
-       "scarytranscludefailed-httpstatus": "[樣 $1 讀取失敗:HTTP $2]",
+       "scarytranscludefailed": "[樣 $1 讀取失敗]",
+       "scarytranscludefailed-httpstatus": "[樣 $1 讀取失敗:HTTP $2]",
        "scarytranscludetoolong": "[URL 過長]",
        "deletedwhileediting": "<strong>警告:</strong>此頁在您開始編輯之後已經被刪除﹗",
        "confirmrecreate": "在您編輯的同時,使用者 [[User:$1|$1]] ([[User talk:$1|對話]]) 刪除了此頁面,原因為:\n: <em>$2</em>\n請確認您是否真的要重新建立此頁面。",
        "redirect-revision": "頁面修訂 ID",
        "redirect-file": "檔案名稱",
        "redirect-not-exists": "查無值",
-       "fileduplicatesearch": "æ\90\9cå°\8bé\87\8dè¦\86檔案",
+       "fileduplicatesearch": "æ\90\9cå°\8bé\87\8dè¤\87檔案",
        "fileduplicatesearch-summary": "依據雜湊值 (Hash) 來搜尋重複的檔案。",
        "fileduplicatesearch-legend": "搜尋重覆",
        "fileduplicatesearch-filename": "檔案名稱:",
        "fileduplicatesearch-submit": "搜尋",
        "fileduplicatesearch-info": "$1 × $2 像素<br />檔案大小:$3<br />MIME 類型:$4",
-       "fileduplicatesearch-result-1": "æª\94æ¡\88 $1 ç\84¡é\87\8dè¦\86的檔案。",
+       "fileduplicatesearch-result-1": "æª\94æ¡\88 $1 ç\84¡é\87\8dè¤\87的檔案。",
        "fileduplicatesearch-result-n": "檔案 $1 有 $2 筆重覆的檔案。",
        "fileduplicatesearch-noresults": "查無名稱為 \"$1\" 的檔案。",
        "specialpages": "特殊頁面",
        "limitreport-ppgeneratednodes": "預處理器產生節點次數",
        "limitreport-postexpandincludesize": "展開後的引用大小",
        "limitreport-postexpandincludesize-value": "$1/$2 個{{PLURAL:$2|位元組}}",
-       "limitreport-templateargumentsize": "樣參數大小",
+       "limitreport-templateargumentsize": "樣參數大小",
        "limitreport-templateargumentsize-value": "$1/$2 個{{PLURAL:$2|位元組}}",
        "limitreport-expansiondepth": "最高展開深度",
        "limitreport-expensivefunctioncount": "高消耗解析器函數次數",
-       "expandtemplates": "展開樣",
+       "expandtemplates": "展開樣",
        "expand_templates_intro": "本特殊頁面會將文字中的樣版展開,可以包含支援的解析器語法,如 <code><nowiki>{{</nowiki>#language:…}}</code> 與變數如 <code><nowiki>{{</nowiki>CURRENTDAY}}</code>。\n實際上,絕大部分在雙括號中的內容都會被展開。",
        "expand_templates_title": "上下文標題,用於 {{FULLPAGENAME}} 等:",
        "expand_templates_input": "輸入文字:",
diff --git a/tests/phpunit/includes/libs/XhprofTest.php b/tests/phpunit/includes/libs/XhprofTest.php
new file mode 100644 (file)
index 0000000..e0e68e0
--- /dev/null
@@ -0,0 +1,312 @@
+<?php
+/**
+ * @section LICENSE
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @uses Xhprof
+ * @uses AutoLoader
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.25
+ */
+class XhprofTest extends PHPUnit_Framework_TestCase {
+
+       public function setUp() {
+               if ( !function_exists( 'xhprof_enable' ) ) {
+                       $this->markTestSkipped( 'No xhprof support detected.' );
+               }
+       }
+
+       /**
+        * @covers Xhprof::splitKey
+        * @dataProvider provideSplitKey
+        */
+       public function testSplitKey( $key, $expect ) {
+               $this->assertSame( $expect, Xhprof::splitKey( $key ) );
+       }
+
+       public function provideSplitKey() {
+               return array(
+                       array( 'main()', array( null, 'main()' ) ),
+                       array( 'foo==>bar', array( 'foo', 'bar' ) ),
+                       array( 'bar@1==>bar@2', array( 'bar@1', 'bar@2' ) ),
+                       array( 'foo==>bar==>baz', array( 'foo', 'bar==>baz' ) ),
+                       array( '==>bar', array( '', 'bar' ) ),
+                       array( '', array( null, '' ) ),
+               );
+       }
+
+       /**
+        * @covers Xhprof::__construct
+        * @covers Xhprof::stop
+        * @covers Xhprof::getRawData
+        * @dataProvider provideRawData
+        */
+       public function testRawData( $flags, $keys ) {
+               $xhprof = new Xhprof( array( 'flags' => $flags ) );
+               $raw = $xhprof->getRawData();
+               $this->assertArrayHasKey( 'main()', $raw );
+               foreach ( $keys as $key ) {
+                       $this->assertArrayHasKey( $key, $raw['main()'] );
+               }
+       }
+
+       public function provideRawData() {
+               return array(
+                       array( 0, array( 'ct', 'wt' ) ),
+                       array( XHPROF_FLAGS_MEMORY, array( 'ct', 'wt', 'mu', 'pmu' ) ),
+                       array( XHPROF_FLAGS_CPU, array( 'ct', 'wt', 'cpu' ) ),
+                       array( XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU, array(
+                               'ct', 'wt', 'mu', 'pmu', 'cpu',
+                       ) ),
+               );
+       }
+
+       /**
+        * @covers Xhprof::pruneData
+        */
+       public function testInclude() {
+               $xhprof = $this->getXhprofFixture( array(
+                       'include' => array( 'main()' ),
+               ) );
+               $raw = $xhprof->getRawData();
+               $this->assertArrayHasKey( 'main()', $raw );
+               $this->assertArrayHasKey( 'main()==>foo', $raw );
+               $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
+               $this->assertSame( 3, count( $raw ) );
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers Xhprof::getInclusiveMetrics
+        */
+       public function testInclusiveMetricsStructure() {
+               $metricStruct = array(
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+               );
+               $statStruct = array(
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+               );
+
+               $xhprof = $this->getXhprofFixture();
+               $metrics = $xhprof->getInclusiveMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( $type === 'array' ) {
+                                       $this->assertArrayStructure( $statStruct, $metric[$key] );
+                                       if ( $name === 'main()' ) {
+                                               $this->assertEquals( 100, $metric[$key]['percent'] );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers Xhprof::getCompleteMetrics
+        */
+       public function testCompleteMetricsStructure() {
+               $metricStruct = array(
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+                       'calls' => 'array',
+                       'subcalls' => 'array',
+               );
+               $statsMetrics = array( 'wt', 'cpu', 'mu', 'pmu' );
+               $statStruct = array(
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+                       'exclusive' => 'numeric',
+               );
+
+               $xhprof = $this->getXhprofFixture();
+               $metrics = $xhprof->getCompleteMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric, $name );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( in_array( $key, $statsMetrics ) ) {
+                                       $this->assertArrayStructure(
+                                               $statStruct, $metric[$key], $key
+                                       );
+                                       $this->assertLessThanOrEqual(
+                                               $metric[$key]['total'], $metric[$key]['exclusive']
+                                       );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @covers Xhprof::getCallers
+        * @covers Xhprof::getCallees
+        * @uses Xhprof
+        */
+       public function testEdges() {
+               $xhprof = $this->getXhprofFixture();
+               $this->assertSame( array(), $xhprof->getCallers( 'main()' ) );
+               $this->assertSame( array( 'foo', 'xhprof_disable' ),
+                       $xhprof->getCallees( 'main()' )
+               );
+               $this->assertSame( array( 'main()' ),
+                       $xhprof->getCallers( 'foo' )
+               );
+               $this->assertSame( array(), $xhprof->getCallees( 'strlen' ) );
+       }
+
+       /**
+        * @covers Xhprof::getCriticalPath
+        * @uses Xhprof
+        */
+       public function testCriticalPath() {
+               $xhprof = $this->getXhprofFixture();
+               $path = $xhprof->getCriticalPath();
+
+               $last = null;
+               foreach ( $path as $key => $value ) {
+                       list( $func, $call ) = Xhprof::splitKey( $key );
+                       $this->assertSame( $last, $func );
+                       $last = $call;
+               }
+               $this->assertSame( $last, 'bar@1' );
+       }
+
+       /**
+        * Get an Xhprof instance that has been primed with a set of known testing
+        * data. Tests for the Xhprof class should laregly be concerned with
+        * evaluating the manipulations of the data collected by xhprof rather
+        * than the data collection process itself.
+        *
+        * The returned Xhprof instance primed will be with a data set created by
+        * running this trivial program using the PECL xhprof implementation:
+        * @code
+        * function bar( $x ) {
+        *   if ( $x > 0 ) {
+        *     bar($x - 1);
+        *   }
+        * }
+        * function foo() {
+        *   for ( $idx = 0; $idx < 2; $idx++ ) {
+        *     bar( $idx );
+        *     $x = strlen( 'abc' );
+        *   }
+        * }
+        * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
+        * foo();
+        * $x = xhprof_disable();
+        * var_export( $x );
+        * @endcode
+        *
+        * @return Xhprof
+        */
+       protected function getXhprofFixture( array $opts = array() ) {
+               $xhprof = new Xhprof( $opts );
+               $xhprof->loadRawData( array (
+                       'foo==>bar' => array (
+                               'ct' => 2,
+                               'wt' => 57,
+                               'cpu' => 92,
+                               'mu' => 1896,
+                               'pmu' => 0,
+                       ),
+                       'foo==>strlen' => array (
+                               'ct' => 2,
+                               'wt' => 21,
+                               'cpu' => 141,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ),
+                       'bar==>bar@1' => array (
+                               'ct' => 1,
+                               'wt' => 18,
+                               'cpu' => 19,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ),
+                       'main()==>foo' => array (
+                               'ct' => 1,
+                               'wt' => 304,
+                               'cpu' => 307,
+                               'mu' => 4008,
+                               'pmu' => 0,
+                       ),
+                       'main()==>xhprof_disable' => array (
+                               'ct' => 1,
+                               'wt' => 8,
+                               'cpu' => 10,
+                               'mu' => 768,
+                               'pmu' => 392,
+                       ),
+                       'main()' => array (
+                               'ct' => 1,
+                               'wt' => 353,
+                               'cpu' => 351,
+                               'mu' => 6112,
+                               'pmu' => 1424,
+                       ),
+               ) );
+               return $xhprof;
+       }
+
+       /**
+        * Assert that the given array has the described structure.
+        *
+        * @param array $struct Array of key => type mappings
+        * @param array $actual Array to check
+        * @param string $label
+        */
+       protected function assertArrayStructure( $struct, $actual, $label = null ) {
+               $this->assertInternalType( 'array', $actual, $label );
+               $this->assertCount( count($struct), $actual, $label );
+               foreach ( $struct as $key => $type ) {
+                       $this->assertArrayHasKey( $key, $actual );
+                       $this->assertInternalType( $type, $actual[$key] );
+               }
+       }
+}