a-blog cms のベンチマークモードの活用法


a-blog cms の「ベンチマークモード」ってご存知でしょうか?

今回、あるページの表示に 5 秒程度要するケースを確認 しました。この事例を受け、「同様の実装には注意が必要である」という点について、社内向けに Slack で情報共有を書いている途中で、これは a-blog cms をご利用いただいている皆様にも広くお伝えすべき内容ではないかと考え、「ベンチマークモード」について改めて整理し、ブログ記事として公開することにいたしました。


何故、こんなに時間がかかるのか!

原因は、いわゆる「N+1問題」が発生していたためでした。具体的には、Entry_Summary を実行した結果、一覧に表示される件数分だけ追加で Entry_Summary が実行されていたのです。

a-blog cms では基本機能として「モジュールの入れ子」(モジュール内に別のモジュールを配置する)が可能ですが、通常は「内側のモジュール → 外側のモジュール」の順に実行されます。この場合、Entry_Summary は 2 回のみ実行されます。しかし、「モジュールのエスケープ処理」などのテクニックを活用することで、「外側のモジュール → 内側のモジュール」の順に実行させることも可能です。

今回のケースでは、この後者の順序で実装していたため、一覧表示時に「外側のモジュール 1 回 + 表示件数 N 回」、結果として 1+N 回 Entry_Summary が実行される状態となっていました。

以下のコードが問題のものを部分的に抜き出したものです。

<!-- BEGIN_MODULE Entry_Summary id="sample" -->
(略)
 <!-- BEGIN entry:loop -->
  <p><!-- BEGIN_MODULE\ Entry_Summary id="sample2" 
  ctx="eid/{eid}" --><!-- BEGIN\ entry:loop -->\{title\}
  <!-- END\ entry:loop --><!-- END_MODULE\ Entry_Summary --></p>
 <!-- END entry:loop -->
(略)
<!-- END_MODULE Entry_Summary -->

ベンチマークモード で分かること

実際のベンチマークモードがオンの状態にすると、以下のような表がページの下部に表示されるようになります。実際には表は多くの情報が表示され縦に長く表示されます。


各表について説明をしておきます。

グローバル変数

そのページを表示する際に、テンプレートで利用できるグローバル変数の一覧になります。上に表示されているキャプチャ画像では、あまり参考になる情報が見えていないのですが、%{BID} や %{BLOG_NAME} のような変数の値が表示されています。

モジュール処理時間

ここには、テンプレートに書かれている モジュール名と、その名前である モジュールID 、実際の実行時間に加え SQL を実行した回数が書かれています。ここではモジュールの実行時間は 0.001秒のような小さな数値であるのが正常で、0.2, 0.3秒のような時間がかかっているのは少し何か設定を見直した方がいい事がわかります。

表の下部には、実際のトータル時間が書かれています。1つのモジュールの実行時間が少なくても、沢山のモジュールの合計になると、結果 5秒になっている例となります。



テンプレート

@include文で他のテンプレートの読み込んでいるファイルが一覧で表示されます。 a-blog cms の実装作法的にはモジュール毎にテンプレートを分割しているので多くのファイルが読み込まれている事が確認できます。この数が多いと遅くなるのでは?と心配される人がいるかもしれませんが、あまり気にする必要も無さそうです。

メモリー

3行書かれていますが、PHPの設定で許可されているメモリーの上限「memory_limit」と、スクリプト実行中に使用された最大メモリ量である「memory_get_peak_usage」をご確認ください。

Query Count

モジュール処理時間の中にも書かれていますが、SQL の実行回数になります。1ページを表示するのに 5000回以上というのは明らかに問題がある状態です。

ベンチマークモードを利用する方法

以前のバージョンでは、config.server.php の一番下の行にファイルで設定を行う必要がありましたが、Ver. 3.0 以降はログインユーザー毎に設定が可能となっており、気軽に試すことができますので、一度設定を切り替えてお試しください。

// 本番運用時は DEBUG_MODE を必ず 0 に設定して下さい
define('DEBUG_MODE', 0);
define('BENCHMARK_MODE', 0);


最後に

a-blog cms は、バックエンド側で PHP を記述することなく、柔軟に CMS の構築・実装を進められる仕組みを備えています。例えば、Entry_Summary の表示設定画面で必要な項目にチェックを入れるだけで、テンプレート上に求める情報を表示させることが可能です。

しかし、その一方で「タグを表示する」にチェックを入れればタグ情報を取得するための検索処理が追加され、「ページャーを表示する」にチェックを入れれば全件数を取得するための検索処理が実行されるなど、こうした設定が増えるほど SQL の実行回数にも影響を与えます。

そうした状況を適切に把握するために役立つのが 「ベンチマークモード」 です。この機能を活用することで、「モジュールが何回実行されているのか」「そのモジュールの実行時間に異常がないか」など、パフォーマンスに関わる詳細な情報を確認できます。

CMS のカスタマイズは「必要な情報を表示させること」に意識が向きがちですが、安定した運用や快適な閲覧環境を提供するためには、パフォーマンス面も考慮した実装が欠かせません。a-blog cms は、そうしたプロフェッショナルな開発現場にも耐えうる仕組みを備えた CMS です。ぜひ、「ベンチマークモード」も活用しながら、より効率的でパフォーマンスに優れたサイト構築を目指していただければ幸いです。

Entry_Summaryループ内クラス設定を運営者でも触れるようにする


現在提供中の標準テーマの一覧ページでは、横に並ぶコンテンツの数(2列、3列、4列)は、テンプレートの CSS クラスの設定によって決まる仕様になっています。 この記事では、タイトルの通り、運営側でもこの設定を変更できるようにする方法について考えてみます。


問題点の検証

実際にどのような問題が起こるのかを UTSUWA のテーマ「事業紹介」を例に説明していきます。

納品直後の状態

当初、打ち合わせ時には「事業紹介」については増えないと聞いており、コンテンツとしては 2つなので、col-6 と Class を設定し、横に2枚並ぶようにテンプレートに書かれていました。



事業が追加される事に

これがサイトを運営している途中で、新しい事業が増えて 3つになる事になりました。3つ目のコンテンツは下に表示され隣は空白となります。



3つ並べたいと相談される

同じページ内の「ブログ」コンテンツは横に4枚並べて表示していますし、横に3枚並べた方がいいと思う人も多い事でしょう。



CMS で運用している中で、よくある話ではないかと思います。 詳細ページについては、ある程度レイアウト調整もできるのですが、沢山の情報を管理可能な CMS では一覧ページについて自由度が低くなる傾向があります。

対策を考えてみる

対策としては、その制御を行っているのはテンプレート上に書かれている 1つの Class という事になるので、それを管理画面側から変更できるようにする事になります。

実は準備済みでした

実は Ver. 2.5.1(2015年8月) から、モジュールの設定に ループ内クラス という設定が追加されています。これを活用する事で、管理画面上から変更できるようになります。 テンプレートに直接書かれている Class を {entry:loop.class} という変数に置き換えるだけです。



運用担当者に分かるのか?

次の問題は、この記述内容が運用担当者に理解できるのかというところになります。

acms-col-6 acms-col-sm-4 acms-col-md-3

標準テーマの一覧ページでは、コンテンツの横幅を指定するために acms-col-* というクラスを使用します。 このクラスの数字は 12分割のグリッドシステム に基づいており、1行を12としたときに、各コンテンツが何列分の幅を占めるかを決定します。

具体的な例

  • acms-col-6 → 12列中6列分を使用(2列表示)
  • acms-col-sm-4 → 画面幅が smサイズ以上のとき、12列中4列分を使用(3列表示)
  • acms-col-md-3 → 画面幅が mdサイズ以上のとき、12列中3列分を使用(4列表示)

つまり、画面のサイズによって自動的に 2列 → 3列 → 4列 とレイアウトが変化します。



クラス名 デフォルトのブレイクポイント
acms-col- 全て
acms-col-sm- 480px以上〜
acms-col-md- 768px以上〜
acms-col-lg- 1024px以上〜
acms-col-xl- 1440px以上〜

この設定では、制作者には理解できたとしても、サイトを運用している担当者には難しいと感じるのではないでしょうか。

実際の実装

モジュールフィールドに、各画面の幅の時のカラム数のカスタムフィールドを作る事で実装が可能になります。



さらに工夫を

カスタム設定で設定した SELECTcol-base , col-sm , col-md )を元に、表示設定の「ループ内クラス」{entry:loop.class} を自動で書くことができる JavaScript を用意しました。こうすることでテンプレートは標準的な記述のままで済ますことができ、独自に設定したい時にも対応が可能となります。

モジュールフィールドの設定

