Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions benchmarks/micro/bench_col_names_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright ScyllaDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Micro-benchmark: column_names / column_types extraction from metadata.

Measures the cost of building [c[2] for c in metadata] and [c[3] for c in metadata]
vs using pre-cached lists (as done for prepared statements with result_metadata).

Run:
python benchmarks/bench_col_names_cache.py
"""

import sys
import timeit


def make_column_metadata(ncols):
"""Create fake column_metadata tuples like recv_results_metadata produces."""
class FakeType:
pass
return [(f"ks_{i}", f"tbl_{i}", f"col_{i}", FakeType) for i in range(ncols)]


def bench():
for ncols in (5, 10, 20, 50):
metadata = make_column_metadata(ncols)

# Pre-cached (done once at prepare time)
cached_names = [c[2] for c in metadata]
cached_types = [c[3] for c in metadata]

def extract_uncached():
names = [c[2] for c in metadata]
types = [c[3] for c in metadata]
return names, types

def extract_cached():
return cached_names, cached_types

n = 500_000
t_uncached = timeit.timeit(extract_uncached, number=n)
t_cached = timeit.timeit(extract_cached, number=n)

saving_ns = (t_uncached - t_cached) / n * 1e9
speedup = t_uncached / t_cached if t_cached > 0 else float('inf')
print(f" {ncols} cols: uncached={t_uncached / n * 1e9:.1f} ns, "
f"cached={t_cached / n * 1e9:.1f} ns, "
f"saving={saving_ns:.1f} ns ({speedup:.1f}x)")


if __name__ == "__main__":
print(f"Python {sys.version}")
print("\n=== column_names / column_types extraction ===")
bench()
115 changes: 115 additions & 0 deletions benchmarks/micro/bench_result_kind_dispatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright ScyllaDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Micro-benchmark: RESULT_KIND dispatch ordering and getattr vs direct access.

Measures the cost difference between:
1. Checking RESULT_KIND_ROWS first vs third in the if/elif chain
2. getattr(msg, 'continuous_paging_options', None) vs msg.continuous_paging_options

Run:
python benchmarks/bench_result_kind_dispatch.py
"""

import sys
import timeit


def bench():
n = 2_000_000

# Simulate the result kind values
RESULT_KIND_SET_KEYSPACE = 0x0003
RESULT_KIND_SCHEMA_CHANGE = 0x0005
RESULT_KIND_ROWS = 0x0002
RESULT_KIND_VOID = 0x0001

kind = RESULT_KIND_ROWS # the common case

# Old order: SET_KEYSPACE, SCHEMA_CHANGE, ROWS, VOID
def old_dispatch():
if kind == RESULT_KIND_SET_KEYSPACE:
return 'set_keyspace'
elif kind == RESULT_KIND_SCHEMA_CHANGE:
return 'schema_change'
elif kind == RESULT_KIND_ROWS:
return 'rows'
elif kind == RESULT_KIND_VOID:
return 'void'

# New order: ROWS, VOID, SET_KEYSPACE, SCHEMA_CHANGE
def new_dispatch():
if kind == RESULT_KIND_ROWS:
return 'rows'
elif kind == RESULT_KIND_VOID:
return 'void'
elif kind == RESULT_KIND_SET_KEYSPACE:
return 'set_keyspace'
elif kind == RESULT_KIND_SCHEMA_CHANGE:
return 'schema_change'

print(f"=== RESULT_KIND dispatch order ({n:,} iters) ===\n")

# Warmup
for _ in range(10000):
old_dispatch()
new_dispatch()

t_old = timeit.timeit(old_dispatch, number=n)
t_new = timeit.timeit(new_dispatch, number=n)
ns_old = t_old / n * 1e9
ns_new = t_new / n * 1e9
saving = ns_old - ns_new
speedup = ns_old / ns_new if ns_new > 0 else float('inf')
print(f" Old (ROWS=3rd): {ns_old:.1f} ns")
print(f" New (ROWS=1st): {ns_new:.1f} ns")
print(f" Saving: {saving:.1f} ns ({speedup:.2f}x)")

# getattr vs direct attribute access
print(f"\n=== getattr vs direct attribute access ({n:,} iters) ===\n")

class OldMsg:
pass

class NewMsg:
continuous_paging_options = None

old_msg = OldMsg()
new_msg = NewMsg()

def old_getattr():
return getattr(old_msg, 'continuous_paging_options', None)

def new_direct():
return new_msg.continuous_paging_options

for _ in range(10000):
old_getattr()
new_direct()

t_old = timeit.timeit(old_getattr, number=n)
t_new = timeit.timeit(new_direct, number=n)
ns_old = t_old / n * 1e9
ns_new = t_new / n * 1e9
saving = ns_old - ns_new
speedup = ns_old / ns_new if ns_new > 0 else float('inf')
print(f" getattr(msg, 'continuous_paging_options', None): {ns_old:.1f} ns")
print(f" msg.continuous_paging_options: {ns_new:.1f} ns")
print(f" Saving: {saving:.1f} ns ({speedup:.2f}x)")


if __name__ == "__main__":
print(f"Python {sys.version}\n")
bench()
72 changes: 72 additions & 0 deletions benchmarks/micro/bench_session_cluster_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python
"""
Benchmark: caching self.session.cluster as a local variable.

Measures the cost of repeated self.session.cluster double-lookups
vs. a single local assignment.
"""

import sys
import time

ITERS = 5_000_000


class FakeCluster:
class control_connection:
_tablets_routing_v1 = True
protocol_version = 5
class metadata:
class _tablets:
@staticmethod
def add_tablet(ks, tbl, tablet):
pass


class FakeSession:
cluster = FakeCluster()


class FakeResponseFuture:
def __init__(self):
self.session = FakeSession()


def bench_double_lookup(rf, n):
"""Simulates 3 accesses to self.session.cluster (tablet routing block)."""
t0 = time.perf_counter_ns()
for _ in range(n):
_ = rf.session.cluster.control_connection
_ = rf.session.cluster.protocol_version
_ = rf.session.cluster.metadata
return (time.perf_counter_ns() - t0) / n


def bench_cached_local(rf, n):
"""Simulates caching session.cluster in a local."""
t0 = time.perf_counter_ns()
for _ in range(n):
cluster = rf.session.cluster
_ = cluster.control_connection
_ = cluster.protocol_version
_ = cluster.metadata
return (time.perf_counter_ns() - t0) / n


def main():
print(f"Python {sys.version}\n")
rf = FakeResponseFuture()

ns_old = bench_double_lookup(rf, ITERS)
ns_new = bench_cached_local(rf, ITERS)
saving = ns_old - ns_new
speedup = ns_old / ns_new if ns_new else float('inf')

print(f"=== self.session.cluster caching ({ITERS:,} iters) ===\n")
print(f" 3x self.session.cluster (old): {ns_old:.1f} ns")
print(f" 1x local + 3x local (new): {ns_new:.1f} ns")
print(f" Saving: {saving:.1f} ns ({speedup:.2f}x)")


if __name__ == "__main__":
main()
Loading
Loading