kenju's blog

About Programming, Mathematics, Security and Blockchain

rubyzip と zip(1) のメモリ消費量を比較検討した

rubyzipgemが、ある特定のJobサーバーでメモリ消費量的に利用できるかどうかを計測したかったため、Profilerを書いた。比較対象として、Linuxコマンドのzip(1)と比較した。また、単純にzipしようとするファイルをRubyでシンプルに扱う場合どれくらいのメモリを消費するかも比較するために、単純にファイルを読み込んで配列に入れる処理のProfileも計測している。

github.com

結果

zip(1)のメモリ消費量が一番効率が良いが、rubyzipの実装も最適化されている模様。 したがって、以下のメモリ消費量がJobサーバー的に許容範囲であれば、rubyzipを使用するという判断が可能である。

# Summary

[setup fixtures  ]===================================
Generated 8978 number of fixtures
The directory size of 'tmp' is... : 527M

[read files      ]===================================
elapsed=0:00.50 CPU=94% RSS=559808kb
elapsed=0:00.46 CPU=95% RSS=559784kb
elapsed=0:00.44 CPU=97% RSS=559812kb

[zip with rubyzip]===================================
elapsed=0:16.79 CPU=94% RSS=172784kb
elapsed=0:17.34 CPU=96% RSS=183048kb
elapsed=0:17.47 CPU=95% RSS=181800kb

[zip with zip(1) ]====================================
elapsed=0:13.95 CPU=99% RSS=11176kb
elapsed=0:13.79 CPU=98% RSS=11072kb
elapsed=0:13.84 CPU=99% RSS=11000kb

補足

rubyzipライブラジの内部実装を見てみると、zipエントリーに書き出しする際に、それぞれのエントリーをTempfileで最適化していることがわかる。したがって、ファイル単体をそのままメモリに載せているわけではないため、read filesのプロファイル結果よりも良い結果が出ている。

github.com

module Zip
  class StreamableStream < DelegateClass(Entry) # nodoc:all
    def initialize(entry)
      ...
      # Tempfileを利用している
      @temp_file = Tempfile.new(::File.basename(name), dirname)
      @temp_file.binmode
    end

    def get_output_stream
      if block_given?
        begin
          # temp_file に一旦書き込んでいる
          yield(@temp_file)
        ensure
          @temp_file.close
        end
      else
        @temp_file
      end
    end
    ...
  end
end

# Copyright (C) 2002, 2003 Thomas Sondergaard
# rubyzip is free software; you can redistribute it and/or
# modify it under the terms of the ruby license.

参考

上記のGitHub repoに全ソースコードは載せてあるが、ざっくりと↓のようなコードで比較している。

#!/usr/bin/env ruby

require "zip"

class Profiler
  ZIP_SRC = "tmp"
  ZIP_OUT = "build/tmp.zip"
  ZIP_FIXTURE = "fixtures/cat.jpg"

  # FIXTURE_THRESHOLD = 10 * 1024 * 1024 # 10MB
  FIXTURE_THRESHOLD = 500 * 1024 * 1024 # 500MB

  # Setup fixtures
  # by copying one image file to ZIP_OUT 
  # until the whole file size reaches thre FIXTURE_THRESHOLD
  def setup_fixtures
    counter = 0
    filesize_accum = 0

    while filesize_accum < FIXTURE_THRESHOLD
      FileUtils.copy_file(ZIP_FIXTURE, "#{ZIP_SRC}/#{File.basename(ZIP_FIXTURE)}-#{counter}")
      filesize_accum += File.size(ZIP_FIXTURE)
      counter += 1
    end
    puts "Generated #{counter} number of fixtures"
  end

  def cleanup
    FileUtils.remove(ZIP_OUT) if File.file?(ZIP_OUT)
    FileUtils.remove_dir(ZIP_SRC) if File.file?(ZIP_SRC)
  end

  def read_files
    contents = []
    Dir.glob("#{ZIP_SRC}/**/*") do |filepath|
      contents << File.read(filepath)
    end
  end

  def zip_with_zip
    # zip all files in tmp/ to build/tmp.zip
    system("zip --quiet #{ZIP_OUT} #{ZIP_SRC}/*")
  end

  def zip_with_rubyzip
    Zip::File.open(ZIP_OUT, Zip::File::CREATE) do |zipfile|
      # iterate all files in tmp/
      Dir.glob("#{ZIP_SRC}/**/*") do |filepath|
        # and write the file contents into the zipfile stream
        zipfile.get_output_stream(filepath) { |os| os.write File.read(filepath) }
      end
    end
  end
end
Raw