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
1.9 KiB
rails-0009: FilterAttributeHandler — O(A×F) filter_parameters Array#include? per sensitive attribute
Severity: MEDIUM File: activerecord/lib/active_record/filter_attribute_handler.rb Line: 69 Status: PATCHED
Description
FilterAttributeHandler#apply_filter is called once per sensitive attribute per model
class during app initialization. Inside the loop it calls:
app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
app.config.filter_parameters is an Array (initialized as [] in railties). Each
.include?(filter) call is O(F) where F is the current size of filter_parameters.
With A total sensitive attributes across all models and F growing as attributes are added,
total cost is O(A×F) — quadratic in the number of sensitive attributes declared across
the application.
This is the same structural pattern as rails-0010 (encryption variant) and runs at every
app boot for all models using has_secure_password, attr_encrypted, or manual
filter_attributes declarations.
Root Cause
filter_parameters is a plain Array. The deduplication guard include? is O(F) per
insertion rather than using a Set or Hash for O(1) membership.
Fix
Pre-build a Set mirror of filter_parameters for O(1) membership checks, or change
the underlying storage to a Set:
# BEFORE (filter_attribute_handler.rb:69)
app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
# AFTER
existing = app.config.filter_parameters.to_set
list.each do |attribute|
next if klass.abstract_class? || klass == Base
klass_name = klass.name ? klass.model_name.element : nil
filter = [klass_name, attribute.to_s].compact.join(".")
unless existing.include?(filter)
app.config.filter_parameters << filter
existing << filter
end
end
Speedup
~10x at A=500 total attributes, F=200 existing filter_parameters