kpi.class.inc.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. // Copyright (C) 2010-2012 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * Measures operations duration, memory usage, etc. (and some other KPIs)
  20. *
  21. * @copyright Copyright (C) 2010-2012 Combodo SARL
  22. * @license http://opensource.org/licenses/AGPL-3.0
  23. */
  24. class ExecutionKPI
  25. {
  26. static protected $m_bEnabled_Duration = false;
  27. static protected $m_bEnabled_Memory = false;
  28. static protected $m_bBlameCaller = false;
  29. static protected $m_sAllowedUser = '*';
  30. static protected $m_aStats = array(); // Recurrent operations
  31. static protected $m_aExecData = array(); // One shot operations
  32. protected $m_fStarted = null;
  33. protected $m_iInitialMemory = null;
  34. static public function EnableDuration($iLevel)
  35. {
  36. if ($iLevel > 0)
  37. {
  38. self::$m_bEnabled_Duration = true;
  39. if ($iLevel > 1)
  40. {
  41. self::$m_bBlameCaller = true;
  42. }
  43. }
  44. }
  45. static public function EnableMemory($iLevel)
  46. {
  47. if ($iLevel > 0)
  48. {
  49. self::$m_bEnabled_Memory = true;
  50. }
  51. }
  52. /**
  53. * @param string sUser A user login or * for all users
  54. */
  55. static public function SetAllowedUser($sUser)
  56. {
  57. self::$m_sAllowedUser = $sUser;
  58. }
  59. static public function IsEnabled()
  60. {
  61. if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory)
  62. {
  63. if ((self::$m_sAllowedUser == '*') || (UserRights::GetUser() == trim(self::$m_sAllowedUser)))
  64. {
  65. return true;
  66. }
  67. }
  68. return false;
  69. }
  70. static public function GetDescription()
  71. {
  72. $aFeatures = array();
  73. if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration';
  74. if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage';
  75. $sFeatures = implode(', ', $aFeatures);
  76. $sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'";
  77. return "KPI logging is active for $sFor. Measures: $sFeatures";
  78. }
  79. static public function ReportStats()
  80. {
  81. if (!self::IsEnabled()) return;
  82. global $fItopStarted;
  83. $sExecId = microtime(); // id to differentiate the hrefs!
  84. $aBeginTimes = array();
  85. foreach (self::$m_aExecData as $aOpStats)
  86. {
  87. $aBeginTimes[] = $aOpStats['time_begin'];
  88. }
  89. array_multisort($aBeginTimes, self::$m_aExecData);
  90. $sTableStyle = 'background-color: #ccc; margin: 10px;';
  91. self::Report("<hr/>");
  92. self::Report("<div style=\"background-color: grey; padding: 10px;\">");
  93. self::Report("<h3><a name=\"".md5($sExecId)."\">KPIs</a> - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")</h3>");
  94. self::Report("<p>".date('Y-m-d H:i:s', $fItopStarted)."</p>");
  95. self::Report("<p>log_kpi_user_id: ".MetaModel::GetConfig()->Get('log_kpi_user_id')."</p>");
  96. self::Report("<div>");
  97. self::Report("<table border=\"1\" style=\"$sTableStyle\">");
  98. self::Report("<thead>");
  99. self::Report(" <th>Operation</th><th>Begin</th><th>End</th><th>Duration</th><th>Memory start</th><th>Memory end</th><th>Memory peak</th>");
  100. self::Report("</thead>");
  101. foreach (self::$m_aExecData as $aOpStats)
  102. {
  103. $sOperation = $aOpStats['op'];
  104. $sBegin = $sEnd = $sDuration = $sMemBegin = $sMemEnd = $sMemPeak = '?';
  105. $sBegin = round($aOpStats['time_begin'], 3);
  106. $sEnd = round($aOpStats['time_end'], 3);
  107. $fDuration = $aOpStats['time_end'] - $aOpStats['time_begin'];
  108. $sDuration = round($fDuration, 3);
  109. if (isset($aOpStats['mem_begin']))
  110. {
  111. $sMemBegin = self::MemStr($aOpStats['mem_begin']);
  112. $sMemEnd = self::MemStr($aOpStats['mem_end']);
  113. if (isset($aOpStats['mem_peak']))
  114. {
  115. $sMemPeak = self::MemStr($aOpStats['mem_peak']);
  116. }
  117. }
  118. self::Report("<tr>");
  119. self::Report(" <td>$sOperation</td><td>$sBegin</td><td>$sEnd</td><td>$sDuration</td><td>$sMemBegin</td><td>$sMemEnd</td><td>$sMemPeak</td>");
  120. self::Report("</tr>");
  121. }
  122. self::Report("</table>");
  123. self::Report("</div>");
  124. $aConsolidatedStats = array();
  125. foreach (self::$m_aStats as $sOperation => $aOpStats)
  126. {
  127. $fTotalOp = 0;
  128. $iTotalOp = 0;
  129. $fMinOp = null;
  130. $fMaxOp = 0;
  131. $sMaxOpArguments = null;
  132. foreach ($aOpStats as $sArguments => $aEvents)
  133. {
  134. foreach ($aEvents as $aEventData)
  135. {
  136. $fDuration = $aEventData['time'];
  137. $fTotalOp += $fDuration;
  138. $iTotalOp++;
  139. $fMinOp = is_null($fMinOp) ? $fDuration : min($fMinOp, $fDuration);
  140. if ($fDuration > $fMaxOp)
  141. {
  142. $sMaxOpArguments = $sArguments;
  143. $fMaxOp = $fDuration;
  144. }
  145. }
  146. }
  147. $aConsolidatedStats[$sOperation] = array(
  148. 'count' => $iTotalOp,
  149. 'duration' => $fTotalOp,
  150. 'min' => $fMinOp,
  151. 'max' => $fMaxOp,
  152. 'avg' => $fTotalOp / $iTotalOp,
  153. 'max_args' => $sMaxOpArguments
  154. );
  155. }
  156. self::Report("<div>");
  157. self::Report("<table border=\"1\" style=\"$sTableStyle\">");
  158. self::Report("<thead>");
  159. self::Report(" <th>Operation</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th><th>Avg</th>");
  160. self::Report("</thead>");
  161. foreach ($aConsolidatedStats as $sOperation => $aOpStats)
  162. {
  163. $sOperation = '<a href="#'.md5($sExecId.$sOperation).'">'.$sOperation.'</a>';
  164. $sCount = $aOpStats['count'];
  165. $sDuration = round($aOpStats['duration'], 3);
  166. $sMin = round($aOpStats['min'], 3);
  167. $sMax = '<a href="#'.md5($sExecId.$aOpStats['max_args']).'">'.round($aOpStats['max'], 3).'</a>';
  168. $sAvg = round($aOpStats['avg'], 3);
  169. self::Report("<tr>");
  170. self::Report(" <td>$sOperation</td><td>$sCount</td><td>$sDuration</td><td>$sMin</td><td>$sMax</td><td>$sAvg</td>");
  171. self::Report("</tr>");
  172. }
  173. self::Report("</table>");
  174. self::Report("</div>");
  175. self::Report("</div>");
  176. // Report operation details
  177. foreach (self::$m_aStats as $sOperation => $aOpStats)
  178. {
  179. $sOperationHtml = '<a name="'.md5($sExecId.$sOperation).'">'.$sOperation.'</a>';
  180. self::Report("<h4>$sOperationHtml</h4>");
  181. self::Report("<p><a href=\"#".md5($sExecId)."\">Back to page stats</a></p>");
  182. self::Report("<table border=\"1\" style=\"$sTableStyle\">");
  183. self::Report("<thead>");
  184. self::Report(" <th>Operation details (+ blame caller if log_kpi_duration = 2)</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th>");
  185. self::Report("</thead>");
  186. foreach ($aOpStats as $sArguments => $aEvents)
  187. {
  188. $sHtmlArguments = '<a name="'.md5($sExecId.$sArguments).'"><div style="white-space: pre-wrap;">'.$sArguments.'</div></a>';
  189. if ($aConsolidatedStats[$sOperation]['max_args'] == $sArguments)
  190. {
  191. $sHtmlArguments = '<span style="color: red;">'.$sHtmlArguments.'</span>';
  192. }
  193. if (isset($aEvents[0]['callers']))
  194. {
  195. $sHtmlArguments .= '<div style="padding: 10px;">';
  196. $sHtmlArguments .= '<table border="1" bgcolor="#cfc">';
  197. $sHtmlArguments .= '<tr><td colspan="2" bgcolor="#e9b96">Call stack for the <b>FIRST</b> caller</td></tr>';
  198. foreach ($aEvents[0]['callers'] as $aCall)
  199. {
  200. $sHtmlArguments .= '<tr>';
  201. $sHtmlArguments .= '<td>'.$aCall['Function'].'</td>';
  202. $sHtmlArguments .= '<td>'.$aCall['File'].':'.$aCall['Line'].'</td>';
  203. $sHtmlArguments .= '</tr>';
  204. }
  205. $sHtmlArguments .= '</table>';
  206. $sHtmlArguments .= '</div>';
  207. }
  208. $fTotalInter = 0;
  209. $fMinInter = null;
  210. $fMaxInter = 0;
  211. foreach ($aEvents as $aEventData)
  212. {
  213. $fDuration = $aEventData['time'];
  214. $fTotalInter += $fDuration;
  215. $fMinInter = is_null($fMinInter) ? $fDuration : min($fMinInter, $fDuration);
  216. $fMaxInter = max($fMaxInter, $fDuration);
  217. }
  218. $iCountInter = count($aEvents);
  219. $sTotalInter = round($fTotalInter, 3);
  220. $sMinInter = round($fMinInter, 3);
  221. $sMaxInter = round($fMaxInter, 3);
  222. self::Report("<tr>");
  223. self::Report(" <td>$sHtmlArguments</td><td>$iCountInter</td><td>$sTotalInter</td><td>$sMinInter</td><td>$sMaxInter</td>");
  224. self::Report("</tr>");
  225. }
  226. self::Report("</table>");
  227. }
  228. }
  229. public function __construct()
  230. {
  231. $this->ResetCounters();
  232. }
  233. // Get the duration since startup, and reset the counter for the next measure
  234. //
  235. public function ComputeAndReport($sOperationDesc)
  236. {
  237. global $fItopStarted;
  238. $aNewEntry = null;
  239. if (self::$m_bEnabled_Duration)
  240. {
  241. $fStopped = MyHelpers::getmicrotime();
  242. $aNewEntry = array(
  243. 'op' => $sOperationDesc,
  244. 'time_begin' => $this->m_fStarted - $fItopStarted,
  245. 'time_end' => $fStopped - $fItopStarted,
  246. );
  247. // Reset for the next operation (if the object is recycled)
  248. $this->m_fStarted = $fStopped;
  249. }
  250. if (self::$m_bEnabled_Memory)
  251. {
  252. $iCurrentMemory = self::memory_get_usage();
  253. if (is_null($aNewEntry))
  254. {
  255. $aNewEntry = array('op' => $sOperationDesc);
  256. }
  257. $aNewEntry['mem_begin'] = $this->m_iInitialMemory;
  258. $aNewEntry['mem_end'] = $iCurrentMemory;
  259. if (function_exists('memory_get_peak_usage'))
  260. {
  261. $aNewEntry['mem_peak'] = memory_get_peak_usage();
  262. }
  263. // Reset for the next operation (if the object is recycled)
  264. $this->m_iInitialMemory = $iCurrentMemory;
  265. }
  266. if (!is_null($aNewEntry))
  267. {
  268. self::$m_aExecData[] = $aNewEntry;
  269. }
  270. $this->ResetCounters();
  271. }
  272. public function ComputeStats($sOperation, $sArguments)
  273. {
  274. if (self::$m_bEnabled_Duration)
  275. {
  276. $fStopped = MyHelpers::getmicrotime();
  277. $fDuration = $fStopped - $this->m_fStarted;
  278. if (self::$m_bBlameCaller)
  279. {
  280. self::$m_aStats[$sOperation][$sArguments][] = array(
  281. 'time' => $fDuration,
  282. 'callers' => MyHelpers::get_callstack(1),
  283. );
  284. }
  285. else
  286. {
  287. self::$m_aStats[$sOperation][$sArguments][] = array(
  288. 'time' => $fDuration
  289. );
  290. }
  291. }
  292. }
  293. protected function ResetCounters()
  294. {
  295. if (self::$m_bEnabled_Duration)
  296. {
  297. $this->m_fStarted = MyHelpers::getmicrotime();
  298. }
  299. if (self::$m_bEnabled_Memory)
  300. {
  301. $this->m_iInitialMemory = self::memory_get_usage();
  302. }
  303. }
  304. const HtmlReportFile = 'log/kpi.html';
  305. static protected function Report($sText)
  306. {
  307. file_put_contents(APPROOT.self::HtmlReportFile, "$sText\n", FILE_APPEND | LOCK_EX);
  308. }
  309. static protected function MemStr($iMemory)
  310. {
  311. return round($iMemory / 1024).' Kb';
  312. }
  313. static protected function memory_get_usage()
  314. {
  315. if (function_exists('memory_get_usage'))
  316. {
  317. return memory_get_usage(true);
  318. }
  319. // Copied from the PHP manual
  320. //
  321. //If its Windows
  322. //Tested on Win XP Pro SP2. Should work on Win 2003 Server too
  323. //Doesn't work for 2000
  324. //If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642
  325. if (substr(PHP_OS,0,3) == 'WIN')
  326. {
  327. $output = array();
  328. exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output);
  329. return preg_replace( '/[\D]/', '', $output[5] ) * 1024;
  330. }
  331. else
  332. {
  333. //We now assume the OS is UNIX
  334. //Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4
  335. //This should work on most UNIX systems
  336. $pid = getmypid();
  337. exec("ps -eo%mem,rss,pid | grep $pid", $output);
  338. $output = explode(" ", $output[0]);
  339. //rss is given in 1024 byte units
  340. return $output[1] * 1024;
  341. }
  342. }
  343. static public function memory_get_peak_usage($bRealUsage = false)
  344. {
  345. if (function_exists('memory_get_peak_usage'))
  346. {
  347. return memory_get_peak_usage($bRealUsage);
  348. }
  349. // PHP > 5.2.1 - this verb depends on a compilation option
  350. return 0;
  351. }
  352. }