Skill Tracer
Skills › Scientific Agent Skills › simpy › monitoring.md

monitoring.md

268 lines · skills/simpy/references/monitoring.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Monitoring, tracing, and stepping
2
3Reviewed 2026-10-01 against SimPy 4.1.2.
4
5Monitoring is model instrumentation, not an automatic statistical analysis. Define:
6
71. the estimand/state (queue waiting only, users in service, total in system);
82. the observation instant (before request, after request, grant, release, final);
93. the aggregation rule (time average, entity average, count, quantile);
104. the analysis window and warm-up;
115. memory/trace caps.

Monitoring, tracing, and stepping

Reviewed 2026-10-01 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.
Raw lines 1 to 12
13## Prefer explicit domain observations
15Record domain events where their meaning is unambiguous:
17```python
18def customer(env, resource, log):
19 arrival = env.now
20 log.append({"event": "arrival", "time": env.now})
21 with resource.request() as request:
22 yield request
23 log.append(
24 {
25 "event": "service_start",
26 "queue_wait": env.now - arrival,
27 "time": env.now,
28 }
29 )
30 yield env.timeout(2)
31 log.append({"event": "departure", "time": env.now})
32```
34Entity-average wait and time-average queue length are different estimands.

Prefer explicit domain observations

Record domain events where their meaning is unambiguous:

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.

Raw lines 13 to 35
36## Why queue samples are timing-sensitive
38For `Resource.request()`:
40- before the method: the new request is absent;
41- immediately after: an available slot may already be allocated, or the request is
42 in `queue`;
43- when the request Event is processed: waiting Process callbacks run;
44- during release: a queued request may be granted synchronously before the release
45 Event's callbacks finish.
47Several states may therefore exist at the same simulation timestamp. Label sample
48phase. Equal-time samples contribute zero duration to a time integral but affect an
49unweighted sample average.
51`len(resource.queue)` counts pending requests, not users in service. Total number in
52the congestion point is normally `resource.count + len(resource.queue)`.
54For Container/Store, distinguish `level`/`len(items)` from pending
55`put_queue`/`get_queue`.

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.

Raw lines 36 to 56
57## Time-weighted state
59For a right-continuous, post-transition piecewise-constant state `q(t)`, compute:
61`average = sum(q_i * (t_{i+1} - t_i)) / (end - start)`.
63Requirements:
65- initial sample at monitoring start;
66- every relevant state transition;
67- final sample exactly at the reporting horizon;
68- warm-up window clipping;
69- nondecreasing timestamps.
71Do not use `sum(queue_samples) / len(queue_samples)` as time-average queue length;
72busy periods typically generate more events and become overrepresented.

Time-weighted state

For a right-continuous, post-transition 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.

Raw lines 57 to 73
74## Bundled ResourceMonitor
76`scripts/resource_monitor.py` wraps one Resource-like instance, records request,
77grant, cancellation, release, and final states, and calculates time-weighted
78utilization/queue length.
80```python
81import simpy
82from resource_monitor import ResourceMonitor
84env = simpy.Environment()
85resource = simpy.Resource(env, capacity=2)
86monitor = ResourceMonitor(env, resource, "server")
88# Register bounded processes, then run.
89env.run(until=100)
90monitor.finalize(at=100)
91summary = monitor.summary(start=20, end=100)
92```
94The warm-up sample state is reconstructed from the last transition at or before
95`start`; the final sample closes the interval. `export_csv()` writes local private
96CSV atomically and refuses overwrite unless requested.
98Monkey-patching changes method identity and can interact with other wrappers. Attach
99one monitor per instance, patch before processes obtain method references, and call
100`detach()` before another instrumentation layer.
102`summary(start=..., end=...)` clips the two time-weighted averages. Its request
103counts, mean wait, maximum queue, and final state describe the whole collected
104history; they are not filtered to that window. The library monitor accumulates
105samples in memory, so bound its environment and entity count too.

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.

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.

summary(start=..., end=...) clips the two time-weighted averages. Its request counts, mean wait, maximum queue, and final state describe the whole collected history; they are not filtered to that window. The library monitor accumulates samples in memory, so bound its environment and entity count too.

Raw lines 74 to 106
Runs code scripts/resource_monitor.py :76
107## Generic resource wrappers
109The official guide demonstrates pre/post method wrappers:
111```python
112from functools import wraps
114def patch_resource(resource, pre=None, post=None):
115 def wrap(operation):
116 @wraps(operation)
117 def wrapper(*args, **kwargs):
118 if pre is not None:
119 pre(resource)
120 event = operation(*args, **kwargs)
121 if post is not None:
122 post(resource)
123 return event
124 return wrapper
126 for name in ("put", "get", "request", "release"):
127 if hasattr(resource, name):
128 setattr(resource, name, wrap(getattr(resource, name)))
129```
131Here "post" means after the method call, not necessarily after the returned Event
132is processed. To observe completion, append a callback while `event.callbacks` is
133still a list. Handle immediately triggered events before the environment steps.
135Subclassing can be clearer for one stable use case, but it still depends on
136protected `_env` in common examples. Prefer an explicit `env` reference.

Generic resource wrappers

The official guide demonstrates pre/post method wrappers:

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.

Raw lines 107 to 137
138## Event tracing
140The official guide identifies:
142- `Environment.schedule()` — event enters the queue;
143- `Environment.step()` — next queued event is processed.
145The bundled `EventTraceRecorder` wraps `step()`, reads the next queue tuple, and
146records only:
148- simulation time;
149- priority;
150- event ID;
151- event class name;
152- queue size before the step.
154It avoids `repr(event)`, which can contain nondeterministic memory addresses. It
155caps records and writes JSON Lines.
157```python
158from resource_monitor import EventTraceRecorder
160trace = EventTraceRecorder(env, max_records=10_000)
161env.run(until=100)
162trace.detach()
163trace.export_jsonl("trace.jsonl")
164```
166This intentionally accesses `env._queue`, a private implementation detail. Pin
167SimPy and regression-test the tuple shape after upgrades. Full tracing increases
168runtime and memory; use a small deterministic diagnostic scenario.
170Summarize without executing model code:
172```bash
173python skills/simpy/scripts/event_trace_summary.py trace.jsonl
174python skills/simpy/scripts/event_trace_summary.py resource_samples.csv
175```
177The summarizer validates fixed schemas, file size, record count, numeric finiteness,
178and reports decreasing-time violations. It does not treat same-time decreases in
179priority/event ID as errors: callbacks can insert urgent events after normal events
180run. The trace describes step attempts, including the stopping event; an exception
181may interrupt that event's callbacks.

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.

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:

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 reports decreasing-time violations. It does not treat same-time decreases in priority/event ID as errors: callbacks can insert urgent events after normal events run. The trace describes step attempts, including the stopping event; an exception may interrupt that event's callbacks.

Raw lines 138 to 182
Runs code python skills/simpy/scripts/event_trace_summary.py :173
183## Manual stepping
185Stepping is useful for debuggers, GUI integration, invariants, and hard event caps:
187```python
188from simpy.core import EmptySchedule
190max_events = 100_000
191processed = 0
192while env.peek() < 100 and processed < max_events:
193 env.step()
194 processed += 1
196if processed == max_events and env.peek() < 100:
197 raise RuntimeError("event budget exhausted")
198if env.peek() == float("inf"):
199 # Empty schedule; verify intended completion instead of assuming success.
200 pass
201```
203`env.peek() < horizon` gives the same half-open boundary policy as numeric
204`run(until=horizon)`. `<=` processes events at the boundary and is a different
205estimand/termination convention.
207If a stop callback fires during `step()` in 4.1.2, SimPy may reschedule the Event to
208preserve remaining callbacks. See `events.md`.

Manual stepping

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

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 and env.peek() < 100:
    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.

Raw lines 183 to 209
210## Periodic polling
212Polling is simple but approximates the state path and adds events:
214```python
215def poll(env, resource, interval, end, samples):
216 while env.now < end:
217 samples.append((env.now, resource.count, len(resource.queue)))
218 delay = min(interval, end - env.now)
219 if delay <= 0:
220 return
221 yield env.timeout(delay)
222```
224Never use a zero/negative interval. Polling can miss short peaks. It is suitable
225for visualization at a declared resolution, not exact time integrals.

Periodic polling

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

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.

Raw lines 210 to 226
227## Measurement windows and censoring
229At a finite horizon:
231- an entity may arrive but remain queued;
232- service may start but not finish;
233- a future timeout may remain scheduled;
234- numeric `run(until=horizon)` excludes ordinary events exactly at the horizon.
236Report arrived, admitted, rejected, completed, and unfinished counts. A
237completed-only customer mean can be biased when long waits/services are more likely
238to be unfinished. Consider a terminating design that drains the system, a
239right-censoring-aware estimand, or sensitivity to a longer horizon.
241For steady-state replication/deletion, define whether an observation enters the
242analysis by arrival time, service-start time, departure time, or time-integral
243window. Do not choose after seeing favorable results.

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.

Raw lines 227 to 244
245## Monitor non-interference tests
247For a deterministic miniature model, compare monitored and unmonitored runs:
249- same completion order and timestamps;
250- same resource counts and outputs;
251- same random draws/seed manifest;
252- no pending monitor bookkeeping after completion;
253- identical exception/interrupt behavior.
255Also test:
257- simultaneous request/release;
258- cancellation while queued;
259- preemption;
260- zero queue capacity;
261- final interval closure;
262- warm-up starting between transitions;
263- trace-cap behavior.

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.
Raw lines 245 to 264
265## Sources
267See `sources.md` for the official monitoring, environment, time/scheduling, and
268tagged core source links.

Sources

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

Raw lines 265 to 268