Skip to content

Add ManagedObjectCache#16

Merged
colemancda merged 4 commits into
masterfrom
feature/ManagedObjectCache
Jul 9, 2026
Merged

Add ManagedObjectCache#16
colemancda merged 4 commits into
masterfrom
feature/ManagedObjectCache

Conversation

@colemancda

@colemancda colemancda commented Jul 9, 2026

Copy link
Copy Markdown
Member

Fix quadratic performance in NSManagedObjectContext.insert(_:model:) batch inserts

Problem

Batch inserts degrade quadratically with batch size. Inserting a real-world dataset
(11,421 objects with relationships) took ~46 seconds, while the same data inserted
as 360 small groups took ~2.5s — the opposite of what batching should do.

Two causes, both in the find-or-create path:

  1. insert(_ values:model:) issues one fetch request per object to find-or-create it.
    Core Data fetches evaluate their predicate against every pending (unsaved) object
    in the context, so as a batch accumulates inserted-but-unsaved objects, each
    subsequent find gets slower — O(n²) overall.
  2. setRelationship resolves every relationship target with another per-object
    context.find, which pays the same pending-changes penalty.

Fix

insert(_ values:model:) now:

  • Prefetches every object referenced by the batch — value IDs plus all relationship
    target IDs — with a single IN fetch request per entity, into a
    ManagedObjectCache ([EntityName: [ObjectID: NSManagedObject]]), before anything
    is inserted (pending set is empty, so these fetches are clean SQL).
  • Creates without fetching: the prefetch covers every value ID, so a cache miss
    proves the object doesn't exist — no fallback fetch. (This mattered: with the
    fallback in place, the empty-database case still spent ~29s in fetches.)
  • Creates all batch objects before applying values, so relationships between
    values in the same batch resolve regardless of their order in the array.
  • Applies attributes/relationships through cache-aware setValues/setRelationship
    overloads, then saves once.

The single-value insert(_ value:model:) path is unchanged; the original
setValues/setRelationship signatures delegate through an empty cache.

Results

360-site catalog, 11,421 objects with to-many relationships (M-series Mac):

Scenario Before After
Single batch (1 save) 46.6s 0.26s (~175x)
360 groups of ~32 objects 2.5s 0.9s

Post-fix single-batch breakdown: prefetch 0.03s · create 0.02s · setValues 0.08s · save 0.12s.

Behavior notes

  • Duplicate (entity, id) pairs in one batch resolve to the same managed object with
    last-write-wins, matching the previous loop's behavior.
  • Intra-batch relationship resolution is now order-independent (previously a toOne
    referencing a value later in the array could resolve to nil).
  • An unknown entity name now throws CoreModelError.invalidEntity up front instead of
    failing inside a fetch request.

Testing

  • All existing tests pass.

@colemancda colemancda force-pushed the feature/ManagedObjectCache branch from 3fae799 to f91dbbb Compare July 9, 2026 16:07
@colemancda colemancda merged commit 6121979 into master Jul 9, 2026
26 checks passed
@colemancda colemancda deleted the feature/ManagedObjectCache branch July 9, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant