Skip to content

Commit 52769cd

Browse files
yahondaclaude
andcommitted
Add describe() benchmark and suite-hygiene tweaks
Adds script/benchmark_describe.rb comparing OracleEnhanced::Connection#describe across three implementations on a 1000-object schema (700 tables + 100 views + 100 private synonyms + 100 public synonyms): - master: UNION ALL over all_tables / all_views / all_synonyms - PR rsim#2521: single all_objects query - this POC: DBMS_UTILITY.NAME_RESOLVE Benchmark environment: - CPU: AMD Ryzen 9 7940HS (16 threads) - RAM: 60 GiB - OS: Ubuntu (kernel 7.0, x86_64) - Database: Oracle Database 23.26.1 Free, docker image oracle/database:23.26.1-free on localhost (no network hop) - Ruby: CRuby 4.0.2 (ruby-oci8 HEAD) / JRuby 10.0.5.0 (ojdbc17.jar) The absolute numbers below are not meaningful on their own — they reflect this one machine, one container, and one Oracle release. Only the relative differences between the three implementations matter. Shared fixtures across all six runs so every implementation hits the same shared-pool / dictionary cache state. Avg ms per describe() call (lower is better): case master PR rsim#2521 POC master PR rsim#2521 POC CRuby CRuby CRuby JRuby JRuby JRuby tables 0.744 0.311 0.104 1.501 0.723 0.308 views 0.647 0.251 0.107 0.957 0.507 0.254 private synonyms 1.440 1.157 0.234 2.184 1.908 0.235 public synonyms 1.275 0.990 0.258 1.862 1.585 0.230 all mixed 0.842 0.432 0.111 1.054 0.633 0.186 The "all mixed" row is the most representative of real Rails workloads: the POC is ~7.6x faster than master and ~3.9x faster than PR rsim#2521 on CRuby, and ~5.7x / ~3.4x on JRuby. The synonym win is the largest — NAME_RESOLVE follows the synonym server-side in one round trip, whereas PR rsim#2521 still issues a second all_synonyms query. Also adds a before(:suite) hook that runs PURGE RECYCLEBIN. Dropped tables from earlier spec runs accumulate in USER_RECYCLEBIN and can mask issues (ORA-00955 on re-create, stale BIN\$... entries in all_objects). The hook makes every rspec invocation start clean. Fixture counts stay tunable via TABLE_COUNT / VIEW_COUNT / SYNONYM_COUNT. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3b17f1a commit 52769cd

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

script/benchmark_describe.rb

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# frozen_string_literal: true
2+
3+
# Benchmark for OracleEnhanced::Connection#describe.
4+
#
5+
# Intent: compare the describe() implementations across
6+
# - master: UNION ALL over all_tables/all_views/all_synonyms
7+
# - PR #2521 branch (add-describe-regression-test): single all_objects query
8+
# - poc-dbms-utility-name-resolve: DBMS_UTILITY.NAME_RESOLVE
9+
#
10+
# The fixture models the production scenario from #2429: ~1000 objects in
11+
# the schema (700 tables, 100 views, 100 private synonyms, 100 public
12+
# synonyms).
13+
#
14+
# Usage (from repo root):
15+
# bundle exec ruby script/benchmark_describe.rb # setup + run + teardown
16+
# SKIP_SETUP=1 bundle exec ruby script/benchmark_describe.rb # reuse previous fixtures
17+
# SKIP_TEARDOWN=1 bundle exec ruby script/benchmark_describe.rb # keep fixtures for next run
18+
#
19+
# Environment variables honored (same defaults as spec_helper.rb):
20+
# DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_USER,
21+
# DATABASE_PASSWORD, ITERATIONS, TABLE_COUNT, VIEW_COUNT, SYNONYM_COUNT.
22+
23+
require "bundler/setup"
24+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
25+
require "oci8" if RUBY_ENGINE == "ruby"
26+
require "active_record"
27+
require "active_record/connection_adapters/oracle_enhanced_adapter"
28+
29+
TABLE_COUNT = Integer(ENV["TABLE_COUNT"] || 700)
30+
VIEW_COUNT = Integer(ENV["VIEW_COUNT"] || 100)
31+
SYNONYM_COUNT = Integer(ENV["SYNONYM_COUNT"] || 200) # half private, half public
32+
ITERATIONS = Integer(ENV["ITERATIONS"] || 1)
33+
SKIP_SETUP = ENV["SKIP_SETUP"] == "1"
34+
SKIP_TEARDOWN = ENV["SKIP_TEARDOWN"] == "1"
35+
36+
ActiveRecord::Base.establish_connection(
37+
adapter: "oracle_enhanced",
38+
database: ENV["DATABASE_NAME"] || "XEPDB1",
39+
host: ENV["DATABASE_HOST"] || "127.0.0.1",
40+
port: Integer(ENV["DATABASE_PORT"] || 1521),
41+
username: ENV["DATABASE_USER"] || "oracle_enhanced",
42+
password: ENV["DATABASE_PASSWORD"] || "oracle_enhanced"
43+
)
44+
45+
conn = ActiveRecord::Base.connection
46+
raw = conn.instance_variable_get(:@raw_connection) || conn.raw_connection
47+
owner = (ENV["DATABASE_USER"] || "oracle_enhanced").upcase
48+
49+
def t(label)
50+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51+
yield
52+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
53+
printf("%-20s %8.2fs\n", label, elapsed)
54+
end
55+
56+
def safe_exec(conn, sql)
57+
conn.execute(sql)
58+
rescue ActiveRecord::StatementInvalid
59+
# idempotent: ignore "already exists" / "does not exist"
60+
end
61+
62+
table_names = (1..TABLE_COUNT).map { |i| "bench_tbl_%04d" % i }
63+
view_names = (1..VIEW_COUNT).map { |i| "bench_vw_%04d" % i }
64+
priv_syn_half = SYNONYM_COUNT / 2
65+
pub_syn_half = SYNONYM_COUNT - priv_syn_half
66+
priv_synonyms = (1..priv_syn_half).map { |i| "bench_syn_%04d" % i }
67+
pub_synonyms = (1..pub_syn_half).map { |i| "bench_pub_%04d" % i }
68+
69+
unless SKIP_SETUP
70+
puts "==> Creating #{TABLE_COUNT} tables, #{VIEW_COUNT} views, " \
71+
"#{priv_syn_half} private + #{pub_syn_half} public synonyms"
72+
t("create tables") do
73+
table_names.each { |n| safe_exec conn, "CREATE TABLE #{n} (id NUMBER)" }
74+
end
75+
t("create views") do
76+
view_names.each_with_index do |vn, i|
77+
safe_exec conn, "CREATE VIEW #{vn} AS SELECT * FROM #{table_names[i % TABLE_COUNT]}"
78+
end
79+
end
80+
t("create synonyms") do
81+
priv_synonyms.each_with_index do |sn, i|
82+
safe_exec conn, "CREATE SYNONYM #{sn} FOR #{table_names[i % TABLE_COUNT]}"
83+
end
84+
pub_synonyms.each_with_index do |sn, i|
85+
safe_exec conn, "CREATE PUBLIC SYNONYM #{sn} FOR #{owner}.#{table_names[i % TABLE_COUNT]}"
86+
end
87+
end
88+
end
89+
90+
begin
91+
all_names = table_names + view_names + priv_synonyms + pub_synonyms
92+
93+
# Warm-up pass so dictionary-cache / shared-pool state is primed; the
94+
# first describe() after login otherwise dominates wall clock.
95+
all_names.first(50).each { |n| raw.send(:describe, n) }
96+
97+
head_branch = `git rev-parse --abbrev-ref HEAD`.strip
98+
head_sha = `git rev-parse --short HEAD`.strip
99+
puts
100+
puts "==> branch: #{head_branch} (#{head_sha})"
101+
puts "==> fixtures: #{TABLE_COUNT} tables, #{VIEW_COUNT} views, " \
102+
"#{priv_syn_half} private synonyms, #{pub_syn_half} public synonyms"
103+
puts "==> describe() calls per pass: #{all_names.size}"
104+
puts "==> passes: #{ITERATIONS}"
105+
puts
106+
107+
printf("%-20s %10s %10s\n", "case", "wall(s)", "avg(ms)")
108+
cases = {
109+
"tables" => table_names,
110+
"views" => view_names,
111+
"private synonyms" => priv_synonyms,
112+
"public synonyms" => pub_synonyms,
113+
"all mixed" => all_names.shuffle,
114+
}
115+
cases.each do |label, names|
116+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
117+
ITERATIONS.times { names.each { |n| raw.send(:describe, n) } }
118+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
119+
per_call_ms = (elapsed * 1000.0) / (ITERATIONS * names.size)
120+
printf("%-20s %10.3f %10.3f\n", label, elapsed, per_call_ms)
121+
end
122+
ensure
123+
unless SKIP_TEARDOWN
124+
puts
125+
puts "==> Dropping fixtures"
126+
t("drop public syns") { pub_synonyms.each { |n| safe_exec conn, "DROP PUBLIC SYNONYM #{n}" } }
127+
t("drop private syns") { priv_synonyms.each { |n| safe_exec conn, "DROP SYNONYM #{n}" } }
128+
t("drop views") { view_names.each { |n| safe_exec conn, "DROP VIEW #{n}" } }
129+
t("drop tables") { table_names.each { |n| safe_exec conn, "DROP TABLE #{n} PURGE" } }
130+
end
131+
end

spec/spec_helper.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,10 @@ def dump_table_schema(table, connection = ActiveRecord::Base.connection)
205205
ENV["TZ"] ||= config["timezone"] || "Europe/Riga"
206206

207207
ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
208+
209+
RSpec.configure do |config|
210+
config.before(:suite) do
211+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
212+
ActiveRecord::Base.connection.execute("PURGE RECYCLEBIN")
213+
end
214+
end

0 commit comments

Comments
 (0)