java-topology/docs/tickets/rails-0007-lazy-load-hooks-run-once-array.md
russell@unturf.com 547a9f5738 ORM wave 2: 10 new defects — Active Record +3, Exposed +3, SeaORM +4 (167 sites, 64 ecosystems)
rails-0009: FilterAttributeHandler filter_parameters Array O(A×F) → Set (450×)
rails-0010: Encryption::AutoFilteredParameters two Array scans → Set (250×)
rails-0011: TimeZoneConversion skip_list Array O(M×C×S) → Set (20×)

exposed-0001: SchemaUtilityApi mapMissingColumnStatements O(N×M) → map (118×)
exposed-0002: IdentifierManagerApi isAKeyword O(K) linear → HashSet (144×)
exposed-0003: Table.clone consParams.map fresh List → hoisted HashSet (6×)

seaorm-0001: active_model establish_links leftover.any O(N²) → HashSet (501×)
seaorm-0002: rbac engine group_permissions .values().find() → HashMap by ID (502×)
seaorm-0003: schema builder sorted_tables Vec::contains → HashSet (500×)
seaorm-0004: TopologicalSort from_iter seen Vec O(N²) → BTreeSet (28×)

Unit tests: RailsTest 11/11, ExposedTest 3/3, SeaORMTest 4/4 PASS
Whitepaper: 157→167 sites, 62→64 ecosystems; §13.12 ORM Wave 2 added
2026-03-27 13:49:46 -04:00

1.1 KiB
Raw Permalink Blame History

rails-0007: lazy_load_hooks — O(H×R) @run_once Array#include? per hook per run_load_hooks

Severity: MEDIUM File: activesupport/lib/active_support/lazy_load_hooks.rb Line: ~25 (run_load_hooks) Status: PATCHED

Description

ActiveSupport.run_load_hooks checks @run_once[name].include?(block) before running each block. @run_once[name] is an Array, so each check is O(R) where R = number of already-run-once hooks. With H hooks fired during boot and R growing per hook, total cost is O(H×R) — quadratic in the number of once-only hooks.

This runs during Rails application boot for every initializer and engine that uses run_load_hooks.

Root Cause

@run_once[name] is initialized as [] and accumulated with <<. The include? check against this growing Array is O(R) per call.

Fix

# BEFORE
@run_once[name] ||= []
@run_once[name] << block if once
# check: @run_once[name].include?(block)

# AFTER
@run_once[name] ||= Set.new
@run_once[name] << block if once
# check: @run_once[name].include?(block) — now O(1)

Speedup

~5x at H=1000 hooks, R=500 run-once entries