Memory management issues, Rhino 8, C++ SDK

I am developing a plugin for Rhino 8 using C++ SDK and I have strange and undesirable behavior with regard to memory management (garbage collection). Maybe someone can give me an idea of what’s going on? Here’s what I got:

I need to create a single very large mesh from a huge number of small “sub-meshes” created dynamically with new ON_Mesh(...). I manage these “sub-meshes” with ON_MeshRef and pass shared_ptr to append the final mesh (in a fast way):

std::vector<std::shared_ptr<const ON_Mesh>> meshpiece;
...
// create a mesh with few faces and manage it by meshRef (the mesh is created elsewhere)
...
for (i = start; i < end; i++) {
    std::shared_ptr<ON_Mesh> sharedptr = meshRef.SharedMesh();
    meshRef.Clear();
    meshpiece.push_back(std::move(sharedptr));
}
...
ON_Mesh finalMesh;
finalMesh.Append(meshpiece);
...
// clear/destroy 'meshpiece'
...
// eventually destroy finalMesh

I have separate mesh pieces created first (in multiple threads) which go into the final mesh (though the issue is not thread dependent, it happens with 1 thread as well).

Here’s what I observe with memory consumption for a smaller example when I repeatedly run my plugin (and I don’t do anything else in Rhino):

Thin spikes correspond to each run of my code which allocates and deallocates ~3.7Gb of memory (for this case). The first 3 spikes look good, but starting from the 4th spike not all memory is released. However, after every 3 cycles the memory is released going down to ~1.2Gb (e.g., as pointed by the red arrow). This 3-spikes cycle seems to be consistent, after which (I guess) garbage collector kicks-in. However, memory footprint grows within 3-spike frame (going from ~2.4Gb to 3.3Gb). And at the end of all this, memory is not released at all (garbage collector kicks-in much-much later).

I don’t understand that even if I release all my memory, why Rhino as a process starts consuming more memory for these 3-spike intervals? It appears that I need to run my code 3 (or more) times for Rhino to release memory. Note that I don’t do any additional operations, only run my code periodically. With this, if I don’t get to the point when garbage collector kicks-in, Rhino stays with high memory footprint (like at the end).

Is there a way to force garbage collector to run after my code every time? Note that I am using native C++ SDK.

This is a small case, when I run more realistic large case, memory consumption easily goes into 50-100Gb and it makes Rhino unusable.

Thanks for any advice!

Hi @pyati,

Using ON_MeshRef is unnecessary in this case.

Try doing something like this:

/*
Memory for the mesh is allocated and
becomes the responsibility of the caller.
*/
static ON_Mesh* GetMeshPiece(int i)
{
  ON_Mesh* pMesh = new ON_Mesh();

  // TODO: create mesh piece here

  if (!pMesh->IsValid())
  {
    // clean up...
    delete pMesh;
    pMesh = nullptr;
  }
  
  return pMesh;
}

CRhinoCommand::result CCommandTest::RunCommand(const CRhinoCommandContext& context)
{
  const int num_pieces = SOME_LARGE_VALUE;

  ON_SimpleArray<ON_Mesh*> mesh_pieces(num_pieces);
  for (int i = 0; i < num_pieces; i++)
  {
    ON_Mesh* pPiece = GetMeshPiece(i);
    if (nullptr != pPiece)
      mesh_pieces.Append(pPiece);
  }

  if (0 == mesh_pieces.Count())
    return CRhinoCommand::failure;

  CRhinoCommand::result rc = CRhinoCommand::success;

  ON_Mesh* pMesh = new ON_Mesh();
  pMesh->Append(mesh_pieces.Count(), mesh_pieces.Array());
  if (pMesh->IsValid())
  {
    CRhinoMeshObject* pMeshObj = new CRhinoMeshObject();
    pMeshObj->SetMesh(pMesh); // CRhinoMeshObject now owns pFinalMesh
    pMesh = nullptr;

    if (context.m_doc.AddObject(pMeshObj))
    {
      context.m_doc.Redraw();
    }
    else
    {
      // clean up...
      delete pMeshObj;
      pMeshObj = nullptr;
      rc = CRhinoCommand::failure;
    }
  }
  else
  {
    // clean up...
    delete pMesh;
    pMesh = nullptr;
  }

  // clean up...
  for (int i = 0; i < mesh_pieces.Count(); i++)
  {
    if (nullptr != mesh_pieces[i])
    {
      delete mesh_pieces[i];
      mesh_pieces[i] = nullptr;
    }
  }

  return rc;
}

– Dale

Dale,

Thank you for this advise. I agree, there is no need to use tracking pointers.So I followed your suggestion and changed the code to use regular pointers (and take care of them / free all memory at the end). I didn’t quite compare but it looks like the code got a bit faster (as I don’t need to track pointers) but the memory issues remain:

There is still a pattern of 3 (or more) peaks after which memory is freed. To generate this image, I repeatedly called my plugin with same data - each peak correspond to plugin execution (I did it manually, so there are different time intervals between peaks). The problem for me is that if a user calls plugin several times and it does not hit the point at which memory is fully released, Rhino continues to consume large amount of memory and it slows down everything (can render Rhino unusable). I am not sure what to do to make garbage collector (or any other mechanism) to force Rhino to release memory after running my code.

If you need more details or if there is any other trick I may be able to adopt, please let me know.

Thank you,
Andrey

Hi @pyati,

Unlike C#, which has a garbage collector, C++ uses manual memory management. That is, you are responsible for the memory that you allocate. There is lot of information about this on the Internet.

In the case of your code snipped, the CRhinoMeshObject you new up and pass to CRhinoObject::AddObject is now owned by Rhino, and it will take care of cleaning up the allocated memory as needed.

Rhino maintains an Undo stack for when users want to unto a command or operation. For example, if you copy an object, the original is pushed onto the undo stack and a modified version is added to the document. With large objects, doesn’t take long for the Undo stack to grow.

The user can clear the Undo stack, if needed, by using the ClearUndo command.

In order to provide further analysis, we’ll need a way of repeating what you’re seeing.

Thanks,

– Dale