-
Notifications
You must be signed in to change notification settings - Fork 99
Description
Summary
When repeatedly creating and destroying ArtboardInstance + ViewModelInstance with data binding on a .riv file that has nested ViewModel properties, heap memory grows ~6 MB per cycle and is never reclaimed — even after all rcp smart pointers go out of scope.
This is reproducible in pure C++ with no platform wrapper involved. Originally reported via rive-ios (rive-app/rive-ios#427) where it manifests as ~150 MB growth over 10 mount/unmount cycles.
Reproduction
The .riv file used is blinko.riv from the rive-ios example assets. It has 10 ViewModels (7 of 8 default instance properties are nested ViewModel types), 3 nested artboards, and 38 data binds on the default state machine.
C++ reproducer (Catch2 test, runs in rive-runtime test harness)
#include <rive/file.hpp>
#include <rive/artboard.hpp>
#include <rive/animation/state_machine_instance.hpp>
#include <rive/viewmodel/viewmodel_instance.hpp>
#include "rive_file_reader.hpp"
#include <catch.hpp>
#include <cstdio>
#ifdef __APPLE__
#include <malloc/malloc.h>
#include <mach/mach.h>
#else
#include <malloc.h>
#endif
using namespace rive;
static size_t getHeapUsage()
{
#ifdef __APPLE__
malloc_statistics_t stats;
size_t total = 0;
unsigned int count = 0;
malloc_zone_t** zones = nullptr;
kern_return_t err = malloc_get_all_zones(
mach_task_self(), nullptr, (vm_address_t**)&zones, &count);
if (err == KERN_SUCCESS)
{
for (unsigned int i = 0; i < count; i++)
{
malloc_zone_statistics(zones[i], &stats);
total += stats.size_in_use;
}
}
return total;
#else
struct mallinfo mi = mallinfo();
return mi.uordblks;
#endif
}
TEST_CASE("heap growth with data binding", "[data binding]")
{
auto file = ReadRiveFile("assets/blinko.riv");
REQUIRE(file != nullptr);
constexpr int NUM_CYCLES = 10;
constexpr int FRAMES_PER_CYCLE = 30;
// Warm up — first cycle allocates caches, font data, etc.
{
auto artboard = file->artboardDefault()->instance();
auto vmi = file->createDefaultViewModelInstance(artboard.get());
if (vmi)
{
auto machine = artboard->defaultStateMachine();
if (machine)
{
machine->bindViewModelInstance(vmi);
for (int f = 0; f < FRAMES_PER_CYCLE; f++)
machine->advanceAndApply(0.016f);
}
}
}
size_t baselineHeap = getHeapUsage();
for (int i = 0; i < NUM_CYCLES; i++)
{
{
auto artboard = file->artboardDefault()->instance();
auto vmi = file->createDefaultViewModelInstance(artboard.get());
auto machine = artboard->defaultStateMachine();
machine->bindViewModelInstance(vmi);
machine->advanceAndApply(0.0f);
for (int f = 0; f < FRAMES_PER_CYCLE; f++)
machine->advanceAndApply(0.016f);
// artboard, machine, vmi all go out of scope here
}
size_t heap = getHeapUsage();
printf("Cycle %2d: growth_from_baseline=%.1f MB\n",
i, (heap - baselineHeap) / (1024.0 * 1024.0));
}
size_t finalHeap = getHeapUsage();
double totalGrowth = (double)(finalHeap - baselineHeap) / (1024.0 * 1024.0);
printf("Total growth: %.1f MB (%.2f MB/cycle)\n", totalGrowth, totalGrowth / NUM_CYCLES);
// After all objects are destroyed, heap should return near baseline
REQUIRE(totalGrowth < 5.0); // Currently fails: ~63 MB growth
}Run with:
cd tests/unit_tests
./test.sh -m "[data binding]"Observed output
Baseline heap after warmup: 15.2 MB
Cycle 0: growth_from_baseline=6.3 MB
Cycle 1: growth_from_baseline=12.5 MB
Cycle 2: growth_from_baseline=18.8 MB
Cycle 3: growth_from_baseline=25.1 MB
Cycle 4: growth_from_baseline=31.4 MB
Cycle 5: growth_from_baseline=37.7 MB
Cycle 6: growth_from_baseline=44.0 MB
Cycle 7: growth_from_baseline=50.3 MB
Cycle 8: growth_from_baseline=56.6 MB
Cycle 9: growth_from_baseline=62.9 MB
Total growth: 62.9 MB (6.29 MB/cycle)
Heap grows linearly at ~6.3 MB per cycle and never returns to baseline despite all rcp<ArtboardInstance>, rcp<ViewModelInstance>, and unique_ptr<StateMachineInstance> going out of scope.
Expected behavior
Heap should return near baseline after each cycle since all objects are destroyed.
Environment
- macOS 15.7.3, Apple M2 Max
- rive-runtime at commit
a8887748(main as of 2026-03-24) .rivfile:blinko.rivfrom rive-ios Example-iOS/Assets/
Notes
- This is not a traditional leak —
leaks --atExitreports 0 leaks. The memory is still reachable from somewhere. - The
.rivfile has nested ViewModel properties (7 of 8 default instance properties are ViewModel types) and 3 nested artboards, which likely amplifies the issue through data context propagation. - Does not reproduce with simpler
.rivfiles likedata_binding_test.rivthat lack nested ViewModels/artboards. - Related: Memory leak: ~150MB growth when repeatedly creating/destroying RiveViewModel with enableAutoBind rive-ios#427