/xhprof_lib/utils/xhprof_runs_mysql.php

http://github.com/preinheimer/xhprof · PHP · 462 lines · 236 code · 57 blank · 169 comment · 32 complexity · 149f10e2d002b7ed3ddf79f0e810e1a1 MD5 · raw file

  1. <?php
  2. //
  3. // Copyright (c) 2009 Facebook
  4. //
  5. // Licensed under the Apache License, Version 2.0 (the "License");
  6. // you may not use this file except in compliance with the License.
  7. // You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. //
  17. //
  18. // This file defines the interface iXHProfRuns and also provides a default
  19. // implementation of the interface (class XHProfRuns).
  20. //
  21. /**
  22. * iXHProfRuns interface for getting/saving a XHProf run.
  23. *
  24. * Clients can either use the default implementation,
  25. * namely XHProfRuns_Default, of this interface or define
  26. * their own implementation.
  27. *
  28. * @author Kannan
  29. */
  30. interface iXHProfRuns {
  31. /**
  32. * Returns XHProf data given a run id ($run) of a given
  33. * type ($type).
  34. *
  35. * Also, a brief description of the run is returned via the
  36. * $run_desc out parameter.
  37. */
  38. public function get_run($run_id, $type, &$run_desc);
  39. /**
  40. * Save XHProf data for a profiler run of specified type
  41. * ($type).
  42. *
  43. * The caller may optionally pass in run_id (which they
  44. * promise to be unique). If a run_id is not passed in,
  45. * the implementation of this method must generated a
  46. * unique run id for this saved XHProf run.
  47. *
  48. * Returns the run id for the saved XHProf run.
  49. *
  50. */
  51. public function save_run($xhprof_data, $type, $run_id = null);
  52. }
  53. /**
  54. * XHProfRuns_Default is the default implementation of the
  55. * iXHProfRuns interface for saving/fetching XHProf runs.
  56. *
  57. * This modified version of the file uses a MySQL backend to store
  58. * the data, it also stores additional information outside the run
  59. * itself (beyond simply the run id) to make comparisons and run
  60. * location easier
  61. *
  62. * @author Kannan
  63. * @author Paul Reinheimer (http://blog.preinheimer.com)
  64. */
  65. class XHProfRuns_Default implements iXHProfRuns {
  66. private $dir = '';
  67. public $prefix = 't11_';
  68. public $run_details = null;
  69. protected $linkID;
  70. public function __construct($dir = null)
  71. {
  72. $this->db();
  73. }
  74. protected function db()
  75. {
  76. global $_xhprof;
  77. $linkid = mysql_connect($_xhprof['dbhost'], $_xhprof['dbuser'], $_xhprof['dbpass']);
  78. if ($linkid === FALSE)
  79. {
  80. xhprof_error("Could not connect to db");
  81. $run_desc = "could not connect to db";
  82. throw new Exception("Unable to connect to database");
  83. return false;
  84. }
  85. mysql_query("SET NAMES utf8");
  86. mysql_select_db($_xhprof['dbname'], $linkid);
  87. $this->linkID = $linkid;
  88. }
  89. /**
  90. * When setting the `id` column, consider the length of the prefix you're specifying in $this->prefix
  91. *
  92. *
  93. CREATE TABLE `details` (
  94. `id` char(17) NOT NULL,
  95. `url` varchar(255) default NULL,
  96. `c_url` varchar(255) default NULL,
  97. `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  98. `server name` varchar(64) default NULL,
  99. `perfdata` MEDIUMBLOB,
  100. `type` tinyint(4) default NULL,
  101. `cookie` BLOB,
  102. `post` BLOB,
  103. `get` BLOB,
  104. `pmu` int(11) unsigned default NULL,
  105. `wt` int(11) unsigned default NULL,
  106. `cpu` int(11) unsigned default NULL,
  107. `server_id` char(3) NOT NULL default 't11',
  108. `aggregateCalls_include` varchar(255) DEFAULT NULL,
  109. PRIMARY KEY (`id`),
  110. KEY `url` (`url`),
  111. KEY `c_url` (`c_url`),
  112. KEY `cpu` (`cpu`),
  113. KEY `wt` (`wt`),
  114. KEY `pmu` (`pmu`),
  115. KEY `timestamp` (`timestamp`)
  116. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  117. */
  118. private function gen_run_id($type)
  119. {
  120. return uniqid();
  121. }
  122. /**
  123. * This function gets runs based on passed parameters, column data as key, value as the value. Values
  124. * are escaped automatically. You may also pass limit, order by, group by, or "where" to add those values,
  125. * all of which are used as is, no escaping.
  126. *
  127. * @param array $stats Criteria by which to select columns
  128. * @return resource
  129. */
  130. public function getRuns($stats)
  131. {
  132. if (isset($stats['select']))
  133. {
  134. $query = "SELECT {$stats['select']} FROM `details` ";
  135. }else
  136. {
  137. $query = "SELECT * FROM `details` ";
  138. }
  139. $skippers = array("limit", "order by", "group by", "where", "select");
  140. $hasWhere = false;
  141. foreach($stats AS $column => $value)
  142. {
  143. if (in_array($column, $skippers))
  144. {
  145. continue;
  146. }
  147. if ($hasWhere === false)
  148. {
  149. $query .= " WHERE ";
  150. $hasWhere = true;
  151. }elseif($hasWhere === true)
  152. {
  153. $query .= "AND ";
  154. }
  155. if (strlen($value) == 0)
  156. {
  157. $query .= $column;
  158. }
  159. $value = mysql_real_escape_string($value);
  160. $query .= " `$column` = '$value' ";
  161. }
  162. if (isset($stats['where']))
  163. {
  164. if ($hasWhere === false)
  165. {
  166. $query .= " WHERE ";
  167. $hasWhere = true;
  168. }else
  169. {
  170. $query .= " AND ";
  171. }
  172. $query .= $stats['where'];
  173. }
  174. if (isset($stats['group by']))
  175. {
  176. $query .= " GROUP BY `{$stats['group by']}` ";
  177. }
  178. if (isset($stats['order by']))
  179. {
  180. $query .= " ORDER BY `{$stats['order by']}` DESC";
  181. }
  182. if (isset($stats['limit']))
  183. {
  184. $query .= " LIMIT {$stats['limit']} ";
  185. }
  186. $resultSet = mysql_query($query);
  187. return $resultSet;
  188. }
  189. /**
  190. * Obtains the pages that have been the hardest hit over the past N days, utalizing the getRuns() method.
  191. *
  192. * @param array $criteria An associative array containing, at minimum, type, days, and limit
  193. * @return resource The result set reprsenting the results of the query
  194. */
  195. public function getHardHit($criteria)
  196. {
  197. //call thing to get runs
  198. $criteria['select'] = "distinct(`{$criteria['type']}`), count(`{$criteria['type']}`) AS `count` , sum(`wt`) as total_wall, avg(`wt`) as avg_wall";
  199. unset($criteria['type']);
  200. $criteria['where'] = "DATE_SUB(CURDATE(), INTERVAL {$criteria['days']} DAY) <= `timestamp`";
  201. unset($criteria['days']);
  202. $criteria['group by'] = "url";
  203. $criteria['order by'] = "count";
  204. $resultSet = $this->getRuns($criteria);
  205. return $resultSet;
  206. }
  207. public function getDistinct($data)
  208. {
  209. $sql['column'] = mysql_real_escape_string($data['column']);
  210. $query = "SELECT DISTINCT(`{$sql['column']}`) FROM `details`";
  211. $rs = mysql_query($query);
  212. return $rs;
  213. }
  214. public static function getNextAssoc($resultSet)
  215. {
  216. return mysql_fetch_assoc($resultSet);
  217. }
  218. /**
  219. * Retreives a run from the database,
  220. *
  221. * @param string $run_id unique identifier for the run being requested
  222. * @param mixed $type
  223. * @param mixed $run_desc
  224. * @return mixed
  225. */
  226. public function get_run($run_id, $type, &$run_desc)
  227. {
  228. $run_id = mysql_real_escape_string($run_id);
  229. $query = "SELECT * FROM `details` WHERE `id` = '$run_id'";
  230. $resultSet = mysql_query($query, $this->linkID);
  231. $data = mysql_fetch_assoc($resultSet);
  232. //The Performance data is compressed lightly to avoid max row length
  233. if (!isset($GLOBALS['_xhprof']['serializer']) || strtolower($GLOBALS['_xhprof']['serializer'] == 'php')) {
  234. $contents = unserialize(gzuncompress($data['perfdata']));
  235. } else {
  236. $contents = json_decode(gzuncompress($data['perfdata']), true);
  237. }
  238. //This data isnt' needed for display purposes, there's no point in keeping it in this array
  239. unset($data['perfdata']);
  240. // The same function is called twice when diff'ing runs. In this case we'll populate the global scope with an array
  241. if (is_null($this->run_details))
  242. {
  243. $this->run_details = $data;
  244. }else
  245. {
  246. $this->run_details[0] = $this->run_details;
  247. $this->run_details[1] = $data;
  248. }
  249. $run_desc = "XHProf Run (Namespace=$type)";
  250. $this->getRunComparativeData($data['url'], $data['c_url']);
  251. return array($contents, $data);
  252. }
  253. /**
  254. * Get stats (pmu, ct, wt) on a url or c_url
  255. *
  256. * @param array $data An associative array containing the limit you'd like to set for the queyr, as well as either c_url or url for the desired element.
  257. * @return resource result set from the database query
  258. */
  259. public function getUrlStats($data)
  260. {
  261. $data['select'] = '`id`, UNIX_TIMESTAMP(`timestamp`) as `timestamp`, `pmu`, `wt`, `cpu`';
  262. $rs = $this->getRuns($data);
  263. return $rs;
  264. }
  265. /**
  266. * Get comparative information for a given URL and c_url, this information will be used to display stats like how many calls a URL has,
  267. * average, min, max execution time, etc. This information is pushed into the global namespace, which is horribly hacky.
  268. *
  269. * @param string $url
  270. * @param string $c_url
  271. * @return array
  272. */
  273. public function getRunComparativeData($url, $c_url)
  274. {
  275. $url = mysql_real_escape_string($url);
  276. $c_url = mysql_real_escape_string($c_url);
  277. //Runs same URL
  278. // count, avg/min/max for wt, cpu, pmu
  279. $query = "SELECT count(`id`), avg(`wt`), min(`wt`), max(`wt`), avg(`cpu`), min(`cpu`), max(`cpu`), avg(`pmu`), min(`pmu`), max(`pmu`) FROM `details` WHERE `url` = '$url'";
  280. $rs = mysql_query($query, $this->linkID);
  281. $row = mysql_fetch_assoc($rs);
  282. $row['url'] = $url;
  283. $row['95(`wt`)'] = $this->calculatePercentile(array('count' => $row['count(`id`)'], 'column' => 'wt', 'type' => 'url', 'url' => $url));
  284. $row['95(`cpu`)'] = $this->calculatePercentile(array('count' => $row['count(`id`)'], 'column' => 'cpu', 'type' => 'url', 'url' => $url));
  285. $row['95(`pmu`)'] = $this->calculatePercentile(array('count' => $row['count(`id`)'], 'column' => 'pmu', 'type' => 'url', 'url' => $url));
  286. global $comparative;
  287. $comparative['url'] = $row;
  288. unset($row);
  289. //Runs same c_url
  290. // count, avg/min/max for wt, cpu, pmu
  291. $query = "SELECT count(`id`), avg(`wt`), min(`wt`), max(`wt`), avg(`cpu`), min(`cpu`), max(`cpu`), avg(`pmu`), min(`pmu`), max(`pmu`) FROM `details` WHERE `c_url` = '$c_url'";
  292. $rs = mysql_query($query, $this->linkID);
  293. $row = mysql_fetch_assoc($rs);
  294. $row['url'] = $c_url;
  295. $row['95(`wt`)'] = $this->calculatePercentile(array('count' => $row['count(`id`)'], 'column' => 'wt', 'type' => 'c_url', 'url' => $c_url));
  296. $row['95(`cpu`)'] = $this->calculatePercentile(array('count' => $row['count(`id`)'], 'column' => 'cpu', 'type' => 'c_url', 'url' => $c_url));
  297. $row['95(`pmu`)'] = $this->calculatePercentile(array('count' => $row['count(`id`)'], 'column' => 'pmu', 'type' => 'c_url', 'url' => $c_url));
  298. $comparative['c_url'] = $row;
  299. unset($row);
  300. return $comparative;
  301. }
  302. protected function calculatePercentile($details)
  303. {
  304. $limit = (int) ($details['count'] / 20);
  305. $query = "SELECT `{$details['column']}` as `value` FROM `details` WHERE `{$details['type']}` = '{$details['url']}' ORDER BY `{$details['column']}` DESC LIMIT $limit, 1";
  306. $rs = mysql_query($query, $this->linkID);
  307. $row = mysql_fetch_assoc($rs);
  308. return $row['value'];
  309. }
  310. /**
  311. * Save the run in the database.
  312. *
  313. * @param string $xhprof_data
  314. * @param mixed $type
  315. * @param string $run_id
  316. * @param mixed $xhprof_details
  317. * @return string
  318. */
  319. public function save_run($xhprof_data, $type, $run_id = null, $xhprof_details = null)
  320. {
  321. global $_xhprof;
  322. $sql = array();
  323. if ($run_id === null) {
  324. $run_id = $this->gen_run_id($type);
  325. }
  326. /*
  327. Session data is ommitted purposefully, mostly because it's not likely that the data
  328. that resides in $_SESSION at this point is the same as the data that the application
  329. started off with (for most apps, it's likely that session data is manipulated on most
  330. pageloads).
  331. The goal of storing get, post and cookie is to help explain why an application chose
  332. a particular code execution path, pehaps it was a poorly filled out form, or a cookie that
  333. overwrote some default parameters. So having them helps. Most applications don't push data
  334. back into those super globals, so we're safe(ish) storing them now.
  335. We can't just clone the session data in header.php to be sneaky either, starting the session
  336. is an application decision, and we don't want to go starting sessions where none are needed
  337. (not good performance wise). We could be extra sneaky and do something like:
  338. if(isset($_COOKIE['phpsessid']))
  339. {
  340. session_start();
  341. $_xhprof['session_data'] = $_SESSION;
  342. }
  343. but starting session support really feels like an application level decision, not one that
  344. a supposedly unobtrusive profiler makes for you.
  345. */
  346. if (!isset($GLOBALS['_xhprof']['serializer']) || strtolower($GLOBALS['_xhprof']['serializer'] == 'php')) {
  347. $sql['get'] = mysql_real_escape_string(serialize($_GET), $this->linkID);
  348. $sql['cookie'] = mysql_real_escape_string(serialize($_COOKIE), $this->linkID);
  349. //This code has not been tested
  350. if ($_xhprof['savepost'])
  351. {
  352. $sql['post'] = mysql_real_escape_string(serialize($_POST), $this->linkID);
  353. } else {
  354. $sql['post'] = mysql_real_escape_string(serialize(array("Skipped" => "Post data omitted by rule")), $this->linkID);
  355. }
  356. } else {
  357. $sql['get'] = mysql_real_escape_string(json_encode($_GET), $this->linkID);
  358. $sql['cookie'] = mysql_real_escape_string(json_encode($_COOKIE), $this->linkID);
  359. //This code has not been tested
  360. if ($_xhprof['savepost'])
  361. {
  362. $sql['post'] = mysql_real_escape_string(json_encode($_POST), $this->linkID);
  363. } else {
  364. $sql['post'] = mysql_real_escape_string(json_encode(array("Skipped" => "Post data omitted by rule")), $this->linkID);
  365. }
  366. }
  367. $sql['pmu'] = isset($xhprof_data['main()']['pmu']) ? $xhprof_data['main()']['pmu'] : '';
  368. $sql['wt'] = isset($xhprof_data['main()']['wt']) ? $xhprof_data['main()']['wt'] : '';
  369. $sql['cpu'] = isset($xhprof_data['main()']['cpu']) ? $xhprof_data['main()']['cpu'] : '';
  370. // The value of 2 seems to be light enugh that we're not killing the server, but still gives us lots of breathing room on
  371. // full production code.
  372. if (!isset($GLOBALS['_xhprof']['serializer']) || strtolower($GLOBALS['_xhprof']['serializer'] == 'php')) {
  373. $sql['data'] = mysql_real_escape_string(gzcompress(serialize($xhprof_data), 2));
  374. } else {
  375. $sql['data'] = mysql_real_escape_string(gzcompress(json_encode($xhprof_data), 2));
  376. }
  377. $url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_SERVER['PHP_SELF'];
  378. $sname = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '';
  379. $sql['url'] = mysql_real_escape_string($url);
  380. $sql['c_url'] = mysql_real_escape_string(_urlSimilartor($_SERVER['REQUEST_URI']));
  381. $sql['servername'] = mysql_real_escape_string($sname);
  382. $sql['type'] = (int) (isset($xhprof_details['type']) ? $xhprof_details['type'] : 0);
  383. $sql['timestamp'] = mysql_real_escape_string($_SERVER['REQUEST_TIME']);
  384. $sql['server_id'] = mysql_real_escape_string($_xhprof['servername']);
  385. $sql['aggregateCalls_include'] = getenv('xhprof_aggregateCalls_include') ? getenv('xhprof_aggregateCalls_include') : '';
  386. $query = "INSERT INTO `details` (`id`, `url`, `c_url`, `timestamp`, `server name`, `perfdata`, `type`, `cookie`, `post`, `get`, `pmu`, `wt`, `cpu`, `server_id`, `aggregateCalls_include`) VALUES('$run_id', '{$sql['url']}', '{$sql['c_url']}', FROM_UNIXTIME('{$sql['timestamp']}'), '{$sql['servername']}', '{$sql['data']}', '{$sql['type']}', '{$sql['cookie']}', '{$sql['post']}', '{$sql['get']}', '{$sql['pmu']}', '{$sql['wt']}', '{$sql['cpu']}', '{$sql['server_id']}', '{$sql['aggregateCalls_include']}')";
  387. mysql_query($query, $this->linkID);
  388. if (mysql_affected_rows($this->linkID) == 1)
  389. {
  390. return $run_id;
  391. }else
  392. {
  393. global $_xhprof;
  394. if ($_xhprof['display'] === true)
  395. {
  396. echo "Failed to insert: $query <br>\n";
  397. }
  398. return -1;
  399. }
  400. }
  401. }