Back to Claude Scientific Skills

Monitoring, tracing, and stepping

skills/simpy/references/monitoring.md

2.55.08.1 KB
Original Source

Monitoring, tracing, and stepping

Verified 2026-07-23 against SimPy 4.1.2.

Monitoring is model instrumentation, not an automatic statistical analysis. Define:

  1. the estimand/state (queue waiting only, users in service, total in system);
  2. the observation instant (before request, after request, grant, release, final);
  3. the aggregation rule (time average, entity average, count, quantile);
  4. the analysis window and warm-up;
  5. memory/trace caps.

Prefer explicit domain observations

Record domain events where their meaning is unambiguous:

python
def customer(env, resource, log):
    arrival = env.now
    log.append({"event": "arrival", "time": env.now})
    with resource.request() as request:
        yield request
        log.append(
            {
                "event": "service_start",
                "queue_wait": env.now - arrival,
                "time": env.now,
            }
        )
        yield env.timeout(2)
    log.append({"event": "departure", "time": env.now})

Entity-average wait and time-average queue length are different estimands.

Why queue samples are timing-sensitive

For Resource.request():

  • before the method: the new request is absent;
  • immediately after: an available slot may already be allocated, or the request is in queue;
  • when the request Event is processed: waiting Process callbacks run;
  • during release: a queued request may be granted synchronously before the release Event's callbacks finish.

Several states may therefore exist at the same simulation timestamp. Label sample phase. Equal-time samples contribute zero duration to a time integral but affect an unweighted sample average.

len(resource.queue) counts pending requests, not users in service. Total number in the congestion point is normally resource.count + len(resource.queue).

For Container/Store, distinguish level/len(items) from pending put_queue/get_queue.

Time-weighted state

For a left-continuous piecewise-constant state q(t), compute:

average = sum(q_i * (t_{i+1} - t_i)) / (end - start).

Requirements:

  • initial sample at monitoring start;
  • every relevant state transition;
  • final sample exactly at the reporting horizon;
  • warm-up window clipping;
  • nondecreasing timestamps.

Do not use sum(queue_samples) / len(queue_samples) as time-average queue length; busy periods typically generate more events and become overrepresented.

Bundled ResourceMonitor

scripts/resource_monitor.py wraps one Resource-like instance, records request, grant, cancellation, release, and final states, and calculates time-weighted utilization/queue length.

python
import simpy
from resource_monitor import ResourceMonitor

env = simpy.Environment()
resource = simpy.Resource(env, capacity=2)
monitor = ResourceMonitor(env, resource, "server")

# Register bounded processes, then run.
env.run(until=100)
monitor.finalize(at=100)
summary = monitor.summary(start=20, end=100)

The warm-up sample state is reconstructed from the last transition at or before start; the final sample closes the interval. export_csv() writes local private CSV atomically and refuses overwrite unless requested.

Monkey-patching changes method identity and can interact with other wrappers. Attach one monitor per instance, patch before processes obtain method references, and call detach() before another instrumentation layer.

Generic resource wrappers

The official guide demonstrates pre/post method wrappers:

python
from functools import wraps

def patch_resource(resource, pre=None, post=None):
    def wrap(operation):
        @wraps(operation)
        def wrapper(*args, **kwargs):
            if pre is not None:
                pre(resource)
            event = operation(*args, **kwargs)
            if post is not None:
                post(resource)
            return event
        return wrapper

    for name in ("put", "get", "request", "release"):
        if hasattr(resource, name):
            setattr(resource, name, wrap(getattr(resource, name)))

Here "post" means after the method call, not necessarily after the returned Event is processed. To observe completion, append a callback while event.callbacks is still a list. Handle immediately triggered events before the environment steps.

Subclassing can be clearer for one stable use case, but it still depends on protected _env in common examples. Prefer an explicit env reference.

Event tracing

The official guide identifies:

  • Environment.schedule() — event enters the queue;
  • Environment.step() — next queued event is processed.

The bundled EventTraceRecorder wraps step(), reads the next queue tuple, and records only:

  • simulation time;
  • priority;
  • event ID;
  • event class name;
  • queue size before the step.

It avoids repr(event), which can contain nondeterministic memory addresses. It caps records and writes JSON Lines.

python
from resource_monitor import EventTraceRecorder

trace = EventTraceRecorder(env, max_records=10_000)
env.run(until=100)
trace.detach()
trace.export_jsonl("trace.jsonl")

This intentionally accesses env._queue, a private implementation detail. Pin SimPy and regression-test the tuple shape after upgrades. Full tracing increases runtime and memory; use a small deterministic diagnostic scenario.

Summarize without executing model code:

bash
python skills/simpy/scripts/event_trace_summary.py trace.jsonl
python skills/simpy/scripts/event_trace_summary.py resource_samples.csv

The summarizer validates fixed schemas, file size, record count, numeric finiteness, and ordering.

Manual stepping

Stepping is useful for debuggers, GUI integration, invariants, and hard event caps:

python
from simpy.core import EmptySchedule

max_events = 100_000
processed = 0
while env.peek() < 100 and processed < max_events:
    env.step()
    processed += 1

if processed == max_events:
    raise RuntimeError("event budget exhausted")
if env.peek() == float("inf"):
    # Empty schedule; verify intended completion instead of assuming success.
    pass

env.peek() < horizon gives the same half-open boundary policy as numeric run(until=horizon). <= processes events at the boundary and is a different estimand/termination convention.

If a stop callback fires during step() in 4.1.2, SimPy may reschedule the Event to preserve remaining callbacks. See events.md.

Periodic polling

Polling is simple but approximates the state path and adds events:

python
def poll(env, resource, interval, end, samples):
    while env.now < end:
        samples.append((env.now, resource.count, len(resource.queue)))
        delay = min(interval, end - env.now)
        if delay <= 0:
            return
        yield env.timeout(delay)

Never use a zero/negative interval. Polling can miss short peaks. It is suitable for visualization at a declared resolution, not exact time integrals.

Measurement windows and censoring

At a finite horizon:

  • an entity may arrive but remain queued;
  • service may start but not finish;
  • a future timeout may remain scheduled;
  • numeric run(until=horizon) excludes ordinary events exactly at the horizon.

Report arrived, admitted, rejected, completed, and unfinished counts. A completed-only customer mean can be biased when long waits/services are more likely to be unfinished. Consider a terminating design that drains the system, a right-censoring-aware estimand, or sensitivity to a longer horizon.

For steady-state replication/deletion, define whether an observation enters the analysis by arrival time, service-start time, departure time, or time-integral window. Do not choose after seeing favorable results.

Monitor non-interference tests

For a deterministic miniature model, compare monitored and unmonitored runs:

  • same completion order and timestamps;
  • same resource counts and outputs;
  • same random draws/seed manifest;
  • no pending monitor bookkeeping after completion;
  • identical exception/interrupt behavior.

Also test:

  • simultaneous request/release;
  • cancellation while queued;
  • preemption;
  • zero queue capacity;
  • final interval closure;
  • warm-up starting between transitions;
  • trace-cap behavior.

Sources

See sources.md for the official monitoring, environment, time/scheduling, and tagged core source links.