Skip to content

[0.15.0-dev.1] iOS: unbounded Metal object growth on the deferred replay thread → OOM (suspected missing @autoreleasepool) #655

Closed

Description

@lawinski

Submission checklist

  • I have confirmed the issue is present in the latest version of the rive Flutter package
  • I have searched the documentation and forums and could not find an answer
  • I have searched existing issues and this is not a duplicate

Description

Symptom

After upgrading to rive 0.15.0-dev.1 / rive_native 0.2.0-dev.1, memory climbs steadily
on iOS until the app is jetsammed (~1.5 GB). Pinning back to rive 0.14.10 /
rive_native 0.1.10, no other change: flat.

Device: iPhone 13 Pro Max, iOS 26.6. Instruments 26.6, Leaks template, 134 s trace.

Measurement

At trace end, one thread holds 490 MiB in 318,340 live objects — all Metal
render-pass/encoder objects, zero textures:

  • 4,865 MTLDebugRenderCommandEncoder (478 MB — 96 KiB each; Metal API Validation was
    on, so the byte figures are inflated, but the object counts should hold without it)
  • 14,596 MTLRenderPassDescriptorInternal + 121,626 of its attachment children
  • 4,865 AGXG14FamilyRenderContext
  • 2,432 MTLDebugCommandBuffer / AGXG14FamilyCommandBuffer

Three other facts:

  • Leaks instrument: zero leaks. The objects are still reachable — retention, not
    an orphaned allocation.
  • That thread has 181 live autorelease-pool pages. Every other thread has 1.
  • All 318,340 objects were allocated between t=3.2 s and t=61.5 s. At t=134 s none
    had been freed
    — 73 s after the thread stopped allocating. ~12 MiB/s while rendering.

Suspected cause

The accumulating classes are exactly the objects ContextMetal autoreleases per frame
— [m_mtlQueue commandBuffer], [MTLRenderPassDescriptor renderPassDescriptor],
renderCommandEncoderWithDescriptor: — plus their owned attachment descriptors. With
181 undrained pool pages on that one thread, this looks like the deferred replay thread
new in 0.2.0-dev.1 running its Metal work with no @autoreleasepool, so everything
sits in the thread's top-level pool.

The only @autoreleasepool calls in the shipped sources are in path_fiddle,
headless_rendering.mm, and a completion handler at rive_native_plugin.mm:280 —
none covering recording or replay. The replay loop is in deferred_binding.cpp, a
.cpp, so it can't hold one.

Possible fix (untested): wrap each replayed frame in @autoreleasepool;
ContextMetal::beginFrame/endFrame in ore_context_metal.mm look like the seam.


Analysis was AI-assisted. Happy to dig deeper or share the trace if that helps.

Reproduction steps / code

No response

Upload your reproduction files / stack trace

No response

Source .riv / .rev file

No response

Screenshots / video

No response

Rive Flutter package version

0.15.0-dev.1

Flutter version

Flutter 3.47.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 6655482ec0 (12 days ago) • 2026-08-19 10:07:23 -0700
Engine • hash 11d79658c444477b06513d32b52c8c4ccb7276b0 (revision 5d53178869) (12 days ago) •
2026-08-18 23:36:01.000Z
Tools • Dart 3.13.1 • DevTools 2.60.0

Device

iPhone 13 Pro Max

OS version

iOS 26.6

Additional context

No response

Activity

  1. lawinski commented on Oct 6, 2026

    @lawinski
    Author

    It looks like this issue was probably already fixed in dev-2. I’m currently on dev-3 and haven’t experienced any memory issues so far, so I think we can close this for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions