ps とかで表示されるプロセス名をわかりやすくしたいという話です。

Starman だと $0 に適当な値を入れてくれて、master なのか worker なのかとか、どの psgi が動いているかとかがわかるのですが、Starlet ではそういうことをやってくれないので、1つのサーバで複数インスタンス動かしていると、plackup のプロセスだらけで、どれがどれだかわからなくなります。

app.psgi で $0 で設定すれば良いかと思ったが

app.psgi で $0 設定したらとりあえずいいだろ、と思ってやってみましたが、どうも上手くいきません。app.psgi 内で $0 を warn してみるとそもそも plackup になってもいません。

調べてみると、Plack::Util::load_psgi で使っている _load_sandbox 内で local $0 をしてから app.psgi を do しており、ロード後に元のプロセス名に戻ってしまうようです。

ということで、単純に app.psgi で $0 を書きかえても plackup を使っている場合書きかわりません

Starlet 用のハックを入れる

なんかうまい方法がなさそうなので以下のようなクソハックを app.psgi に入れました。

{
    ### set process name for Starlet

    use Parallel::Prefork;
    my $name = $0; 
    my $orig_new = \&Parallel::Prefork::new;
    no warnings 'redefine';
    *Parallel::Prefork::new = sub {
        my ($class, $opts) = @_; 
        $opts->{before_fork} = sub {
            $0 = "$name (worker)"
        };  
        $opts->{after_fork} = sub {
            $0 = "$name (master)"
        };  
        $orig_new->($class, $opts);
    };  
};

Starlet は Parallel::Prefork を使っているので、前もって use して new を書きかえて before_fork / after_fork を設定します。

before_fork / after_fork は Parallel::Prefork の機能でセットできるフックポイントですfork 前に worker プロセス用の $0 を設定して、fork 後に $0 を master 用の値に変えています。なのでタイミングによっては master プロセスも一瞬 worker 表示になります。

app.psgi ロード時のコンテキストでは $0 には .psgi のファイル名が入っています。これは上記の Plack::Util::_load_sandbox で設定されたものです。ということで、ロード時のコンテキストの$0を保存しておいて、後の $0 の設定に使っています。

app.psgi が Starlet 専用みたいになってキモいですが、Parallel::Prefork をロードする以上の害はとりあえずないのと、自分の場合は開発中に Standalone で動かす以外は Starlet を使うことにしているので問題ありません。

これで以下のようになりました。

cho45     4699  0.0  0.5 185432  5332 ?        S    May11   0:00 /home/cho45/project/COQSO/script/app.psgi (master)
cho45     4700  0.0  2.4 296604 25168 ?        S    May11   0:09 /home/cho45/project/COQSO/script/app.psgi (worker)
cho45     4701  0.0  2.5 296820 26468 ?        S    May11   0:11 /home/cho45/project/COQSO/script/app.psgi (worker)
cho45     4702  0.0  1.8 302304 19136 ?        S    May11   0:11 /home/cho45/project/COQSO/script/app.psgi (worker)
  1. トップ
  2. tech
  3. Starlet でプロセス名をわかりやすくしたい

  • RTT (ping) が約30ms〜50msぐらいの環境
  • * がついているのは http2 の push
  • HTML 内で直接参照しているリソースについてもダウンロードしている (nghttp の --get-assets オプション)


岡元太郎の母の塔です。子供がこわいこわい言う。

  1. トップ
  2. photo


トラックバックを実装しました

といってもサイト内のエントリ間の言及を表示するだけです。いわゆる古代のオープンなトラックバックではありません。

古いエントリに新しいエントリへのリンクがないのは不便だなと思い、とりあえずさくっと LIKE 検索で機能を足してみていたのですが、良さそうなので、ちゃんとリレーションテーブルをこしらえたりしました。

しかし、全体的にページキャッシュ機能を入れているので、単に機能を足すだけでも考えることが増えてしまいます。イメージではトラックバックが送られたページに更新がかかるというだけですが…

キャッシュ処理も見直して、全体的にテストも書いたりしました。キャッシュが入ると複雑性が増す分、独り人力QAには限界があるので、みっちりテストを書くほうがかえって省力化します。

  1. トップ
  2. tech
  3. トラックバックを実装しました

Stokke ストッケ ベビーチェア ハイチェア 本体 トリップトラップ 食卓 赤ちゃん 椅子 ウォールナットブラウン - Stokke

Stokke

5.0 / 5.0

2歳になって自分でできることが増えてきたり、「みいつけた」で椅子を見ているせいか、椅子が好きになってきたみたいなので、本人だけで座れる椅子を買いました。

ちょっと高いけど、10年ぐらい使えそうなので、そう考えればそこまで高くないかなと思ったのと、こういう大物は買ったり捨てたりするのが面倒なので、ストッケ・トリップトラップというのを買ってみました。色はウォールナットブラウン。

届いて組み立ててる最中から子供はだいぶテンションが高くて、六角レンチでボルトを締めようとしたり(むしろ緩めてたけど)していて面白かった。

ちゃんと独りで座ることもできるし、安定しているので良さそう。ただ子供は「新しいものは様子を見ながら使う」というのがわかってないので、ドンドコドンドコやっててハラハラする。

ところで最初組み立て図を見ても、座面とかにはボルトがなくて、どうやって固定されるかがよくわからなかった。組み立ててる途中でようやくわかったが、これの座面は左右の支えの窪みに入れた状態で、他の部分のボルトを締めていくことで挟みこんで固定されるようになっている。面で固定されるから直接ボルトで固定するよりもいいのかな。増し締め忘れないようにしないといけなそう。

1週間ぐらい既に使っているけど、表面塗装のせいか汚れても思いのほか拭き取りやすい。あと、倒れないように足の底部に滑る部品(滑り止めではない)がついていて、面白かった。子供がテーブルを蹴って後ろに倒れないように、椅子自体を滑らせるという形のセーフティになっている。ただ、性質上、床面がフローリングでないと機能しない。うちはフローリングにマットを敷いているので残念ながら意味ない。

今まで使っていたもの

 -

5.0 / 5.0

これを今まで使っていた。まだ使えそうだけど、自分で座ることができなくてちょっと可哀そうになってきた。それに毎回大人が持ちあげる必要があるが、子供も既にそこそこ重い(12kgぐらい)ので結構つらい。

汚れたら風呂場でザーーーと洗って干すという雑な運用をしていたけど、半日ぐらいですぐ乾いてくれるので良かった。値段も安かったし、十分に使えたと思う。

RiZKiZ 子供用絵本ラック 木目調 キッズブックシェルフ マガヒンラック 収納に便利な仕切り棚付き (3段タイプ/ダークブラウン) - RiZKiZ(リズキズ)

RiZKiZ(リズキズ)

4.0 / 5.0

今までの本棚に不満があったのでこれを買ってみました。本体がかなり重くてしっかりしてます。

子供は特に何の反応もなく本棚と認識して使っていて、あっけない感じだった。絵本渡して「ないないして」というとちゃんと入れてくれる。よかった。

今まで使っていたもの

森井紙器 すまいるキッズ 絵本ラック レッド - 森井紙器工業(Moriishiki)

森井紙器工業(Moriishiki)

2.0 / 5.0

買ったときはあんまり本もないからいいかと思ったけど、急激に増えてしまって納まりきらなくなった。

この商品はダンボールでできていて軽いんだけど、おかげで子供が中の本を全部出して、本棚を振り回して遊んでることが多々あった。さすがに最近はそんなことしないけど、軽くて小さいというのも考えものだなあと思った。あとダンボールなので、乱暴に扱うとかなりヨレてしまう。

結果的にはこれを買ったのは失敗だった。最初からそこそこのものを買ったほうが良かった (どうせ必要だから)。

今までは Cache::FileCache によるファイルシステムキャッシュにしていたけど、いくつか問題があって SQLite にかえた

ファイルシステムキャッシュで困っていたこと

  • なんか遅い
  • キャッシュ無効化の処理のためにキャッシュを生成元とキャッシュのキーをキャッシュ内に格納したいが、アトミックにやるうまい方法がなかった

SQLite 選択

Redis とかを立てるのが機能的には便利そうだけど、リソース的にあんまりサーバプロセスを増やしたくはないので、SQLite とした。

CREATE TABLE cache (
	`cache_key` TEXT NOT NULL PRIMARY KEY,
	`content` BLOB NOT NULL
);

CREATE TABLE cache_relation (
	`id` INTEGER PRIMARY KEY,
	`cache_key` TEXT NOT NULL,
	`source_id` TEXT NOT NULL
);
CREATE INDEX cache_relation_index_cache_key ON cache_relation (`cache_key`);
CREATE INDEX cache_relation_index_source_id ON cache_relation (`source_id`);

CREATE TRIGGER on_cache_deleted AFTER DELETE ON cache BEGIN
	DELETE FROM cache_relation WHERE cache_key = old.cache_key;
END;

CREATE TRIGGER on_cache_related_deleted AFTER DELETE ON cache_relation BEGIN
	DELETE FROM cache WHERE cache_key = old.cache_key;
END;

こんな感じでキャッシュ本体 (cacheテーブル)と、そのキャッシュを生成するのに使ったもののid(文字列)のリスト(cache_relationテーブル)を持って、お互いにトリガーで消しあうようにしておく。

こうしておくと、通常のキャッシュキーによる追加・削除だけではなくて、生成元が更新された時に関連するキャッシュをまとめて消せる。


なお、キャッシュ用のDBファイルは元データと分けた。というのも、元データのDBは毎日バックアップとしてGmailに送りつけているので、キャッシュを含めたくなかったから。

  1. トップ
  2. tech
  3. ブログのキャッシュバックエンドの変更

1週間前ぐらいまで毎日いろいろと資料を読んでコード書きまくったりしていたけど、例によって急激にやる気が失われてほとんど何もしていない。

  1. ハードウェア周りのコード書くやる気が急激に失われる
  2. サイトの最適化まわりにやる気を出す
  3. 全てのやる気が失われる (だいたいやったから)

という感じで進行した。やる気があるときは「ゲームとかやってる暇全くないし、ゲームとか完全に無駄な時間を過ごすだけ。酒飲んで頭回らない時間をつくるのも無駄」みたいな意識の高さがあるが、そうでないときはとにかく頭を一切使いたくないという感じになる。

ハードウェアまわりは明確にやりたいことはあるけど、途中でやる気がなくなったのでそれ以来全くすすめていない。

ディアブロ3を久しぶりにちょっとやってるけど、目が疲れる。

どっか散歩したいけれど、忌中なので神社には行けない。つつじヶ丘あたりを地図なしで歩いたりしてみたりしたけど気分は晴れず。ストレスは溜まるがやはり解消方法がない。

