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

Caching is working but slower #259

Open
a14m opened this issue Mar 22, 2015 · 21 comments
Open

Caching is working but slower #259

a14m opened this issue Mar 22, 2015 · 21 comments

Comments

@a14m
Copy link

a14m commented Mar 22, 2015

I've tried to use caching with collections (with multiple solutions) the problem is that when ever I try caching the response become slower
consider the following example of a collection that renders 2 partials for every item in it (around 25 item)

    json.data do
      json.array! @organizations do |organization|
        json.partial! 'api/v1/organizations/organization', organization: organization
        json.partial! 'api/v1/organizations/links', organization: organization
      end
    end

without caching the average response time is around ~38ms (on average)

now with caching

    json.data do
      json.array! @organizations do |organization|
        json.cache! organization do
          json.partial! 'api/v1/organizations/organization', organization: organization
          json.partial! 'api/v1/organizations/links', organization: organization
        end
      end
    end

with the jbuilder default caching and dalli store is properly installed and configured (I could verify that there was no cache miss)

the average response is around ~59ms (on average)

using the syntax found on Cache Digest

json.data do
  json.cache! @organizations do
    json.partial! 'api/v1/organizations/organization', collection: @organizations, as: :organization
    json.partial! 'api/v1/organizations/links', collection: @organizations, as: :organization
  end
end

the average response time is ~41ms (on average), and the response is different than the other responses

    # Instead of getting
    [{ "data":{}, "links":{} }, { "data":{}, "links":{} }]
    # I get
    [{ "data":{}, "data":{}, "links":{}, "links":{} }]

