You are here

performance.module in Performance Logging and Monitoring 7

Logs detailed and/or summary page generation time and memory consumption for page requests. Copyright Khalid Baheyeldin 2008 of http://2bits.com

File

performance.module
View source
<?php

/**
 * @file
 *
 * Logs detailed and/or summary page generation time and memory consumption for
 * page requests.
 * Copyright Khalid Baheyeldin 2008 of http://2bits.com
 */

// Check for a variable for the performance key. This allows you to set a
// unique key in the case that you have multiple domains accessing the same
// site. If this is not set, fall back to the hostname which we get from the
// base_url global variable for drush compatibility (you will need to pass the
// --url parameter to drush).
define('PERFORMANCE_KEY', 'dru-perf:' . variable_get('performance_key', parse_url($GLOBALS['base_url'], PHP_URL_HOST)) . ':');
define('PERFORMANCE_MEMCACHE_BIN', 'cache_performance');
define('PERFORMANCE_ZEND_NAMESPACE', 'dru-perf-' . variable_get('performance_key', $_SERVER['HTTP_HOST']));
define('PERFORMANCE_ZEND_KEYS', PERFORMANCE_ZEND_NAMESPACE . '::all-of-the-keys');
define('PERFORMANCE_QUERY_VAR', 'performance_query');
define('PERFORMANCE_SETTINGS', 'admin/config/development/performance-logging');

/**
 * Implementation of hook_menu().
 */
function performance_menu() {
  $items = array();
  $items[PERFORMANCE_SETTINGS] = array(
    'title' => 'Performance logging',
    'description' => 'Logs performance data: page generation times and memory usage.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'performance_settings_form',
    ),
    'access arguments' => array(
      'administer performance logging',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/development/performance-logging/settings'] = array(
    'title' => 'Performance logging',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
  $items['admin/config/development/performance-logging/clear'] = array(
    'title' => 'Clear logs',
    'description' => 'Clears all collected performance statistics.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'performance_clear_form',
    ),
    'access callback' => 'performance_clear_access',
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
  );
  $items['admin/reports/performance-logging'] = array(
    'title' => 'Performance logs',
    'description' => 'View summary performance logs: page generation times and memory usage.',
    'page callback' => 'performance_view_summary',
    'access arguments' => array(
      'administer performance logging',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/reports/performance-logging/summary'] = array(
    'title' => 'Summary',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
  $items['admin/reports/performance-logging/details'] = array(
    'title' => 'Details',
    'description' => 'View detailed, per page, performance logs: page generation times and memory usage.',
    'page callback' => 'performance_view_details',
    'access arguments' => array(
      'administer performance logging',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
  );
  return $items;
}

/**
 * Implementation of hook_permission().
 */
function performance_permission() {
  return array(
    'administer performance logging' => array(
      'title' => t('Administer performance logging'),
      'description' => t('Allows both configuring the performance module and accessing its reports.'),
    ),
  );
}

/**
 * Implementation of hook_cron().
 */
function performance_cron() {

  // One day ago.
  $timestamp = REQUEST_TIME - 24 * 60 * 60;
  $threshold = variable_get('performance_threshold_accesses', 0);
  foreach (performance_data_stores() as $store => $data) {
    if ($data['#enabled']) {
      call_user_func('performance_prune_' . $store, $timestamp, $threshold);
    }
  }
}

/**
 * Implementation of hook_views_api().
 */
function performance_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'performance') . '/includes',
  );
}

/**
 * Access callback for the clear logs page.
 */
function performance_clear_access() {
  $sum = array();
  $access = TRUE;
  foreach (performance_data_stores() as $store => $data) {
    $sum[] = $data['#enabled'];
  }
  $go = array_sum($sum);
  if (!$go) {
    $access = FALSE;
  }
  return $access;
}

/**
 * List of available data stores. For each store you must provide the following
 * functions:
 *   performance_enabled_store($form = FALSE, $message = FALSE)
 *   performance_log_summary_store($params)
 *   performance_get_data_store($timestamp = 0)
 *   performance_prune_store($timestamp = 0, $threshold = 0)
 *   performance_clear_store()
 * where the 'store' part is the machine name (key used in the array) of your
 * data store.
 */
function performance_data_stores() {

  // No drupal_static(). Don't want this to be reset.
  static $stores = array();
  if (!empty($stores)) {
    return $stores;
  }
  $function = 'performance_enabled_';

  // Array key is the 'machine readable' name, used in the functions.
  $stores['db'] = array(
    '#name' => t('Database'),
    // Human readable name.
    '#prod_use' => FALSE,
    // Whether this store is OK to be enabled on a production environment.
    '#enabled' => call_user_func($function . 'db'),
  );
  $stores['apc'] = array(
    '#name' => t('APC'),
    '#prod_use' => TRUE,
    '#enabled' => call_user_func($function . 'apc'),
  );
  $stores['memcache'] = array(
    '#name' => t('Memcache'),
    '#prod_use' => TRUE,
    '#enabled' => call_user_func($function . 'memcache'),
  );
  $stores['zend'] = array(
    '#name' => t('Zend Datacache'),
    '#prod_use' => TRUE,
    '#enabled' => call_user_func($function . 'zend'),
  );
  return $stores;
}

/**
 * System settings form.
 */
function performance_settings_form() {
  performance_caching_message();

  // Setup settings form.
  $form['mode'] = array(
    '#type' => 'fieldset',
    '#title' => t('Logging mode'),
    '#collapsible' => TRUE,
  );
  $form['mode']['performance_detail'] = array(
    '#type' => 'checkbox',
    '#title' => t('Detailed logging'),
    '#default_value' => variable_get('performance_detail', 0),
    '#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.'),
  );
  foreach (performance_data_stores() as $store => $data) {
    $disabled = TRUE;
    if (call_user_func('performance_enabled_' . $store, TRUE)) {
      $disabled = FALSE;
    }
    $form['mode']['performance_summary_' . $store] = array(
      '#type' => 'checkbox',
      '#title' => t('Summary logging (%data_store)', array(
        '%data_store' => $data['#name'],
      )),
      '#default_value' => variable_get('performance_summary_' . $store, 0),
      '#disabled' => $disabled,
      '#description' => t('Log summary data, such as average and maximum page generation times and memory usage.'),
    );
    if ($data['#prod_use']) {
      $form['mode']['performance_summary_' . $store]['#description'] .= ' ' . t('The summary will be stored in memory, 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.');
    }
    else {
      $form['mode']['performance_summary_' . $store]['#description'] .= ' ' . t('This logging mode is <strong>not</strong> suitable for most live sites.');
    }
  }
  $form['other'] = array(
    '#type' => 'fieldset',
    '#title' => t('Other'),
    '#collapsible' => TRUE,
  );
  $form['other'][PERFORMANCE_QUERY_VAR] = array(
    '#type' => 'checkbox',
    '#title' => t('Database Query timing and count'),
    '#default_value' => variable_get(PERFORMANCE_QUERY_VAR, 0),
    '#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.'),
  );
  $form['other']['performance_threshold_accesses'] = array(
    '#type' => 'select',
    '#title' => t('Accesses threshold'),
    '#default_value' => variable_get('performance_threshold_accesses', 0),
    '#options' => array(
      0,
      1,
      2,
      5,
      10,
    ),
    '#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."),
  );
  $form['other']['performance_nodrush'] = array(
    '#type' => 'checkbox',
    '#title' => t('Do not log drush access'),
    '#default_value' => variable_get('performance_nodrush', 1),
    '#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',
        ),
      )),
    )),
  );
  $form['other']['performance_skip_paths'] = array(
    '#type' => 'textarea',
    '#title' => t('Paths to exclude'),
    '#default_value' => variable_get('performance_skip_paths', ''),
    '#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>',
    )),
  );
  return system_settings_form($form);
}

/**
 * Display message on settings form.
 */
function performance_caching_message() {
  $caches = $available = array();
  $type = 'error';
  foreach (performance_data_stores() as $store => $data) {
    if (call_user_func('performance_enabled_' . $store, TRUE, TRUE) && $data['#prod_use']) {
      $caches[] = $data['#name'];
      $type = 'status';
    }
    else {
      if ($data['#prod_use']) {
        $available[] = $data['#name'];
      }
    }
  }
  if (count($caches) > 0) {
    $message = t('%stores enabled. It is reasonably safe to enable summary logging on live sites.', array(
      '%stores' => implode(' + ', $caches),
    ));
  }
  else {
    $message = t('None of the available data stores (%stores) are enabled. It is <strong>not</strong> safe to enable summary logging to the database on live sites!', array(
      '%stores' => implode(', ', $available),
    ));
  }
  drupal_set_message($message, $type, FALSE);
}

/**
 * Implementation of hook_boot().
 */
function performance_boot() {
  register_shutdown_function('performance_shutdown');
  if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {

    //TODO: See if devel.module has changed this ...
    @(include_once DRUPAL_ROOT . '/includes/database/log.inc');
    Database::startLog('performance', 'default');
  }
}

/**
 * Shutdown function that collects all performance data.
 */
function performance_shutdown() {

  // 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 certain paths defined by the user.
  if (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;
  $anon = !empty($data['anon']) ? 'Yes' : 'No';
  $header = array(
    'path' => $path,
    'timer' => $params['timer'],
    'anon' => $anon,
  );

  // TODO: what is this for? Find out and document it, or remove it.
  module_invoke_all('performance', 'header', $header);
  if (variable_get('performance_detail', 0)) {

    // TODO: what is this for? Find out and document it, or remove it.
    $data = module_invoke_all('performance', 'data');
    if (!empty($data[0])) {
      $params['data'] = $data[0];
    }
    else {
      $params['data'] = NULL;
    }
    performance_log_details($params);
  }
  else {

    // TODO: what is this for? Find out and document it, or remove it.
    module_invoke_all('performance', 'disable');
  }
  foreach (performance_data_stores() as $store => $data) {
    if ($data['#enabled']) {
      call_user_func('performance_log_summary_' . $store, $params);
    }
  }
}

/**
 * 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, $engine = '') {
  if (is_object($data)) {

    // Handle Memcache object.
    if (isset($data->data)) {
      $data = $data->data;
    }
    else {

      // Handle DB result row object.
      $data = (array) $data;
    }
  }
  if ($data) {
    $type = 'existing';
    $data = array(
      'last_access' => REQUEST_TIME,
      'bytes_max' => max($params['mem'], $data['bytes_max']),
      'bytes_avg' => performance_average($data['bytes_avg'], $params['mem'], $data['num_accesses']),
      'ms_max' => max($params['timer'], $data['ms_max']),
      'ms_avg' => performance_average($data['ms_avg'], $params['timer'], $data['num_accesses']),
      'query_timer_max' => max($params['query_timer'], $data['query_timer_max']),
      'query_timer_avg' => performance_average($data['query_timer_avg'], $params['query_timer'], $data['num_accesses']),
      'query_count_max' => max($params['query_count'], $data['query_count_max']),
      'query_count_avg' => performance_average($data['query_count_avg'], $params['query_count'], $data['num_accesses']),
      'num_accesses' => $data['num_accesses'] + 1,
      'path' => $data['path'],
    );

    // TODO: this is a quick fix. These lines broke DB summary logging!
    // See if zend really needs this!
    if ($engine == 'zend') {
      $data['updated'] = REQUEST_TIME;
    }
  }
  else {
    $type = 'new';
    $data = array(
      'last_access' => REQUEST_TIME,
      'bytes_max' => $params['mem'],
      'bytes_avg' => $params['mem'],
      'ms_max' => (int) $params['timer'],
      'ms_avg' => (int) $params['timer'],
      'query_timer_max' => $params['query_timer'],
      'query_timer_avg' => $params['query_timer'],
      'query_count_max' => (int) $params['query_count'],
      'query_count_avg' => (int) $params['query_count'],
      'num_accesses' => 1,
      'path' => $params['path'],
    );

    // TODO: this is a quick fix. These lines broke DB summary logging!
    // See if zend really needs this!
    if ($engine == 'zend') {
      $data['created'] = $data['updated'] = REQUEST_TIME;
    }
  }
  return array(
    'data' => $data,
    'type' => $type,
  );
}

/**
 * Calculate average.
 * Note that this will always lose accuracy. In the 2.0 version we will NOT
 * store the average anymore, but we will store the sum of all values and the
 * number of accesses. That way we calculate the average when displaying without
 * losing accuracy.
 */
function performance_average($original_avg, $value, $weight) {
  return ($original_avg * $weight + $value) / ($weight + 1);
}

// --- APC ---

/**
 * Helper function to check if APC is available.
 *
 * @see performance_data_stores()
 *
 * @param $form whether or not we're called from the settings form
 * @param $message whether or not to display an additional message
 * @return boolean
 */
function performance_enabled_apc($form = FALSE, $message = FALSE) {
  $setting = variable_get('performance_summary_apc', 0);
  if ($form) {
    $setting = TRUE;
  }
  if (function_exists('apc_cache_info') && !function_exists('zend_shm_cache_store') && $setting) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Helper function to store summary data in APC.
 *
 * @see performance_data_stores()
 */
function performance_log_summary_apc($params = array()) {
  $key = PERFORMANCE_KEY . $params['path'];
  $result = performance_build_summary_data(apc_fetch($key), $params);
  apc_store($key, $result['data']);
}

/**
 * Helper function to get all Performance logging APC keys.
 *
 * @param $timestamp unix timestamp to start fetching data from
 * @return array of APC keys
 */
function performance_apc_list_all($timestamp = 0) {
  $key_list = array();
  $list = apc_cache_info('user');
  if (!empty($list['cache_list'])) {
    foreach ($list['cache_list'] as $cache_id => $cache_data) {
      $regex = '/^' . PERFORMANCE_KEY . '/';

      // creation_time and mtime are always the same in our case.
      if (preg_match($regex, $cache_data['info']) && $cache_data['creation_time'] >= $timestamp) {
        $key_list[] = $cache_data['info'];
      }
    }
  }
  return $key_list;
}

/**
 * Helper function to get data from APC.
 *
 * @see performance_data_stores()
 *
 * @param $timestamp unix timestamp to start fetching data from
 * @return array of fetched data
 */
function performance_get_data_apc($timestamp = 0) {
  $data_list = array();
  $list = performance_apc_list_all($timestamp);
  foreach ($list as $key) {
    $data_list[] = apc_fetch($key);
  }
  return $data_list;
}

/**
 * Helper function to cleanup APC data.
 *
 * @see performance_data_stores()
 */
function performance_prune_apc($timestamp = 0, $threshold = 0) {

  // Get all entries in APC's user cache.
  $list = performance_apc_list_all();
  if (!count($list)) {

    // Nothing stored yet.
    return;
  }
  foreach ($list as $key) {
    if ($data = apc_fetch($key)) {
      if ($data['last_access'] <= $timestamp || $threshold && $data['num_accesses'] <= $threshold) {
        apc_delete($key);
      }
    }
  }
}

/**
 * Clear APC confirm form submit handler.
 */
function performance_clear_apc() {
  $list = performance_apc_list_all();
  if (!count($list)) {

    // Nothing stored yet
    return;
  }
  foreach ($list as $key) {
    if ($data = apc_fetch($key)) {
      apc_delete($key);
    }
  }
}

// --- Memcache ---

/**
 * Helper function to check if Memcache is available.
 *
 * @see performance_data_stores()
 *
 * @param $form whether or not we're called from the settings form
 * @param $message whether or not to display an additional message
 * @return boolean
 */
function performance_enabled_memcache($form = FALSE, $message = FALSE) {
  global $conf;
  $setting = variable_get('performance_summary_memcache', 0);
  if ($form) {
    $setting = TRUE;
  }
  if (function_exists('dmemcache_set') && $setting) {
    if (isset($conf['memcache_bins']['cache_performance'])) {
      return TRUE;
    }
    elseif ($message) {
      $message = t("Memcache detected, but no specific 'cache_performance' bin has been defined. See README.txt for configuration details.");
      drupal_set_message($message, 'warning', FALSE);
      return FALSE;
    }
  }
}

/**
 * Helper function to store summary data in Memcache.
 *
 * @see performance_data_stores()
 */
function performance_log_summary_memcache($params = array()) {
  $key = PERFORMANCE_KEY . $params['path'];
  $result = performance_build_summary_data(cache_get($key, PERFORMANCE_MEMCACHE_BIN), $params);
  if ($result['type'] == 'new') {

    // $keys_cache is used to easily retrieve our data later on.
    if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_MEMCACHE_BIN)) {
      $keys_values = $keys_cache->data;
    }
    $keys_values[$key] = 1;
    cache_set(PERFORMANCE_KEY, $keys_values, PERFORMANCE_MEMCACHE_BIN, CACHE_PERMANENT);
  }
  cache_set($key, $result['data'], PERFORMANCE_MEMCACHE_BIN, CACHE_PERMANENT);
}

/**
 * Helper function to get data from memcache.
 *
 * @see performance_data_stores()
 *
 * @param $timestamp unix timestamp to start fetching data from
 * @return array of fetched data
 */
function performance_get_data_memcache($timestamp = 0) {
  $data_list = array();
  if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_MEMCACHE_BIN)) {
    if ($keys_cache->data) {
      foreach ($keys_cache->data as $key => $v) {
        $cache = cache_get($key, PERFORMANCE_MEMCACHE_BIN);
        if ($cache->created >= $timestamp) {
          $data_list[] = $cache->data;
        }
      }
    }
  }
  return $data_list;
}

/**
 * Helper function to cleanup Memcache data.
 *
 * @see performance_data_stores()
 */
function performance_prune_memcache($timestamp = 0, $threshold = 0) {
  if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_MEMCACHE_BIN)) {
    if ($keys_cache->data) {
      foreach ($keys_cache->data as $key => $v) {
        $cache = cache_get($key, PERFORMANCE_MEMCACHE_BIN);
        if ($cache->created <= $timestamp || $threshold && $cache->data['num_accesses'] <= $threshold) {
          cache_clear_all($key, PERFORMANCE_MEMCACHE_BIN);
        }
      }
    }
  }
}

/**
 * Clear Memcache confirm form submit handler.
 */
function performance_clear_memcache() {

  // We have to iterate over all entries and delete them, reaching down
  // the API stack and calling dmemcache_delete directly.
  // This is suboptimal, but there is no other alternative.
  if ($keys_cache = cache_get(PERFORMANCE_KEY, PERFORMANCE_MEMCACHE_BIN)) {
    if ($keys_cache->data) {
      foreach ($keys_cache->data as $key => $v) {
        dmemcache_delete($key, PERFORMANCE_MEMCACHE_BIN);
      }
      dmemcache_delete(PERFORMANCE_KEY, PERFORMANCE_MEMCACHE_BIN);
    }
  }
}

// --- Zend Datacache ---
// Reference: http://files.zend.com/help/Zend-Server-IBMi/zend_data_cache_-_php_api.htm

/**
 * Helper function to check if Memcache is available.
 *
 * @see performance_data_stores()
 *
 * @param $form whether or not we're called from the settings form
 * @param $message whether or not to display an additional message
 * @return boolean
 */