二重の意味で(出勤も保育園も)はぁ嫌だなあと思いながら保育園に行ったら、保育園が開いていなかった。意味がわからなかったけどとにかく開いていなくて、どうしようもないので帰ってきた。

自分はこういう予期していないトラブルに滅法弱くて、普段に数倍に嫌な気持ちが増幅されるんだけど、そうはいっても現実として「誰が子供の面倒見るの?」という問題を解決しないといけないので、会社を休んだ。

結局なんだったかといえば保育園は諸事情 (特に複雑な事情ではなくて、年間行事表には書いてあった) によって休みだった。昨年は土曜日にあたっていたようで意識しなかったし、休みになるという認識がなかった 。そして今年は直前に子供の発熱があったせいか特別休みの予告がなかったので、誰も休みを認識してなかった。

来年同じことがないようにカレンダーにリピートを入れた。

別に保育園が悪いわけではないけど、ほんと保育園関係は嫌な気持ちになることが多い。暦通りじゃない休みがこんなにアナウンスされないものなのか。

準備して、子供が寝てるとこを起こして(朝食をとったあと、たまに寝ることがある)、だっこして、門まで連れていって、開いてない。平日だから当然出勤するつもりでいる。出勤する人の流れが見えるから平日なのは間違いない。寝ボケていて時間を間違えたのか。開いてないから帰るしかない。おやつやら昼食やらをどうするか考えないといけない。夕方までの間のすごしかたを考えないといけない。スワップアウトされてるもろもろを頭の中のワーキングメモリにロードしなおさないといけない。

自分のイライラするシチュエーションを考えみると「ワーキングメモリに大きなデータをロードする必要がある」ことと「予期してない」ことが大きく関係しているような気がしている。

自分は頭の中のワーキングメモリに、何らかの仕事(プログラム全体の設計とか)をロードするのにとても時間がかかる。このロードされたイメージが何らかの出来事(外部割込み由来)で無駄になるときにイライラする。

生活全般で

保育園なんかで、1円にもならない仕事をやらされたりする。全体としてたいしたことじゃなくても、その仕事をロードするたびに、趣味にしろ仕事にしろ、頭の中のイメージにリセットがかかる。そして再度元の仕事をロードする必要がある。

予期できることもあるけど、そもそもストレスかかる仕事が多いのでコンテキストスイッチが発生するだけで不愉快。

仕事において

しばしばあるのが、些細な仕様変更・追加で、これにより根本的に直す必要がありそうだというケース。

そういうことが起こらないように、将来的にどんな仕様の追加がありそうかということをエスパーして、予期されるそれらへの変更耐性を設計に組み込んだりするする。そういうのも含めて、ピースをうまく噛み合せて全体的に「これでうまくいきそうだな」というイメージを構築するまで、まずとても時間がかかる。

しかしいろいろ考えたとしても、考慮外の仕様が出現したりする。この「考慮外の追加仕様」はイライラする。というのも

  1. 頭の中にロードされたイメージの中にどのように割り込ませるかを考えたうえで
  2. 場合によって全体のイメージを再構築する必要がある

それほど複雑ではない場合は部分的な再構築ですむのでそれほど最悪な気分にはならないが、全体を再構築する必要がありそうな場合は、頭の中を全部一旦空にするので再度全体の設計をロードしなおすまで時間がかかる。その間ずっとイライラしている。

どうすればいいのか

頭の中にロードする時間自体を短縮するのは難しそうなので、イメージのサイズを小さくするとか、予期できそうなことは予期しておくしかない? しかし予期できそうなことを予期しておくというのをやるとイメージのサイズが大きくなる。

そもそも何故頭の中のイメージが初期化されることでイライラするのか。

前提知識:レスポンシブ広告といっても、既定の広告サイズからコンテナサイズによって選ばれて表示されるだけで、広告そのもののサイズは固定です。

そのまま使うと、広告がロードされた時にページの高さの再計算が入って、コンテンツがガタガタと動いて大層鬱陶しいです。とりあえず便利そうだから貼ってみると、この挙動でギョギョっとします。

しかし、レスポンシブ広告はCSSで設定する width/height によって正確に対応する広告サイズを入れることができ、この場合は高さが固定になるのでガタガタしません。CSSなのでメディアクエリでサイズを変えられ、レスポンシブサイトと大変相性が良いです。

公式のドキュメントに詳細な設定方法が書いてあります。といっても width/height をコンテナに設定しているだけです。

既にサイトがメディアクエリによってレスポンシブになっているなら、レスポンシブ広告にした場合、広告サイズもCSSで指定するだけでよくなってます。

余談:Adsense の広告コードの修正・改変

「広告コードには一切の変更が認められない」みたいなことが過去に書いてあったような記憶があるんですが (記憶違いかも)、現状では上記の通り、改変が認められるケースがあり、悪意をもってやるようなことじゃなければだいたい大丈夫そうな雰囲気があります。

  1. トップ
  2. tech
  3. Adsense のレスポンシブ広告の正しい使いかた

ティファール ケトル 1.2L ジャスティンプラス サーブル たっぷり 空焚き防止 自動電源OFF 湯沸かし KO340177 - ティファール(T-fal)

ティファール(T-fal)

3.0 / 5.0

これの古いモデルをずっと(7年ぐらい)つかっていたが、底のステンレス部分が緑色に錆びのようなものができていて、さすがにどうかと思ったので買い替えた。

緑色の何かの正体はよくわからず。こすっても落ちないし使用頻度が高いのでカビではないと思うが、クエン酸を入れて沸かしてもとれなかった。

ティファール ケトル 電気ケトル 0.8L アプレシアプラス メタリックノワール コンパクト 空焚き防止 自動電源OFF 湯沸かし ステンレスボディ BI805D70 - ティファール(T-fal)

ティファール(T-fal)

3.0 / 5.0

これにした。沸かせる量が減ったが、1.2L 沸かすことがまずなかったように思えるので小さいモデルに。外装がプラスチックのものは嫌だったのでステンレスのものにした。燃費を上げるためか全体を金属にしたものはないみたいだった。

電気ケトルの電気料金

メーカーのページを見てみると、同じメーカーのほぼ同じモデルでも微妙に電気料金が違く書かれている。

例えば、アプレシアプラス(プラスチックモデル)では約0.44円/カップ1杯と書いてあるが、アプレシアプラスメタリックでは約0.50円/カップ1杯となっている。といっても、この差だと365回沸かしてようやく約22円程度の差なので誤差ではありそう。

なお、このメーカーの場合だとスペック上で最も燃費が悪いもので約0.56円/カップ1杯、最も良いもので約0.44円/カップ1杯と、0.12円の差がある。

ES2015 の iterable/iterator/generator による無限 FizzBuzz | tech - 氾濫原 に続いて、オブジェクト指向っぽく書けるようにしてみました。

ポイントはジェネレータ的なものをラップして常に It というクラスのインスタンスにするところです。

"use strict";

function It(iterable) { if (typeof this === 'undefined') return new It(iterable); this.iterable = iterable; }
It.countup = function* (n) {
	for (;;) yield n++;
};
It.prototype = {
	zip : function* () {
		const iterators = [];
		for (let it of this) {
			iterators.push(it[Symbol.iterator]());
		}
		for (;;) {
			const nexts = [];
			for (let it of iterators) {
				nexts.push(it.next());
			}
			yield nexts.map( (i) => i.value);
			if (nexts.some( (i) => i.done) ) break;
		}
	},
	map : function* (func) {
		for (let value of this) {
			yield func(value);
		}
	},
	cycle : function* () {
		for (;;) {
			for (let value of this) {
				yield value;
			}
		}
	},
	take : function (n) {
		const ret = [];
		for (let value of this) {
			ret.push(value);
			if (!(ret.length < n)) break;
		}
		return ret;
	}
};
It.prototype[Symbol.iterator] = function () { return this.iterable[Symbol.iterator]() };
{
	let wrapGenerator = function (generator) {
		return function () {
			const self = this;
			const args = arguments;
			const iterable = {};
			iterable[Symbol.iterator] = function () {
				return generator.apply(self, args);
			}
			return new It(iterable);
		}
	};
	let generatorConstructor = Object.getPrototypeOf(function*(){}).constructor;
	for (let key of Object.keys(It.prototype)) {
		if (It.prototype[key] instanceof generatorConstructor) {
			It.prototype[key] = wrapGenerator(It.prototype[key]);
		}
	}
}


console.log(Array.from(
	It([
		It.countup(1),
		It(["", "", "Fizz"]).cycle(),
		It(["", "", "", "", "Buzz"]).cycle()
	]).
		zip().
		map( (_) => _[1] + _[2] || _[0] ).
		take(30)
));
  1. トップ
  2. tech
  3. ES2015 の iterable/iterator/generator による無限 FizzBuzz (オブジェクト指向編)

表題の通りですが、Generator にはいずれの protocol も実装されています。気になるのは iterable の挙動ですが、どうやらレシーバーの Generator 自身を返すようです。

function* count (n) {
	for (;;) yield n++;
}

var c = count(1);
console.log(c.next, c[Symbol.iterator]);
//=> [Function: next] [Function: [Symbol.iterator]]

// iterator protocol
console.log(c.next()); //=> { value: 1, done: false }
console.log(c.next()); //=> { value: 2, done: false }
console.log(c.next()); //=> { value: 3, done: false }

// iterable protocol
console.log(c[Symbol.iterator]().next()); //=> { value: 4, done: false }

console.log(c[Symbol.iterator]() === c); //=> true

iterator protocol (next) で状態をすすめたあとに iterable protocol (Symbol.iterator) で iterator を取得すると、状態は継続されています。

  1. トップ
  2. tech
  3. Generator は iterator であり、iterable でもある

最近になって「関連コンテンツ」と「ページ単位の広告」というのが beta になって登場した。サイト最適化と同時にこれらも有効にしてみたりしていた。

関連コンテンツ

広告ユニットではあるが、サイト内の関連コンテンツを表示してくれるので普通に便利。先頭に2つぐらい広告が入る感じ。便利になるだけで収益にならないユニットなのかと思ったけど、ちゃんと収益にもなる模様。

パフォーマンスレポートを見てみても、他のユニットと比較してかなり収益性が高そう (このサイトの場合そもそも微々たるものだけど)

ページ単位の広告

「ページ単位の広告」という名前の中にさらに2種類ある。

「アンカー広告」はよくあるスマフォ向けの広告で、画面下を常時占領するタイプ。ただ、position: fixed で固定しているようで、JS の scroll イベントを奪っている系のウザさはないのが救い。

「モバイル全面広告」はときどきあるだいぶうざい全面広告に似ているが、ページを開いたときではなくて、次ページに遷移しようとするときに次のページをロードしながら表示される広告、ということになっているらしい。

ページのパフォーマンスと広告

