performance.module 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  1. <?php
  2. /**
  3. * @file
  4. *
  5. * Logs detailed and/or summary page generation time and memory consumption for
  6. * page requests.
  7. * Copyright Khalid Baheyeldin 2008 of http://2bits.com
  8. */
  9. // Check for a variable for the performance key. This allows you to set a
  10. // unique key in the case that you have multiple domains accessing the same
  11. // site. If this is not set, fall back to the hostname which we get from the
  12. // base_url global variable for drush compatibility (you will need to pass the
  13. // --url parameter to drush).
  14. define('PERFORMANCE_KEY', 'dru-perf:' . variable_get('performance_key', parse_url($GLOBALS['base_url'], PHP_URL_HOST)) . ':');
  15. define('PERFORMANCE_BIN', 'cache_performance');
  16. define('PERFORMANCE_QUERY_VAR', 'performance_query');
  17. define('PERFORMANCE_CACHE', 'cache_default_class');
  18. define('PERFORMANCE_SETTINGS', 'admin/config/development/performance-logging');
  19. /**
  20. * Implements hook_menu().
  21. */
  22. function performance_menu() {
  23. $items = array();
  24. $items[PERFORMANCE_SETTINGS] = array(
  25. 'title' => 'Performance logging',
  26. 'description' => 'Logs performance data: page generation times and memory usage.',
  27. 'page callback' => 'drupal_get_form',
  28. 'page arguments' => array('performance_settings_form'),
  29. 'access arguments' => array('administer performance logging'),
  30. 'type' => MENU_NORMAL_ITEM,
  31. );
  32. $items['admin/reports/performance-logging'] = array(
  33. 'title' => 'Performance logs',
  34. 'description' => 'View summary performance logs: page generation times and memory usage.',
  35. 'page callback' => 'performance_view_summary',
  36. 'access arguments' => array('administer performance logging'),
  37. 'type' => MENU_NORMAL_ITEM,
  38. );
  39. $items['admin/reports/performance-logging/summary'] = array(
  40. 'title' => 'Summary',
  41. 'type' => MENU_DEFAULT_LOCAL_TASK,
  42. 'weight' => 0,
  43. );
  44. $items['admin/reports/performance-logging/details'] = array(
  45. 'title' => 'Details',
  46. 'description' => 'View detailed, per page, performance logs: page generation times and memory usage.',
  47. 'page callback' => 'performance_view_details',
  48. 'access arguments' => array('administer performance logging'),
  49. 'type' => MENU_LOCAL_TASK,
  50. 'weight' => 1,
  51. );
  52. // We use no % after clear here (clear/%) to catch the arguments because we do
  53. // not want the clear page to fall back on its parent when no argument is
  54. // passed. See the callback of the page for more info.
  55. $items['admin/reports/performance-logging/clear'] = array(
  56. 'title' => 'Clear logs',
  57. 'description' => 'Clears all collected performance statistics.',
  58. 'page callback' => 'drupal_get_form',
  59. 'page arguments' => array('performance_clear_form'),
  60. 'access arguments' => array('administer performance logging'),
  61. 'type' => MENU_CALLBACK,
  62. 'weight' => 1,
  63. );
  64. return $items;
  65. }
  66. /**
  67. * Implements hook_permission().
  68. */
  69. function performance_permission() {
  70. return array(
  71. 'administer performance logging' => array(
  72. 'title' => t('Administer performance logging'),
  73. 'description' => t('Allows both configuring the performance module and accessing its reports.'),
  74. )
  75. );
  76. }
  77. /**
  78. * Implements hook_cron().
  79. */
  80. function performance_cron() {
  81. // Remove all entries that have expired.
  82. // TODO: make this work as expected, seems to throw away everything now :-s
  83. cache_clear_all(NULL, PERFORMANCE_BIN);
  84. // Remove entries that have less than so many accesses
  85. performance_traverse_cache('performance_cron_prune');
  86. // Remove performance_detail rows on a daily basis.
  87. db_delete('performance_detail')
  88. ->condition('timestamp', REQUEST_TIME - (24 * 60 * 60), '<=')
  89. ->execute();
  90. }
  91. /**
  92. * Callback used by performance_traverse_cache() for pruning data on cron based
  93. * on a preset threshold.
  94. *
  95. * @param $cache cache object
  96. *
  97. * @see performance_traverse_cache()
  98. */
  99. function performance_cron_prune($cache) {
  100. static $threshold;
  101. // Prevent a variable_get() each time this callback is triggered.
  102. if(!isset($threshold)) {
  103. $threshold = variable_get('performance_threshold_accesses', 0);
  104. }
  105. if ($threshold && $cache->data['num_accesses'] <= $threshold) {
  106. cache_clear_all($cache->cid, PERFORMANCE_BIN);
  107. }
  108. return;
  109. }
  110. /**
  111. * Implements hook_views_api().
  112. */
  113. function performance_views_api() {
  114. return array(
  115. 'api' => 3,
  116. 'path' => drupal_get_path('module', 'performance') . '/includes',
  117. );
  118. }
  119. /**
  120. * System settings form.
  121. */
  122. function performance_settings_form() {
  123. $status = performance_caching_message();
  124. // Setup settings form.
  125. $form['mode'] = array(
  126. '#type' => 'fieldset',
  127. '#title' => t('Logging mode'),
  128. '#collapsible' => TRUE,
  129. );
  130. $form['mode']['performance_detail'] = array(
  131. '#type' => 'checkbox',
  132. '#title' => t('Detailed logging'),
  133. '#default_value' => variable_get('performance_detail', 0),
  134. '#description' => t('Log memory usage and page generation times for every page. This logging mode is <strong>not</strong> suitable for large sites, as it can degrade performance severly. It is intended for use by developers, or on a test copy of the site.'),
  135. );
  136. $form['mode']['performance_summary'] = array(
  137. '#type' => 'checkbox',
  138. '#title' => t('Summary logging'),
  139. '#default_value' => variable_get('performance_summary', 0),
  140. '#description' => t('Log summary data, such as average and maximum page generation times and memory usage.'),
  141. );
  142. if ($status != 'error') {
  143. $form['mode']['performance_summary']['#description'] .= ' ' . t('The summary will be stored in an alternative cache, and hence there is no load on the database. This logging is suitable for most live sites, unless the number of unique page accesses is excessively high.');
  144. }
  145. else {
  146. $form['mode']['performance_summary']['#description'] .= ' ' . t('This logging mode is <strong>not</strong> suitable for most live sites.');
  147. }
  148. $form['other'] = array(
  149. '#type' => 'fieldset',
  150. '#title' => t('Other'),
  151. '#collapsible' => TRUE,
  152. );
  153. $form['other'][PERFORMANCE_QUERY_VAR] = array(
  154. '#type' => 'checkbox',
  155. '#title' => t('Database Query timing and count'),
  156. '#default_value' => variable_get(PERFORMANCE_QUERY_VAR, 0),
  157. '#description' => t('Log database query timing and query count for each page. This is useful to know if the bottleneck is in excessive database query counts, or the time required to execute those queries is high. Enabling this will incurr some memory overhead as query times and the actual query strings are cached in memory as arrays for each page, hence skewing the overall page memory reported.'),
  158. );
  159. $form['other']['performance_threshold_accesses'] = array(
  160. '#type' => 'select',
  161. '#title' => t('Accesses threshold'),
  162. '#default_value' => variable_get('performance_threshold_accesses', 0),
  163. '#options' => array(0, 1, 2, 5, 10),
  164. '#description' => t("When displaying the summary report, only pages with the number of accesses larger than the specified threshold will be shown. Also, when cron runs and summary is <strong>not</strong> logged to DB, pages with that number of accesses or less will be removed, so as not to overflow the cache's memory. This is useful on a live site with a high volume of hits. On a development site, you probably want this set to 0, so you can see all pages."),
  165. );
  166. $form['other']['performance_nodrush'] = array(
  167. '#type' => 'checkbox',
  168. '#title' => t('Do not log drush access'),
  169. '#default_value' => variable_get('performance_nodrush', 1),
  170. '#description' => t('Prevent !link access to the site from being logged.', array('!link' => l(t('drush'), 'http://www.drupal.org/project/drush', array('attributes' => array('target' => '_blank'))))),
  171. );
  172. $form['other']['performance_skip_paths'] = array(
  173. '#type' => 'textarea',
  174. '#title' => t('Paths to exclude'),
  175. '#default_value' => variable_get('performance_skip_paths', ''),
  176. '#description' => t("Enter one path per line as Drupal paths. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.", array('%blog' => 'blog', '%blog-wildcard' => 'blog/*', '%front' => '<front>')),
  177. );
  178. return system_settings_form($form);
  179. }
  180. /**
  181. * Display message on settings form.
  182. */
  183. function performance_caching_message() {
  184. $default = 'DrupalDatabaseCache';
  185. $type = 'error';
  186. $cache = variable_get(PERFORMANCE_CACHE, $default);
  187. if ($cache != $default) {
  188. $message = t('Alternative caching (%class) is enabled. It is reasonably safe to enable summary logging on live sites.', array('%class' => $cache));
  189. $type = 'status';
  190. }
  191. else {
  192. $message = t('Only the default database caching mechanism is enabled. It is <strong>not</strong> safe to enable summary logging to the database on live sites!');
  193. }
  194. drupal_set_message($message, $type, FALSE);
  195. return $type;
  196. }
  197. /**
  198. * Implements hook_boot().
  199. */
  200. function performance_boot() {
  201. register_shutdown_function('performance_shutdown');
  202. if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
  203. @include_once DRUPAL_ROOT . '/includes/database/log.inc';
  204. Database::startLog('performance', 'default');
  205. }
  206. }
  207. /**
  208. * Shutdown function that collects all performance data.
  209. */
  210. function performance_shutdown() {
  211. global $user, $language;
  212. // Don't log drush access.
  213. if (drupal_is_cli() && variable_get('performance_nodrush', 1)) {
  214. return;
  215. }
  216. if (isset($_GET['q']) && $_GET['q']) {
  217. // q= has a value, use that for the path
  218. $path = $_GET['q'];
  219. }
  220. elseif (drupal_is_cli()) {
  221. $path = 'drush';
  222. }
  223. else {
  224. // q= is empty, use whatever the site_frontpage is set to
  225. $path = variable_get('site_frontpage', 'node');
  226. }
  227. // Skip certain paths defined by the user.
  228. if (drupal_match_path($path, variable_get('performance_skip_paths', ''))) {
  229. return;
  230. }
  231. $params = array(
  232. 'timer' => timer_read('page'),
  233. 'path' => $path,
  234. );
  235. // Memory.
  236. // No need to check if this function exists in D7, as it has a minimal
  237. // requirement of PHP 5.2.5.
  238. $params['mem'] = memory_get_peak_usage(TRUE);
  239. // Query time and count
  240. $query_count = $query_timer = $sum = 0;
  241. if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
  242. // See http://drupal.org/node/1022204
  243. $queries = Database::getLog('performance', 'default');
  244. foreach ($queries as $query) {
  245. $sum += $query['time'];
  246. $query_count++;
  247. }
  248. $query_timer = round($sum * 1000, 2);
  249. }
  250. $params['query_count'] = $query_count;
  251. $params['query_timer'] = $query_timer;
  252. // Anonymous access?
  253. $params['anon'] = ($user->uid) ? 0 : 1;
  254. // Language
  255. $params['language'] = $language->language;
  256. // There used to be a module_invoke_all('performance', 'header', $header) call
  257. // here but it has been removed. $header was an associative array containing
  258. // path, timer (ms) and anon ('Yes' or 'No').
  259. if (variable_get('performance_detail', 0)) {
  260. // There used to be a module_invoke_all('performance', 'data') call here. As
  261. // it was undocumented and therefore unknown, it has been removed. The data
  262. // column has been kept so that we can re-implement if needed.
  263. $params['data'] = NULL;
  264. performance_log_details($params);
  265. }
  266. // There used to be a module_invoke_all('performance', 'disable') call here in
  267. // an else statement.
  268. if (variable_get('performance_summary', 0)) {
  269. performance_log_summary($params);
  270. }
  271. }
  272. /**
  273. * Store the summary data.
  274. */
  275. function performance_log_summary($params) {
  276. $key = PERFORMANCE_KEY . $params['path'] . ':' . $params['language'] . ':' . $params['anon'];
  277. $data = cache_get($key, PERFORMANCE_BIN);
  278. if (is_object($data)) {
  279. $data = $data->data;
  280. }
  281. $result = performance_build_summary_data($data, $params);
  282. if ($result['type'] == 'new') {
  283. // $keys_cache is used to easily retrieve our data later on.
  284. if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_BIN)) {
  285. $keys_values = $keys_cache->data;
  286. }
  287. // Keep the key for the key cache store. We do it this way so that keys
  288. // will replace eachother which would not happen when using
  289. // $keys_values[] = $key;
  290. $keys_values[$key] = 1;
  291. cache_set(PERFORMANCE_KEY, $keys_values, PERFORMANCE_BIN);
  292. }
  293. // Keep records for 1 day.
  294. $expire = $result['last_access'] + (24 * 60 * 60);
  295. cache_set($key, $result['data'], PERFORMANCE_BIN, $expire);
  296. }
  297. /**
  298. * Helper function to build summary data array.
  299. *
  300. * @param data array of previous data
  301. * @param params array of current data
  302. * @return array holding summary data
  303. */
  304. function performance_build_summary_data($data, $params) {
  305. if ($data) {
  306. $type = 'existing';
  307. $data = array(
  308. 'path' => $data['path'],
  309. 'bytes_max' => max($params['mem'], $data['bytes_max']),
  310. 'bytes_sum' => $data['bytes_sum'] + $params['mem'],
  311. 'ms_max' => max($params['timer'], $data['ms_max']),
  312. 'ms_sum' => $data['ms_sum'] + $params['timer'],
  313. 'query_timer_max' => max($params['query_timer'], $data['query_timer_max']),
  314. 'query_timer_sum' => $data['query_timer_sum'] + $params['query_timer'],
  315. 'query_count_max' => max($params['query_count'], $data['query_count_max']),
  316. 'query_count_sum' => $data['query_count_sum'] + $params['query_count'],
  317. 'num_accesses' => $data['num_accesses'] + 1,
  318. 'last_access' => REQUEST_TIME,
  319. 'anon' => $params['anon'],
  320. 'language' => $params['language'],
  321. );
  322. }
  323. else {
  324. $type = 'new';
  325. $data = array(
  326. 'path' => $params['path'],
  327. 'bytes_max' => $params['mem'],
  328. 'bytes_sum' => $params['mem'],
  329. 'ms_max' => (int)$params['timer'],
  330. 'ms_sum' => (int)$params['timer'],
  331. 'query_timer_max' => $params['query_timer'],
  332. 'query_timer_sum' => $params['query_timer'],
  333. 'query_count_max' => (int)$params['query_count'],
  334. 'query_count_sum' => (int)$params['query_count'],
  335. 'num_accesses' => 1,
  336. 'last_access' => REQUEST_TIME,
  337. 'anon' => $params['anon'],
  338. 'language' => $params['language'],
  339. );
  340. }
  341. return array('data' => $data, 'type' => $type);
  342. }
  343. /**
  344. * Helper function to traverse the cache_bin data for retrieving and/or
  345. * pruning data.
  346. *
  347. * @param $callback string function to execute on the data fetched
  348. * @param $args optional additional argument(s) to pass to the callback (use an
  349. * array or object to pass multiple arguments)
  350. * @return array of data where the contents depends on the callback
  351. */
  352. function performance_traverse_cache($callback, $args = NULL) {
  353. $data_list = array();
  354. $pruned = FALSE;
  355. if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_BIN)) {
  356. // is_array() check to prevent anything from ever going wrong here.
  357. if (is_array($keys_cache->data)) {
  358. foreach ($keys_cache->data as $key => $value) {
  359. $cache = cache_get($key, PERFORMANCE_BIN);
  360. if (!$cache) {
  361. // Cache entry for this key has expired, remove the key.
  362. unset($keys_cache->data[$key]);
  363. // Mark as pruned: we have to rewrite the keys cache!
  364. $pruned = TRUE;
  365. }
  366. else {
  367. // call_user_func() does not support passing by reference. See
  368. // http://php.net/manual/en/function.call-user-func-array.php and the
  369. // note about PHP 5.4 concerning the possibility of passing by
  370. // reference there. Hence this approach to prevent future
  371. // compatibility issues
  372. if ($data = call_user_func($callback, $cache, $args)) {
  373. $data_list[] = $data;
  374. }
  375. }
  376. }
  377. }
  378. }
  379. // Write the pruned key cache if needed.
  380. if ($pruned) {
  381. cache_set(PERFORMANCE_KEY, $keys_cache->data, PERFORMANCE_BIN);
  382. }
  383. return $data_list;
  384. }
  385. /**
  386. * Callback used by performance_traverse_cache() for fetching summary data.
  387. *
  388. * @param $cache cache object
  389. * @param $timestamp unix timestamp to start fetching data from
  390. * @return the processed data or NULL
  391. *
  392. * @see performance_traverse_cache()
  393. */
  394. function performance_get_summary($cache, $timestamp) {
  395. static $count = 0;
  396. // Don't combine these IF statemens here, otherwise else might get executed
  397. // while $timestamp IS set!
  398. if ($timestamp !== NULL) {
  399. if($cache->created >= $timestamp) {
  400. // return based on timestamp
  401. return $cache->data;
  402. }
  403. }
  404. else {
  405. // return paged
  406. global
  407. $pager_page_array, // array of element-keyed current page - 1
  408. $pager_total_items, // array of element-keyed total number of data rows
  409. $pager_limits, // array of element-keyed number of rows per page
  410. $pager_total; // array of element-keyed total number of pages
  411. $pager_total_items[0]++;
  412. if (($pager_page_array[0] * $pager_limits[0]) < $pager_total_items[0] && $count < $pager_limits[0]) {
  413. $count++;
  414. return $cache->data;
  415. }
  416. }
  417. return;
  418. }
  419. /**
  420. * Helper function to store detailed data in database.
  421. */
  422. function performance_log_details($params = array()) {
  423. $fields = array(
  424. 'timestamp' => REQUEST_TIME,
  425. 'bytes' => $params['mem'],
  426. 'ms' => (int)$params['timer'],
  427. 'query_count' => $params['query_count'],
  428. 'query_timer' => (int)$params['query_timer'],
  429. 'anon' => $params['anon'],
  430. 'path' => $params['path'],
  431. 'language' => $params['language'],
  432. 'data' => $params['data'],
  433. );
  434. try {
  435. db_insert('performance_detail')
  436. ->fields($fields)
  437. ->execute();
  438. }
  439. catch (Exception $e) {
  440. watchdog_exception('performance', $e, NULL, array(), WATCHDOG_ERROR);
  441. }
  442. }
  443. /**
  444. * Summary page callback.
  445. */
  446. function performance_view_summary() {
  447. drupal_set_title(t('Performance logs: Summary'));
  448. global
  449. $pager_page_array, // array of element-keyed current page - 1
  450. $pager_total_items, // array of element-keyed total number of data rows
  451. $pager_limits, // array of element-keyed number of rows per page
  452. $pager_total; // array of element-keyed total number of pages
  453. $rows = $data_list = array();
  454. // Build table header.
  455. $header = array(
  456. array('data' => t('Path'), 'field' => 'path'),
  457. array('data' => t('Last access'), 'field' => 'last_access'),
  458. array('data' => t('# accesses'), 'field' => 'num_accesses'),
  459. array('data' => t('MB Memory (Max)'), 'field' => 'bytes_max'),
  460. array('data' => t('MB Memory (Avg)'), 'field' => 'bytes_sum'),
  461. array('data' => t('ms (Max)'), 'field' => 'ms_max'),
  462. array('data' => t('ms (Avg)'), 'field' => 'ms_sum'),
  463. array('data' => t('Language'), 'field' => 'language'),
  464. array('data' => t('Anonymous?'), 'field' => 'anon'),
  465. );
  466. if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
  467. $header[] = array('data' => t('Query ms (Max)'), 'field' => 'query_timer_max');
  468. $header[] = array('data' => t('Query ms (Avg)'), 'field' => 'query_timer_sum');
  469. $header[] = array('data' => t('Query Count (Max)'), 'field' => 'query_count_max');
  470. $header[] = array('data' => t('Query Count (Avg)'), 'field' => 'query_count_sum');
  471. }
  472. // Set up pager since this is not done automatically when using caching bins.
  473. // Note that there can be data in these variables already hence the 'keyed'
  474. // setup of the arrays.
  475. $pager_height = 50;
  476. $pager_total_items = array(0 => 0);
  477. $pager_limits = array(0 => $pager_height);
  478. $page = isset($_GET['page']) ? sprintf('%d', $_GET['page']) : 0;
  479. $pager_page_array = array(0 => $page);
  480. $data_list = performance_traverse_cache('performance_get_summary');
  481. if (empty($data_list) && !variable_get('performance_summary', 0)) {
  482. return t('Summary performance log is not enabled. Go to the !link to enable it.', array('!link' => l(t('settings page'), PERFORMANCE_SETTINGS, array('query' => drupal_get_destination())))
  483. );
  484. }
  485. elseif (!variable_get('performance_summary', 0)) {
  486. drupal_set_message(t('Summary performance log is not enabled! Showing stored logs.'), 'warning');
  487. }
  488. $pager_total = array(0 => ceil($pager_total_items[0] / $pager_limits[0]));
  489. // Setup sorting since this is not done automatically when using caching bins.
  490. $sort_direction = tablesort_get_sort($header);
  491. $sort_field = tablesort_get_order($header);
  492. // TODO: find a solution for the avg columns! These need to be calculated
  493. // first, prolly...
  494. $data_list = performance_sort_summary($data_list, $sort_direction, $sort_field['sql']);
  495. // Format data into table.
  496. $threshold = variable_get('performance_threshold_accesses', 0);
  497. $total_rows = $shown = $last_max = $total_bytes = $total_ms = $total_accesses = 0;
  498. $last_min = REQUEST_TIME;
  499. foreach ($data_list as $data) {
  500. $total_rows++;
  501. $last_max = max($last_max, $data['last_access']);
  502. $last_min = min($last_min, $data['last_access']);
  503. // Calculate running averages.
  504. $total_bytes += $data['bytes_sum'] / $data['num_accesses'];
  505. $total_ms += $data['ms_sum'] / $data['num_accesses'];
  506. $total_accesses += $data['num_accesses'];
  507. $row_data = array();
  508. if ($data['num_accesses'] > $threshold) {
  509. $shown++;
  510. $row_data[] = l($data['path'], $data['path']);
  511. $row_data[] = format_date($data['last_access'], 'small');
  512. $row_data[] = $data['num_accesses'];
  513. $row_data[] = number_format($data['bytes_max'] / 1024 / 1024, 2);
  514. $row_data[] = number_format($data['bytes_sum'] / $data['num_accesses'] / 1024 / 1024, 2);
  515. $row_data[] = number_format($data['ms_max'], 1);
  516. $row_data[] = number_format($data['ms_sum'] / $data['num_accesses'], 1);
  517. $row_data[] = $data['language'];
  518. $row_data[] = ($data['anon']) ? t('Yes') : t('No');
  519. if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
  520. $row_data[] = number_format($data['query_timer_max'], 1);
  521. $row_data[] = number_format($data['query_timer_sum'] / $data['num_accesses'], 1);
  522. $row_data[] = $data['query_count_max'];
  523. $row_data[] = $data['query_count_sum'] / $data['num_accesses'];
  524. }
  525. }
  526. $rows[] = array('data' => $row_data);
  527. }
  528. $output = '';
  529. if ($threshold) {
  530. $output .= t('Showing !shown paths with more than !threshold accesses, out of !total total paths.',
  531. array('!threshold' => $threshold, '!shown' => $shown, '!total' => $total_rows)) . '<br/>';
  532. }
  533. else {
  534. $output .= t('Showing all !total paths.', array('!total' => $total_rows)) . '<br/>';
  535. }
  536. // Protect against divide by zero.
  537. if ($total_rows > 0) {
  538. $mb_avg = number_format($total_bytes / $total_rows / 1024 / 1024, 1);
  539. $ms_avg = number_format($total_ms / $total_rows, 2);
  540. }
  541. else {
  542. $mb_avg = 'n/a';
  543. $ms_avg = 'n/a';
  544. }
  545. $output .= t('Average memory per page: !mb_avg MB', array('!mb_avg' => $mb_avg)) . '<br/>';
  546. $output .= t('Average duration per page: !ms_avg ms', array('!ms_avg' => $ms_avg)) . '<br/>';
  547. $output .= t('Total number of page accesses: !accesses', array('!accesses' => $total_accesses)) . '<br/>';
  548. $output .= t('First access: !access.', array('!access' => format_date($last_min, 'small'))) . '<br/>';
  549. $output .= t('Last access: !access.', array('!access' => format_date($last_max, 'small')));
  550. // Return a renderable array.
  551. return array(
  552. 'general_info' => array(
  553. '#prefix' => '<p>',
  554. '#markup' => $output,
  555. '#suffix' => '</p><p>&nbsp;</p>',
  556. ),
  557. 'query_data_summary' => array(
  558. '#theme' => 'table',
  559. '#header' => $header,
  560. '#rows' => $rows,
  561. '#sticky' => TRUE,
  562. '#empty' => t('No statistics available yet.'),
  563. ),
  564. 'pager' => array(
  565. '#theme' => 'pager',
  566. '#quantity' => $pager_height,
  567. ),
  568. 'clear' => array(
  569. '#markup' => l(t('Clear logs'), 'admin/reports/performance-logging/clear/summary'),
  570. ),
  571. );
  572. }
  573. /**
  574. * Helper function to sort data from the cache bin.
  575. *
  576. * @param $data array of data to sort
  577. * @param $direction string asc or desc
  578. * @param $field string name of field to sort
  579. * @return sorted $data array
  580. *
  581. * @see array_multisort()
  582. */
  583. function performance_sort_summary($data, $direction, $field) {
  584. if(empty($data)) {
  585. return $data;
  586. }
  587. switch($direction) {
  588. case 'asc':
  589. $direction = SORT_ASC;
  590. break;
  591. case 'desc':
  592. $direction = SORT_DESC;
  593. break;
  594. }
  595. // Extract the column of data to be sorted.
  596. $column = array();
  597. foreach ($data as $key => $row) {
  598. $column[$key] = $row[$field];
  599. }
  600. array_multisort($column, $direction, $data);
  601. return $data;
  602. }
  603. /**
  604. * Detail page callback.
  605. */
  606. function performance_view_details() {
  607. drupal_set_title(t('Performance logs: Details'));
  608. $header = array(
  609. array('data' => t('#'), 'field' => 'pid', 'sort' => 'desc'),
  610. array('data' => t('Path'), 'field' => 'path'),
  611. array('data' => t('Date'), 'field' => 'timestamp'),
  612. array('data' => t('Memory (MB)'), 'field' => 'bytes'),
  613. array('data' => t('ms (Total)'), 'field' => 'ms'),
  614. array('data' => t('Language'), 'field' => 'language'),
  615. array('data' => t('Anonymous?'), 'field' => 'anon'),
  616. );
  617. if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
  618. $header[] = array('data' => t('# Queries'), 'field' => 'query_count');
  619. $header[] = array('data' => t('Query ms'), 'field' => 'query_timer');
  620. }
  621. $pager_height = 50;
  622. $result = db_select('performance_detail', 'p')
  623. ->fields('p')
  624. ->extend('PagerDefault')
  625. ->limit($pager_height)
  626. ->extend('TableSort')
  627. ->orderByHeader($header)
  628. ->execute();
  629. $rows = array();
  630. foreach ($result as $data) {
  631. $row_data = array();
  632. $row_data[] = $data->pid;
  633. $row_data[] = l($data->path, $data->path);
  634. $row_data[] = format_date($data->timestamp, 'small');
  635. $row_data[] = number_format($data->bytes / 1024 / 1024, 2);
  636. $row_data[] = $data->ms;
  637. $row_data[] = $data->language;
  638. $row_data[] = ($data->anon) ? t('Yes') : t('No');
  639. if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
  640. $row_data[] = $data->query_count;
  641. $row_data[] = $data->query_timer;
  642. }
  643. $rows[] = array('data' => $row_data);
  644. }
  645. if (empty($rows) && !variable_get('performance_detail', 0)) {
  646. return array(
  647. 'content' => array(
  648. '#markup' => t('Detail performance log is not enabled. Go to the !link to enable it.', array('!link' => l(t('settings page'), PERFORMANCE_SETTINGS, array('query' => drupal_get_destination()))))
  649. ),
  650. );
  651. }
  652. elseif (!variable_get('performance_detail', 0)) {
  653. drupal_set_message(t('Detail performance log is not enabled! Showing stored logs.'), 'warning');
  654. }
  655. // Return a renderable array.
  656. return array(
  657. 'query_data_detail' => array(
  658. '#theme' => 'table',
  659. '#header' => $header,
  660. '#rows' => $rows,
  661. '#sticky' => TRUE,
  662. '#empty' => t('No log messages available.'),
  663. ),
  664. 'clear' => array(
  665. '#markup' => l(t('Clear logs'), 'admin/reports/performance-logging/clear/details'),
  666. ),
  667. 'pager' => array(
  668. '#theme' => 'pager',
  669. '#quantity' => $pager_height,
  670. ),
  671. );
  672. }
  673. /**
  674. * Clear logs form.
  675. */
  676. function performance_clear_form($form, &$form_state, $store = NULL) {
  677. $base = 'admin/reports/performance-logging/';
  678. // Seemed the best solution, instead of doing something like
  679. // t('Are you sure you want to clear all @store data?', array ('@store' => $store));
  680. switch ($store) {
  681. case 'summary':
  682. $question = t('Are you sure you want to clear all summary data?');
  683. $path = $base . 'summary';
  684. break;
  685. case 'details':
  686. $question = t('Are you sure you want to clear all detail data?');
  687. $path = $base . 'details';
  688. break;
  689. default:
  690. // None or unrecognised store => 404.
  691. drupal_not_found();
  692. return 2;
  693. }
  694. $form['store'] = array(
  695. '#type' => 'value',
  696. '#value' => $store,
  697. );
  698. $form['redirect'] = array(
  699. '#type' => 'value',
  700. '#value' => $path,
  701. );
  702. return confirm_form($form, $question, $path);
  703. }
  704. /**
  705. * Clear logs form submit handler.
  706. */
  707. function performance_clear_form_submit($form, &$form_state) {
  708. switch ($form_state['values']['store']) {
  709. case 'summary':
  710. cache_clear_all('*', PERFORMANCE_BIN, TRUE);
  711. break;
  712. case 'details':
  713. db_truncate('performance_detail')->execute();
  714. break;
  715. }
  716. $form_state['redirect'] = array($form_state['values']['redirect']);
  717. }
  718. /**
  719. * Gather performance data for external modules.
  720. */
  721. function performance_gather_summary_data() {
  722. // Data from last 15 minutes.
  723. $timestamp = REQUEST_TIME - 15 * 60;
  724. $data_list = performance_traverse_cache('performance_get_summary', $timestamp);
  725. // Initialize variables.
  726. $total_rows = $total_bytes = $total_ms = $total_accesses = $total_query_time = $total_query_count = 0;
  727. foreach ($data_list as $data) {
  728. $total_rows++;
  729. // Calculate running averages.
  730. $total_bytes += $data['bytes_sum'] / $data['num_accesses'];
  731. $total_ms += $data['ms_sum'] / $data['num_accesses'];
  732. $total_accesses += $data['num_accesses'];
  733. $total_query_time += $data['query_timer_sum'] / $data['num_accesses'];
  734. $total_query_count += $data['query_count_sum'] / $data['num_accesses'];
  735. }
  736. $results = array();
  737. $results['total_accesses'] = $total_accesses;
  738. // Protect against divide by zero.
  739. if ($total_rows > 0) {
  740. $results['ms_avg'] = number_format($total_ms / $total_rows, 1, '.', '');
  741. $results['ms_query'] = number_format($total_query_time / $total_rows, 1, '.', '');
  742. $results['query_count'] = number_format($total_query_count / $total_rows, 2, '.', '');
  743. $results['mb_avg'] = number_format($total_bytes / $total_rows / 1024 / 1024, 1);
  744. }
  745. else {
  746. $results['ms_avg'] = '';
  747. $results['ms_query'] = '';
  748. $results['mb_avg'] = '';
  749. $results['query_count'] = '';
  750. }
  751. return $results;
  752. }
  753. /**
  754. * Implements hook_nagios_info().
  755. */
  756. function performance_nagios_info() {
  757. return array(
  758. 'name' => 'Performance logging',
  759. 'id' => 'PERF',
  760. );
  761. }
  762. /**
  763. * Implements hook_nagios().
  764. */
  765. function performance_nagios() {
  766. $data = performance_gather_summary_data();
  767. if (!$data) {
  768. $info = performance_nagios_info();
  769. return array(
  770. $info['id'] => array(
  771. 'status' => NAGIOS_STATUS_UNKNOWN,
  772. 'type' => 'perf',
  773. 'text' => t('Performance logging is not enabled'),
  774. ),
  775. );
  776. }
  777. $status = NAGIOS_STATUS_OK;
  778. return array(
  779. 'ACC' => array(
  780. 'status' => $status,
  781. 'type' => 'perf',
  782. 'text' => $data['total_accesses'],
  783. ),
  784. 'MS' => array(
  785. 'status' => $status,
  786. 'type' => 'perf',
  787. 'text' => $data['ms_avg'],
  788. ),
  789. 'MMB' => array(
  790. 'status' => $status,
  791. 'type' => 'perf',
  792. 'text' => $data['mb_avg'],
  793. ),
  794. 'QRC' => array(
  795. 'status' => $status,
  796. 'type' => 'perf',
  797. 'text' => $data['query_count'],
  798. ),
  799. 'QRT' => array(
  800. 'status' => $status,
  801. 'type' => 'perf',
  802. 'text' => $data['ms_query'],
  803. ),
  804. );
  805. }
  806. /**
  807. * Implements hook_prod_check_alter().
  808. */
  809. function performance_prod_check_alter(&$checks) {
  810. $checks['perf_data']['functions']['performance_prod_check_return_data'] = 'Performance logging';
  811. }
  812. /**
  813. * Return performance data to Production Monitor.
  814. */
  815. function performance_prod_check_return_data() {
  816. $data = performance_gather_summary_data();
  817. if (!$data) {
  818. return array(
  819. 'performance' => array(
  820. 'title' => 'Performance logging',
  821. 'data' => 'No performance data found.',
  822. ),
  823. );
  824. }
  825. return array(
  826. 'performance' => array(
  827. 'title' => 'Performance logging',
  828. 'data' => array(
  829. 'Total number of page accesses' => array($data['total_accesses']),
  830. 'Average duration per page' => array($data['ms_avg'], 'ms'),
  831. 'Average memory per page' => array($data['mb_avg'], 'MB'),
  832. 'Average querycount' => array($data['query_count']),
  833. 'Average duration per query' => array($data['ms_query'], 'ms'),
  834. ),
  835. ),
  836. );
  837. }