? views_caching.patch Index: views.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/views.install,v retrieving revision 1.46 diff -u -p -r1.46 views.install --- views.install 7 Apr 2009 20:39:51 -0000 1.46 +++ views.install 23 May 2009 00:47:32 -0000 @@ -126,6 +126,11 @@ function views_schema_1() { ); $schema['cache_views'] = drupal_get_schema_unprocessed('system', 'cache'); + + $schema['cache_views_data'] = drupal_get_schema_unprocessed('system', 'cache'); + $schema['cache_views_data']['description'] = 'Cache table for views to store pre-rendered queries, results, and display output.'; + $schema['cache_views_data']['fields']['serialized']['default'] = 1; + $schema['views_object_cache'] = array( 'description' => 'A special cache used to store objects that are being edited; it serves to save state in an ordinarily stateless environment.', @@ -239,3 +244,18 @@ function views_update_6004() { return $ret; } + +/** + * Add the cache_views_data table to support standard caching. + */ +function views_update_6005() { + $ret = array(); + + $table = drupal_get_schema_unprocessed('system', 'cache'); + $table['description'] = 'Cache table for views to store pre-rendered queries, results, and display output.'; + $table['fields']['serialized']['default'] = 1; + + db_create_table($ret, 'cache_views_data', $table); + + return $ret; +} Index: views.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/views.module,v retrieving revision 1.332.2.1 diff -u -p -r1.332.2.1 views.module --- views.module 20 May 2009 03:09:36 -0000 1.332.2.1 +++ views.module 23 May 2009 00:47:32 -0000 @@ -397,7 +397,7 @@ function views_block($op = 'list', $delt * Implementation of hook_flush_caches(). */ function views_flush_caches() { - return array('cache_views'); + return array('cache_views', 'cache_views_data'); } /** Index: includes/plugins.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/plugins.inc,v retrieving revision 1.152.2.1 diff -u -p -r1.152.2.1 plugins.inc --- includes/plugins.inc 20 May 2009 03:03:38 -0000 1.152.2.1 +++ includes/plugins.inc 23 May 2009 00:47:32 -0000 @@ -247,6 +247,26 @@ function views_views_plugins() { 'handler' => 'views_plugin_query_default' ), ), + 'cache' => array( + 'parent' => array( + 'no ui' => TRUE, + 'handler' => 'views_plugin_cache', + 'parent' => '', + ), + 'none' => array( + 'title' => t('None'), + 'help' => t('No caching of Views data.'), + 'handler' => 'views_plugin_cache_none', + 'help topic' => 'cache-none', + ), + 'time' => array( + 'title' => t('Time-based'), + 'help' => t('Simple time-based caching of data.'), + 'handler' => 'views_plugin_cache_time', + 'uses options' => TRUE, + 'help topic' => 'cache-time', + ), + ), ); } @@ -256,7 +276,7 @@ function views_views_plugins() { * @return Nested array of plugins, grouped by type. */ function views_discover_plugins() { - $cache = array('display' => array(), 'style' => array(), 'row' => array(), 'argument default' => array(), 'argument validator' => array(), 'access' => array()); + $cache = array('display' => array(), 'style' => array(), 'row' => array(), 'argument default' => array(), 'argument validator' => array(), 'access' => array(), 'cache' => array()); // Get plugins from all mdoules. foreach (module_implements('views_plugins') as $module) { $function = $module . '_views_plugins'; Index: includes/view.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/view.inc,v retrieving revision 1.151.2.3 diff -u -p -r1.151.2.3 view.inc --- includes/view.inc 20 May 2009 03:09:36 -0000 1.151.2.3 +++ includes/view.inc 23 May 2009 00:47:32 -0000 @@ -685,7 +685,23 @@ class view extends views_db_object { $function($this); } - $this->query->execute($this); + // Check for already-cached results. + if ($this->preview) { + $cache = FALSE; + } + else { + $cache = $this->display_handler->get_cache_plugin(); + } + if ($cache && $cache->cache_get('results')) { + vpr('Used cached results'); + } + else { + $this->query->execute($this); + if ($cache) { + $cache->cache_set('results'); + } + } + $this->executed = TRUE; } @@ -693,9 +709,6 @@ class view extends views_db_object { * Render this view for display. */ function render($display_id = NULL) { - // Check for cached output. - // @todo: Implement this - $this->execute($display_id); // Check to see if the build failed. @@ -707,34 +720,50 @@ class view extends views_db_object { if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) { $this->start_query_capture(); } - - // Initialize the style plugin. - $this->init_style(); - - $this->style_plugin->pre_render($this->result); - - // Let modules modify the view just prior to executing it. - foreach (module_implements('views_pre_render') as $module) { - $function = $module . '_views_pre_render'; - $function($this); + + // Check for already-cached output. + if ($this->preview) { + $cache = FALSE; } - - // Give field handlers the opportunity to perform additional queries - // using the entire resultset prior to rendering. - if ($this->style_plugin->uses_fields()) { - foreach ($this->field as $id => $handler) { - if (!empty($this->field[$id])) { - $this->field[$id]->pre_render($this->result); + else { + $cache = $this->display_handler->get_cache_plugin(); + } + if ($cache && $cache->cache_get('output')) { + vpr('Used cached output'); + } + else { + // Initialize the style plugin. + $this->init_style(); + + $this->style_plugin->pre_render($this->result); + + // Let modules modify the view just prior to executing it. + foreach (module_implements('views_pre_render') as $module) { + $function = $module . '_views_pre_render'; + $function($this); + } + + // Give field handlers the opportunity to perform additional queries + // using the entire resultset prior to rendering. + if ($this->style_plugin->uses_fields()) { + foreach ($this->field as $id => $handler) { + if (!empty($this->field[$id])) { + $this->field[$id]->pre_render($this->result); + } } } + $this->display_handler->output = $this->display_handler->render(); + if ($cache) { + $cache->cache_set('output'); + } } - $output = $this->display_handler->render(); if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) { $this->end_query_capture(); } $this->render_time = views_microtime() - $start; - return $output; + + return $this->display_handler->output; } /** Index: plugins/views_plugin_cache.inc =================================================================== RCS file: plugins/views_plugin_cache.inc diff -N plugins/views_plugin_cache.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/views_plugin_cache.inc 23 May 2009 00:47:32 -0000 @@ -0,0 +1,148 @@ +view = &$view; + $this->display = &$display; + $this->options = array(); + + if (is_object($display->handler)) { + // Note: The below is read only. + $this->options = $display->handler->get_option('cache'); + } + } + + /** + * Retrieve the default options when this is a new access + * control plugin + */ + function option_defaults(&$options) { } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + function summary_title() { + return t('Unknown'); + } + + /** + * Save data to the cache. + */ + function cache_set($type, $data = NULL) { } + + /** + * Retrieve data from the cache. + */ + function cache_get($type) { + return FALSE; + } + + /** + * Clear out cached data for the view. + */ + function cache_flush() { } + + + + /** + * Set out-of-band-data for caching. Copied from Panels. + */ + function gather_headers() { + // Simple replacement for head + $this->head = str_replace($this->head, '', drupal_set_html_head()); + + // Slightly less simple for CSS: + $css = drupal_add_css(); + $start = $this->css; + $this->css = array(); + + foreach ($css as $media => $medias) { + foreach ($medias as $type => $types) { + foreach ($types as $path => $preprocess) { + if (!isset($start[$media][$type][$path])) { + $this->css[] = array($path, $type, $media, $preprocess); + } + } + } + } + + $js = array(); + // A little less simple for js + foreach (array('header', 'footer') as $scope) { + $js[$scope] = drupal_add_js(NULL, NULL, $scope); + } + + $start = $this->js; + $this->js = array(); + + foreach ($js as $scope => $scopes) { + foreach ($scopes as $type => $types) { + foreach ($types as $id => $info) { + if (!isset($start[$scope][$type][$id])) { + switch ($type) { + case 'setting': + $this->js[] = array($info, $type, $scope); + break; + + case 'inline': + $this->js[] = array($info['code'], $type, $scope, $info['defer']); + break; + + default: + $this->js[] = array($id, $type, $scope, $info['defer'], $info['cache']); + } + } + } + } + } + } + + /** + * Restore out of band data saved to cache. Copied from Panels. + */ + function restore_headers() { + if (!empty($this->head)) { + drupal_set_html_head($this->head); + } + if (!empty($this->css)) { + foreach ($this->css as $args) { + call_user_func_array('drupal_add_css', $args); + } + } + if (!empty($this->js)) { + foreach ($this->js as $args) { + call_user_func_array('drupal_add_js', $args); + } + } + } +} Index: plugins/views_plugin_cache_none.inc =================================================================== RCS file: plugins/views_plugin_cache_none.inc diff -N plugins/views_plugin_cache_none.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/views_plugin_cache_none.inc 23 May 2009 00:47:32 -0000 @@ -0,0 +1,11 @@ +view = &$view; + $this->display = &$display; + $this->options = array(); + + if (is_object($display->handler)) { + // Note: The below is read only. + $this->options = $display->handler->get_option('cache'); + } + } + + /** + * Retrieve the default options when this is a new access + * control plugin + */ + function option_defaults(&$options) { + $options['results_lifespan'] = 3600; + $options['output_lifespan'] = 3600; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + $options = array(60, 300, 1800, 3600, 21600, 518400); + $options = drupal_map_assoc($options, 'format_interval'); + $options = array(-1 => t('Never cache')) + $options; + + $form['results_lifespan'] = array( + '#type' => 'select', + '#title' => t('Query results'), + '#description' => t('The length of time raw query results should be cached.'), + '#options' => $options, + '#default_value' => $this->options['results_lifespan'], + ); + $form['output_lifespan'] = array( + '#type' => 'select', + '#title' => t('Rendered output'), + '#description' => t('The length of time rendered HTML output should be cached.'), + '#options' => $options, + '#default_value' => $this->options['output_lifespan'], + ); + } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + function summary_title() { + return format_interval($this->options['results_lifespan'], 1) .'/'. format_interval($this->options['output_lifespan'], 1); + } + + /** + * Save data to the cache. + */ + function cache_set($type) { + if ($lifespan = $this->options[$type .'_lifespan']) { + $cutoff = time() - $lifespan; + } + else { + return FALSE; + } + + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + break; + case 'results': + $data = array( + 'result' => $this->view->result, + 'total_rows' => $this->view->total_rows, + 'pager' => $this->view->pager, + ); + cache_set($this->_results_key(), $data, 'cache_views_data'); + break; + case 'output': + $this->gather_headers(); + $data = array( + 'head' => $this->head, + 'css' => $this->css, + 'js' => $this->js, + 'output' => $this->view->display_handler->output, + ); + cache_set($this->_output_key(), $data, 'cache_views_data'); + break; + } + } + + /** + * Retrieve data from the cache. + */ + function cache_get($type) { + if ($lifespan = $this->options[$type .'_lifespan']) { + $cutoff = time() - $lifespan; + } + else { + return FALSE; + } + + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + return FALSE; + case 'results': + // Values to set: $view->result, $view->total_rows, $view->execute_time, + // $view->pager['current_page']. + if ($cache = cache_get($this->_results_key(), 'cache_views_data')) { + if ($cache->created > $cutoff) { + $this->view->result = $cache->data['result']; + $this->view->total_rows = $cache->data['total_rows']; + $this->view->pager = $cache->data['pager']; + $this->view->execute_time = 0; + return TRUE; + } + } + return FALSE; + case 'output': + if ($cache = cache_get($this->_output_key(), 'cache_views_data')) { + if ($cache->created > $cutoff) { + $this->view->display_handler->output = $cache->data['output']; + $this->head = $cache->data['head']; + $this->css = $cache->data['css']; + $this->js = $cache->data['js']; + $this->restore_headers(); + return TRUE; + } + } + return FALSE; + } + } + + /** + * Clear out cached data for a view. + * + * We're jsut going to nuke anything related to the view, regardless of display, + * to be sure that we catch everything. Maybe that's a bad idea. + */ + function cache_flush() { + cache_clear_all($this->view->name .':', 'cache_views_data', TRUE); + } + + function _results_key() { + $key_data = array( + 'build_info' => $this->view->build_info, + 'exposed_info' => $this->view->exposed_info, + 'page' => $_GET['page'], + ); + return $this->view->name .':'. $this->display->id .':results:'. md5(serialize($key_data)); + } + + function _output_key() { + $key_data = array( + 'result' => $this->view->result, + ); + return $this->view->name .':'. $this->display->id .':output:'. md5(serialize($key_data)); + } +} Index: plugins/views_plugin_display.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/plugins/views_plugin_display.inc,v retrieving revision 1.20.2.2 diff -u -p -r1.20.2.2 views_plugin_display.inc --- plugins/views_plugin_display.inc 20 May 2009 03:09:37 -0000 1.20.2.2 +++ plugins/views_plugin_display.inc 23 May 2009 00:47:32 -0000 @@ -148,6 +148,7 @@ class views_plugin_display extends views function defaultable_sections($section = NULL) { $sections = array( 'access' => array('access'), + 'cache' => array('cache'), 'title' => array('title'), 'header' => array('header', 'header_format', 'header_empty'), 'footer' => array('footer', 'footer_format', 'footer_empty'), @@ -212,6 +213,7 @@ class views_plugin_display extends views 'defaults' => array( 'default' => array( 'access' => TRUE, + 'cache' => TRUE, 'title' => TRUE, 'header' => TRUE, 'header_format' => TRUE, @@ -270,6 +272,11 @@ class views_plugin_display extends views 'type' => array('default' => 'none'), ), ), + 'cache' => array( + 'contains' => array( + 'type' => array('default' => 'none'), + ), + ), 'title' => array( 'default' => '', 'translatable' => TRUE, @@ -482,6 +489,22 @@ class views_plugin_display extends views } /** + * Get the cache plugin + */ + function get_cache_plugin($name = NULL) { + if (!$name) { + $cache = $this->get_option('cache'); + $name = $cache['type']; + } + + $plugin = views_get_plugin('cache', $name); + if ($plugin) { + $plugin->init($this->view, $this->display); + return $plugin; + } + } + + /** * Get the handler object for a single handler. */ function &get_handler($type, $id) { @@ -687,6 +710,25 @@ class views_plugin_display extends views $options['access']['links']['access_options'] = t('Change settings for this access type.'); } + $cache_plugin = $this->get_cache_plugin(); + if (!$cache_plugin) { + // default to the no cache control plugin. + $cache_plugin = views_get_plugin('cache', 'none'); + } + + $cache_str = $cache_plugin->summary_title(); + + $options['cache'] = array( + 'category' => 'basic', + 'title' => t('Caching'), + 'value' => $cache_str, + 'desc' => t('Specify caching type for this display.'), + ); + + if (!empty($cache_plugin->definition['uses options'])) { + $options['cache']['links']['cache_options'] = t('Change settings for this caching type.'); + } + if ($this->uses_link_display()) { // Only show the 'link display' if there is more than one option. $count = 0; @@ -893,6 +935,47 @@ class views_plugin_display extends views $plugin->options_form($form['access_options'], $form_state); } break; + case 'cache': + $form['#title'] .= t('Caching'); + $form['cache'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#tree' => TRUE, + ); + + $cache = $this->get_option('cache'); + $form['cache']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('cache'), + '#default_value' => $cache['type'], + ); + + $cache_plugin = views_fetch_plugin_data('cache', $cache['type']); + if (!empty($cache_plugin['uses options'])) { + $form['markup'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#value' => t('You may also adjust the !settings for the currently selected style by clicking on the icon.', array('!settings' => $this->option_link(t('settings'), 'cache_options'))), + ); + } + break; + case 'cache_options': + $cache = $this->get_option('cache'); + $plugin = $this->get_cache_plugin(); + $form['#title'] .= t('Caching options'); + if ($plugin) { + $form['#help_topic'] = $plugin->definition['help topic']; + + $form['cache_options'] = array( + '#tree' => TRUE, + ); + $form['cache_options']['type'] = array( + '#type' => 'value', + '#value' => $cache['type'], + ); + $plugin->options_form($form['cache_options'], $form_state); + } + break; case 'header': $form['#title'] .= t('Header'); $form['header_empty'] = array( @@ -1269,6 +1352,12 @@ class views_plugin_display extends views $plugin->options_validate($form['access_options'], $form_state); } break; + case 'cache_options': + $plugin = $this->get_cache_plugin(); + if ($plugin) { + $plugin->options_validate($form['cache_options'], $form_state); + } + break; } } @@ -1277,6 +1366,12 @@ class views_plugin_display extends views * There is no need for this function to actually store the data. */ function options_submit(&$form, &$form_state) { + // Not sure I like this being here, but it seems (?) like a logical place. + $cache_plugin = $this->get_cache_plugin(); + if ($cache_plugin) { + $cache_plugin->cache_flush(); + } + $section = $form_state['section']; switch ($section) { case 'display_title': @@ -1304,6 +1399,28 @@ class views_plugin_display extends views $this->set_option('access', $form_state['values'][$section]); } break; + case 'cache': + $cache = $this->get_option('cache'); + if ($cache['type'] != $form_state['values']['cache']['type']) { + $plugin = views_get_plugin('cache', $form_state['values']['cache']['type']); + if ($plugin) { + $cache = array('type' => $form_state['values']['cache']['type']); + $plugin->option_defaults($cache); + $this->set_option('cache', $cache); + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('cache_options')); + } + } + } + + break; + case 'cache_options': + $plugin = views_get_plugin('cache', $form_state['values'][$section]['type']); + if ($plugin) { + $plugin->options_submit($form['cache_options'], $form_state); + $this->set_option('cache', $form_state['values'][$section]); + } + break; case 'title': case 'link_display': $this->set_option($section, $form_state['values'][$section]);