rive-app / rive-app/rive-runtime

Memory retention: ~6 MB/cycle heap growth when creating/destroying artboards with data binding

Open
#88 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
1.2k
Forks
121
PR merge metrics
No merged PRs in 30d

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 (https://github.com/rive-app/rive-ios/issues/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)
  • .riv file: blinko.riv from rive-ios Example-iOS/Assets/

Notes

  • This is not a traditional leak — leaks --atExit reports 0 leaks. The memory is still reachable from somewhere.
  • The .riv file 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 .riv files like data_binding_test.riv that lack nested ViewModels/artboards.
  • Related: https://github.com/rive-app/rive-ios/issues/427

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the Catch2 reproducer in tests/unit_tests with ./test.sh -m "[data binding]", using assets/blinko.riv and the included rive_file_reader.hpp setup. Trace what remains reachable after each ArtboardInstance, ViewModelInstance, and StateMachineInstance scope ends; done means the test passes with total heap growth under 5 MB.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.