Skip to content

perf(PERF-002): skip discarded parquet reads with footer stats#103

Merged
DoRmAmMu1997 merged 2 commits into
mainfrom
perf/perf-002-footer-stats-bounds
Jul 11, 2026
Merged

perf(PERF-002): skip discarded parquet reads with footer stats#103
DoRmAmMu1997 merged 2 commits into
mainfrom
perf/perf-002-footer-stats-bounds

Conversation

@DoRmAmMu1997

@DoRmAmMu1997 DoRmAmMu1997 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

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.py reads 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.
  • Missing, corrupt, mistyped, null, or statless footer metadata returns (None, None) and falls back to the established full-read behavior.

Safety behavior after review

  • A prefetch never declares a cache fresh from footer statistics alone. It reads and validates the actual frame, so a readable footer cannot hide corrupt data pages.
  • When footer bounds appear to cover a requested range, the loader recomputes bounds from the frame it actually read before returning a cache hit. This closes file-replacement/TOCTOU mismatches between the metadata check and data read.
  • No cache format, sidecar, public API, or Parquet write behavior changes.

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

  • Focused Parquet/daily-loader suite: 50 passed
  • Full suite: 1,403 passed, 1 skipped; coverage 87.95%
  • compileall, Ruff, mypy, Bandit, pinned pip-audit, pre-commit hooks, and git diff --check: passed
  • Security diff review: no findings

The change remains intentionally limited to footer-statistics helpers, loader decision logic, and focused regressions for corrupt data pages and changed files.

…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 DoRmAmMu1997 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?":

  1. Fail-safe shape verified branch-by-branchtimestamp_bounds returns (None, None) for: missing file, corrupt file, no timestamp column, 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.
  2. Fresh-condition equivalence_covers_full_window uses first ≤ start and last ≥ today, cross-checked against ensure_daily_history: its "fresh" verdict requires NOT first > start (:471) and last ≥ today (:487) → identical. The checked_through sidecar path only matters when last < today, which the fast path never claims.
  3. 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 by test_bounds_span_multiple_row_groups + test_writer_without_statistics_returns_none_pair.
  4. Semantics of footer min/max vs _date_bounds — footer statistics ignore nulls; _date_bounds drops NaT via errors="coerce". Same nulls-excluded bounds. Mixed valid+NaT columns therefore agree; all-NaT columns have no stats → fallback (test-locked).
  5. The "no pandas I/O" claim is proven, not asserted — two integration tests monkeypatch daily_data_loader.pd.read_parquet to raise AssertionError; the prefetch fresh path and the get_daily_history miss decision both pass under that trap.
  6. No former hit becomes a misstest_get_daily_history_covered_range_without_statistics_is_still_a_hit uses a client that fails the test if Dhan is reached, on a statless (fallback-only) file.
  7. 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.
  8. 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).

Copy link
Copy Markdown
Owner Author

Codex review plan before fixes:

  • Treat Parquet footer statistics as advisory, not proof that an unreadable cache is fresh.
  • Keep full-frame validation in the prefetch freshness path.
  • Recompute bounds on the frame actually read before returning a cache hit, closing replacement/TOCTOU gaps.
  • Add corrupt-data-page and changed-file regressions.
  • Update the PR title/body to describe only the discarded-read optimization that remains.

I’ll implement this with TDD, run the full quality/security/Docker gates, and push the follow-up here.

@DoRmAmMu1997 DoRmAmMu1997 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread backend/daily_data_loader.py Outdated
Comment thread backend/daily_data_loader.py
Co-authored-by: Hemant <hemantdhamija@gmail.com>
Co-authored-by: Codex <codex@openai.com>
@DoRmAmMu1997 DoRmAmMu1997 changed the title perf(PERF-002): answer cache-coverage decisions from parquet footer stats (~12x) perf(PERF-002): skip discarded parquet reads with footer stats Jul 11, 2026
@DoRmAmMu1997 DoRmAmMu1997 merged commit baddeca into main Jul 11, 2026
3 checks passed
@DoRmAmMu1997 DoRmAmMu1997 deleted the perf/perf-002-footer-stats-bounds branch July 11, 2026 13:30
@DoRmAmMu1997

Copy link
Copy Markdown
Owner Author

Completion verdict

Complete — no remaining reportable findings. This is a completion comment, not an approval.

Commit 4224ac1 made Parquet footer statistics advisory, retained full-read prefetch validation, rechecked the actual frame bounds, added corrupt/changed-file regressions, and corrected the PR title and body. Verification completed with 1,403 tests passed, 1 skipped, 87.95% coverage, and green hosted CI.

A formal diff-security review found no remaining reportable findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant