Help us understand the problem. What is going on with this article?

Movable Typeのプラグインを今の自分ならどう作るかをまとめてみる

この記事は?

この記事は Movable Type Advent Calendar 2019 21日目の記事です。

元々「Vue.jsにMTMLを移植してみよう」と思って進めていたんですが、どうしても上手くいかない部分があり断念(それはそれで別途まとめたい)。その後、AWS LambdaでMT Data APIを動かすべくトライしていたのですが、動作一歩手前で期日までに乗り越えられるか微妙な不具合を踏んだので断念(それもそれで別途まとめたい)。

丁度、先日 こちらの記事 でMTの開発環境をローカルに建てた所だったので、「それならプラグインでも書いてみるか」と。「プラグインの書き方なんて今更書いたって」とも思ったんですが、逆に「今の自分だったら、どんな風にプラグインを作っていくのか」を手順を追って書いていくと、ニッチな所に需要があるかもしれないとポジティブに考えて進める事にします。

簡単に自己紹介

  • 元Six Apart社員
  • TypePad(現Lekumo)チームで開発 ⇒ Movable Typeチームでドキュメント(プラグイン開発ガイド、MTMLガイド、等) ⇒ 退社してからはMTプラグイン構築 ⇒ 現在はMTからは遠ざかってます
  • 今はPHP(Laravel), node.js(Vue.js)を中心にFlutterやらKotlinやら雑食系エンジニアやってます
  • 今回久々にAdvent Calendarに参加

開発環境

  • MacBook Pro 2018 : macOS Mojave 10.14.6
  • IntelliJ IDEA Ultimate 2019.3 + Perlプラグイン
  • Docker for Mac 2.1.0.5(40693)

OS

上にも書いてあるようにMac使いです。Catalinaにして痛い目にあったのでMojaveに戻しました。

IDE

以前、RubyMineを使う機会があり、それ以来JetBrainsのIDEに惚れ込んでます。RubyMine ⇒ WebStorm ⇒ PHPStorm ⇒ Rider ⇒ IntelliJ IDEA Ultimateと流れて来ました。
Perlを使うにはサードパーティー製のPerlプラグインを入れる必要があるんですが、先日のIntelliJ IDEAのアップデートで突然 Perl Docker プラグインが動作しなくなり(IDE起動時にこける)、かなり焦りました。外すと動作するのと、結局使っていなかったので大事には至りませんでした。とはいえ、本当なら他の言語みたいにJetBrainsの純正サポートが欲しい所。Perlはもう、、、(言葉少なめ
PerlプラグインはWebStormやPHPStormでもインストールできるので、そちらを普段使っている人ならインストールして利用出来るはずです。お試しあれ。

Docker

Six Apart在席時はVM WareにLinux入れて作業していた記憶があるのですが、もう昔過ぎて記憶の彼方。その後、VirtualBox + Vagrantを使って開発をし、今ではすっかりDocker + docker-composeで作業するようになりました。今回もDockerを使って開発を進めていきます。

今宵、どんなプラグインを作ろうか?

大学時代、動画圧縮演算の高速化を研究室でずっとやっていたのを今思い出しました。その際大事だったのは「一に計測、二に計測、三四がなくて、五に計測」。そもそもMTの再構築が遅いのがずっと疑問だったので、それを解く所まではいかなくても内部で何をやっているのかを計測する所までやってみようと思います。

名前は Performance Counter Plugin とでもしておきましょう(安直)。

開発開始

Dockerにて環境を作る

上でも紹介した記事で作成したリポジトリからDocker関連のファイルをcloneします。

terminal
$ cd ${WORK_DIR}
$ git clone https://github.com/uehatsu/mt-docker.git

cloneしたらbuildします(しばらくかかります)。

terminal
$ cd mt-docker
$ docker-compose build

以下の位置にMovable Typeのコード一式を配置します。(2019/12/21 20:47修正 Movalbe Type => Movable Type 西山さん感謝)

${WORK_DIR}/mt-docker/movabletype/ ← ここの直下にmt.cgiなどが置かれるように

起動してみます。

terminal
$ docker-compose up

mt-check.cgiにブラウザからアクセスしてみます。動いているようです。

http://localhost/cgi-bin/mt/mt-check.cgi

スクリーンショット 2019-12-17 21.35.48(4).png

mt-wizard.cgiでmt-config.cgiを作ります。手順はここでは省略。

その他、諸々手動で設定を追加してmt-config.cgiは以下の感じになりました。特にプラグイン開発のためにPluginPathを追加しているのがミソでしょうか。
docker-compose.ymlと同じ階層のpluginsディレクトリが/usr/local/src/plugins/にマウントされています。こうすることでMT直下のpluginsディレクトリと/usr/local/src/plugins/ディレクトリの両方がプラグインを入れておけるディレクトリとして認識されます。
この後のプラグイン開発は${WORK_DIR}/mt-docker/plugins/PerformanceCounter/以下で行います。

#======== REQUIRED SETTINGS ==========

CGIPath        /cgi-bin/mt/
StaticWebPath  /mt-static/
StaticFilePath /usr/local/apache2/cgi-bin/mt/mt-static

#======== DATABASE SETTINGS ==========

ObjectDriver DBI::mysql
Database mt
DBUser admin
DBPassword pass
DBHost mysql

#======== MAIL =======================
EmailAddressMain test@example.com
MailTransfer smtp
SMTPServer mailhog
SMTPPort 1025
MailEncoding UTF-8

#======== PLUGIN PATH ================
PluginPath plugins
PluginPath /usr/local/src/plugins

#======== Memcached ==================
MemcachedDriver Cache::Memcached
MemcachedNamespace MT
MemcachedServers memcached:11211

DefaultLanguage ja

ImageDriver ImageMagick

開発

MT Plugin開発最初の一歩

最初の一歩はやはりYAMLファイルを作って、プラグイン名を表示させるところでしょうか。

何年も前に自分で書いたであろう文章を紐解きます。

https://github.com/movabletype/Documentation/wiki/Japanese-plugin-dev-1-1

${WORK_DIR}/mt-docker/plugins/PerformanceCounter/config.yamlを作ります。

config.yaml
id: PerformanceCounter
name: Performance Counter Plugin
version: 0.1
description: Performance Counter with Devel::NYTProf
author_name: Hatsuhito UENO
author_link: https://uehatsu.info/
doc_link: https://uehatsu.info/

MT管理画面の「システム>設定>プラグイン>プラグイン設定」の一覧に「Performance Counter Plugin 0.1」が表示されていたらOKです。

ついでなので多言語対応

多言語対応と言っても日英だけですが、こちらも以下のドキュメントを元に行っておきます。

config.yamlを以下のように書き換えます。

config.yaml
id: PerformanceCounter
name: <__trans phrase="Performance Counter Plugin">
version: 0.1
description: <__trans phrase="_PLUGIN_DESCRIPTION">
author_name: <__trans phrase="_PLUGIN_AUTHOR">
author_link: https://uehatsu.info/
doc_link: https://uehatsu.info/
l10n_class: PerformanceCounter::L10N

plugins/PerformanceCounter/lib/PerformanceCounter/L10N.pmを以下のように作ります。

L10N.pm
package PerformanceCounter::L10N;
use strict;
use warnings;
use base 'MT::Plugin::L10N';

1;

この状態で管理画面を見て見ると、以下のようになっているはずです。

スクリーンショット 2019-12-17 22.06.43.png

<__trans phrase="foo bar">と書かれていたとすると、そのままfoo barが表示されています。

plugins/PerformanceCounter/lib/PerformanceCounter/L10N/en_us.pmを以下のように作ります。

en_us.pm
package PerformanceCounter::L10N::en_us;

use strict;
use warnings;
use base 'PerformanceCounter::L10N';
our %Lexicon;

%Lexicon = (
    '_PLUGIN_DESCRIPTION' => 'Performance Counter with Devel::NYTProf',
    '_PLUGIN_AUTHOR' => 'Hatsuhito UENO',
);

1;

この状態で管理画面を見て見ます。

スクリーンショット 2019-12-17 22.11.45.png

詳細(Description)と作者(Author)が英語表記に変わりました。L10N.pmを導入する前の情報になりましたね。

さて、最後は日本語化です。plugins/PerformanceCounter/lib/PerformanceCounter/L10N/ja.pmを以下のように作ります。

ja.pm
package PerformanceCounter::L10N::ja;

use strict;
use warnings;
use base 'PerformanceCounter::L10N::en_us';
our %Lexicon;

%Lexicon = (
    'Performance Counter Plugin' => 'パフォーマンス計測プラグイン',
    '_PLUGIN_DESCRIPTION' => 'Devel::NYTProf を利用したパフォーマンス計測を行います',
    '_PLUGIN_AUTHOR' => '上野 初仁',
);

1;

スクリーンショット 2019-12-17 22.15.42.png

日本語化されました。

ここまででドキュメントとは違ったことが書かれている事に気付かれた方もいらっしゃると思います。

use warnings;our %Lexicon;です。

ドキュメントにはuse warnings;は書かれていません。our %Lexicon;use vars qw( %Lexicon );と元々は書かれています。
これは、IntelliJ IDEAのPerlプラグインが「イマドキのお作法に則ってないよ」と教えてくれたので書き換えました。すでに10年近く前のドキュメントなので、お作法が変わっているのですね。最近のPerlのお作法は追っていないので勉強になりました。

先に進む前にDevel::NYTProfを試してみる

プラグイン開発を進める前に、ちょっとDevel::NYTProfを試してみたいと思います。

先ほど紹介したmt-dockerにはDevel::NYTProfがインストール済みなのですぐ使えます。他の環境の方はcpanm Devel::NYTProfとしてインストールしてから進めてください。Pure Perlモジュールではないのでmakeが必要になります。

再構築用のデータを準備する

実際に計測を始める前に再構築時のパフォーマンスを計測したいのでテスト用のデータを準備します。
データの作成にはPostmanを利用します。

サイトIDが1となるようなサイトを作成し、そこにEntryをData APIを使って500件投入する事にします。Data API便利(今更感)。

  • METHOD
    • POST
  • エンドポイント
    • {{url}}/v4/sites/1/entries
  • Headers
    • Content-Type
      • application/x-www-form-urlencoded
    • X-MT-Authorization
      • MTAuth accessToken={{accessToken}}
  • Body
    • enry
      • {{entry}}
    • publish
      • 0
  • Pre-request Script
    • (下記)
  • Environment
    • url
    • username
      • (ユーザ名)
    • password
      • (CMSのログインパスワードではなく、Webサービスパスワード)
    • clientId
      • (任意の値)
    • accessToken
      • (空欄)
    • sessionId
      • (空欄)
    • i
      • 0
    • entry
      • (空欄)
Pre-request_Script
const postRequest = {
    url: pm.environment.get('url') + '/v4/authentication',
    method: 'POST',
    header: [
        {
            key: 'Accept',
            value: 'application/json',
        }
    ],
    body: {
        mode: 'urlencoded',
        urlencoded: [
            {
                key: 'username',
                value: pm.environment.get('username'),
            },
            {
                key: 'password',
                value: pm.environment.get('password'),
            },
            {
                key: 'clientId',
                value: pm.environment.get('clientId'),
            },
        ],
    },
}
pm.sendRequest(postRequest, function (err, res) {
    if (err) {
        console.error(err)
    }
    if (res.code === 200 && res.status === 'OK') {
        console.log(res.json())
        pm.environment.set('accessToken', res.json().accessToken)
        pm.environment.set('sessionId', res.json().sessionId)

        var i = pm.environment.get('i') * 1 + 1
        entry = {
            'title': 'Test Entry ' + i,
            'body': ''.repeat(1024)
        }
        pm.environment.set('entry', JSON.stringify(entry))
        pm.environment.set('i', i)
    }
});

上記のような設定で、PostmanのCollection Runnerから500回投入を実行すれば、'Test Entry 123'といった連番のタイトルのエントリーが500件作成されます。本文はなんとなく「卍」を1024文字連続させたものになっています。テストデータなので、まぁ、こんな所で。

結構な時間かかりますが、POST時にpublishを0に設定してありますので投稿時の再構築はしていません。この設定しないと、もっと時間がかかるはずです。手元環境では30分ちょっとかかりました。

mt.cgiにDevel::NYTProfをしかけてみる

では、mt.cgiを以下のように書き換えてみます。(mt.cgiはバックアップを取るなどしておいてください)。あと絶対本番環境では試さないでください。色々と問題がありますので。

mt.cgi
#!/usr/bin/perl -w

# Movable Type (r) (C) 2001-2019 Six Apart, Ltd. All Rights Reserved.
# This code cannot be redistributed without permission from www.sixapart.com.
# For more information, consult your Movable Type license.
#
# $Id$

use strict;
use Devel::NYTProf;
use lib $ENV{MT_HOME} ? "$ENV{MT_HOME}/lib" : 'lib';
DB::enable_profile("/usr/local/src/plugins/nytprof.out");
use MT::Bootstrap App => 'MT::App::CMS';
DB::disable_profile();

Devel::NYTProfをしかけた状態で、先ほどエントリーを500件しかけたサイトを再構築してみます。Devel::NYTProf自体がボトルネックになるので、通常よりも再構築はかなり遅くなります。
6分半ほどで再構築が完了しました。実行が終わるとplugins直下にnytprof.outという計測結果を格納したDBファイルが出来ているはずです。このままでは見られないので、dockerコンテナに入って専用のコマンドを実行してHTMLファイルに整形します。

terminal
$ docker-compoase exec apache bash
# cd /usr/local/src/plugins/
# nytprofhtml -f nytprof.out
Reading nytprof.out
Processing nytprof.out data
Writing line reports to nytprof directory
 100% ... 
Extracting subroutine call data ...
Extracting subroutine links
Generating subroutine stack flame graph ...

plugins/nytprofというディレクトリが作られているので、ブラウザでこのディレクトリの中のindex.htmlを開いてみます。

スクリーンショット 2019-12-18 00.24.59.png

色々と出てきます。Top 15 Subroutinesのうち、一番上はDevel::NYTProf自体なので、それ以下を見ますが、あまりピンときません。その場合は、その下に書かれているtreemap of subroutine exclusive timeのリンクをクリックしてみます。すると関数一つを一つの四角として視覚的に全体が表示されます。

スクリーンショット 2019-12-18 01.07.50.png

実際に出てきた結果を色々と見て見ると、MT::Template::Handler::invokeの辺りが香ばしいように見て取れます。手元の結果ではExclusive Timeに14.0ms、Inclusive Timeに1.97sかかっています。

スクリーンショット 2019-12-18 01.06.03.png

さて、これ以上深掘りしているといつまでたってもプラグインができないので、この辺りでDevel::NYTProfのお触りは終わらせましょう。mt.cgiをバックアップから戻しておきます。

プラグインとしてDevel::NYTProfを仕掛ける場所を考える

先ほどはmt.cgiの動作全体にDevel::NYTProfをしかけました。プラグイン化する際もほぼ同じ挙動となるように、MTの動作の開始と終了時にDB::enable_profile(), DB::disable_profile()をしかけたいと思います。
(本当はリビルドの開始と終了に仕掛けたかったのですが、該当するフックポイントが見当たらず断念しました)

config.yamlを以下のように書き換えます。callbacks行以下が追加されていますね。

config.yaml
id: PerformanceCounter
name: <__trans phrase="Performance Counter Plugin">
version: 0.1
description: <__trans phrase="_PLUGIN_DESCRIPTION">
author_name: <__trans phrase="_PLUGIN_AUTHOR">
author_link: https://uehatsu.info/
doc_link: https://uehatsu.info/
l10n_class: PerformanceCounter::L10N

callbacks:
  MT::App::CMS::pre_run: $PerformanceCounter::PerformanceCounter::Callbacks::pre_run
  MT::App::CMS::post_run: $PerformanceCounter::PerformanceCounter::Callbacks::post_run

以下の風になっていて、コールバックに対して、どんな関数を実行するかを指定します。

calbacks:
  (コールバック名): $(プラグインID)::(プラグインクラス)::(ハンドラ名)

ハンドラをL10N.pmと同じ階層のCallbacks.pmに実装してみます。まずはログ出力のみです。これでMTの動作スタート時にログにpre_runと表示され、動作終了時にpost_runと表示されるはずです。

Callbacks.pm
package PerformanceCounter::Callbacks;

use strict;
use warnings;

use Time::HiRes qw(gettimeofday);
use Time::Piece;

sub pre_run {
    my ($cb, $app) = @_;

    my ($sec, $microsec) = gettimeofday();

    doLog(sprintf("pre_run: %s.%06d", localtime($sec)->strftime('%F, %T'), $microsec))
}

sub post_run {
    my ($cb, $app) = @_;

    my ($sec, $microsec) = gettimeofday();

    doLog(sprintf("post_run: %s.%06d", localtime($sec)->strftime('%F, %T'), $microsec))
}

sub doLog {
    my ($msg, $class) = @_;
    return unless defined($msg);

    require MT::Log;
    my $log = new MT::Log;
    $log->message($msg);
    $log->level(MT::Log::DEBUG());
    $log->class($class) if defined $class;
    $log->save or die $log->errstr;
}

1;

テストしてみます。MTの管理画面をリロードした上でシステムのログを見てみます。

スクリーンショット 2019-12-20 23.15.45.png

ぱっと見post_runの後にpre_runが並んでいて変ですが、マイクロ秒まで見ると表示順が間違っていて、ちゃやんとpre_run => post_runの順番に動いているのが分かります。最後にpre_runが一個だけあるのはログ取得のAPIを内部で叩いた際のスタートを拾っているものと思われます(深追いはしません)。

これでMTの動作開始と終了を取れる事は確認出来たので、実際にDevel::NYTProfを仕掛けてみます。以下のようにCallbacks.pmを修正します。

Callbacks.pm
package PerformanceCounter::Callbacks;

use strict;
use warnings;

use Devel::NYTProf;

sub pre_run {
    my ($cb, $app) = @_;

    DB::enable_profile("/usr/local/src/plugins/nytprof.out");
}

sub post_run {
    my ($cb, $app) = @_;

    DB::disable_profile();
}

1;

nytprof.outファイルを削除した上で再構築を実施すると、同じ位置にnytprof.outが作られ、Devel::NYTProfがプラグイン経由で実施されたことがわかります。

プラグイン設定でnytprof.outファイルの書き出し先を変更してみる

管理画面のプラグイン設定は以下の資料にやり方が書いてあります。

https://github.com/movabletype/Documentation/wiki/Japanese-plugin-dev-3-1

今回はシステム設定のみを利用してブログ設定は利用しないので、以下のようにconfig.yamlを書き換えます。

config.yaml
id: PerformanceCounter
name: <__trans phrase="Performance Counter Plugin">
version: 0.1
description: <__trans phrase="_PLUGIN_DESCRIPTION">
author_name: <__trans phrase="_PLUGIN_AUTHOR">
author_link: https://uehatsu.info/
doc_link: https://uehatsu.info/
l10n_class: PerformanceCounter::L10N

callbacks:
  MT::App::CMS::pre_run: $PerformanceCounter::PerformanceCounter::Callbacks::pre_run
  MT::App::CMS::post_run: $PerformanceCounter::PerformanceCounter::Callbacks::post_run

system_config_template: path_setting_system.tmpl
settings:
    path_setting_system:
        default: /tmp/nytprof.out
        scope: system

プラグインのlibディレクトリと同じ階層にtmplディレクトリを作って、その中にpath_setting_system.tmplを以下の内容で作成します。

path_setting_system.tmpl
<mtapp:setting id="path_setting_system" label="<__trans phrase='_PerformanceCounter_Path'>"
    hint="<__trans phrase="Please input the nytprof.out path.">" show_hint=1>
    <input type="text" name="path_setting_system" id="path_setting_system"
        value="<mt:GetVar name="path_setting_system">" />
</mtapp:setting>

L10Nファイルも修正しておきます。

en_us.pm
package PerformanceCounter::L10N::en_us;

use strict;
use warnings;
use base 'PerformanceCounter::L10N';
our %Lexicon;

%Lexicon = (
    '_PLUGIN_DESCRIPTION'      => 'Performance Counter with Devel::NYTProf',
    '_PLUGIN_AUTHOR'           => 'Hatsuhito UENO',
    '_PerformanceCounter_Path' => 'nytprof.out PATH',
);

1;
ja.pm
package PerformanceCounter::L10N::ja;

use strict;
use warnings;
use base 'PerformanceCounter::L10N::en_us';
our %Lexicon;

%Lexicon = (
    'Performance Counter Plugin'         => 'パフォーマンス計測プラグイン',
    '_PLUGIN_DESCRIPTION'                => 'Devel::NYTProf を利用したパフォーマンス計測を行います',
    '_PLUGIN_AUTHOR'                     => '上野 初仁',
    '_PerformanceCounter_Path'           => 'nytprof.out 出力パス',
    'Please input the nytprof.out path.' => 'nytprof.out の出力パスを入力してください。',
);

1;

プラグイン設定を見てみます。

スクリーンショット 2019-12-20 23.50.43.png

スタイルが当たってませんが気にせず先に進みます(というか今のMTのお作法分からない、、、)。

Callback.pmを以下のように書き換えます。

Callback.pm
package PerformanceCounter::Callbacks;

use strict;
use warnings;

use Devel::NYTProf;

sub pre_run {
    my ($cb, $app) = @_;

    DB::enable_profile(get_path());
}

sub post_run {
    my ($cb, $app) = @_;

    DB::disable_profile();
}

sub get_path {
    my $plugin = MT->component("PerformanceCounter");
    return $plugin->get_config_value("path_setting_system", "system");
}

1;

関数get_path()でプラグイン設定のpath_setting_systemが取れるようになっています。この状態でMTをリロードして、/tmp/nytprof.outが作成されているかをコンテナに入って確認します。

terminal
 docker-compose exec apache bash
root@246da37f8d78:/usr/local/apache2# ls /tmp/
nytprof.out

ありますね。では、次は設定でパスを/tmp/nytprof_2.outに変えてMTをリロードしてみます。

terminal
root@246da37f8d78:/usr/local/apache2# ls /tmp/
nytprof.out  nytprof_2.out

ちゃんと設定が保存された上で動いていますね。

いきなりのまとめ

本当ならもうちょっとテスト周りなど突っ込んで書きたかったのですが、12月21日(土)の0:00を回ってしまったので、ここまでにしたいと思います。DIなど使いづらい所をどうやってテストするかなど含めて調べて書きたかったんですが、、、
そもそも今のMTプラグインの書き方のお作法を分かっていない状態で、古い知識のおぼろげな記憶と、自分の書いた大昔のドキュメントのみを頼りに書いたので、どこまで「今っぽいか」は自分では分からないまま。できればコメントで色々とお教え頂きたいです。
このプラグインで取得した結果を使って、MTのパフォーマンスチューニングできないかなと淡い野望を抱いておりますが、どうなることやら。
ともかく自分なりに「今風な開発環境」が作れたと思っております。さて、年末年始はプラグイン書くかな(^^)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away