ontocast.util.loop_lag¶
Event-loop lag sampling, for telling provider latency apart from CPU stalls.
A fan-out node that awaits N provider calls concurrently and a fan-out node that serialises N synchronous rdflib merges look identical in a wall-clock measurement: both take a long time and both report the same summed per-unit duration. They are not the same problem, and the fixes are opposite -- the first wants more concurrency, the second wants the CPU work hoisted out of the loop.
Awaited I/O yields control, so it produces zero lag no matter how slow the provider is. A synchronous block does not yield, so every task on the loop -- including the callbacks that would read the other units' sockets -- waits for it. Sampling how late a fixed-interval sleep actually wakes up therefore measures exactly the on-loop CPU stall and nothing else.
Usage::
async with loop_lag() as lag:
await run_the_fan_out()
tracker.add_duration(f"{node}/loop_lag_total", lag.total)
tracker.add_duration(f"{node}/loop_lag_max", lag.peak)
LoopLag
dataclass
¶
Accumulated event-loop delay observed while sampling.
Attributes:
| Name | Type | Description |
|---|---|---|
total |
float
|
Sum of all observed delays, in seconds. Roughly the time the loop spent unable to service ready callbacks. |
peak |
float
|
Longest single observed delay, in seconds. A peak above ~0.3s is an unambiguous fingerprint of one long synchronous block. |
samples |
int
|
Number of probes taken, for sanity-checking coverage. |
Source code in ontocast/util/loop_lag.py
loop_lag(interval=DEFAULT_SAMPLE_INTERVAL)
async
¶
Sample event-loop lag for the duration of the block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interval
|
float
|
Seconds between probes. |
DEFAULT_SAMPLE_INTERVAL
|
Yields:
| Name | Type | Description |
|---|---|---|
LoopLag |
AsyncIterator[LoopLag]
|
Live accumulator; read it after the block exits. |