Skip to content
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

Instrumentation - Skylight Integration #88

Merged
merged 7 commits into from
Mar 13, 2018
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ rvm:
- 2.5.0
script:
- bundle exec rspec
- bundle exec rspec spec/lib/instrumentation/as_notifications.rb

Choose a reason for hiding this comment

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

If breaking out the spec is the only way to do it, chaining them with && might be preferable (https://docs.travis-ci.com/user/customizing-the-build/#Customizing-the-Build-Step) as it would always generate testing output from both sets but still fail properly.

That said, I think your test tear-down could alias the method back to the original and remove the newly added method easily enough and eliminate the need for the second running of rspec.

1 change: 1 addition & 0 deletions OSSMETADATA
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
osslifecycle=active
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects.

# Performance Comparison

We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least `25 times` faster than Active Model Serializers on up to current benchmark of 1000 records.
We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least `25 times` faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the [performance document](https://github.com/Netflix/fast_jsonapi/blob/master/performance_methodology.md) for any questions related to methodology.

## Benchmark times for 250 records

Expand Down Expand Up @@ -244,6 +244,36 @@ object_method_name | Set custom method name to get related objects | ```has_many
record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user```
serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor```

### Instrumentation

`fast_jsonapi` also has builtin [Skylight](https://www.skylight.io/) integration. To enable, add the following to an initializer:

```ruby
require 'fast_jsonapi/instrumentation/skylight'
```

Skylight relies on `ActiveSupport::Notifications` to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration:

```ruby
require 'fast_jsonapi/instrumentation'
```

The two instrumented notifcations are supplied by these two constants:
* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION`
* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION`

It is also possible to instrument one method without the other by using one of the following require statements:

```ruby
require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'
```

Same goes for the Skylight integration:
```ruby
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
```

## Contributing
Please see [contribution check](https://github.com/Netflix/fast_jsonapi/blob/master/CONTRIBUTING.md) for more details on contributing
Expand Down
2 changes: 2 additions & 0 deletions lib/fast_jsonapi/instrumentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'
15 changes: 15 additions & 0 deletions lib/fast_jsonapi/instrumentation/serializable_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'active_support/notifications'

module FastJsonapi
module ObjectSerializer

alias_method :serializable_hash_without_instrumentation, :serializable_hash

def serializable_hash
ActiveSupport::Notifications.instrument(SERIALIZABLE_HASH_NOTIFICATION, { name: self.class.name }) do
serializable_hash_without_instrumentation
end
end

end
end
15 changes: 15 additions & 0 deletions lib/fast_jsonapi/instrumentation/serialized_json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'active_support/notifications'

module FastJsonapi
module ObjectSerializer

alias_method :serialized_json_without_instrumentation, :serialized_json

def serialized_json
ActiveSupport::Notifications.instrument(SERIALIZED_JSON_NOTIFICATION, { name: self.class.name }) do
serialized_json_without_instrumentation
end
end

end
end
2 changes: 2 additions & 0 deletions lib/fast_jsonapi/instrumentation/skylight.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'skylight'
require 'fast_jsonapi/instrumentation/serializable_hash'

module FastJsonapi
module Instrumentation
module Skylight
module Normalizers
class SerializableHash < Skylight::Normalizers::Normalizer

register FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION

CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION}".freeze

def normalize(trace, name, payload)
[ CAT, payload[:name], nil ]
end

end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'skylight'
require 'fast_jsonapi/instrumentation/serializable_hash'

module FastJsonapi
module Instrumentation
module Skylight
module Normalizers
class SerializedJson < Skylight::Normalizers::Normalizer

register FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION

CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION}".freeze

def normalize(trace, name, payload)
[ CAT, payload[:name], nil ]
end

end
end
end
end
end
21 changes: 3 additions & 18 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,15 @@
require 'active_support/inflector'
require 'fast_jsonapi/serialization_core'

begin
require 'skylight'
SKYLIGHT_ENABLED = true
rescue LoadError
SKYLIGHT_ENABLED = false
end

module FastJsonapi
module ObjectSerializer
extend ActiveSupport::Concern
include SerializationCore

included do
# Skylight integration
# To remove Skylight
# Remove the included do block
# Remove the Gemfile entry
if SKYLIGHT_ENABLED
include Skylight::Helpers

instrument_method :serializable_hash
instrument_method :to_json
end
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'.freeze
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'.freeze

included do
# Set record_type based on the name of the serializer class
set_type(reflected_record_type) if reflected_record_type
end
Expand Down
44 changes: 44 additions & 0 deletions performance_methodology.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Performance using Fast JSON API

We have been getting a few questions on Github about [Fast JSON API’s](https://github.com/Netflix/fast_jsonapi) performance statistics and the methodology used to measure the performance. This article is an attempt at addressing this aspect of the gem.

## Prologue

With use cases like infinite scroll on complex models and bulk update on index pages, we started observing performance degradation on our Rails APIs. Our first step was to enable instrumentation and then tune for performance. We realized that, on average, more than 50% of the time was being spent on AMS serialization. At the same time, we had a couple of APIs that were simply proxying requests on top of a non-Rails, non-JSON API endpoint. Guess what? The non-Rails endpoints were giving us serialized JSON back in a fraction of the time spent by AMS.

This led us to explore AMS documentation in depth in an effort to try a variety of techniques such as caching, using OJ for JSON string generation etc. It didn’t yield the consistent results we were hoping to get. We loved the developer experience of using AMS, but wanted better performance for our use cases.

We came up with patterns that we can rely upon such as:

* We always use [JSON:API](http://jsonapi.org/) for our APIs
* We almost always serialize a homogenous list of objects (Example: An array of movies)

On the other hand:

* AMS is designed to serialize JSON in several different formats, not just JSON:API
* AMS can also handle lists that are not homogenous

This led us to build our own object serialization library that would be faster because it would be tailored to our requirements. The usage of fast_jsonapi internally on production environments resulted in significant performance gains.

## Benchmark Setup

The benchmark setup is simple with classes for ``` Movie, Actor, MovieType, User ``` on ```movie_context.rb``` for fast_jsonapi serializers and on ```ams_context.rb``` for AMS serializers. We benchmark the serializers with ```1, 25, 250, 1000``` movies, then we output the result. We also ensure that JSON string output is equivalent to ensure neither library is doing excess work compared to the other. Please checkout [object_serializer_performance_spec](https://github.com/Netflix/fast_jsonapi/blob/master/spec/lib/object_serializer_performance_spec.rb).

## Benchmark Results

We benchmarked results for creating a Ruby Hash. This approach removes the effect of chosen JSON string generation engines like OJ, Yajl etc. Benchmarks indicate that fast_jsonapi consistently performs around ```25 times``` faster than AMS in generating a ruby hash.

We applied a similar benchmark on the operation to serialize the objects to a JSON string. This approach helps with ensuring some important criterias, such as:

* OJ is used as the JSON engine for benchmarking both AMS and fast_jsonapi
* The benchmark is easy to understand
* The benchmark helps to improve performance
* The benchmark influences design decisions for the gem

This gem is currently used in several APIs at Netflix and has reduced the response times by more than half on many of these APIs. We truly appreciate the Ruby and Rails communities and wanted to contribute in an effort to help improve the performance of your APIs too.

## Epilogue

[Fast JSON API](https://github.com/Netflix/fast_jsonapi) is not a replacement for AMS. AMS is a great gem, and it does many things and is very flexible. We still use it for non JSON:API serialization and deserialization. What started off as an internal performance exercise evolved into fast_jsonapi and created an opportunity to give something back to the awesome **Ruby and Rails communities**.

We are excited to share it with all of you since we believe that there will be **no** end to this need for speed on APIs. :)
69 changes: 69 additions & 0 deletions spec/lib/instrumentation/as_notifications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require 'spec_helper'
require 'fast_jsonapi/instrumentation'

describe FastJsonapi::ObjectSerializer do
include_context 'movie class'

context 'instrument' do

before(:each) do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]

@serializer = MovieSerializer.new([movie, movie], options)
end

context 'serializable_hash' do

it 'should send notifications' do
events = []

ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end

serialized_hash = @serializer.serializable_hash

expect(events.length).to eq(1)

event = events.first

expect(event.duration).to be > 0
expect(event.payload).to eq({ name: 'MovieSerializer' })
expect(event.name).to eq(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION)

expect(serialized_hash.key?(:data)).to eq(true)
expect(serialized_hash.key?(:meta)).to eq(true)
expect(serialized_hash.key?(:included)).to eq(true)
end

end

context 'serialized_json' do

it 'should send notifications' do
events = []

ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end

json = @serializer.serialized_json

expect(events.length).to eq(1)

event = events.first

expect(event.duration).to be > 0
expect(event.payload).to eq({ name: 'MovieSerializer' })
expect(event.name).to eq(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION)

expect(json.length).to be > 50
end

end

end

end
56 changes: 56 additions & 0 deletions spec/lib/instrumentation/as_notifications_negative_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require 'spec_helper'

describe FastJsonapi::ObjectSerializer do
include_context 'movie class'

context 'instrument' do

before(:each) do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]

@serializer = MovieSerializer.new([movie, movie], options)
end

context 'serializable_hash' do

it 'should send not notifications' do
events = []

ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end

serialized_hash = @serializer.serializable_hash

expect(events.length).to eq(0)

expect(serialized_hash.key?(:data)).to eq(true)
expect(serialized_hash.key?(:meta)).to eq(true)
expect(serialized_hash.key?(:included)).to eq(true)
end

end

context 'serialized_json' do

it 'should send not notifications' do
events = []

ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end

json = @serializer.serialized_json

expect(events.length).to eq(0)

expect(json.length).to be > 50
end

end

end

end