h2o は mruby ハンドラで link ヘッダを使って push を指示すると、バックエンドへの問合せと非同期で静的ファイルを push してくれます。

もしバックエンドアプリケーションで link ヘッダを吐いて push する場合、バックエンドアプリケーションの処理が終わったあとから push が始まることになるので、アプリケーションの実行時間分、push できる時間を失うことになります。

server-push の指示をテンプレートに書きたい病

自分はプリロード指示をバックエンド側のテンプレートに書きたい病にかかっており、現状で以下のようなテンプレートコードを書いて、バックエンドから preload ヘッダを吐いています。

r.preload() は link ヘッダを追加するメソッドになっており、これを実際に読みこんでいるHTMLの部分の近くに置くことでリソース管理を簡略化しています。

しかし、これだとバックエンドから h2o へ server-push を指示する形になるので、前述のようにアプリケーションの実行時間分、push できる時間を無駄にします。

バックエンドから server-push するリストをあらかじめ取得する

できれば無駄をなくしたいので、やはり h2o の mruby ハンドラでも link ヘッダを吐くことにします。

ただ、二重に設定を書きたくはないので、バックエンドの吐く link ヘッダを、h2o 起動時に取得しておき、以降のリクエストではそれを元に server-push させるようにします。

まずバックエンドのヘッダをファイルに保存しておきます。ここでは適当に curl を使ってます。

curl -s --head -H 'Cache-Control: no-cache' https://lowreal.net > /srv/www/lowreal.net.link.txt

そして、起動時に read して push する分の link ヘッダを作っておき、ハンドラでそれを送るように設定します。

        mruby.handler: |
          LINK = File.read("/srv/www/lowreal.net.link.txt").
            split(/\r?\n/).select{|l| l.sub!(/^link: /, "") and l.match(/rel=preload/) and !l.match(/nopush/) }.
            join("\n")


          $stderr.puts LINK.inspect


          lambda do |env|
            [ 399, { "link" => LINK }, [] ]
         end

運用上は、適当なタイミング(デプロイタイミング)で curl を打って再度ヘッダを保存して h2o を再起動するようにします。

余談ですが curl を使わずとも mruby ハンドラ内で使える http_request() があるので h2o で完結しそうと思いきや、起動中のコンテキストでは使えないみたいです。

効果

mruby ハンドラで server-push を指示しない場合

mruby ハンドラで server-push を指示する場合

見るべきは、各リソースの responseEnd の時間と、 / に対する requestStart の時間の差です。ただ requestStart はいずれも 1ms 程度なので、注目するのは responseEnd だけで良さそうです。

mruby ハンドラで指示しない場合各リソースの responseEnd は100ms以上になっています。これはバックエンドの処理に約100msほどかかっているからです。一方 mruby ハンドラで指示する場合、responseEnd は 22ms ぐらいになっています。

この例だと、静的ファイルの responseEnd から、バックエンドのレスポンス開始までまだ時間があるので、もっと静的ファイルを server-push する余地がありそうです。

('cache-control: no-cache' をつけているのはバックエンド側のキャッシュを無効にして処理時間を作って差をわかりやすくしているだけです)

  1. トップ
  2. tech
  3. h2o での server-push タイミングの最適化

h2o の casper (cache-aware server-push) を有効にしていると、force reload したときでも push されなくなってしまって、だんだん混乱してきます。YAML を一時的に変えて再起動したりしていたのですが、自分以外にも影響が及ぶのでちょっとなんとかしました。

JS で h2o_casper クッキーを削除してからリロードする

最初に思いつく方法で手軽なやつです。以下のようなブックマークレットでリロードすると cookie がない状態からのリロードになります。

javascript:document.cookie="h2o_casper=; max-age=-1; path=/";location.reload(true);

h2o へパッチをあてて、force reload 時は常に h2o_casper を無視してプッシュさせる

force reload 時にブラウザはリクエストヘッダに Cache-Control: no-cache をつけるので (全てのブラウザかどうかわかりませんが)、その場合にはクッキーを無視して push します (set-cookie は吐かれます)

Cache-Control: no-cache とクライアントが宣言しているなら、casper も無効になっているのは正当ではないか?と思い実装しましたが、ほんとにそうか自信がないので、ひとまず自分のところでテストしています (このサーバには適用済み)。

diff --git a/lib/http2/connection.c b/lib/http2/connection.c
index 4395728..bc83829 100644
--- a/lib/http2/connection.c
+++ b/lib/http2/connection.c
@@ -1185,6 +1185,19 @@ static void push_path(h2o_req_t *src_req, const char *abspath, size_t abspath_le
                 src_stream->pull.casper_state = H2O_HTTP2_STREAM_CASPER_DISABLED;
                 return;
             }
+
+            /* disable casper (always push) when super-reloaded (cache-control is exactly matched to no-cache) */
+            if ( (header_index = h2o_find_header(&src_stream->req.headers, H2O_TOKEN_CACHE_CONTROL, -1)) != -1) {
+                h2o_header_t *header = src_stream->req.headers.entries + header_index;
+                if (h2o_lcstris(header->value.base, header->value.len, H2O_STRLIT("no-cache"))) {
+                    /* casper enabled for this request but ignore cookie */
+                    if (conn->casper == NULL)
+                        h2o_http2_conn_init_casper(conn, src_stream->req.hostconf->http2.casper.capacity_bits);
+                    src_stream->pull.casper_state = H2O_HTTP2_STREAM_CASPER_READY;
+                    break;
+                }
+            }
+
             /* casper enabled for this request */
             if (conn->casper == NULL)
                 h2o_http2_conn_init_casper(conn, src_stream->req.hostconf->http2.casper.capacity_bits);

mruby.handler でなんとかできないかと思いましたが、mruby 側の env に渡ってくる HTTP_COOKIE とかを書きかえても h2o 内部の処理には影響しないみたいなので無理そうでした。

  1. トップ
  2. tech
  3. h2o の casper を一時的に無効にする

すこし昔書いたGoogle Keepのメモを発掘してきました。

NASの維持コスト

電気料金 を30円/kWh、NAS の消費電力を 20W で計算すると、446円/月。24時間動かすので思いのほか電気料金がかかっていることになり、初期コストも含めて考えると、オンラインサービスと案外競合する。

NAS の特徴は

  • 良:データが手元にある安心感
  • 良:LANで完結するので速度が早い
  • 悪:災害によってデータ消失が起こる
  • 悪:自分でメンテする必要があり、故障が起こると思いがけずコストがかかる

オンラインサービスの維持コスト

オンラインサービスの特徴

  • 良:災害があってもデータが失われない
  • 良:自分でメンテナンスする必要がない
  • 悪:大量のデータの取り出しが難しいことがある
  • 悪:ネットワーク経由でしか入出力できないことがあるので速度が遅い
  • 悪:サービスが終了することがある

Google Drive

エンドユーザ向けサービスだが値下げによりかなり安価になっている。
1TBプランが月額$9.99、30Tまではほぼ$0.01/GBとなる。Nearline とほぼ同じだが、容量を使いきっていなくてもプラン容量分課金される点で異なる。
取り出しは当然無料だが速度の保証はない

Dropbox

エンドユーザ向けのサービス。有料プランは1TBのみで、月額1200円。1TB 以上のプランがないので、それ以上になる予測がたつ場合選択肢にあがらない。

Amazon Clould Drive

エンドユーザ向けサービス。有料プランの最大が1TB、1TBのとき年額40000円。月額換算で3333円とかなり高い。

ただし、プライム会員(3900円/年)の場合、写真データ(RAWも含む)が無制限となっており写真だけ上げるなら異常に安い。しかしこの手の無制限サービスはすぐ終了する予測しかない。

Amazon Glacier

約1円/GB
取り出しコストが大変複雑。月あたり使用容量の5%までを1時間あたりに分散してゆっくり取り出せば (約0.0067%/時) 無料。つまり取り出しソフトウェアがちゃんとしていれば、20ヶ月で常に無料で全量を取り出し可能

Google Cloud Storage Nearline

約1円/GB ($0.01/GB)
取り出しも$0.01/GBと書いてあるが、実際はこれに加えて転送量課金がある。$0.12/GB

なので、1TB ダウンロードしようとすると16000円ぐらいかかる。純粋に従量課金なので Glacier より料金はわかりやすいが、無料の範囲というのは存在しないので、ダウンロードでは必ずこの金額必要になる。

エンドユーザ的には価格で Google Drive に対するメリットがない。

  1. トップ
  2. tech
  3. 大量のファイルのバックアップにNASを使うのは適切なのか?

Firefox だと上記のようにリクエストが消滅してタイミングも全てない表示になるみたいです。

Chrome の Network タブだとプッシュしてもキャッシュからひいてくる時間を表示しているのか区別できなくてモヤっとします。

ref. [tech] lowreal.net のHTTP2/HTTPS 化を実施 | Tue, Apr 5. 2016 - 氾濫原

  1. トップ
  2. tech
  3. Firefox の開発者ツールのほうが HTTP2 でサーバプッシュされたコンテンツがわかりやすい

JSなしのソーシャルボタンというのを作ってみました。このサイトの各エントリ下部に実装されているものです。

動機

各サービス、JS を読みこんでボタンを表示するタイプのものをメインに提供していますが、ソーシャルサービスへのシェアという機能で外部リソースの読み込みとJSの実行が発生するのは、提供される機能に対して割に合わないのではないかと思っていました。

実際ウェブサイトのパフォーマンスチューニングをしていると、細かいボタンのJSのダウンロード・パース・実行・表示後のリフローが結構多くて気になります。

実装

サービス

  • Facebook
  • Twitter
  • Google+
  • LINE
  • はてなブックマーク

HTML+CSS+各サービスのアイコン画像(5つ)です。

各サービスのアイコンをSVGにしたかったのですが、各サービスのブランドガイドラインを読んでいると面倒になったのでオフィシャルなものを使っています。オフィシャルにSVG版が提供されていれば悩まないんですが、はてなブックマークしか提供していないようでした。

使っている画像はオフィシャルのものですが、さらに optipng や svgo をかけてあります (一部の画像にしか効きませんでしたが)。

LINE it! はスマフォかつアプリが入ってないと機能しません。JS版のボタンはスマフォの場合だけ出すような判定も入っているようです。テストページでは出しわけをしていませんが、このサイト内では画面サイズが小さいときだけ出るようにしています。

  1. トップ
  2. tech
  3. JavaScript の必要ないソーシャルボタン

国産原料、国内生産のグリシン1kg 【3gぴったり計量スプーン付き】 -

4.0 / 5.0

毎日寝る前に飲んでみてる。そんな期待してなかったけど、寝起きは確かに多少良くなった。元々ものすごく寝起きが悪いので少しマシになった程度だと思う。

ただし、短い睡眠時間を補うようなものではないので、睡眠時間が短いとあまり変わらずただただ眠い。どうしようもない。

https://buffer.com

buffer というのをちょっと使ってみようとしています。予め Facebook / Twtter などを接続して、スケジュールを設定しておくと、buffer 経由で投稿したときにいい感じにマルチポストするというツールです。

スケジュールの設定がおもしろくて「このサービスはこの時間帯に投稿すると効果がたかいぞ!」みたいなことをサジェストしてくれて、それを時刻に設定できます。

buffer への投稿自体は chrome 拡張経由で手動でやってみています。

使い続けるかはよくわかりませんが、現在 Twitter とか Facebook を見ないようにしているので、buffer のような別サービスを挟んでおけば、うっかり見ることも減って良さそうです。

  1. トップ
  2. tech
  3. ソーシャルサービスに一括でして予約投稿してくれる buffer

そういえば なんとなく思いたったので Twitter 使うのをやめてみ… | Mon, Feb 15. 2016 - 氾濫原 というエントリを書いてから2ヶ月ぐらい経ってました。意外と見ないと決めてしまえば見ないものです。

最初の一ヶ月ぐらい twitter.com, www.facebook.com, b.hatena.ne.jp を /etc/hosts を使ってブロックしていましたが、twitter.com と www.facebook.com はそんなことしなくても案外見ないのでブロックするのをやめました。b.hatena.ne.jp はうっかり見たい欲求にかられることがあるので、PCによってはブロックしたままにしてあります。

あたり前ですが Twitter や Facebook 由来のストレス(けまらしい感・バカにされている感)と時間の浪費はなくなったので、その点はいい感じです。代わりにそれらよりは多少生産的なこと (日記とか) に時間を消費している気がします。


精神の安定のため、内在的かつアウトプットを増やそうというのが自然にできるようになるといいなと思っています。

  • モチベーションは内在し、無限に沸いてくる
    • 必要なのは沸いてくるものを守ることだけ
  • 幸福感は内在する
  • 「アウトプットした」という事実が幸福感を生むはずだ
  • 昨日より少しだけでも進歩することを繰替えすことが、のちに振り替えったときに幸福感を生むはずだ

誰かに褒められれば嬉しいのは確かだけれども、それは直接次のモチベーションには繋がらないし、「誰かに褒められる」ことよりも「誰かに貶される」という期待値のほうが圧倒的に高いので、そういう場は精神を不安定にするだけで割に合わないという判断にまとめています。

あんまりスター付かないので気付いてなかったのですが、Chrome 拡張の「はてなのお知らせ」とかに通知がこなくなっていることに気付きました。

おそらく「HTTPS にしたこと」というより、http: から https: に URL が代わったことにより Hatena.Star.Token の更新が必要なんだと思います。が、s.hatena.ne.jp/cho45/blogs にログイン状態でアクセスすると現状タイムアウトしてしまうので、詰んでいます。

HatenaStar.js を読んでて気付いたのですが、

Hatena.Star.Token = null;

がベタに書いてあるため、async と併用するとそもそも Token が初期化されてしまうようでした……

HTTPS とか関係なかったです。HTTP ではてなスターに登録していても、リダイレクトしているなら、リダイレクト先の Token を読んで判断するようです。

しかしHTTPSにしたことによって過去のスターが消えてそうなことに気付いたので、どうしようか考えています。

結局にっちもさっちも行かないことがわかったので、HatenaStar.js のコピーを編集して使うようにしました。

はてなスターに渡すURLは http: に戻しました。HTTPS になってからついたスターが表示されなくなってしまいますが (申し分けないのですが)、HTTP のときについたスターは復活するはずです……

まぁそもそも、そろそろはてなスター止めてもいいかもしれないんですが、もうちょっと頑張ってみようという感じです。

--- HatenaStar.orig.js	2016-04-15 23:11:20.355944766 +0900
+++ HatenaStar.js	2016-04-15 23:11:34.687944255 +0900
@@ -4655,7 +4655,39 @@
 
 
 /* start */
