Henriksen's two-shutdown-mode barbershop — one barber, exponential
arrivals, exponential service, run for N days. The canonical
GPSS Example #1 (GENERATE / SEIZE / ADVANCE / RELEASE / TERMINATE).
This is the reference model for the puck idiom; read it first.
Customers arrive at a one-chair barbershop with mean inter-arrival
15 minutes and mean service 12 minutes (utilization ≈ 0.8). The shop
opens at t=0 and closes at closing_time (default 480 min = 8 hours).
Two shutdown modes are simulated:
The interesting output is the difference: average sojourn, max queue length, and the overtime the barber works under graceful close.
GPSS reference: see docs/gpss-examples.md
example #1.
Two puck types share a ^Shop pointer:
Customer (barbershop.odin:97-173) — the
four-state machine (Arriving → Waiting → In_Service → Done)
described as the canonical pattern in modeling guide §7.Gen (barbershop.odin:198-226) — the
generator. Creates a Customer, schedules itself for the next
arrival, and either suspends (graceful) or just keeps advancing
(hard).Shop (barbershop.odin:56-71) holds:
barber: sim.Facility — the single resourcewait_list: sim.Set — the model's own FIFO queue (see below)svc_stream, svc — independent RNG stream + handle for servicecompleted, total_sojourn, q_area, q_last_*,
max_in_queueThe customer pool is a sim.Pool(Customer, 256) allocated in
run_day and destroyed via defer. Gen has its own RNG stream
(index_key=2) for inter-arrivals.
The wait list is shop.wait_list, not barber.waiters. The
barbershop bypasses sim.seize / sim.release and manipulates
barber.busy and barber.owner directly
(barbershop.odin:119-145). This is deliberate:
the model wants peek-then-seize fairness — when an arriving customer
finds the barber idle and the wait list empty, it cuts the line
and starts service in the same tick. Going through seize would
queue it on the Facility's internal waiters even when nobody else
is there, costing a needless reactivate. See modeling guide §4.2
for the gotcha this avoids in the general case, and §7 for the
state machine.
Two RNG streams, not one. svc_stream uses index_key=1,
ia_stream uses index_key=2, both seeded from the same model
seed. This means for any given seed, service times are reproducible
independent of how many customers have arrived — useful for
debugging a specific customer's behavior. See modeling guide §5.
Graceful close = eng.suspend on the generator, not end_time.
At the closing instant the generator stops scheduling new arrivals
but stays in the engine
(barbershop.odin:220-225). The engine continues
running until the process heap empties — i.e., until the last
customer terminates. This is the GPSS / SLX equivalent of
wait until (clock ≥ close and queue empty and barber idle) with
no special predicate needed; quiescence detection is built into
the engine loop. See modeling guide §8.
flush_queue is called at every queue change, not on a timer.
The area-under-curve accumulator
(barbershop.odin:74-86) is updated whenever a
customer joins or leaves the wait list, never on a periodic sample.
This is exact, not approximate. See modeling guide §9.
sim.seize / sim.release and the Facility's built-in waitersWould shorten the customer tick by ~15 lines (no manual busy/owner
juggling, no separate wait_list Set, no flush_queue since the
Facility tracks waiters). The cost is losing the peek-then-seize
fast path: every arrival pays a queue-and-reactivate even when the
barber is idle and nobody is waiting. For a one-server shop this is
mostly aesthetic, but the same pattern in the clinic (multi-physician,
priority classes) becomes meaningful overhead, so we use the
explicit form here for consistency with what production models need.
GPSS would write this as one transaction flow inside GENERATE.
We could mirror that with a single self-cloning puck. We don't,
because separating the generator from the customer makes the close
condition (graceful vs hard) a property of the generator rather
than embedded in every customer's tick. It also makes the customer
struct reusable for variants that arrive via a different mechanism
(e.g., scheduled arrivals from a roster).
flush_queue as a Notify_VarIf wait_list.count were a Notify_Var, wait_until-style waiters
could trigger off it. We don't need that here — nothing waits on
queue length — and the area-under-curve update is cheaper inline.
This is the reference for:
Arriving / Waiting / In_Service / Done)Setflush_queueindex_key)pool_free + terminate on Doneseeds arrayWhen other docs reference any of these patterns, they should link back here.
./barbershop [options]
Add --json to emit the uniform envelope (metadata, execution_stats,
metrics, details) instead of the default text output.
| Flag | Type | Default | Description |
|---|---|---|---|
--closing-time |
float | 480 |
Shop closing time in minutes. |
--days |
int | 30 |
Number of days to simulate. |
--json |
bool | false |
Emit uniform JSON envelope instead of text. |
Example runs:
./barbershop # default text run
./barbershop --json # uniform envelope
./barbershop --days=10 --closing-time=600 --json # custom params, JSON output
odin run examples/barbershop # default 30 days
odin run examples/barbershop -- --days=10 # 10 days
odin run examples/barbershop -- --closing-time=600 # 10-hour shop
odin run examples/barbershop -- --json # JSON output
Default output is a console table comparing hard vs graceful close across the seed set. JSON mode is what the qsimhealth.com viewer ingests.
flush_queue)gated_shop — adds a Gate for shop
capacity; uses sim.seize/sim.release instead of the bypass
pattern (worth contrasting)pruning-status.md — pattern
catalogue and helper-candidate listdocs/an-introduction-to-slx-2v6r221zdt.pdf)
— section on the barbershop as the SLX vocabulary motivator