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

Action View: Remove internal calls to tag with positional arguments #49377

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

seanpdoyle
Copy link
Contributor

@seanpdoyle seanpdoyle commented Sep 25, 2023

Motivation / Background

Follow-up to #49371

In the wake of the deprecation of tag(:div) in favor of tag.div, this commit replaces all calls made internally with the more modern syntax.

This change is separate from the deprecation cycle, since there are slight behavioral changes, since the HTML output changes from self-closing elements like <input /> to open void elements like <input>.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch 7 times, most recently from 6cd9799 to c15cde8 Compare September 25, 2023 23:20
@rafaelfranca
Copy link
Member

How much slower are the new calls?

@@ -1007,8 +1010,8 @@ def extra_tags_for_form(html_options)

def form_tag_html(html_options)
extra_tags = extra_tags_for_form(html_options)
html = tag(:form, html_options, true) + extra_tags
prevent_content_exfiltration(html)
html = content_tag(:form, extra_tags, html_options).then { |html| html.delete_suffix!("</form>") }
Copy link
Member

Choose a reason for hiding this comment

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

Building a tag, just to remove the closing tag, and then calling html_safe doesn't seem worthy doing. You are making the framework do more work than it is necessary. This has performance impact.

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 change would be unnecessary if #44275 were merged.

@rafaelfranca
Copy link
Member

Simple benchmark shows me it is almost 2 slower

# frozen_string_literal: true

require "bundler/setup"

require "action_view"
require "minitest/autorun"
require "benchmark/ips"

class Foo
  include ActionView::Helpers
end

helpers = Foo.new

Benchmark.ips do |x|
  x.report("tag") { helpers.tag("a", href: "foo") }
  x.report("tag_builder") { helpers.tag.a(href: "foo") }
  x.compare!
end
Warming up --------------------------------------
                 tag    48.738k i/100ms
         tag_builder    29.990k i/100ms
Calculating -------------------------------------
                 tag    441.345k (±21.9%) i/s -      2.096M in   5.067122s
         tag_builder    265.445k (±23.2%) i/s -      1.230M in   5.085887s

Comparison:
                 tag:   441344.9 i/s
         tag_builder:   265445.4 i/s - 1.66x  (± 0.00) slower

I don't think it is worthy changing internal usage.

@seanpdoyle
Copy link
Contributor Author

@rafaelfranca thank you so much for your detailed review, especially the benchmark.

I would love to address these performance issues enough to make my recent batch of TagBuilder pull requests more viable.

I've opened #49390 to explore what it'd be like to dynamically define methods to avoid calls to #method_missing. I'd really appreciate feedback on the approach. #49390 only explores void and self-closing elements, but if it's deemed to be worth exploring, I'm happy to work on elements with content too.

@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch from c15cde8 to 86b7bd1 Compare October 5, 2023 14:23
@seanpdoyle
Copy link
Contributor Author

seanpdoyle commented Oct 5, 2023

In response to #49377 (comment), I've opened #49390. With that being merged, I consider the following to be the remaining barriers to this work progressing:

  1. The ActiveModel tag helper: Action View: Remove internal calls to tag with positional arguments #49377 (comment)
  2. Rendering open <form> elements requiring String manipulation (comment)

While 1. is a common enough issue to make exploring alternatives a good idea, I wonder if 2. might be addressed by a change like #44275.

@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch from 86b7bd1 to c9aa1d8 Compare October 5, 2023 23:45
@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch 3 times, most recently from 4ab4a53 to 33d611d Compare October 16, 2023 22:14
@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch 3 times, most recently from d90b3e0 to 927dbfc Compare December 2, 2023 20:13
@seanpdoyle
Copy link
Contributor Author

seanpdoyle commented Jan 12, 2024

Simple benchmark shows me it is almost 2 slower

The changes introduced by #49390 have changed the difference in performance to 1.34x slower:

# frozen_string_literal: true

require "bundler/setup"
require "action_view"
require "benchmark/ips"

class Foo
  include ActionView::Helpers
end

helpers = Foo.new

Benchmark.ips do |x|
  x.report("tag") { helpers.tag("a", href: "foo") }
  x.report("tag_builder") { helpers.tag.a(href: "foo") }
  x.compare!
end
Warming up --------------------------------------
                 tag    80.406k i/100ms
         tag_builder    59.307k i/100ms
Calculating -------------------------------------
                 tag    808.012k (± 0.7%) i/s -      4.101M in   5.075322s
         tag_builder    602.288k (± 0.7%) i/s -      3.025M in   5.022178s

Comparison:
                 tag:   808012.0 i/s
         tag_builder:   602288.3 i/s - 1.34x  slower

While that's still a degradation in performance, a benchmark one layer up the stack based on ActionDispatch::IntegrationTest yields comparable performance:

<%# app/views/documents/index.html.erb %>
<%= @documents.each do |document| %>
  <% if @use_builder %>
    <%= tag.a document.title, href: "/" %>
  <% else %>
    <%= tag "a", document.title, href: "/" %>
  <% end %>
  <p><%= document.content %></p>
<% end %>
# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
  class_attribute :use_builder, default: false

  def index
    @documents = Document.all
    @use_builder = use_builder
  end
end

# test/integration/documents_benchmark_test.rb

require "test_helper"
require "benchmark/ips"

class DocumentsIntegrationTest < ActionDispatch::IntegrationTest
  test "index" do
    get "/documents"
    assert_equal 200, response.status
  end
end

Benchmark.ips do |bm|
  bm.report "tag" do
    DocumentsController.use_builder = false
    Minitest.run_one_method(DocumentsIntegrationTest, "test_index")
  end

  bm.report "tag_builder" do
    DocumentsController.use_builder = true
    Minitest.run_one_method(DocumentsIntegrationTest, "test_index")
  end

  bm.compare!
end

The results:

Calculating -------------------------------------
                 tag   153.000  i/100ms
         tag_builder   163.000  i/100ms
-------------------------------------------------
                 tag      1.658k (± 1.6%) i/s -      8.415k
         tag_builder      1.659k (± 1.4%) i/s -      8.313k

Comparison:
         tag_builder:     1659.0 i/s
                 tag:     1657.5 i/s - 1.00x slower

@skipkayhil
Copy link
Member

skipkayhil commented Jan 12, 2024

The changes introduced by #49390 have changed the difference in performance to 1.34x slower:

Here's a PR to make it only 12% slower: #50737

@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch 3 times, most recently from 5a54ba5 to c4f668f Compare January 16, 2024 21:18
@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch 2 times, most recently from 2871543 to 085f2df Compare January 21, 2024 15:26
@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch from 085f2df to 0ac3117 Compare January 29, 2024 14:28
@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch 2 times, most recently from 50eaddb to a493b2d Compare March 8, 2024 13:31
Follow-up to [rails#49371][]

In the wake of the deprecation of `tag(:div)` in favor of `tag.div`,
this commit replaces all calls made internally with the more modern
syntax.

This change is separate from the deprecation cycle, since there are
**slight** behavioral changes, since the HTML output changes from
self-closing elements like `<input />` to open void elements like
`<input>`.

[rails#49371]: rails#49371
@seanpdoyle seanpdoyle force-pushed the remove-internal-tag-with-positional-arguments branch from a493b2d to 5c99edf Compare April 18, 2024 23:37
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.

3 participants