Skip to content

Commit abb7765

Browse files
authored
RUBY-3299 Direct access to mongocrypt_binary_t for better decryption performance (#2899)
* add decryption benchmark * Access mongocrypt_binary_t fields directly for better performance Actual performance improvement is marginal for Ruby. * linter appeasement
1 parent a8fa54e commit abb7765

File tree

7 files changed

+150
-10
lines changed

7 files changed

+150
-10
lines changed

lib/mongo/crypt/binary.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ def write(data)
108108

109109
# Cannot write a string that's longer than the space currently allocated
110110
# by the mongocrypt_binary_t object
111-
str_p = Binding.mongocrypt_binary_data(ref)
112-
len = Binding.mongocrypt_binary_len(ref)
111+
str_p = Binding.get_binary_data_direct(ref)
112+
len = Binding.get_binary_len_direct(ref)
113113

114114
if len < data.bytesize
115115
raise ArgumentError.new(
116116
"Cannot write #{data.bytesize} bytes of data to a Binary object " +
117-
"that was initialized with #{Binding.mongocrypt_binary_len(@bin)} bytes."
117+
"that was initialized with #{Binding.get_binary_len_direct(@bin)} bytes."
118118
)
119119
end
120120

@@ -127,8 +127,8 @@ def write(data)
127127
#
128128
# @return [ String ] Data stored in the mongocrypt_binary_t as a string
129129
def to_s
130-
str_p = Binding.mongocrypt_binary_data(ref)
131-
len = Binding.mongocrypt_binary_len(ref)
130+
str_p = Binding.get_binary_data_direct(ref)
131+
len = Binding.get_binary_len_direct(ref)
132132
str_p.read_string(len)
133133
end
134134

lib/mongo/crypt/binding.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ def self.validate_version(lmc_version)
178178
# @return [ Integer ] The length of the data array.
179179
attach_function :mongocrypt_binary_len, [:pointer], :int
180180

181+
def self.get_binary_data_direct(mongocrypt_binary_t)
182+
mongocrypt_binary_t.get_pointer(0)
183+
end
184+
185+
def self.get_binary_len_direct(mongocrypt_binary_t)
186+
mongocrypt_binary_t.get_uint32(FFI::NativeType::POINTER.size)
187+
end
188+
181189
# @!method self.mongocrypt_binary_destroy(binary)
182190
# @api private
183191
#
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# frozen_string_literal: true
2+
3+
require 'mongo'
4+
require_relative '../base'
5+
6+
module Mongo
7+
module DriverBench
8+
module Crypto
9+
# Benchmark for reporting the performance of decrypting a document with
10+
# a large number of encrypted fields.
11+
class Decrypt < Mongo::DriverBench::Base
12+
ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
13+
KEY_VAULT_NAMESPACE = 'encryption.__keyVault'
14+
N = 10
15+
16+
def run
17+
doc = build_encrypted_doc
18+
19+
# warm up
20+
run_test(doc, 1)
21+
22+
[ 1, 2, 8, 64 ].each do |thread_count|
23+
run_test_with_thread_count(doc, thread_count)
24+
end
25+
end
26+
27+
private
28+
29+
def run_test_with_thread_count(doc, thread_count)
30+
results = []
31+
32+
N.times do
33+
threads = Array.new(thread_count) do
34+
Thread.new { Thread.current[:ops_sec] = run_test(doc, 1) }
35+
end
36+
37+
results << threads.each(&:join).sum { |t| t[:ops_sec] }
38+
end
39+
40+
median = results.sort[N / 2]
41+
puts "thread_count=#{thread_count}; median ops/sec=#{median}"
42+
end
43+
44+
def build_encrypted_doc
45+
data_key_id = client_encryption.create_data_key('local')
46+
47+
pairs = Array.new(1500) do |i|
48+
n = format('%04d', i + 1)
49+
key = "key#{n}"
50+
value = "value #{n}"
51+
52+
encrypted = client_encryption.encrypt(value,
53+
key_id: data_key_id,
54+
algorithm: ALGORITHM)
55+
56+
[ key, encrypted ]
57+
end
58+
59+
BSON::Document[pairs]
60+
end
61+
62+
def timeout_holder
63+
@timeout_holder ||= Mongo::CsotTimeoutHolder.new
64+
end
65+
66+
def encrypter
67+
@encrypter ||= Crypt::AutoEncrypter.new(
68+
client: new_client,
69+
key_vault_client: key_vault_client,
70+
key_vault_namespace: KEY_VAULT_NAMESPACE,
71+
kms_providers: kms_providers
72+
)
73+
end
74+
75+
def run_test(doc, duration)
76+
finish_at = Mongo::Utils.monotonic_time + duration
77+
count = 0
78+
79+
while Mongo::Utils.monotonic_time < finish_at
80+
encrypter.decrypt(doc, timeout_holder)
81+
count += 1
82+
end
83+
84+
count
85+
end
86+
87+
def key_vault_client
88+
@key_vault_client ||= new_client
89+
end
90+
91+
def kms_providers
92+
@kms_providers ||= { local: { key: SecureRandom.random_bytes(96) } }
93+
end
94+
95+
def client_encryption
96+
@client_encryption ||= Mongo::ClientEncryption.new(
97+
key_vault_client,
98+
key_vault_namespace: KEY_VAULT_NAMESPACE,
99+
kms_providers: kms_providers
100+
)
101+
end
102+
end
103+
end
104+
end
105+
end

profile/driver_bench/rake/tasks.rake

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# frozen_string_literal: true
22

3+
$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
4+
35
task driver_bench: %i[ driver_bench:data driver_bench:run ]
46

57
SPECS_REPO_URI = '[email protected]:mongodb/specifications.git'
68
SPECS_PATH = File.expand_path('../../../specifications', __dir__)
79
DRIVER_BENCH_DATA = File.expand_path('../../data/driver_bench', __dir__)
810

11+
# rubocop:disable Metrics/BlockLength
912
namespace :driver_bench do
1013
desc 'Downloads the DriverBench data files, if necessary'
1114
task :data do
@@ -35,4 +38,12 @@ namespace :driver_bench do
3538

3639
Mongo::DriverBench::Suite.run!
3740
end
41+
42+
desc 'Runs the crypto benchmark'
43+
task :crypto do
44+
require_relative '../crypto/decrypt'
45+
46+
Mongo::DriverBench::Crypto::Decrypt.new.run
47+
end
3848
end
49+
# rubocop:enable Metrics/BlockLength

spec/mongo/crypt/binding/binary_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,28 @@
4343
end
4444
end
4545

46+
describe '#get_binary_data_direct' do
47+
let(:binary) { Mongo::Crypt::Binding.mongocrypt_binary_new_from_data(bytes_pointer, bytes.length) }
48+
49+
it 'returns the pointer to the data' do
50+
expect(Mongo::Crypt::Binding.get_binary_data_direct(binary)).to eq(bytes_pointer)
51+
end
52+
end
53+
4654
describe '#mongocrypt_binary_len' do
4755
let(:binary) { Mongo::Crypt::Binding.mongocrypt_binary_new_from_data(bytes_pointer, bytes.length) }
4856

4957
it 'returns the length of the data' do
5058
expect(Mongo::Crypt::Binding.mongocrypt_binary_len(binary)).to eq(bytes.length)
5159
end
5260
end
61+
62+
describe '#get_binary_len_direct' do
63+
let(:binary) { Mongo::Crypt::Binding.mongocrypt_binary_new_from_data(bytes_pointer, bytes.length) }
64+
65+
it 'returns the length of the data' do
66+
expect(Mongo::Crypt::Binding.get_binary_len_direct(binary)).to eq(bytes.length)
67+
end
68+
end
5369
end
5470
end

spec/mongo/crypt/binding/context_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@
228228
it 'returns a BSON document' do
229229
expect(result).to be true
230230

231-
data = Mongo::Crypt::Binding.mongocrypt_binary_data(out_binary)
232-
len = Mongo::Crypt::Binding.mongocrypt_binary_len(out_binary)
231+
data = Mongo::Crypt::Binding.get_binary_data_direct(out_binary)
232+
len = Mongo::Crypt::Binding.get_binary_len_direct(out_binary)
233233

234234
response = data.get_array_of_uint8(0, len).pack('C*')
235235
expect(response).to be_a_kind_of(String)

spec/mongo/crypt/helpers/mongo_crypt_spec_helper.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ def mongocrypt_binary_t_from(string)
3030
private
3131

3232
def string_from_binary(binary_p)
33-
str_p = Mongo::Crypt::Binding.mongocrypt_binary_data(binary_p)
34-
len = Mongo::Crypt::Binding.mongocrypt_binary_len(binary_p)
33+
str_p = Mongo::Crypt::Binding.get_binary_data_direct(binary_p)
34+
len = Mongo::Crypt::Binding.get_binary_len_direct(binary_p)
3535
str_p.read_string(len)
3636
end
3737
module_function :string_from_binary
3838

3939
def write_to_binary(binary_p, data)
40-
str_p = Mongo::Crypt::Binding.mongocrypt_binary_data(binary_p)
40+
str_p = Mongo::Crypt::Binding.get_binary_data_direct(binary_p)
4141
str_p.put_bytes(0, data)
4242
end
4343
module_function :write_to_binary

0 commit comments

Comments
 (0)