From 4b5f4f4ed319790fe7f0729560616b55c4e64715 Mon Sep 17 00:00:00 2001
From: chris <chris@jalakai.co.uk>
Date: Mon, 11 Sep 2006 04:14:18 +0200
Subject: [PATCH] parser caching update

This patch primarily updates p_cached_xhtml() and p_cached_instructions() to
allow their caching logic to be surrounded by an event trigger.

p_cached_xhtml() has been rewritten as the more general p_cached_output() to
support other render output formats besides 'xhtml'. All calls to
p_cached_xhtml() have been changed to refer to the new function.

New event:

name:        PARSER_CACHE_USE
data:        cache object (see below)
action:      determine if cache file can be used
preventable: yes
result:      bool, true to use cache file, false otherwise

Cache operations have been generalised in a new class, cache, extended to
cache_parser, cache_renderer & cache_instructions. Details can be found in
inc/cache.php

For handling of above event, key properties are:
- page, if present the wiki page id,
        may not always be present, e.g. when called for locale xhtml files
- file, source file
- mode, renderer mode (e.g. 'xhtml') or 'i' for instructions

Other changes:
- cache class counts cache hits against attempts, results are stored in
  {cache_dir}/cache_stats.txt
- adds metadata dependency to renderer page cache
- replaces purgefile dependency for renderer cache with metadata
  'relation references' (internal link) dependency for wiki pages only

darcs-hash:20060911021418-9b6ab-19601ed194b8c8e45236ab72c3e23d78bf777e6c.gz
---
 inc/cache.php                | 221 +++++++++++++++++++++++++++++++++++
 inc/fulltext.php             |   4 +-
 inc/parserutils.php          |  88 ++++++--------
 lib/plugins/base.php         |   2 +-
 lib/plugins/plugin/admin.php |   2 +-
 lib/plugins/syntax.php       |   2 +-
 6 files changed, 261 insertions(+), 58 deletions(-)
 create mode 100644 inc/cache.php