<select name="col-base" class="acms-admin-form-width-mini">
  <option value="acms-col-12" {col-base:selected#acms-col-12}>1カラム</option>
  <option value="acms-col-6" {col-base:selected#acms-col-6}>2カラム</option>
  <option value="acms-col-4" {col-base:selected#acms-col-4}>3カラム</option>
  <option value="acms-col-3" {col-base:selected#acms-col-3}>4カラム</option>
</select>
<input type="hidden" name="field[]" value="col-base" />

<select name="col-sm" class="acms-admin-form-width-mini">
  <option value="" {col-sm:selected#}>未設定 (480px以上)</option>
  <option value="acms-col-sm-12" {col-sm:selected#acms-col-sm-12}>1カラム</option>
  <option value="acms-col-sm-6" {col-sm:selected#acms-col-sm-6}>2カラム</option>
  <option value="acms-col-sm-4" {col-sm:selected#acms-col-sm-4}>3カラム</option>
  <option value="acms-col-sm-3" {col-sm:selected#acms-col-sm-3}>4カラム</option>
</select>
<input type="hidden" name="field[]" value="col-sm" />

<select name="col-md" class="acms-admin-form-width-mini">
  <option value="" {col-md:selected#}>未設定 (768px以上)</option>
  <option value="acms-col-md-12" {col-md:selected#acms-col-md-12}>1カラム</option>
  <option value="acms-col-md-6" {col-md:selected#acms-col-md-6}>2カラム</option>
  <option value="acms-col-md-4" {col-md:selected#acms-col-md-4}>3カラム</option>
  <option value="acms-col-md-3" {col-md:selected#acms-col-md-3}>4カラム</option>
</select>
<input type="hidden" name="field[]" value="col-md" />

<span id="col-etc"></span>

今回の JavaScript

最初に INPUT name="entry_summary_loop_class" の値からカスタム設定の SELECT を調整します。その後、SELECT を修正すると INPUT のテキストを書き換え、INPUT 側をダイレクトに修正すると SELECT 側を書き換えるようにしています。

SELECT の OPTION に無い acms-col-2 などが INPUT に書かれた際には、自動的に SELECT に追加し、acms-col- , acms-col-sm- , acms-col-md- でない Class を書いた時には <span id="col-etc"></span> に表示するようにして追加で Class を書いていることも分かるようにしておきました。

<script>
ACMS.addListener("acmsReady", function() {

  const loopClass = document.querySelector('input[name="entry_summary_loop_class"]');

  const colBase = document.querySelector('select[name="col-base"]');
  const colSm = document.querySelector('select[name="col-sm"]');
  const colMd = document.querySelector('select[name="col-md"]');
  const colEtc = document.getElementById("col-etc");

  function updateLoopClass() {
    const values = [colBase.value, colSm.value, colMd.value].filter(value => value !== "");
    loopClass.value = values.join(" ");
  }

  function ensureOptionExists(select, value) {
    if (!value || select.querySelector(`option[value="${value}"]`)) return;
    const existingValues = new Set(Array.from(select.options).map(opt => opt.value));
    if (!existingValues.has(value)) {
      const option = document.createElement("option");
      option.value = value;
      option.textContent = value;
      option.setAttribute("data-custom", "true");
      select.appendChild(option);
    }
  }

  function updateSelectValues() {
    const values = loopClass.value.split(" ").map(val => val.trim()).filter(val => val !== "");
    let baseValue = "", smValue = "", mdValue = "";
    let extraValues = [];

    values.forEach(val => {
      if (/^acms-col-sm-\d+$/.test(val)) {
        smValue = val;
      } else if (/^acms-col-md-\d+$/.test(val)) {
        mdValue = val;
      } else if (/^acms-col-\d+$/.test(val)) {
        baseValue = val;
      } else {
        extraValues.push(val);
      }
    });

    ensureOptionExists(colBase, baseValue);
    ensureOptionExists(colSm, smValue);
    ensureOptionExists(colMd, mdValue);

    colBase.value = baseValue;
    colSm.value = smValue;
    colMd.value = mdValue;

    colEtc.textContent = extraValues.length > 0 ? extraValues.join(", ") : "";
  }

  colBase.addEventListener("change", updateLoopClass);
  colSm.addEventListener("change", updateLoopClass);
  colMd.addEventListener("change", updateLoopClass);

  loopClass.addEventListener("input", updateSelectValues);

  updateSelectValues();
});
</script>

a-blog cms 独自の実装ポイント

通常の管理画面のモジュールID の設定でテストを行っていた際には

document.addEventListener("DOMContentLoaded", function () {
...
});

のように記述して動作していましたが、モジュールIDの管理画面は表の表示画面の「編集」リンクからモーダル表示が可能です。その際には既に DOMコンテンツがロード済みという事になり、ここで書かれた JavaScript を動作させることができません。

そこで a-blog cms の方で用意されている ACMS.addListener("acmsReady" ... を利用します。詳しくは「組み込みJSのイベントハンドラ」をご覧ください。

ACMS.addListener("acmsReady", function() {
...
});

追加のカスタマイズを考える

今回の JavaScript には input[name="entry_summary_loop_class" と Entry_Summary のコンフィグのフィールドが書かれていますが、他のモジュールでも使う事を考慮するような実装も考えられます。

このカスタム設定の SELECT 部分 + JavaScript のファイルを別ファイルにし target_input を変数化します。

@include("/admin/parts/loop-class.html", {"target_input": "entry_summary_loop_class"})   

/admin/parts/loop-class.html の中の JavaScript 部分を

const loopClass = document.querySelector('input[name="{{target_input}}"]');

のように指定する事で、他のモジュールでも同様に扱いたいことがあれば共用して利用することができるようにできます。

最後に

次のバージョン Ver. 3.2 で標準提供するテーマには {entry:loop.class} を活用したテーマにするようにしようと考えています。

また private/config.system.default.yaml

entry_summary_loop_class   : acms-col-6 acms-col-sm-4

のような設定を追加を検討します。どのような初期値がいいかは検討しないといけませんが、こうする事で標準テーマで表示設定にある項目が機能する事になります。

Entry_Continueモジュールと組み合わせて記事の続きをページ遷移なしで表示する

通常は続きを読むのリンク部分をクリックすると1つの記事が表示されるようになりますが、Entry_Continue というモジュールとポストインクルード機能を組み合わせることで、ページの遷移無しに続き部分を読み込んでくる事ができるようになります。