dry-configurable: use of method_missing

I’ve been brainstorming ideas for performance improvements on dry-view but it seems most of the initial wins are coming from dry-configurable so, rather than post another PR to the dry-view repo, I thought a discussion here might be better.

In most of my code, I prefer clarity over performance, but in a contrived benchmark comparing the render time of 100 nested partials in Action View, dry-view, Phlex, and View Component, I found that dry-view was ~10x slower than Action View. I hope to bring some ideas to the table for levelling the playing field in the knowledge that any suggestion needs to be weighed against many other factors and may be undesirable to the community.

Since I’m unfamiliar with all the ways dry-configurable is used, my experiments have been inside dry-view. I can’t imagine this is what we want, but I figured it could drive discussion, and we can worry about the details at a later stage.

Most recently, I’ve been looking at how dry-configurable relies upon method_missing to forward requests onto [](name) and []=(name, value). Unfortunately, method_missing is quite slow, which has a noticeable impact on a componentised site.

Interestingly, I see no gain when working off main, however, if I apply PR 156 and PR 157, iterations per second are increased by 30% when generating settings getters and setters as instance methods.

                Main    100.263  i/s -      1.008k in  10.076792s
            Baseline    245.789  i/s -      2.472k in  10.085665s
          Experiment    320.663  i/s -      3.224k in  10.078677s

For the sake of proving the idea, I created the following (rather ugly) code:

module Dry
  class View
    class Config < Dry::Configurable::Config
      # PR 156
      include Dry::Equalizer(:values, immutable: true)

      # PR 157
      def [](name)
        name = name.to_sym
        return _values[name] if _values.key?(name)

        super
        _values[name] = _settings[name].to_value unless _values.key?(name)
        _values[name]
      end

      def method_missing(name, *args)
        setting_name = setting_name_from_method(name)
        setting = _settings[setting_name]

        return super unless setting

        if name.end_with?("=")
          instance_eval <<~RUBY, __FILE__, __LINE__ + 1
            def #{setting_name}=                # def path=
              self[:#{setting_name}] = args[0]  #   self[:path] = args[0]
            end                                 # end
          RUBY
          self[setting_name] = args[0]
        else
          instance_eval <<~RUBY, __FILE__, __LINE__ + 1
            def #{setting_name}       # def path
              self[:#{setting_name}]  #   self[:path]
            end                       # end
          RUBY
          self[setting_name]
        end
      end
    end
    ...
    # PR 156
    extend Dry::Configurable(config_class: Dry::View::Config)
1 Like