Skip to content

Performance improvements to TagSerializer#155

Merged
remeh merged 2 commits intoDataDog:masterfrom
marcotc:perf/tags-serializer
Nov 16, 2020
Merged

Performance improvements to TagSerializer#155
remeh merged 2 commits intoDataDog:masterfrom
marcotc:perf/tags-serializer

Conversation

@marcotc
Copy link
Copy Markdown
Member

@marcotc marcotc commented Oct 26, 2020

While profiling the ddtrace gem, we noticed that enabling runtime metrics, which uses dogstatsd-ruby, caused some performance degradation.

Running a small Rails application under ruby-prof showed us that the TagSerializer and StatSerializer were responsible for most of the time spend on dogstatsd-ruby calls.

This PR improves the wall time and memory usage of the TagSerializer.

The largest gains came from processing the provided message_tags in a single pass and by caching a serialized version of the global tags.

I've added a benchmark test to tag_serializer_spec.rb that allows one to replicate my results. This required the addition of benchmark-related gems to the :development group.

Benchmark results

A detailed explanation of all benchmarked categories:

  1. no tags: no global tags nor argument tags
  2. global tags: only global tags, no argument tags
  3. tags Array: only argument tags, as an array object; no global tags
  4. tags Array + global tags: argument tags, as an array object, with global tags
  5. tags Hash: only argument tags, as a hash object; no global tags
  6. tags Hash + global tags: argument tags, as a hash object, with global tags

Before this change

Calculating -------------------------------------
             no tags     12.608M (± 1.2%) i/s -     63.543M in   5.040483s
         global tags      2.146M (± 1.7%) i/s -     10.825M in   5.044554s
          tags Array    578.551k (± 1.4%) i/s -      2.912M in   5.033544s
           tags Hash    357.388k (± 1.7%) i/s -      1.799M in   5.035976s
tags Array + global tags
                        516.454k (± 2.0%) i/s -      2.628M in   5.091344s
tags Hash + global tags
                        334.824k (± 1.8%) i/s -      1.686M in   5.037281s

Comparison:
             no tags: 12608244.2 i/s
         global tags:  2146444.1 i/s - 5.87x  (± 0.00) slower
          tags Array:   578551.3 i/s - 21.79x  (± 0.00) slower
tags Array + global tags:   516453.9 i/s - 24.41x  (± 0.00) slower
           tags Hash:   357387.6 i/s - 35.28x  (± 0.00) slower
tags Hash + global tags:   334824.1 i/s - 37.66x  (± 0.00) slower

.Calculating -------------------------------------
             no tags     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
         global tags   168.000  memsize (     0.000  retained)
                         1.000  objects (     0.000  retained)
                         1.000  strings (     0.000  retained)
          tags Array   368.000  memsize (     0.000  retained)
                         6.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)
           tags Hash   728.000  memsize (     0.000  retained)
                        15.000  objects (     0.000  retained)
                         7.000  strings (     0.000  retained)
tags Array + global tags
                       416.000  memsize (     0.000  retained)
                         6.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)
tags Hash + global tags
                       776.000  memsize (     0.000  retained)
                        15.000  objects (     0.000  retained)
                         7.000  strings (     0.000  retained)

Comparison:
             no tags:          0 allocated
         global tags:        168 allocated - Infx more
          tags Array:        368 allocated - Infx more
tags Array + global tags:        416 allocated - Infx more
           tags Hash:        728 allocated - Infx more
tags Hash + global tags:        776 allocated - Infx more

After this change

Calculating -------------------------------------
             no tags     19.240M (± 1.3%) i/s -     97.968M in   5.092676s
         global tags     18.060M (± 1.3%) i/s -     91.859M in   5.087262s
          tags Array    636.860k (± 1.4%) i/s -      3.209M in   5.039865s
           tags Hash    429.507k (± 1.8%) i/s -      2.188M in   5.097028s
tags Array + global tags
                        534.067k (± 1.6%) i/s -      2.683M in   5.024473s
tags Hash + global tags
                        383.639k (± 1.3%) i/s -      1.942M in   5.063872s

Comparison:
             no tags: 19240289.1 i/s
         global tags: 18059975.6 i/s - 1.07x  (± 0.00) slower
          tags Array:   636860.1 i/s - 30.21x  (± 0.00) slower
tags Array + global tags:   534066.8 i/s - 36.03x  (± 0.00) slower
           tags Hash:   429507.0 i/s - 44.80x  (± 0.00) slower
tags Hash + global tags:   383639.4 i/s - 50.15x  (± 0.00) slower

.Calculating -------------------------------------
             no tags     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
         global tags     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
          tags Array   328.000  memsize (     0.000  retained)
                         5.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)
           tags Hash   568.000  memsize (     0.000  retained)
                        11.000  objects (     0.000  retained)
                         7.000  strings (     0.000  retained)
tags Array + global tags
                       368.000  memsize (     0.000  retained)
                         6.000  objects (     0.000  retained)
                         4.000  strings (     0.000  retained)
tags Hash + global tags
                       608.000  memsize (     0.000  retained)
                        12.000  objects (     0.000  retained)
                         7.000  strings (     0.000  retained)

Comparison:
             no tags:          0 allocated
         global tags:          0 allocated - same
          tags Array:        328 allocated - Infx more
tags Array + global tags:        368 allocated - Infx more
           tags Hash:        568 allocated - Infx more
tags Hash + global tags:        608 allocated - Infx more

Results

Wall time % improvement
no tags 34.4%
global tags 88.1%
tags Array 9.1%
tags Hash 16.7%
tags Array + global tags 3.2%
tags Hash + global tags 12.7%
Memory (bytes allocated) % improvement
no tags 0.0%
global tags 100.0%
tags Array 10.8%
tags Array + global tags 11.5%
tags Hash 21.9%
tags Hash + global tags 21.6%

end

tags = if @global_tags_formatted
[@global_tags_formatted, to_tags_list(message_tags)]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was surprised to find out that Array#join flattens the provided array:

[1,[2,3]].join
# => "123"

This actually came in handy, as it has proven faster than adding @global_tags_formatted to the array returned by to_tags_list(message_tags) or concatenating the result of to_tags_list(message_tags) to an existing array.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is interesting; it's like a flatten then regular join operation.

@@ -44,11 +44,11 @@

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git diff doesn't interpret this diff very well: I've just reduced all values by one.

@marcotc
Copy link
Copy Markdown
Member Author

marcotc commented Oct 26, 2020

👋 @remeh @kbogtob, from across the virtual room! I couldn't find who are the canonical maintainers of this repo, so I guessed you two based on recent history :)
I have at least one other PR to contribute, so let me know if there's anything I can do to make the process smoother.

@global_tags_formatted = @global_tags.join(',') if @global_tags.any?
end

def format(message_tags)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember doing something in this file ages ago, I think because Statsd was returning some format that we couldn't use downstream in the tracing library. I think we wanted to merge tags at the tracer library level into the global stats tags?

I would double check the history on this, and make sure it doesn't break tags downstream.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I searched ddtraces history, and we don't access these global tags.

Regarding this in this repo, there are no modification attempts to global tags.

One thing that is technically possible today is to get a reference to it and mutate it:

stats = Datadog::Statsd.new
stats.tags << 'new:tag' # stats.tags is effectively +TagSerializer#global_tags+.

Instead of using the public interface:

stats = Datadog::Statsd.new(tags: ['new:tag'])

I'm not too convinced this should be supported behaviour by this library.

My suggestion is to freeze the internally stored TagSerializer#global_tags, thus having TagSerializer#global_tags guaranteed to return a read-only object.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with the sentiment of making global_tags immutable, from an interface perspective it feels intuitive to me.

Copy link
Copy Markdown
Member

@truthbk truthbk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me!

@global_tags_formatted = @global_tags.join(',') if @global_tags.any?
end

def format(message_tags)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with the sentiment of making global_tags immutable, from an interface perspective it feels intuitive to me.

@remeh remeh merged commit e356b9f into DataDog:master Nov 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants