lib/sidekiq/profiler.rb



require "fileutils"
require "sidekiq/component"

module Sidekiq
  # Allows the user to profile jobs running in production.
  # See details in the Profiling wiki page.
  class Profiler
    EXPIRY = 86400 # 1 day
    DEFAULT_OPTIONS = {
      mode: :wall
    }

    include Sidekiq::Component
    def initialize(config)
      @config = config
      @vernier_output_dir = ENV.fetch("VERNIER_OUTPUT_DIR") { Dir.tmpdir }
    end

    def call(job, &block)
      return yield unless job["profile"]

      token = job["profile"]
      type = job["class"]
      jid = job["jid"]
      started_at = Time.now

      rundata = {
        started_at: started_at.to_i,
        token: token,
        type: type,
        jid: jid,
        # .gz extension tells Vernier to compress the data
        filename: File.join(
          @vernier_output_dir,
          "#{token}-#{type}-#{jid}-#{started_at.strftime("%Y%m%d-%H%M%S")}.json.gz"
        )
      }
      profiler_options = profiler_options(job, rundata)

      require "vernier"
      begin
        a = Time.now
        rc = Vernier.profile(**profiler_options, &block)
        b = Time.now

        # Failed jobs will raise an exception on previous line and skip this
        # block. Only successful jobs will persist profile data to Redis.
        key = "#{token}-#{jid}"
        data = File.read(rundata[:filename])
        redis do |conn|
          conn.multi do |m|
            m.zadd("profiles", Time.now.to_f + EXPIRY, key)
            m.hset(key, rundata.merge(elapsed: (b - a), data: data, size: data.bytesize))
            m.expire(key, EXPIRY)
          end
        end
        rc
      ensure
        FileUtils.rm_f(rundata[:filename])
      end
    end

    private

    def profiler_options(job, rundata)
      profiler_options = (job["profiler_options"] || {}).transform_keys(&:to_sym)
      profiler_options[:mode] = profiler_options[:mode].to_sym if profiler_options[:mode]

      DEFAULT_OPTIONS.merge(profiler_options, {out: rundata[:filename]})
    end
  end
end