Shake のキャッシュが効いてなかった

下記の記事を覚えていましょうか?

kakkun61.hatenablog.com

Shake を使って静的ウェブサイト生成するようにしたことを書いた記事です。

ただ、どうも生成が遅いんです。しょっちゅうは更新しないので「まあいいか」と放置していたのですが、最近手を付けることがあったので改善してみました。

fsatrace はどう追跡してくれるのか

前方型1の Shake アクションの場合、「fsatrace で変更を追跡する」というようなことがリファレンスに書いてあり、「よく分からんけどそうなんか」ぐらいに思って使っていました。

hackage.haskell.org

実は fsatrace は動的リンクライブラリーのロード時にトレース付きのラッパーを噛ますツールで、外部実行バイナリーを実行したときにしか依存の追跡をしてくれないということが分かりました2

つまり、今回の場合は主たる処理を Hint で行なっているため、ほぼキャッシュがされていなかったのです!

そこで、何をキャッシュするべきかを明示しないといけません。

キャッシュするよう Shake に伝える

Shake に「これキャッシュしておいて」と指示するために cacheActionWith を使用しました。

cacheActionWith
  :: ( Typeable a, Binary a, Show a
     , Typeable b, Binary b, Show b
     , Typeable c, Binary c, Show c
     )
  => a -> b -> Action c -> Action c 

a が呼び出し箇所ごとに異なるキーの型で、b はその値がキャッシュ時と同じならキャッシュ済みの c の値を返す、c はキャッシュされる結果の型です。

今回は下記のようにしました。

lucid source destination param = do
  …
  hsContents <- Shake.forP hsFiles Shake.readFile' -- hsFiles はインタープリター内で読むファイル
  let hash = Shake.hash (hsContents, param) -- インタープリターで実行する処理の入力をまとめてハッシュ値を得る
  result <- Shake.cacheActionWith ("hint: " ++ source) hash $ Hint.runInterpreter $ do

hsContentsparam のどちらも変更されなければキャッシュされた値が result になります。

(今書いていて思ったけどハッシュ値を取らなくても cacheActionWith の第2引数にペアのタプルを渡せばいいかも。)

また、キャッシュの都合、result の型が Binaryインスタンスでなければならず、元々は関数を内部に持つ HtmlT だったため単純にはインスタンスにできず、インタープリター内で show までしてしまうことで対処しました。

結果、初回ビルドは

Build completed in 1m27s

無変更で2度目は

Build completed in 0.85s

やったね🎉


  1. Makefile のようなビルドツールが後方型。
  2. なので Go 製実行バイナリーだと追跡できない。