diff --git a/Matrixfile b/Matrixfile index 1cbd9461de6..54db12cdf67 100644 --- a/Matrixfile +++ b/Matrixfile @@ -7,18 +7,12 @@ '' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby', 'core-old' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby' }, - 'crashtracking' => { + 'core_with_libdatadog_api' => { '' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ❌ jruby', }, 'error_tracking' => { '' => '❌ 2.5 / ❌ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ❌ jruby', }, - 'process_discovery' => { - '' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ❌ jruby' - }, - 'stable_config' => { - '' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ❌ jruby' - }, 'appsec:main' => { '' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby' }, diff --git a/Rakefile b/Rakefile index 14b9fb1a4a1..99d970ce435 100644 --- a/Rakefile +++ b/Rakefile @@ -22,6 +22,13 @@ Dir.glob('tasks/*.rake').each { |r| import r } TEST_METADATA = eval(File.read('Matrixfile')).freeze # rubocop:disable Security/Eval +CORE_WITH_LIBDATADOG_API = [ + 'spec/datadog/core/crashtracking/**/*_spec.rb', + 'spec/datadog/core/process_discovery_spec.rb', + 'spec/datadog/core/configuration/stable_config_spec.rb', + 'spec/datadog/core/ddsketch_spec.rb', +].freeze + # rubocop:disable Metrics/BlockLength namespace :test do desc 'Run all tests' @@ -75,8 +82,9 @@ namespace :spec do desc '' # "Explicitly hiding from `rake -T`" RSpec::Core::RakeTask.new(:main) do |t, args| t.pattern = 'spec/**/*_spec.rb' - t.exclude_pattern = 'spec/**/{appsec/integration,contrib,benchmark,redis,auto_instrument,opentelemetry,profiling,crashtracking,error_tracking}/**/*_spec.rb,'\ - ' spec/**/{auto_instrument,opentelemetry,process_discovery,stable_config}_spec.rb, spec/datadog/gem_packaging_spec.rb' + t.exclude_pattern = 'spec/**/{appsec/integration,contrib,benchmark,redis,auto_instrument,opentelemetry,profiling,error_tracking}/**/*_spec.rb, ' \ + 'spec/**/{auto_instrument,opentelemetry}_spec.rb, spec/datadog/gem_packaging_spec.rb, ' \ + "#{CORE_WITH_LIBDATADOG_API.join(', ')}" t.rspec_opts = args.to_a.join(' ') end @@ -197,27 +205,8 @@ namespace :spec do end # rubocop:disable Style/MultilineBlockChain - RSpec::Core::RakeTask.new(:crashtracking) do |t, args| - t.pattern = 'spec/datadog/core/crashtracking/**/*_spec.rb' - t.rspec_opts = args.to_a.join(' ') - end.tap do |t| - Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"]) - end - # rubocop:enable Style/MultilineBlockChain - - # rubocop:disable Style/MultilineBlockChain - RSpec::Core::RakeTask.new(:process_discovery) do |t, args| - t.pattern = 'spec/datadog/core/process_discovery_spec.rb' - t.rspec_opts = args.to_a.join(' ') - end.tap do |t| - Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"]) - end - # rubocop:enable Style/MultilineBlockChain - - # rubocop:disable Style/MultilineBlockChain - desc '' # "Explicitly hiding from `rake -T`" - RSpec::Core::RakeTask.new(:stable_config) do |t, args| - t.pattern = 'spec/datadog/core/configuration/stable_config_spec.rb' + RSpec::Core::RakeTask.new(:core_with_libdatadog_api) do |t, args| + t.pattern = CORE_WITH_LIBDATADOG_API.join(', ') t.rspec_opts = args.to_a.join(' ') end.tap do |t| Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"]) diff --git a/ext/LIBDATADOG_DEVELOPMENT.md b/ext/LIBDATADOG_DEVELOPMENT.md new file mode 100644 index 00000000000..310f8a67b42 --- /dev/null +++ b/ext/LIBDATADOG_DEVELOPMENT.md @@ -0,0 +1,60 @@ +# Libdatadog development + +These instructions can quickly get outdated, so feel free to open an issue if they're not working (and/or ping @ivoanjo). + +## Using libdatadog builds from CI or GitHub + +If you're developing inside docker/natively on Linux, you can use libdatadog builds from CI and GitHub. + +Here's what to do: + +1. Create a folder for extracting libdatadog into based on your ruby platform (for instance inside the dd-trace-rb repo): + +```bash +export DD_RUBY_PLATFORM=`ruby -e 'puts Gem::Platform.local.to_s'` +echo "Current ruby platform: $DD_RUBY_PLATFORM" +mkdir -p my-libdatadog-build/$DD_RUBY_PLATFORM +``` + +2. Find a libdatadog build from CI or [GitHub releases](https://github.com/DataDog/libdatadog/releases). This should match the Ruby platform seen above. +3. Extract the libdatadog build into the folder: + +```bash +# In this example the build is in my downloads; notice the use of strip-components to get the correct folder structure +tar zxvf ~/Downloads/libdatadog-x86_64-unknown-linux-gnu.tar.gz -C my-libdatadog-build/$DD_RUBY_PLATFORM/ --strip-components=1 +# Here's how it should look after +ls my-libdatadog-build/$DD_RUBY_PLATFORM +bin cmake include lib LICENSE LICENSE-3rdparty.yml NOTICE +``` + +6. Tell Ruby where to find libdatadog: ```export LIBDATADOG_VENDOR_OVERRIDE=`pwd`/my-libdatadog-build/``` (Notice no platform + use of pwd for full path here) +7. From dd-trace-rb, run `bundle exec rake clean compile` +8. For incremental builds, usually `bundle exec rake compile` is faster and `clean` is not needed + +If you additionally want to run the profiler test suite, also remember to `export DD_PROFILING_MACOS_TESTING=true` and re-run `rake clean compile`. + +## Native development on macOS + +As of this writing (August 2025), the libdatadog builds on rubygems.org only support Linux. + +We don't officially support using libdatadog for Ruby on other platforms yet, but it is possible to use it for local development on macOS. +(**Note that you don't need these instructions if you develop inside docker.**) + +Here's how you can do so: + +1. [Install rust](https://www.rust-lang.org/tools/install) +2. Install `cbindgen`: `cargo install cbindgen` +3. Clone [libdatadog](https://github.com/datadog/libdatadog) +4. Create a folder for building into based on your ruby platform: + +```bash +export DD_RUBY_PLATFORM=`ruby -e 'puts Gem::Platform.local.to_s'` +mkdir -p my-libdatadog-build/$DD_RUBY_PLATFORM +``` + +5. From inside of the libdatadog repo, build libdatadog into this folder: `./build-profiling-ffi.sh my-libdatadog-build/$DD_RUBY_PLATFORM` +6. Tell Ruby where to find libdatadog: `export LIBDATADOG_VENDOR_OVERRIDE=/adjust/this/to/be/the/full/path/to/my-libdatadog-build/` (Notice no platform here) +7. From dd-trace-rb, run `bundle exec rake clean compile` +8. For incremental builds, usually `bundle exec rake compile` is faster and `clean` is not needed + +If you additionally want to run the profiler test suite, also remember to `export DD_PROFILING_MACOS_TESTING=true` and re-run `rake clean compile`. diff --git a/ext/libdatadog_api/ddsketch.c b/ext/libdatadog_api/ddsketch.c new file mode 100644 index 00000000000..d0deab44955 --- /dev/null +++ b/ext/libdatadog_api/ddsketch.c @@ -0,0 +1,106 @@ +#include +#include + +#include "datadog_ruby_common.h" + +static VALUE _native_new(VALUE klass); +static void ddsketch_free(void *ptr); +static VALUE native_add(VALUE self, VALUE point); +static VALUE native_add_with_count(VALUE self, VALUE point, VALUE count); +static VALUE native_count(VALUE self); +static VALUE native_encode(VALUE self); +NORETURN(static void raise_ddsketch_error(const char *message, ddog_VoidResult result)); + +void ddsketch_init(VALUE core_module) { + VALUE ddsketch_class = rb_define_class_under(core_module, "DDSketch", rb_cObject); + + rb_define_alloc_func(ddsketch_class, _native_new); + rb_define_method(ddsketch_class, "add", native_add, 1); + rb_define_method(ddsketch_class, "add_with_count", native_add_with_count, 2); + rb_define_method(ddsketch_class, "count", native_count, 0); + rb_define_method(ddsketch_class, "encode", native_encode, 0); +} + +// This structure is used to define a Ruby object that stores a pointer to a ddsketch_Handle_DDSketch +// See also https://github.com/ruby/ruby/blob/master/doc/extension.rdoc for how this works +static const rb_data_type_t ddsketch_typed_data = { + .wrap_struct_name = "Datadog::DDSketch", + .function = { + .dmark = NULL, // We don't store references to Ruby objects so we don't need to mark any of them + .dfree = ddsketch_free, + .dsize = NULL, // We don't track memory usage (although it'd be cool if we did!) + //.dcompact = NULL, // Not needed -- we don't store references to Ruby objects + }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY +}; + +static VALUE _native_new(VALUE klass) { + ddsketch_Handle_DDSketch *state = ruby_xcalloc(1, sizeof(ddsketch_Handle_DDSketch)); + + *state = ddog_ddsketch_new(); + + return TypedData_Wrap_Struct(klass, &ddsketch_typed_data, state); +} + +static void ddsketch_free(void *ptr) { + ddsketch_Handle_DDSketch *state = (ddsketch_Handle_DDSketch *) ptr; + ddog_ddsketch_drop(state); + ruby_xfree(ptr); +} + +static void raise_ddsketch_error(const char *message, ddog_VoidResult result) { + rb_raise(rb_eRuntimeError, "%s: %"PRIsVALUE, message, get_error_details_and_drop(&result.err)); +} + +static VALUE native_add(VALUE self, VALUE point) { + ddsketch_Handle_DDSketch *state; + TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state); + + ddog_VoidResult result = ddog_ddsketch_add(state, NUM2DBL(point)); + + if (result.tag == DDOG_VOID_RESULT_ERR) raise_ddsketch_error("DDSketch add failed", result); + + return self; +} + +static VALUE native_add_with_count(VALUE self, VALUE point, VALUE count) { + ddsketch_Handle_DDSketch *state; + TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state); + + ddog_VoidResult result = ddog_ddsketch_add_with_count(state, NUM2DBL(point), NUM2DBL(count)); + + if (result.tag == DDOG_VOID_RESULT_ERR) raise_ddsketch_error("DDSketch add_with_count failed", result); + + return self; +} + +static VALUE native_count(VALUE self) { + ddsketch_Handle_DDSketch *state; + TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state); + + double count_out; + ddog_VoidResult result = ddog_ddsketch_count(state, &count_out); + + if (result.tag == DDOG_VOID_RESULT_ERR) raise_ddsketch_error("DDSketch count failed", result); + + return DBL2NUM(count_out); +} + +static VALUE native_encode(VALUE self) { + ddsketch_Handle_DDSketch *state; + TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state); + + ddog_Vec_U8 encoded = ddog_ddsketch_encode(state); + + // Copy into a Ruby string + VALUE bytes = rb_str_new((const char *) encoded.ptr, encoded.len); + + ddog_Vec_U8_drop(encoded); + + // The sketch is consumed by encode; to make this a bit more user-friendly for + // a Ruby API (since we can't "kill" the Ruby object), let's re-initialize it so + // it can be used again. + *state = ddog_ddsketch_new(); + + return bytes; +} diff --git a/ext/libdatadog_api/init.c b/ext/libdatadog_api/init.c index fe713bbae60..132272d5354 100644 --- a/ext/libdatadog_api/init.c +++ b/ext/libdatadog_api/init.c @@ -5,6 +5,8 @@ #include "process_discovery.h" #include "library_config.h" +void ddsketch_init(VALUE core_module); + void DDTRACE_EXPORT Init_libdatadog_api(void) { VALUE datadog_module = rb_define_module("Datadog"); VALUE core_module = rb_define_module_under(datadog_module, "Core"); @@ -12,4 +14,5 @@ void DDTRACE_EXPORT Init_libdatadog_api(void) { crashtracker_init(core_module); process_discovery_init(core_module); library_config_init(core_module); + ddsketch_init(core_module); } diff --git a/ext/libdatadog_api/macos_development.md b/ext/libdatadog_api/macos_development.md deleted file mode 100644 index 399a9d40c4f..00000000000 --- a/ext/libdatadog_api/macos_development.md +++ /dev/null @@ -1,26 +0,0 @@ -# Developing on macOS - -As of this writing (August 2024), the libdatadog builds on rubygems.org only support Linux. - -We don't officially support using libdatadog for Ruby on other platforms yet, but it is possible to use it for local development on macOS. -(**Note that you don't need these instructions if you develop inside docker.**) - -Here's how you can do so: - -1. [Install rust](https://www.rust-lang.org/tools/install) -2. Install `cbindgen`: `cargo install cbindgen` -3. Clone [libdatadog](https://github.com/datadog/libdatadog) -4. Create a folder for building into based on your ruby platform: - -``` -export DD_RUBY_PLATFORM=`ruby -e 'puts Gem::Platform.local.to_s'` -mkdir -p my-libdatadog-build/$DD_RUBY_PLATFORM -``` - -5. From inside of the libdatadog repo, build libdatadog into this folder: `./build-profiling-ffi.sh my-libdatadog-build/$DD_RUBY_PLATFORM` -6. Tell Ruby where to find libdatadog: `export LIBDATADOG_VENDOR_OVERRIDE=/full/path/to/my-libdatadog-build/` (Notice no platform here) -7. From dd-trace-rb, run `bundle exec rake clean compile` - -If you additionally want to run the profiler test suite, also remember to `export DD_PROFILING_MACOS_TESTING=true` and re-run `rake clean compile`. - -These instructions can quickly get outdated, so feel free to open an issue if they're not working (and/or ping @ivoanjo). diff --git a/lib/datadog/core/ddsketch.rb b/lib/datadog/core/ddsketch.rb new file mode 100644 index 00000000000..51de65b1983 --- /dev/null +++ b/lib/datadog/core/ddsketch.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'datadog/core' + +module Datadog + module Core + # Used to access ddsketch APIs. + # APIs in this class are implemented as native code. + class DDSketch + def self.supported? + Datadog::Core::LIBDATADOG_API_FAILURE.nil? + end + + def initialize + unless self.class.supported? + raise(ArgumentError, "DDSketch is not supported: #{Datadog::Core::LIBDATADOG_API_FAILURE}") + end + end + end + end +end diff --git a/sig/datadog/core/ddsketch.rbs b/sig/datadog/core/ddsketch.rbs new file mode 100644 index 00000000000..bb24c2bf041 --- /dev/null +++ b/sig/datadog/core/ddsketch.rbs @@ -0,0 +1,26 @@ +module Datadog + module Core + class DDSketch + def self.supported?: () -> bool + + # Adds a single point to the sketch + # @param point [::Numeric] The value to add to the sketch + # @return [true] Always returns true on success, raises RuntimeError on failure + def add: (::Numeric point) -> true + + # Adds a point with a count to the sketch + # @param point [::Numeric] The value to add to the sketch + # @param count [::Numeric] The count/weight for this point + # @return [true] Always returns true on success, raises RuntimeError on failure + def add_with_count: (::Numeric point, ::Numeric count) -> true + + # Returns the total count of points in the sketch + # @return [::Float] The total count of points + def count: () -> ::Float + + # Encodes the sketch to bytes and resets it for reuse + # @return [::String] The encoded sketch as a binary string + def encode: () -> ::String + end + end +end diff --git a/spec/datadog/core/ddsketch_pprof/ddsketch.proto b/spec/datadog/core/ddsketch_pprof/ddsketch.proto new file mode 100644 index 00000000000..a284ea1ac9b --- /dev/null +++ b/spec/datadog/core/ddsketch_pprof/ddsketch.proto @@ -0,0 +1,68 @@ +/* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2021 Datadog, Inc. + */ + +syntax = "proto3"; + +option go_package = "github.com/DataDog/sketches-go/ddsketch/pb/sketchpb"; + +package test; + +// A DDSketch is essentially a histogram that partitions the range of positive values into an infinite number of +// indexed bins whose size grows exponentially. It keeps track of the number of values (or possibly floating-point +// weights) added to each bin. Negative values are partitioned like positive values, symmetrically to zero. +// The value zero as well as its close neighborhood that would be mapped to extreme bin indexes is mapped to a specific +// counter. +message DDSketch { + // The mapping between positive values and the bin indexes they belong to. + IndexMapping mapping = 1; + + // The store for keeping track of positive values. + Store positiveValues = 2; + + // The store for keeping track of negative values. A negative value v is mapped using its positive opposite -v. + Store negativeValues = 3; + + // The count for the value zero and its close neighborhood (whose width depends on the mapping). + double zeroCount = 4; +} + +// How to map positive values to the bins they belong to. +message IndexMapping { + // The gamma parameter of the mapping, such that bin index that a value v belongs to is roughly equal to + // log(v)/log(gamma). + double gamma = 1; + + // An offset that can be used to shift all bin indexes. + double indexOffset = 2; + + // To speed up the computation of the index a value belongs to, the computation of the log may be approximated using + // the fact that the log to the base 2 of powers of 2 can be computed at a low cost from the binary representation of + // the input value. Other values can be approximated by interpolating between successive powers of 2 (linearly, + // quadratically or cubically). + // NONE means that the log is to be computed exactly (no interpolation). + Interpolation interpolation = 3; + enum Interpolation { + NONE = 0; + LINEAR = 1; + QUADRATIC = 2; + CUBIC = 3; + } +} + +// A Store maps bin indexes to their respective counts. +// Counts can be encoded sparsely using binCounts, but also in a contiguous way using contiguousBinCounts and +// contiguousBinIndexOffset. Given that non-empty bins are in practice usually contiguous or close to one another, the +// latter contiguous encoding method is usually more efficient than the sparse one. +// Both encoding methods can be used conjointly. If a bin appears in both the sparse and the contiguous encodings, its +// count value is the sum of the counts in each encodings. +message Store { + // The bin counts, encoded sparsely. + map binCounts = 1; + + // The bin counts, encoded contiguously. The values of contiguousBinCounts are the counts for the bins of indexes + // o, o+1, o+2, etc., where o is contiguousBinIndexOffset. + repeated double contiguousBinCounts = 2 [packed = true]; + sint32 contiguousBinIndexOffset = 3; +} diff --git a/spec/datadog/core/ddsketch_pprof/ddsketch_pb.rb b/spec/datadog/core/ddsketch_pprof/ddsketch_pb.rb new file mode 100644 index 00000000000..a84686aa58a --- /dev/null +++ b/spec/datadog/core/ddsketch_pprof/ddsketch_pb.rb @@ -0,0 +1,38 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: spec/datadog/core/ddsketch_pprof/ddsketch.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("spec/datadog/core/ddsketch_pprof/ddsketch.proto", :syntax => :proto3) do + add_message "test.DDSketch" do + optional :mapping, :message, 1, "test.IndexMapping" + optional :positiveValues, :message, 2, "test.Store" + optional :negativeValues, :message, 3, "test.Store" + optional :zeroCount, :double, 4 + end + add_message "test.IndexMapping" do + optional :gamma, :double, 1 + optional :indexOffset, :double, 2 + optional :interpolation, :enum, 3, "test.IndexMapping.Interpolation" + end + add_enum "test.IndexMapping.Interpolation" do + value :NONE, 0 + value :LINEAR, 1 + value :QUADRATIC, 2 + value :CUBIC, 3 + end + add_message "test.Store" do + map :binCounts, :sint32, :double, 1 + repeated :contiguousBinCounts, :double, 2 + optional :contiguousBinIndexOffset, :sint32, 3 + end + end +end + +module Test + DDSketch = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.DDSketch").msgclass + IndexMapping = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.IndexMapping").msgclass + IndexMapping::Interpolation = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.IndexMapping.Interpolation").enummodule + Store = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("test.Store").msgclass +end diff --git a/spec/datadog/core/ddsketch_spec.rb b/spec/datadog/core/ddsketch_spec.rb new file mode 100644 index 00000000000..ae024632a59 --- /dev/null +++ b/spec/datadog/core/ddsketch_spec.rb @@ -0,0 +1,100 @@ +require 'datadog/core/ddsketch' +require 'datadog/core/ddsketch_pprof/ddsketch_pb' + +RSpec.describe Datadog::Core::DDSketch do + context 'when DDSketch is not supported' do + before do + stub_const('Datadog::Core::LIBDATADOG_API_FAILURE', 'Example error loading libdatadog_api') + end + + it 'raises an error' do + expect { described_class.new }.to raise_error(ArgumentError, 'DDSketch is not supported: Example error loading libdatadog_api') + end + end + + context 'when DDSketch is supported' do + subject(:sketch) { described_class.new } + + describe '#add' do + it 'adds a point to the sketch' do + expect { sketch.add(123.456) }.to change { sketch.count }.from(0.0).to(1.0) + end + + it 'returns the sketch' do + expect(sketch.add(123.456)).to be sketch + end + + context 'when the point is a negative number' do + it 'raises an error' do + expect { sketch.add(-1.0) }.to raise_error(RuntimeError, 'DDSketch add failed: point is invalid') + end + end + end + + describe '#add_with_count' do + it 'adds a point with count to the sketch' do + expect { sketch.add_with_count(10.0, 5.0) }.to change { sketch.count }.from(0.0).to(5.0) + end + + it 'returns the sketch' do + expect(sketch.add_with_count(10.0, 5.0)).to be sketch + end + + context 'when the point is a negative number' do + it 'raises an error' do + expect { sketch.add_with_count(-1.0, 1.0) }.to raise_error(RuntimeError, 'DDSketch add_with_count failed: point is invalid') + end + end + end + + describe '#count' do + subject(:count) { sketch.count } + + context 'when sketch is empty' do + it 'returns zero' do + expect(count).to be 0.0 + end + end + + context 'when sketch has points' do + before do + sketch.add(1.0) + sketch.add(2.0) + sketch.add(3.0) + end + + it 'returns the total count' do + expect(count).to be 3.0 + end + end + end + + describe '#encode' do + subject(:encode) { sketch.encode } + + before do + sketch.add(1.0) + sketch.add(2.0) + sketch.add(3.0) + end + + it 'returns a binary string' do + result = encode + expect(result).to be_a(String) + expect(result.encoding).to eq(Encoding::BINARY) + end + + it 'resets the sketch for reuse' do + expect { sketch.encode }.to change { sketch.count }.from(3.0).to(0.0) + end + + it 'can be decoded' do + 42.times { sketch.add(0) } + decoded = Test::DDSketch.decode(encode) + + # @ivoanjo: Not amazingly interesting, but just a simple sanity check that the round trip works + expect(decoded.zeroCount).to be(42.0) + end + end + end +end