function performance_enabled_zend($form = FALSE, $message = FALSE) {
  $setting = variable_get('performance_summary_zend', 0);
  if ($form) {
    $setting = TRUE;
  }
  if (function_exists('zend_shm_cache_store') && $setting) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Helper function to store summary data in Zend Datacache.
 *
 * @see performance_data_stores()
 */
function performance_log_summary_zend($params = array()) {
  $key = PERFORMANCE_ZEND_NAMESPACE . '::' . $params['path'];
  $result = performance_build_summary_data(zend_shm_cache_fetch($key), $params, 'zend');
  if ($result['type'] == 'new') {
    $keys_values = zend_shm_cache_fetch(PERFORMANCE_ZEND_KEYS);
    if ($keys_values === FALSE) {
      $keys_values = array();
    }
    $keys_values[$key] = 1;
    zend_shm_cache_store(PERFORMANCE_ZEND_KEYS, $keys_values, CACHE_PERMANENT);
  }
  zend_shm_cache_store($key, $result['data'], CACHE_PERMANENT);
}

/**
 * Helper function to get data from Zend Datacache.
 *
 * @see performance_data_stores()
 *
 * @param $timestamp unix timestamp to start fetching data from
 * @return array of fetched data
 */
function performance_get_data_zend($timestamp = 0) {
  $data_list = array();
  if ($keys_values = zend_shm_cache_fetch(PERFORMANCE_ZEND_KEYS)) {
    if ($keys_values !== FALSE) {
      foreach ($keys_values as $key => $v) {
        $cache = zend_shm_cache_fetch($key);
        if ($cache !== FALSE && $cache['created'] >= $timestamp) {
          $data_list[] = $cache;
        }
      }
    }
  }
  return $data_list;
}

/**
 * Helper function to cleanup Zend Datacache data.
 *
 * @see performance_data_stores()
 */
function performance_prune_zend($timestamp = 0) {
  if ($keys_values = zend_shm_cache_fetch(PERFORMANCE_ZEND_KEYS)) {
    if ($keys_values !== FALSE) {
      foreach ($keys_cache as $key => $v) {
        $cache = zend_shm_cache_fetch($key);
        if ($cache['created'] <= $timestamp) {
          zend_shm_cache_delete($key);
        }
      }
    }
  }
}

/**
 * Clear Zend Datacache confirm form submit handler.
 */
function performance_clear_zend() {
  zend_shm_cache_clear(PERFORMANCE_ZEND_NAMESPACE);
}

// --- Database ---

/**
 * Helper function to check if Detailed logging is enabled.
 *
 * @see performance_data_stores()
 *
 * @param $form whether or not we're called from the settings form
 * @param $message whether or not to display an additional message
 * @return boolean
 */
function performance_enabled_db($form = FALSE, $message = FALSE) {
  $setting = variable_get('performance_summary_db', 0);
  if ($form) {
    $setting = TRUE;
  }
  return $setting;
}

/**
 * Helper function to store summary data in database.
 *
 * @see performance_data_stores()
 */
function performance_log_summary_db($params = array()) {

  // SQL: SELECT * FROM {performance_summary} WHERE path = '%s'
  $row = db_select('performance_summary', 'p')
    ->fields('p')
    ->condition('path', $params['path'])
    ->execute()
    ->fetch();
  $result = performance_build_summary_data($row, $params);
  if ($result['type'] == 'existing') {

    // Do not add the path field!
    unset($result['data']['path']);

    // Update record based on path.
    db_update('performance_summary')
      ->condition('path', $params['path'])
      ->fields($result['data'])
      ->execute();
  }
  else {

    // First time we log this path, write fresh values
    try {
      db_insert('performance_summary')
        ->fields($result['data'])
        ->execute();
    } catch (Exception $e) {
      watchdog_exception('performance', $e);
    }
  }
}

/**
 * Wrapper function for consistency with the other data stores.
 *
 * @see performance_data_stores()
 * @see performance_gather_summary_data()
 */
function performance_get_data_db($timestamp = 0) {
  return performance_db_get_data(NULL, NULL, $timestamp);
}

/**
 * Helper function to get data from the database.
 *
 * @param header table header array
 * @param pager_height int num of rows per page
 * @param time unix timestamp to start fetching data
 * @return array of fetched data
 */
function performance_db_get_data($header, $pager_height, $timestamp = 0) {
  $data_list = array();

  // SQL: SELECT * FROM {performance_summary}
  $query = db_select('performance_summary', 'p')
    ->fields('p');
  if ($timestamp) {

    // SQL: WHERE last_access >= %d
    $query
      ->condition('last_access', $timestamp, '>=');
  }
  else {

    // Add pager and tablesort.
    $query
      ->extend('PagerDefault')
      ->limit($pager_height)
      ->extend('TableSort')
      ->orderByHeader($header);
  }

  // Run the query.
  $result = $query
    ->execute();
  foreach ($result as $row) {
    $data_list[] = $row;
  }
  return $data_list;
}

/**
 * Helper function to cleanup database data.
 *
 * @see performance_data_stores()
 */
function performance_prune_db($timestamp = 0) {

  // Remove rows which have not been accessed since a certain timestamp
  db_delete('performance_summary')
    ->condition('last_access', $timestamp, '<=')
    ->execute();

  // Remove performance_detail rows on a daily basis
  // TODO: check the comment with the SQL. Which one is wrong? Look back in the
  // code history...
  db_delete('performance_detail')
    ->condition('timestamp', $timestamp, '<=')
    ->execute();
}

/**
 * Clear database confirm form submit handler.
 */
function performance_clear_db() {

  // Can't use TRUNCATE as this permission is not neccessarily given.
  db_delete('performance_summary')
    ->execute();
  db_delete('performance_detail')
    ->execute();
}

/**
 * Helper function to store detailed data in database.
 */
function performance_log_details($params = array()) {
  global $user;
  $fields = array(
    'timestamp' => REQUEST_TIME,
    'bytes' => $params['mem'],
    'ms' => (int) $params['timer'],
    'query_count' => $params['query_count'],
    'query_timer' => (int) $params['query_timer'],
    'anon' => $user->uid ? 0 : 1,
    'path' => $params['path'],
    'data' => $params['data'],
  );
  try {
    db_insert('performance_detail')
      ->fields($fields)
      ->execute();
  } catch (Exception $e) {
    drupal_set_message($e
      ->getMessage(), 'error');
  }
}

/**
 * Summary page callback.
 */

// TODO: get tablesort to work for APC & Memcache (if that's even possible).
function performance_view_summary() {
  drupal_set_title(t('Performance logs: Summary'));
  global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;

  // array of element-keyed number of rows per page
  $go = FALSE;
  $rows = $data_list = array();
  $source = '';

  // Get selected source.
  if (isset($_GET['source'])) {
    $source = check_plain($_GET['source']);
  }
  else {

    // Get data from first active source.
    foreach (performance_data_stores() as $store => $data) {
      if ($data['#enabled']) {
        $source = $store;
        break;
      }
    }
  }

  // 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_avg',
    ),
    array(
      'data' => t('ms (Max)'),
      'field' => 'ms_max',
    ),
    array(
      'data' => t('ms (Avg)'),
      'field' => 'ms_avg',
    ),
  );
  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_avg',
    );
    $header[] = array(
      'data' => t('Query Count (Max)'),
      'field' => 'query_count_max',
    );
    $header[] = array(
      'data' => t('Query Count (Avg)'),
      'field' => 'query_count_avg',
    );
  }
  $pager_height = 50;

  // Fetch data. Exception here when logging to DB. Should get rid of this one
  // still. May not be possible though...
  if ($source == 'db') {
    $data_list = performance_db_get_data($header, $pager_height);
    $go = TRUE;
  }
  elseif (!empty($source)) {
    $data_list = call_user_func('performance_get_data_' . $source);
    $go = TRUE;
  }
  if (!$go) {
    return t('Summary performance log is not enabled. Go to the !link to enable it.', array(
      '!link' => l(t('settings page'), PERFORMANCE_SETTINGS),
    ));
  }
  $total_rows = $shown = $last_max = $total_bytes = $total_ms = $total_accesses = 0;
  $last_min = REQUEST_TIME;
  $threshold = variable_get('performance_threshold_accesses', 0);

  // TODO: make this work properly!
  // Set up pager since this is not done automatically when not using DB.
  if ($source != 'db' && $data_list) {
    $page = isset($_GET['page']) ? sprintf('%d', $_GET['page']) : 0;
    $pager_page_array = array(
      0 => $page,
    );
    $pager_total_items = array(
      0 => count($data_list),
    );
    $pager_limits = array(
      0 => $pager_height,
    );
    $pager_total = array(
      0 => ceil($pager_total_items[0] / $pager_limits[0]),
    );

    // Extract the data subset we need.
    $data_list = array_slice($data_list, $page * $pager_height, $pager_height);
  }

  // Format data into table.
  foreach ($data_list as $data) {

    // Cast to array because of the DB API now returning row objects by default.
    $data = is_object($data) ? (array) $data : $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_avg'];
    $total_ms += $data['ms_avg'];
    $total_accesses += $data['num_accesses'];
    $row_data = array();
    if ($data['num_accesses'] > $threshold) {
      $shown++;
      $row_data[] = l(check_plain($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_avg'] / 1024 / 1024, 2);
      $row_data[] = number_format($data['ms_max'], 1);
      $row_data[] = number_format($data['ms_avg'], 1);
      if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
        $row_data[] = number_format($data['query_timer_max'], 1);
        $row_data[] = number_format($data['query_timer_avg'], 1);
        $row_data[] = $data['query_count_max'];
        $row_data[] = $data['query_count_avg'];
      }
    }
    $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,
    )) . '<br/>';
  }
  else {
    $output .= t('Showing all !total paths.', array(
      '!total' => $total_rows,
    )) . '<br/>';
  }

  // 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,
  )) . '<br/>';
  $output .= t('Average duration per page: !ms_avg ms', array(
    '!ms_avg' => $ms_avg,
  )) . '<br/>';
  $output .= t('Total number of page accesses: !accesses', array(
    '!accesses' => $total_accesses,
  )) . '<br/>';
  $output .= t('First access: !access.', array(
    '!access' => format_date($last_min, 'small'),
  )) . '<br/>';
  $output .= t('Last access: !access.', array(
    '!access' => format_date($last_max, 'small'),
  ));
  $output .= performance_sources_switcher($source);

  // Return a renderable array.
  return array(
    'general_info' => array(
      '#prefix' => '<p>',
      '#markup' => $output,
      '#suffix' => '</p><p>&nbsp;</p>',
    ),
    '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,
    ),
  );
}

/**
 * Helper function to output sources switcher on summary page.
 * Made this function easily expandable to add additional souces. Just add one
 * to the sources array and you're done.
 *
 * @param source currently enabled source as string
 */
function performance_sources_switcher($source) {
  $sources = array();
  foreach (performance_data_stores() as $store => $data) {
    if ($data['#enabled']) {
      $sources[$store] = $data['#name'];
    }
  }
  $current = $sources[$source];
  unset($sources[$source]);

  // Build the switcher. Note that we do not keep the paging/filter settings as
  // this will probably just cause confusion.
  $switch = '';
  if (count($sources) >= 1) {
    $switch = array();
    foreach ($sources as $src => $txt) {
      $switch[] = l($txt, 'admin/reports/performance-logging', array(
        'query' => array(
          'source' => $src,
        ),
      ));
    }
    $switch = ' (switch to ' . implode(' | ', $switch) . ')';
  }
  return '<br/><strong>' . t('Data source') . ':</strong> ' . $current . $switch;
}

/**
 * Detail page callback.
 */

// TODO: get tablesort to work for APC & Memcache (if that's even possible).
function performance_view_details() {
  drupal_set_title(t('Performance logs: Details'));
  if (!variable_get('performance_detail', 0)) {
    return t('Detail performance log is not enabled. Go to the !link to enable it.', array(
      '!link' => l(t('settings page'), PERFORMANCE_SETTINGS),
    ));
  }
  $header = array(
    array(
      'data' => t('#'),
      'field' => 'pid',
      'sort' => 'desc',
    ),
    array(
      'data' => t('Date'),
      'field' => 'timestamp',
    ),
    array(
      'data' => t('Path'),
      'field' => 'path',
    ),
    array(
      'data' => t('Memory (MB)'),
      'field' => 'bytes',
    ),
    array(
      'data' => t('ms (Total)'),
      'field' => 'ms',
    ),
    array(
      'data' => t('Anonymous?'),
      'field' => 'anon',
    ),
  );
  if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
    $header[] = array(
      'data' => t('# Queries'),
      'field' => 'query_count',
    );
    $header[] = array(
      'data' => t('Query ms'),
      'field' => 'query_timer',
    );
  }
  $pager_height = 50;
  $result = db_select('performance_detail', 'p')
    ->fields('p')
    ->extend('PagerDefault')
    ->limit($pager_height)
    ->extend('TableSort')
    ->orderByHeader($header)
    ->execute();
  $rows = array();
  foreach ($result as $data) {
    $row_data = array();
    $row_data[] = $data->pid;
    $row_data[] = format_date($data->timestamp, 'small');
    $row_data[] = l(check_plain($data->path), $data->path);
    $row_data[] = number_format($data->bytes / 1024 / 1024, 2);
    $row_data[] = $data->ms;
    $row_data[] = $data->anon ? t('Yes') : t('No');
    if (variable_get(PERFORMANCE_QUERY_VAR, 0)) {
      $row_data[] = $data->query_count;
      $row_data[] = $data->query_timer;
    }
    $rows[] = array(
      'data' => $row_data,
    );
  }

  // Return a renderable array.
  return array(
    'query_data_detail' => array(
      '#theme' => 'table',
      '#header' => $header,
      '#rows' => $rows,
      '#sticky' => TRUE,
      '#empty' => t('No log messages available.'),
    ),
    'pager' => array(
      '#theme' => 'pager',
      '#quantity' => $pager_height,
    ),
  );
}

/**
 * Clear logs form. This is actually a multistep form: first choose the data
 * store you want to clear, second confirm that you actually want this.
 */
function performance_clear_form($form, &$form_state) {

  // Check if we are in step 2 of the form.
  if (isset($form_state['storage']['store']) && !empty($form_state['storage']['store'])) {
    return performance_clear_form_confirm($form, $form_state);
  }

  // Step one of the form.
  $form = $options = array();
  foreach (performance_data_stores() as $store => $data) {
    if ($data['#enabled']) {
      $options[$store] = $data['#name'];
    }
  }
  $form['store'] = array(
    '#type' => 'radios',
    '#title' => t('Select data store to clear'),
    '#options' => $options,
    '#required' => TRUE,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Clear'),
  );
  return $form;
}

/**
 * Clear logs form submit handler.
 */
function performance_clear_form_submit($form, &$form_state) {

  // Store the user's choice.
  $form_state['storage']['store'] = $form_state['values']['store'];

  // Make sure the confirm form is displayed.
  $form_state['rebuild'] = TRUE;
}

/**
 * Clear data store confirm form callback.
 */
function performance_clear_form_confirm($form, &$form_state) {
  $form = array();
  $stores = performance_data_stores();
  $store = $form_state['storage']['store'];

  // Needed because this form is called from another form, see
  // performance_clear_form().
  $form['#submit'] = array(
    'performance_clear_form_confirm_submit',
  );
  return confirm_form($form, t('Are you sure you want to clear the statistics collected in %store for this site?', array(
    '%store' => $stores[$store]['#name'],
  )), PERFORMANCE_SETTINGS, t('This will clear <strong>all</strong> the collected performance statistics stored in %store. This action cannot be undone.', array(
    '%store' => $stores[$store]['#name'],
  )), t('Clear'), t('Cancel'));
}