明かなことだけど、Adsense を貼るだけで、おそろしくページのロードは遅くなる。非同期コードを使おうがページ全体の onload までの時間が伸びるのはどうしようもない。

たいして儲かるわけでもない Adsense を貼って遅くするのもどうかとは思うけど、Adsense なくしたらロード早いのは当たり前すぎるので、かえって意固地になって広告を貼りまくっている。遅くなっても儲かってくれるならいいんだけど

  1. トップ
  2. tech
  3. Adsense の新しい広告ユニット

ひさしぶりに Chmertron をビルドしてみたら動かなくてつらい。codesign しなければフリーズしないんだけど、原因がわからない。

これのようだ。そのうち対策する https://github.com/electron/electron/issues/3871

ES2015 の iterable protocol / iterator protocol だとそこそこ自然に無限リストを作れるわけなので、ちょっと試しにやってみました。node v5.2.0 で動かしました。

"use strict";


function* countup(n) {
	for (;;) yield n++;
}


function* map(iterable, func) {
	for (let value of iterable) {
		yield func(value);
	}
}


function* cycle(iterable) {
	for (;;) {
		for (let value of iterable) {
			yield value;
		}
	}
}


function take(iterable, n) {
	const ret = [];
	for (let value of iterable) {
		ret.push(value);
		if (!(ret.length < n)) break;
	}
	return ret;
}


function* zip(iterable) {
	for (;;) {
		const nexts = [];
		for (let it of iterable) {
			nexts.push(it.next());
		}
		yield nexts.map( (i) => i.value);
		if (nexts.some( (i) => i.done) ) break;
	}
}




console.log(
	take(
		map(
			zip([
				countup(1),
				cycle(["", "", "Fizz"]),
				cycle(["", "", "", "", "Buzz"])
			]),
			(_) => _[1] + _[2] || _[0]
		),
		30
	)
);

FizzBuzz の map のところは destructive assignment ができるともうちょい綺麗に書けますが、現時点だとまだオプションが必要なのでキモい書きかたになりました。

気になった点

protocol と言っている通り、next() を適切なインターフェイスで実装しているものは全て iterator なため、イテレータ全般に対して基底クラスみたいなものがありません。これはこれでいいんですが、不便な点があります。イテレータに対してメソッドの追加というのができません。

自分の中のオブジェクト指向の気持ちは以下のように書きたいのです。

[
	countup(1),
	cycle(["", "", "Fizz"]),
	cycle(["", "", "", "", "Buzz"])
].
	zip().
	map( (_) => _[1] + _[2] || _[0] ).
	take(30)

しかし、イテレータの prototype というのは存在しないので、毎回必ず何らかの関数形式のラッパーが必要になってしまいます。

  1. トップ
  2. tech
  3. ES2015 の iterable/iterator/generator による無限 FizzBuzz

お葬式で酒・塩・鰹節

身内(配偶者の祖母)が亡くなったため、かなり久しぶりにお葬式という行事にあった。最中に気になったこととして、出棺前・納骨後に酒・塩・鰹節を使ったお清めがあった。具体的には、手を流水で清めたあと酒と塩と鰹節を口に含む。自分の祖父母は既に亡くなっているが、そういうことをした覚えがなかった。

特にいえば酒・塩はともかく、鰹節を使うイメージが全くなかった。鰹節というと、どちらかといえばおめでたいイメージがあるので、違和感を覚えたのだった。検索するとこの地域独自の文化らしいが、そもそも酒や塩も神饌と考えれば、さらに他にものがあってもおかしくはない気はしてきた。

殆ど子供の様子を書いてないが、育っている。最近の様子を書いておく

日常生活での意思疎通がかなり可能になった

例えば

  • 「パパ、トイレ行ってもいい?」と聞くと「うん」と言ったあとトイレまでついてきてドアを開けてくれる
  • 「鼻でてるよ」と言うとティッシュをとって鼻をゴシゴシやって、ゴミ箱に捨てる
  • うんちで力んでいるとき「ウンチでた?」と聞くと「うん」や「ううん」と言う (あんまりしつこく見てるとバイバイと言う)
  • ウンチ出たあと「じゃあオムツ変えよう」というと袋やオムツで持ってついてくる

理解できてるはずのことを言っても無視することは多々ある。目があってても意図的に無視していることがある。

叱ったとき

叱った直後に即座に「あーんあーん」と泣かないことがある。無表情か、泣くのを我慢しているような表情 (ないしは悔しそうな表情) をする。叱られてることはわかってるようだが、説明がわからないのかもしれない。

最後に「わかった?」「うん」「はいでしょ?」「はい」みたいなのを毎回やってる。これたぶんわかってないけど、これ以上確認する術がないし、やらないと話が終わらないので、どうすればいいかよくわからない。

あと叱るときはかなり真剣に叱らないとダメで、中途半端に叱るとニヤニヤしていて冗談だと思っている節がある。両手握って眼を見て叱るとすくなくとも真剣なのは伝わる模様。

泣くのを我慢しているような表情はあきらかに面白いんだけど、ここで笑うと気持ちが伝わらない(気がする)ので難しい。

言葉

言葉はそれほど話せない。2語出ることもあるが基本的には1語

  • 泣くとすぐに「ママ・だっこ」という (正確には「ママ、ぁっこ〜」みたいな発音)
  • 「ない!」「あった!」「もっかい」が得意
  • 「バイバイ」も良くいう。電車に対して自発的に言うこともある。
  • 一緒に風呂に入ったりすると「チンチン!」とかいう(ほんとに全く教えてないんだけど……)
  • テレビのキャラクターは割とわかってて、見たいものを主張してくる
    • 「ワンワン」「ウータン」「モーモー(メーコブ ※ただしメーコブは羊)」「ニャーニャー (ミーニャ)」「チューチュー (ムテキチ)」「イシュ (コッシーのこと)」「ッチ (ピタゴラスイッチ)」「ダンダンダン (ピタゴラ拳法)」
  • 「らりるれろ」の発音ができない (母音だけになる)

イヤイヤ

もうすぐ2歳なので結構言い出している。「イヤ」ではなくて「あイヤ・あイヤ」という。謎。

  • 「手繋ごう」→「あいや」といって手を後ろに隠す
  • 「爪切ろう」→「あいや」
  • 本を読んであげようとすると「あいや」といって別の本を渡されるが、それを読もうとすると「あいや」と言ってまた別の本になる。謎

食事

あいかわらず食事のマナーは良くない。口に詰め込みまくって吐きだすことがある。詰め込むまえに「口の中にまだ入ってるよ」というと一応気付くみたいで、飲みこんでから口をあけて「ア!」と言ってもう中にないことを主張する。

薬飲むとき毎回ヨーグルトを使っているが、なぜかヨーグルトだけは毎日食べても飽きない(習慣と認識している?)みたいで素直に食べてくれる。

その他

テレビがついていないときに、PS4 のコントローラを探し出して電源をつけていることがある。本人はそれ以上ちゃんと操作ができないので大人にコントローラを押し付けてくる。

大人が大人用の番組 (アニメを含む) を見ているとたいそう不満なようで、ひたすらワンワンワンワンと主張する。

「ないない (片付け)」も本人の気がのってればやってくれる。おもちゃの一部がないときは「これがないよ」というと、多少は探してくれる。

めっちゃ面白かった。

艦コレみたいなノリのアニメなのだと思って見ていなかったけど、とりあえず1話見てみるかと思った結果そういったものノリのものではないことがわかって、12話まで一気に見てしまった。その後OVA版を見てからもう一周見てしまった。

全体的には弱小高校が部活の全国大会で勝ち上がる王道スポコンみたいなストーリーだった。メインの部分を戦車道という架空の競技に置き換えた感じ。

演出がかっこいいとかいろいろあるけど、結局のところ主人公の徳が高いってのが良かった。

プライムビデオだと、テレビシリーズ12話とOVA1話(これが本当のアンツィオ戦です!)が見れる。OVAはテレビシシーズで飛ばされた試合。劇場版はまだ発売とかされない模様。

やたら人気?だから2期とかあるのかと思ったけど、これが全部みたい。

ガールズ&パンツァー - 渕上舞

渕上舞

5.0 / 5.0

ガールズ&パンツァー これが本当のアンツィオ戦です! - 渕上舞

渕上舞

5.0 / 5.0

ガールズ&パンツァー 劇場版 [Blu-ray] - 渕上舞

渕上舞

5.0 / 5.0

そろそろやることなくなったので minify などをやることにしました。

ただ、ブログシステムの出力の最後ほうでページごとに全体を minify すると、全体としてどうしても処理に時間がかかってしまいます。要求として、キャッシュなしの状態からでも1エントリあたり0.1秒ぐらいでは処理したいので、これだと厳しい感じでした。(約7900エントリぐらいあるので、0.1s で処理しても全体のキャッシュ再構築に13分かかる計算です)

いろいろ考えたのですが (そもそも minify しないとかも)、以下のようにしました

  • エントリ本文はエントリ保存時に minify しておく (本文はDB に格納)
  • テンプレートをテンプレートの段階で minify してキャッシュしておく

minify には html-minifier を使っています。html-minifier はテンプレートを対象にした minify も一応サポートしていて、ある程度妥協すればテンプレート対象でも十分に minify できます。

テンプレートとエントリが前もってをminifyされていれば、あとは繋げて出すだけです。

テンプレートの minify

このブログシステムのテンプレートは Text::Xslate::Syntax::TTerse です。

まず、Xslate のビュー読みこみをフックして、テンプレートを動的に minify するようにしました。

{
	no warnings 'redefine';
	*Text::Xslate::slurp_template = sub {
		my ($self, $input_layer, $fullpath) = @_;
		my $source = sub {
			if (ref $fullpath eq 'SCALAR') {
				return $$fullpath;
			} else {
				open my($source), '<' . $input_layer, $fullpath
					or $self->_error("LoadError: Cannot open $fullpath for reading: $!");
				local $/;
				return scalar <$source>;
			}
		}->();
		if ($fullpath =~ /\.html$/) {
			$source = Nogag::Utils->minify($source);
			return $source;
		} else {
			return $source;
		}
	};
	$XSLATE->load_file($_) for qw{
		index.html
		_article.html
		_adsense.html
		_images.html
	};
};

Xslate には pre_process_handler というのがあって、 読みこまれたテンプレートにフィルタをかけることができます。が、この機能だとファイル名がわからないので使っておらず、slurp_template を上書きしています。

(明示的に load_file しているのは、エラーが起きるなら起動時にしたいというのと、fork 前にロードすることでメモリの節約になるからで、本題とは関係ありません)

TTerse なテンプレートに対して html-minifier を使う場合、配慮する点がいくつかあります。

全体的にHTMLとしてパースできること

