buildrにrfscを組み込んでみた

あまりにコンパイルが遅くなってしまったので、buildrにrfsc
http://blog.8-p.info/2010/2-fsc-ruby
を組み込んでみた。
結論としては、fsc + compile引数が重要。
引数無しだとfscのキャッシュが全然効いていなかった。

default target

> buildr
Completed in 26.049s

ソースとテストのコンパイルとテスト実行

fsc有効に(fscサーバ起動済み)

> buildr
Completed in 24.011s

(scalaコンパイラの初期化節約 - fscクライアント起動) x 2 = 2秒 ? 減少

rfscを有効に(fscサーバ起動済み)

> buildr
Completed in 22.362s

fscクライアント起動 x 2 = 2秒 ? 減少

compile target

> buildr compile
Completed in 13.560s

テストのビルドがなくなって半分になる

compile target & fsc(fscサーバ起動済み)

> buildr compile
Completed in 10.987s

> buildr compile
Completed in 2.882s

2回目以降fscのキャッシュが効いて爆速になるみたいだ。
キャッシュが効く条件は、同じコマンドラインが投げられることと思われる。そのため、テストが含まれていると交互に違うコマンドラインが投げられて毎回キャッシュが捨てられるのではないかと。sbtのccはこの条件をクリアしていると思う。

compile target & rfsc(fscサーバ起動済み)

> buildr compile
Completed in 10.153s

> buildr compile
Completed in 2.253s

さらにfscクライアントの起動を節約できる。


ということで26秒から2秒に減りました。
ちゃんと、compile引数を渡していないことでfscのキャッシュ破壊が起きているのが遅さの最大の原因だったとは。
となると、fscを改造してコマンドラインをキーにして複数のキャッシュを持てる富豪仕様にするとよさげです。ソースとテストの両方をコンパイルしてもそこそこの早さでできそうだ。

buildr改造

compile.scalaへのモンキーパッチ

gems\buildr-1.4.6\lib\buildr\scala.rb

  require 'buildr/scala/compiler'
+ require 'buildr/scala/fsc'
  require 'buildr/scala/tests'

gems\buildr-1.4.6\lib\buildr\scala\fsc.rb

require 'socket'
require 'pathname'

module Buildr::Scala

  class Scalac

    def compile(sources, target, dependencies) #:nodoc:
      check_options(options, OPTIONS + (Scala.version?(2.8) ? [:make] : []))

      java_sources = java_sources(sources)
      enable_dep_tracing = Scala.version?(2.8) && java_sources.empty?

      dependencies.unshift target if enable_dep_tracing

      cmd_args = []
      cmd_args << '-classpath' << dependencies.join(File::PATH_SEPARATOR)
      source_paths = sources.select { |source| File.directory?(source) }
      cmd_args << '-sourcepath' << source_paths.join(File::PATH_SEPARATOR) unless source_paths.empty?
      cmd_args << '-d' << File.expand_path(target)
      cmd_args += scalac_args

      if enable_dep_tracing
        dep_dir = File.expand_path(target)
        Dir.mkdir dep_dir unless File.exists? dep_dir

        cmd_args << '-make:' + options[:make].to_s
        cmd_args << '-dependencyfile'
        cmd_args << File.expand_path('.scala-deps', dep_dir)
      end

      cmd_args += files_from_sources(sources)

      unless Buildr.application.options.dryrun

        if Scalac.use_fsc
          fsc(cmd_args) # この関数の改造点はここだけ
        else
          trace((['scalac'] + cmd_args).join(' '))
          Java.load
          begin
            Java.scala.tools.nsc.Main.process(cmd_args.to_java(Java.java.lang.String))
          rescue => e
            fail "Scala compiler crashed:\n#{e.inspect}"
          end
          fail 'Failed to compile, see errors above' if Java.scala.tools.nsc.Main.reporter.hasErrors
        end

        unless java_sources.empty?
          trace 'Compiling mixed Java/Scala sources'

          # TODO  includes scala-compiler.jar
          deps = dependencies + Scalac.dependencies + [ File.expand_path(target) ]
          @java.compile(java_sources, target, deps)
        end
      end
    end

    # 以下、fsc用追加関数
    def find_tmp_dir
      path=Pathname.new(`which scala`.chomp).dirname + '../var/scala-devel'
      if path.exist? then
        return path
      end
      path=Pathname.new(ENV['TEMP']+'/scala-devel/'+ENV['USERNAME'])
      if path.exist? then
        return path
      end
      throw "no port directory"
    end

    def find_port(dir)
      ports = dir.entries.map{|e|e.to_s}.grep(/^\d+$/)
      raise "Failed to find port file." if ports.empty?
      return ports[0], (dir + ports[0]).read.chomp
    end

    def open_socket
      port, password = find_port(find_tmp_dir + 'scalac-compile-server-port')
      socket = TCPSocket.open('localhost', port)
      socket.puts(password)
      return socket
    end

    def transform_argv(argv)
      dir = if argv.include?('-d')
              []
            else
              ['-d', '.']
            end

      (dir + argv).map do |i|
        case i
        when /^-/, /;/
          i
        else
          Pathname.new(i).realpath
        end
      end
    end

    def fsc(cmd_args)
      #cmd_args << "-verbose"
      argv = cmd_args
      begin
        socket = open_socket
        trace((['rfsc'] + cmd_args).join(' '))
        socket.puts argv.join("\0")
        print socket.read
        true
      rescue => e
        p e
        #cmd_args << "-JXmx512M"
        trace((['fsc'] + cmd_args).join(' '))
        system(([File.expand_path('bin/fsc', Scalac.scala_home)] + cmd_args).join(' ')) or
        fail 'Failed to compile, see errors above'
      end
    end

  end
end