fixtureqa 0.4.20__tar.gz → 0.4.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fixtureqa-0.4.20/fixtureqa.egg-info → fixtureqa-0.4.22}/PKG-INFO +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/perf.py +13 -8
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/perf_charts.py +37 -10
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/perf_engine.py +29 -10
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/perf_models.py +4 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/perf_stats.py +24 -1
- fixtureqa-0.4.20/fixture/static/assets/index-BhxUj8V-.js → fixtureqa-0.4.22/fixture/static/assets/index-BQKTSxpO.js +23 -23
- fixtureqa-0.4.20/fixture/static/assets/index-o_l1KBYd.css → fixtureqa-0.4.22/fixture/static/assets/index-V2H2CzlQ.css +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/static/index.html +2 -2
- {fixtureqa-0.4.20 → fixtureqa-0.4.22/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixtureqa.egg-info/SOURCES.txt +2 -2
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/pyproject.toml +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_perf_api.py +3 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_perf_engine.py +34 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/LICENSE +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/README.md +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/__main__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/app.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/deps.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/auth.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/events.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/models.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/session.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/server.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/setup.cfg +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_auth.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_config_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_health.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_inbound.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_message_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_sessions.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_templates.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.22}/tests/test_ws.py +0 -0
|
@@ -212,13 +212,18 @@ def run_export(run_id: str, reg: REG, pw: PW, _admin: Admin,
|
|
|
212
212
|
|
|
213
213
|
@router.get("/runs/{run_id}/chart")
|
|
214
214
|
async def run_chart(run_id: str, reg: REG, _admin: Admin,
|
|
215
|
-
metric: str = Query("one_way"), agg: str = Query("both")
|
|
215
|
+
metric: str = Query("one_way"), agg: str = Query("both"),
|
|
216
|
+
scale: str = Query("log")):
|
|
216
217
|
"""Per-message latency chart (PNG) from the run's recorded csv.gz. Rendered
|
|
217
|
-
off-loop on first request and cached per (metric, agg) — the CSV is
|
|
218
|
-
once the run finishes. Needs record_latency; 404 otherwise, 409
|
|
219
|
-
|
|
218
|
+
off-loop on first request and cached per (metric, agg, scale) — the CSV is
|
|
219
|
+
immutable once the run finishes. Needs record_latency; 404 otherwise, 409
|
|
220
|
+
while live."""
|
|
221
|
+
if (metric not in ("one_way", "round_trip", "both")
|
|
222
|
+
or agg not in ("scatter", "percentile", "both")
|
|
223
|
+
or scale not in ("log", "linear")):
|
|
220
224
|
raise HTTPException(status_code=422,
|
|
221
|
-
detail="metric must be one_way|round_trip|both;
|
|
225
|
+
detail="metric must be one_way|round_trip|both; "
|
|
226
|
+
"agg must be scatter|percentile|both; scale must be log|linear")
|
|
222
227
|
if not reg.exists(run_id):
|
|
223
228
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
224
229
|
if _run_is_live(reg, run_id):
|
|
@@ -234,13 +239,13 @@ async def run_chart(run_id: str, reg: REG, _admin: Admin,
|
|
|
234
239
|
one_way_label = None
|
|
235
240
|
if st is not None and st.mode not in ("loopback", "manager"):
|
|
236
241
|
one_way_label = "one-way (tag-60 wire send → recv; needs synced clocks)"
|
|
237
|
-
out_path = f"{csv_path[:-len('.csv.gz')]}_{metric}_{agg}.png"
|
|
242
|
+
out_path = f"{csv_path[:-len('.csv.gz')]}_{metric}_{agg}_{scale}.png"
|
|
238
243
|
if not os.path.exists(out_path):
|
|
239
244
|
try:
|
|
240
245
|
await asyncio.to_thread(
|
|
241
246
|
render_latency_chart, csv_path, out_path,
|
|
242
|
-
metric=metric, agg=agg,
|
|
243
|
-
one_way_label=one_way_label)
|
|
247
|
+
metric=metric, agg=agg, logy=(scale == "log"),
|
|
248
|
+
title=f"Per-message latency — {run_id}", one_way_label=one_way_label)
|
|
244
249
|
except ValueError as e:
|
|
245
250
|
# No chartable points (e.g. recording produced only un-latencied rows).
|
|
246
251
|
raise HTTPException(status_code=404, detail=str(e))
|
|
@@ -53,10 +53,25 @@ def _pctl(sorted_vals: list[float], q: float) -> float:
|
|
|
53
53
|
return sorted_vals[i]
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def _humanize_us(v, _pos=None) -> str:
|
|
57
|
+
"""Format a microsecond value as a self-describing tick label, picking the
|
|
58
|
+
unit per tick so a log axis spanning µs→s reads '500µs / 1ms / 10ms / 1s'
|
|
59
|
+
instead of 10^1 / 10^2. Handles negatives (tag-60 clock skew)."""
|
|
60
|
+
if v == 0:
|
|
61
|
+
return "0"
|
|
62
|
+
sign, a = ("-" if v < 0 else ""), abs(v)
|
|
63
|
+
if a < 1_000:
|
|
64
|
+
return f"{sign}{a:g}µs"
|
|
65
|
+
if a < 1_000_000:
|
|
66
|
+
return f"{sign}{a / 1_000:g}ms"
|
|
67
|
+
return f"{sign}{a / 1_000_000:g}s"
|
|
68
|
+
|
|
69
|
+
|
|
56
70
|
def _read_points(csv_gz_path: str, metric: str):
|
|
57
|
-
"""Group (x_seconds_since_start,
|
|
58
|
-
metric ('one_way' or 'round_trip'): y = that latency column
|
|
59
|
-
|
|
71
|
+
"""Group (x_seconds_since_start, y_microseconds) points by kind from the gz
|
|
72
|
+
CSV for one metric ('one_way' or 'round_trip'): y = that latency column (raw
|
|
73
|
+
µs — the axis humanizes per tick), x = its send column rebased to seconds
|
|
74
|
+
since the first send."""
|
|
60
75
|
y_col, x_col, _ = _METRIC[metric]
|
|
61
76
|
by_kind: dict[str, list[tuple[float, float]]] = defaultdict(list)
|
|
62
77
|
t0: Optional[float] = None
|
|
@@ -68,11 +83,11 @@ def _read_points(csv_gz_path: str, metric: str):
|
|
|
68
83
|
continue
|
|
69
84
|
try:
|
|
70
85
|
x = float(xraw)
|
|
71
|
-
|
|
86
|
+
y_us = float(yraw)
|
|
72
87
|
except ValueError:
|
|
73
88
|
continue
|
|
74
89
|
t0 = x if t0 is None else min(t0, x)
|
|
75
|
-
by_kind[r.get("kind") or "unknown"].append((x,
|
|
90
|
+
by_kind[r.get("kind") or "unknown"].append((x, y_us))
|
|
76
91
|
if t0 is not None:
|
|
77
92
|
for pts in by_kind.values():
|
|
78
93
|
pts[:] = [(x - t0, y) for (x, y) in pts]
|
|
@@ -98,15 +113,25 @@ def _draw(ax, by_kind, agg: str, bucket_s: float, logy: bool, ylabel: str, title
|
|
|
98
113
|
color=color, label=None if agg == "both" else kind,
|
|
99
114
|
edgecolors="none")
|
|
100
115
|
if agg in ("percentile", "both"):
|
|
101
|
-
bx, p50, p99 = _bucketed(pts, bucket_s)
|
|
116
|
+
bx, p50, p99, pmax = _bucketed(pts, bucket_s)
|
|
102
117
|
if bx:
|
|
103
118
|
ax.plot(bx, p50, color=color, lw=1.6, label=f"{kind} p50")
|
|
104
119
|
ax.plot(bx, p99, color=color, lw=1.0, ls="--", alpha=0.7,
|
|
105
120
|
label=f"{kind} p99")
|
|
121
|
+
# p100 / worst-case per bucket — the outlier envelope (faint dots)
|
|
122
|
+
ax.plot(bx, pmax, color=color, lw=0.8, ls=":", alpha=0.55,
|
|
123
|
+
label=f"{kind} max")
|
|
124
|
+
from matplotlib.ticker import FuncFormatter, LogLocator, NullFormatter
|
|
106
125
|
ax.set_xlabel("run time (s, from first send)")
|
|
107
126
|
ax.set_ylabel(ylabel + (" [log]" if logy else ""))
|
|
108
127
|
if logy:
|
|
109
128
|
ax.set_yscale("log")
|
|
129
|
+
# 1-2-5 per decade as labelled ticks (so narrow ranges still get several
|
|
130
|
+
# labels); finer minors as unlabelled gridlines.
|
|
131
|
+
ax.yaxis.set_major_locator(LogLocator(base=10, subs=(1.0, 2.0, 5.0)))
|
|
132
|
+
ax.yaxis.set_minor_locator(LogLocator(base=10, subs=tuple(x / 10 for x in range(10, 100))))
|
|
133
|
+
ax.yaxis.set_minor_formatter(NullFormatter())
|
|
134
|
+
ax.yaxis.set_major_formatter(FuncFormatter(_humanize_us))
|
|
110
135
|
ax.grid(True, which="both", ls=":", alpha=0.4)
|
|
111
136
|
ax.legend(loc="upper left", fontsize=8, ncol=2, framealpha=0.85)
|
|
112
137
|
ax.set_title(title)
|
|
@@ -147,7 +172,7 @@ def render_latency_chart(csv_gz_path: str, out_path: str, *, metric: str = "one_
|
|
|
147
172
|
base = title or "Per-message latency"
|
|
148
173
|
for ax, (m, by_kind) in zip(axes, panels):
|
|
149
174
|
label = one_way_label if (m == "one_way" and one_way_label) else _METRIC[m][2]
|
|
150
|
-
_draw(ax, by_kind, agg, bucket_s, logy, f"latency
|
|
175
|
+
_draw(ax, by_kind, agg, bucket_s, logy, f"latency — {label}",
|
|
151
176
|
base if len(panels) == 1 else f"{base} — {label}")
|
|
152
177
|
fig.tight_layout()
|
|
153
178
|
fig.savefig(out_path)
|
|
@@ -155,16 +180,18 @@ def render_latency_chart(csv_gz_path: str, out_path: str, *, metric: str = "one_
|
|
|
155
180
|
|
|
156
181
|
|
|
157
182
|
def _bucketed(sorted_pts: list[tuple[float, float]], bucket_s: float):
|
|
158
|
-
"""Per-bucket (center_x, p50, p99) over fixed-width time buckets
|
|
183
|
+
"""Per-bucket (center_x, p50, p99, max) over fixed-width time buckets
|
|
184
|
+
(max = p100, the worst-case latency in the bucket)."""
|
|
159
185
|
if bucket_s <= 0:
|
|
160
186
|
bucket_s = 1.0
|
|
161
187
|
buckets: dict[int, list[float]] = defaultdict(list)
|
|
162
188
|
for x, y in sorted_pts:
|
|
163
189
|
buckets[int(x // bucket_s)].append(y)
|
|
164
|
-
bx, p50, p99 = [], [], []
|
|
190
|
+
bx, p50, p99, pmax = [], [], [], []
|
|
165
191
|
for b in sorted(buckets):
|
|
166
192
|
ys = sorted(buckets[b])
|
|
167
193
|
bx.append((b + 0.5) * bucket_s)
|
|
168
194
|
p50.append(_pctl(ys, 0.50))
|
|
169
195
|
p99.append(_pctl(ys, 0.99))
|
|
170
|
-
|
|
196
|
+
pmax.append(ys[-1])
|
|
197
|
+
return bx, p50, p99, pmax
|
|
@@ -397,8 +397,19 @@ class PerfRun:
|
|
|
397
397
|
self._finalize(entry, "dropped")
|
|
398
398
|
return True
|
|
399
399
|
|
|
400
|
+
async def _send(self, session_id: str, msg: Message) -> bool:
|
|
401
|
+
"""Single send path — tallies every outbound app message per leg
|
|
402
|
+
(type-agnostic) for the messages-per-second chart, then delegates."""
|
|
403
|
+
ok = await self._sm.send_message(session_id, msg)
|
|
404
|
+
if ok:
|
|
405
|
+
if session_id == self.config.client_session_id:
|
|
406
|
+
self.stats.msgs_sent_client += 1
|
|
407
|
+
else:
|
|
408
|
+
self.stats.msgs_sent_venue += 1
|
|
409
|
+
return ok
|
|
410
|
+
|
|
400
411
|
async def _send_order(self, corr: str, msg: Message) -> bool:
|
|
401
|
-
ok = await self.
|
|
412
|
+
ok = await self._send(self.config.client_session_id, msg)
|
|
402
413
|
if ok:
|
|
403
414
|
self.stats.orders_sent += 1
|
|
404
415
|
else:
|
|
@@ -493,6 +504,8 @@ class PerfRun:
|
|
|
493
504
|
sub = self._venue_sub
|
|
494
505
|
while True:
|
|
495
506
|
inb = await sub.get()
|
|
507
|
+
if not inb.admin:
|
|
508
|
+
self.stats.msgs_recv_venue += 1
|
|
496
509
|
recv_ns = inb.recv_perf_ns or time.perf_counter_ns()
|
|
497
510
|
if inb.msg_type == "G":
|
|
498
511
|
await self._handle_amend(inb.fields, recv_ns)
|
|
@@ -517,7 +530,7 @@ class PerfRun:
|
|
|
517
530
|
last_qty=0, cum_qty=0, leaves_qty=order_qty, px="0")
|
|
518
531
|
if ack is not None:
|
|
519
532
|
send_ns = time.perf_counter_ns()
|
|
520
|
-
if await self.
|
|
533
|
+
if await self._send(cfg.venue_session_id, ack):
|
|
521
534
|
self.stats.acks_sent += 1
|
|
522
535
|
self._register_venue_send(ack, "ack", corr, of.get(T_CLORDID), send_ns)
|
|
523
536
|
|
|
@@ -573,7 +586,7 @@ class PerfRun:
|
|
|
573
586
|
cum_qty=cum_qty, leaves_qty=leaves_qty, px=entry["px"])
|
|
574
587
|
if er is not None:
|
|
575
588
|
send_ns = time.perf_counter_ns()
|
|
576
|
-
if await self.
|
|
589
|
+
if await self._send(cfg.venue_session_id, er):
|
|
577
590
|
self.stats.fills_sent += 1
|
|
578
591
|
self._register_venue_send(er, "fill", corr,
|
|
579
592
|
entry["of"].get(T_CLORDID), send_ns)
|
|
@@ -647,14 +660,14 @@ class PerfRun:
|
|
|
647
660
|
orig_clordid=orig)
|
|
648
661
|
if er is not None:
|
|
649
662
|
send_ns = time.perf_counter_ns()
|
|
650
|
-
if await self.
|
|
663
|
+
if await self._send(cfg.venue_session_id, er):
|
|
651
664
|
self._register_venue_send(er, "pending-replace", corr, clordid, send_ns)
|
|
652
665
|
er = self._build_exec(of, exec_type="5", ord_status="5", last_qty=0,
|
|
653
666
|
cum_qty=cum, leaves_qty=leaves, px=px,
|
|
654
667
|
orig_clordid=orig)
|
|
655
668
|
if er is not None:
|
|
656
669
|
send_ns = time.perf_counter_ns()
|
|
657
|
-
if await self.
|
|
670
|
+
if await self._send(cfg.venue_session_id, er):
|
|
658
671
|
self._register_venue_send(er, "replace", corr, clordid, send_ns)
|
|
659
672
|
|
|
660
673
|
async def _handle_cancel(self, of: dict, recv_ns: int = 0) -> None:
|
|
@@ -691,12 +704,12 @@ class PerfRun:
|
|
|
691
704
|
orig_clordid=of.get(T_ORIGCLORDID))
|
|
692
705
|
if er is not None:
|
|
693
706
|
send_ns = time.perf_counter_ns()
|
|
694
|
-
if await self.
|
|
707
|
+
if await self._send(cfg.venue_session_id, er):
|
|
695
708
|
self._register_venue_send(er, "canceled", corr, clordid, send_ns)
|
|
696
709
|
return
|
|
697
710
|
msg = self._build_cancel_reject(of, entry)
|
|
698
711
|
send_ns = time.perf_counter_ns()
|
|
699
|
-
if await self.
|
|
712
|
+
if await self._send(cfg.venue_session_id, msg):
|
|
700
713
|
self._register_venue_send(msg, "cancel-reject", corr, clordid, send_ns)
|
|
701
714
|
|
|
702
715
|
def _build_cancel_reject(self, of: dict, entry: Optional[dict]) -> Message:
|
|
@@ -811,6 +824,8 @@ class PerfRun:
|
|
|
811
824
|
sub = self._client_sub
|
|
812
825
|
while True:
|
|
813
826
|
inb = await sub.get()
|
|
827
|
+
if not inb.admin:
|
|
828
|
+
self.stats.msgs_recv_client += 1
|
|
814
829
|
if inb.msg_type == "9":
|
|
815
830
|
self._on_cancel_reject(inb.fields, inb.recv_perf_ns or time.perf_counter_ns())
|
|
816
831
|
continue
|
|
@@ -998,7 +1013,7 @@ class PerfRun:
|
|
|
998
1013
|
entry["cancel_clordid"] = new_clordid
|
|
999
1014
|
entry["cancel_sent_ns"] = time.perf_counter_ns()
|
|
1000
1015
|
entry["cancel_sent_at"] = time.time()
|
|
1001
|
-
if await self.
|
|
1016
|
+
if await self._send(self.config.client_session_id, msg):
|
|
1002
1017
|
self.stats.cancels_sent += 1
|
|
1003
1018
|
else:
|
|
1004
1019
|
entry["cancel_inflight"] = False
|
|
@@ -1129,7 +1144,7 @@ class PerfRun:
|
|
|
1129
1144
|
entry["amends_sent"] = n + 1
|
|
1130
1145
|
entry["amend_sent_ns"] = time.perf_counter_ns()
|
|
1131
1146
|
entry["amend_sent_at"] = time.time()
|
|
1132
|
-
if await self.
|
|
1147
|
+
if await self._send(self.config.client_session_id, msg):
|
|
1133
1148
|
self.stats.amends_sent += 1
|
|
1134
1149
|
else:
|
|
1135
1150
|
entry["amend_inflight"] = False
|
|
@@ -1219,6 +1234,8 @@ class PerfRun:
|
|
|
1219
1234
|
cps_live=round(r.cps, 1),
|
|
1220
1235
|
crps_live=round(r.crps, 1),
|
|
1221
1236
|
ccps_live=round(r.ccps, 1),
|
|
1237
|
+
msgs_out_live=round(r.mco, 1),
|
|
1238
|
+
msgs_in_live=round(r.mci, 1),
|
|
1222
1239
|
scenarios_dispatched=self.stats.scenarios_dispatched,
|
|
1223
1240
|
scenarios_per_sec=round(r.sps, 1),
|
|
1224
1241
|
scenarios_completed=self.stats.scenarios_completed,
|
|
@@ -1235,7 +1252,9 @@ class PerfRun:
|
|
|
1235
1252
|
amends_uncorrelated=self.stats.amends_uncorrelated,
|
|
1236
1253
|
amend_waits_expired=self.stats.amend_waits_expired,
|
|
1237
1254
|
cancels_received=self.stats.cancels_received,
|
|
1238
|
-
cancels_uncorrelated=self.stats.cancels_uncorrelated
|
|
1255
|
+
cancels_uncorrelated=self.stats.cancels_uncorrelated,
|
|
1256
|
+
msgs_out_live=round(r.mvo, 1),
|
|
1257
|
+
msgs_in_live=round(r.mvi, 1))
|
|
1239
1258
|
snap = LiveSnapshot(
|
|
1240
1259
|
run_id=self.run_id, status=self.status, mode=self.config.mode,
|
|
1241
1260
|
elapsed_s=round(self._elapsed_s(), 1), duration_s=self.config.test.duration,
|
|
@@ -252,6 +252,8 @@ class ClientLeg(BaseModel):
|
|
|
252
252
|
cps_live: float = 0.0
|
|
253
253
|
crps_live: float = 0.0
|
|
254
254
|
ccps_live: float = 0.0
|
|
255
|
+
msgs_out_live: float = 0.0 # all app messages this leg sent/s (MPS chart)
|
|
256
|
+
msgs_in_live: float = 0.0 # all app messages this leg received/s
|
|
255
257
|
scenarios_dispatched: int = 0
|
|
256
258
|
scenarios_per_sec: float = 0.0
|
|
257
259
|
scenarios_completed: int = 0
|
|
@@ -271,6 +273,8 @@ class VenueLeg(BaseModel):
|
|
|
271
273
|
amend_waits_expired: int = 0
|
|
272
274
|
cancels_received: int = 0
|
|
273
275
|
cancels_uncorrelated: int = 0
|
|
276
|
+
msgs_out_live: float = 0.0 # all app messages this leg sent/s (MPS chart)
|
|
277
|
+
msgs_in_live: float = 0.0 # all app messages this leg received/s
|
|
274
278
|
|
|
275
279
|
|
|
276
280
|
class SnapshotErrors(BaseModel):
|
|
@@ -25,6 +25,12 @@ class Rates(NamedTuple):
|
|
|
25
25
|
cps: float # cancels sent/s (35=F)
|
|
26
26
|
crps: float # cancel-rejects received/s (35=9)
|
|
27
27
|
ccps: float # canceled received/s (150=4)
|
|
28
|
+
# Type-agnostic message rates (every app message sent/received per leg) for
|
|
29
|
+
# the messages-per-second chart — counts all kinds, so it can't drift.
|
|
30
|
+
mco: float # client leg messages sent/s (orders + amends + cancels …)
|
|
31
|
+
mci: float # client leg messages received/s (acks + fills + replies …)
|
|
32
|
+
mvo: float # venue leg messages sent/s (acks + fills + replies …)
|
|
33
|
+
mvi: float # venue leg messages received/s (orders + amends + cancels …)
|
|
28
34
|
|
|
29
35
|
|
|
30
36
|
class TokenBucket:
|
|
@@ -147,6 +153,11 @@ class PerfStats:
|
|
|
147
153
|
self.cancels_received = 0 # venue saw an inbound 35=F
|
|
148
154
|
self.cancels_uncorrelated = 0 # inbound 35=F lacked the correlation tag (no reply)
|
|
149
155
|
self.venue_sends_unmatched = 0 # one-way send records evicted w/o a matching recv
|
|
156
|
+
# Type-agnostic per-leg message tallies (every app message), for the MPS chart.
|
|
157
|
+
self.msgs_sent_client = 0
|
|
158
|
+
self.msgs_recv_client = 0
|
|
159
|
+
self.msgs_sent_venue = 0
|
|
160
|
+
self.msgs_recv_venue = 0
|
|
150
161
|
self.lost_timeout = 0
|
|
151
162
|
self.dropped = 0
|
|
152
163
|
self.rejected = 0
|
|
@@ -170,6 +181,10 @@ class PerfStats:
|
|
|
170
181
|
self._last_cancels = 0
|
|
171
182
|
self._last_cancel_rejects = 0
|
|
172
183
|
self._last_canceled = 0
|
|
184
|
+
self._last_msc = 0
|
|
185
|
+
self._last_mrc = 0
|
|
186
|
+
self._last_msv = 0
|
|
187
|
+
self._last_mrv = 0
|
|
173
188
|
|
|
174
189
|
def rates(self) -> Rates:
|
|
175
190
|
"""Per-second deltas since the last call. Stateful — call exactly once
|
|
@@ -177,7 +192,7 @@ class PerfStats:
|
|
|
177
192
|
now = time.monotonic()
|
|
178
193
|
dt = now - self._last_t
|
|
179
194
|
if dt <= 0:
|
|
180
|
-
return Rates(0.0
|
|
195
|
+
return Rates(*([0.0] * 11))
|
|
181
196
|
r = Rates(
|
|
182
197
|
ops=(self.orders_sent - self._last_orders) / dt,
|
|
183
198
|
fps=(self.fills_sent - self._last_fills) / dt,
|
|
@@ -186,6 +201,10 @@ class PerfStats:
|
|
|
186
201
|
cps=(self.cancels_sent - self._last_cancels) / dt,
|
|
187
202
|
crps=(self.cancel_rejects - self._last_cancel_rejects) / dt,
|
|
188
203
|
ccps=(self.cancels_canceled - self._last_canceled) / dt,
|
|
204
|
+
mco=(self.msgs_sent_client - self._last_msc) / dt,
|
|
205
|
+
mci=(self.msgs_recv_client - self._last_mrc) / dt,
|
|
206
|
+
mvo=(self.msgs_sent_venue - self._last_msv) / dt,
|
|
207
|
+
mvi=(self.msgs_recv_venue - self._last_mrv) / dt,
|
|
189
208
|
)
|
|
190
209
|
self._last_t = now
|
|
191
210
|
self._last_orders = self.orders_sent
|
|
@@ -195,6 +214,10 @@ class PerfStats:
|
|
|
195
214
|
self._last_cancels = self.cancels_sent
|
|
196
215
|
self._last_cancel_rejects = self.cancel_rejects
|
|
197
216
|
self._last_canceled = self.cancels_canceled
|
|
217
|
+
self._last_msc = self.msgs_sent_client
|
|
218
|
+
self._last_mrc = self.msgs_recv_client
|
|
219
|
+
self._last_msv = self.msgs_sent_venue
|
|
220
|
+
self._last_mrv = self.msgs_recv_venue
|
|
198
221
|
return r
|
|
199
222
|
|
|
200
223
|
def fill_ratio(self) -> float:
|