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

Utilize JSONAPI family of gems for deserialization. #1928

Conversation

NullVoxPopuli
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli commented Sep 15, 2016

Purpose

To further break apart AMS into functional sections that can be easily swapped with native extension gems to allow for much faster serialization/deserialization.

This PR cleans up a lot of Deserialization functionality, and offloads it to jsonapi-deserialization.

Changes

  • Remove existing deserialization / replace with jsonapi-deserialization

Caveats

  • only / except have been removed in favor of either creating a whitelisting custom deserializer, or using strong parameters.

Related GitHub issues

#1927 - use JSON API gem for deserialization
#1925 - data can be an array

Additional helpful information

/cc @remear @richmolj @beauby

@mention-bot
Copy link

@NullVoxPopuli, thanks for your PR! By analyzing the annotation information on this pull request, we identified @domitian, @beauby and @lawitschka to be potential reviewers

@NullVoxPopuli
Copy link
Contributor Author

thanks, @mention-bot ...

@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
# 'thread_safe'

spec.add_runtime_dependency 'jsonapi', '~> 0.1.1.beta2'
spec.add_runtime_dependency 'json_key_transform', '>= 0.1'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure on the name of this?

Copy link
Member

Choose a reason for hiding this comment

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

case_transformer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's since been renamed to case_transform :-)

@@ -1,12 +1,12 @@
require 'jsonapi'

module ActiveModelSerializers
module Adapter
class JsonApi
# NOTE(Experimental):
# This is an experimental feature. Both the interface and internals could be subject
# to changes.
module Deserialization
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the end of this PR, this should be removed / deprecated

/cc @richmolj - rumor has it you want to tackle moving deserialization out?

@NullVoxPopuli
Copy link
Contributor Author

nil check PR pending beauby/jsonapi#37

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Sep 20, 2016

Some early benchmark comparisons:

With rustified case_transform gem (so for just the string inflectors have been rustified. The whole case_transform module should probably be written in rust)

camel 0.04078577016289254/ips; 624 objects
camel_lower 2105.112726671057/ips; 624 objects
dash 2507.3989844391144/ips; 624 objects
unaltered 7591550.592845934/ips; 1 objects
underscore 3448.4536144949116/ips; 498 objects
Benchmark results:
{
  "commit_hash": "408c5c8",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "benchmark_run[environment]": "2.3.0p0",
  "runs": [
    {
      "benchmark_type[category]": "camel",
      "benchmark_run[result][iterations_per_second]": 0.041,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 624
    },
    {
      "benchmark_type[category]": "camel_lower",
      "benchmark_run[result][iterations_per_second]": 2105.113,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 624
    },
    {
      "benchmark_type[category]": "dash",
      "benchmark_run[result][iterations_per_second]": 2507.399,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 624
    },
    {
      "benchmark_type[category]": "unaltered",
      "benchmark_run[result][iterations_per_second]": 7591550.593,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 1
    },
    {
      "benchmark_type[category]": "underscore",
      "benchmark_run[result][iterations_per_second]": 3448.454,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 498
    }
  ]
}

I think this is proof enough that all of case_transform needs to be rustified, because there is probably a lot of overhead going back and forth between rust and ruby objects every iteration. Overall, it's a performance loss. :-(

Today (pure ruby)

camel 495.464618705588/ips; 3653 objects
camel_lower 599.8635486518252/ips; 2622 objects
dash 3767.134560447186/ips; 770 objects
unaltered 7499960.517196772/ips; 1 objects
underscore 7048.791853154917/ips; 313 objects
Benchmark results:
{
  "commit_hash": "91b37ce",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "benchmark_run[environment]": "2.3.0p0",
  "runs": [
    {
      "benchmark_type[category]": "camel",
      "benchmark_run[result][iterations_per_second]": 495.465,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 3653
    },
    {
      "benchmark_type[category]": "camel_lower",
      "benchmark_run[result][iterations_per_second]": 599.864,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 2622
    },
    {
      "benchmark_type[category]": "dash",
      "benchmark_run[result][iterations_per_second]": 3767.135,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 770
    },
    {
      "benchmark_type[category]": "unaltered",
      "benchmark_run[result][iterations_per_second]": 7499960.517,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 1
    },
    {
      "benchmark_type[category]": "underscore",
      "benchmark_run[result][iterations_per_second]": 7048.792,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 313
    }
  ]
}

