The Require Cost Report
The Require Cost Report: `require` is a purchase - memory, objects, and boot time, paid again by every process you fork and every worker you scale. This measures what the gem and each major dependency cost AT REQUIRE TIME, each in a clean subprocess so nobody's cost gets billed to a neighbor's account.
Scheduling & Concurrency
Round 12
Richard Schneeman
exit 0
bundle exec ruby examples/require_cost.rb
a real captured run
REQUIRE COST REPORT (each row measured in a pristine child process) require RSS objects time json (stdlib) 0.2MB 3853 24ms zeitwerk 0.2MB 8955 30ms # async 3.1MB 37177 167ms ###### dry-schema 3.8MB 64274 142ms ######## agentic (require only) 0.4MB 13586 42ms # agentic + first real touch 6.7MB 103091 213ms ############# the bill, read like a Heroku support ticket - and it's a plot twist: `require "agentic"` costs 0.4MB / 42ms, nearly FREE, because Zeitwerk (the round-1 cleanup) defers every constant. the first real touch is where the bill lands: 6.7MB and 213ms, as async and dry-schema come in through the autoloader. deferred is not free - it's a bill that arrives during your first request instead of your boot, which is either exactly what you want (CLI tools, tiny scripts pay only for what they touch) or exactly what you don't (a web worker's first request eats the latency). the moves this report funds: eager_load in servers + preload_app (pay once, share copy-on-write), stay lazy in CLIs, and run this script in CI so a new dependency's bill arrives in the PR that adds it - not in the invoice at month's end.
source
# frozen_string_literal: true # The Require Cost Report: `require` is a purchase - memory, objects, # and boot time, paid again by every process you fork and every # worker you scale. This measures what the gem and each major # dependency cost AT REQUIRE TIME, each in a clean subprocess so # nobody's cost gets billed to a neighbor's account. # # bundle exec ruby examples/require_cost.rb # # Runs offline; each row is an isolated child process. require class="s">"open3" require class="s">"rbconfig" RUBY = RbConfig.ruby LIB = File.expand_path(class="s">"../lib", __dir__) # Measure inside a pristine child: RSS and allocated objects, before # and after the require - so each row is that require's WHOLE bill, # transitive dependencies included PROBE = <<~'RUBY' def rss_kb = File.read(class="s">"/proc/self/status")[/VmRSS:\s+(\d+)/, 1].to_i target, touch = ARGV objects_before = GC.stat(class="y">:total_allocated_objects) rss_before = rss_kb t0 = Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) require target eval(touch) if touch && !touch.empty? # standard:disable Security/Eval ms = (Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) - t0) * 1000 puts [rss_kb - rss_before, GC.stat(class="y">:total_allocated_objects) - objects_before, ms.round(1)].join(class="s">",") RUBY def cost_of(target, touch = class="s">"") out, status = Open3.capture2(RUBY, class="s">"-I", LIB, class="s">"-e", PROBE, target, touch) raise class="s">"probe failed for #{target}" unless status.success? rss_kb, objects, ms = out.strip.split(class="s">",") {rss_mb: rss_kb.to_f / 1024, objects: objects.to_i, ms: ms.to_f} end TARGETS = { class="s">"json (stdlib)" => [class="s">"json"], class="s">"zeitwerk" => [class="s">"zeitwerk"], class="s">"async" => [class="s">"async"], class="s">"dry-schema" => [class="s">"dry/schema"], class="s">"agentic (require only)" => [class="s">"agentic"], class="s">"agentic + first real touch" => [class="s">"agentic", class="s">"Agentic:class="y">:PlanOrchestrator.new; Agentic:class="y">:CapabilityValidator"] }.freeze puts class="s">"REQUIRE COST REPORT (each row measured in a pristine child process)" puts puts format(class="s">" %-28s %10s %14s %10s", class="s">"require", class="s">"RSS", class="s">"objects", class="s">"time") rows = TARGETS.transform_values { |target, touch| cost_of(target, touch || class="s">"") } rows.each do |name, cost| puts format(class="s">" %-28s %8.1fMB %14d %8.0fms %s", name, cost[class="y">:rss_mb], cost[class="y">:objects], cost[class="y">:ms], class="s">"#" * (cost[class="y">:rss_mb] * 2).round) end bare = rows[class="s">"agentic (require only)"] touched = rows[class="s">"agentic + first real touch"] puts puts class="s">" the bill, read like a Heroku support ticket - and it's a plot" puts format(class="s">" twist: `require \"agentic\class="s">"` costs %.1fMB / %dms, nearly FREE,", bare[class="y">:rss_mb], bare[class="y">:ms]) puts class="s">" because Zeitwerk (the round-1 cleanup) defers every constant." puts format(class="s">" the first real touch is where the bill lands: %.1fMB and %dms,", touched[class="y">:rss_mb], touched[class="y">:ms]) puts class="s">" as async and dry-schema come in through the autoloader. deferred" puts class="s">" is not free - it's a bill that arrives during your first" puts class="s">" request instead of your boot, which is either exactly what you" puts class="s">" want (CLI tools, tiny scripts pay only for what they touch) or" puts class="s">" exactly what you don't (a web worker's first request eats the" puts class="s">" latency). the moves this report funds: eager_load in servers +" puts class="s">" preload_app (pay once, share copy-on-write), stay lazy in CLIs," puts class="s">" and run this script in CI so a new dependency's bill arrives in" puts class="s">" the PR that adds it - not in the invoice at month's end."