Skip to content

Commit d73e202

Browse files
committed
perf: cache session.cluster as local in _set_result to avoid repeated double-lookup
In the ResultMessage hot path, self.session.cluster was accessed 3 times in the tablet routing block plus additional times in SET_KEYSPACE and SCHEMA_CHANGE branches. Cache session = self.session and cluster = session.cluster once at entry to eliminate redundant attribute-chain lookups. Also reuse the cached 'session' local for the SET_KEYSPACE and SCHEMA_CHANGE branches instead of re-reading self.session. Benchmark (5M iters): 3x self.session.cluster (old): 66.2 ns 1x local + 3x local (new): 39.9 ns Saving: 26.3 ns (1.66x)
1 parent 30e01b8 commit d73e202

2 files changed

Lines changed: 82 additions & 7 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env python
2+
"""
3+
Benchmark: caching self.session.cluster as a local variable.
4+
5+
Measures the cost of repeated self.session.cluster double-lookups
6+
vs. a single local assignment.
7+
"""
8+
9+
import sys
10+
import time
11+
12+
ITERS = 5_000_000
13+
14+
15+
class FakeCluster:
16+
class control_connection:
17+
_tablets_routing_v1 = True
18+
protocol_version = 5
19+
class metadata:
20+
class _tablets:
21+
@staticmethod
22+
def add_tablet(ks, tbl, tablet):
23+
pass
24+
25+
26+
class FakeSession:
27+
cluster = FakeCluster()
28+
29+
30+
class FakeResponseFuture:
31+
def __init__(self):
32+
self.session = FakeSession()
33+
34+
35+
def bench_double_lookup(rf, n):
36+
"""Simulates 3 accesses to self.session.cluster (tablet routing block)."""
37+
t0 = time.perf_counter_ns()
38+
for _ in range(n):
39+
_ = rf.session.cluster.control_connection
40+
_ = rf.session.cluster.protocol_version
41+
_ = rf.session.cluster.metadata
42+
return (time.perf_counter_ns() - t0) / n
43+
44+
45+
def bench_cached_local(rf, n):
46+
"""Simulates caching session.cluster in a local."""
47+
t0 = time.perf_counter_ns()
48+
for _ in range(n):
49+
cluster = rf.session.cluster
50+
_ = cluster.control_connection
51+
_ = cluster.protocol_version
52+
_ = cluster.metadata
53+
return (time.perf_counter_ns() - t0) / n
54+
55+
56+
def main():
57+
print(f"Python {sys.version}\n")
58+
rf = FakeResponseFuture()
59+
60+
ns_old = bench_double_lookup(rf, ITERS)
61+
ns_new = bench_cached_local(rf, ITERS)
62+
saving = ns_old - ns_new
63+
speedup = ns_old / ns_new if ns_new else float('inf')
64+
65+
print(f"=== self.session.cluster caching ({ITERS:,} iters) ===\n")
66+
print(f" 3x self.session.cluster (old): {ns_old:.1f} ns")
67+
print(f" 1x local + 3x local (new): {ns_new:.1f} ns")
68+
print(f" Saving: {saving:.1f} ns ({speedup:.2f}x)")
69+
70+
71+
if __name__ == "__main__":
72+
main()

cassandra/cluster.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4730,26 +4730,30 @@ def _set_result(self, host, connection, pool, response):
47304730
# custom_payload in __slots__, always initialised in __init__,
47314731
# so direct attribute access is safe and faster than getattr().
47324732
trace_id = response.trace_id
4733+
session = self.session
47334734
if trace_id:
47344735
if not self._query_traces:
47354736
self._query_traces = []
4736-
self._query_traces.append(QueryTrace(trace_id, self.session))
4737+
self._query_traces.append(QueryTrace(trace_id, session))
47374738

47384739
self._warnings = response.warnings
47394740
custom_payload = response.custom_payload
47404741
self._custom_payload = custom_payload
47414742

4742-
if custom_payload and self.session.cluster.control_connection._tablets_routing_v1:
4743+
# Cache session.cluster to avoid repeated double-lookup in the
4744+
# tablet routing block (3 accesses) and schema-change path.
4745+
cluster = session.cluster
4746+
if custom_payload and cluster.control_connection._tablets_routing_v1:
47434747
info = custom_payload.get('tablets-routing-v1')
47444748
if info is not None:
47454749
ctype = ResponseFuture._TABLET_ROUTING_CTYPE
47464750
if ctype is None:
47474751
ctype = types.lookup_casstype('TupleType(LongType, LongType, ListType(TupleType(UUIDType, Int32Type)))')
47484752
ResponseFuture._TABLET_ROUTING_CTYPE = ctype
4749-
first_token, last_token, tablet_replicas = ctype.from_binary(info, self.session.cluster.protocol_version)
4753+
first_token, last_token, tablet_replicas = ctype.from_binary(info, cluster.protocol_version)
47504754
tablet = Tablet.from_row(first_token, last_token, tablet_replicas)
47514755
if tablet is not None:
4752-
self.session.cluster.metadata._tablets.add_tablet(self.query.keyspace, self.query.table, tablet)
4756+
cluster.metadata._tablets.add_tablet(self.query.keyspace, self.query.table, tablet)
47534757

47544758
if response.kind == RESULT_KIND_ROWS:
47554759
self._paging_state = response.paging_state
@@ -4771,7 +4775,6 @@ def _set_result(self, host, connection, pool, response):
47714775
elif response.kind == RESULT_KIND_VOID:
47724776
self._set_final_result(None)
47734777
elif response.kind == RESULT_KIND_SET_KEYSPACE:
4774-
session = getattr(self, 'session', None)
47754778
# since we're running on the event loop thread, we need to
47764779
# use a non-blocking method for setting the keyspace on
47774780
# all connections in this session, otherwise the event
@@ -4786,9 +4789,9 @@ def _set_result(self, host, connection, pool, response):
47864789
# refresh the schema before responding, but do it in another
47874790
# thread instead of the event loop thread
47884791
self.is_schema_agreed = False
4789-
self.session.submit(
4792+
session.submit(
47904793
refresh_schema_and_set_result,
4791-
self.session.cluster.control_connection,
4794+
cluster.control_connection,
47924795
self, connection, **response.schema_change_event)
47934796
else:
47944797
self._set_final_result(response)

0 commit comments

Comments
 (0)