Class: Ruby2D::Benchmark

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby2d/benchmark.rb

Overview

Lightweight benchmarking harness for Ruby 2D rendering performance.

Runs a scene with fps_cap: :infinity (vsync off, no frame cap) and records per-frame deltas plus GC activity over a timed window. After a warmup period the harness samples each frame's duration, then auto-closes and prints a frame-time histogram and GC summary.

Usage:

Ruby2D::Benchmark.run('My Scene', warmup: 10, duration: 5) do
# set up your scene / render block here
end

Defined Under Namespace

Modules: Clock, PresentTimer, RenderTimer

Constant Summary collapse

DEFAULTS =
{ warmup: 10, duration: 5 }.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, warmup: DEFAULTS[:warmup], duration: DEFAULTS[:duration]) ⇒ Benchmark

Returns a new instance of Benchmark.



64
65
66
67
68
# File 'lib/ruby2d/benchmark.rb', line 64

def initialize(name, warmup: DEFAULTS[:warmup], duration: DEFAULTS[:duration])
  @name     = name
  @warmup   = warmup
  @duration = duration
end

Instance Attribute Details

#durationObject (readonly)

Returns the value of attribute duration.



62
63
64
# File 'lib/ruby2d/benchmark.rb', line 62

def duration
  @duration
end

#nameObject (readonly)

Returns the value of attribute name.



62
63
64
# File 'lib/ruby2d/benchmark.rb', line 62

def name
  @name
end

#warmupObject (readonly)

Returns the value of attribute warmup.



62
63
64
# File 'lib/ruby2d/benchmark.rb', line 62

def warmup
  @warmup
end

Class Method Details

.run(name, **opts, &block) ⇒ Object



70
71
72
# File 'lib/ruby2d/benchmark.rb', line 70

def self.run(name, **opts, &block)
  new(name, **opts).run(&block)
end

Instance Method Details

#run(&setup) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/ruby2d/benchmark.rb', line 74

def run(&setup)
  frame_times   = []
  cpu_times     = []
  render_times  = []
  phase         = :warmup
  phase_start   = nil
  warmup_frames = 0
  measure_start = nil
  prev_t        = nil

  gc_alloc_start = nil
  gc_minor_start = nil
  gc_major_start = nil
  # mruby (the web build) has no `GC.stat`; skip the allocation section
  # there rather than fault. CRuby reports it as before.
  gc_available = GC.respond_to?(:stat)
  @reported = false

  w = DSL.window

  # Install the CPU/present split probes (idempotent — prepend is a no-op
  # if the module is already in the ancestor chain from a prior run).
  Ext.singleton_class.prepend(PresentTimer)
  w.singleton_class.prepend(RenderTimer)

  # Uncap frame rate so we measure raw throughput
  w.set(fps_cap: :infinity)

  # Let the caller configure the scene (add objects, render blocks, etc.)
  setup.call

  # Chain any update proc the setup block registered so per-frame
  # benchmark work runs alongside the harness phase logic.
  user_update = w.instance_variable_get(:@update_proc)

  w.update do
    # Monotonic clock (matching the engine): immune to wall-clock jumps that
    # would corrupt the frame-time histogram mid-measurement.
    now = Ext.now

    # Lazily initialise the phase timer on the first real frame —
    # this avoids counting window-creation overhead as warmup time.
    first_frame = phase_start.nil?
    if first_frame
      phase_start = now
      prev_t = now
    end

    delta = now - prev_t
    prev_t = now

    # Read + reset the split the wrappers accumulated since the previous
    # update tick — i.e. the frame that just completed, the same frame
    # `delta` spans. `cpu` is the frame minus its present wait; clamp away
    # sub-microsecond clock jitter that could make it momentarily negative.
    present = Clock.present_ns
    render  = Clock.render_ns
    Clock.present_ns = 0.0
    Clock.render_ns  = 0.0
    cpu = delta - present
    cpu = 0.0 if cpu.negative?

    # Run the caller's update proc, forwarding the frame delta if it takes
    # one (mirrors Window#update_callback's arity handling).
    if user_update
      user_update.arity.zero? ? user_update.call : user_update.call(delta)
    end

    next if first_frame  # exclude the first frame from phase timing

    case phase
    when :warmup
      warmup_frames += 1
      if now - phase_start >= @warmup
        phase = :measure
        measure_start = now
        if gc_available
          gc_alloc_start = GC.stat(:total_allocated_objects)
          gc_minor_start = GC.stat(:minor_gc_count)
          gc_major_start = GC.stat(:major_gc_count)
        end
      end
    when :measure
      frame_times  << delta
      cpu_times    << cpu
      render_times << render
      if now - measure_start >= @duration
        if gc_available
          @gc_alloc_delta = GC.stat(:total_allocated_objects) - gc_alloc_start
          @gc_minor_delta = GC.stat(:minor_gc_count) - gc_minor_start
          @gc_major_delta = GC.stat(:major_gc_count) - gc_major_start
        end
        # Print from inside the loop: on the web build `w.show` never
        # returns (emscripten unwinds the C stack, and `close` is a no-op
        # there), so a report after `show` would be dead code
        # there. The `@reported` guard keeps it to a single print.
        unless @reported
          report(frame_times, cpu_times, render_times, warmup_frames)
          @reported = true
        end
        w.close
      end
    end
  end

  w.show

  # CRuby returns here once the window closes; the web build never does
  # (it reported from inside the loop above). Report only if the loop
  # somehow exited without doing so.
  report(frame_times, cpu_times, render_times, warmup_frames) unless @reported
end