For reference for myself, both of these bin/bench runs were on my ASUS laptop.

@richmolj
Copy link
Contributor

@NullVoxPopuli this one seems a little off to me:

"benchmark_type[category]": "camel",
      "benchmark_run[result][iterations_per_second]": 0.041

That's quite the decrease from 495 :) And not in-line with the rest of the benches. Maybe something is off with the script?

@NullVoxPopuli
Copy link
Contributor Author

yeah, it's super disappointing. There are two things for me to look at:

  • Re-Write all of case_transform in rust.
  • see if the way the rust lib is being loaded is actually the problem. It's using fiddler, and this doesn't seem right, thought it's the way ruru's example repo loads the lib... so.. I'll need to just investigate.

@NullVoxPopuli
Copy link
Contributor Author

@richmolj for what it's worth, the current implementation https://github.com/NullVoxPopuli/case_transform/blob/ruru-rust-speed-boost/lib/case_transform.rb#L45

It goes from Ruby -> Rust -> Ruby -> Rust -> Ruby. Just to do .to_snake_case.to_class_case.

And this is the weird loading thing I was talking about: https://github.com/NullVoxPopuli/case_transform/blob/ruru-rust-speed-boost/lib/case_transform.rb#L12

@NullVoxPopuli
Copy link
Contributor Author

Latest bin/bench re: key transform:

camel 0.30715830565312585/ips; 12 objects
camel_lower 11241.452342317594/ips; 12 objects
dash 20589.596212046636/ips; 12 objects
unaltered 2317189.7469825707/ips; 1 objects
underscore 33231.58121372963/ips; 12 objects
Benchmark results:
{
  "commit_hash": "d9f17c3",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "benchmark_run[environment]": "2.3.0p0",
  "runs": [
    {
      "benchmark_type[category]": "camel",
      "benchmark_run[result][iterations_per_second]": 0.307,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 12
    },
    {
      "benchmark_type[category]": "camel_lower",
      "benchmark_run[result][iterations_per_second]": 11241.452,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 12
    },
    {
      "benchmark_type[category]": "dash",
      "benchmark_run[result][iterations_per_second]": 20589.596,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 12
    },
    {
      "benchmark_type[category]": "unaltered",
      "benchmark_run[result][iterations_per_second]": 2317189.747,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 1
    },
    {
      "benchmark_type[category]": "underscore",
      "benchmark_run[result][iterations_per_second]": 33231.581,
      "benchmark_run[result][total_allocated_objects_per_iteration]": 12
    }
  ]
}

@NullVoxPopuli
Copy link
Contributor Author

This is a little easier to digest

$ ruby benchmark.rb 

Comparison:
         Ruby: camel:    13179.9 i/s
         Rust: camel:        1.9 i/s - 6978.60x  slower


Comparison:
   Rust: camel_lower:   101904.9 i/s
   Ruby: camel_lower:    10441.9 i/s - 9.76x  slower


Comparison:
          Rust: dash:   230069.3 i/s
          Ruby: dash:    24660.9 i/s - 9.33x  slower


Comparison:
     Ruby: unaltered:  6053346.9 i/s
     Rust: unaltered:  2143459.5 i/s - 2.82x  slower


Comparison:
    Rust: underscore:   271433.8 i/s
    Ruby: underscore:   166316.0 i/s - 1.63x  slower

@NullVoxPopuli
Copy link
Contributor Author

dashed is at least much faster now

$ ruby benchmark.rb 

Comparison:
         Rust: camel:   238774.9 i/s
         Ruby: camel:    13107.3 i/s - 18.22x  slower


Comparison:
   Rust: camel_lower:   230641.5 i/s
   Ruby: camel_lower:     8244.1 i/s - 27.98x  slower


Comparison:
          Rust: dash:   243197.8 i/s
          Ruby: dash:    20350.9 i/s - 11.95x  slower


Comparison:
     Ruby: unaltered:  6030498.8 i/s
     Rust: unaltered:  1960989.6 i/s - 3.08x  slower


Comparison:
    Rust: underscore:   291052.3 i/s
    Ruby: underscore:   169458.2 i/s - 1.72x  slower

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Sep 22, 2016

and again with AMS:

# with the rust extension
camel 33733.21269803018/ips; 12 objects
camel_lower 34301.23301110913/ips; 12 objects
dash 40138.212617021505/ips; 12 objects
unaltered 2370122.8825021028/ips; 1 objects
underscore 41124.286490095896/ips; 12 objects

# on master
camel 512.0742922881544/ips; 3653 objects
camel_lower 640.6939252768957/ips; 2622 objects
dash 3897.919771677366/ips; 770 objects
unaltered 7775212.778836493/ips; 1 objects
underscore 7040.229576764806/ips; 313 objects

@NullVoxPopuli
Copy link
Contributor Author

Pure Ruby case_transform:

camel 6043.920511505979/ips; 189 objects
camel_lower 6956.799794356406/ips; 189 objects
dash 7503.778823967263/ips; 189 objects
unaltered 7347707.175991204/ips; 1 objects
underscore 7668.166854859702/ips; 189 objects

# on master
camel 556.2479170910697/ips; 3653 objects
camel_lower 667.1673377402234/ips; 2622 objects
dash 4186.290920899872/ips; 770 objects
unaltered 7218581.424425041/ips; 1 objects
underscore 7379.84926592023/ips; 313 objects

@beauby
Copy link
Contributor

beauby commented Sep 22, 2016

Btw, the code for deserialization in the jsonapi gem already exists (not sure where anymore, I think it's a branch or a pending PR) - will post more info tonight or tomorrow.

@NullVoxPopuli
Copy link
Contributor Author

yup, I discovered that while investigating, and have been changing the tests to expect the exception thrown from your gem

@NullVoxPopuli
Copy link
Contributor Author

So, all that's remaining here is to extract the jsonapi -> rails json format stuff

this could be merged as is, and just represent the extraction of a couple other things.

@@ -1,12 +1,12 @@
require 'jsonapi'
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this gem already required elsewhere? I know we use it for the include directives...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i'll take a look

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is the first place it's used.

{ 'data' => { 'attributes' => [] } },
{ 'data' => { 'relationships' => [] } },
{ 'data' => { 'relationships' => { 'rel' => nil } } },
{ 'data' => { 'relationships' => { 'rel' => {} } } }]
Copy link
Contributor

Choose a reason for hiding this comment

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

💯

@@ -76,9 +65,14 @@ def test_illformed_payloads_safe
end
end

test 'null data is allow per the JSON API Schema' do
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, should we also be adding a test for []?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
# 'thread_safe'

spec.add_runtime_dependency 'jsonapi', '~> 0.1.1.beta2'
spec.add_runtime_dependency 'case_transform', '>= 0.2'
Copy link
Contributor

Choose a reason for hiding this comment

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

I always prefer ~> here or at least < 1. Who knows if you'll give up on AMS and break something with a 1.0 release some day :)

@@ -7,6 +7,9 @@ eval_gemfile local_gemfile if File.readable?(local_gemfile)
# Specify your gem's dependencies in active_model_serializers.gemspec
gemspec

# TODO: remove when @beauby re-publishes this gem
gem 'jsonapi', github: 'beauby/jsonapi'
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this would prevent a merge, right? Or at the very least we'd have to tell anyone running off of master (like me) to add a similar entry to their app's Gemfile.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup. gotta see if beauby cut a new gem with his recent merges

@richmolj
Copy link
Contributor

richmolj commented Oct 1, 2016

@NullVoxPopuli I know this came up in slack at some point - does this bring in additional dependencies due to rust, or does installation stay the same?

@richmolj
Copy link
Contributor

richmolj commented Oct 1, 2016

Also heads up it looks like the parsing logic may be changing beauby/jsonapi#43

@NullVoxPopuli
Copy link
Contributor Author

the case transform gem is pure ruby. i'll probably make a doc later with performance improvement tips, and mention the rust-extensions version. i've gotten the rust build to be effortless. (just gem install), but it's still opt-in.

return {}
end
document = JSONAPI.parse(document, options)
document = document.to_hash
Copy link
Contributor

Choose a reason for hiding this comment

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

The API of jsonapi-parser is about to change. You probably want to do JSONAPI.parse_resource!(document) now (which does only the validation).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what specifically is changing / could AMS utilize any of those changes for the better?

Copy link
Contributor

Choose a reason for hiding this comment

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

The parser now only validates a document, it does not build an object-like structure for traversing it anymore (which was a perf issue). AMS could possibly make use of jsonapi-validator which does domain-specific validations of payloads, or jsonapi-deserializable which does general-purpose deserialization of JSONAPI resource/relationship payloads.

Copy link
Contributor Author

@NullVoxPopuli NullVoxPopuli Oct 2, 2016

Choose a reason for hiding this comment

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

I think the validator could be interesting as long as it was quick. In my project, I had to disable validation, cause I am doing embedded records in relationships -- so.. we'll want to document how to override this for some people.

I was looking at jsonapi-deserializable the other day. It errored, so I didn't add it to this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

The validator is as quick as it could possibly be (given it is written in ruby, but then again so is rails so I doubt it is a bottleneck).
Would you mind opening an issue on jsonapi-deserializable?

Copy link
Contributor

@beauby beauby Oct 2, 2016

Choose a reason for hiding this comment

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

Note that jsonapi-deserializable is not expected to work right now, as it relies on an unpublished gem (jsonapi-validator), which in turn relies on an unpublished version of jsonapi-parser. All this should be fixed tonight though.

@NullVoxPopuli
Copy link
Contributor Author

So, I'll just wait till it's all published before pokin' around :-)

On Sun, Oct 2, 2016 at 12:57 PM Lucas Hosseini [email protected]
wrote:

@beauby commented on this pull request.

In lib/active_model_serializers/adapter/json_api/deserialization.rb
#1928:

       end
     end
     # Same as parse!, but returns an empty hash instead of raising InvalidDocument
     # on invalid payloads.
     def parse(document, options = {})

- document = document.dup.permit!.to_h if document.is_a?(ActionController::Parameters)

  •      validate_payload(document) do |invalid_document, reason|
    
  •        yield invalid_document, reason if block_given?
    
  •        return {}
    
  •      end
    
  •      document = JSONAPI.parse(document, options)
    
  •      document = document.to_hash
    

Note that it jsonapi-deserializable is not expected to work right now, as
it relies on an unpublished gem (jsonapi-validator), which in turn relies
on an unpublished version of jsonapi-parser. All this should be fixed
tonight though.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#1928, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAMJahXCqDjs-2aiMa0Zo-dQ7OfzeMWyks5qv-JngaJpZM4J-KPp
.

@beauby
Copy link
Contributor

beauby commented Oct 2, 2016

Just released jsonapi v0.1.1.beta3 (containing jsonapi-parser v0.1.1.beta1).

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Oct 18, 2016

@beauby this is what I was talking about earlier:

# Running:

{"data"=>nil}
E

Finished in 0.001320s, 757.5792 runs/s, 0.0000 assertions/s.


  1) Error:
ActiveModelSerializers::Adapter::JsonApi::Deserialization::ParseTest#test_null_data_is_allow_per_the_JSON_API_Schema:
JSONAPI::Parser::InvalidDocument: Invalid payload (): 
    /home/lprestonsegoiii/Development/NullVoxPopuli/active_model_serializers/lib/active_model_serializers/adapter/json_api/deserialization.rb:78:in `block in parse!'
    /home/lprestonsegoiii/Development/NullVoxPopuli/active_model_serializers/lib/active_model_serializers/adapter/json_api/deserialization.rb:110:in `rescue in parse'
    /home/lprestonsegoiii/Development/NullVoxPopuli/active_model_serializers/lib/active_model_serializers/adapter/json_api/deserialization.rb:85:in `parse'
    /home/lprestonsegoiii/Development/NullVoxPopuli/active_model_serializers/lib/active_model_serializers/adapter/json_api/deserialization.rb:77:in `parse!'
    /home/lprestonsegoiii/Development/NullVoxPopuli/active_model_serializers/test/adapter/json_api/parse_test.rb:70:in `block in <class:ParseTest>'


1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

where parse is

        def parse!(document, options = {})
          parse(document, options) do |invalid_payload, reason|
            fail JSONAPI::Parser::InvalidDocument, "Invalid payload (#{reason}): #{invalid_payload}"
          end
        end

        def parse(document, options = {})
          JSONAPI.parse_response!(document)
          document = document.to_h
          puts document
          return JSONAPI::Deserializable::Resource.new(document)
        rescue JSONAPI::Parser::InvalidDocument => e
          puts e.message
          return {} unless block_given?
          yield
        end

@NullVoxPopuli
Copy link
Contributor Author

ok, that's fair. So, for the deserialization implementation in AMS, should a parameter be passed for differentiate between create / update / etc? since different validations exist for the different scenarios?

@beauby
Copy link
Contributor

beauby commented Oct 18, 2016

The only difference between create and update resources is whether the id is required or not. But this can be easily set using jsonapi-validations or jsonapi-deserializable.

@NullVoxPopuli
Copy link
Contributor Author

ok, cool. and does deserializable have a way to split apart polymorphic relationships, like how AMS currently does?

@beauby
Copy link
Contributor

beauby commented Oct 18, 2016

Sure, basically how it works is:

  1. You optionally define the required/optional attributes/relationships, possibly with their arity and the accepted types
  2. You define the result hash in terms of the input payload (see examples and tests in jsonapi-deserializable).

@NullVoxPopuli
Copy link
Contributor Author

I'm making some good progress on this beauby/jsonapi-rails#1

some things I wanted to ask clarification on:

the ActiveModelSerializers::Adapter::JsonApi::Deserialization.parse! options,

  • only
  • except
  • keys

They have the ability to infer a relationship (author means both author_id and author_type).
is this something we want to move forward with?

I remember there being talk of using only fields for whitelisting, rather than only/except

@NullVoxPopuli NullVoxPopuli force-pushed the 1927-change-deserialization-to-json-api-gem branch from db20605 to f3332f5 Compare November 13, 2016 04:02
@NullVoxPopuli
Copy link
Contributor Author

@beauby I think all that's left for this is to publish merge by jsonapi-rails branch and cut a release.

extract key_transfor

chaneg to case_transform gem

fix

tests pass

add empty array test

upgrade to jsonapi beta5, improve tests to be valid resource requests

use github links in gemfile for now, due to unreleased code

use github links in gemfile for now, due to unreleased code

some clenaup

deserialization would work now if it didn't depend on the existance of certain objects

progress on implementing new deserializer -- just have some behavior differences with relationships

tests pass

address rubocop issue -- update dependencies... wonder if we need to move @beauby's jsonapi gems to rails-api, in order to faster turnaround time

reduce to minimum dependencies. Now we need beauby to merge my jsonapi-rails branch :-)
@NullVoxPopuli NullVoxPopuli force-pushed the 1927-change-deserialization-to-json-api-gem branch from 871ad86 to 10b4850 Compare December 7, 2016 15:53
@NullVoxPopuli
Copy link
Contributor Author

This should also close this PR: #1986

@NullVoxPopuli NullVoxPopuli changed the title [WIP] begin playing with using JSON API for deserialization Utilize JSONAPI family of gems for deserialization. Dec 9, 2016
@NullVoxPopuli
Copy link
Contributor Author

Ready for re-review.
Tests appear to be failing on Rails 5+ :-(

@NullVoxPopuli
Copy link
Contributor Author

I don't know if you guys want the tests gone, since the functionality is handled by an external gem?

@bf4
Copy link
Member

bf4 commented May 1, 2017

@NullVoxPopuli can this still be done?

@NullVoxPopuli
Copy link
Contributor Author

I believe so :)

@bf4
Copy link
Member

bf4 commented May 1, 2017

@NullVoxPopuli probably best to just reset against current master then swap in jsonapi-deserializable and update code/docs/tests :)

@bf4
Copy link
Member

bf4 commented May 1, 2017

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented May 1, 2017 via email

@NullVoxPopuli
Copy link
Contributor Author

Cleaning up my PRs. If this work is still desired, feel free to cherry-pick

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants