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

Fix for lotus/lotus issue 185 - Performance issue with rendering a partial view #86

Merged
merged 19 commits into from Jan 29, 2016

Conversation

stevehook
Copy link

This is a potential fix for hanami/hanami#185

Partial templates are loaded on start-up and cached in the framework configuration object - Lotus::View.configuration and pulled from there by the PartialFinder at runtime.

I'm not too sure about the cache structure. It's a two-level hash at the moment, so that you can lookup the templates for a given path and then pick the one with the required format.

I have left the old implementation as a fallback for now. Are there any good reasons not to remove it completely (other than the fact that there are a few tests I would need to fix)?

The framework configuration object should hold a collection of all the
partials in the file system for the given load paths.
view_test.rb doesn't actually load the framework
This seems to be failing because other tests are overwritting the
`configuration.root` path causing a side effect in this test. Solution
is to explicitly reload the configuration with a known root path value.
Also add some tests to assert that the cache is actually being used
rather than re-reading the partial template files
@pascalbetz
Copy link

@stevehook thanks for the PR. Some remarks/questions:

  • did you check the effect of caching partials, performance wise?
  • what about reloading modified partials (in development mode)?
  • what do you think about loading all templates (layouts, partials, templates) the same way?

@runlevel5
Copy link
Member

@stevehook great work, would you be able to provide a benchmark too?

@stevehook
Copy link
Author

Thanks for the comments @pascalbetz.

  • I must admit I haven't run any proper performance tests, I should be able to do that tomorrow.
  • Reloading I'll need to check on also, I thought that the whole framework got reloaded but I'm not sure about that.
  • I think it would be a good idea to load all the templates in the same way, for consistency mainly, as there isn't really a performance issue with non-partials. To be honest I didn't want to attempt too big a change to begin with, but I'd be happy to follow it up when you are happy with the direction it's going in.

@pascalbetz
Copy link

@stevehook i think it's a good start and its good to start small.

@pascalbetz
Copy link

@stevehook

I made a small example to test this, my findings:

i never had a cache hit, the @partials instance was not the same when loading the partials vs. when looking them up. Hence no performance gain as it always finds/instantiates the template anew. Did not investigate further though.

Can you check again? Or perhaps share your sample app?

About the "unified" mechanism for template loading: i think this is something that @jodosha, @joneslee85, @AlfonsoUceda would need to chime in. What do you guys think?
Currently Templates/Layouts/Partials are preloaded differently. I think that mechanism should be unified so that the 'only' difference is the lookup (partials by :partials argument on the render call, templates by action/controller name, layout by layout config).
Not sure how code reloading would work? Would the cache need to deal with that (mtime, if file system) or would shotgun/entr handle this?
Also i think the template loading mechanism should be able to handle templates stored somewhere else than file system (quite useful for CMS applications). Not that it need to be provided by lotus (at this time) but a clean/simple extension point/interface would be great.

# @since 0.6.0
# @api private
def find_cached_template
partials = Lotus::View.configuration.partials[[view_template_dir, template_name].join(separator)] ||
Copy link
Member

Choose a reason for hiding this comment

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

@stevehook Referencing Lotus::View here is unsafe. Multiple "copies" of the framework, with different configuration instances are allowed within the same Ruby process.

In Lotus projects we can have Web::View, or Admin::View, with different set of partials. Lotus::View shouldn't have partials in its registry.


To solve this problem, you should reference @view.configuration (see #find in this class).

Copy link
Member

Choose a reason for hiding this comment

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

@stevehook This is Inappropriate Intimacy of Configuration. This method knows that partials are stored as a Hash and the structure of keys and values.

It could be opportune to expose Configuration#partial_for, we can pass all the needed arguments to this new method and let find_cached_template to just invoke it.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for all the helpful comments @jodosha

This is the part that I found most difficult to understand. I've changed this to use Lotus::View::Configuration.for(@view). Not sure if it is quite right yet.

CC @pascalbetz I think this is the reason that the sample application you created would have not benefitted from the cache.

@stevehook
Copy link
Author

@pascalbetz I'll check my sample app as soon as I get chance. One thing I did have to do was run it with the --no-code-reloading option to get any kind of caching working but it sounds like you are hitting a different issue.

@jodosha
Copy link
Member

jodosha commented Jan 3, 2016

@stevehook Thank you for this PR. It will give an perf boost to Lotus apps!! 👍

We probably need to add an integration test to prove that the whole stack works as expected. We have Store and CardDeck fixtures (see test/fixtures.rb) that can be expanded a bit to prove that:

  • One application (eg. Store) is able to render its own partial.
  • Referencing a partial that belongs to another application, will raise an error.

@jodosha
Copy link
Member

jodosha commented Jan 3, 2016

@stevehook The next version of lotus-view is v0.5.0, that will be released on Jan 12th. However, I see this new logic to be merged later that date.

Can you please replace all the @since 0.6.0 with @since x.x.x? This is our convention for master branch. Thank you!

# @since 0.6.0
# @api private
def load_partials!
Lotus::View::Rendering::PartialTemplatesFinder.new.find_partials(root).each { |key, format, template| add_partial(key, format, template) }

Choose a reason for hiding this comment

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

Could you move this to multiple lines with do/end? i think this would be beter for readability.

Copy link
Author

Choose a reason for hiding this comment

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

👍

I'm still not sure I fully understand the configuration hierarchy but
I think this handles the case in which there are multiple applications
in the same process.

Also force the format keys to be symbols.
We need to explicitly reload the configuration in some of the tests to
make this work. The common code is factored out into
reload_configuration_helper.rb.
@stevehook
Copy link
Author

I've updated the PR and addressed some of the comments. I still need to get some performance figures and look at the integration tests among other things.

# @since x.x.x
# @api private
def load_partials!
Lotus::View::Rendering::PartialTemplatesFinder.new.find_partials(root).each do |key, format, template|
Copy link
Member

Choose a reason for hiding this comment

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

what do you think about module or class method instead?

Copy link
Author

Choose a reason for hiding this comment

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

👍

PartialTemplatesFinder doesn't have any state so makes sense to me.

@pascalbetz
Copy link

@stevehook i'll try and unify template loading on top of your changes.

@pascalbetz
Copy link

@stevehook can you run a benchmark with/without partials, with/without your changes so we can be sure that the change has the desired effect?

@stevehook
Copy link
Author

I've done some rough benchmarks, using the technique that @smt116 used to diagnose the original problem. The test is hitting a very simple test application (that renders a single partial - https://github.com/stevehook/lotus-test).

Seems to me there is a measurable difference especially with Puma.

branch master with Webbrick:

$  wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     9.68ms    3.51ms  25.93ms   69.77%
    Req/Sec   103.76     16.02   141.00     61.50%
  2072 requests in 5.02s, 0.92MB read
Requests/sec:    413.13
Transfer/sec:    187.20KB

branch issue_185 with Webbrick:

$  wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.28ms    2.65ms  31.21ms   69.31%
    Req/Sec   160.58     19.74   232.00     73.50%
  3206 requests in 5.01s, 1.42MB read
Requests/sec:    639.34
Transfer/sec:    289.77KB

branch master with Puma:

$ wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.50ms    2.06ms  19.05ms   72.06%
    Req/Sec   154.54     16.84   191.00     78.00%
  3083 requests in 5.01s, 1.10MB read
Requests/sec:    615.32
Transfer/sec:    224.31KB

branch issue_185 with Puma:

$ wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.88ms    1.30ms  19.90ms   76.02%
    Req/Sec   259.65     25.51   320.00     67.50%
  5177 requests in 5.01s, 1.84MB read
Requests/sec:   1033.85
Transfer/sec:    376.73KB

Also fix inconsistency in Configuration#load_partials!
@runlevel5
Copy link
Member

nice, I love what I am seeing 👍

@jodosha
Copy link
Member

jodosha commented Jan 5, 2016

@stevehook 💯 Thank you very much! I just need some more time to review the entire PR. I want to merge this after we'll release v0.6.0 (next Tue).

@jodosha jodosha self-assigned this Jan 5, 2016
@stevehook
Copy link
Author

Thanks @jodosha - I didn't get change to add those integration tests yet. I can try to do that over the next few days.

@jodosha
Copy link
Member

jodosha commented Jan 5, 2016

@stevehook Please do 😄

Created two new applications in fixtures because extending Store and
CardDeck caused too many side-effects in/from existing tests
PartialFinder shouldn't have knowledge of the Configuration objects
internal data structures
@stevehook
Copy link
Author

@jodosha I was having some trouble with those integration tests.

My first attempt was to extend the existing fixtures as you suggested. stevehook/view@issue_185...stevehook:issue_185_integration_tests

I extended the fixtures for CardDeck and Store apps so that each has a view with a partial in their own app, added tests for those together with a test to verify that an exception is raised when attempting to include a partial from the other app. These work fine in isolation but not when running the full test suite because there are other tests that unload these apps. A simple reload doesn't restore the original configuration; the root attribute is reset in the unload! but not restored in load!.

I needed to add a layout template file to render CardDeck views - test/fixtures/templates/card_deck/app/templates/application.html.erb. This change broke a couple of other tests that render the Articles::Index view I think because this new layout file was getting used instead of test/fixtures/templates/application.html.erb.

So to avoid these side-effects I ended up creating two new small apps in fixtures (App1 and App2) just to test partials. These seem to work but there may be a simpler way to do this that I'm not seeing at the moment.

@jodosha
Copy link
Member

jodosha commented Jan 17, 2016

@stevehook

So to avoid these side-effects I ended up creating two new small apps in fixtures (App1 and App2) just to test partials.

You took the right decision then. 👍


These problems for our builds are due to global state (CardDeck or Lotus::View), being (un)loaded. We need to fix those, but after Lotus 1.0 will be out.


# @since x.x.x
# @api private
def find_cached_template
Copy link
Member

Choose a reason for hiding this comment

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

@stevehook This abstraction is not needed because #find only invokes it and don't do anything else. So #find_cached_template can become #find instead.

Add default encoding to the partial template
Introduce `PartialFile` class to eliminate data clump
Instantiate `PartialTemplatesFinder` with instance of configuration
Remove redundant `PartialFinder#find_cached_template` method
@stevehook
Copy link
Author

I've rerun the simple performance benchmark using latest master and issue_185 branches, results below.

It looks like the results are pretty consistent with the original measurements I took before the latest changes, neither master or this branch have got any better or worse.

branch master with Webrick:

wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     9.20ms    3.08ms  21.89ms   70.32%
    Req/Sec   108.93     13.61   141.00     73.00%
  2180 requests in 5.03s, 0.96MB read
Requests/sec:    433.63
Transfer/sec:    196.56KB

branch issue_185 with Webrick:

wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.25ms    2.54ms  19.51ms   66.78%
    Req/Sec   161.29     17.80   202.00     59.50%
  3225 requests in 5.02s, 1.43MB read
Requests/sec:    642.17
Transfer/sec:    291.12KB

branch master with Puma:

wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.67ms    1.78ms  16.38ms   71.65%
    Req/Sec   150.43     13.40   200.00     80.50%
  3003 requests in 5.01s, 1.07MB read
Requests/sec:    599.08
Transfer/sec:    218.27KB

branch issue_185 with Puma:

wrk --connections 4 --duration 5 --threads 4 http://localhost:2300/books
Running 5s test @ http://localhost:2300/books
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.94ms    1.15ms  10.36ms   73.10%
    Req/Sec   254.18     17.33   373.00     79.00%
  5068 requests in 5.01s, 1.80MB read
Requests/sec:   1011.78
Transfer/sec:    368.67KB

@jodosha jodosha merged commit 94fbb3d into hanami:master Jan 29, 2016
@jodosha
Copy link
Member

jodosha commented Jan 29, 2016

@stevehook I've finally merged this. Thank you very much! 👍

@stevehook
Copy link
Author

thank you @jodosha and love the new project name by the way!

timriley added a commit that referenced this pull request Mar 15, 2020
…context

Rename controller `context` setting to `default_context`
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.

None yet

4 participants