PukiWiki bodycache改良版

2016/05/29Programming::PukiWiki

ECO-Wiki (acronia)で使っている改良したPukiWiki bodycacheの改良内容とソースコード(パッチ)。

PukiWiki bodycacheについて

PukiWikiのWiki記法→HTMLの変換結果をキャッシュすることで、負荷を削減するパッチ/プラグインです。

詳しいことは下記ページを参照してください。

PukiWiki bodycache改良版

※本記事内で特に断り無く「キャッシュ」と記載している場合、bodycacheが生成したキャッシュのことを指します。ブラウザが持っているキャッシュはブラウザキャッシュと記載。

bodycacheに対して以下の改良を実施した物です。

  • キャッシュの自動再生成機能
    • ページが更新されたとき、そのページを参照しているページのキャッシュも再生成。
    • メニューバーの編集は特別扱いで全キャッシュを削除。*1
  • 条件付きリクエスト If-Modified-Since 対応
    • キャッシュの生成日時を、httpレスポンスのLast-Modifiedヘッダで返却。*2
    • httpリクエスト時に、If-Modified-Sinceとキャッシュの生成日時を比較し、キャッシュが更新されていなければ 304 Not Modified を返却。
    • これによりブラウザキャッシュが参照され、サーバー側の負荷が下がります。
  • id重複対策
    • メニューバーに見出しを使ったときにidが重複する問題に対応しています。
    • ただし、かなり場当たり的な対応です。
  • ページのタイトル(<title>要素、<h1>要素)もキャッシュ

副作用と既知の問題

以下の副作用・既知の問題があります。

  • pcommentとincludeプラグインによるページ参照が、「関連ページ」として扱われるようになる
    • ?plugin=related の出力などに影響
  • メニューバーのidが、キャッシュが存在するときとしないときで異なる
  • newプラグインの表示はキャッシュが再生成されるまで更新されない
  • 304 Not Modified返却時はカウンタ(counterプラグイン)は動作しない
  • ページ更新時に「タイムスタンプを変更しない」を有効にするとキャッシュが更新されない

また、AutoLinkは使っていないので、一緒に使ったときうまく動くかは不明です。

PukiWiki bodycache改良版 注意事項

bodycache改良版はECO-Wiki (acronia)にも適用している機能ですが、ECO-Wiki (acronia)では他の改造も加えているため、本記事で公開するにあたってbodycache改良版に関連する部分だけ抜き出しています。そのため、その際の作業ミス等でうまく動作しないコードになっている可能性もあります。本番環境で運用する前に、十分テストしてください。

問題を見つけた場合は本記事のコメント等で連絡ください。

*1 : メニューバーはキャッシュの対象外なので、本来はキャッシュを作り直す必要はありません。しかし、If-Modified-Since対応にあたって、キャッシュの更新日時を新しくする必要が生じたため、この機能を追加しています。

*2 : 初回アクセス時などキャッシュ生成・更新を伴うアクセス時(State: generated)は、まだ有効なキャッシュが存在しないのでLast-Modifiedとして現在時刻を返却します。この場合は、次のアクセス時には304 Not Modifiedは返却されず、キャッシュから生成されたページ(State: cached)を改めて受信することになります。

*3 : 機能削除するのも面倒なので含めました^^;

PukiWiki bodycache改良版の適用

既に運用中のWikiに適用する場合、cacheディレクトリ内の、*.rel、*.refファイルを全て削除するか、?plugin=linksにアクセスしてページ間キャッシュを再生成してください。

patchコマンド

本記事の添付ファイルに含まれるbodycache.patchを用いて以下の通りpatchを当ててください。UTF-8版のPukiWiki 1.5.1用です。

$ ls
bodycache.patch  pukiwiki-1.5.1_utf8.zip
$ unzip pukiwiki-1.5.1_utf8.zip
$ cd pukiwiki-1.5.1_utf8
$ patch -p1 < ../bodycache.patch

手修正

patchコマンドがうまく使えない場合などのために、手修正による適用方法を記載していきます。

本記事の添付ファイルに含まれるbodycache.phpをlibディレクトリに格納し、以下のファイルを修正してください。

  • pukiwiki.ini.php
    • どこでも良いので以下の設定を追加します。
      // Bodycache feature
       
      // enable bodycache or not
      // default : true
      $enable_bodycache = true;
       
      // use bodycache as default. If it's false,
      // #bodycache(enable) is required per page.
      // default : true
      $enable_bodycache_default = true;
       
      // If these (block) plugins are contained in page, bodycache will be disabled.
      // Users can increase this plugin list to control bodycache.
      // default : array( 'ls2', 'recent', 'popular', 'menu' );
      $bodycache_disable_plugins = array( 'ls2', 'recent', 'popular', 'menu' );
      
      // Specify search depth to delete old caches when page is updated.
      // 0 means no deleting.
      // default : 2
      $bodycache_del_depth = 2;
      
      // Use last-modiefied time of $whatsnew (RecentChanges) for each wiki page.
      // If it's false, use only page cached time as wiki page last-modiefied.
      // default : false
      $bodycache_lastmod_whatsnew = false; //false or true
      
  • skin/pukiwiki.skin.php
    • 最後の方に以下の記述を追加
      • 変更前
        <div id="footer">
         Site admin: <a href="<?php echo $modifierlink ?>"><?php echo $modifier ?></a><p />
         <?php echo S_COPYRIGHT ?>.
         Powered by PHP <?php echo PHP_VERSION ?>. HTML convert time: <?php echo elapsedtime() ?> sec.
        </div>
        
      • 変更後
        <div id="footer">
         Site admin: <a href="<?php echo $modifierlink ?>"><?php echo $modifier ?></a><p />
         <?php echo S_COPYRIGHT ?>.
         Powered by PHP <?php echo PHP_VERSION ?>. HTML convert time: <?php echo elapsedtime() ?> sec.
         <br /><?php echo bodycache_signature_gen() ?>
        </div>
        
  • lib/convert_html.php
    • id重複対策の記述を追加します。(不要なら省略可能)
    • 最初の方にある以下の記述を修正。
      • 変更前
        function convert_html($lines)
        {
        	global $vars, $digest;
        	static $contents_id = 0;
        
      • 変更後
        function convert_html($lines)
        {
        	global $vars, $digest;
        	global $bodycache_status;
        	static $contents_id = 0;
        
        	if ( $bodycache_status === 'cached' ) {
        		$contents_id += 90 ;
        	}
        
  • lib/link.php
    • キャッシュ自動再生成のための記述を追加します。
      • この部分で実際にやっている処理は、キャッシュファイルの更新日時の過去(1970年1月1日)への書き換えです。こうすることで、次回アクセス時にキャッシュが古いと判断されキャッシュが再生成されます。
    • function links_updateの最初に以下の記述を追加。
      • 変更前
        function links_update($page)
        {
        
      • 変更後
        function links_update($page)
        {
        	global $whatsnew;
        	global $menubar;
        	global $bodycache_del_depth;
        
    • function links_updateの末尾~function links_initの直前に以下の記述を追加。
      • 変更前
        }
        
        // Init link cache (Called from link plugin)
        function links_init()
        {
        
      • 変更後
        
        	// bodycache対応
        	if ( $page === $menubar ) {
        		foreach (get_existfiles(CACHE_DIR, '.body') as $body_cache) {
        			unlink($body_cache);
        		}
        	} else {
        		links_bodycache_del($ref_file, $bodycache_del_depth);
        		pkwk_touch_file( get_cachename($whatsnew) , 0);
        	}
        }
        
        function links_bodycache_del($ref_file, $depth)
        {
        	$depth--;
        
        	if ( file_exists($ref_file) ) {
        		foreach (file($ref_file) as $line) {
        			list($ref_page) = explode("\t", rtrim($line));
        			$cachename = get_cachename($ref_page);
        			if ( file_exists($cachename) ) { // cache exists.
        				pkwk_touch_file($cachename, 0);
        			}
        			if( $depth > 0 ){
        				$ref_file_2nd = CACHE_DIR . encode($ref_page) . '.ref';
        				links_bodycache_del($ref_file_2nd, $depth);
        			}
        		}
        	}
        
        }
        
        // Init link cache (Called from link plugin)
        function links_init()
        {
        
  • lib/make_link.php
    • pcommentとincludeをリンクと同様にたどるための修正をします。
    • function InlineConverterを以下の通り修正。
      • 変更前
        	function InlineConverter($converters = NULL, $excludes = NULL)
        	{
        		if ($converters === NULL) {
        			$converters = array(
        				'plugin',        // Inline plugins
        				'note',          // Footnotes
        				'url',           // URLs
        				'url_interwiki', // URLs (interwiki definition)
        				'mailto',        // mailto: URL schemes
        				'interwikiname', // InterWikiNames
        				'autolink',      // AutoLinks
        				'bracketname',   // BracketNames
        				'wikiname',      // WikiNames
        				'autolink_a',    // AutoLinks(alphabet)
        			);
        		}
        
      • 変更後
        	function InlineConverter($converters = NULL, $excludes = array('pcomment','include'))
        	{
        		if ($converters === NULL) {
        			$converters = array(
        				'plugin',        // Inline plugins
        				'note',          // Footnotes
        				'url',           // URLs
        				'url_interwiki', // URLs (interwiki definition)
        				'mailto',        // mailto: URL schemes
        				'interwikiname', // InterWikiNames
        				'autolink',      // AutoLinks
        				'bracketname',   // BracketNames
        				'wikiname',      // WikiNames
        				'autolink_a',    // AutoLinks(alphabet)
        				'pcomment',      // pcomment
        				'include',       // include
        			);
        		}
        
    • function convertを以下の通り修正。
      • 変更前
        	function convert($string, $page)
        	{
        		$this->page   = $page;
        		$this->result = array();
        
        		$string = preg_replace_callback('/' . $this->pattern . '/x',
        			array(& $this, 'replace'), $string);
        
      • 変更後: '/x' を '/xm' にします
        	function convert($string, $page)
        	{
        		$this->page   = $page;
        		$this->result = array();
        
        		$string = preg_replace_callback('/' . $this->pattern . '/xm',
        			array(& $this, 'replace'), $string);
        
    • function get_objectsを以下の通り修正。
      • 変更前
        	function get_objects($string, $page)
        	{
        		$matches = $arr = array();
        		preg_match_all('/' . $this->pattern . '/x', $string, $matches, PREG_SET_ORDER);
        
      • 変更後: '/x' を '/xm' にします
        	function get_objects($string, $page)
        	{
        		$matches = $arr = array();
        		preg_match_all('/' . $this->pattern . '/xm', $string, $matches, PREG_SET_ORDER);
        
    • function make_pagelinkの直前に以下の記述を追加。
      • 変更前
        // Make hyperlink for the page
        function make_pagelink($page, $alias = '', $anchor = '', $refer = '', $isautolink = FALSE)
        {
        
      • 変更後
        // #pcomment
        class Link_pcomment extends Link
        {
        	var $anchor, $refer;
        
        	function Link_pcomment($start)
        	{
        		parent::Link($start);
        	}
        
        	function get_pattern()
        	{
        		global $WikiName, $BracketName;
        
        		return <<<EOD
        ^\#pcomment              # pcomment plugin
        (?:\(
        (                        # (1) PageName
         [^,\)]+
        )
        [,\)])?
        EOD;
        	}
        
        	function get_count()
        	{
        		return 1;
        	}
        
        	function set($arr, $page)
        	{
        		global $WikiName;
        
        		list(, $name) = $this->splice($arr);
        		if ($name == '') {
        			$name = 'コメント/' . $page;
        			if ( !is_page($name) ) {
        				$name = 'Comments/' . $page;
        			}
        		}
        		$name = get_fullname($name, $page);
        		if (! is_pagename($name)) return FALSE;
        
        		return parent::setParam($page, $name, '', 'pagename' , '');
        	}
        
        	function toString()
        	{
        		return '';
        	}
        }
        
        // #include
        class Link_include extends Link
        {
        	var $anchor, $refer;
        
        	function Link_include($start)
        	{
        		parent::Link($start);
        	}
        
        	function get_pattern()
        	{
        		global $WikiName, $BracketName;
        
        		return <<<EOD
        ^\#include               # include plugin
        (?:\(
        (                        # (1) PageName
         [^,\)]+
        )
        [,\)])
        EOD;
        	}
        
        	function get_count()
        	{
        		return 1;
        	}
        
        	function set($arr, $page)
        	{
        		global $WikiName;
        
        		list(, $name) = $this->splice($arr);
        		$name = get_fullname($name, $page);
        		if (! is_pagename($name)) return FALSE;
        
        		return parent::setParam($page, $name, '', 'pagename' , '');
        	}
        
        	function toString()
        	{
        		return '';
        	}
        }
        
        // Make hyperlink for the page
        function make_pagelink($page, $alias = '', $anchor = '', $refer = '', $isautolink = FALSE)
        {
        
  • lib/pukiwiki.php
    • 最初の方でbodycache.phpを読み込みます。
      • 変更前
        require(LIB_DIR . 'func.php');
        require(LIB_DIR . 'file.php');
        require(LIB_DIR . 'plugin.php');
        require(LIB_DIR . 'html.php');
        require(LIB_DIR . 'backup.php');
        
      • 変更後
        require(LIB_DIR . 'func.php');
        require(LIB_DIR . 'file.php');
        require(LIB_DIR . 'plugin.php');
        require(LIB_DIR . 'html.php');
        require(LIB_DIR . 'backup.php');
        require(LIB_DIR . 'bodycache.php');
        
    • 最後の方にbodycacheを呼び出す記述を追加します。
      • 変更前
        	$vars['cmd']  = 'read';
        	$vars['page'] = & $base;
        
        	$body  = convert_html(get_source($base));
        }
        
      • 変更後
        	$vars['cmd']  = 'read';
        	$vars['page'] = & $base;
        
        	global $enable_bodycache;
        	if ( $enable_bodycache ) {
        		$body  = render_body( $base );
        	} else {
        		$body  = convert_html(get_source($base));
        	}
        }
        
  • plugin/read.inc.php
    • If-Modified-Since対応を追加します。(不要なら省略可能)
    • function plugin_read_action内を、以下のように修正。
      • 変更前
        	if (is_page($page)) {
        		// ページを表示
        		check_readable($page, true, true);
        		header_lastmod($page);
        		return array('msg'=>'', 'body'=>'');
        
        	} else if (! PKWK_SAFE_MODE && is_interwiki($page)) {
        
      • 変更後
        	if (is_page($page)) {
        		// ページを表示
        		check_readable($page, true, true);
        		
        		$ims = get_if_modified_since();
        		if ( $ims ) {
        			$ctm = get_lastmodtime($page);
        			if ( $ctm && $ctm <= $ims) {
        				// 更新されていない
        				header('HTTP/1.1 304 Not Modified') ;
        				header_lastmod_cache($page);
        				exit;
        			}
        		}
        
        		//header_lastmod($page);
        		header_lastmod_cache($page);
        		return array('msg'=>'', 'body'=>'');
        
        	} else if (! PKWK_SAFE_MODE && is_interwiki($page)) {
        
    • ファイル末尾に、以下の内容を追記。
      • 変更前
        }
        ?>
        
      • 変更後
        }
        
        function get_if_modified_since()
        {
        	static $unixtime = null ;
        	if ($unixtime !== null) {
        		return $unixtime ;
        	}
        
        	if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
        		$unixtime = false ;
        		return false ; // If-Modified-Sinceなし
        	}
        	
        	$unixtime = strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
        	return $unixtime ;
        }
        ?>
        

bodycache関連の設定

キャッシュ自動再生成時の参照階層数の変更

$bodycache_del_depth の値を変更してください。

  • pukiwiki.ini.php
    // Specify search depth to delete old caches when page is updated.
    // 0 means no deleting.
    // default : 2
    $bodycache_del_depth = 2;
    

多重にinclude/pcommentを使っている場合は、$bodycache_del_depth を増やさないとキャッシュの再生成漏れが起こります。しかし、あまり大きな値を設定すると、編集時の負荷が増える恐れがあります。

そもそも、includeを多重に使うことを禁止したい場合は、PukiWiki 多重includeの深さ制限を試してみてください。

RecentChangesを考慮したキャッシュ制御

メニューバーで #recent プラグインを使っている場合など、RecentChangesページの更新時にブラウザキャッシュを更新されると都合が悪い場合は、$bodycache_lastmod_whatsnewをtrueに設定してください。

  • pukiwiki.ini.php
    // Use last-modiefied time of $whatsnew (RecentChanges) for each wiki page.
    // If it's false, use only page cached time as wiki page last-modiefied.
    // default : false
    $bodycache_lastmod_whatsnew = true; //false or true
    

この設定により、キャッシュ更新日時とRecentChanges更新日時の内、新しい方を更新日時として扱うようになります。

タイムスタンプ変更の強制

Wikiページ更新時の「タイムスタンプを変更しない」は、更新日時を判定条件にするbodycacheとの相性が良くないので、$notimeupdateの設定は0にすることを推奨します。

  • pukiwiki.ini.php
    /////////////////////////////////////////////////
    // Allow to use 'Do not change timestamp' checkbox
    // (0:Disable, 1:For everyone,  2:Only for the administrator)
    $notimeupdate = 0;
    

WikiNameを使用しない

WikiNameによる自動リンクが生成されるとその分負荷も増えるので、WikiNameが不要であればオフにすることを推奨します。

  • pukiwiki.ini.php
    /////////////////////////////////////////////////
    // _Disable_ WikiName auto-linking
    $nowikiname = 1;
    

おまけ: reload.inc.php

PukiWikiのpluginディレクトリにreload.inc.phpを格納し、「?cmd=reload&page=ページ名」にアクセスすると、指定したページのキャッシュが再生成されます。

ECO-Wiki (acronia)では、「リロード」リンクでこのプラグインを使っています。

このプラグインへのリンクを張る場合、検索エンジン等にクロールされて負荷が高くなる恐れがあることに注意してください。

更新履歴

  • 2016/09/11
    • ソースコード更新
      • RecentChangesページの更新日時を考慮する機能を追加(デフォルトオフ)。
      • ユーザーの変更を意図した変数をbodycache.phpからpukiwiki.ini.phpに移動。
      • 更新されたファイル
        lib/bodycache.php
        plugin/read.inc.php
        pukiwiki.ini.php
        
    • ページ更新時の「タイムスタンプを変更しない」について注意を追加。
    • pukiwiki.ini.php設定に関する説明を追加。
  • 2016/06/25
  • 2016/06/14
    • ソースコード更新
      • typoを修正
      • bodycacheのシグネチャを更新(この記事へのリンクにした)
    • 副作用に「304 Not Modified返却時はカウンタ(counterプラグイン)は動作しない」を追加

添付ファイル

  • 2016/09/11版: bodycache_20160911.zip
    • bodycache.patch: patchコマンド用
    • bodycache.php: bodycache本体
      • bodycache.patchにはbodycache.phpも含まれているので、patchコマンドでパッチを当てるなら不要
    • reload.inc.php: おまけ

ライセンスはGPLv2 or laterです。(PukiWikiや元のbodycacheと同じ。)