fixtureqa 0.4.20__tar.gz → 0.4.21__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.21}/PKG-INFO +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/perf.py +13 -8
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_charts.py +37 -10
- fixtureqa-0.4.20/fixture/static/assets/index-BhxUj8V-.js → fixtureqa-0.4.21/fixture/static/assets/index-BzzfsRz2.js +2 -2
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/index.html +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.21/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/SOURCES.txt +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/pyproject.toml +1 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_api.py +3 -1
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_engine.py +17 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/LICENSE +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/README.md +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/__main__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/app.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/deps.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/auth.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/events.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/models.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_engine.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/session.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/server.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/index-o_l1KBYd.css +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/setup.cfg +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_auth.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_config_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_health.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_inbound.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_message_store.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_sessions.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_templates.py +0 -0
- {fixtureqa-0.4.20 → fixtureqa-0.4.21}/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
|