The Plan Gantt
The Plan Gantt: lifecycle hooks timestamp every task, then the run is rendered as an ASCII timeline - where your wall clock actually went. A diamond dependency graph with a tight concurrency limit makes the scheduler's decisions visible to the naked eye.
Scheduling & Concurrency
Round 3
Aaron Patterson
exit 0
bundle exec ruby examples/plan_gantt.rb
a real captured run
PLAN GANTT (concurrency 2, 414ms wall)
fetch:users |############ | 0-121ms
fetch:orders |#################### | 0-203ms
fetch:events |............######## | 0-203ms
join:revenue | ############### | 203-353ms
join:activity | ########## | 203-303ms
report:weekly | ###### | 353-413ms
|0 1 2 3 4 |
(one column = 10ms; '.' = queued for a slot, '#' = running)
serial floor 710ms -> actual 414ms (1.7x from the scheduler)
source
# frozen_string_literal: true # The Plan Gantt: lifecycle hooks timestamp every task, then the run is # rendered as an ASCII timeline - where your wall clock actually went. # A diamond dependency graph with a tight concurrency limit makes the # scheduler's decisions visible to the naked eye. # # bundle exec ruby examples/plan_gantt.rb [concurrency] # # Runs offline; task durations are simulated IO. require class="s">"bundler/setup" require class="s">"agentic" WORK = { class="s">"fetch:users" => {sleep: 0.12, deps: []}, class="s">"fetch:orders" => {sleep: 0.20, deps: []}, class="s">"fetch:events" => {sleep: 0.08, deps: []}, class="s">"join:activity" => {sleep: 0.10, deps: [class="s">"fetch:users", class="s">"fetch:events"]}, class="s">"join:revenue" => {sleep: 0.15, deps: [class="s">"fetch:users", class="s">"fetch:orders"]}, class="s">"report:weekly" => {sleep: 0.06, deps: [class="s">"join:activity", class="s">"join:revenue"]} }.freeze concurrency = (ARGV.first || 2).to_i timeline = {} plan_start = nil hooks = { before_task_execution: ->(task_id:, task:) { plan_start ||= Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) (timeline[task.description] ||= {})[class="y">:start] = Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) - plan_start }, task_slot_acquired: ->(task_id:, task:, waited:) { timeline[task.description][class="y">:running] = Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) - plan_start }, after_task_success: ->(task_id:, task:, result:, duration:) { timeline[task.description][class="y">:finish] = Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) - plan_start } } orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: concurrency, lifecycle_hooks: hooks) tasks = {} WORK.each do |name, spec| task = Agentic:class="y">:Task.new( description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"simulate"}, payload: spec[class="y">:sleep] ) tasks[name] = task orchestrator.add_task(task, spec[class="y">:deps].map { |d| tasks.fetch(d) }, agent: ->(t) { sleep(t.payload) class="y">:done }) end result = orchestrator.execute_plan # Render: 1 column = 10ms total = timeline.values.map { |t| t[class="y">:finish] }.max columns = (total * 100).ceil puts class="s">"PLAN GANTT (concurrency #{concurrency}, #{(result.execution_time * 1000).round}ms wall)" puts timeline.each do |name, t| from = (t[class="y">:start] * 100).round slot = ((t[class="y">:running] || t[class="y">:start]) * 100).round queued = [slot - from, 0].max width = [((t[class="y">:finish] - (t[class="y">:running] || t[class="y">:start])) * 100).round, 1].max bar = ((class="s">" " * from) + (class="s">"." * queued) + (class="s">"#" * width)).ljust(columns) puts format(class="s">" %-16s |%s| %3d-%3dms", name, bar, t[class="y">:start] * 1000, t[class="y">:finish] * 1000) end puts puts format(class="s">" %-16s |%s|", class="s">"", (0..columns).step(10).map { |c| (c / 10).to_s.ljust(10) }.join[0, columns + 1]) puts class="s">" (one column = 10ms; '.' = queued for a slot, '#' = running)" puts serial_floor = WORK.values.sum { |w| w[class="y">:sleep] } puts format(class="s">" serial floor %.0fms -> actual %.0fms (%.1fx from the scheduler)", serial_floor * 1000, result.execution_time * 1000, serial_floor / result.execution_time)