diff --git a/inc/cache.php b/inc/cache.php
new file mode 100644
index 000000000..19d431560
--- /dev/null
+++ b/inc/cache.php
@@ -0,0 +1,221 @@
+<?php
+/**
+ * Generic class to handle caching
+ *
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author     Chris Smith <chris@jalakai.co.uk>
+ */
+
+if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../').'/');
+
+require_once(DOKU_INC.'inc/io.php');
+require_once(DOKU_INC.'inc/pageutils.php');
+require_once(DOKU_INC.'inc/parserutils.php');
+
+class cache {
+  var $key = '';        // primary identifier for this item
+  var $ext = '';        // file ext for cache data, secondary identifier for this item
+  var $cache = '';      // cache file name
+
+  var $_event = '';      // event to be triggered during useCache
+
+  function cache($key,$ext) {
+    $this->key = $key;
+    $this->ext = $ext;
+    $this->cache = getCacheName($key,$ext);
+  }
+
+  /**
+   * public method to determine whether the cache can be used
+   *
+   * to assist in cetralisation of event triggering and calculation of cache statistics, 
+   * don't override this function override _useCache()
+   *
+   * @param  array   $depends   array of cache dependencies, support dependecies:
+   *                            'age'   => max age of the cache in seconds
+   *                            'files' => cache must be younger than mtime of each file
+   *
+   * @return bool    true if cache can be used, false otherwise
+   */
+  function useCache($depends=array()) {
+    $this->depends = $depends;
+
+    if ($this->_event) {
+      return $this->_stats(trigger_event($this->_event,$this,array($this,'_useCache')));
+    } else {
+      return $this->_stats($this->_useCache());
+    }
+  }
+
+  /*
+   * private method containing cache use decision logic
+   *
+   * this function can be overridden
+   *
+   * @return bool               see useCache()
+   */
+  function _useCache() {
+
+    if (isset($_REQUEST['purge'])) return false;                    // purge requested?
+    if (!($this->_time = @filemtime($this->cache))) return false;   // cache exists?
+
+    // cache too old?
+    if (!empty($this->depends['age']) && ((time() - $this->_time) > $this->depends['age'])) return false;
+
+    if (!empty($this->depends['files'])) {
+      foreach ($this->depends['files'] as $file) {
+        if ($this->_time < @filemtime($file)) return false;         // cache older than files it depends on?
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * retrieve the cached data
+   *
+   * @param   bool   $clean   true to clean line endings, false to leave line endings alone
+   * @return  string          cache contents
+   */
+  function retrieveCache($clean=true) {
+    return io_readFile($this->cache, $clean);
+  }
+
+  /**
+   * cache $data
+   *
+   * @param   string $data   the data to be cached
+   * @return  none
+   */
+  function storeCache($data) {
+    io_savefile($this->cache, $data);
+  }
+
+  /**
+   * remove any cached data associated with this cache instance
+   */
+  function removeCache() {
+    @unlink($this->cache);
+  }
+
+  /**
+   * record cache hits statistics
+   *
+   * @param    bool   $success   result of this cache use attempt
+   * @return   bool              pass-thru $success value
+   */
+  function _stats($success) {
+    global $conf;
+    static $stats = NULL;
+    static $file;
+
+    if (is_null($stats)) {
+      $file = $conf['cachedir'].'/cache_stats.txt';
+      $lines = explode("\n",io_readFile($file));
+
+      foreach ($lines as $line) {
+        $i = strpos($line,',');
+	$stats[substr($line,0,$i)] = $line;
+      }
+    }
+
+    if (isset($stats[$this->ext])) {
+      list($ext,$count,$successes) = explode(',',$stats[$this->ext]);
+    } else {
+      $ext = $this->ext;
+      $count = 0;
+      $successes = 0;
+    }
+
+    $count++;
+    $successes += $success ? 1 : 0;
+    $stats[$this->ext] = "$ext,$count,$successes";
+
+    io_saveFile($file,join("\n",$stats));
+
+    return $success;
+  }
+}
+
+class cache_parser extends cache {
+
+  var $file = '';       // source file for cache
+  var $mode = '';       // input mode (represents the processing the input file will undergo)
+
+  var $_event = 'PARSER_CACHE_USE';
+
+  function cache_parser($id, $file, $mode) {
+    if ($id) $this->page = $id;
+    $this->file = $file;
+    $this->mode = $mode;
+
+    parent::cache($file.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'],'.'.$mode);
+  }
+
+  function _useCache() {
+    global $conf;
+
+    if (!@file_exists($this->file)) return false;                   // source exists?
+
+    if (!isset($this->depends['age'])) $this->depends['age'] = $conf['cachetime'];
+
+    // parser cache file dependencies ...
+    $files = array($this->file,                                     // ... source
+                   DOKU_CONF.'dokuwiki.php',                        // ... config
+                   DOKU_CONF.'local.php',                           // ... local config
+                   DOKU_INC.'inc/parser/parser.php',                // ... parser
+                   DOKU_INC.'inc/parser/handler.php',               // ... handler
+             );
+
+    $this->depends['files'] = !empty($this->depends['files']) ? array_merge($files, $this->depends['files']) : $files;
+    return parent::_useCache($depends);
+  }
+
+}
+
+class cache_renderer extends cache_parser {
+
+  function _useCache() {
+    global $conf;
+
+    // renderer cache file dependencies ...
+    $files = array(
+#                   $conf['cachedir'].'/purgefile',                 // ... purgefile - time of last add
+                   DOKU_INC.'inc/parser/'.$this->mode.'.php',      // ... the renderer
+             );
+
+    if (isset($this->page)) { $files[] = metaFN($this->page,'.meta'); }
+
+    $this->depends['files'] = !empty($this->depends['files']) ? array_merge($files, $this->depends['files']) : $files;
+    if (!parent::_useCache($depends)) return false;
+
+    // for wiki pages, check for internal link status changes
+    if (isset($this->page)) {
+      $links = p_get_metadata($this->page,"relation references");
+
+      if (!empty($links)) {
+        foreach ($links as $id => $exists) {
+          if ($exists != @file_exists(wikiFN($id,'',false))) return false;
+	}
+      }
+    }
+
+    return true;
+  }
+}
+
+class cache_instructions extends cache_parser {
+
+  function cache_instructions($id, $file) {
+    parent::cache_parser($id, $file, 'i');
+  }
+
+  function retrieveCache() {
+    $contents = io_readFile($this->cache, false);
+    return !empty($contents) ? unserialize($contents) : array();
+  }
+
+  function storeCache($instructions) {
+    io_savefile($this->cache,serialize($instructions));
+  }
+}
diff --git a/inc/fulltext.php b/inc/fulltext.php
index 94cc947e7..4aeb622e2 100644
--- a/inc/fulltext.php
+++ b/inc/fulltext.php
@@ -119,7 +119,7 @@ function ft_backlinks($id){
     // check instructions for matching links
     foreach($docs as $match){
 /*
-        // orig code, examine each page's instruction list
+// orig code, examine each page's instruction list
         $instructions = p_cached_instructions(wikiFN($match),true);
         if(is_null($instructions)) continue;
 
@@ -137,7 +137,7 @@ function ft_backlinks($id){
             }
         }
 */
-// now with metadata
+// now with metadata (metadata relation reference links are already resolved)
         $links = p_get_metadata($match,"relation references");
         if (isset($links[$id])) $result[] = $match;
     }
diff --git a/inc/parserutils.php b/inc/parserutils.php
index f48580b0f..4b3c8c209 100644
--- a/inc/parserutils.php
+++ b/inc/parserutils.php
@@ -12,6 +12,7 @@
   require_once(DOKU_INC.'inc/confutils.php');
   require_once(DOKU_INC.'inc/pageutils.php');
   require_once(DOKU_INC.'inc/pluginutils.php');
+  require_once(DOKU_INC.'inc/cache.php');
 
 /**
  * Returns the parsed Wikitext in XHTML for the given id and revision.
@@ -38,7 +39,7 @@ function p_wiki_xhtml($id, $rev='', $excuse=true){
     }
   }else{
     if(@file_exists($file)){
-      $ret = p_cached_xhtml($file);
+      $ret = p_cached_output($file,'xhtml',$id);
     }elseif($excuse){
       $ret = p_locale_xhtml('newpage');
     }
@@ -114,48 +115,48 @@ function p_wiki_xhtml_summary($id, &$title, $rev='', $excuse=true){
  */
 function p_locale_xhtml($id){
   //fetch parsed locale
-  $html = p_cached_xhtml(localeFN($id));
+  $html = p_cached_output(localeFN($id));
   return $html;
 }
 
 /**
+ *     *** DEPRECATED ***
+ *
+ * use p_cached_output()
+ *
  * Returns the given file parsed to XHTML
  *
  * Uses and creates a cachefile
  *
+ * @deprecated
  * @author Andreas Gohr <andi@splitbrain.org>
  * @todo   rewrite to use mode instead of hardcoded XHTML
  */
 function p_cached_xhtml($file){
+  return p_cached_output($file);
+}
+
+/**
+ * Returns the given file parsed into the requested output format
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ */
+function p_cached_output($file, $format='xhtml', $id='') {
   global $conf;
-  $cache  = getCacheName($file.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'],'.xhtml');
-  $purge  = $conf['cachedir'].'/purgefile';
-
-  // check if cache can be used
-  $cachetime = @filemtime($cache); // 0 if not exists
-
-  if( @file_exists($file)                                             // does the source exist
-      && $cachetime > @filemtime($file)                               // cache is fresh
-      && ((time() - $cachetime) < $conf['cachetime'])                 // and is cachefile young enough
-      && !isset($_REQUEST['purge'])                                   // no purge param was set
-      && ($cachetime > @filemtime($purge))                            // and newer than the purgefile
-      && ($cachetime > @filemtime(DOKU_CONF.'dokuwiki.php'))      // newer than the config file
-      && ($cachetime > @filemtime(DOKU_CONF.'local.php'))         // newer than the local config file
-      && ($cachetime > @filemtime(DOKU_INC.'inc/parser/xhtml.php'))   // newer than the renderer
-      && ($cachetime > @filemtime(DOKU_INC.'inc/parser/parser.php'))  // newer than the parser
-      && ($cachetime > @filemtime(DOKU_INC.'inc/parser/handler.php')))// newer than the handler
-  {
-    //well then use the cache
-    $parsed = io_readfile($cache);
-    if($conf['allowdebug']) $parsed .= "\n<!-- cachefile $cache used -->\n";
-  }else{
-    $parsed = p_render('xhtml', p_cached_instructions($file),$info); //try to use cached instructions
 
-    if($info['cache']){
-      io_saveFile($cache,$parsed); //save cachefile
+  $cache = new cache_renderer($id, $file, $format);
+  if ($cache->useCache()) {
+    $parsed = $cache->retrieveCache();
+    if($conf['allowdebug']) $parsed .= "\n<!-- cachefile {$cache->cache} used -->\n";
+  } else {
+    $parsed = p_render($format, p_cached_instructions($file,false,$id), $info);
+
+    if ($info['cache']) {
+      $cache->storeCache($parsed);               //save cachefile
       if($conf['allowdebug']) $parsed .= "\n<!-- no cachefile used, but created -->\n";
     }else{
-      @unlink($cache); //try to delete cachefile
+      $cache->removeCache();                     //try to delete cachefile
       if($conf['allowdebug']) $parsed .= "\n<!-- no cachefile used, caching forbidden -->\n";
     }
   }
@@ -170,36 +171,17 @@ function p_cached_xhtml($file){
  *
  * @author Andreas Gohr <andi@splitbrain.org>
  */
-function p_cached_instructions($file,$cacheonly=false){
+function p_cached_instructions($file,$cacheonly=false,$id='') {
   global $conf;
-  $cache  = getCacheName($file.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'],'.i');
 
-  // check if cache can be used
-  $cachetime = @filemtime($cache); // 0 if not exists
-
-  // cache forced?
-  if($cacheonly){
-    if($cachetime){
-      return unserialize(io_readfile($cache,false));
-    }else{
-      return array();
-    }
-  }
+  $cache = new cache_instructions($id, $file);
 
-  if( @file_exists($file)                                             // does the source exist
-      && $cachetime > @filemtime($file)                               // cache is fresh
-      && !isset($_REQUEST['purge'])                                   // no purge param was set
-      && ($cachetime > @filemtime(DOKU_CONF.'dokuwiki.php'))      // newer than the config file
-      && ($cachetime > @filemtime(DOKU_CONF.'local.php'))         // newer than the local config file
-      && ($cachetime > @filemtime(DOKU_INC.'inc/parser/parser.php'))  // newer than the parser
-      && ($cachetime > @filemtime(DOKU_INC.'inc/parser/handler.php')))// newer than the handler
-  {
-    //well then use the cache
-    return unserialize(io_readfile($cache,false));
-  }elseif(@file_exists($file)){
+  if ($cacheonly || $cache->useCache()) {
+    return $cache->retrieveCache();
+  } else if (@file_exists($file)) {
     // no cache - do some work
     $ins = p_get_instructions(io_readfile($file));
-    io_savefile($cache,serialize($ins));
+    $cache->storeCache($ins);
     return $ins;
   }
 
@@ -313,7 +295,7 @@ function p_render_metadata($id, $orig){
   require_once DOKU_INC."inc/parser/metadata.php";
 
   // get instructions
-  $instructions = p_cached_instructions(wikiFN($id));
+  $instructions = p_cached_instructions(wikiFN($id),false,$id);
 
   // set up the renderer
   $renderer = & new Doku_Renderer_metadata();
diff --git a/lib/plugins/base.php b/lib/plugins/base.php
index f53c75444..a895166e6 100644
--- a/lib/plugins/base.php
+++ b/lib/plugins/base.php
@@ -69,7 +69,7 @@ class DokuWiki_Plugin {
    * @return  string  parsed contents of the wiki page in xhtml format
    */
   function locale_xhtml($id) {
-    return p_cached_xhtml($this->localFN($id));
+    return p_cached_output($this->localFN($id));
   }
   
   /**
diff --git a/lib/plugins/plugin/admin.php b/lib/plugins/plugin/admin.php
index 2c47de665..03efae3bf 100644
--- a/lib/plugins/plugin/admin.php
+++ b/lib/plugins/plugin/admin.php
@@ -275,7 +275,7 @@ class ap_manage {
           global $lang;
 
           // check the url
-                    $matches = array();
+          $matches = array();
           if (!preg_match("/[^\/]*$/", $url, $matches) || !$matches[0]) {
             $this->manager->error = $this->lang['error_badurl']."\n";
             return false;
diff --git a/lib/plugins/syntax.php b/lib/plugins/syntax.php
index 256bf7519..2a4d1e0ff 100644
--- a/lib/plugins/syntax.php
+++ b/lib/plugins/syntax.php
@@ -174,7 +174,7 @@ class DokuWiki_Syntax_Plugin extends Doku_Parser_Mode {
      * @return  string  parsed contents of the wiki page in xhtml format
      */
     function locale_xhtml($id) {
-      return p_cached_xhtml($this->localFN($id));
+      return p_cached_output($this->localFN($id));
     }
     
     /**
-- 
GitLab