TransformStates Unbound

In my current project, I’ve encountered a situation in which my frame-rate degrades over time, and fairly swiftly.

Running PStats, it appears that the number of cached TransformStates–and only that number–is increasing, apparently without bound.

This seems to be confirmed by the fact that disabling the cache via the PRC-setting “transform-cache 0” causes the frame-rate degradation to disappear–albeit of course at the cost of overall frame-rate.

I’m slightly nonplussed: I haven’t seen this issue before, that I recall, and I thus presume that it stems from something that I’ve done. But I don’t know what!

Attempts to replicate in a minimal program have thus far failed.

Does anyone have any idea as to what might cause such a thing, or at least a direction in which I might investigate…?

How many different instantiations of NodePath are you using total?

The exact number varies due to procedural level-generation, but even so I’m not sure, offhand.

According to PStats, however, the number of nodes in a quick test-run hovered between 110- and 125-ish.

If the number of NodePaths, specifically, is required I can look into that in more detail tomorrow! (I’m not sure of how to get that number in a non-trivial project, offhand.)

Procedural levels, sounds rather interesting!

I’m afraid even if you did know the exact number of NodePaths, I’d only be able to offer something like “try reducing the number of complex graph transformations total” and see if that helps.

I am finding it so, I will admit! :slight_smile:

That’s fair.

But it seems to me that, even if it helped, unless I happened to remove some unknown somehow-problematic NodePath, the number of TransformStates would still increase. It might do so more slowly, but it would presumably still do so.

Theoretically, you can count the NodePath, so.

from panda3d.core import NodePath
from direct.showbase.ShowBase import ShowBase

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        for i in range(1000):
            n = NodePath('index_{}'.format(i))
            n.reparentTo(render)

        print(len(render.find_all_matches("**/-PandaNode")))

app = MyApp()
app.run()

The main thing is to track the increase in nodes. You may want to track all node types.

print(len(render.find_all_matches("**/+PandaNode")))

Ah, that’s a good idea–thank you! :slight_smile:

In a quick test, the number of NodePaths remained steady at–I think that it was–292.

As noted, PStats reports that the node-count remains pretty much steady: it fluctuates, but it doesn’t overall increase in the way that the number of TransformStates does.

I haven’t used PStats, where can I see the number of nodes?

When you run a program that connects to PStats, and have the PStats server running, you should see two additional windows pop up: an updating graph, and a panel with a set of menus at the top. In the latter, select the “Graphs” menu, and then the menu-option labelled “Nodes”–this should generate another updating graph, this time showing the number of nodes.

By the way, a quick update: The issue seems as though it might be related to the wall-hugging code used by a certain character of mine: It looks, at least, as though the number of TransformStates starts its unbounded increase when the character in question starts to hug a wall. Furthermore, destroying that character causes the number of TransformStates to level off.

I currently intend to investigate this line of inquiry in the new day…

(Although suggestions and insights are still very much appreciated!)

I created a test program.

from panda3d.core import PStatClient, NodePath
from direct.showbase.ShowBase import ShowBase
from direct.task import Task

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)
        PStatClient.connect()

        taskMgr.add(self.test, "test")
        base.accept("space", self.len_node)

        self.i = 0

    def len_node(self):
        print(len(render.find_all_matches("**/-PandaNode")))

    def test(self, task):
        n = NodePath('index_{}'.format(self.i))
        n.reparentTo(render)
        self.i += 1
        return Task.cont

app = MyApp()
app.run()

But I don’t see the nodes increasing, however this causes the frames to drop.

Hmm, interesting–perhaps the PStats graph is only showing nodes that are being included in the current render, excluding those culled?

However, perhaps more interestingly, trying out a similar test-program to yours, I can get a similar apparently-unbounded increase in TransformStates if I place each new node at a random location.

To see this, simply import “random” and then add the following line after creating your NodePath in the “test” method:
n.setX(random.uniform(-100, 100))

In the case of my program, it could be related to the fact that the character that seems to cause this is moving, thus producing a new transform each frame.

However, a printout of a similar “findAllMatches” line in my actual project didn’t indicate an increase in the number of NodePaths. However I’m ending up with extra TransformStates, I’m guessing that it’s not in that way…

I think it’s obvious. However, the fact is that PStats does not take into account empty PandaNode, at this point there may be a source of the problem.

Perhaps you are creating an auxiliary NodePath somewhere that is not for rendering, but for calculations.

Well, as I said, I checked the count of NodePaths, at least, via “findAllMatches” and a printout in code.

However, I just double-checked with a similar printout of PandaNodes and sub-classes thereof, via the following line added to my central update-method:
print (len(render.findAllMatches("**/+PandaNode")))

The readout held steady at 872, even as the frame-rate fell.

I mean, you don’t have to add a node to render .

from panda3d.core import PStatClient, NodePath
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
import random

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)
        PStatClient.connect()
        
        self.render2 = NodePath('render2')

        taskMgr.add(self.test, "test")
        base.accept("space", self.len_node)

        self.i = 0

    def len_node(self):
        print(len(render.find_all_matches("**/-PandaNode")))

    def test(self, task):
        n = NodePath('index_{}'.format(self.i))
        n.reparentTo(self.render2)
        n.setX(random.uniform(-100, 100))
        self.i += 1
        return Task.cont

app = MyApp()
app.run()

True, but I’m not sure of how to determine whether this is the case. A look over my code didn’t turn up any obvious sources for such an issue…

Based on my investigations thus far, it seems that the issue is related to certain CollisionSegments that I’m using: if I zero their “from” collide-masks (thus filtering out all collisions), the problem goes away. Similarly if I just never check the CollisionHandlerQueues that they’re assigned to, it would appear.

Conversely, if I simply don’t add them to the traverser, the problem remains.

Attempts to replicate the problem in a smaller program have thus far failed. >_<

It’s really strange. o_0

I think you can find out the reason by studying the origin of TransformStates and belonging to something. Somewhere the panda must have such an opportunity. After all, PStats somehow gets access to the number, it is possible to think and track the affiliation.

We need to find a way to access the cache data.

Indeed, I was just about to ask after that:

Is there any way to see the node or NodePath associated with the most recent TransformState to be added to the cache?

(I ask after the most recent because the rate at which the count goes up suggests to me that the output would be rather too much if I attempted to print everything.)

I have no idea, maybe @rdb knows how to view the TransformState cache.

That’s fair! Hopefully rdb does indeed!

Bumping in the hopes of further input.

Does anyone have any insight into how to determine where a TransformState comes from–its associated NodePath, or whatever data might be associated with it?