Repository navigation
Commit 4235a9c
Integrate steady state into report (#514)
* feat(core): announce each phase's shape with a PHASE_START event
`BenchmarkSession` publishes `session.phase_start` immediately before
`START_PERFORMANCE_TRACKING`, carrying `PhaseData` (phase type, whether
the phase drains, and the phase's turn/trajectory counts). Published for
every phase, not just performance ones.
`num_turns` is the super-pass size a steady-state consumer needs, and
`events.jsonl` consumers get the phase shape recorded for free -- until
now nothing on the wire said how large a dataset pass was.
`_publish_session_event` grows the payload argument rather than gaining a
second publish path, so PHASE_START keeps the publish-then-flush ordering
that makes these control signals reliable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(metrics): promote the steady-state detector into the package
`git mv` of the detector, its tests, and its operator reference into
`src/inference_endpoint/metrics/`, `tests/unit/metrics/`, and `docs/`.
It has to be importable before the metrics aggregator can feed it live.
No algorithm change. The PEP-723 script header and the `importlib`
loader shim in the tests go away; `--model`/`--tokenizer` resolution, the
CLI, and the JSON blob are untouched. `python -m
inference_endpoint.metrics.steady_state_diagnostics <run_dir>` replaces
`uv run scripts/steady_state_diagnostics.py <run_dir>`.
The `transformers` import moves to module scope, as the repo requires:
the CLI already imports it through `full_run_osl`, so it costs nothing.
mypy now sees the module, which caught one genuine type collision in
`main()` between the NATL and window result shapes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(metrics): narrow the steady-state result and add the missing metrics
Splits the algorithm from the event-log parse. `compute_steady_state_metrics`
is pure over a `SuperPassRollup` series -- warmup crop, plateau selection,
verdict -- so a second producer can feed it without re-reading and
re-tokenizing `events.jsonl`, and both reach the same verdict.
The result carries only what a report needs: the window (now with its
wall-clock bounds), TTFT/TPOT plus OSL and end-to-end latency, TPS with
its confidence intervals, the CoV of the metrics *inside* the reported
window, and the anomaly/trend flags. Flat at 4-8 KiB regardless of run
length.
Dropped from it: `trajectories`, `per_super_pass`, the rolling drift scan,
and the per-window-size CoV table -- 85-90% of the bytes and a third of
the compute, and none of it explains the verdict. `rolling_windows` and
`per_super_pass_diagnostics` go with them.
The standalone CLI composes the debug tables on top in `run()`: a
trailing-window CoV table per `--window-sizes` plus one whole-run drift
table (it was previously repeated identically per window size). The
window CoV now appears in the headline, where it belongs -- it is the
number that justifies the verdict, unlike the trailing table it joins.
`DEFAULT_COV_BOUNDS` and `DEFAULT_WINDOW_SIZES` give the library and the
CLI one grid, so a hand re-derived verdict is comparable to a live one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(metrics): collect the steady-state series inside the aggregator
`SuperPassCollector` becomes the one implementation of super-pass
bucketing. The metrics aggregator drives it live from the event router it
already has, and `build_super_pass_series` now drives the same object by
replaying an `events.jsonl`, so the two producers cannot drift: the
duplicate bucketing loop in the parse is gone.
`--steady-state-profile <name>` is the whole opt-in surface. The profile
carries the CoV bounds and warmup driver the verdict is judged on, and
only the parent knows the load pattern they follow from, so a hand re-run
of the standalone detector is judged the same way. Omitted = no
collection, and no boolean can contradict it.
The bucket size arrives on PHASE_START and is latched while the series is
still empty, so a phase announced after the performance one cannot
re-size a series already built.
Two ordering rules hold the TPOT path together, both pinned by tests that
were confirmed to fail when the order is swapped:
- `on_complete` runs *after* `set_field`, because `TpotTrigger` fires from
inside `set_field` and must read the sample's super-pass before
`on_complete` releases it.
- `TpotTrigger` reads that super-pass at fire time, not inside the
recorder closure: the token count arrives at the drain flush, long
after the row is gone. Carrying the index in the closure is also what
spares the collector a uuid map growing for the length of the run.
ISSUED is gated on `table.is_tracking` as well as the row. `set_field`
returns early for an ISSUED outside the tracking window *without*
dropping the in-flight row, so a sample re-issued after
STOP_PERFORMANCE_TRACKING still has one while the log parse skips it --
which would move `last_issue_ns` and give the same run two system TPS
figures. The equivalence test carries exactly that sample.
Buffering the parse's pending TPOT by batch position rather than by uuid
falls out of the shared implementation, and with it the `KeyError` a
sample completing twice inside one flush window used to raise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(metrics): carry the steady-state verdict on the final snapshot
`MetricsSnapshot` gains a trailing `steady_state` field, so no new
artifact file and no second write of `final_snapshot.json`: the
aggregator computes the verdict just before the terminal snapshot is
built, and the one atomic write carries it. The pub/sub frame carries it
for free, which is what a TUI would read.
Computed only for a run its series actually describes -- terminal state
COMPLETE, and every tokenization drained. An incomplete drain means TPOT
and OSL counts are missing.
The computation is wrapped, not the write: a detector failure yields
`steady_state=None` and the snapshot is written exactly as before. The
verdict is also the one snapshot field not built key by key, so
`snapshot_to_dict` scrubs it recursively -- a single non-finite float
inside it would make `json.dumps(..., allow_nan=False)` raise and cost
the run the file its Report is built from.
`Report.steady_state` reads it straight through and keeps it in
`to_json` (unlike `accuracy`), so it reaches `result_summary.json`,
`report.txt`, and the console. `display` prints the window, how long it
held, steady TPS, and the two warnings; the bulky parts stay in the JSON.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(config): make steady-state detection an opt-in run setting
`settings.steady_state.enabled` / `--steady-state`, off by default.
The field carries an explicit cyclopts `name=`, not an `alias=`: an alias
would also expose the field name, and a bare `--enabled` collides with
every other flattened single-field option model. Both `--steady-state`
and `--no-steady-state` parse, and neither adds an `--enabled`.
`steady_state_profile` in `pipeline.py` is the whole gate: enabled, not
accuracy-only, a tokenizer resolved, and a load pattern whose
`Profile.supported` accepts it. Eligibility is read from the detector's
own profile table, so the parent's gate and the profile the aggregator is
judged on cannot drift. It has to run before the run starts, because
collection happens as the run happens.
`profile_for_load_pattern` now fails closed -- an unclassified load
pattern resolves to no profile rather than inheriting `concurrency` --
and the `offline` profile is marked unsupported: its min-duration gate
measures the issue span, which collapses to ~0 when every query is
issued at t=0.
The standalone CLI takes the super-pass size off the wire
(`session.phase_start`) instead of a `run_meta.json` sidecar nothing
writes, so a hand re-run against a report directory needs no arguments
and cannot disagree with the size the run itself bucketed on. Verified
end to end: a live `--steady-state` run and a zero-flag re-run over its
own `events.jsonl` report the same verdict.
Audit phases turn it off (`commands/audit.py`): TEST04 issues one
repeated sample, which no steady-window verdict describes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(metrics): refuse steady-state collection for agentic runs
Agentic already resolves to an unsupported profile, so this is a second,
explicit check on each side of the process boundary. The parent's gate
refuses `LoadPatternType.AGENTIC_INFERENCE` by name, and the aggregator
refuses any profile whose `Profile.supported` is False -- agentic and
offline today. Neither side alone can now turn collection on for a workload
it does not describe.
An agentic run's steady-state metric is per-trajectory NATL, which this
collector does not produce. Its behaviour must be identical to a run before
steady state existed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(metrics): store super-pass samples in array("d")
The five per-sample series on `SuperPassRollup` were `list[float]`: a boxed
float plus a pointer for every sample. `array("d")` packs them at 8 bytes,
measured at 108 -> 34 bytes per sample across the five, so a 2M-sample run
retains ~70 MB in the collector instead of ~220 MB.
Nothing else moves: every reader needs only a Sequence -- append, len,
truthiness, `sorted`, `sum` -- and `array("d")` satisfies all of them. Only
the tests that compared a series against a list literal change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(metrics): drop the collector's duplicate uuid map
`SuperPassCollector` kept a `_PendingRow` per in-flight sample, mirroring
`SampleRow.issued_ns`/`recv_first_ns`: inside the aggregator the same sample
lived in two uuid maps for as long as it was in flight. `SampleRow` gains an
`sp_index` field instead, and the collector keeps only the series and the
rollup arithmetic -- callers pass the super-pass index and the timestamps
back in.
The rollup arithmetic stays in one place, which is what makes the live
collector and `build_super_pass_series` reach the same verdict; only the
in-flight row storage differs. The parse keeps its `_PendingRow`, since it
has no table to hang the index off.
`TokenTrigger._make_recorder` now takes the row, so `TpotTrigger` can read
`row.sp_index` at fire time -- the token count arrives at the drain flush, by
which point `set_field` has dropped the row from the table.
Two ordering rules move with it, both mutation-checked: the router reads the
row before `set_field` (COMPLETE drops it; RECV_FIRST overwrites
`recv_first_ns`, which is how a retry's second first-chunk is told apart).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(benchmark): keep the run deadline off the post-run drain
`drain_and_build_report` waited on the metrics services while `RunWatchdog`
was still armed -- `watchdog.cancel()` only ran in the outer `finally`. On a
`--timeout` run the deadline could fire during that wait, calling
`session.stop()` and setting `abort_event`, after which the teardown grace
expired and the services were killed mid-finalize.
That used to cost the run only the steady-state verdict, which was computed
after the snapshot was written. It now costs `final_snapshot.json` itself:
the verdict is computed before the single atomic write, so a kill in that
window leaves the parent falling back to the last live pub/sub snapshot --
non-terminal -- and a clean run reports as incomplete.
The watchdog is cancelled immediately before the drain. Everything past the
session's end is cleanup, not the run; `watchdog.fired` is untouched, so a
run that genuinely timed out still reports as timed out. The parent cannot
cancel between the drain and the analysis -- both happen inside the
subprocess and the wait is opaque from outside.
`wait_for_exit(None)` becomes `metrics_drain_timeout_s + 60s`, so removing
the deadline does not let a wedged aggregator hang the parent forever. The
grace covers the publisher's 10s ZMQ linger, the analysis (13.7s measured at
2M samples), the snapshot write and process exit. An unlimited drain budget
leaves the wait unbounded: bounding only the tail would buy nothing and could
kill a legitimately slow drain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(metrics): state the supported workloads as a table
Replaces the prose sentence listing what steady-state detection silently
skips with a table of load patterns and the reason each is or is not
judged, and says plainly that detection is best-effort and cannot cost a
run its result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(metrics): say why an explicit --steady-state was refused
The gate returned None for five different reasons without saying which,
so a user who asked for detection on the command line got nothing and no
explanation. It now warns with the reason. Opting out stays silent:
nothing was promised, so there is nothing to explain.
Moves the post-drain service-exit grace onto `Timeouts` beside the other
deadlines rather than leaving it a module constant in pipeline.py. It
carries `show=False`, so it is co-located without becoming an operator
knob.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(audit): lean on the steady-state default instead of restating it
Detection is off by default, so an audit phase needs a default-constructed
SteadyStateConfig rather than an explicit enabled=False.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(metrics): point steady-state policy at the rules repo
What counts as an official result is set by endpoints_policies §4.4, not
here. Links it and says the rules win on disagreement, so this document
can stay a description of the tool rather than a second copy of policy
that drifts from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(metrics): drop window sizes, report CoV over what was measured
The detector grows the maximal admissible plateau, so the fixed (4, 6, 8)
window grid never reached a verdict -- it only produced trailing-CoV debug
tables in the standalone CLI. Removes `--window-sizes`,
`DEFAULT_WINDOW_SIZES` and the per-size scan.
What those tables were for still matters: a run that found no window had
`cov: {}`, so the one case worth debugging carried no CoV at all. The
verdict now measures CoV over the longest *trend-steady* span instead,
and `cov_basis` says which span that was. Splitting the gate into its
trend and scatter halves makes that distinguishable, so a run rejected
for noise now says so:
no admissible steady plateau: the longest trend-steady span is 12
super-passes, but its CoV exceeds 0.08
rather than reporting only that no plateau was admissible. That reason
reaches the console and result_summary.json.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(metrics): answer the CoV question without searching for it
`largest_trend_steady_span` scanned every candidate window to find the
longest trend-steady span, and each probe rebuilt the per-super-pass
percentile trajectory from scratch -- re-sorting raw samples that do not
depend on the window -- then ran a pairwise trend test over it. Measured
on a drifting series at 100 samples per super-pass:
n= 80 0.54s
n=160 5.42s
n=240 22.19s
~n^3.4. `n` is passes over the dataset, so 600k samples over a 500-row
dataset is n=1200, which extrapolates past an hour -- on the `found:
false` path, which is the run an operator most wants a verdict for, and
in front of the snapshot write.
The search was re-deriving something already at hand: whether the run
drifts is a whole-span question, and one trend test answers it. Same
rule as `_window_gate`, effect-size floor included, so "trend-steady"
still means what it means for admissibility. Trend-steady throughout ->
report CoV over that span; drifting -> no CoV to claim, and the reason
names the metric.
n=240 22.19s -> 0.008s
What this gives up: a flat-but-noisy sub-span inside an otherwise
drifting run is no longer singled out. The anomaly/plateau detail and
`drifting_up` already name what went wrong there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(metrics): give the steady-state analysis a deadline of its own
The docstring claimed the analysis was bounded by the metrics drain
budget. It was not: that budget covers `flush_remaining`, which has
already returned by the time the verdict is computed. Nothing else
bounded it, and it runs in front of the final snapshot write -- so an
overrun meant the parent's grace expired, `wait_for_exit` killed the
process before `publish_final`, and the run lost `final_snapshot.json`
entirely rather than just its verdict. The Report then fell back to a
non-terminal tick and the run failed.
"No failure here may cost the run its snapshot" held for exceptions but
not for wall-clock. It does now: the analysis runs off the event loop
with a 30s ceiling, comfortably inside the parent's 60s grace, and a run
that blows it publishes without a verdict. Off-loop because a
synchronous call cannot be interrupted; abandoning the thread is fine
because the process exits moments later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(benchmark): keep a ceiling on the post-run wait by default
Cancelling the run watchdog before the drain removed the only thing that
could interrupt a wedged service, and the replacement bound existed only
when the operator had set `metrics_drain_timeout_s`. That is `None` by
default -- the generated templates all ship `metrics_drain_timeout_s:
null` -- so on a default config the parent waited on `wait_for_exit(None)`
forever with nothing left to abort it. A `--timeout` run used to fail at
its deadline; it would now hang the CLI indefinitely.
An unlimited drain has no budget to extend, so fall back to the run
deadline: it is the operator's own number for how long anything should
take, and it restores exactly the ceiling the watchdog used to provide
without letting a late firing relabel a run whose work already finished.
Unbounded only when neither knob is set, which is the operator declining
to bound either.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(metrics): stop the verdict advertising a flag its reader may not have
The "all plateaus too short" reason ended with "pass --no-min-duration
to override". That reason now rides the snapshot into result_summary.json,
report.txt and the console, where the reader ran `benchmark online
--steady-state` and has no such flag -- it exists only on the standalone
CLI, whose own --help documents it.
Also corrects three stale descriptions the window-sizes removal left
behind: the module docstring still described a per-window-size CoV table,
the operator doc claimed `offline` uses a completion-based window (it
uses the same issue-time span, which is precisely why it is unsupported),
and a test comment still named `build_steady_state`.
Guards the aggregator's profile lookup while here: an unknown name raised
a bare KeyError that killed the subprocess at startup, so the parent
reported a launch timeout naming nothing. It now degrades to
collection-off with a warning, like every other ineligibility.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(metrics): survive a scrubbed verdict, and size super-passes from the pass
Two defects that hide each other.
`snapshot_to_dict` scrubs every non-finite float in the verdict to
`None` before the snapshot is written, and `dict.get(key, default)`
returns that stored `None` rather than the default. Formatting it with
`:.2f` raised out of `Report.display()` -- on the success path of every
run that produced a verdict. `cov()` divides by a mean that can be zero,
so it was reachable. Values are coerced at the display boundary now.
The super-pass size was taken from `dataset.num_samples()`, but the
sample order cycles over `RuntimeSettings.n_samples_from_dataset`. The
ruleset path sets that from `ds_subset_size` without truncating the
dataloader, so a submission-ref run with a subset sized super-passes to
a pass that never happens -- bucket boundaries silently stopped meaning
"one pass over the dataset", which is the premise of the whole rollup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(core): move PhaseType to core/types, and decode PhaseData
`PhaseType` lived beside `BenchmarkSession`, so the two consumers that
need it on the wire could not import it: `importing load_generator.session
costs 13.9s against 18ms for core.types`, and the metrics aggregator
subprocess imports the detector. The string was therefore written out
three times -- `PERFORMANCE_PHASE` in the detector, `_PERFORMANCE_PHASE`
in full-run OSL, and the enum itself.
Moving it to `core/types.py` deletes all three copies and lets
`PhaseData.phase_type` be a `PhaseType` rather than a bare `str`, which
is where it belonged: the field exists to carry this enum over the wire.
Also replaces the hand-indexed `PhaseData` read in
`superpass_size_from_events` with `msgspec.convert`. The struct is
`array_like` -- every member of the `EventRecord.data` tagged union must
be -- so it lands in the log as `["PhaseData", "performance", true, 1000,
0]` and indexing it by position would quietly read the wrong field the
day one is added. `convert` checks the tag and the arity too.
The agentic `num_turns` becomes `len(conv.samples)`: turns issued in one
pass over the dataset, matching what the single-turn branch reports,
rather than `max_turns_per_conv` -- a per-conversation ceiling that is
not a count of anything issued.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(metrics): make the steady-state verdict a typed Struct tree
The verdict's seven types were TypedDicts because this file used to be a
standalone script: no msgspec dependency, output written with json.dump.
Neither holds now -- it lives in the package, rides `MetricsSnapshot`,
and lands in result_summary.json -- so the shape was declared in one
place and then re-guessed with `.get()` defaults at every consumer.
They are msgspec Structs now, `MetricsSnapshot.steady_state` is
`SteadyState | None`, and `Report.from_snapshot` converts rather than
carrying `dict[str, Any]` through to the renderer.
A verdict that does not match the schema is dropped, not coerced: it is a
diagnostic, and a run that produced one still deserves its report, so the
absence is made visible instead of printing a plausible wrong number.
That replaces the `_num` coercion added a few commits ago -- the crash it
guarded is now unrepresentable, since `end_ns` cannot be None.
mypy checks field types through this (verified: `return ss.reason` from
an `-> int` is now an error). The `type: ignore[call-arg]` on each class
line is the repo's existing convention for msgspec class kwargs -- eight
other modules carry it, and msgspec 0.20 ships no mypy plugin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(metrics): pin the resize guard, and note what archived runs need
The test named for the "a later phase cannot re-size a started series"
invariant announced an accuracy phase, which the router drops on
phase_type before the collector sees it -- so the guard it was named for
had no coverage at all. Splits it: one case for the router's check, one
for a *second performance* announcement, which clears that check and
leaves `announce_phase`'s own empty-series guard as the only thing
stopping a mid-run re-bucket. Removing that guard now fails the second
case and only the second case.
Also documents the back-compat edge the PHASE_START switch introduced:
run directories written before the event carry no announced super-pass
size, so the zero-argument re-run needs `--superpass-size` for those.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(metrics): rewrite the steady-state prose for readability
Review feedback was that several docstrings and comments were harder to
follow than the code they described. Rewrites them to lead with what the
function does before the rationale, and drops the narrative phrasing.
Prose only: every file here is byte-identical after stripping docstrings
and comments, apart from two argparse `help=` strings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(metrics): drop the new timeouts, and name the super-pass index in full
Review asked not to add another user-tunable timeout, and questioned
whether the analysis needed a deadline at all. Both hold up on measurement,
so this removes the whole chain:
- `Timeouts.service_exit_grace_s` is gone. It only existed because
cancelling the watchdog removed the post-run ceiling, which needed a
bound on `wait_for_exit`, which needed a grace on top of the drain
budget. `Timeouts` is now unchanged by this branch.
- The watchdog is no longer cancelled before the metrics drain, so
`execute.py` matches main again.
- `wait_for_exit` goes back to an unbounded wait. It SIGKILLs on expiry,
and the aggregator writes final_snapshot.json as the last thing it
does, so a parent-side deadline trades a wedged-service hang for a lost
snapshot. The abort path keeps `interrupted_teardown_grace_s`.
- The steady-state analysis carries no deadline. It is pure CPU over
rollups the collector already built: 2.9s at 100 super-passes (438,800
samples, the longest shape we run) and 19s at 245. A deadline guards a
cost that does not occur.
Failure stays contained either way: `_steady_state_verdict` catches, so a
failure publishes the snapshot with every other stat and no verdict, and it
runs in `asyncio.to_thread` so a SIGTERM mid-analysis still reaches its
handler.
Also renames `sp_index` to `superpass_index` throughout, per review.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>1 parent e71b928 commit 4235a9c
38 files changed
Lines changed: 2818 additions & 950 deletions
File tree
- docs
- scripts
- src/inference_endpoint
- async_utils/services/metrics_aggregator
- commands
- benchmark
- config
- templates
- core
- load_generator
- metrics
- tests
- integration
- unit
- async_utils/services/metrics_aggregator
- commands
- core
- load_generator
- metrics
- scripts
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
0 commit comments