-new Hatena.Star.WindowObserver();
+// new Hatena.Star.WindowObserver();
+
+Hatena.Star.Token = '7743b0e60f0e3b267f9723d3a5cf96981a59e4f3';
+Hatena.Star.EntryLoader.loadEntries = function (node) {
+    console.log('custom EntryLoader');
+    var entries = [];
+    var entryNodes = node.getElementsByTagName('article');
+    for (var i = 0, entryNode; (entryNode = entryNodes[i]); i++) {
+        var uri = entryNode.querySelector('a.bookmark').href || '';
+        var title = entryNode.querySelector('h1').innerText;
+        var container = entryNode.querySelector('.social .hatena-star');
+
+        var sc = Hatena.Star.EntryLoader.createStarContainer();
+        container.appendChild(sc);
+        var cc = Hatena.Star.EntryLoader.createCommentContainer();
+        container.appendChild(cc);
+
+        entries.push({
+            uri: uri.replace(/^https:/, 'http:'),
+            title: title,
+            star_container: sc,
+            comment_container: cc
+        });
+    }
+
+    console.log('custom EntryLoader loaded', entries);
+
+    return entries;
+};
+
+window.addEventListener('load', function () {
+    new Hatena.Star.EntryLoader();
+});
 
 /* Hatena.Star.SiteConfig */
 /* sample configuration for Hatena Diary */

[tech] JavaScript の必要ないソーシャルボタン | Fri, Apr 15. 2016 - 氾濫原 これを作るとき、最初のうちは全てSVGにするぞと意気込んでいて、Ligature Symbols に含まれるものをSVGに変換したらいいのではないかと、いろいろ試していました。

結局その方法はやめたのですが、SVG フォントから、個別の SVG ファイルに変換するスクリプトを雑に書いたので残しておきます。SVG フォント全体だとファイルサイズが大きすぎるので、必要なファイルだけ普通の SVG 画像として抽出するということです。

以下のように perl + XML::LibXML で書きました。グリフ名を引数に与えると、該当するグリフを個別の .svg に書き出します。LigatureSymbols でしか試していませんが、SVG フォントなら他のでもいけるかもしれません。

#!/usr/bin/env perl

use utf8;
use strict;
use warnings;
use v5.10.0;
use lib lib => glob 'modules/*/lib';

use XML::LibXML;

open(my $fh, "<", "LigatureSymbols-2.11.svg") or die "cannot open < input.txt: $!";
my $font = do { local $/; scalar <$fh> };
close $fh;

my $doc = XML::LibXML->load_xml( string => $font, load_ext_dtd => 0 );
my $xpc = XML::LibXML::XPathContext->new($doc);

# get copyright metadata
my $original_metadata = $xpc->findvalue('/svg/metadata');
my $units_per_em = $xpc->findvalue('/svg/defs/font/font-face/@units-per-em');
my $ascent = $xpc->findvalue('/svg/defs/font/font-face/@ascent');
my $bbox = $xpc->findvalue('/svg/defs/font/font-face/@bbox');

for my $glyph_name (@ARGV) {
	my $glyph = $xpc->findnodes(sprintf('/svg/defs/font/glyph[@glyph-name="%s"]', $glyph_name))->[0];
	my $horiz_adv_x = $xpc->findvalue('./@horiz-adv-x', $glyph);

	my $document = XML::LibXML::Document->new('1.0', 'UTF-8');
	my $svg = $document->createElement('svg');
	$svg->setAttribute('width',  $horiz_adv_x);
	$svg->setAttribute('height', $units_per_em);
	# $svg->setAttribute('viewBox', $bbox);
	$svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg');
	$document->setDocumentElement($svg);

	my $metadata = $document->createElement('metadata');
	$metadata->appendChild($document->createTextNode($original_metadata));
	$svg->appendChild($metadata);

	my $path = $document->createElement('path');
	$path->setAttribute('transform', sprintf("scale(1, -1) translate(0,%s)", -$ascent));
	$path->setAttribute('fill', '#fff');
	$path->setAttribute('d', $xpc->findvalue('./@d', $glyph));
	$svg->appendChild($path);

	warn "write to $glyph_name.svg";
	say $document->toString(1) ;

	open(my $fh, ">", "$glyph_name.svg");
	print $fh $document->toString;
	close $fh;
}
  1. トップ
  2. tech
  3. SVGフォントのグリフを個別のSVG画像に変換する