rcmdnk's blog
Last update

Amazon.co.jp: Speed Up (With Remixes) [Explicit]: Matt Harder: デジタルミュージック

Octopress(Jekyll)はビルドするのに物凄く時間がかかるので 特に記事数が増えてくると嫌になってしまって 他に移った、と言う話が最近良く見かける様になりました。

サイドバーのランダムリストのアップデート でさらに大幅に時間がかかるようになってしまったりして ちょっとどうしようかと思いましたが、 Octopressのgenerateタスクを大幅にスピードアップできたのでそれについて。

Sponsored Links

Octopressで時間がかかっている部分

以前、ちょっとOctopressのビルドが流石に時間かかって困るな、となってきた時、 どの辺が時間かかってるのか調べてみました。

RubyのBenchmarkを使ってOctopressのgenerateでどれ位時間がかかってるか測ってみる

Rubyのruby-profを使ってOctopressのgenerateでどれ位時間がかかってるか測ってみる

結局の所、実際にマークダウンをレンダリングしてさらにLiquidタグの変換とかをしているrender の部分に時間がかかっていて、この部分はJekyll内、さらには MarkdownのパーサーとLiquidの仕様の問題なのでどうしようもないかな、 という点がありました。

他のMarkdownで作るブログツールなんかだともっと高速なものがいくらでもあるので、 どちらかと言うとLiquidの方だと思います。

今回、 ランダムリストを埋め込んだ 上でもう一度測ってみました。

この前作ってた計測用のパッチがあれだったので 以下の様な感じのパッチを作って pluginsディレクトリに突っ込んで計測しています。

jekyll_patch.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
require "ruby-progressbar"

module Jekyll
  class Site

    @@space = 35

    def diff_time(name, t_pre, t_now)
      t_diff = t_now - t_pre
      sec = t_diff % 60
      min = ((t_diff - sec) % 3600) / 60
      hour = (t_diff - sec - (min * 60)) / 3600
      puts "%#{@@space}s: %02d:%02d:%02d" % [name, hour, min, sec]
    end

    def process
      exe = ['reset', 'read', 'generate', 'render', 'cleanup', 'write']
      t_now = Time.now
      exe.each { |e|
        send(e)
        t_pre = t_now
        t_now = Time.now
        diff_time(e, t_pre, t_now)
      }
    end

    def render
      if defined?(Octopress::Hooks) and defined?(old_render) # Updated on 2015/06/28
        site_hooks.each do |hook|
          hook.pre_render(self)
        end
      end

      relative_permalinks_deprecation_method

      collections.each do |label, collection|
        collection.docs.each do |document|
          document.output = Jekyll::Renderer.new(self, document).run
        end
      end

      payload = site_payload
      progressbar = ProgressBar.create(:title => "%#{@@space-5}s" % "render::posts-pages", :starting_at => 0,
                                       :total => [posts, pages].flatten.size,
                                       :format => '%t %a |%B| %p%')
      #Parallel.map([posts, pages].flatten, :in_threads => self.config['n_cores'] ? self.config['n_cores'] : 1) do |page_or_post|
      [posts, pages].flatten.each { |page_or_post|
        page_or_post.render(layouts, payload)
        progressbar.increment
      }
    end

    def generate
      t_now = Time.now
      generators.each do |generator|
        generator.generate(self)
        t_pre = t_now
        t_now = Time.now
        diff_time('generate::' + generator.class.name.demodulize, t_pre, t_now)
      end
    end
  end
end

追記: 2015/06/28

renderメソッドにパッチを宛てる場合には少し注意が必要で、 Octopressでは Octopress Hooks というツールを使ってJekyllのサイト生成コマンドの各段階への Hookを入れられる様な機能を追加しているのですが、 この中でrenderメソッドに手を加えています。

上の様にパッチを宛ててしまうと plugins側の方が後から読み込まれるので Octopress Hooksでの上書きが無視されHookが効かなくなります。

これを避けるために

if defined?(Octopress::Hooks) and defined?(old_render)

の部分を追加しました。

ただ、これもOctopress Hooksの中の上書き方法をトレースしてる形なので、 Octopress Hooksのアップデートにも注意して使う必要があります。

追記ここまで

これで測ってみると

## GEnerating Site with Jekyll
Configuration file: /pipeline/build/_config.yml
            Source: source
      Destination: ./public
     Generating...
                              reset: 00:00:00
                               read: 00:00:00
          generate::GoogleAnalytics: 00:00:03
             generate::ShareNumbers: 00:01:53
    generate::RelatedPostsGenerator: 00:00:00
          generate::GemojiGenerator: 00:00:00
             generate::GenerateTags: 00:00:13
       generate::GenerateCategories: 00:00:03
           generate::GenerateMontly: 00:00:05
             generate::PopularPosts: 00:00:00
            generate::JekyllSitemap: 00:00:00
               generate::Pagination: 00:00:00
            generate::JekyllVarToJs: 00:00:00
                           generate: 00:02:20
           render::posts-pages Time: 00:18:28 |===========================| 100%
                             render: 00:18:28
                            cleanup: 00:00:00
                              write: 00:00:00
                   done.

こんな感じになっています。 wercker で行った結果です。

ソーシャルボタンの数をビルド時に取ってくる の所で書いたとおり、 generate::ShareNumbers:の所は8つのスレッドで実行してこの時間です。 (シングルスレッドだとこれも15分以上かかったりします。。。)

やはり一番時間がかかっているのはrenderの部分で、 各ページを変換してる部分です。

ちなみにこの部分もParallelを使って行けるかと思ったんですが、

octopress/plugins/include_array.rb:44: warning: conflicting chdir during another chdir block
octopress/plugins/include_array.rb:44: warning: conflicting chdir during another chdir block

みたいなwarningが出てたので取り敢えずやめておきます。 (ちょっと注意すればなんとかなりそうなものですが。)

ランダムポストを入れる前はここが5分位になっていましたので 4倍位にはなってます。

前回書いた通りwerckerは1つのステップで60分まで使える様になったので まだその点では問題無いですが、 やはり毎回これだけかかると困りますし、 ローカルでは絶対ビルドしたくありません。

改善策

取り敢えずレンダリングしてる所が遅いわけですが、 JekyllやLiquidの仕様の中まで除いて色々するのはまだ手が出ないところなので もっと簡単に出来そうな事をやってみます。

ランダムリストを入れて時間がかかったのは、 各ページに全てのポストの一覧を埋め込もうとしたからです。

この一覧は全てのページに対して共通なので、これを 1回だけレンダリングして付け加えられないか、と考えます。

よく考えてみると現状サイドバーはページ固有の物は載ってないので、 サイドバー全体を別にレンダリングして最後に各ページに貼り付ける、みたいなことをしてみます。

まず、サイドバー用のページとして source/common/common_sidebar.htmlというファイルを

common_sidebar.html
1
2
3
4
5
6
7
8
---
layout: null
sitemap: false
---
{% capture root_url %}{{ site.root | strip_slash }}{% endcapture %}
<aside class="sidebar">
  {% include_array default_asides %}
</aside>

こんな感じで作ります。 普通のサイドバー部分の内容です。

追記: 2015/06/21

このままだとこの消すページもsitemap.xmlに載ってしまうので、 それを避けるためにYAMLブロックに 上の様にsitemap: falseを足しておきます1

追記ここまで

これで普通にjekyll buildするとサイドバー部分だけが public/common/common_sidebar.htmlに出来ます。

次に、埋め込む部分として、source/_layouts/post.html

post.html
1
2
3
4
5
6
7
8
9
10
11
12
13
 {% unless page.sidebar == false %}
+{% if site.post_asides.size %}
 <aside class="sidebar">
-  {% if site.post_asides.size %}
     {% include_array post_asides %}
-  {% else %}
-    {% include_array default_asides %}
-  {% endif %}
 </aside>
+{% else %}
+COMMON__SIDEBAR
 {% endunless %}
-

という感じに変更します。

ここでdefault_asidesを使ってる部分を先ほどの common_sidebar.htmlに移して そこをCOMMON__SIDEBARで置き換えた形になっています。 (上では_&#95;として書いてますが実際には_です。 これを直接書いてしまうとこの部分すらもサイドバーに変更されて大変なことになるので。 以下も同様に。)

ページの方も同様に

page.html
1
2
3
4
5
6
7
8
9
10
11
12
13
 {% unless page.sidebar == false %}
+{% if site.page_asides.size %}
 <aside class="sidebar">
-  {% if site.post_asides.size %}
     {% include_array post_asides %}
-  {% else %}
-    {% include_array default_asides %}
-  {% endif %}
 </aside>
+{% else %}
+COMMON&__SIDEBAR
 {% endunless %}
-

もし、ページとポストで別々のサイドバーを使ってたら それぞれcommon_page_sidebar.htmlとか作って 同様にpage.htmlとかに埋め込んでください。

これでjekyll buildを下直後はサイドバーは現れず、 COMMON__SIDEBARという文字列だけが出ます。

実際にこのブログではCOMMON__*ではなくてCOMMON_*と アンダーバー一個の変数を使ってますが、 1つの物をここに書くとその部分にもサイドバーが埋め込まれてしまうので ここでは2つの物を例としてます。

同じような事をヘッダーに関しても行います。

common_header.html
1
2
3
4
5
6
---
layout: null
---
{% capture root_url %}{{ site.root | strip_slash }}{% endcapture %}
<header role="banner">{% include header.html %}</header>
<nav role="navigation">{% include navigation.html %}</nav>

こんな感じの共通パーツを用意して、 source/_layouts/default.html

default.html
1
2
3
-  <header role="banner">{% include header.html %}</header>
-  <nav role="navigation">{% include navigation.html %}</nav>
+  COMMON__HEADER

こんな感じで変更。

次に、Rakefileに以下の様なタスクを加えます。

Rakefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Replace common words
desc "Replace common words"
task :common do
  puts "## Replace common words"
  Parallel.map(Dir.glob("#{public_dir}/**/*.html"), :in_threads => n_cores) do |h|
    if h.start_with?("#{common_dir}")
      next
    end
    html = IO.read(h)
    common_words.each do |c|
      replace = IO.read("#{common_dir}/#{c.downcase}.html")
      html.gsub!(c, replace)
    end
    File.open(h , 'w') do |f|
      f.write html
    end
  end
  rm_rf common_dir
end

さらにRakefileの先頭の方に

common_words    = ["COMMON__HEADER", "COMMON__SIDEBAR"]
common_dir      = "#{public_dir}/common"

と言う行を加えます。

これでgenerateタスクのjekyll buildの後に

  system "jekyll build"
  Rake::Task[:common].execute

とやってあげると全部置き換えてくれます。

これで実際ビルドしてみると

           render::posts-pages Time: 00:01:15 |===========================| 100%
                             render: 00:01:15

と、一分ちょっとになりました!

commmonタスク自体は450ページほどあっても1秒もかからず終わります。

これはランダムポストを入れる前でも5分位かかってたので 普通の使い方をしていても ヘッダーとサイドバーが共通なものであれば 五分の一位までgenerate時間を短縮出来る事になります。

1分程度であれば手元で行ってもまあ許せる程度でもあります。

ということで原理的にこういったやり方でOctopressのビルドを劇的に スピードアップできます。

今回はサイドバーとヘッダーだけでしたが、 他にもまとめられる物は特に中でLiquidタグを多用している部分はまとめたら速くなります。

その他の改善案

タグを減らす(特にinclude)

Octopress (Liquid)のfor文内等でタグを使う時の注意 で見ましたが、どうもJekyll(というかLiquid)の仕様として、 includeタグで他のファイルを埋め込むとき、 全てを埋め込んでからそのページをレンダリングするのではなく、 includeまで行くと、その埋め込むファイルを開いてそのファイルをレンダリングして、 さらにその中にincludeがあればそれをまた開いてレンダリングして。。。 みたいな事をやってるみたいなので、 includeの入れ子のしすぎは遅くなりそうです。(きちんと見てないので勝手な予測ですが)

ただ、少なくともLiquidタグの負担は結構大きいみたいなのでなるべく減らした方が良く、 source/_includes内の整理をすることでもっと速くすることは出来そうです。

posts-pagesでParalleslを使う

関係ない別のページをレンダリングしている際には並列で走らせてもLiquidタグの変換自体は大丈夫そうなもんですが、 上でParallelposts-pagesの所で使えないことを書きました。

どうもinclude_array.rbの中でDir.chdirしてる所があるので ここで引っかかってるみたいですが、このプラグインはOctopressの物なので なんとかなりそうな感じはあります。

追記: 2015/06/18

plugins/include_array.rbをこんな感じで変えてやることで 上で出てたwarningを出さないように出来ました。

include_array.rb
1
2
3
4
5
6
7
8
9
-        Dir.chdir(includes_dir) do
-          choices = Dir['**/*']
-          if choices.include?(file)
-            source = File.read(file)
+        file_inc = "#{includes_dir}/#{file}"
+        if File.directory?(includes_dir)
+          choices = Dir["#{includes_dir}/**/*"]
+          if choices.include?(file_inc)
+            source = File.read(file_inc)

ただ、手元の環境だとこれで上手く行くのですが、 werckerだと

  Liquid Exception: lexical error: invalid string in json text. n ID."} 00000000000000000000000 (right here) ------^ in _posts/y_2013/2013-03-07-setup-octopress.md/#excerpt | 0%
  Liquid Exception: Pygments can't parse unknown language: html. in _posts/y_2013/2013-03-10-pages.md/#excerpt
/pipeline/build/plugins/pygments_code.rb:27:in `rescue in pygments': Pygments can't parse unknown language: html. (RuntimeError) 
...

みたいなエラーが出ました。 ちょっと手元の環境との違いをきちんとみて調べてみます。。。

追記ここまで

COMMONをJekyllの中に埋め込む

今回COMMON__SIDEBARみたいな単なる文字列の置き換えは 上にも書き換えましたがこの文字列を実際他の所で一切書けなくなる、 という問題があるのでちょっと考えないといけません。

なるべく使わないような長い名前にする、というのも一つの手ですが余りすまーとではないです。

Octopressで’ダブルハイフン’をそのまま残す の追記に書きましたが、 OctopressではJekyllの各段階の前後にタスクを挟み込む octopress/hooks というツールを用意しているので、 これを使ってrenderの後に上手く入れ込める様な事が出来ないかな、とも。

それもタグを上手いこと使って他と区別できるような感じで。

まとめ

まあまだ色々と改善の余地はありそうですが、 取り敢えずCOMMONな差し込みだけでも劇的に速くなったので大分満足。

取り敢えずこれで手元でもちょっと気になったら全部をビルドしてみる、 ということも気軽にできる様になりました。

追記: 2015/07/02

さらに改善してプラグイン化しました。

octopress-common-partでOctopressを高速化

追記ここまで

Sponsored Links
Sponsored Links

« サイドバーのランダムリストのアップデート Related postsをLinkwithinから自作版にする »