すぎゃーんメモ

2014-03-13

helm-perldocを使ってインストール済みモジュールのドキュメントを参照する(carton対応も)

インストール済みのモジュールたちから、perldocを引くためのリストを作る - すぎゃーんメモ を書いた後 id:syohexさんと相談させていただいた結果、きれいにモジュール一覧を取得する方法を確立していただき

これでhelm-perldocインストール済みのモジュールpodをすべて取得して参照できるようになりました。ありがとうございます。


helm-perldoc:setup

このコマンドが非同期でモジュール一覧を取得してくれる。

既にモジュールリストが取得されていれば何もせずにスルーするので、cperl-modeでファイルを開くたびにhookで実行させておけばよい。

(defun my-cperl-mode-hook ()
  (helm-perldoc:setup)
  (flymake-mode t))
(add-hook 'cperl-mode-hook 'my-cperl-mode-hook)

helm-perldoc:perl5lib で取得対象パスを追加

helm-perldoc:setupモジュール一覧を取得するけれど、普通に実行すると その環境のPerlインストールしているもののみが対象になり、例えばcarton install~/myproject/local/以下にインストールされているものは取得されない。

そこで、そういうときのためにhelm-perldoc:perl5libという変数が用意されていて、これを指定してからhelm-perldoc:setupを呼ぶことで そのpath以下のモジュールたちも取得し、ドキュメントを引けるようになる。

helm-perldoc:modulesnilにするかprefix-argを指定してやればhelm-perldoc:setupは再びモジュール一覧を取得しなおしてくれる。

(setq helm-perldoc:perl5lib "~/myproject/local/lib/perl5")
(let ((current-prefix-arg t))
  (helm-perldoc:setup))

これで、~/myproject/local/以下にインストールされたモジュールhelm-perldocで引けるようになる。便利。


projectileを使って自動で helm-perldoc:perl5lib を追加

carton環境のプロジェクト下のファイルをそれほど使わないのであれば上記の操作を一回やれば済むのでそれほど面倒ではないけれど、

例えば複数のプロジェクトでcartonを使っていて それぞれの環境でしか入れていないモジュールのドキュメントを参照したい、となるとそれぞれのlocalディレクトリをhelm-perldoc:perl5libに追加する必要が出てくる。

いちいち手動で変更するのも面倒だし、起動時の設定ファイルに書いたりするのもイヤだ。

…というわけで、「ファイルを開いたときに、そのファイルがcarton環境下であり、且つ まだhelm-perldoc:perl5libにその環境用のpathが設定されていなければ自動で追加する」ということをしてみる。

projectile でルート検出

Projectile は、特定のファイル検索してプロジェクトの起点を判別してくれる。

デフォルトではcpanfileは判別ファイルとして含まれていないので、

(custom-set-variables
 '(projectile-project-root-files
   '(".projectile"        ; projectile project marker
     "Gemfile"            ; Bundler file
     "package.json"       ; npm package file
     "cpanfile"           ; CPAN dependencies for Perl applications
     ".git"               ; Git VCS root dir
     )))

のように変更しておく。順番的に".git"とかより先に"cpanfile"を書いておかないとgit管理下のisucon/perlみたいなプロジェクトでperlプロジェクトとしてより先にgitプロジェクトとして認識されてしまうので注意。

initファイルの記述

で、carton環境のプロジェクトか否かは(projectile-verify-file "cpanfile")で判別できるので、その際は(projectile-expand-root "local/lib/perl5")で得られるpathがhelm-perldoc:perl5libに追加されているべき、ということになる。

もし現在のhelm-perldoc:perl5libにそれが含まれていないようであれば追加してsetupしなおす、ということで

(defun my-cperl-mode-hook ()
  (let ((perl5libs (split-string (or helm-perldoc:perl5lib "") path-separator t))
        (local-lib (projectile-expand-root "local/lib/perl5")))
    (when (and (projectile-verify-file "cpanfile")
               (not (member local-lib perl5libs)))
      (setq helm-perldoc:perl5lib
            (if perl5libs
                (mapconcat 'identity (cons local-lib perl5libs) path-separator)
              local-lib))
      (message "helm-perldoc:perl5lib is updated. (%s)" helm-perldoc:perl5lib)
      (setq helm-perldoc:modules nil)))
  (helm-perldoc:setup)
  (flymake-mode t))
(add-hook 'cperl-mode-hook 'my-cperl-mode-hook)

このように起動時の設定ファイルに書いておくと、あとは何もしなくても開いたPerlファイルが属するプロジェクトでインストールされているモジュールは自動で取得されドキュメントを参照できるようになる。嬉しい!


今後?

2014.3.12現在 上記のようなものはhelm-perldocの標準機能を追加することでもう少し簡潔にできるようにしようか?という話もしているので、これからまた変わっていくかも

2014-02-07

diff-highlightでマルチバイト文字の差分が化けてしまう

Git の diff を美しく表示するために必要なたった 1 つの設定 #git - 詩と創作・思索のひろば (Poetry, Writing and Contemplation)

diff-highlightがイイなーと思って、早速~/.gitconfig

[pager]
    log  = /usr/local/opt/git/share/git-core/contrib/diff-highlight/diff-highlight | less -R
    diff = /usr/local/opt/git/share/git-core/contrib/diff-highlight/diff-highlight | less -R
    show = /usr/local/opt/git/share/git-core/contrib/diff-highlight/diff-highlight | less -R

と書いて使ってみているのですが、

f:id:sugyan:20140207200359p:image

のようになってしまっていてマルチバイト文字の差分がハイライトされるときに化けてしまって困っています。

どうすればいいんだろう…。


追記

とりあえず手元では

diff --git a/diff-highlight b/diff-highlight
index c4404d4..e959903 100755
--- a/diff-highlight
+++ b/diff-highlight
@@ -2,6 +2,7 @@
 
 use warnings FATAL => 'all';
 use strict;
+use Encode qw(decode_utf8 encode_utf8);
 
 # Highlight by reversing foreground and background. You could do
 # other things like bold or underline if you prefer.
@@ -20,10 +21,10 @@ while (<>) {
 		$in_hunk = /^$COLOR*\@/;
 	}
 	elsif (/^$COLOR*-/) {
-		push @removed, $_;
+		push @removed, decode_utf8($_);
 	}
 	elsif (/^$COLOR*\+/) {
-		push @added, $_;
+		push @added, decode_utf8($_);
 	}
 	else {
 		show_hunk(\@removed, \@added);
@@ -74,10 +75,10 @@ sub show_hunk {
 	my @queue;
 	for (my $i = 0; $i < @$a; $i++) {
 		my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]);
-		print $rm;
+		print encode_utf8($rm);
 		push @queue, $add;
 	}
-	print @queue;
+	print encode_utf8($_) for @queue;
 }
 
 sub highlight_pair {

て書きかえて使う。

2014-01-31

Time::Pieceでadd_monthsするときは月末の扱いに気をつける

現在のTime::Pieceの最新版は、1.27。

翌月は何月か、というのを得るのに

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;

my $t = localtime;
print $t->add_months(1)->mon, "\n";

というコードを書いていて、1月31日に実行したら3が返ってきていてハマった。という話。

Time::Piece::add_monthsで月を操作した結果の日付がその月の月末を超えていると、正規化されて翌月の日付になってしまうようだ。

$ perl -MTime::Piece -E 'say Time::Piece->localtime->ymd'
2014-01-31
$ perl -MTime::Piece -E 'say Time::Piece->localtime->add_months(1)->ymd'
2014-03-03

ので、「翌月は何月か」を正確にとりたい場合は安易にadd_months(1)を使わずに、別の方法で取得する必要がある。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds;

my $now = Time::Piece->localtime;
my $last_day = Time::Piece->localtime->strptime(
    sprintf('%04d-%02d-%02d', $now->year, $now->mon, $now->month_last_day),
    '%Y-%m-%d',
);
my $next_month = ($last_day + ONE_DAY)->mon;
print $next_month, "\n";

こんなかんじで、「現在の月の最終日」をmonth_last_dayで取得できるので、それを使って月末の日付をstrptimeから生成、その1日後の日付から月数を取得する。

非常に面倒だけど。


DateTimeの場合は

DateTime(現在の最新は1.06)はそのへん考慮して計算できるようになっていて、普通にaddすると同じような挙動だが、end_of_monthを指定することでその扱いを変えることができる。

$ perl -MDateTime -E 'say DateTime->now->ymd'
2014-01-31
$ perl -MDateTime -E 'say DateTime->now->add(months => 1)->ymd'
2014-03-03
$ perl -MDateTime -E 'say DateTime->now->add(months => 1, end_of_month => "preserve")->ymd'
2014-02-28

end_of_monthは"wrap", "limit", "preserve"の3種類を指定可能で、加算の場合は"wrap"が、減算の場合は"preserve"がデフォルトになっているらしい。

"wrap"はTime::Pieceの挙動と同じで、月末を超えていた場合は翌月の日付にする。が、"limit", "preserve"の場合は月が変わることなく月末の日付になる。

"limit"と"preserve"の違いは元々の日付が月末だったときに加減算後にも合わせて月末にするか否か、の挙動らしい。例えば翌月の方が長い場合。

$ perl -MDateTime -E 'say DateTime->new(year => 2014, month => 2, day => 28)->add(months => 1, end_of_month => "limit")->ymd'
2014-03-28
$ perl -MDateTime -E 'say DateTime->new(year => 2014, month => 2, day => 28)->add(months => 1, end_of_month => "preserve")->ymd'
2014-03-31

"preserve"だと、月末に加算した結果も月末になっている。


まとめ

…というようなことが全部podにちゃんと書いてあるから、よく読もう。


ちなみに

Rubyの場合は

$ ruby -rDate -e 'puts Date.new(2014, 1, 31)'
2014-01-31
$ ruby -rDate -e 'puts Date.new(2014, 1, 31) >> 1'
2014-02-28

簡単でいいですね…

2013-11-09

#isucon 2013で優勝しました

第三回 ISUCONの本選に、参加しました。予選から引き続き、@さん、@さんとの「LINE選抜チーム」。

f:id:sugyan:20131109220940j:image:w360

第三回 #isucon 本選リアルタイムフォトレポート【更新終了】 : ISUCON公式Blog

結果はなんと、優勝!!

おや、優勝2回目だ。→第1回のとき


タイムライン

予選のとき同様に、自分の手元にある記録と記憶を辿ってどんな雰囲気だったか書き残してみます。間違っていたらゴメンナサイ。

使用言語はPerlです。

〜10:00
  • 出社…じゃなくて会場入り。ちゃんと前日に早寝したので寝坊せずに済みました。
〜11:00
  • 開会待ち。早くきすぎた、でも他の参加者さんたちも早くからしっかり集まってる。
  • ルール説明。ストーリー仕立てで緊張感が走る。画像系サービスか〜。
11:00〜
  • 開始。用意されたのは5台、初期実装で動くものは1台目のみ。
    • サーバの扱いに慣れているお2人に任せて鍵やらhost情報など整理していただき
  • webapp/perl以下をgitリポジトリにして、github private repositoryにpush
    • このへんは事前に共有しておいていたのでスムーズに。
    • 前回の反省なんかも踏まえ余計なものを入れないようにignoreとか
      • そのへんの手順やチェック項目も予選後にgithub wikiで共有しておいてたのは良かった
  • access logを分析してbenchmarkでどのURLにどれくらいリクエストくるか調べる
    • これも必要だろうと事前にtagomorisさんが用意していた
  • 手元のローカル環境で同じアプリを動かせるように
    • 画像はリポジトリに入れなかったのでそれさえ落としてくれば問題なく動くようになった
  • なんかlib以下にKossyが入ってるけど0.23で古いしCartonで0.25入れて使えばいいよね?
    • 11:42 initial commit
    • ついでにgit pullcarton installsupervisorctl reloadするdeploy scriptとかも用意
12:00〜
  • 一度手を止めてチューニング方針を話し合い。
    • 予選のときの反省で「発見順に手をつけていく」ではなくある程度の方針を決める相談タイムを設けるべき、と決めていた
  • 12:23 決めたのが以下。
    • **1をDBサーバ、画像サーバ、ワーカを動かすものとして使う
    • **2**5 の4台で受ける
    • 画像は投稿された時点で変換したものを作る
    • 既存の画像はバッチ処理で変換後のものを用意
  • 1台目に画像データを集約しフロントを4台で受けて、というおおまかな構成はこの時点で決定
  • 画像をPOSTされた際にローカルに保存だけして、レスポンスはすぐに返し別のワーカーで変換・転送処理しよう、としていた
    • のちに、これでは駄目で破綻することを知る。
  • 12:47 カスタマイズしたStarletに入れ替え、Proclet化など(kazeburoさん)
  • 12:50 apacheをnginxに入れ替えて再度ログを取ってみたり(tagomorisさん)
13:00〜
  • お昼ごはん。
  • 13:41 画像データ集約のためのWebDAVサーバ構築(tagomorisさん)
  • 13:45 アイコン画像はキャッシュするようにする、など(kazeburoさん)
  • 14:12 画像POST時にローカルに保存し、別のworkerでconvert処理しGET時にそれを返すよう変更(sugyan)
    • まだWebDAVは使ってない
  • 14:45 timelineまわりのSQLチューニング(kazeburoさん)
  • 15:22 uri_forのチューニング(kazeburoさん)
  • 15:24 WebDAVにFurlからのPUT/GETでconvert画像をやり取りするよう変更(sugyan)
  • benchmarkを走らせてみたがエラー頻出、ダメだ
    • POSTのレスポンスからすぐにGETで画像とりにこられたらダメだよね、ということに気付く
    • workerでconvertする方法自体が破綻している、ということでボツに
  • 既存の画像のcrop_square & convertした画像を生成する作業に取り組む(tagomorisさん)
  • 15:59 iconのPOSTされたものはconvertしてWebDAVへPUT/GETするよう変更(kazeburoさん)
  • 16:28 画像もPOSTされた時点でconvertしてWebDAVへPUT/GETするよう変更(sugyan)
    • 全台で動かしても大丈夫だよね、ということで**2**5へのデプロイも始めたり
17:00あたり〜
  • 画像の変換も無事に終わる
  • なんか色々うまく動いていなかった部分を修正したり
  • 17:30くらい? ようやくエラーなく走り切り、workloadも増やし40,000点越え
    • 特別賞獲得!知らなかったけど次のチームと1分半差だったんだ…
  • まだDB初期化せずに再度benchmark走らせるとエラーでるとか
  • あやしいところを潰して、最後に初期化して提出
  • で、終了
結果発表
  • ドキドキ
  • 177,290点で優勝!

まとめ・雑感

予選のときの反省を活かし、目についたところから手をつける、ではなく一度話し合って方針を決める時間をはやめに設けた。(ほんとうはその実装は15時頃には終わらせてそこからさらにチューニングするくらいのつもりだったけど最終的にはそこでほぼ終了だった…)

やっぱり焦って色々ミスったりもしたけど、冷静に(?)間違っている部分を探して素早く直して進めていけていたのではないかな、と。どハマりして死んだ、とかは無くてよかった。最終的に時間ギリギリにはなってしまったけど。

IRCのチームchannelはログ40行くらいしか残らず、ほとんどが口頭でのやりとりで対面のコミュニケーションで進めていた。良かったのか悪かったのかは分からないw


ともかく、良い結果を出すことができてよかった!kazeburoさん、tagomorisさん、ありがとうございました!

出題者、運営の皆様、ありがとうございました!


先月の予選と今日の本選と行きたい現場に行けなかったぶん、賞金でたくさんアイドルに会いに行こうと思います。

2013-09-12

アメブロ「いいね!」機能を使うためのChrome拡張を作ってみた

先月末あたりに、スマートフォン版アメーバブログに新機能として「いいね!」というのが加わったらしい。

どうやらスマートフォン専用の機能となっているようで、スマフォで記事にアクセスした際にのみ「いいね!」ボタンが現れ、PCブラウザからは通常は利用出来ないようになっているようだ。

多くのアイドルちゃんが利用しているアメーバブログ、巡回ついでに「いいね!」つけて回りたいのだけど、わざわざそれだけのために手元のiPhoneからアクセスし直すのも面倒。PCからもアイドルちゃんたちのブログ記事に「いいね!」したい。

調べてみたところ、この「いいね!」ボタンはスマフォ版の"s.ameblo.jp"でのみ表示されるが、これはiframeで"iine.blog.ameba.jp"ドメイン下にあるボタン専用ページを開いているようだ。このページはリファラを見ているようで直にアクセスしてもはじかれる。ちゃんとtokenも含められていて、単純なリクエスト送るだけではダメそうだ。しかもスマフォ専用で作られているのでそもそもtouchイベントでしか動作しなくてマウスclickでは反応してくれない。

ということで、「別ドメインのiframe表示」「touchイベントでの作動」の壁があり単純なブックマークレットのようなものでは「いいね!」操作までもっていくのは厳しそう。

ということでChrome拡張で解決することにした。

Chrome Web Store - ameblo-iine

manifest.jsonはこんなカンジで、

{
    "manifest_version": 2,
    "name": "ameblo-iine",
    "description": "This extension enables 'iine!' on ameba blog",
    "version": "1.0.1",
    "content_scripts": [
        {
            "matches": ["http://ameblo.jp/*"],
            "js": ["zepto.min.js", "iine.js"]
        },
        {
            "matches": ["http://iine.blog.ameba.jp/*"],
            "js": ["mouse2touch.js"],
            "all_frames": true
        }
    ]
}

ブログ記事("ameblo.jp"ドメイン)を開いたときに記事タイトルとなる要素を見つけ、対応する「いいね!」ボタンページのiframeを差し込むようにする。

$('h3.title a, a.skinArticleTitle').each(function (i, a) {
    var url = $(a).attr('href');
    var m = url.match('/([^/]+?)/entry-([0-9]+)\.html');
    if (m) {
        $(a).after(
            $('<iframe>').attr({
                src: 'http://iine.blog.ameba.jp/web/display_iine.html?receiveAmebaId=' + m[1] + '&entryId=' + m[2] + '&from=entry&device=sp&skinCode=',
                width: '100%',
                height: '65',
                style: 'border: 0;'
            })
        );
    }
});

iframeの中身となる"iine.blog.ameba.jp"ドメインではマウスクリックをtouchイベントに変換するJSだけ読み込ませておく。

document.addEventListener("mouseup", function (e) {
    var event = document.createEvent("Event");
    event.initEvent("touchend", true, true);
    e.target.dispatchEvent(event);
}, false);

みたいなかんじで大丈夫なようだ。これはiframe上でも動作してもらわないと困るので、manifest.json"all_frames": trueと指定するのが大事。


とりあえずこれだけで一通りのアメブロ記事のページでタイトル下に「いいね!」ボタンが表示され、クリックするだけで「いいね!」できるようになった。

f:id:sugyan:20130912005933p:image


ブログテーマによっては記事タイトルの要素が見つけられずに失敗することがあるかもしれない。あとブログ主の設定によって「いいね!」を許可していない場合もあるので、その場合は無駄な空白ができてしまうようだ。まぁ気にしなくていいかな…。