Skip to content

resource: allow resource_class to be passed as a String#8848

Merged
tagliala merged 2 commits intoactiveadmin:masterfrom
HoneyryderChuck:resource-name-as-string
Dec 7, 2025
Merged

resource: allow resource_class to be passed as a String#8848
tagliala merged 2 commits intoactiveadmin:masterfrom
HoneyryderChuck:resource-name-as-string

Conversation

@HoneyryderChuck
Copy link
Contributor

the current pattern of using the class directly loads all models bound to activeadmin before using them, needlessly. besides that, the only purpose of it is to store the class name

this patch allows one to pass the class name directly instead, kind of like how activerecord avoids eager-loading association classes.

@tagliala
Copy link
Contributor

Hello, thanks for this PR, it looks like a good change

Can you please:

  • Rebase on master, there is a merge conflict
  • Add a spec
  • Provide more information like:
    • is this trying to solve an existing bug? If it is, any chance to better describe the bug?
    • is this a performance-only change

@HoneyryderChuck HoneyryderChuck force-pushed the resource-name-as-string branch 2 times, most recently from 7776685 to 7234b87 Compare November 14, 2025 09:56
@HoneyryderChuck
Copy link
Contributor Author

@tagliala thx for the review, addressed all points.

Provide more information like

This is trying to improve dev-local performance of using activeadmin with autoloading enabled; while resources get eager-loaded as part of the route injection, it doesn't really need to load the resources until the page is loaded.

@tagliala
Copy link
Contributor

tagliala commented Nov 14, 2025

This is trying to improve dev-local performance of using activeadmin with autoloading enabled; while resources get eager-loaded as part of the route injection, it doesn't really need to load the resources until the page is loaded.

Thanks. I did try to measure the performance gain, but I'm afraid I did not succeed (Rails 8.1 / Ruby 3.4 / Apple Silicon M1 Pro). I've tried to enable eager_load, and switch to classic mode instead of zeitwerk, but unfortunately I cannot see improvements or changes. Maybe it depends on the number of models? Any chance to provide a demo application on github to see improvements? Do you want me to create

PS: is this also related to #8782 ?

Also involving @javierjulio and @mgrunberg . This is like the rails change in class_name. I think this is somehow related to https://www.rubydoc.info/gems/rubocop-rails/RuboCop/Cop/Rails/ReflectionClassName rubocop/rubocop#6704

@codecov
Copy link

codecov bot commented Nov 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.08%. Comparing base (0996891) to head (c7651ba).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #8848   +/-   ##
=======================================
  Coverage   99.08%   99.08%           
=======================================
  Files         139      139           
  Lines        4046     4046           
=======================================
  Hits         4009     4009           
  Misses         37       37           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@HoneyryderChuck
Copy link
Contributor Author

It's highly dependent on the size of the application and what you're doing at boot time. I've enabled this in our app as a monkeypatch, and the overall gains from reenabling autoloading (not only including activeadmin changes) amounted to ~10s saved when opening a console / running a spec touching 1-3 models.

And I can confirm it's related to what you linked.

@tagliala
Copy link
Contributor

tagliala commented Nov 14, 2025

@HoneyryderChuck thanks

I really would have the ability to test this by myself. I've tried against a different application with more models without success, I do not see benefits. Any chance to provide information about the Rails version used, the ruby version, some kind of time rspec ... commands to try?

Or maybe to edit https://github.com/activeadmin/demo.activeadmin.info to demonstrate the improvement

And I can confirm it's related to what you linked.

Is this change going to make #8782 unnecessary? Or, if both approaches are used, is this going to boost the performance even more?

Copy link
Contributor

@tagliala tagliala left a comment

Choose a reason for hiding this comment

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

Hi @HoneyryderChuck,

I appreciate the change and am inclined to merge it as a best practice. That said, I'd like to quantify its benefits and uncover any potential issues related to early-loading classes.

Could you share a reproducible repository, ideally based on activeadmin/demo.activeadmin.info, along with a clear procedure for measuring load times?

This would help me validate the impact for the team and proceed with confidence

Comment on lines 74 to 84

it "allows registering with string" do
namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin)
resource = namespace.register("Post")
resource.clear_action_items!
resource.add_action_item :empty do
# Empty ...
end
expect(resource.action_items.size).to eq 1
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

If you can move this spec to the namespace and check for the string registration, something like

diff --git a/spec/unit/namespace/register_resource_spec.rb b/spec/unit/namespace/register_resource_spec.rb
index c97cc181..0176bd1a 100644
--- a/spec/unit/namespace/register_resource_spec.rb
+++ b/spec/unit/namespace/register_resource_spec.rb
@@ -31,6 +31,16 @@ RSpec.describe ActiveAdmin::Namespace, "registering a resource" do
     end
   end
 
+  context "when resource class is a string" do
+    before do
+      namespace.register 'Category'
+    end
+
+    it "should store the namespaced registered configuration" do
+      expect(namespace.resources.keys).to include("Category")
+    end
+  end
+
   context "with a block configuration" do
     it "should be evaluated in the dsl" do
       expect do |block|
diff --git a/spec/unit/resource/action_items_spec.rb b/spec/unit/resource/action_items_spec.rb
index 2fea4fbc..0dd26177 100644
--- a/spec/unit/resource/action_items_spec.rb
+++ b/spec/unit/resource/action_items_spec.rb
@@ -71,14 +71,4 @@ RSpec.describe ActiveAdmin::Resource::ActionItems do
       expect(resource.action_items.size).to eq 2
     end
   end
