The Allocation Audit
The Allocation Audit: every object is a promissory note the GC collects on later. This audit counts exactly what each framework operation allocates (GC.stat's total_allocated_objects is an exact counter, not a sample) and where the GC actually runs during a plan. Latency spikes that "come from nowhere" come from here.
Observability & Ops
Round 11
Koichi Sasada (ko1)
exit 0
bundle exec ruby examples/allocation_audit.rb
a real captured run
ALLOCATION AUDIT (objects per operation, exact via GC.stat)
Task.new 18 objects
validator: happy path 37 objects
validator: rejection 448 objects ########
graph snapshot (10 tasks) 381 objects #######
to_json_schema 29 objects
...graph, isolated 96 objects (snapshot alone, build subtracted)
a full 10-task plan allocates 1592 objects and triggered 0 GC run(s).
reading the audit like a VM person: the happy-path validation
(37 objects) is what you multiply by requests-per-second -
37 x 1000 rps is 37000 promissory notes a second, and the GC
collects on schedule whether you budgeted or not. rejection costs
12x the happy path in objects (exceptions carry backtraces;
error paths are allocation paths), and the graph snapshot's
96 objects of dup+freeze buy the immutability every round-8
tool leans on - that's not waste, that's a purchase. allocation
isn't evil; UNBUDGETED allocation is. now there's a budget.
source
# frozen_string_literal: true # The Allocation Audit: every object is a promissory note the GC # collects on later. This audit counts exactly what each framework # operation allocates (GC.stat's total_allocated_objects is an exact # counter, not a sample) and where the GC actually runs during a # plan. Latency spikes that "come from nowhere" come from here. # # bundle exec ruby examples/allocation_audit.rb # # Runs offline; counts are exact for this Ruby version. require class="s">"bundler/setup" require class="s">"agentic" Agentic.logger.level = class="y">:fatal def allocations(iterations = 100) yield # warm: first call pays memoization, schema compilation, caches GC.start before = GC.stat(class="y">:total_allocated_objects) iterations.times { yield } (GC.stat(class="y">:total_allocated_objects) - before) / iterations end def task_named(name) Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => class="s">"w", class="s">"instructions" => class="s">"work"}) end SPEC = Agentic:class="y">:CapabilitySpecification.new( name: class="s">"audit", description: class="s">"x", version: class="s">"1.0.0", inputs: {mode: {type: class="s">"string", required: true, enum: %w[a b]}, weight: {type: class="s">"number", required: true, min: 1, max: 100}}, rules: {fits: {relation: class="y">:sum_lte, fields: [class="y">:weight], limit: 100}} ) VALIDATOR = Agentic:class="y">:CapabilityValidator.new(SPEC) def ten_task_orchestrator orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 4) previous = nil 10.times do |i| task = task_named(class="s">"t#{i}") orchestrator.add_task(task, previous ? [previous] : [], agent: ->(_t) { class="y">:ok }) previous = task end orchestrator end puts class="s">"ALLOCATION AUDIT (objects per operation, exact via GC.stat)" puts rows = { class="s">"Task.new" => -> { task_named(class="s">"x") }, class="s">"validator: happy path" => -> { VALIDATOR.validate_inputs!(mode: class="s">"a", weight: 50) }, class="s">"validator: rejection" => -> { begin VALIDATOR.validate_inputs!(mode: class="s">"z", weight: 500) rescue Agentic:class="y">:Errors:class="y">:ValidationError nil end }, class="s">"graph snapshot (10 tasks)" => -> { ten_task_orchestrator.graph }, class="s">"to_json_schema" => -> { SPEC.to_json_schema } } counts = rows.transform_values { |op| allocations(&op) } counts.each do |label, objects| puts format(class="s">" %-28s %6d objects %s", label, objects, class="s">"#" * [objects / 50, 40].min) end # The graph row includes building the orchestrator - separate the two build_only = allocations { ten_task_orchestrator } graph_only = counts[class="s">"graph snapshot (10 tasks)"] - build_only puts format(class="s">" %-28s %6d objects (snapshot alone, build subtracted)", class="s">" ...graph, isolated", graph_only) # --- where the GC actually fires during a plan --------------------------------- orchestrator = ten_task_orchestrator GC.start gc_before = GC.count allocated_before = GC.stat(class="y">:total_allocated_objects) orchestrator.execute_plan plan_allocations = GC.stat(class="y">:total_allocated_objects) - allocated_before gc_runs = GC.count - gc_before puts puts format(class="s">" a full 10-task plan allocates %d objects and triggered %d GC run(s).", plan_allocations, gc_runs) puts per_call = counts[class="s">"validator: happy path"] puts class="s">" reading the audit like a VM person: the happy-path validation" puts class="s">" (#{per_call} objects) is what you multiply by requests-per-second -" puts class="s">" #{per_call} x 1000 rps is #{per_call * 1000} promissory notes a second, and the GC" puts class="s">" collects on schedule whether you budgeted or not. rejection costs" puts class="s">" #{counts["validator: rejectionclass="s">"] / per_call}x the happy path in objects (exceptions carry backtraces;" puts class="s">" error paths are allocation paths), and the graph snapshot's" puts class="s">" #{graph_only} objects of dup+freeze buy the immutability every round-8" puts class="s">" tool leans on - that's not waste, that's a purchase. allocation" puts class="s">" isn't evil; UNBUDGETED allocation is. now there's a budget."