santiago.pastorino

ActiveModel::Serializers Rewrite (upcoming 0.9.0.pre version)

Posted by santiago.pastorino
on October 15, 2013

First of all, I want to apologize to all for the long time it has taken me to push this humble new code.

I started to work on ActiveModel::Serializers because I'm interested in the Rails API project in general and ActiveModel::Serializers in particular. Given that ActiveModel::Serializers has few contributors, I thought it could be a good opportunity to understand the code and help the community around the project.

I began contributing to the project on a trip to San Francisco. There, I had the opportunity to work a bit with Yehuda Katz and Tom Dale at Tilde, but when I returned back to my job at WyeWorks, day-to-day responsibilities did not allow me to continue the project at the same velocity and manner I would have liked. This, in part, explains the above mentioned delay.

You can check out the code following this link

Improvements

The code structure after the rewrite is pretty similar but with a number of advantages I will try to share here with you:

The code is cleaner and clearer to understand and maintain.

The following is a list of previous solutions that were either fixed or changed:

  • Refine method used here, which implemented a solution that I found complicated. Now at load time, associations create objects that represent each and hold needed options, instead of generating an anonymous class per association.

  • Associations knew the source and target serializer. That is, if we had a post serializer that had many comments, an object was created to represent the association recording, not only the comment serializer, but also the post serializer. Such visibility was eliminated in the rewrite. In fact, the responsibility of the associations object was changed as well. Now the responsibility is only that of building a serializer using the options specified for that association.

  • The fast_attributes solution and its invalidation strategy were removed.

  • Mutability as seen here and here. The previous solution passed a global hash that was modified by the objects that intervened in the solution. This is now changed using a more functional approach, decoupling objects and making them more testable.

  • ActiveModel::Serializer talked to the controller. This is now the responsibility of ActionController::Serializable.

  • ActiveModel::Serializer responsibility is to decorate the object we want to serialize but that contract wasn't honored in the previous code. Now there's no direct access to the serialized object and everything is done inside the serializer.

  • The responsibilities of a few objects were not very clear. For example, Association. Those were used to register associations to be serialized together with their respective options. In addition, it recorded the serializer origin and destination of the association. It also built a serializer for the target relationship. Lastly, it was responsible for initiating the objects serialization process and serialization of ids. Association current responsibility is only to construct and configure a serializer for the association.

  • Given the structure of the code and the new responsibilities, it is now easier to implement adapters for different formats.

The tests now make it easier to understand how each part of the code works, are better organized and make sure that everything gets tested.

There are test files organized by functionality:

├── fixtures
│   ├── active_record.rb
│   └── poro.rb
├── integration
│   ├── action_controller
│   │   └── serialization_test.rb
│   └── active_record
│       └── active_record_test.rb
├── test_helper.rb
└── unit
    └── active_model
        ├── array_serializer
        │   ├── meta_test.rb
        │   ├── root_test.rb
        │   ├── scope_test.rb
        │   └── serialize_test.rb
        ├── default_serializer_test.rb
        └── serializer
            ├── attributes_test.rb
            ├── filter_test.rb
            ├── has_many_test.rb
            ├── has_one_test.rb
            ├── meta_test.rb
            ├── root_test.rb
            ├── scope_test.rb
            └── settings_test.rb

A lot less code for equivalent functionality.

LOC went down by ~50% from 682 to 340.

Performance is much better than before.

But please try this out in your apps. I've ran Sam Saffron's Discourse benchmarks using Rails 4 and current master of AMS and the rewrite branch. Here you have the results.

Current master branch:

Starting benchmark...
Running apache bench warmup
Benchmarking /
Running apache bench warmup
Benchmarking /t/oh-how-i-wish-i-could-shut-up-like-a-tunnel-for-so/69
Your Results: (note for timings- percentile is first, duration is second in millisecs)
---
home_page:
 50: 51
 75: 53
 90: 55
 99: 237
topic_page:
 50: 84
 75: 87
 90: 91
 99: 142
timings:
 load_rails: 4786
ruby-version: 2.0.0-p247
rails4?: true
architecture: x86_64
operatingsystem: Darwin
kernelversion: 12.5.0
memorysize: 8.00 GB
virtual: physical

Rewrite branch:

Starting benchmark...
Running apache bench warmup
Benchmarking /
Running apache bench warmup
Benchmarking /t/oh-how-i-wish-i-could-shut-up-like-a-tunnel-for-so/69
Your Results: (note for timings- percentile is first, duration is second in millisecs)
---
home_page:
 50: 40
 75: 41
 90: 44
 99: 236
topic_page:
 50: 76
 75: 78
 90: 81
 99: 130
timings:
 load_rails: 4331
ruby-version: 2.0.0-p247
rails4?: true
architecture: x86_64
operatingsystem: Darwin
kernelversion: 12.5.0
memorysize: 8.00 GB
virtual: physical

Why is it faster? Because AMS now does less work in order to produce the same results. Plus, much of the work that used to happen per each request are now carried out at load time.

We have a new project maintainer

For some time, AMS has been somewhat inactive as a project. Thankfully, Steve Klabnik has been recently working a great deal on issues and merging pull requests. From now on, I will also be actively contributing to the project. My intention is to resolve all pending issues and pull requests.

What's next?

It would be great if you could test your applications against this new branch and report any issues. I successfully ran the Discourse tests applying this patch. That patch gives an idea of some of the changes introduced but, depending on what you're using, problems can show up. Consider yourself warned. :)

After all that, here I have my to-do list. Several people have offered help via mail, Twitter, etc. Please feel free to grab tasks from here and make them yours it if you'd like. I'll continue to work on these issues as well.

To-do list

  • Complete the CHANGELOG of version 0.9.0 (part of this post can be reused)
  • Write the project's RDoc
  • Make the current format interchangeable
  • Tests for MongoID
  • Make Rails 4 controller generator output code that responds to JSON. This was true for Rails 3.2 but currently not with Rails 4.
  • Think more about the filter method. Should it filter by association name or by serialization keys? This is probably easier to explain with code. For example: def filter(keys); keys - [:comments]; end versus def filter(keys); keys - [:comments, :comment_ids]; end.
  • Re-implement merge_assoc which cached serialization output to improve performance for larger serialization operations. We could change the current implementation with a per-serializer output cache.
  • Implement the JSON API format
  • Add support for polymorphic associations
  • Come up with a better caching solution
  • Re-add Serializer#schema
| |
comments powered by Disqus