pythonalgorithmoptimizationmemory-managementoctree

Python3. Octree implementation engages a lot of memory. How to optimize?


I develop game server and want to keep real-time objects positions on the maps. For this purpose I'm using octree algorithm. But for now my implementation engages a lot of RAM, for test I tried to populate several maps and even without objects octree engages about 1 GB + about 1 GB per map for objects (I store all objects in the dict and separately store list of guids for each octree nodes according to their coordinates).

Below my implementation:

class OctreeNode(object):

    MAX_CHILD_NODES = 8

    def __init__(self, **kwargs):
        self.x0 = kwargs.pop('x0')
        self.x1 = kwargs.pop('x1')
        self.y0 = kwargs.pop('y0')
        self.y1 = kwargs.pop('y1')
        self.z0 = kwargs.pop('z0')
        self.z1 = kwargs.pop('z1')

        self.root_node: OctreeNode = None
        self.parent_node: OctreeNode = None
        self.child_nodes = None

       self.objects = None
       self.guids = None

    def get_root_node(self) -> 'OctreeNode':
        return self.root_node

    def set_root_node(self, node: 'OctreeNode') -> None:
        self.root_node = node

    def get_parent_node(self) -> 'OctreeNode':
        return self.parent_node

    def set_parent_node(self, node: 'OctreeNode') -> None:
        self.parent_node = node

    def get_child_nodes(self) -> List['OctreeNode']:
        return self.child_nodes

    def set_child_nodes(self, nodes: List['OctreeNode']) -> None:
        self.child_nodes = nodes

    def can_contain_child_nodes(self) -> bool:
        update_dist = Config.World.Gameplay.update_dist

        return ((self.x1 - self.x0) > update_dist and
                (self.y1 - self.y0) > update_dist and
                (self.z1 - self.z0) > update_dist)

    def get_object(self, guid: int):
        return self.objects.get(guid, None)

    def set_object(self, obj: Union[Unit, Player]) -> None:
        if self.get_child_nodes():
            node = self._get_nearest_child_node(obj)
            node.set_object(obj)
        else:
            self.objects[obj.guid] = obj

    def should_contain_object(self, obj: Union[Unit, Player]) -> bool:
        return (self.x0 <= obj.x <= self.x1 and
                self.y0 <= obj.y <= self.y1 and
                self.z0 <= obj.z <= self.z1)

    def _get_nearest_child_node(self, obj: Union[Unit, Player]):
        for i in range(0, OctreeNode.MAX_CHILD_NODES):
            if self.child_nodes[i].should_contain_object(obj):
                return self.child_nodes[i]

And builder for this:

class OctreeBuilder(object):

    def __init__(self, **kwargs):

        self.x0 = kwargs.pop('x0')
        self.x1 = kwargs.pop('x1')
        self.y0 = kwargs.pop('y0')
        self.y1 = kwargs.pop('y1')

        # FIXME: should get actual height for each map (use ADT, WDT, WMO for this purpose)
        self.z0 = -2000
        self.z1 = 2000

        self.root_node = OctreeNode(x0=self.x0, x1=self.x1, y0=self.y0, y1=self.y1, z0=self.z0, z1=self.z1)
        self.objects = kwargs.pop('objects', {})

    def build(self) -> OctreeNode:
        self._build_child_nodes(self.root_node, self.root_node)
        self.root_node.objects = self.objects
        return self.root_node

    def _set_objects(self) -> None:
        for obj in self.objects.values():
            self.root_node.set_object(obj)

    def _build_child_nodes(self, node: OctreeNode, root_node: OctreeNode) -> None:
        middle_x = (node.x0 + node.x1) / 2
        middle_y = (node.y0 + node.y1) / 2
        middle_z = (node.z0 + node.z1) / 2

        x = ((node.x0, middle_x), (middle_x, node.x1))
        y = ((node.y0, middle_y), (middle_y, node.y1))
        z = ((node.z0, middle_z), (middle_z, node.z1))

        child_nodes = []

        for i in range(1, OctreeNode.MAX_CHILD_NODES + 1):
            x0, x1 = x[i % 2 == 0]
            y0, y1 = y[(i & 3) % 3 == 0]
            z0, z1 = z[i > 4]

            child_node = OctreeBuilder._build_node(x0, x1, y0, y1, z0, z1)
            child_node.set_root_node(root_node)
            child_node.set_parent_node(node)

            if child_node.can_contain_child_nodes():
                self._build_child_nodes(child_node, root_node)
            else:
                child_node.guids = []

            child_nodes.append(child_node)

        node.set_child_nodes(child_nodes)

    @staticmethod
    def _build_node(x0: float, x1: float, y0: float, y1: float, z0: float, z1: float) -> OctreeNode:
        return OctreeNode(x0=x0, x1=x1, y0=y0, y1=y1, z0=z0, z1=z1)

I have spent a lot of time to find the ways to optimize memory usage. So, I tried to using tuple where possible (for example on the middle_x line of OctreeBuilder). Also I'm using __slots__ (removed from the code above due to big code example). And so on. But it seems my optimizations are not enough. And for now my code can't be working because of a lot of engaged memory. Please, help me to optimize it!

P.S. to see the full code example you can visit my project on https://github.com/sergio-ivanuzzo/idewave-core (dev branch)

NOTICE!: I want (if possible) to keep object-oriented approach in my project. So, it will be very nice if answer for this question will contain class-based solution.

Also, according to @zch comment I tried to replace my OctreeNode class with namedtuple, but this approach only increased used memory.

I want to keep in nodes next information:

  1. parent node
  2. child nodes
  3. coordinates (x0 x1 y0 y1 z0 z1)

if node is leaf node it also should keep list of objects ids.

UPDATED For building octree for each map I load map coords from db. As test we can use next data:

x0 = -1277.08
x1 = 3814.58
y0 = 8437.5
y1 = 11831.2

builder = OctreeBuilder(x0=x0, x1=x1, y0=y0, y1=y1, objects=objects)
octree = builder.build()
# attach octree to map object to save it in memory

The object example:

{'max_rage': None, 'char_class': None, 'min_damage': None, 'stamina': None, 'resistance_arcane': 0, 'max_ranged_damage': None, 'unit_template_id': 11183, 'id': 2897, 'focus': None, 'gender': None, 'max_damage': None, 'intellect': None, 'armor': 20, 'x': 1940.93, 'region_id': 1, 'health': 300, 'max_focus': None, 'level': 1, 'min_offhand_damage': None, 'spirit': None, 'attack_power': None, 'y': -4322.39, 'max_health': 300, 'energy': None, 'unit_flags': None, 'max_offhand_damage': None, 'resistance_fire': 0, 'base_mana': 0, 'z': 27.7612, 'mana': 0, 'max_energy': None, 'display_id': 11686, 'unit_bytes_1': None, 'resistance_nature': 0, 'base_health': 300, 'orientation': None, 'scale_x': 1.0, 'max_mana': None, 'happiness': None, 'native_display_id': 11686, 'mod_cast_speed': None, 'resistance_frost': 0, 'unit_bytes_2': None, 'map_id': None, 'rage': None, 'max_happiness': None, 'faction_template': 35, 'strength': None, 'resistance_shadow': 0, 'ranged_attack_power': None, 'power_type': None, 'race': None, 'agility': None, 'min_ranged_damage': None,  '_tracked_guids': set(), '_target': None}

Solution

  • Well, I have fixed the problem. First, I have decided to increase minimal size for nodes, thus OctreeBuilder returned less nodes. So, from 2 GB memory usage was reduced to 200 MB. Next, I have removed methods from OctreeNode class. So, I have left class without methods:

    class OctreeNode(object):
    
        __slots__ = (
            'x0',
            'x1',
            'y0',
            'y1',
            'z0',
            'z1',
            'parent_node',
            'child_nodes',
            'guids'
        )
    
        def __init__(self, **kwargs):
            self.x0: float = kwargs.pop('x0')
            self.x1: float = kwargs.pop('x1')
            self.y0: float = kwargs.pop('y0')
            self.y1: float = kwargs.pop('y1')
            self.z0: float = kwargs.pop('z0')
            self.z1: float = kwargs.pop('z1')
    
            self.parent_node: Union[OctreeNode, None] = kwargs.pop('parent_node', None)
            self.child_nodes: Union[List, None] = None
    
            self.guids: Union[List, None] = None
    

    Thanks to all for hints and for discussion. If you have another approaches for optimizing please let me know in the comments.