(improvement) (Cython only) row_parser: cache ParseDesc for prepared statements#742
(improvement) (Cython only) row_parser: cache ParseDesc for prepared statements#742mykaul wants to merge 2 commits intoscylladb:masterfrom
Conversation
e355833 to
eb3ae98
Compare
There was a problem hiding this comment.
Pull request overview
This PR optimizes the Cython row parsing fast-path for prepared statements by caching the ParseDesc built in recv_results_rows(), reducing repeated per-response construction overhead.
Changes:
- Add a module-level cache in
cassandra/row_parser.pyxkeyed byid(column_metadata)to reuseParseDesc,column_names, andcolumn_typesfor prepared statement executions. - Expose
clear_parse_desc_cache()and call it fromCluster.shutdown(). - Add a benchmark/correctness benchmark module to measure and validate the caching behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| cassandra/row_parser.pyx | Introduces ParseDesc caching and adds a public cache-clear helper. |
| cassandra/cluster.py | Clears the module-level ParseDesc cache during Cluster.shutdown(). |
| benchmarks/test_parse_desc_cache_benchmark.py | Adds benchmarks and cache-behavior checks for the new caching approach. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ee3c279 to
9f2f3ff
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves the Cython result-row parsing hot path by caching the ParseDesc constructed in recv_results_rows() for repeated executions of the same prepared statement (stable result_metadata object), reducing repeated metadata processing and deserializer construction.
Changes:
- Add a bounded module-level cache in
cassandra/row_parser.pyxkeyed byid(column_metadata)with identity + session-settings validation. - Add cache management helpers (
clear_parse_desc_cache(),get_parse_desc_cache_size()) intended for test/benchmark visibility. - Add a new benchmark module to measure ParseDesc construction and end-to-end parsing impact.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| cassandra/row_parser.pyx | Introduces ParseDesc caching for prepared-statement row decoding and adds cache clear/size helpers. |
| benchmarks/test_parse_desc_cache_benchmark.py | Adds benchmarks (and some correctness assertions) for cached vs uncached ParseDesc construction/parsing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @pytest.mark.parametrize("nrows,ncols", [(1, 10), (100, 5), (1000, 5)]) | ||
| def test_integration_cython_cached(benchmark, nrows, ncols): | ||
| """Integration: Cython recv_results_rows with ParseDesc cache hit (prepared stmt).""" | ||
| col_meta = _build_column_metadata(ncols) |
| # Only use the cache for prepared statements (self.column_metadata is | ||
| # None, so column_metadata comes from result_metadata which is a | ||
| # stable list stored on PreparedStatement). Inline metadata from | ||
| # non-prepared queries creates a fresh list every time and would | ||
| # cause unbounded cache growth. | ||
| if self.column_metadata is None and result_metadata is not None: | ||
| desc, self.column_names, self.column_types = _get_or_build_parse_desc( | ||
| column_metadata, column_encryption_policy, protocol_version) | ||
| else: | ||
| desc, self.column_names, self.column_types = _build_parse_desc( | ||
| column_metadata, column_encryption_policy, protocol_version) |
| @@ -0,0 +1,542 @@ | |||
| # Copyright DataStax, Inc. | |||
Cache the ParseDesc object constructed in recv_results_rows() so that repeated executions of the same prepared statement skip the list comprehensions, ColDesc construction, and make_deserializers() call. The cache is keyed by id(column_metadata). For prepared statements the result_metadata list is stored on PreparedStatement and reused, so id() is stable. On cache hit we verify object identity (cached_ref is column_metadata) and that session-level settings (column_encryption_policy, protocol_version) still match. Implementation details: - _get_or_build_parse_desc: cached path, used only when column_metadata comes from result_metadata (prepared statements with stable id()). - _build_parse_desc: uncached path, used for inline metadata from non-prepared queries that creates a fresh list every execution. - Cache is bounded to 256 entries; cleared entirely when full. - Returns only (desc, column_names, column_types) to the caller, avoiding exposure of internal cache fields. - Thread-safety: dict get/set are atomic in CPython (GIL), but concurrent cache misses may cause redundant construction (benign). A clear_parse_desc_cache() and get_parse_desc_cache_size() function are exposed for testing. ## Benchmark results (median, pytest-benchmark) ### ParseDesc construction only (reference benchmarks) | Columns | **Before** (original) | **After** (with cache) | |---------|-----------------------|------------------------| | 5 cols | 3,966 ns | 191 ns | | 10 cols | 5,730 ns | 175 ns | | 20 cols | 9,266 ns | 166 ns | | 50 cols | 19,388 ns | 193 ns | ### Full pipeline integration (recv_results_rows through Cython) | Scenario | **Before** (original) | **After** (with cache) | |-------------------|-----------------------|------------------------| | 1 row x 10 col | 40,867 ns | 2,977 ns | | 100 rows x 5 col | 145,584 ns | 73,206 ns | | 1000 rows x 5 col | 1,099,825 ns | 999,517 ns | For small result sets (single-row lookups common with prepared statements), ParseDesc construction is a large fraction of the total response-path cost. Caching eliminates it entirely after the first execution. All 623 unit tests pass (16 skipped - pre-existing).
- Fix copyright header in benchmark file (DataStax -> ScyllaDB) - Add pytest.importorskip guard for pytest-benchmark in benchmark file - Add unit tests for ParseDesc cache under tests/unit/cython: cache hit, miss, protocol version invalidation, clear, bounded eviction, correctness
9f2f3ff to
203080e
Compare
Cache the ParseDesc object constructed in recv_results_rows() so that repeated executions of the same prepared statement skip the list comprehensions, ColDesc construction, and make_deserializers() call.
The cache is keyed by id(column_metadata). For prepared statements the result_metadata list is stored on PreparedStatement and reused, so id() is stable. On cache hit we verify object identity (cached_ref is column_metadata) and that session-level settings (column_encryption_policy, protocol_version) still match.
A clear_parse_desc_cache() function is exposed for testing.
Benchmark results (median, pytest-benchmark)
ParseDesc construction only
Full pipeline (ParseDesc + row parsing)
For small result sets (single-row lookups common with prepared statements), ParseDesc construction is a large fraction of the total response-path cost. Caching eliminates it entirely after the first execution.
All 116 unit tests pass (1 skipped — pre-existing test_datetype issue).
Pre-review checklist
./docs/source/.Fixes:annotations to PR description.