')),
);
return system_settings_form($form);
}
/**
* Display message on settings form.
*/
function performance_caching_message() {
$default = 'DrupalDatabaseCache';
$type = 'error';
$cache = variable_get(PERFORMANCE_CACHE, $default);
if ($cache != $default) {
$message = t('Alternative caching (%class) is enabled. It is reasonably safe to enable summary logging on live sites.', array('%class' => $cache));
$type = 'status';
}
else {
$message = t('Only the default database caching mechanism is enabled. It is not safe to enable summary logging to the database on live sites!');
}
drupal_set_message($message, $type, FALSE);
return $type;
}
/**
* Implements hook_boot().
*/
function performance_boot() {
register_shutdown_function('performance_shutdown');
if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
@include_once DRUPAL_ROOT . '/includes/database/log.inc';
Database::startLog('performance', 'default');
}
}
/**
* Shutdown function that collects all performance data.
*/
function performance_shutdown() {
global $user, $language;
// Don't log drush access.
if (drupal_is_cli() && variable_get('performance_nodrush', 1)) {
return;
}
if (isset($_GET['q']) && $_GET['q']) {
// q= has a value, use that for the path
$path = $_GET['q'];
}
elseif (drupal_is_cli()) {
$path = 'drush';
}
else {
// q= is empty, use whatever the site_frontpage is set to
$path = variable_get('site_frontpage', 'node');
}
// Skip if page from cache or on certain paths defined by the user.
if (!function_exists('drupal_match_path') || drupal_match_path($path, variable_get('performance_skip_paths', ''))) {
return;
}
$params = array(
'timer' => timer_read('page'),
'path' => $path,
);
// Memory.
// No need to check if this function exists in D7, as it has a minimal
// requirement of PHP 5.2.5.
$params['mem'] = memory_get_peak_usage(TRUE);
// Query time and count
$query_count = $query_timer = $sum = 0;
if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
// See http://drupal.org/node/1022204
$queries = Database::getLog('performance', 'default');
foreach ($queries as $query) {
$sum += $query['time'];
$query_count++;
}
$query_timer = round($sum * 1000, 2);
}
$params['query_count'] = $query_count;
$params['query_timer'] = $query_timer;
// Anonymous access?
$params['anon'] = ($user->uid) ? 0 : 1;
// Language
$params['language'] = $language->language;
// There used to be a module_invoke_all('performance', 'header', $header) call
// here but it has been removed. $header was an associative array containing
// path, timer (ms) and anon ('Yes' or 'No').
if (variable_get('performance_detail', 0)) {
// There used to be a module_invoke_all('performance', 'data') call here. As
// it was undocumented and therefore unknown, it has been removed. The data
// column has been kept so that we can re-implement if needed.
$params['data'] = NULL;
performance_log_details($params);
}
// There used to be a module_invoke_all('performance', 'disable') call here in
// an else statement.
if (variable_get('performance_summary', 0)) {
performance_log_summary($params);
}
}
/**
* Store the summary data.
*/
function performance_log_summary($params) {
$key = PERFORMANCE_KEY . $params['path'] . ':' . $params['language'] . ':' . $params['anon'];
$data = cache_get($key, PERFORMANCE_BIN);
if (is_object($data)) {
$data = $data->data;
}
$result = performance_build_summary_data($data, $params);
if ($result['type'] == 'new') {
// $keys_cache is used to easily retrieve our data later on.
if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_BIN)) {
$keys_values = $keys_cache->data;
}
// Keep the key for the key cache store. We do it this way so that keys
// will replace eachother which would not happen when using
// $keys_values[] = $key;
$keys_values[$key] = 1;
cache_set(PERFORMANCE_KEY, $keys_values, PERFORMANCE_BIN);
}
// Keep records for 1 day.
$expire = $result['data']['last_access'] + (24 * 60 * 60);
cache_set($key, $result['data'], PERFORMANCE_BIN, $expire);
}
/**
* Helper function to build summary data array.
*
* @param data array of previous data
* @param params array of current data
* @return array holding summary data
*/
function performance_build_summary_data($data, $params) {
if ($data) {
$type = 'existing';
$data = array(
'path' => $data['path'],
'bytes_max' => max($params['mem'], $data['bytes_max']),
'bytes_sum' => $data['bytes_sum'] + $params['mem'],
'ms_max' => max($params['timer'], $data['ms_max']),
'ms_sum' => $data['ms_sum'] + $params['timer'],
'query_timer_max' => max($params['query_timer'], $data['query_timer_max']),
'query_timer_sum' => $data['query_timer_sum'] + $params['query_timer'],
'query_count_max' => max($params['query_count'], $data['query_count_max']),
'query_count_sum' => $data['query_count_sum'] + $params['query_count'],
'num_accesses' => $data['num_accesses'] + 1,
'last_access' => REQUEST_TIME,
'anon' => $params['anon'],
'language' => $params['language'],
);
}
else {
$type = 'new';
$data = array(
'path' => $params['path'],
'bytes_max' => $params['mem'],
'bytes_sum' => $params['mem'],
'ms_max' => (int)$params['timer'],
'ms_sum' => (int)$params['timer'],
'query_timer_max' => $params['query_timer'],
'query_timer_sum' => $params['query_timer'],
'query_count_max' => (int)$params['query_count'],
'query_count_sum' => (int)$params['query_count'],
'num_accesses' => 1,
'last_access' => REQUEST_TIME,
'anon' => $params['anon'],
'language' => $params['language'],
);
}
return array('data' => $data, 'type' => $type);
}
/**
* Helper function to traverse the cache_bin data for retrieving and/or
* pruning data.
*
* @param $callback string function to execute on the data fetched
* @param $args optional additional argument(s) to pass to the callback (use an
* array or object to pass multiple arguments)
* @return array of data where the contents depends on the callback
*/
function performance_traverse_cache($callback, $args = NULL) {
$data_list = array();
$pruned = FALSE;
if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_BIN)) {
// is_array() check to prevent anything from ever going wrong here.
if (is_array($keys_cache->data)) {
foreach ($keys_cache->data as $key => $value) {
$cache = cache_get($key, PERFORMANCE_BIN);
if (!$cache) {
// Cache entry for this key has expired, remove the key.
unset($keys_cache->data[$key]);
// Mark as pruned: we have to rewrite the keys cache!
$pruned = TRUE;
}
else {
// call_user_func() does not support passing by reference. See
// http://php.net/manual/en/function.call-user-func-array.php and the
// note about PHP 5.4 concerning the possibility of passing by
// reference there. Hence this approach to prevent future
// compatibility issues
if ($data = call_user_func($callback, $cache, $args)) {
$data_list[] = $data;
}
}
}
}
}
// Write the pruned key cache if needed.
if ($pruned) {
cache_set(PERFORMANCE_KEY, $keys_cache->data, PERFORMANCE_BIN);
}
return $data_list;
}
/**
* Callback used by performance_traverse_cache() for fetching summary data.
*
* @param $cache cache object
* @param $timestamp unix timestamp to start fetching data from
* @return the processed data or NULL
*
* @see performance_traverse_cache()
*/
function performance_get_summary($cache, $timestamp) {
static $count = 0;
// Don't combine these IF statemens here, otherwise else might get executed
// while $timestamp IS set!
if ($timestamp !== NULL) {
if($cache->created >= $timestamp) {
// return based on timestamp
return $cache->data;
}
}
else {
// return paged
global
$pager_page_array, // array of element-keyed current page - 1
$pager_total_items, // array of element-keyed total number of data rows
$pager_limits, // array of element-keyed number of rows per page
$pager_total; // array of element-keyed total number of pages
$pager_total_items[0]++;
if (($pager_page_array[0] * $pager_limits[0]) < $pager_total_items[0] && $count < $pager_limits[0]) {
$count++;
return $cache->data;
}
}
return;
}
/**
* Summary page callback.
*/
function performance_view_summary() {
drupal_set_title(t('Performance logs: Summary'));
global
$pager_page_array, // array of element-keyed current page - 1
$pager_total_items, // array of element-keyed total number of data rows
$pager_limits, // array of element-keyed number of rows per page
$pager_total; // array of element-keyed total number of pages
$rows = $data_list = array();
// Build table header.
$header = array(
array('data' => t('Path'), 'field' => 'path'),
array('data' => t('Last access'), 'field' => 'last_access'),
array('data' => t('# accesses'), 'field' => 'num_accesses'),
array('data' => t('MB Memory (Max)'), 'field' => 'bytes_max'),
array('data' => t('MB Memory (Avg)'), 'field' => 'bytes_sum'),
array('data' => t('ms (Max)'), 'field' => 'ms_max'),
array('data' => t('ms (Avg)'), 'field' => 'ms_sum'),
array('data' => t('Language'), 'field' => 'language'),
array('data' => t('Anonymous?'), 'field' => 'anon'),
);
if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
$header[] = array('data' => t('Query ms (Max)'), 'field' => 'query_timer_max');
$header[] = array('data' => t('Query ms (Avg)'), 'field' => 'query_timer_sum');
$header[] = array('data' => t('Query Count (Max)'), 'field' => 'query_count_max');
$header[] = array('data' => t('Query Count (Avg)'), 'field' => 'query_count_sum');
}
// Set up pager since this is not done automatically when using caching bins.
// Note that there can be data in these variables already hence the 'keyed'
// setup of the arrays.
$pager_height = 50;
$pager_total_items = array(0 => 0);
$pager_limits = array(0 => $pager_height);
$page = isset($_GET['page']) ? sprintf('%d', $_GET['page']) : 0;
$pager_page_array = array(0 => $page);
$data_list = performance_traverse_cache('performance_get_summary');
if (empty($data_list) && !variable_get('performance_summary', 0)) {
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())))
);
}
elseif (!variable_get('performance_summary', 0)) {
drupal_set_message(t('Summary performance log is not enabled! Showing stored logs.'), 'warning');
}
$pager_total = array(0 => ceil($pager_total_items[0] / $pager_limits[0]));
// Setup sorting since this is not done automatically when using caching bins.
$sort_direction = tablesort_get_sort($header);
$sort_field = tablesort_get_order($header);
// TODO: find a solution for the avg columns! These need to be calculated
// first, prolly...
$data_list = performance_sort_summary($data_list, $sort_direction, $sort_field['sql']);
// Format data into table.
$threshold = variable_get('performance_threshold_accesses', 0);
$total_rows = $shown = $last_max = $total_bytes = $total_ms = $total_accesses = 0;
$last_min = REQUEST_TIME;
foreach ($data_list as $data) {
$total_rows++;
$last_max = max($last_max, $data['last_access']);
$last_min = min($last_min, $data['last_access']);
// Calculate running averages.
$total_bytes += $data['bytes_sum'] / $data['num_accesses'];
$total_ms += $data['ms_sum'] / $data['num_accesses'];
$total_accesses += $data['num_accesses'];
$row_data = array();
if ($data['num_accesses'] > $threshold) {
$shown++;
$row_data[] = l($data['path'], $data['path']);
$row_data[] = format_date($data['last_access'], 'small');
$row_data[] = $data['num_accesses'];
$row_data[] = number_format($data['bytes_max'] / 1024 / 1024, 2);
$row_data[] = number_format($data['bytes_sum'] / $data['num_accesses'] / 1024 / 1024, 2);
$row_data[] = number_format($data['ms_max'], 1);
$row_data[] = number_format($data['ms_sum'] / $data['num_accesses'], 1);
$row_data[] = $data['language'];
$row_data[] = ($data['anon']) ? t('Yes') : t('No');
if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
$row_data[] = number_format($data['query_timer_max'], 1);
$row_data[] = number_format($data['query_timer_sum'] / $data['num_accesses'], 1);
$row_data[] = $data['query_count_max'];
$row_data[] = $data['query_count_sum'] / $data['num_accesses'];
}
}
$rows[] = array('data' => $row_data);
}
$output = '';
if ($threshold) {
$output .= t('Showing !shown paths with more than !threshold accesses, out of !total total paths.',
array('!threshold' => $threshold, '!shown' => $shown, '!total' => $total_rows)) . '
';
}
else {
$output .= t('Showing all !total paths.', array('!total' => $total_rows)) . '
';
}
// Protect against divide by zero.
if ($total_rows > 0) {
$mb_avg = number_format($total_bytes / $total_rows / 1024 / 1024, 1);
$ms_avg = number_format($total_ms / $total_rows, 2);
}
else {
$mb_avg = 'n/a';
$ms_avg = 'n/a';
}
$output .= t('Average memory per page: !mb_avg MB', array('!mb_avg' => $mb_avg)) . '
';
$output .= t('Average duration per page: !ms_avg ms', array('!ms_avg' => $ms_avg)) . '
';
$output .= t('Total number of page accesses: !accesses', array('!accesses' => $total_accesses)) . '
';
$output .= t('First access: !access.', array('!access' => format_date($last_min, 'small'))) . '
';
$output .= t('Last access: !access.', array('!access' => format_date($last_max, 'small')));
// Return a renderable array.
return array(
'general_info' => array(
'#prefix' => '',
'#markup' => $output,
'#suffix' => '
',
),
'query_data_summary' => array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#sticky' => TRUE,
'#empty' => t('No statistics available yet.'),
),
'pager' => array(
'#theme' => 'pager',
'#quantity' => $pager_height,
),
'clear' => array(
'#markup' => l(t('Clear logs'), 'admin/reports/performance-logging/clear/summary'),
),
);
}
/**
* Helper function to sort data from the cache bin.
*
* @param $data array of data to sort
* @param $direction string asc or desc
* @param $field string name of field to sort
* @return sorted $data array
*
* @see array_multisort()
*/
function performance_sort_summary($data, $direction, $field) {
if(empty($data)) {
return $data;
}
switch($direction) {
case 'asc':
$direction = SORT_ASC;
break;
case 'desc':
$direction = SORT_DESC;
break;
}
// Extract the column of data to be sorted.
$column = array();
foreach ($data as $key => $row) {
$column[$key] = $row[$field];
}
array_multisort($column, $direction, $data);
return $data;
}
/**
* Clear logs form.
*/
function performance_clear_form($form, &$form_state, $store = NULL) {
$base = 'admin/reports/performance-logging/';
// Seemed the best solution, instead of doing something like
// t('Are you sure you want to clear all @store data?', array ('@store' => $store));
switch ($store) {
case 'summary':
$question = t('Are you sure you want to clear all summary data?');
$path = $base . 'summary';
break;
case 'details':
$question = t('Are you sure you want to clear all detail data?');
$path = $base . 'details';
break;
default:
// None or unrecognised store => 404.
drupal_not_found();
return 2;
}
$form['store'] = array(
'#type' => 'value',
'#value' => $store,
);
$form['redirect'] = array(
'#type' => 'value',
'#value' => $path,
);
return confirm_form($form, $question, $path);
}
/**
* Clear logs form submit handler.
*/
function performance_clear_form_submit($form, &$form_state) {
switch ($form_state['values']['store']) {
case 'summary':
cache_clear_all('*', PERFORMANCE_BIN, TRUE);
break;
case 'details':
performance_clear_details();
break;
}
$form_state['redirect'] = array($form_state['values']['redirect']);
}
/**
* Gather performance data for external modules.
*/
function performance_gather_summary_data() {
// Data from last 15 minutes.
$timestamp = REQUEST_TIME - 15 * 60;
$data_list = performance_traverse_cache('performance_get_summary', $timestamp);
// Initialize variables.
$total_rows = $total_bytes = $total_ms = $total_accesses = $total_query_time = $total_query_count = 0;
foreach ($data_list as $data) {
$total_rows++;
// Calculate running averages.
$total_bytes += $data['bytes_sum'] / $data['num_accesses'];
$total_ms += $data['ms_sum'] / $data['num_accesses'];
$total_accesses += $data['num_accesses'];
$total_query_time += $data['query_timer_sum'] / $data['num_accesses'];
$total_query_count += $data['query_count_sum'] / $data['num_accesses'];
}
$results = array();
$results['total_accesses'] = $total_accesses;
// Protect against divide by zero.
if ($total_rows > 0) {
$results['ms_avg'] = number_format($total_ms / $total_rows, 1, '.', '');
$results['ms_query'] = number_format($total_query_time / $total_rows, 1, '.', '');
$results['query_count'] = number_format($total_query_count / $total_rows, 2, '.', '');
$results['mb_avg'] = number_format($total_bytes / $total_rows / 1024 / 1024, 1);
}
else {
$results['ms_avg'] = '';
$results['ms_query'] = '';
$results['mb_avg'] = '';
$results['query_count'] = '';
}
return $results;
}
/**
* Implements hook_nagios_info().
*/
function performance_nagios_info() {
return array(
'name' => 'Performance logging',
'id' => 'PERF',
);
}
/**
* Implements hook_nagios().
*/
function performance_nagios() {
$data = performance_gather_summary_data();
if (!$data) {
$info = performance_nagios_info();
return array(
$info['id'] => array(
'status' => NAGIOS_STATUS_UNKNOWN,
'type' => 'perf',
'text' => t('Performance logging is not enabled'),
),
);
}
$status = NAGIOS_STATUS_OK;
return array(
'ACC' => array(
'status' => $status,
'type' => 'perf',
'text' => $data['total_accesses'],
),
'MS' => array(
'status' => $status,
'type' => 'perf',
'text' => $data['ms_avg'],
),
'MMB' => array(
'status' => $status,
'type' => 'perf',
'text' => $data['mb_avg'],
),
'QRC' => array(
'status' => $status,
'type' => 'perf',
'text' => $data['query_count'],
),
'QRT' => array(
'status' => $status,
'type' => 'perf',
'text' => $data['ms_query'],
),
);
}
/**
* Implements hook_prod_check_alter().
*/
function performance_prod_check_alter(&$checks) {
$checks['perf_data']['functions']['performance_prod_check_return_data'] = 'Performance logging';
}
/**
* Return performance data to Production Monitor.
*/
function performance_prod_check_return_data() {
$data = performance_gather_summary_data();
if (!$data) {
return array(
'performance' => array(
'title' => 'Performance logging',
'data' => 'No performance data found.',
),
);
}
return array(
'performance' => array(
'title' => 'Performance logging',
'data' => array(
'Total number of page accesses' => array($data['total_accesses']),
'Average duration per page' => array($data['ms_avg'], 'ms'),
'Average memory per page' => array($data['mb_avg'], 'MB'),
'Average querycount' => array($data['query_count']),
'Average duration per query' => array($data['ms_query'], 'ms'),
),
),
);
}