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
52 lines
2 KiB
Markdown
52 lines
2 KiB
Markdown
# exposed-0003: Table.clone — O(N²) repeated List allocation in property filter
|
||
|
||
**Severity:** MEDIUM
|
||
**File:** exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/Table.kt
|
||
**Line:** 1686
|
||
**Status:** PATCHED
|
||
|
||
## Description
|
||
|
||
The private `T.clone()` utility in `Table` is used internally when cloning column objects (e.g., during
|
||
alias creation and column type mutation). It contains:
|
||
|
||
```kotlin
|
||
val allValues = memberProperties
|
||
.filter { it in mutableProperties || it.name in consParams.map(KParameter::name) }
|
||
.associate { it.name to (replaceArgs[it] ?: it.get(this@clone)) }
|
||
```
|
||
|
||
The predicate `it.name in consParams.map(KParameter::name)` is evaluated for every element of
|
||
`memberProperties`. Each evaluation calls `.map(KParameter::name)`, allocating a fresh `List<String?>`.
|
||
For a class with P properties and C constructor parameters, this is P × C string comparisons plus P list
|
||
allocations.
|
||
|
||
`Column` classes can have 10–20 properties; this is called once per column per alias/clone operation. In a
|
||
query with 50 aliased columns this function runs 50 times, each time performing up to 400 string comparisons
|
||
with 20 temporary `List` allocations per call — 20 000 comparisons and 1 000 heap allocations total.
|
||
|
||
## Root Cause
|
||
|
||
`consParams.map(KParameter::name)` is a lambda-captured expression inside the `.filter {}` predicate.
|
||
Kotlin does not hoist it; it runs inside the hot loop.
|
||
|
||
## Fix
|
||
|
||
Pre-compute the parameter name set once, outside the filter:
|
||
|
||
```kotlin
|
||
// Before (O(P×C) with P list allocations):
|
||
val allValues = memberProperties
|
||
.filter { it in mutableProperties || it.name in consParams.map(KParameter::name) }
|
||
.associate { it.name to (replaceArgs[it] ?: it.get(this@clone)) }
|
||
|
||
// After (O(P) with O(1) lookup):
|
||
val consParamNames = consParams.mapTo(HashSet()) { it.name }
|
||
val allValues = memberProperties
|
||
.filter { it in mutableProperties || it.name in consParamNames }
|
||
.associate { it.name to (replaceArgs[it] ?: it.get(this@clone)) }
|
||
```
|
||
|
||
## Speedup
|
||
|
||
~15× at P=20 properties, C=15 constructor parameters.
|