この記事は?
この記事は 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します。
$ cd ${WORK_DIR}
$ git clone https://github.com/uehatsu/mt-docker.git
cloneしたらbuildします(しばらくかかります)。
$ cd mt-docker
$ docker-compose build
以下の位置にMovable Typeのコード一式を配置します。(2019/12/21 20:47修正 Movalbe Type
=> Movable Type
西山さん感謝)
${WORK_DIR}/mt-docker/movabletype/
← ここの直下にmt.cgi
などが置かれるように
起動してみます。
$ docker-compose up
mt-check.cgi
にブラウザからアクセスしてみます。動いているようです。
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ファイルを作って、プラグイン名を表示させるところでしょうか。
何年も前に自分で書いたであろう文章を紐解きます。
${WORK_DIR}/mt-docker/plugins/PerformanceCounter/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
を以下のように書き換えます。
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
を以下のように作ります。
package PerformanceCounter::L10N;
use strict;
use warnings;
use base 'MT::Plugin::L10N';
1;
この状態で管理画面を見て見ると、以下のようになっているはずです。
<__trans phrase="foo bar">
と書かれていたとすると、そのままfoo bar
が表示されています。
plugins/PerformanceCounter/lib/PerformanceCounter/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',
);
1;
この状態で管理画面を見て見ます。
詳細(Description)と作者(Author)が英語表記に変わりました。L10N.pmを導入する前の情報になりましたね。
さて、最後は日本語化です。plugins/PerformanceCounter/lib/PerformanceCounter/L10N/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;
日本語化されました。
ここまででドキュメントとは違ったことが書かれている事に気付かれた方もいらっしゃると思います。
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}}
- Content-Type
- Body
- enry
- {{entry}}
- publish
- 0
- enry
- Pre-request Script
- (下記)
- Environment
- url
- username
- (ユーザ名)
- password
- (CMSのログインパスワードではなく、Webサービスパスワード)
- clientId
- (任意の値)
- accessToken
- (空欄)
- sessionId
- (空欄)
- i
- 0
- entry
- (空欄)
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はバックアップを取るなどしておいてください)。あと絶対本番環境では試さないでください。色々と問題がありますので。
#!/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ファイルに整形します。
$ 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
を開いてみます。
色々と出てきます。Top 15 Subroutines
のうち、一番上はDevel::NYTProf
自体なので、それ以下を見ますが、あまりピンときません。その場合は、その下に書かれているtreemap of subroutine exclusive time
のリンクをクリックしてみます。すると関数一つを一つの四角として視覚的に全体が表示されます。
実際に出てきた結果を色々と見て見ると、MT::Template::Handler::invoke
の辺りが香ばしいように見て取れます。手元の結果ではExclusive Timeに14.0ms、Inclusive Timeに1.97sかかっています。
さて、これ以上深掘りしているといつまでたってもプラグインができないので、この辺りでDevel::NYTProf
のお触りは終わらせましょう。mt.cgi
をバックアップから戻しておきます。
プラグインとしてDevel::NYTProfを仕掛ける場所を考える
先ほどはmt.cgi
の動作全体にDevel::NYTProf
をしかけました。プラグイン化する際もほぼ同じ挙動となるように、MTの動作の開始と終了時にDB::enable_profile()
, DB::disable_profile()
をしかけたいと思います。
(本当はリビルドの開始と終了に仕掛けたかったのですが、該当するフックポイントが見当たらず断念しました)
config.yaml
を以下のように書き換えます。callbacks行以下が追加されていますね。
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
と表示されるはずです。
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の管理画面をリロードした上でシステムのログを見てみます。
ぱっと見post_run
の後にpre_run
が並んでいて変ですが、マイクロ秒まで見ると表示順が間違っていて、ちゃやんとpre_run
=> post_run
の順番に動いているのが分かります。最後にpre_run
が一個だけあるのはログ取得のAPIを内部で叩いた際のスタートを拾っているものと思われます(深追いはしません)。
これでMTの動作開始と終了を取れる事は確認出来たので、実際にDevel::NYTProf
を仕掛けてみます。以下のように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ファイルの書き出し先を変更してみる
管理画面のプラグイン設定は以下の資料にやり方が書いてあります。
今回はシステム設定のみを利用してブログ設定は利用しないので、以下のように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
を以下の内容で作成します。
<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ファイルも修正しておきます。
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;
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;
プラグイン設定を見てみます。
スタイルが当たってませんが気にせず先に進みます(というか今のMTのお作法分からない、、、)。
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
が作成されているかをコンテナに入って確認します。
docker-compose exec apache bash
root@246da37f8d78:/usr/local/apache2# ls /tmp/
nytprof.out
ありますね。では、次は設定でパスを/tmp/nytprof_2.out
に変えてMTをリロードしてみます。
root@246da37f8d78:/usr/local/apache2# ls /tmp/
nytprof.out nytprof_2.out
ちゃんと設定が保存された上で動いていますね。
いきなりのまとめ
本当ならもうちょっとテスト周りなど突っ込んで書きたかったのですが、12月21日(土)の0:00を回ってしまったので、ここまでにしたいと思います。DIなど使いづらい所をどうやってテストするかなど含めて調べて書きたかったんですが、、、
そもそも今のMTプラグインの書き方のお作法を分かっていない状態で、古い知識のおぼろげな記憶と、自分の書いた大昔のドキュメントのみを頼りに書いたので、どこまで「今っぽいか」は自分では分からないまま。できればコメントで色々とお教え頂きたいです。
このプラグインで取得した結果を使って、MTのパフォーマンスチューニングできないかなと淡い野望を抱いておりますが、どうなることやら。
ともかく自分なりに「今風な開発環境」が作れたと思っております。さて、年末年始はプラグイン書くかな(^^)