/**
 * Clear data store confirm form submit handler.
 */
function performance_clear_form_confirm_submit($form, &$form_state) {
  $stores = performance_data_stores();
  $store = $form_state['storage']['store'];
  call_user_func('performance_clear_' . $store);
  drupal_set_message(t('Performance statistics collected in %store have been cleared.', array(
    '%store' => $stores[$store]['#name'],
  )));
  $form_state['redirect'] = PERFORMANCE_SETTINGS;
}

/**
 * Gather performance data for external modules.
 */
function performance_gather_summary_data() {
  $go = FALSE;
  $data_list = array();

  // Data from last 15 minutes.
  $timestamp = REQUEST_TIME - 15 * 60;

  // Get data from first active store.
  foreach (performance_data_stores() as $store => $data) {
    if ($data['#enabled']) {
      $data_list = call_user_func('performance_get_data_' . $store, $timestamp);
      $go = TRUE;
      break;
    }
  }
  if (!$go) {
    return FALSE;
  }

  // Initialize variables.
  $total_rows = $total_bytes = $total_ms = $total_accesses = $total_query_time = $total_query_count = 0;
  foreach ($data_list as $data) {

    // Cast to array because of the DB API now returning row objects by default.
    $data = is_object($data) ? (array) $data : $data;
    $total_rows++;

    // Calculate running averages.
    $total_bytes += $data['bytes_avg'];
    $total_ms += $data['ms_avg'];
    $total_accesses += $data['num_accesses'];
    $total_query_time += $data['query_timer_avg'];
    $total_query_count += $data['query_count_avg'];
  }
  $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;
}

/**
 * Implementation of hook_nagios_info().
 */
function performance_nagios_info() {
  return array(
    'name' => 'Performance logging',
    'id' => 'PERF',
  );
}

/**
 * Implementation of 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'],
    ),
  );
}

/**
 * Implementation of 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',
        ),
      ),
    ),
  );
}

Functions

Namesort descending Description
performance_apc_list_all Helper function to get all Performance logging APC keys.
performance_average Calculate average. Note that this will always lose accuracy. In the 2.0 version we will NOT store the average anymore, but we will store the sum of all values and the number of accesses. That way we calculate the average when displaying without losing…
performance_boot Implementation of hook_boot().
performance_build_summary_data Helper function to build summary data array.
performance_caching_message Display message on settings form.
performance_clear_access Access callback for the clear logs page.
performance_clear_apc Clear APC confirm form submit handler.
performance_clear_db Clear database confirm form submit handler.
performance_clear_form Clear logs form. This is actually a multistep form: first choose the data store you want to clear, second confirm that you actually want this.
performance_clear_form_confirm Clear data store confirm form callback.
performance_clear_form_confirm_submit Clear data store confirm form submit handler.
performance_clear_form_submit Clear logs form submit handler.
performance_clear_memcache Clear Memcache confirm form submit handler.
performance_clear_zend Clear Zend Datacache confirm form submit handler.
performance_cron Implementation of hook_cron().
performance_data_stores List of available data stores. For each store you must provide the following functions: performance_enabled_store($form = FALSE, $message = FALSE) performance_log_summary_store($params) performance_get_data_store($timestamp =…
performance_db_get_data Helper function to get data from the database.
performance_enabled_apc Helper function to check if APC is available.
performance_enabled_db Helper function to check if Detailed logging is enabled.
performance_enabled_memcache Helper function to check if Memcache is available.
performance_enabled_zend Helper function to check if Memcache is available.
performance_gather_summary_data Gather performance data for external modules.
performance_get_data_apc Helper function to get data from APC.
performance_get_data_db Wrapper function for consistency with the other data stores.
performance_get_data_memcache Helper function to get data from memcache.
performance_get_data_zend Helper function to get data from Zend Datacache.
performance_log_details Helper function to store detailed data in database.
performance_log_summary_apc Helper function to store summary data in APC.
performance_log_summary_db Helper function to store summary data in database.
performance_log_summary_memcache Helper function to store summary data in Memcache.
performance_log_summary_zend Helper function to store summary data in Zend Datacache.
performance_menu Implementation of hook_menu().
performance_nagios Implementation of hook_nagios().
performance_nagios_info Implementation of hook_nagios_info().
performance_permission Implementation of hook_permission().
performance_prod_check_alter Implementation of hook_prod_check_alter().
performance_prod_check_return_data Return performance data to Production Monitor.
performance_prune_apc Helper function to cleanup APC data.
performance_prune_db Helper function to cleanup database data.
performance_prune_memcache Helper function to cleanup Memcache data.
performance_prune_zend Helper function to cleanup Zend Datacache data.
performance_settings_form System settings form.
performance_shutdown Shutdown function that collects all performance data.
performance_sources_switcher Helper function to output sources switcher on summary page. Made this function easily expandable to add additional souces. Just add one to the sources array and you're done.
performance_views_api Implementation of hook_views_api().
performance_view_details
performance_view_summary

Constants