but the cache digest of the file is a very big string that will easily exceed the unix max file name length.
this is the filename for example.

    Cache write: jbuilder/organizations/5509f9284162643526000000-20150322012449497000000/organizations/5509e5924162643056020000-20150320223230684000000/organizations/550b54d8416264add2040000-20150321004501311000000/organizations/550e35704162640a98030000-20150322032224768000000/organizations/550e357b4162640a98050000-20150322032235260000000/organizations/550e35834162640a98080000-20150322032243162000000/organizations/550e35894162640a980a0000-20150322032249767000000/organizations/550e35904162640a980c0000-20150322032256464000000/organizations/550e35944162640a980e0000-20150322032300519000000/organizations/550e35984162640a98100000-20150322032304428000000/organizations/550e359c4162640a98120000-20150322032308542000000/organizations/550e35a04162640a98140000-20150322032312514000000/organizations/550e35a54162640a98160000-20150322032317066000000/organizations/550e35a84162640a98180000-20150322032320850000000/organizations/550e35ac4162640a981a0000-20150322032324716000000/organizations/550e35b04162640a981c0000-20150322032328643000000/organizations/550e35b54162640a981e0000-20150322032333651000000/organizations/550e35ba4162640a98200000-20150322032338114000000/organizations/550e35bd4162640a98220000-20150322032341889000000/organizations/550e35c14162640a98240000-20150322032345602000000/organizations/550e35c54162640a98260000-20150322032349739000000/3fcda1f9c320ab4284da56b4b2337cf5`

I've also tired Jbuilder Cache Multi

 json.data do
  json.cache_collection! @organizations do |organization|
    json.partial! 'api/v1/organizations/organization', organization: organization
    json.partial! 'api/v1/organizations/links', organization: organization
  end
end

and the response was around ~57ms (on average)

plus with both jbuilder cache and multi I'm getting a lot of these in the logs

Cache digest for app/views/api/v1/organizations/index.json.jbuilder: 3a51096b9c8da6a2cdb5b5a33ee58ea4
Cache digest for app/views/api/v1/organizations/_organization.json.jbuilder: 4a1f1d49c90fdd867d88701f8a3fd6e1
Cache digest for app/views/api/v1/organizations/_links.json.jbuilder: f2a881e125f95421d566edd571fdec73
Cache digest for app/views/api/v1/organizations/index.json.jbuilder: 3a51096b9c8da6a2cdb5b5a33ee58ea4
Cache digest for app/views/api/v1/organizations/_organization.json.jbuilder: 4a1f1d49c90fdd867d88701f8a3fd6e1
Cache digest for app/views/api/v1/organizations/_links.json.jbuilder: f2a881e125f95421d566edd571fdec73
Cache digest for app/views/api/v1/organizations/index.json.jbuilder: 3a51096b9c8da6a2cdb5b5a33ee58ea4
Cache digest for app/views/api/v1/organizations/_organization.json.jbuilder: 4a1f1d49c90fdd867d88701f8a3fd6e1

so is it something wrong with my implementation or machine or local environment ?
Rails 4.2.0, and Jbuilder 2.2.11

I also posted this issue to StackOverflow

@vincentwoo
Copy link
Contributor

The issue is that jbuilder caching is fairly naive - it basically dumps a serialized version of a giant activerecord blob into the cache store, then pulls it out, deserializes it, and then EVENTUALLY serializes THAT to JSON.

In the future, jbuilder will hopefully act directly on strings, but until then, I think jbuilder caching is best not used russian-doll style.

@rwz
Copy link
Collaborator

rwz commented Mar 30, 2015

Well, @vincentwoo is right. Currently caching provides benefits only when it allows to skip some of AR queries or you're using really computionally heavy view helpers.

When you already have the records fetched and only perform basic extractions, caching only slows things down.

@rwz rwz closed this as completed Mar 30, 2015
@a14m
Copy link
Author

a14m commented Mar 30, 2015

Can anyone confirm that this behaviour also exist on RABL ?

@vincentwoo
Copy link
Contributor

@artmees: I think that question is best asked to the RABL user group or github issues list.

@rwz: Any chance you could document the caching tradeoffs of jbuilder in the README? Have definitely seen this issue trip up a few developers. They expected that jbuilder have basically the same tradeoffs that normal erb view caching does, and it's surprising to find out that it does not.

@a14m
Copy link
Author

a14m commented Mar 31, 2015

BTW if anyone of you both @rwz and @vincentwoo wants to add the answer to the stackoverflow question and claim the bounty... be my guest...

+1 for adding the documentation

@vincentwoo
Copy link
Contributor

@rwz rwz reopened this Mar 31, 2015
@drobtravels
Copy link

Any thoughts on what would be the best approach to cache the JSON itself? I'm happy to put in a PR, but don't see a clear path forward.

Right now the entire Jbuilder is serialized to JSON at once with MultiJson.dump. If instead each nested Hash / Array was instead its own Jbuilder object, they could all be independently serialized (and cached), but the cached portion would always need to be nested under its own key.

It would be ideal if merge! could accept already serialized JSON but from some quick research I don't believe the common JSON serializers support mixing together Ruby objects with already serialized JSON.

@xinuc
Copy link

xinuc commented Apr 15, 2015

anyone working on this issue or any fork addressing this?

@rwz
Copy link
Collaborator

rwz commented Apr 15, 2015

Not that I'm aware of. I keep this issue open to remind me to update README.

@drobtravels
Copy link

@xinuc Due to the current way jbuilder works it would likely require large changes (and possibly API changes) which is a problem due to the large adoption of jbuilder.

I think the best path forward would be creating a new gem focused on better caching. Feel free to reach out if you are interested in collaborating.

@rwz
Copy link
Collaborator

rwz commented Apr 15, 2015

@droberts84 @xinuc I think the main problem is not Jbuilder, but the total absence of partial caching API for JSON.

@drobtravels
Copy link

@rzw What do you think about my approach mentioned above, and caching the JSON as an object in the cache store?

I would love to get this working with Jbuilder, and am happy to contribute. However, I currently have a use case which requires very large JSON structures that are often the same, so for best performance I need to create some sort of workaround for this issue.

@xinuc
Copy link

xinuc commented Apr 15, 2015

As far as I can understand, the underlying problem is the lack of mean to append a json string to the serializer. Please CMIIW.

a stupid hack I can think of is to parse the string and merge it again as a hash

json.something JSON.parse('{"some": "json string"}')

is there any better way we can achieve this?

@xinuc
Copy link

xinuc commented Apr 15, 2015

well, some quick searches, and apparently appending arbitrary string is not possible.
the resulting json can be invalid.

so I think parsing the json and merging it again is not that stupid after all 😓

@rwz
Copy link
Collaborator

rwz commented Apr 15, 2015

Parsing JSON string and merging it doesn't make too much sense because it's even slower than existing caching workflow we have right now.

@xinuc
Copy link

xinuc commented Apr 16, 2015

yeah, I'd be surprised if it's not.
Since you're a maintainer of multi_json, can you confirm that appending an arbitrary string to json is not possible for now?
I was expecting it'd be as easy as appending string to html erb template :(

@smileyan
Copy link

I think @rwz is right: the main reason is partial.
The performance of iterative a partial is very slow. Iterative a collection in a partial is better.
See the code https://github.com/rails/jbuilder/blob/master/lib/jbuilder/jbuilder_template.rb#L89,
the partial in Jbuilder does not equal the partial in actionview.
There is the case: https://gist.github.com/smileyan/66168893917c3ac5ebf4

@TonsOfFun
Copy link

I agree with @smileyan I experienced the same performance benefits by abandoning that approach.

@benubois
Copy link

benubois commented Apr 19, 2020

Would it make sense to swap out the cache system in favor of ActionView cache helpers? This would be less surprising and more performant than the way the current cache works.

ActionView::CollectionCaching can already be (ab)used to cache json strings from jbuilder templates. This retains the benefit of recyclable cache keys and nested template digests (Russian doll caching).

jbuilder cache:

Started GET "/users/jbuilder_cache.json" for ::1 at 2020-04-19 14:10:17 +0200
Processing by UsersController#jbuilder_cache as JSON
  Rendering users/jbuilder_cache.json.jbuilder
Read fragment jbuilder/views/users/_cached_user:f4d940b182e11dcdd5c336b56db13198/0 (0.4ms)
  Rendered users/_cached_user.json.jbuilder (Duration: 0.6ms | Allocations: 918)
  …
Read fragment jbuilder/views/users/_cached_user:f4d940b182e11dcdd5c336b56db13198/99 (0.2ms)
  Rendered users/_cached_user.json.jbuilder (Duration: 0.3ms | Allocations: 803)
  Rendered users/jbuilder_cache.json.jbuilder (Duration: 95.8ms | Allocations: 118304)
Completed 200 OK in 102ms (Views: 96.3ms | Allocations: 134403)

No cache:

Started GET "/users/no_cache.json" for ::1 at 2020-04-19 14:11:34 +0200
Processing by UsersController#no_cache as JSON
  Rendering users/no_cache.json.jbuilder
  Rendered users/_user.json.jbuilder (Duration: 0.2ms | Allocations: 275)
  …
  Rendered users/_user.json.jbuilder (Duration: 0.1ms | Allocations: 274)
  Rendered users/no_cache.json.jbuilder (Duration: 43.6ms | Allocations: 64822)
Completed 200 OK in 50ms (Views: 44.1ms | Allocations: 80921)

Collection cache:

Started GET "/users/collection_cache.json" for ::1 at 2020-04-19 14:12:09 +0200
Processing by UsersController#collection_cache as JSON
  Rendering users/collection_cache.html.erb
  Rendered collection of users/_user.json.jbuilder [100 / 100 cache hits] (Duration: 9.7ms | Allocations: 8080)
  Rendered users/collection_cache.html.erb (Duration: 9.9ms | Allocations: 8175)
Completed 200 OK in 16ms (Views: 10.3ms | Allocations: 24194)

Here's an example project for how to get this to work. Run it and then you can visit:

#!/bin/bash

set -eux

gem install rails

rails new blog              \
--skip-action-cable         \
--skip-action-mailbox       \
--skip-action-mailer        \
--skip-action-text          \
--skip-active-record        \
--skip-active-storage       \
--skip-bootsnap             \
--skip-git                  \
--skip-javascript           \
--skip-keeps                \
--skip-listen               \
--skip-puma                 \
--skip-spring               \
--skip-sprockets            \
--skip-system-test          \
--skip-test                 \
--skip-turbolinks           \
--skip-webpack-install

mkdir -p blog/app/views/users

cat > blog/config/routes.rb <<- EOF
Rails.application.routes.draw do
  resources :users do
    collection do
      get :jbuilder_cache
      get :collection_cache
      get :no_cache
    end
  end
end
EOF

cat > blog/app/views/users/collection_cache.html.erb <<- EOF
[<%= render partial: "users/user", collection: @users, as: :user, spacer_template: "comma", cached: true %>]
EOF

cat > blog/app/views/users/_comma.html.erb <<- EOF
,
EOF

cat > blog/app/views/users/no_cache.json.jbuilder <<- EOF
json.array!(@users) do |user|
  json.partial! "users/user", user: user
end
EOF

cat > blog/app/views/users/jbuilder_cache.json.jbuilder <<- EOF
json.array!(@users) do |user|
  json.partial! "users/cached_user", user: user
end
EOF

cat > blog/app/views/users/_cached_user.json.jbuilder <<- EOF
json.cache! user do
  json.partial! "users/user", user: user
end
EOF

cat > blog/app/controllers/users_controller.rb <<- EOF
class UsersController < ApplicationController
  before_action :users

  def collection_cache
    render template: "users/collection_cache.html.erb", layout: nil, content_type: "application/json"
  end

  def jbuilder_cache
  end

  def no_cache
  end

  private

  def users
    @users = 100.times.map.with_index { |index| user(index) }
  end

  def user(index)
    OpenStruct.new({
      cache_key: index.to_s,
      cache_version: Time.parse("2020-04-19T11:37:11+02:00"),
      content: "<p>This is <i>serious</i> monkey business</p>",
      visitors: 15,
      author: OpenStruct.new({
        name: "David H.",
        email_address: "'David Heinemeier Hansson' <[email protected]>",
        url: "http://example.com/users/1-david.json"
      }),
      comments: [
        OpenStruct.new({ content: "Hello everyone!", created_at: Time.parse("2011-10-29T20:45:28-05:00") }),
        OpenStruct.new({ content: "To you my good sir!", created_at: Time.parse("2011-10-29T20:47:28-05:00") })
      ],
      attachments: [
        OpenStruct.new({ filename: "forecast.xls", url: "http://example.com/downloads/forecast.xls" }),
        OpenStruct.new({ filename: "presentation.pdf", url: "http://example.com/downloads/presentation.pdf" })
      ]
    })
  end
end
EOF

cat > blog/app/views/users/_user.json.jbuilder <<- EOF
json.content user.content
json.(user, :created_at, :updated_at)
json.author do
  json.name user.author.name
  json.email_address user.author.email_address
  json.url user.author.url
end
json.visitors user.visitors
json.comments user.comments, :content, :created_at
json.attachments user.attachments do |attachment|
  json.filename attachment.filename
  json.url attachment.url
end
EOF

touch blog/tmp/caching-dev.txt

blog/bin/rails server

@malomalo
Copy link

@benubois I tried a similar approach a while back from what I recall there were numerous cases where the template didn't contain enough information about where the cache was being inserted to know how to insert it and give valid JSON.

I followed up on #289 here; TLDR: tried, failed, ended up making TurboStreamer

@malomalo
Copy link

Just reread your comment @benubois and this may work if you only cache partials that are always make up an array

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

No branches or pull requests

9 participants