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 #9

Closed
ajb opened this issue Nov 14, 2014 · 9 comments
Closed

Caching #9

ajb opened this issue Nov 14, 2014 · 9 comments

Comments

@ajb
Copy link

ajb commented Nov 14, 2014

I wrote this caching module for erector-rails4, which adds all of the neat cache_digests stuff to widgets. What's even cooler about using cache_digests in Erector is that because your needs are explicit, you can calculate the cache_key for a widget without having to manually specify the objects it depends on.

Is this something you'd be interested in implementing in fortitude? Anything about the implementation that you'd want to change?

@ageweke
Copy link
Owner

ageweke commented Nov 14, 2014

This looks great! Let me take a look it in more depth this weekend. (You're correct that Fortitude is not an employer-sponsored project, and I'm quite scrupulous about not working on it on my employer’s time or premises.) I'd love to have a clean, simple caching mechanism built in that integrates well with various things (Rails’ cache most particularly) without actually requiring any of them.

Vaguely related:

  • I’d be curious to see the speedup with caching in Fortitude. Erector widgets, at least when I used them, always required aggressive caching to perform well, but that was because Erector was something like 30x slower than ERb. Fortitude is actually faster than ERb and hence 40+x faster than ERb, so I'm hoping caching becomes less necessary — not that it’s a bad thing per se, just always more conceptual overhead to add to any system.
  • Don’t know if you’ve seen the static stuff in Fortitude, but, if you have a method of a widget (which can be content, i.e., the entire widget, if you want) that doesn’t actually depend on any needs, you can call static :foo at class level, and it will replace that method with a version that just splats pre-rendered HTML onto the output directly. This can make a big difference when rendering significant sections of static HTML.

@ajb
Copy link
Author

ajb commented Nov 14, 2014

That all sounds 👍, and yes, I saw the staticization stuff, it's a great idea, but I'm not sure we'd be able to use it in too many places.

@ageweke
Copy link
Owner

ageweke commented Nov 19, 2014

I spent some time today wrapping my head around this and how it interacts with Rails’ caching stuff, and I really like it — extremely simple. I plan to write it (among other things, it should use an around_content filter, not override #emit, which doesn’t exist in Fortitude) + integrate it (i.e., add lots of specs) as soon as I get a chance. Thank you much!

@ajb
Copy link
Author

ajb commented Nov 19, 2014

Awesome! I'd love to start contributing code myself, but my first goal is still to find an application of ours to port over to Fortitude.

@ageweke
Copy link
Owner

ageweke commented Nov 20, 2014

Completely understood! By all means, let me know if there’s anything I can help with. I’m always excited to have more people using it.

@jdickey
Copy link

jdickey commented Oct 8, 2015

Hey, @ageweke, just came across static and kicking myself for not reading this issue sooner. 😸

One question: we have something of a shop standard (for classes in general) that methods that are mutually ignorant of object state get put into an Internals module as class methods. For instance, this is close to one of our actual page widgets:

require_relative 'index_content' # declares Views::Welcome::Index::Content class
require 'menu_factory'

# Class encapsulating all page-specific view code for `welcome/index`.
class Views::Welcome::Index < Views::Base
  # Methods neither dependent on nor affecting instance state.
  module Internals
    def self.menu_factory
      MenuFactory.new
        .add_item(href: '#', caption: 'Home')
        .add_separator
        .add_item(href: '#', caption: 'Sign up')
        .add_item(href: '#', caption: 'Sign in')
    end
  end
  private_constant :Internals

  def content
    widget Views::Page, page_content: Content, nav_menu: Internals.menu_factory
  end
end # class Views::Welcome::Index

(For those wondering, we do this to silence Reek complaints about methods that don't affect instance state, that simply declaring def self.menu_factory in the outer class won't kill.)

How does Fortitude see this class? Will it treat the Internals module (and thus .menu_factory) as something it can automagically treat as static? Am I confusing the hell out of someone or something? And what's "best practice" here as far as Fortitude is concerned?


And why do I feel like a fxxxing n00b asking so many questions instead of diving in and getting even less sleep? Sorry, guys… I've been "live-blogging" a couple of exploratory apps (actually, variations on the same app); anybody with any interest and/or advice should be able to get a pretty fair handle on my thought processes there.

@ageweke
Copy link
Owner

ageweke commented Oct 8, 2015

@jdickey — first off, don't worry about feeling like a n00b! Fortitude still has effectively no documentation (which I'm working on), and so it's just impressive that folks are able to use it at all. Feel free to ask away…anything at all. I'm more than happy to answer any questions!

A brief introduction to static: static is a performance improvement for Fortitude when rendering large amounts of purely static HTML — i.e., HTML that is independent of any server or user data. For example, if you have a "typical" footer for a site that has links to the FAQ, About, Facebook, Twitter, and so on, probably none of that data ever changes. The code thus might look something a little like this:

  def footer
    div {
      ul {
        li { a("Facebook", :href => "http://www.facebook.com/…") }
        li { a("Twitter", :href => "http://www.twitter.com/…") }
        
      }
      
    }
  end

Because Fortitude works like it does, every single time any page is rendered, it's going to invoke every method — div, ul, li, a, etc. — in that footer, generating that same identical HTML.

This is inefficient — it's the exact same string every time, so why are we doing all this work? If you add static :footer above, here's what Fortitude does:

  • The first time #footer is called, anywhere in your application, Fortitude runs the method. However, when it does so, it (a) prohibits any access to any needs variables (since the whole idea here is to speed up HTML that never changes), and (b) saves the output of that method.
  • Next, it replaces the original #footer method on that class with one that simply splats out that same output.

In essence, once it's run the first time, that method now becomes:

  def footer
    rawtext "<div><ul><li><a href="http://www.facebook.com/">Facebook</a></li><li><a href="http://www.twitter.com/">Twitter</a></li>…"
  end

This runs much faster than the method above, because it's a single string concatenation, rather than a bunch of nested methods with blocks.

One of the reasons that I've placed documenting static lower on the list than some other things is that I think it's very rarely necessary. My own, real-world-esque benchmarks show Fortitude without any use of static whatsoever as being 20% faster than ERb, 3x faster than HAML, and a whopping 29x faster than Erector. Slim is 8% faster than Fortitude without static, but, with static, Fortitude is about 28% faster than Slim. Fortitude generates a ~375KByte page of complex, real-world HTML in about 32ms without static, and about 22ms with static.

(An interesting point: most other templating engines are fastest when cranking out large chunks of static HTML — because, to them, it's just giant strings to be concatenated directly onto the output — and slowest when doing lots of interpolation, if statements, and so on. Fortitude is exactly the opposite, which I feel good about. If you have lots of static HTML, speeding it up is usually very easy (there are tons of technologies, in and out of Rails, for doing so), and it's not really where Rails shines, either. But if you have lots of dynamic HTML, partials, conditionals, sub-views, etc. that depend on data from your server, that is where Rails shines, and that's where Fortitude shines, too.)

My guess is that for most applications, this difference is going to be in the noise until they've spent lots of time optimizing elsewhere. So I'd encourage folks to ignore static completely until they get deep into the optimization phase and find that squeezing an extra 5–10ms out of their views becomes important to them. But it's there when you need it, and can be valuable.

Coming back to your example: without seeing the code for MenuFactory, I can't tell if Internals.menu_factory returns some object that exposes methods that Views::Page would call on it (i.e., "return the list of menu items"), or if simply calling Internals.menu_factory actually renders HTML itself. If it's the former, there's no point in even trying to make it static, because static only applies to methods that render HTML. If it's the latter: you can't make a method static unless it's an instance method of a widget itself, since only those will have access to tag methods (p, div, etc.).

Is it the latter? If so, it's an interesting question you pose. I can definitely see the value in the convention you describe (and I love it when Rubyists think like this), but also don't see an immediate solution. I'll think more on it, though, if that's what you're trying to do. Is it?

Cheers,
Andrew

@jdickey
Copy link

jdickey commented Oct 9, 2015

@ageweke Thanks for the wondrously informative reply! I've a much clearer understanding of static now, and I don't find your benchmarks/comparisons wrt other templating engines surprising at all.

For the record, the code in Internals.menu_factory just loads up an internal data structure (a list of entries, if you will) that a call to MenuFactory#widget (from within Views::Page) then uses to render a widget. It's (quite probably) overly ceremonious, but it got the job done while the bulk of the development of the three widgets involved took place at different times and assumptions.

Fortitude is already turning into Great Stuff for us; documentation would be an Amazingly Good Thing,, of course, and in a few weeks I'm going to have to sit down and figure out how to put several of these widgets (the standard-page-layout widgets discussed here among them) into Gems we can decouple maintenance of from the apps that use them. I put a week into trying to wrap my head around that while you were enjoying your hike and gave up; hopefully I'll have better success next time.

Thanks again!

@ajb ajb mentioned this issue Nov 17, 2015
@ageweke
Copy link
Owner

ageweke commented Oct 12, 2016

I’m gonna close this for now — it’d be great to fold discussion in under PR #54. :)

@ageweke ageweke closed this as completed Oct 12, 2016
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

3 participants