TTerse の場合以下のようにも書けますが、ダブルクオートの入れ子が HTML として見ると syntax error なので html-minifier で Parse Error になります。

<meta content="[% "foo" _ "bar" %]">

属性を出しわけるとき個別に条件をつける

テンプレートの構文をタグに混ぜるには html-minifier 側にオプションを設定します (customAttrSurround)。

ただ、複数属性を囲うとうまく属性を分解できなくて死ぬっぽいので、個別に囲う必要がありました。

<!-- ダメ -->
<article
   [% IF xxx %]
   itemscope
   itemprop="blogPosts"
   [% END %]
   >
<!-- よろしい -->
<article
   [% IF xxx %]itemscope[% END %]
   [% IF xxx %]itemprop="blogPosts"[% END %]
   >

sortClassName は false にする

クラスをテンプレートで出しわけしていると死にます

実際の html-minifier へのオプション

TTerse 対応のためオプションは以下のようになりました。customAttrSurround が適切に設定されていれば、sortAttributes は true にしても問題ないようです。

function processMinify (html) {
	return Promise.resolve(minify(html, {
		html5: true,
		customAttrSurround: [
			[/\[%\s*(?:IF|UNLESS)\s+.+?\s*%\]/, /\[%\s*END\s*%\]/]
		],
		decodeEntities: true,
		collapseBooleanAttributes: true,
		collapseInlineTagWhitespace: true,
		collapseWhitespace: true,
		conservativeCollapse: true,
		preserveLineBreaks: false,
		minifyCSS: true,
		minifyJS: true,
		removeAttributeQuotes: true,
		removeOptionalTags: true,
		removeRedundantAttributes: true,
		removeScriptTypeAttributes: true,
		removeStyleLinkTypeAttributes: true,
		processConditionalComments: true,
		removeComments: true,
		sortAttributes: true,
		sortClassName: false,
		useShortDoctype: true
	}));
}

エントリ本文のポストプロセス

エントリ保存時には、minify もそうですが、ほかにも修正を加えたいことが多々あります。例えば画像を強制的に https 化して mixed content を防ぎたいとか、コードハイライトを行いたいとかです。

コードハイライトは今まで highlight.js を使い、クライアントサイドでやっていました。これも本来ダイナミックにやる必要はなくポストプロセスでできることなので、そのように変えることにしました。highlight.js をサーバサイドで行うと、付与されるマークアップ分 HTML の転送量が増えますが、highlight.js 自体が結構大きいので、かなり長いコードをハイライトしない限り、サーバサイドでやったほうが得そうです。

以前サーバサイドでMathJaxを処理するようにしましたが、この node.js の内部向けサーバをさらに拡張して、JS でいろいろなポストプロセスの処理を書けるようにしました。

これにより、クライアントサイドでやってたことをそのままサーバサイドできるようになりました。すなわち、hightlight.js をそのままサーバサイドでも動かしていますし、jsdom を使って細かいHTMLの書きかえを querySelector など標準の DOM 操作でできるようにしています。

ポストプロセス用のデーモン

前述のように node.js で動くデーモンで、ブログシステム(Perl)とは別のプロセスで動き、APIサーバになっています。

全体的には以下のようなコードです。なお書き換え部分は変な挙動をするとやっかいなので、テストを書けるようにしてあります

#!/usr/bin/env node


const jsdom = require("jsdom").jsdom;
const mjAPI = require("mathjax-node/lib/mj-page.js");
const hljs = require('highlight.js');


const minify = require('html-minifier').minify;
const http = require('http');
const https = require('https');
const url = require('url');
const vm = require('vm');


const HTTPS = {
	GET : function (url) {
		var body = '';
		return new Promise( (resolve, reject) => {
			https.get(
				url,
				(res) => {
					res.on('data', function (chunk) {
						body += chunk;
					});
					res.on('end', function() {
						res.body = body;
						resolve(res);
					})
				}
			).on('error', reject);
		});
	}
};


mjAPI.start();
mjAPI.config({
	tex2jax: {
		inlineMath: [["\\(","\\)"]],
		displayMath: [ ["$$", "$$"] ]
	},
	extensions: ["tex2jax.js"]
});


function processWithString (html) {
	console.log('processWithString');
	return Promise.resolve(html).
		then(processMathJax).
		then(processMinify);
}


function processWithDOM (html) {
	console.log('processWithDOM');
	var document = jsdom(undefined, {
		features: {
			FetchExternalResources: false,
			ProcessExternalResources: false,
			SkipExternalResources: /./
		}
	});
	document.body.innerHTML = html;
	var dom = document.body;
	return Promise.resolve(dom).
		then(processHighlight).
		then(processImages).
		then(processWidgets).
		then( (dom) => dom.innerHTML );
}




function processHighlight (node) {
	console.log('processHighlight');
	var codes = node.querySelectorAll('pre.code');
	for (var i = 0, it; (it = codes[i]); i++) {
		if (/lang-(\S+)/.test(it.className)) {
			console.log('highlightBlock', it);
			hljs.highlightBlock(it);
		}
	}
	return Promise.resolve(node);
}


function processImages (node) {
	console.log('processImages');
	{
		var imgs = node.querySelectorAll('img[src*="googleusercontent"], img[src*="ggpht"]');
		for (var i = 0, img; (img = imgs[i]); i++) {
			img.src = img.src.
				replace(/^http:/, 'https:').
				replace(/\/s\d+\//g, '/s2048/');
		}
	}
	{
		var imgs = node.querySelectorAll('img[src*="cdn-ak.f.st-hatena.com"]');
		for (var i = 0, img; (img = imgs[i]); i++) {
			img.src = img.src.
				replace(/^http:/, 'https:');
		}
	}
	{
		var imgs = node.querySelectorAll('img[src*="ecx.images-amazon.com"]');
		for (var i = 0, img; (img = imgs[i]); i++) {
			img.src = img.src.
				replace(/^http:\/\/ecx\.images-amazon\.com/, 'https://images-na.ssl-images-amazon.com');
		}
	}
	return Promise.resolve(node);
}


function processWidgets (node) {
	var promises = [];


	console.log('processWidgets');
	var iframes = node.querySelectorAll('iframe[src*="www.youtube.com"]');
	for (var i = 0, iframe; (iframe = iframes[i]); i++) {
		iframe.src = iframe.src.replace(/^http:/, 'https:');
	}


	var scripts = node.getElementsByTagName('script');
	for (var i = 0, it; (it = scripts[i]); i++) (function (it) {
		if (!it.src) return;
		if (it.src.match(new RegExp('https://gist.github.com/[^.]+?.js'))) {
			var promise = HTTPS.GET(it.src).
				then( (res) => {
					var written = '';
					vm.runInNewContext(res.body, {
						document : {
							write : function (str) {
								written += str;
							}
						}
					});
					var div = node.ownerDocument.createElement('div');
					div.innerHTML = written;
					div.className = 'gist-github-com-js';
					it.parentNode.replaceChild(div, it);
				}).
				catch( (e) => {
					console.log(e);
				});


			promises.push(promise);
		}
	})(it);


	return Promise.all(promises).then( () => {
		return node;
	});
}


function processMathJax (html) {
	console.log('processMathJax');
	if (!html.match(/\\\(|\$\$/)) {
		return Promise.resolve(html);
	}
	return new Promise( (resolve, reject) => {
		mjAPI.typeset({
			html: html,
			renderer: "SVG",
			inputs: ["TeX"],
			ex: 6,
			width: 40
		}, function (result) {
			console.log('typeset done');
			console.log(result);


			resolve(result.html);
		});
	});
}


function processMinify (html) {
	return Promise.resolve(minify(html, {
		html5: true,
		customAttrSurround: [
			[/\[%\s*(?:IF|UNLESS)\s+.+?\s*%\]/, /\[%\s*END\s*%\]/]
		],
		decodeEntities: true,
		collapseBooleanAttributes: true,
		collapseInlineTagWhitespace: true,
		collapseWhitespace: true,
		conservativeCollapse: true,
		preserveLineBreaks: false,
		minifyCSS: true,
		minifyJS: true,
		removeAttributeQuotes: true,
		removeOptionalTags: true,
		removeRedundantAttributes: true,
		removeScriptTypeAttributes: true,
		removeStyleLinkTypeAttributes: true,
		processConditionalComments: true,
		removeComments: true,
		sortAttributes: true,
		sortClassName: false,
		useShortDoctype: true
	}));
}
const port = process.env['PORT'] || 13370


http.createServer(function (req, res) {
	var html = '';
	var location = url.parse(req.url, true);
	req.on('readable', function () {
		var chunk = req.read();
		console.log('readable');
		if (chunk) html += chunk.toString('utf8');
	});
	req.on('end', function() {
		console.log('end');


		if (location.query.minifyOnly) {
			Promise.resolve(html).
				then(processMinify).
				then( (html) => {
					console.log('done');
					res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
					res.end(html);
				}).
				catch( (e) => {
					console.log(e);
					console.log(e.stack);
					res.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8'});
					res.end(html);
				});
		} else {
			Promise.resolve(html).
				then(processWithDOM).
				then(processWithString).
				then( (html) => {
					console.log('done');
					res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
					res.end(html);
				}).
				catch( (e) => {
					console.log(e);
					console.log(e.stack);
					res.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8'});
					res.end(html);
				});
		}
	});
}).listen(port, '127.0.0.1');
console.log('Server running at http://127.0.0.1:' + port);

デーモン利用側

利用側は単純に http リクエストを送っているだけです。ときどきパースエラーで失敗したりするので、そういう場合は元々の HTML をそのまま使うようにしています。

運用

  1. エントリ保存時にはてな記法→HTMLと上記のような処理をしてDBに保存する(キャッシュ)
  2. レンダリング結果をページキャッシュ

という2段階のキャッシュがあります。テンプレートを変更しただけの場合、レンダリング結果のキャッシュを作りなおします。これは冒頭の通り約13分程度かかります。このページ単位のキャッシュはエントリを保存すると関連するキャッシュが自動的に無効になるようになっています。この場合、次回アクセス時に再生成になります。

記法フォーマッターのコード変更や、ポストプロセス処理に変更を入れた場合には、エントリのリフォーマットが必要です。これは全てやると約10分ぐらいかかります。ただ一部エントリだけにリフォーマットをかけるようなこともできるようにしています。

  1. トップ
  2. tech
  3. ブログシステムの HTML 生成を効率化

Default と Dark テーマの違い。お分かりいただけるだろうか……?

よく見ると Default のほうは、リソースの Timeline に DOMContentLoaded の線(青い線)がない。

今まで、なぜ表示されないんだろう? と疑問だったけど、Dark にしないと表示されないとは…… (バグじゃないか)

  1. トップ
  2. tech
  3. Chrome の Developer Tools でテーマが Dark のときだけ分かる情報