Module: Wurk::Profiler

Defined in:
lib/wurk/profiler.rb

Overview

Job profiling (Sidekiq 8.0+, OSS). When a job is pushed with a profile option, the processor wraps perform in a Vernier capture; the resulting Firefox-profiler (gecko) JSON is gzipped and stored so the dashboard can hand it to https://profiler.firefox.com for flame-graph inspection.

Redis schema (spec §1.7, §19.8), field-for-field Sidekiq 8.1's Profiler:

profiles            ZSET   member = "<token>-<jid>", score = expiry epoch (float)
<token>-<jid>       HASH   started_at (int epoch), token (the job's
                         `profile` value), type (wrapped || class), jid,
                         elapsed (float seconds), size (bytes of data),
                         data (gzipped gecko JSON)

sid is not written here: it is the profile-store id the Web UI caches after its first upload, and a value in it makes the UI skip the upload.

Capture is a no-op unless the vernier gem is loaded — profiling is an opt-in, dev/staging tool, so vernier stays an optional dependency.

Constant Summary collapse

EXPIRY =
86_400
DEFAULT_OPTIONS =
{ mode: :wall }.freeze

Class Method Summary collapse

Class Method Details

.call(job_hash) ⇒ Object

Server-side hook called from Processor#dispatch. Returns the perform result. Only captures when the job opted in AND vernier is present — otherwise it's a plain yield. There is NO blanket rescue: the job's own exceptions (a normal failure, or JobRetry::Skip) must propagate untouched, and a failed job stores no profile (upstream behaviour). Only the storage step is made failure-safe (see #safe_store). No &block parameter: every job passes through here, and declaring one would reify the caller's block into a Proc even on the yield-straight- through path that opted-out jobs take.



40
41
42
43
44
# File 'lib/wurk/profiler.rb', line 40

def call(job_hash)
  return yield unless job_hash['profile'] && defined?(::Vernier)

  capture(job_hash) { yield } # rubocop:disable Style/ExplicitBlockArgument
end

.gunzip(bytes) ⇒ Object



75
76
77
78
# File 'lib/wurk/profiler.rb', line 75

def gunzip(bytes)
  require 'zlib'
  Zlib::GzipReader.new(StringIO.new(bytes)).read
end

.gzip(str) ⇒ Object



66
67
68
69
70
71
72
73
# File 'lib/wurk/profiler.rb', line 66

def gzip(str)
  require 'zlib'
  io = StringIO.new(+'', 'wb')
  gz = Zlib::GzipWriter.new(io)
  gz.write(str)
  gz.close
  io.string
end

.profile_key(token, jid) ⇒ Object



62
63
64
# File 'lib/wurk/profiler.rb', line 62

def profile_key(token, jid)
  "#{token}-#{jid}"
end

.store(jid:, type:, token:, gecko_json:, started_at:, elapsed:, pool: nil) ⇒ Object

Persists a profile. Extracted from capture so it is unit-testable without vernier: tests pass a ready gecko JSON blob.



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/wurk/profiler.rb', line 48

def store(jid:, type:, token:, gecko_json:, started_at:, elapsed:, pool: nil)
  key = profile_key(token, jid)
  gz = gzip(gecko_json)
  with_pool(pool) do |conn|
    conn.multi do |tx|
      tx.call('ZADD', Keys::PROFILES, ::Time.now.to_f + EXPIRY, key)
      tx.call('HSET', key, 'started_at', started_at.to_i, 'token', token, 'type', type, 'jid', jid,
              'elapsed', elapsed.to_f, 'data', gz, 'size', gz.bytesize)
      tx.call('EXPIRE', key, EXPIRY)
    end
  end
  key
end