vtkOpenGLGlyph3DMapper serious memory leak
This bug was discovered when using ParaView but the underlying issue is in VTK class. In our application, it results in VRAM leak of 30mb/s but the severity is dependent on the glyphed object complexity.
When visualizing data in ParaView and setting representation to 3D Glyph memory (both CPU and GPU) is leaked. It all boils down to this part of code in vtkOpenGLGlyph3DMapper.cxx
:
GVIter found = this->GlyphValues->Entries.find(dataset);
if (found == this->GlyphValues->Entries.end())
{
subarray = new vtkOpenGLGlyph3DMapper::vtkOpenGLGlyph3DMapperSubArray();
this->GlyphValues->Entries.insert(std::make_pair(dataset, subarray));
rebuild = true;
}
Here, a new subarray
is added to GlyphValues
map every time there is a new dataset
object. I don't know the purpose of this map, but it is only cleared when mapper is destroyed.
The problem is, this works with plain VTK as the dataset doesn't really change between invocation of render, it may be only filled with different data. However, ParaView is using delivery mechanism that breaks the pipeline into two parts - one part running on server, and the second one on client. Server part of pipeline ends just before the data would be passed to mapper and instead ParaView stores the output data object using SetPiece
method. Client then retrieves this data object through vtkTrivialProducer
by calling GetPieceProducer
. What happens behind the scenes is that data object is delivered from Server to Client and set as the output of the mentioned trivial producer as can be seen here. Note that this behaviour is also true for the case when Server and Client are in fact the same ParaView instance.
This results in the fact that this dataset object which is being rendered is no longer static but can be new object that was created upon delivery. So now we have GlyphValues
map storing separate subarray and subsequently whole vtkOpenGLGlyph3DHelper
for each dataset and the entries doesn't get ever cleared until the whole vtkOpenGLGlyph3DMapper
is destroyed. Of course vtkOpenGLGlyph3DHelper
is storing some OpenGL resources, most notably Index Buffer Objects, so overall we hog not only CPU but also GPU memory.
The fix I currently implemented is based on the fact that these data sets that we store in the map are really not valid anymore (they were overwritten by different data set when trivial producer output was set, and thus are no longer needed for pipeline execution). So I just iterate the map and look for data set objects that have ReferenceCount <= 1
and delete those. Why? Because we know for sure that vtkOpenGLGlyph3DMapperEntry
- child of the subarray (second item in map tuple), will be holding reference to the dataset (it is deleting it here and if the dataset is still part of pipeline, there will be at least another reference from the vtkTrivialProducer
where it's acting as output. Therefore, the dataset is valid if it has at least 2 references. If there is only one reference, it's the vtkOpenGLGlyph3DMapperEntry
who is holding it so it's safe to delete the entry. This is the replacement for the code segment presented above which fixes the issue:
GVIter found = this->GlyphValues->Entries.find(dataset);
if (found == this->GlyphValues->Entries.end())
{
subarray = new vtkOpenGLGlyph3DMapper::vtkOpenGLGlyph3DMapperSubArray();
// Iterate GlyphValues (map of vtkDataset and vtkOpenGLGlyph3DMapperSubArray)
for (auto gvIt = this->GlyphValues->Entries.begin(); gvIt != this->GlyphValues->Entries.end();)
{
// Check the reference count of vtkDataSet. If it is equal to 1, it is safe to remove this entry because it's the
// second value in tuple (vtkOpenGLGlyph3DMapperEntry) who holds this reference, otherwise the dataset
// would be already deleted.
if (((vtkDataSet *)(gvIt->first))->GetReferenceCount() <= 1)
{
// First, release graphics resources before removing entries.
// Iterate vector of vtkOpenGLGlyph3DMapperEntry
for (auto entryIt = gvIt->second->Entries.begin(); entryIt != gvIt->second->Entries.end(); ++entryIt)
{
// Iterate map of all vtkOpenGLGlyph3DHelper (different DataObjects have each their own helper).
for (auto helperIt = (*entryIt)->Mappers.begin(); helperIt != (*entryIt)->Mappers.end(); ++helperIt)
{
helperIt->second->ReleaseGraphicsResources(ren->GetRenderWindow());
}
}
// Second, delete the SubArray which will cascade and delete all other resources in maps and vectors.
// This should also unregister the dataset and release it (see ~vtkOpenGLGlyph3DMapperEntry).
delete gvIt->second;
// Finally, it's safe to remove this entry from GlyphValues.
gvIt = this->GlyphValues->Entries.erase(gvIt);
continue;
}
++gvIt;
}
this->GlyphValues->Entries.insert(std::make_pair(dataset, subarray));
rebuild = true;
}