Rails Sum ActiveSupport Instrument Times

We wanted to show the sum of multiple ActiveSupport notifications during a long process, so here is a tiny snipped to do that, an advanced version is used in Samson

# sum activesupport notification duration for given metrics
def time_sum(metrics, &block)
  sum = Hash.new(0.0)
  add = ->(m, s, f, *) { sum[m] += 1000 * (f - s) }
  metrics.inject(block) do |inner, m|
    -> { ActiveSupport::Notifications.subscribed(add, m, &inner) }
  end.call
  sum
end

time_sum(["sql.active_record"]) { 10.times { User.first } }
# {"sql.active_record" => 10.3}

Validating ActiveRecord Backlinks exist

Whenever a new association is added usually we also need the opposite association to ensure things get cleaned up properly during deletion.
To never forget this and audit the current state, these two tests can help.

  def all_models
    models = Dir["app/models/**/*.rb"].grep_v(/\/concerns\//)
    models.size.must_be :>, 20
    models.each { |f| require f }
    ActiveRecord::Base.descendants
  end

  it "explicity defines what should happen to dependencies" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
        next if association.options.key?(:through)
        next if association.options.key?(:dependent)
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      "These associations need a :dependent defined (most likely :destroy or nil)\n#{bad.join("\n")}"
    )
  end

  it "links all dependencies both ways so dependencies get deleted reliably" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.name == :audits
        next if association.options.fetch(:inverse_of, false).nil? # disabled on purpose
        next if association.inverse_of
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      <<~TEXT
        These associations need an inverse association.
        For example project has stages and stage has project.
        If automatic connection does not work, use `:inverse_of` option on the association.
        If inverse association is missing AND the inverse should not destroyed when dependency is destroyed, use `inverse_of: nil`.
        #{bad.join("\n")}
      TEXT
    )
  end

Faster Page Through Reused HTML Options with Rails & JS

We render an edit page the repeats many long option lists, so we made rails only render it’s options once and made javascript reuse them, resulting in reduced render ~70% time, ~90% page size.

It resets options  when a users reload a partially filled out page, but otherwise supports keyboard and mouse navigation nicely.

 

# app/views/projects/_form.html.erb
<% list = [["Foo", "foo"], ["Bar", "bar"], ["Name", @project.name]] %>
  <%= form.reused_select :name, list %>
  <%= form.reused_select :name, list %>
  <%= form.reused_select :name, list %>

# config/initializers/reused_select.rb
ActionView::Helpers::FormBuilder.class_eval do
  def reused_select(column, values, options={})
    value = object.public_send(column)
    options_id = "options_id-#{values.object_id}"
    options[:html] = (options[:html] || {}).merge("data-selected": value, "data-options-id": options_id)
    placeholder_values = [values.detect { |_, v| v == value }] # make select look normal
    rendered = select column, placeholder_values, options

    # render the real values only once and reuse them via js
    if @template.instance_eval { (@reused_select ||= Set.new).add? options_id }
      rendered << @template.tag(:span, id: options_id, style: "display: none", data: {options: [["", ""]] + values})
    end
    rendered
  end
end

# app/assets/javascripts/application.js
function reuseSelect(e){
  var select = e.target;
  var $select = $(select);
  var options_id = $select.data("options-id");
  var selected = $select.data("selected"); // values come from json, so be careful to match the type
  select.innerHTML = ''; // clear out fake options
  $($("#" + options_id).data("options")).each(function(_, e){
    var name = e[0];
    var value = e[1];
    var option = document.createElement("option");
    option.innerText = name;
    option.value = value;
    if(value === selected) { option.selected = "selected"; }
    select.appendChild(option);
  });
}

$("select[data-options-id]").one("mousedown", reuseSelect).one("focus", reuseSelect);

 

Bundler + Rails: realpath expand_path and symlinked vendor/bundle cause ‘already loaded’ errors

Symlinked vendor/bundle results in double load errors since:

  • rails adds the realpath of each engines lib (and various other folders) to the $LOAD_PATH
  • bundler adds the symlinked version to the $LOAD_PATH
  • require_relative uses the realpath

which looks like: `already initialized constant Arturo::Middleware::MISSING_FEATURE_ERROR`

Reproduce:

  • bundle install –path vendor/bundle
  • mv vendor/bundle tmp
  • cd vendor && ln -s ../tmp/bundle bundle
  • enable eager_load + preload_frameworks in config/environment/development.rb
  • rails runner 1

Ruby issue

Patch:

# Gemfile
require_relative 'lib/bundler_realpath'
 
# lib/bundler_realpath.rb
# https://grosser.it/2017/08/19/bundler-rails-realpath-expand_path-and-symlinked-vendorbundle-cause-already-loaded-errors
Bundler::Runtime.prepend(Module.new do
  def setup(*)
    super
  ensure
    linked_bundle = File.expand_path(Bundler.bundle_path)
    real_bundle = File.realpath(Bundler.bundle_path)
    if linked_bundle != real_bundle
      $LOAD_PATH.each_with_index do |path, i|
        $LOAD_PATH[i] = path.sub(linked_bundle, real_bundle)
      end
    end
  end
end)