perf: fast feature-frame builds and response-body release in the chunked OGC path - #388
Draft
thodson-usgs wants to merge 4 commits into
Draft
perf: fast feature-frame builds and response-body release in the chunked OGC path#388thodson-usgs wants to merge 4 commits into
thodson-usgs wants to merge 4 commits into
Conversation
Flat feature properties build through the plain DataFrame constructor instead of json_normalize, and all-point pages build their GeoDataFrame through one points_from_xy call instead of GeoDataFrame.from_features' per-feature walk. Measured against live pages: 3.0x and 4.8x, with byte-identical output. A page keeps the previous path when any geometry is non-point or malformed, when a properties key is named geometry, or when a nested value forces json_normalize -- from_features leaves nested values as raw dicts, so normalizing on the fast path alone would make a chunked call's column names depend on which pages happened to be all points.
An aggregated response's content was one arbitrary page's bytes, and httpx responses sit in a reference cycle, so every parsed page stayed charged until the call returned. paginate now frees each page as it is parsed and _merge_response empties the copy it returns. Measured on a live 7-page, 303k-row walk: 434.0 -> 306.5 MB peak heap. _drop_body clears both the raw and the decoded slot: the copy is taken before the drop, so clearing only _content would leave an aggregate answering b"" to .content and the whole page to .text. Behavior change: the aggregated httpx.Response a completed call or FanOutInterrupted.partial_response carries now has an empty body. Status, headers, URL and elapsed are unchanged; per-page responses are untouched.
format_datetime shaped qw service responses and lost its last caller when qwdata usage was removed (491eb5c); it appears in no docs or demos. Public name, so it warns through the shared mechanism with a 2027-08-22 horizon rather than vanishing. Combine the columns with pandas.to_datetime instead.
thodson-usgs
force-pushed
the
perf/ogc-shaping-fastpaths
branch
from
August 26, 2026 21:18
1602d15 to
460905a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A design review of the async parallel chunking stack (planning → fan-out → pagination → shaping/combining → transport), with each candidate improvement evaluated experimentally against real Water Data queries. Two changes survived the bar; everything else is reported below with the numbers that killed it. A post-review cleanup pass (4 parallel review agents: reuse / simplification / efficiency / altitude) converged the fast-path code and made it faster still, and a follow-up pass trimmed comments to current-code constraints and deprecated the one dead function a package-wide sweep found.
Accepted (this PR):
perf(shaping): vectorized feature-frame fast paths. Flat feature properties (every Water Data / NGWMN collection) build viapd.DataFrameinstead ofpd.json_normalize(~2.5x — nested values are detected by scanning only object-dtype columns after the cheap build); all-2D-point pages build geometry via one vectorizedgeopandas.points_from_xycall over two flat coordinate lists instead of per-featureGeoDataFrame.from_features(~4.8x). Nested properties, non-point/malformed geometry, or ageometryproperty column all fall back to the previous path. This CPU runs on the fan-out's event loop, so it sits on every chunked call's critical path.perf(transport): free response bodies once parsed. Every per-chunk aggregate stored for resume shared its first page's decompressed body, so a ~1-page-per-chunk fan-out held the whole download in RAM until the call finished.paginatelikewise pinned its first page's body for the whole walk. Aggregates now carry an empty body via a single named helper (combining._drop_body); status/headers/URL/elapsed unchanged. Behavior change (in NEWS): the aggregatehttpx.Responsebehind a call's metadata andFanOutInterrupted.partial_responseno longer carries body bytes — it was previously one arbitrary page's fragment, not the query's data.chore(utils): deprecateformat_datetime. A dead-function sweep over the whole package found exactly one orphan: it shaped qw-service responses, lost its last caller in491eb5c3, and appears in no docs or demos. Public name, so it warns through_deprecation.warn_deprecatedwith a 2027-08-22 horizon instead of vanishing.Two cleanup passes (each: 4 parallel review agents across reuse / simplification / efficiency / altitude) also landed on the branch. The second pass replaced the direct
shapely.pointscall withgeopandas.points_from_xy— the idiomnwisalready uses, retiring the package's only directshapelyimport — and gated_properties_frame's Python dict scan behindinfer_dtype, worth 2.3x on that helper under pandas 2 (supported by this package, and where every string column is object dtype).Efficiency benchmarks (current branch head)
get_daily, median of 7 trials/armCorrectness: outputs verified identical (
assert_frame_equal) against recorded API responses for full/missing/mixed geometry, polygons, nested properties, 3-D coordinates, single-feature pages, and a properties-levelidcolumn; full suite (1004 tests) andmypy --strictpass.Evaluated and rejected (with numbers)
DataFrame(records)): 1.08x best case — pandas 3's record path is already good.cursor=<opaque>;numberMatchedabsent fordaily), so page N+1's URL cannot be constructed ahead of page N.MRE
Offline, deterministic, no API key or quota (compares against the pre-change implementation inlined):
Typical output on an M-series laptop:
from_features=554ms vectorized=116ms (4.8x, identical output).Methodology notes
httpx.MockTransport, sleeping each request's recorded wall time — exact pairing, no quota, no cache noise.tracemallocpeak, one arm per process, response bodies freshly allocated per request.🤖 Generated with Claude Code
https://claude.ai/code/session_01UKweWmHg8cu1WuJ17kiHdh