Skip to content

RUBY-3299 Direct access to mongocrypt_binary_t for better decryption performance #2899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 14, 2024
Merged
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
10 changes: 5 additions & 5 deletions lib/mongo/crypt/binary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ def write(data)

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

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

Expand All @@ -127,8 +127,8 @@ def write(data)
#
# @return [ String ] Data stored in the mongocrypt_binary_t as a string
def to_s
str_p = Binding.mongocrypt_binary_data(ref)
len = Binding.mongocrypt_binary_len(ref)
str_p = Binding.get_binary_data_direct(ref)
len = Binding.get_binary_len_direct(ref)
str_p.read_string(len)
end

Expand Down
8 changes: 8 additions & 0 deletions lib/mongo/crypt/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ def self.validate_version(lmc_version)
# @return [ Integer ] The length of the data array.
attach_function :mongocrypt_binary_len, [:pointer], :int

def self.get_binary_data_direct(mongocrypt_binary_t)
mongocrypt_binary_t.get_pointer(0)
end

def self.get_binary_len_direct(mongocrypt_binary_t)
mongocrypt_binary_t.get_uint32(FFI::NativeType::POINTER.size)
end

# @!method self.mongocrypt_binary_destroy(binary)
# @api private
#
Expand Down
105 changes: 105 additions & 0 deletions profile/driver_bench/crypto/decrypt.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

require 'mongo'
require_relative '../base'

module Mongo
module DriverBench
module Crypto
# Benchmark for reporting the performance of decrypting a document with
# a large number of encrypted fields.
class Decrypt < Mongo::DriverBench::Base
ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
KEY_VAULT_NAMESPACE = 'encryption.__keyVault'
N = 10

def run
doc = build_encrypted_doc

# warm up
run_test(doc, 1)

[ 1, 2, 8, 64 ].each do |thread_count|
run_test_with_thread_count(doc, thread_count)
end
end

private

def run_test_with_thread_count(doc, thread_count)
results = []

N.times do
threads = Array.new(thread_count) do
Thread.new { Thread.current[:ops_sec] = run_test(doc, 1) }
end

results << threads.each(&:join).sum { |t| t[:ops_sec] }
end

median = results.sort[N / 2]
puts "thread_count=#{thread_count}; median ops/sec=#{median}"
end

def build_encrypted_doc
data_key_id = client_encryption.create_data_key('local')

pairs = Array.new(1500) do |i|
n = format('%04d', i + 1)
key = "key#{n}"
value = "value #{n}"

encrypted = client_encryption.encrypt(value,
key_id: data_key_id,
algorithm: ALGORITHM)

[ key, encrypted ]
end

BSON::Document[pairs]
end

def timeout_holder
@timeout_holder ||= Mongo::CsotTimeoutHolder.new
end

def encrypter
@encrypter ||= Crypt::AutoEncrypter.new(
client: new_client,
key_vault_client: key_vault_client,
key_vault_namespace: KEY_VAULT_NAMESPACE,
kms_providers: kms_providers
)
end

def run_test(doc, duration)
finish_at = Mongo::Utils.monotonic_time + duration
count = 0

while Mongo::Utils.monotonic_time < finish_at
encrypter.decrypt(doc, timeout_holder)
count += 1
end

count
end

def key_vault_client
@key_vault_client ||= new_client
end

def kms_providers
@kms_providers ||= { local: { key: SecureRandom.random_bytes(96) } }
end

def client_encryption
@client_encryption ||= Mongo::ClientEncryption.new(
key_vault_client,
key_vault_namespace: KEY_VAULT_NAMESPACE,
kms_providers: kms_providers
)
end
end
end
end
end
11 changes: 11 additions & 0 deletions profile/driver_bench/rake/tasks.rake
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# frozen_string_literal: true

$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)

task driver_bench: %i[ driver_bench:data driver_bench:run ]

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

# rubocop:disable Metrics/BlockLength
namespace :driver_bench do
desc 'Downloads the DriverBench data files, if necessary'
task :data do
Expand Down Expand Up @@ -35,4 +38,12 @@ namespace :driver_bench do

Mongo::DriverBench::Suite.run!
end

desc 'Runs the crypto benchmark'
task :crypto do
require_relative '../crypto/decrypt'

Mongo::DriverBench::Crypto::Decrypt.new.run
end
end
# rubocop:enable Metrics/BlockLength
16 changes: 16 additions & 0 deletions spec/mongo/crypt/binding/binary_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,28 @@
end
end

describe '#get_binary_data_direct' do
let(:binary) { Mongo::Crypt::Binding.mongocrypt_binary_new_from_data(bytes_pointer, bytes.length) }

it 'returns the pointer to the data' do
expect(Mongo::Crypt::Binding.get_binary_data_direct(binary)).to eq(bytes_pointer)
end
end

describe '#mongocrypt_binary_len' do
let(:binary) { Mongo::Crypt::Binding.mongocrypt_binary_new_from_data(bytes_pointer, bytes.length) }

it 'returns the length of the data' do
expect(Mongo::Crypt::Binding.mongocrypt_binary_len(binary)).to eq(bytes.length)
end
end

describe '#get_binary_len_direct' do
let(:binary) { Mongo::Crypt::Binding.mongocrypt_binary_new_from_data(bytes_pointer, bytes.length) }

it 'returns the length of the data' do
expect(Mongo::Crypt::Binding.get_binary_len_direct(binary)).to eq(bytes.length)
end
end
end
end
4 changes: 2 additions & 2 deletions spec/mongo/crypt/binding/context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@
it 'returns a BSON document' do
expect(result).to be true

data = Mongo::Crypt::Binding.mongocrypt_binary_data(out_binary)
len = Mongo::Crypt::Binding.mongocrypt_binary_len(out_binary)
data = Mongo::Crypt::Binding.get_binary_data_direct(out_binary)
len = Mongo::Crypt::Binding.get_binary_len_direct(out_binary)

response = data.get_array_of_uint8(0, len).pack('C*')
expect(response).to be_a_kind_of(String)
Expand Down
6 changes: 3 additions & 3 deletions spec/mongo/crypt/helpers/mongo_crypt_spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ def mongocrypt_binary_t_from(string)
private

def string_from_binary(binary_p)
str_p = Mongo::Crypt::Binding.mongocrypt_binary_data(binary_p)
len = Mongo::Crypt::Binding.mongocrypt_binary_len(binary_p)
str_p = Mongo::Crypt::Binding.get_binary_data_direct(binary_p)
len = Mongo::Crypt::Binding.get_binary_len_direct(binary_p)
str_p.read_string(len)
end
module_function :string_from_binary

def write_to_binary(binary_p, data)
str_p = Mongo::Crypt::Binding.mongocrypt_binary_data(binary_p)
str_p = Mongo::Crypt::Binding.get_binary_data_direct(binary_p)
str_p.put_bytes(0, data)
end
module_function :write_to_binary
Expand Down
Loading