<?php1// Copyright (c) 2009 Facebook2//3// Licensed under the Apache License, Version 2.0 (the "License");4// you may not use this file except in compliance with the License.5// You may obtain a copy of the License at6//7// http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// distributed under the License is distributed on an "AS IS" BASIS,11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12// See the License for the specific language governing permissions and13// limitations under the License.14//1516//17// This file contains various XHProf library (utility) functions.18// Do not add any display specific code here.19//2021function xhprof_error($message) {22error_log($message);23}2425/*26* The list of possible metrics collected as part of XHProf that27* require inclusive/exclusive handling while reporting.28*29* @author Kannan30*/31function xhprof_get_possible_metrics() {32static $possible_metrics =33array("wt" => array("Wall", "microsecs", "walltime" ),34"ut" => array("User", "microsecs", "user cpu time" ),35"st" => array("Sys", "microsecs", "system cpu time"),36"cpu" => array("Cpu", "microsecs", "cpu time"),37"mu" => array("MUse", "bytes", "memory usage"),38"pmu" => array("PMUse", "bytes", "peak memory usage"),39"samples" => array("Samples", "samples", "cpu time"));40return $possible_metrics;41}4243/*44* Get the list of metrics present in $xhprof_data as an array.45*46* @author Kannan47*/48function xhprof_get_metrics($xhprof_data) {4950// get list of valid metrics51$possible_metrics = xhprof_get_possible_metrics();5253// return those that are present in the raw data.54// We'll just look at the root of the subtree for this.55$metrics = array();56foreach ($possible_metrics as $metric => $desc) {57if (isset($xhprof_data["main()"][$metric])) {58$metrics[] = $metric;59}60}6162return $metrics;63}6465/**66* Takes a parent/child function name encoded as67* "a==>b" and returns array("a", "b").68*69* @author Kannan70*/71function xhprof_parse_parent_child($parent_child) {72$ret = explode("==>", $parent_child);7374// Return if both parent and child are set75if (isset($ret[1])) {76return $ret;77}7879return array(null, $ret[0]);80}8182/**83* Given parent & child function name, composes the key84* in the format present in the raw data.85*86* @author Kannan87*/88function xhprof_build_parent_child_key($parent, $child) {89if ($parent) {90return $parent . "==>" . $child;91} else {92return $child;93}94}959697/**98* Checks if XHProf raw data appears to be valid and not corrupted.99*100* @param int $run_id Run id of run to be pruned.101* [Used only for reporting errors.]102* @param array $raw_data XHProf raw data to be pruned103* & validated.104*105* @return bool true on success, false on failure106*107* @author Kannan108*/109function xhprof_valid_run($run_id, $raw_data) {110111$main_info = $raw_data["main()"];112if (empty($main_info)) {113xhprof_error("XHProf: main() missing in raw data for Run ID: $run_id");114return false;115}116117// raw data should contain either wall time or samples information...118if (isset($main_info["wt"])) {119$metric = "wt";120} else if (isset($main_info["samples"])) {121$metric = "samples";122} else {123xhprof_error("XHProf: Wall Time information missing from Run ID: $run_id");124return false;125}126127foreach ($raw_data as $info) {128$val = $info[$metric];129130// basic sanity checks...131if ($val < 0) {132xhprof_error("XHProf: $metric should not be negative: Run ID $run_id"133. serialize($info));134return false;135}136if ($val > (86400000000)) {137xhprof_error("XHProf: $metric > 1 day found in Run ID: $run_id "138. serialize($info));139return false;140}141}142return true;143}144145146/**147* Return a trimmed version of the XHProf raw data. Note that the raw148* data contains one entry for each unique parent/child function149* combination.The trimmed version of raw data will only contain150* entries where either the parent or child function is in the list151* of $functions_to_keep.152*153* Note: Function main() is also always kept so that overall totals154* can still be obtained from the trimmed version.155*156* @param array XHProf raw data157* @param array array of function names158*159* @return array Trimmed XHProf Report160*161* @author Kannan162*/163function xhprof_trim_run($raw_data, $functions_to_keep) {164165// convert list of functions to a hash with function as the key166$function_map = array_fill_keys($functions_to_keep, 1);167168// always keep main() as well so that overall totals can still169// be computed if need be.170$function_map['main()'] = 1;171172$new_raw_data = array();173foreach ($raw_data as $parent_child => $info) {174list($parent, $child) = xhprof_parse_parent_child($parent_child);175176if (isset($function_map[$parent]) || isset($function_map[$child])) {177$new_raw_data[$parent_child] = $info;178}179}180181return $new_raw_data;182}183184/**185* Takes raw XHProf data that was aggregated over "$num_runs" number186* of runs averages/nomalizes the data. Essentially the various metrics187* collected are divided by $num_runs.188*189* @author Kannan190*/191function xhprof_normalize_metrics($raw_data, $num_runs) {192193if (empty($raw_data) || ($num_runs == 0)) {194return $raw_data;195}196197$raw_data_total = array();198199if (isset($raw_data["==>main()"]) && isset($raw_data["main()"])) {200xhprof_error("XHProf Error: both ==>main() and main() set in raw data...");201}202203foreach ($raw_data as $parent_child => $info) {204foreach ($info as $metric => $value) {205$raw_data_total[$parent_child][$metric] = ($value / $num_runs);206}207}208209return $raw_data_total;210}211212213/**214* Get raw data corresponding to specified array of runs215* aggregated by certain weightage.216*217* Suppose you have run:5 corresponding to page1.php,218* run:6 corresponding to page2.php,219* and run:7 corresponding to page3.php220*221* and you want to accumulate these runs in a 2:4:1 ratio. You222* can do so by calling:223*224* xhprof_aggregate_runs(array(5, 6, 7), array(2, 4, 1));225*226* The above will return raw data for the runs aggregated227* in 2:4:1 ratio.228*229* @param object $xhprof_runs_impl An object that implements230* the iXHProfRuns interface231* @param array $runs run ids of the XHProf runs..232* @param array $wts integral (ideally) weights for $runs233* @param string $source source to fetch raw data for run from234* @param bool $use_script_name If true, a fake edge from main() to235* to __script::<scriptname> is introduced236* in the raw data so that after aggregations237* the script name is still preserved.238*239* @return array Return aggregated raw data240*241* @author Kannan242*/243function xhprof_aggregate_runs($xhprof_runs_impl, $runs,244$wts, $source="phprof",245$use_script_name=false) {246247$raw_data_total = null;248$raw_data = null;249$metrics = array();250251$run_count = count($runs);252$wts_count = count($wts);253254if (($run_count == 0) ||255(($wts_count > 0) && ($run_count != $wts_count))) {256return array('description' => 'Invalid input..',257'raw' => null);258}259260$bad_runs = array();261foreach($runs as $idx => $run_id) {262263$raw_data = $xhprof_runs_impl->get_run($run_id, $source, '?');264265// use the first run to derive what metrics to aggregate on.266if ($idx == 0) {267foreach ($raw_data["main()"] as $metric => $val) {268if ($metric != "pmu") {269// for now, just to keep data size small, skip "peak" memory usage270// data while aggregating.271// The "regular" memory usage data will still be tracked.272if (isset($val)) {273$metrics[] = $metric;274}275}276}277}278279if (!xhprof_valid_run($run_id, $raw_data)) {280$bad_runs[] = $run_id;281continue;282}283284if ($use_script_name) {285$page = '?';286287// create a fake function '__script::$page', and have and edge from288// main() to '__script::$page'. We will also need edges to transfer289// all edges originating from main() to now originate from290// '__script::$page' to all function called from main().291//292// We also weight main() ever so slightly higher so that293// it shows up above the new entry in reports sorted by294// inclusive metrics or call counts.295if ($page) {296foreach($raw_data["main()"] as $metric => $val) {297$fake_edge[$metric] = $val;298$new_main[$metric] = $val + 0.00001;299}300$raw_data["main()"] = $new_main;301$raw_data[xhprof_build_parent_child_key("main()",302"__script::$page")]303= $fake_edge;304} else {305$use_script_name = false;306}307}308309// if no weights specified, use 1 as the default weightage..310$wt = ($wts_count == 0) ? 1 : $wts[$idx];311312// aggregate $raw_data into $raw_data_total with appropriate weight ($wt)313foreach ($raw_data as $parent_child => $info) {314if ($use_script_name) {315// if this is an old edge originating from main(), it now316// needs to be from '__script::$page'317if (substr($parent_child, 0, 9) == "main()==>") {318$child =substr($parent_child, 9);319// ignore the newly added edge from main()320if (substr($child, 0, 10) != "__script::") {321$parent_child = xhprof_build_parent_child_key("__script::$page",322$child);323}324}325}326327if (!isset($raw_data_total[$parent_child])) {328foreach ($metrics as $metric) {329$raw_data_total[$parent_child][$metric] = ($wt * $info[$metric]);330}331} else {332foreach ($metrics as $metric) {333$raw_data_total[$parent_child][$metric] += ($wt * $info[$metric]);334}335}336}337}338339$runs_string = implode(",", $runs);340341if (isset($wts)) {342$wts_string = "in the ratio (" . implode(":", $wts) . ")";343$normalization_count = array_sum($wts);344} else {345$wts_string = "";346$normalization_count = $run_count;347}348349$run_count = $run_count - count($bad_runs);350351$data['description'] = "Aggregated Report for $run_count runs: ".352"$runs_string $wts_string\n";353$data['raw'] = xhprof_normalize_metrics($raw_data_total,354$normalization_count);355$data['bad_runs'] = $bad_runs;356357return $data;358}359360361/**362* Analyze hierarchical raw data, and compute per-function (flat)363* inclusive and exclusive metrics.364*365* Also, store overall totals in the 2nd argument.366*367* @param array $raw_data XHProf format raw profiler data.368* @param array &$overall_totals OUT argument for returning369* overall totals for various370* metrics.371* @return array Returns a map from function name to its372* call count and inclusive & exclusive metrics373* (such as wall time, etc.).374*375* @author Kannan Muthukkaruppan376*/377function xhprof_compute_flat_info($raw_data, &$overall_totals) {378379global $display_calls;380381$metrics = xhprof_get_metrics($raw_data);382383$overall_totals = array( "ct" => 0,384"wt" => 0,385"ut" => 0,386"st" => 0,387"cpu" => 0,388"mu" => 0,389"pmu" => 0,390"samples" => 0391);392393// compute inclusive times for each function394$symbol_tab = xhprof_compute_inclusive_times($raw_data);395396/* total metric value is the metric value for "main()" */397foreach ($metrics as $metric) {398$overall_totals[$metric] = $symbol_tab["main()"][$metric];399}400401/*402* initialize exclusive (self) metric value to inclusive metric value403* to start with.404* In the same pass, also add up the total number of function calls.405*/406foreach ($symbol_tab as $symbol => $info) {407foreach ($metrics as $metric) {408$symbol_tab[$symbol]["excl_" . $metric] = $symbol_tab[$symbol][$metric];409}410if ($display_calls) {411/* keep track of total number of calls */412$overall_totals["ct"] += $info["ct"];413}414}415416/* adjust exclusive times by deducting inclusive time of children */417foreach ($raw_data as $parent_child => $info) {418list($parent, $child) = xhprof_parse_parent_child($parent_child);419420if ($parent) {421foreach ($metrics as $metric) {422// make sure the parent exists hasn't been pruned.423if (isset($symbol_tab[$parent])) {424$symbol_tab[$parent]["excl_" . $metric] -= $info[$metric];425}426}427}428}429430return $symbol_tab;431}432433/**434* Hierarchical diff:435* Compute and return difference of two call graphs: Run2 - Run1.436*437* @author Kannan438*/439function xhprof_compute_diff($xhprof_data1, $xhprof_data2) {440global $display_calls;441442// use the second run to decide what metrics we will do the diff on443$metrics = xhprof_get_metrics($xhprof_data2);444445$xhprof_delta = $xhprof_data2;446447foreach ($xhprof_data1 as $parent_child => $info) {448449if (!isset($xhprof_delta[$parent_child])) {450451// this pc combination was not present in run1;452// initialize all values to zero.453if ($display_calls) {454$xhprof_delta[$parent_child] = array("ct" => 0);455} else {456$xhprof_delta[$parent_child] = array();457}458foreach ($metrics as $metric) {459$xhprof_delta[$parent_child][$metric] = 0;460}461}462463if ($display_calls) {464$xhprof_delta[$parent_child]["ct"] -= $info["ct"];465}466467foreach ($metrics as $metric) {468$xhprof_delta[$parent_child][$metric] -= $info[$metric];469}470}471472return $xhprof_delta;473}474475476/**477* Compute inclusive metrics for function. This code was factored out478* of xhprof_compute_flat_info().479*480* The raw data contains inclusive metrics of a function for each481* unique parent function it is called from. The total inclusive metrics482* for a function is therefore the sum of inclusive metrics for the483* function across all parents.484*485* @return array Returns a map of function name to total (across all parents)486* inclusive metrics for the function.487*488* @author Kannan489*/490function xhprof_compute_inclusive_times($raw_data) {491global $display_calls;492493$metrics = xhprof_get_metrics($raw_data);494495$symbol_tab = array();496497/*498* First compute inclusive time for each function and total499* call count for each function across all parents the500* function is called from.501*/502foreach ($raw_data as $parent_child => $info) {503504list($parent, $child) = xhprof_parse_parent_child($parent_child);505506if ($parent == $child) {507/*508* XHProf PHP extension should never trigger this situation any more.509* Recursion is handled in the XHProf PHP extension by giving nested510* calls a unique recursion-depth appended name (for example, foo@1).511*/512xhprof_error("Error in Raw Data: parent & child are both: $parent");513return;514}515516if (!isset($symbol_tab[$child])) {517518if ($display_calls) {519$symbol_tab[$child] = array("ct" => $info["ct"]);520} else {521$symbol_tab[$child] = array();522}523foreach ($metrics as $metric) {524$symbol_tab[$child][$metric] = $info[$metric];525}526} else {527if ($display_calls) {528/* increment call count for this child */529$symbol_tab[$child]["ct"] += $info["ct"];530}531532/* update inclusive times/metric for this child */533foreach ($metrics as $metric) {534$symbol_tab[$child][$metric] += $info[$metric];535}536}537}538539return $symbol_tab;540}541542543/*544* Prunes XHProf raw data:545*546* Any node whose inclusive walltime accounts for less than $prune_percent547* of total walltime is pruned. [It is possible that a child function isn't548* pruned, but one or more of its parents get pruned. In such cases, when549* viewing the child function's hierarchical information, the cost due to550* the pruned parent(s) will be attributed to a special function/symbol551* "__pruned__()".]552*553* @param array $raw_data XHProf raw data to be pruned & validated.554* @param double $prune_percent Any edges that account for less than555* $prune_percent of time will be pruned556* from the raw data.557*558* @return array Returns the pruned raw data.559*560* @author Kannan561*/562function xhprof_prune_run($raw_data, $prune_percent) {563564$main_info = $raw_data["main()"];565if (empty($main_info)) {566xhprof_error("XHProf: main() missing in raw data");567return false;568}569570// raw data should contain either wall time or samples information...571if (isset($main_info["wt"])) {572$prune_metric = "wt";573} else if (isset($main_info["samples"])) {574$prune_metric = "samples";575} else {576xhprof_error("XHProf: for main() we must have either wt "577."or samples attribute set");578return false;579}580581// determine the metrics present in the raw data..582$metrics = array();583foreach ($main_info as $metric => $val) {584if (isset($val)) {585$metrics[] = $metric;586}587}588589$prune_threshold = (($main_info[$prune_metric] * $prune_percent) / 100.0);590591// init_metrics($raw_data, null, null, false);592$flat_info = xhprof_compute_inclusive_times($raw_data);593594foreach ($raw_data as $parent_child => $info) {595596list($parent, $child) = xhprof_parse_parent_child($parent_child);597598// is this child's overall total from all parents less than threshold?599if ($flat_info[$child][$prune_metric] < $prune_threshold) {600unset($raw_data[$parent_child]); // prune the edge601} else if ($parent &&602($parent != "__pruned__()") &&603($flat_info[$parent][$prune_metric] < $prune_threshold)) {604605// Parent's overall inclusive metric is less than a threshold.606// All edges to the parent node will get nuked, and this child will607// be a dangling child.608// So instead change its parent to be a special function __pruned__().609$pruned_edge = xhprof_build_parent_child_key("__pruned__()", $child);610611if (isset($raw_data[$pruned_edge])) {612foreach ($metrics as $metric) {613$raw_data[$pruned_edge][$metric]+=$raw_data[$parent_child][$metric];614}615} else {616$raw_data[$pruned_edge] = $raw_data[$parent_child];617}618619unset($raw_data[$parent_child]); // prune the edge620}621}622623return $raw_data;624}625626627/**628* Set one key in an array and return the array629*630* @author Kannan631*/632function xhprof_array_set($arr, $k, $v) {633$arr[$k] = $v;634return $arr;635}636637/**638* Removes/unsets one key in an array and return the array639*640* @author Kannan641*/642function xhprof_array_unset($arr, $k) {643unset($arr[$k]);644return $arr;645}646647/**648* Type definitions for URL params649*/650define('XHPROF_STRING_PARAM', 1);651define('XHPROF_UINT_PARAM', 2);652define('XHPROF_FLOAT_PARAM', 3);653define('XHPROF_BOOL_PARAM', 4);654655656/**657* Internal helper function used by various658* xhprof_get_param* flavors for various659* types of parameters.660*661* @param string name of the URL query string param662*663* @author Kannan664*/665function xhprof_get_param_helper($param) {666$val = null;667if (isset($_GET[$param]))668$val = $_GET[$param];669else if (isset($_POST[$param])) {670$val = $_POST[$param];671}672return $val;673}674675/**676* Extracts value for string param $param from query677* string. If param is not specified, return the678* $default value.679*680* @author Kannan681*/682function xhprof_get_string_param($param, $default = '') {683$val = xhprof_get_param_helper($param);684685if ($val === null)686return $default;687688return $val;689}690691/**692* Extracts value for unsigned integer param $param from693* query string. If param is not specified, return the694* $default value.695*696* If value is not a valid unsigned integer, logs error697* and returns null.698*699* @author Kannan700*/701function xhprof_get_uint_param($param, $default = 0) {702$val = xhprof_get_param_helper($param);703704if ($val === null)705$val = $default;706707// trim leading/trailing whitespace708$val = trim($val);709710// if it only contains digits, then ok..711if (ctype_digit($val)) {712return $val;713}714715xhprof_error("$param is $val. It must be an unsigned integer.");716return null;717}718719720/**721* Extracts value for a float param $param from722* query string. If param is not specified, return723* the $default value.724*725* If value is not a valid unsigned integer, logs error726* and returns null.727*728* @author Kannan729*/730function xhprof_get_float_param($param, $default = 0) {731$val = xhprof_get_param_helper($param);732733if ($val === null)734$val = $default;735736// trim leading/trailing whitespace737$val = trim($val);738739// TBD: confirm the value is indeed a float.740if (true) // for now..741return (float)$val;742743xhprof_error("$param is $val. It must be a float.");744return null;745}746747/**748* Extracts value for a boolean param $param from749* query string. If param is not specified, return750* the $default value.751*752* If value is not a valid unsigned integer, logs error753* and returns null.754*755* @author Kannan756*/757function xhprof_get_bool_param($param, $default = false) {758$val = xhprof_get_param_helper($param);759760if ($val === null)761$val = $default;762763// trim leading/trailing whitespace764$val = trim($val);765766switch (strtolower($val)) {767case '0':768case '1':769$val = (bool)$val;770break;771case 'true':772case 'on':773case 'yes':774$val = true;775break;776case 'false':777case 'off':778case 'no':779$val = false;780break;781default:782xhprof_error("$param is $val. It must be a valid boolean string.");783return null;784}785786return $val;787788}789790/**791* Initialize params from URL query string. The function792* creates globals variables for each of the params793* and if the URL query string doesn't specify a particular794* param initializes them with the corresponding default795* value specified in the input.796*797* @params array $params An array whose keys are the names798* of URL params who value needs to799* be retrieved from the URL query800* string. PHP globals are created801* with these names. The value is802* itself an array with 2-elems (the803* param type, and its default value).804* If a param is not specified in the805* query string the default value is806* used.807* @author Kannan808*/809function xhprof_param_init($params) {810/* Create variables specified in $params keys, init defaults */811foreach ($params as $k => $v) {812switch ($v[0]) {813case XHPROF_STRING_PARAM:814$p = xhprof_get_string_param($k, $v[1]);815break;816case XHPROF_UINT_PARAM:817$p = xhprof_get_uint_param($k, $v[1]);818break;819case XHPROF_FLOAT_PARAM:820$p = xhprof_get_float_param($k, $v[1]);821break;822case XHPROF_BOOL_PARAM:823$p = xhprof_get_bool_param($k, $v[1]);824break;825default:826xhprof_error("Invalid param type passed to xhprof_param_init: "827. $v[0]);828exit();829}830831// create a global variable using the parameter name.832$GLOBALS[$k] = $p;833}834}835836837/**838* Given a partial query string $q return matching function names in839* specified XHProf run. This is used for the type ahead function840* selector.841*842* @author Kannan843*/844function xhprof_get_matching_functions($q, $xhprof_data) {845846$matches = array();847848foreach ($xhprof_data as $parent_child => $info) {849list($parent, $child) = xhprof_parse_parent_child($parent_child);850if (stripos($parent, $q) !== false) {851$matches[$parent] = 1;852}853if (stripos($child, $q) !== false) {854$matches[$child] = 1;855}856}857858$res = array_keys($matches);859860// sort it so the answers are in some reliable order...861asort($res);862863return ($res);864}865866867868