-
-  it "allows registering with string" do
-    namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin)
-    resource = namespace.register("Post")
-    resource.clear_action_items!
-    resource.add_action_item :empty do
-      # Empty ...
-    end
-    expect(resource.action_items.size).to eq 1
-  end
 end

@HoneyryderChuck
Copy link
Contributor Author

@tagliala sorry haven't had the time to go back to this, as providing this minimal change is less overhead than providing a full blown app which measures the impact. I honestly believe that this is only noticeable in really big applications with convoluted boot time model logic, so the mileage always varies, and a reproduction will be of marginal value.

the current pattern of using the class directly loads all models bound to activeadmin before using them, needlessly. besides that, the only purpose of it is to store the class name

this patch allows one to pass the class name directly instead, kind of like how activerecord avoids eager-loading association classes.
@javierjulio
Copy link
Member

@tagliala from what you shared in Slack, the change makes sense and I think should be safe enough. Let's move the test case since as you shown we already have a spec for namespace registration. Thank you.

@tagliala
Copy link
Contributor

tagliala commented Dec 7, 2025

@javierjulio I've AI-generated a script to create thousands of models, and I'm still not able to replicate or see differences in loading time.

I've tested with 1k models

I've tested both config.eager_load values in development without seeing any difference when used with or without strings

I'm using demo.activeadmin.com as a base application on Apple Silicon M1 Pro.

The "solution" seems trivial, but it would be good to be able to understand the correct issue here.

Maybe it is a different version of Ruby or Rails? Classic and not zeitwerk mode?

Script:

#!/usr/bin/env ruby

require "fileutils"
require "active_support/inflector"

# CONFIGURABLE:
MODEL_PREFIX = "ExampleModel" # hardcoded for demo, could make configurable
MODEL_DIR = "app/models"
ADMIN_DIR = "app/admin"
MIGRATION_DIR = "db/migrate"

def usage
  puts "Usage: #{$0} <number_of_models>"
  exit 1
end

unless ARGV.size == 1 && ARGV[0] =~ /^\d+$/
  usage
end

num_models = ARGV[0].to_i
raise "Number must be > 0" if num_models < 1

models = (1..num_models).map { |i| "#{MODEL_PREFIX}#{i}" }

# 1. Remove previous models and active admin files for these models
models.each do |model|
  model_file = File.join(MODEL_DIR, "#{model.underscore}.rb")
  File.delete(model_file) if File.exist?(model_file)

  admin_file = File.join(ADMIN_DIR, "#{model.underscore}.rb")
  File.delete(admin_file) if File.exist?(admin_file)
end

# 2. Generate and run a migration to drop all the tables for these models
drop_migration_name = "drop_example_model_tables_#{Time.now.strftime('%Y%m%d%H%M%S')}"
drop_migration_file = File.join(MIGRATION_DIR, "#{Time.now.strftime('%Y%m%d%H%M%S')}_#{drop_migration_name}.rb")

drop_migration_content = <<~RUBY
  class #{drop_migration_name.camelize} < ActiveRecord::Migration[6.1]
    def change
      #{models.map { |model|
        "drop_table :#{model.underscore.pluralize} if table_exists?(:#{model.underscore.pluralize})"
      }.join("\n      ")}
    end
  end
RUBY

File.write(drop_migration_file, drop_migration_content)
puts "Generated migration to drop previous tables: #{drop_migration_file}"
system("bin/rails db:migrate") # runs the drop migration

# Remove the drop migration file after use (optional for idempotency)
File.delete(drop_migration_file) if File.exist?(drop_migration_file)

# 3. Generate model files
models.each do |model|
  model_file = File.join(MODEL_DIR, "#{model.underscore}.rb")
  File.write(model_file, <<~RUBY)
    class #{model} < ApplicationRecord
    end
  RUBY
  puts "Created model #{model_file}"
end

# 4. Generate migration to create tables
create_migration_name = "create_example_model_tables_#{Time.now.strftime('%Y%m%d%H%M%S')}"
create_migration_file = File.join(MIGRATION_DIR, "#{Time.now.strftime('%Y%m%d%H%M%S')}_#{create_migration_name}.rb")

create_migration_content = <<~RUBY
  class #{create_migration_name.camelize} < ActiveRecord::Migration[6.1]
    def change
      #{models.map { |model|
        <<~TABLE
          create_table :#{model.underscore.pluralize} do |t|
            t.string :name
            t.timestamps
          end
        TABLE
      }.join("\n      ")}
    end
  end
RUBY

File.write(create_migration_file, create_migration_content)
puts "Generated migration to create tables: #{create_migration_file}"
system("bin/rails db:migrate") # runs the create migration

# 5. Create ActiveAdmin config files
models.each do |model|
  admin_file = File.join(ADMIN_DIR, "#{model.underscore}.rb")
  File.write(admin_file, <<~RUBY)
    # frozen_string_literal: true

    ActiveAdmin.register #{model} do
    end
  RUBY
  puts "Created ActiveAdmin config: #{admin_file}"
end

puts "Done. Idempotent models and tables created for: #{models.join(', ')}"

1k models, Eager load: false, non-string

$ time rails runner 'puts "ok"'
ok

real	0m1.025s
user	0m0.733s
sys	0m0.286s

1k models, Eager load: false, strings

$ time rails runner 'puts "ok"'
ok

real	0m1.052s
user	0m0.733s
sys	0m0.312s

@tagliala tagliala force-pushed the resource-name-as-string branch from 7234b87 to c7651ba Compare December 7, 2025 22:42
@tagliala tagliala merged commit be9ec62 into activeadmin:master Dec 7, 2025
26 checks passed
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