fixtureqa 0.4.18__tar.gz → 0.4.19__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.18/fixtureqa.egg-info → fixtureqa-0.4.19}/PKG-INFO +1 -1
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/perf.py +8 -1
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/exec_csv_writer.py +5 -5
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/perf_charts.py +2 -2
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/perf_engine.py +76 -26
- {fixtureqa-0.4.18 → fixtureqa-0.4.19/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/pyproject.toml +1 -1
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_perf_engine.py +47 -9
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/LICENSE +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/README.md +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/__init__.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/__main__.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/app.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/deps.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/auth.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/events.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/models.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/session.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/server.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/static/assets/index-BSuHxk6b.js +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/static/assets/index-o_l1KBYd.css +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/static/index.html +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixtureqa.egg-info/SOURCES.txt +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/setup.cfg +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_auth.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_config_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_health.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_inbound.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_message_store.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_sessions.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_templates.py +0 -0
- {fixtureqa-0.4.18 → fixtureqa-0.4.19}/tests/test_ws.py +0 -0
|
@@ -228,12 +228,19 @@ async def run_chart(run_id: str, reg: REG, _admin: Admin,
|
|
|
228
228
|
if csv_path is None or not os.path.exists(csv_path):
|
|
229
229
|
raise HTTPException(status_code=404,
|
|
230
230
|
detail="No per-message recording for this run (enable record_execs before starting)")
|
|
231
|
+
# Single-leg runs (client/venue) derive one-way from the wire send time;
|
|
232
|
+
# label it so the chart can't be misread as the single-clock measurement.
|
|
233
|
+
st = reg.get_status(run_id)
|
|
234
|
+
one_way_label = None
|
|
235
|
+
if st is not None and st.mode not in ("loopback", "manager"):
|
|
236
|
+
one_way_label = "one-way (tag-60 wire send → recv; needs synced clocks)"
|
|
231
237
|
out_path = f"{csv_path[:-len('.csv.gz')]}_{metric}_{agg}.png"
|
|
232
238
|
if not os.path.exists(out_path):
|
|
233
239
|
try:
|
|
234
240
|
await asyncio.to_thread(
|
|
235
241
|
render_latency_chart, csv_path, out_path,
|
|
236
|
-
metric=metric, agg=agg, title=f"Per-message latency — {run_id}"
|
|
242
|
+
metric=metric, agg=agg, title=f"Per-message latency — {run_id}",
|
|
243
|
+
one_way_label=one_way_label)
|
|
237
244
|
except ValueError as e:
|
|
238
245
|
# No chartable points (e.g. recording produced only un-latencied rows).
|
|
239
246
|
raise HTTPException(status_code=404, detail=str(e))
|
|
@@ -32,15 +32,15 @@ EXEC_COLS = (
|
|
|
32
32
|
# client→venue: order / amend / cancel
|
|
33
33
|
# venue→client: ack / fill / pending-replace / replace /
|
|
34
34
|
# pending-cancel / canceled / cancel-reject / amend-reject
|
|
35
|
-
"direction", # c2v (client→hub→venue) | v2c (venue→hub→client)
|
|
36
|
-
# (rt = round-trip-only fallback, client/venue-only modes)
|
|
35
|
+
"direction", # c2v (client→hub→venue) | v2c (venue→hub→client)
|
|
37
36
|
"send_at", # wall clock of THIS message's send on the near leg (one-way x-axis)
|
|
38
37
|
"recv_at", # wall clock of THIS message's receive on the far leg
|
|
39
38
|
"req_send_at", # wall clock of the originating request's send (order/amend/cancel
|
|
40
39
|
# send; == send_at for c2v rows) — the round-trip x-axis
|
|
41
|
-
"one_way_us", #
|
|
42
|
-
# monotonic clock
|
|
43
|
-
|
|
40
|
+
"one_way_us", # this message's one-way transit. manager/loopback: recv − send on
|
|
41
|
+
# one monotonic clock. client/venue-only: recv − the wire send time
|
|
42
|
+
# (TransactTime 60 / SendingTime 52), needs a synced clock.
|
|
43
|
+
"round_trip_us", # v2c only: recv − the originating request's send (our clock); empty for c2v
|
|
44
44
|
"exec_type", "ord_status", "cum_qty", "leaves_qty", # tags 150 / 39 / 14 / 151 (forensics)
|
|
45
45
|
)
|
|
46
46
|
|
|
@@ -115,7 +115,7 @@ def _draw(ax, by_kind, agg: str, bucket_s: float, logy: bool, ylabel: str, title
|
|
|
115
115
|
def render_latency_chart(csv_gz_path: str, out_path: str, *, metric: str = "one_way",
|
|
116
116
|
agg: str = "both", bucket_s: float = 2.0,
|
|
117
117
|
logy: bool = True, kinds: Optional[list[str]] = None,
|
|
118
|
-
title: Optional[str] = None) -> str:
|
|
118
|
+
title: Optional[str] = None, one_way_label: Optional[str] = None) -> str:
|
|
119
119
|
"""Render a per-kind latency chart from a recorded run CSV to out_path (PNG).
|
|
120
120
|
|
|
121
121
|
metric: 'one_way' (recv − send, this message's hub transit; x = its own send
|
|
@@ -146,7 +146,7 @@ def render_latency_chart(csv_gz_path: str, out_path: str, *, metric: str = "one_
|
|
|
146
146
|
axes = fig.subplots(len(panels), 1, squeeze=False)[:, 0]
|
|
147
147
|
base = title or "Per-message latency"
|
|
148
148
|
for ax, (m, by_kind) in zip(axes, panels):
|
|
149
|
-
label = _METRIC[m][2]
|
|
149
|
+
label = one_way_label if (m == "one_way" and one_way_label) else _METRIC[m][2]
|
|
150
150
|
_draw(ax, by_kind, agg, bucket_s, logy, f"latency (ms) — {label}",
|
|
151
151
|
base if len(panels) == 1 else f"{base} — {label}")
|
|
152
152
|
fig.tight_layout()
|
|
@@ -26,6 +26,7 @@ import random
|
|
|
26
26
|
import time
|
|
27
27
|
import uuid
|
|
28
28
|
from collections import deque
|
|
29
|
+
from datetime import datetime, timezone
|
|
29
30
|
from typing import Optional
|
|
30
31
|
|
|
31
32
|
from fixcore.message.message import Message
|
|
@@ -96,6 +97,29 @@ def _to_float(v) -> Optional[float]:
|
|
|
96
97
|
return None
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
# Wire send-time tags for the tag-60 latency path (client/venue-only modes):
|
|
101
|
+
# TransactTime(60), falling back to the session SendingTime(52) which every
|
|
102
|
+
# message carries (e.g. 35=9 has no TransactTime).
|
|
103
|
+
_LAT_TS_TAGS = (60, 52)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _parse_fix_ts(fields: dict) -> Optional[float]:
|
|
107
|
+
"""The counterparty's wall-clock send time (epoch seconds, UTC) from
|
|
108
|
+
TransactTime(60) → SendingTime(52). Returns None if absent/unparseable.
|
|
109
|
+
Used only when FIXture sees one leg, so the send timestamp must come off the
|
|
110
|
+
wire — accuracy then depends on the two hosts' clocks being NTP/PTP-synced."""
|
|
111
|
+
for tag in _LAT_TS_TAGS:
|
|
112
|
+
val = fields.get(tag)
|
|
113
|
+
if not val:
|
|
114
|
+
continue
|
|
115
|
+
for fmt in ("%Y%m%d-%H:%M:%S.%f", "%Y%m%d-%H:%M:%S"):
|
|
116
|
+
try:
|
|
117
|
+
return datetime.strptime(val, fmt).replace(tzinfo=timezone.utc).timestamp()
|
|
118
|
+
except ValueError:
|
|
119
|
+
continue
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
99
123
|
class PerfRun:
|
|
100
124
|
def __init__(self, run_id: str, config: RunConfig, sm: SessionManager, writer: PerfWriter,
|
|
101
125
|
template_store=None, owner_uid: str = "",
|
|
@@ -181,16 +205,23 @@ class PerfRun:
|
|
|
181
205
|
self.status = "running"
|
|
182
206
|
self.started_at = time.time()
|
|
183
207
|
self._start_ns = time.perf_counter_ns()
|
|
208
|
+
# Wall-clock anchored to the same instant as _start_ns, so an inbound
|
|
209
|
+
# message's monotonic recv_perf_ns can be expressed as a UTC wall time
|
|
210
|
+
# (for the tag-60 latency path) without a second clock read per message.
|
|
211
|
+
self._start_wall = time.time()
|
|
184
212
|
if self.config.test.record_execs:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if not self._has_client:
|
|
188
|
-
self.warnings.append("record_execs has no effect in venue mode (no client leg)")
|
|
213
|
+
if not (self._has_client or self._has_venue):
|
|
214
|
+
self.warnings.append("record_execs has no effect with no client or venue leg")
|
|
189
215
|
elif not self._execs_dir:
|
|
190
216
|
self.warnings.append("record_execs unavailable (no exec CSV directory configured)")
|
|
191
217
|
else:
|
|
192
218
|
self._exec_csv = ExecCsvWriter(
|
|
193
219
|
os.path.join(self._execs_dir, f"{self.run_id}.csv.gz"))
|
|
220
|
+
if not self._one_way:
|
|
221
|
+
self.warnings.append(
|
|
222
|
+
"single-leg run: per-message one-way latency uses the wire "
|
|
223
|
+
"send time (TransactTime 60 / SendingTime 52) — accurate only "
|
|
224
|
+
"if this host and the counterparty share a synced clock")
|
|
194
225
|
if self.config.amend.enabled and not self._has_client:
|
|
195
226
|
self.warnings.append(
|
|
196
227
|
"amend injection has no effect in venue mode (no client leg); "
|
|
@@ -817,38 +848,58 @@ class PerfRun:
|
|
|
817
848
|
sends[eid] = {"kind": kind, "send_ns": send_ns, "send_wall": time.time(),
|
|
818
849
|
"corr": corr, "clordid": clordid}
|
|
819
850
|
|
|
851
|
+
def _recv_wall(self, recv_ns: int) -> float:
|
|
852
|
+
"""Wall-clock (epoch secs) of an inbound message, anchored to the same
|
|
853
|
+
instant as its monotonic recv_perf_ns — for the tag-60 latency path."""
|
|
854
|
+
return self._start_wall + (recv_ns - self._start_ns) / 1e9
|
|
855
|
+
|
|
820
856
|
def _record_c2v(self, of: dict, corr: str, recv_ns: int, kind: str) -> None:
|
|
821
857
|
"""Record a client→venue crossing (order/amend/cancel) at the venue
|
|
822
|
-
inbound side: one-way
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
entry = self._pending.get(corr)
|
|
827
|
-
if entry is None:
|
|
828
|
-
return
|
|
829
|
-
ns_key = {"order": "sent_ns", "amend": "amend_sent_ns",
|
|
830
|
-
"cancel": "cancel_sent_ns"}[kind]
|
|
831
|
-
send_ns = entry.get(ns_key)
|
|
832
|
-
if not send_ns:
|
|
858
|
+
inbound side. Manager/loopback: one-way = venue recv − the client's send
|
|
859
|
+
(from _pending, one clock). Venue-only: one-way = our recv − the
|
|
860
|
+
counterparty's wire send time (tag-60), needing a synced clock."""
|
|
861
|
+
if self._exec_csv is None:
|
|
833
862
|
return
|
|
834
|
-
|
|
863
|
+
if self._one_way:
|
|
864
|
+
entry = self._pending.get(corr)
|
|
865
|
+
if entry is None:
|
|
866
|
+
return
|
|
867
|
+
ns_key = {"order": "sent_ns", "amend": "amend_sent_ns",
|
|
868
|
+
"cancel": "cancel_sent_ns"}[kind]
|
|
869
|
+
send_ns = entry.get(ns_key)
|
|
870
|
+
if not send_ns:
|
|
871
|
+
return
|
|
872
|
+
send_at = entry.get(ns_key[:-2] + "at") # *_sent_ns → *_sent_at
|
|
873
|
+
one_way_us = (recv_ns - send_ns) / 1000.0
|
|
874
|
+
else:
|
|
875
|
+
ts = _parse_fix_ts(of)
|
|
876
|
+
if ts is None:
|
|
877
|
+
return
|
|
878
|
+
send_at = ts
|
|
879
|
+
one_way_us = (self._recv_wall(recv_ns) - ts) * 1e6
|
|
835
880
|
self._emit_crossing(
|
|
836
881
|
corr=corr, clordid=of.get(T_CLORDID), exec_id=None, kind=kind,
|
|
837
882
|
direction="c2v", send_at=send_at, recv_at=time.time(),
|
|
838
|
-
req_send_at=send_at, one_way_us=
|
|
839
|
-
round_trip_us=None)
|
|
883
|
+
req_send_at=send_at, one_way_us=one_way_us, round_trip_us=None)
|
|
840
884
|
|
|
841
885
|
def _record_v2c(self, f: dict, corr, exec_id, now_ns: int, kind: str,
|
|
842
886
|
entry: Optional[dict]) -> None:
|
|
843
|
-
"""Record a venue→client crossing at the client inbound side
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
887
|
+
"""Record a venue→client crossing at the client inbound side. one-way:
|
|
888
|
+
from the venue send registry (manager/loopback) or the counterparty's
|
|
889
|
+
wire send time (tag-60, client-only — synced clock needed). round-trip:
|
|
890
|
+
from the originating request's send in _pending (always our own clock)."""
|
|
847
891
|
if self._exec_csv is None:
|
|
848
892
|
return
|
|
849
893
|
vs = self._venue_sends.pop(exec_id, None) if exec_id else None
|
|
850
|
-
|
|
851
|
-
|
|
894
|
+
if vs:
|
|
895
|
+
one_way_us = (now_ns - vs["send_ns"]) / 1000.0
|
|
896
|
+
send_at = vs["send_wall"]
|
|
897
|
+
elif not self._one_way:
|
|
898
|
+
ts = _parse_fix_ts(f)
|
|
899
|
+
one_way_us = ((self._recv_wall(now_ns) - ts) * 1e6) if ts is not None else None
|
|
900
|
+
send_at = ts
|
|
901
|
+
else:
|
|
902
|
+
one_way_us = send_at = None
|
|
852
903
|
rt_us = req_send_at = None
|
|
853
904
|
ns_key = _KIND_REQ_NS.get(kind)
|
|
854
905
|
if entry is not None and ns_key:
|
|
@@ -858,8 +909,7 @@ class PerfRun:
|
|
|
858
909
|
req_send_at = entry.get(ns_key[:-2] + "at") # *_sent_ns → *_sent_at
|
|
859
910
|
clordid = (entry.get("clordid") if entry else None) or (vs["clordid"] if vs else None)
|
|
860
911
|
self._emit_crossing(
|
|
861
|
-
corr=corr, clordid=clordid, exec_id=exec_id, kind=kind,
|
|
862
|
-
direction="v2c" if self._one_way else "rt",
|
|
912
|
+
corr=corr, clordid=clordid, exec_id=exec_id, kind=kind, direction="v2c",
|
|
863
913
|
send_at=send_at, recv_at=time.time(), req_send_at=req_send_at,
|
|
864
914
|
one_way_us=one_way_us, round_trip_us=rt_us,
|
|
865
915
|
exec_type=f.get(T_EXECTYPE, ""), ord_status=f.get(T_ORDSTATUS, ""),
|
|
@@ -497,9 +497,9 @@ def test_record_c2v_one_way(tmp_path):
|
|
|
497
497
|
assert r["round_trip_us"] == "" # c2v has no round-trip
|
|
498
498
|
|
|
499
499
|
|
|
500
|
-
def
|
|
501
|
-
# client-only mode
|
|
502
|
-
#
|
|
500
|
+
def test_record_v2c_round_trip_without_wire_ts_in_client_mode(tmp_path):
|
|
501
|
+
# client-only mode, reply has no TransactTime/SendingTime → one-way empty,
|
|
502
|
+
# round-trip (our own clock) still recorded.
|
|
503
503
|
from fixture.core.exec_csv_writer import ExecCsvWriter
|
|
504
504
|
run = _run(_cfg(mode="client"))
|
|
505
505
|
assert run._one_way is False
|
|
@@ -509,12 +509,50 @@ def test_record_v2c_round_trip_only_in_client_mode(tmp_path):
|
|
|
509
509
|
run._record_v2c({150: "2", 39: "2", 14: "100", 151: "0"}, "X", "E1",
|
|
510
510
|
base + 5_000_000, "fill", entry)
|
|
511
511
|
run._exec_csv.close()
|
|
512
|
-
|
|
513
|
-
assert
|
|
514
|
-
r
|
|
515
|
-
assert r["
|
|
516
|
-
|
|
517
|
-
|
|
512
|
+
r = _read_gz(tmp_path / "cm.csv.gz")[0]
|
|
513
|
+
assert r["direction"] == "v2c"
|
|
514
|
+
assert r["one_way_us"] == "" # no wire send time to compare
|
|
515
|
+
assert 4900 <= float(r["round_trip_us"]) <= 5100 # ~5 ms, our clock
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _fix_ts(epoch: float) -> str:
|
|
519
|
+
from datetime import datetime, timezone
|
|
520
|
+
return datetime.fromtimestamp(epoch, tz=timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def test_record_v2c_tag60_one_way_in_client_mode(tmp_path):
|
|
524
|
+
# client-only mode: one-way derived from the reply's TransactTime(60) vs our recv.
|
|
525
|
+
from fixture.core.exec_csv_writer import ExecCsvWriter
|
|
526
|
+
run = _run(_cfg(mode="client"))
|
|
527
|
+
run._start_ns = time.perf_counter_ns()
|
|
528
|
+
run._start_wall = time.time()
|
|
529
|
+
run._exec_csv = ExecCsvWriter(str(tmp_path / "t60.csv.gz"))
|
|
530
|
+
# venue stamped tag 60 eight ms before our (anchored) receive
|
|
531
|
+
recv_ns = run._start_ns + 1_000_000
|
|
532
|
+
tag60 = _fix_ts(run._recv_wall(recv_ns) - 0.008)
|
|
533
|
+
run._record_v2c({60: tag60, 150: "2", 39: "2"}, "X", "E1", recv_ns, "fill", None)
|
|
534
|
+
run._exec_csv.close()
|
|
535
|
+
r = _read_gz(tmp_path / "t60.csv.gz")[0]
|
|
536
|
+
assert r["direction"] == "v2c"
|
|
537
|
+
assert 7000 <= float(r["one_way_us"]) <= 9000 # ~8 ms from tag 60
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_record_c2v_tag60_one_way_in_venue_mode(tmp_path):
|
|
541
|
+
# venue-only mode: inbound order one-way from its TransactTime(60) vs our recv.
|
|
542
|
+
from fixture.core.exec_csv_writer import ExecCsvWriter
|
|
543
|
+
run = _run(_cfg(mode="venue"))
|
|
544
|
+
assert run._one_way is False
|
|
545
|
+
run._start_ns = time.perf_counter_ns()
|
|
546
|
+
run._start_wall = time.time()
|
|
547
|
+
run._exec_csv = ExecCsvWriter(str(tmp_path / "v60.csv.gz"))
|
|
548
|
+
recv_ns = run._start_ns + 500_000
|
|
549
|
+
tag60 = _fix_ts(run._recv_wall(recv_ns) - 0.012)
|
|
550
|
+
run._record_c2v({11: "O1", 60: tag60}, "X", recv_ns, "order")
|
|
551
|
+
run._exec_csv.close()
|
|
552
|
+
r = _read_gz(tmp_path / "v60.csv.gz")[0]
|
|
553
|
+
assert r["kind"] == "order" and r["direction"] == "c2v"
|
|
554
|
+
assert 11000 <= float(r["one_way_us"]) <= 13000 # ~12 ms from tag 60
|
|
555
|
+
assert r["round_trip_us"] == ""
|
|
518
556
|
|
|
519
557
|
|
|
520
558
|
def test_render_latency_chart_metrics(tmp_path):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|