perf(PERF-002): skip discarded parquet reads with footer stats#103
Conversation
…tats The prefetch discards the candle frame (it only needs the status), yet the "is this cache fresh?" verdict decompressed the whole multi-year parquet just to learn its first/last dates. Parquet footers already carry per-row- group min/max statistics, and backend/health.py has read them for its cache snapshot since OBS-002. This generalizes that trick: - backend/parquet_stats.py: timestamp_bounds(path) -> (first, last) dates from footer statistics ONLY; (None, None) whenever the footer cannot answer authoritatively (missing stats/column, all-null, corrupt) - never raises, so the caller's full-read fallback is the error handler. - daily_data_loader._ensure_one_row: footer-provably-fresh caches yield "fresh" without any pandas I/O; anything else takes the unchanged ensure_daily_history slow path (public contract untouched). - daily_data_loader.get_daily_history: the coverage check reads the footer first; an insufficient cache now goes straight to the Dhan fetch without decompressing the frame it was about to discard. The covered path still reads the frame (it returns the candles). Statless files fall back to the original full read, so no former cache hit can become a miss. Benchmark (100 synthetic 10-year caches, pandas-written, warm, monotonic): OLD full-read + _date_bounds : 861.0 ms (8.61 ms/file) NEW footer timestamp_bounds : 72.0 ms (0.72 ms/file) ~12x On a ~500-symbol universe that removes seconds of pure decision overhead from every app relaunch. health.py deliberately NOT rewired: its exception handling feeds the unreadable-file counter, which this helper's never-raise contract would silently change. Tests: 9 unit tests for timestamp_bounds (multi-row-group, statless, all-NaT, mistyped column, corrupt file) + 6 loader integration tests that prove the fast paths do no pandas I/O (read_parquet monkeypatched to raise) and that statless files keep their old behavior. The pre-existing loader suite (33 tests) passes unmodified. Gates: 1,401 passed, coverage 88.19% (floor 87); pre-commit validate, compileall, ruff, mypy (120 files), bandit, pip-audit all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DoRmAmMu1997
left a comment
There was a problem hiding this comment.
Self-review (posted as COMMENT — the bot runs as the PR author and cannot APPROVE). Verdict: Approve.
This diff changes cache-decision logic, so the review focused on "can any file be treated differently than before, other than faster?":
- Fail-safe shape verified branch-by-branch —
timestamp_boundsreturns(None, None)for: missing file, corrupt file, notimestampcolumn, zero row groups, ANY row group without min/max stats, all-null column, non-date-like statistic. Every caller maps(None, None)to the pre-PERF-002 full-read logic. So the set of files whose handling changed = exactly the set the footer proves fresh/covered — and for those, the old code computed the same two dates from the same data and made the same decision. - Fresh-condition equivalence —
_covers_full_windowusesfirst ≤ start and last ≥ today, cross-checked againstensure_daily_history: its"fresh"verdict requires NOTfirst > start(:471) andlast ≥ today(:487) → identical. Thechecked_throughsidecar path only matters whenlast < today, which the fast path never claims. - One subtle correctness point in the helper — a single statless row group aborts to
(None, None)even if other groups have stats, because that group's rows could extend past every other group's bounds. Locked bytest_bounds_span_multiple_row_groups+test_writer_without_statistics_returns_none_pair. - Semantics of footer min/max vs
_date_bounds— footer statistics ignore nulls;_date_boundsdrops NaT viaerrors="coerce". Same nulls-excluded bounds. Mixed valid+NaT columns therefore agree; all-NaT columns have no stats → fallback (test-locked). - The "no pandas I/O" claim is proven, not asserted — two integration tests monkeypatch
daily_data_loader.pd.read_parquetto raiseAssertionError; the prefetch fresh path and theget_daily_historymiss decision both pass under that trap. - No former hit becomes a miss —
test_get_daily_history_covered_range_without_statistics_is_still_a_hituses a client that fails the test if Dhan is reached, on a statless (fallback-only) file. - health.py left alone, deliberately — rewiring it onto a never-raise helper would silently stop counting corrupt files as unreadable on the health page. Documented in the PR body as the reason the "reuse if trivial" option was declined.
- Benchmark honesty — synthetic files are written by the same
to_parquet(index=False)call production uses (default statistics), sized ~2,600 rows × 100 files; both passes reported; the 12× is the warm steady state the relaunch prefetch actually lives in.
Gates: 1,401 passed + 1 skipped, coverage 88.19% (floor 87); pre-existing loader suite (33 tests) unmodified; ruff/mypy(120)/bandit/pip-audit/pre-commit/compileall clean; constraints/pyproject diff vs main empty. Security review: no findings (local trusted cache files; no new error-message surface; no network/writes).
|
Codex review plan before fixes:
I’ll implement this with TDD, run the full quality/security/Docker gates, and push the follow-up here. |
DoRmAmMu1997
left a comment
There was a problem hiding this comment.
Codex review findings for PERF-002. Footer metadata is useful for avoiding a discarded read, but it is advisory rather than proof that the data pages remain readable or unchanged.
Co-authored-by: Hemant <hemantdhamija@gmail.com> Co-authored-by: Codex <codex@openai.com>
Completion verdictComplete — no remaining reportable findings. This is a completion comment, not an approval. Commit A formal diff-security review found no remaining reportable findings. |
Ticket scope (PERF-002)
This PR centralizes Parquet footer date-statistics reading and uses those bounds as an advisory miss optimization in the daily-data loader.
What remains optimized
backend/parquet_stats.pyreads optional timestamp min/max statistics without decompressing data pages.get_daily_history()can skip a full read when the footer already proves the cached file cannot cover the requested range; that frame would otherwise be read and immediately discarded before fetching fresh data.(None, None)and falls back to the established full-read behavior.Safety behavior after review
freshfrom footer statistics alone. It reads and validates the actual frame, so a readable footer cannot hide corrupt data pages.Benchmark interpretation
The original synthetic microbenchmark measured footer-bound extraction at roughly 12x faster than loading a whole multi-year frame solely to inspect two dates. That measurement still explains the discarded-read miss-path optimization, but it is not a blanket warm-prefetch or app-relaunch speedup: correctness requires warm prefetch to read the frame before reporting it fresh.
Verification
50 passed1,403 passed, 1 skipped; coverage87.95%git diff --check: passedThe change remains intentionally limited to footer-statistics helpers, loader decision logic, and focused regressions for corrupt data pages and changed files.