blob: 62147a9b21471eb12676057f93512b711e83cc87 [file]
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
""" Tests for memory_usage_view.py"""
import json
import os
import pandas as pd
import pathlib
import sys
import unittest
_SRC_PATH = pathlib.Path(__file__).resolve().parents[4]
sys.path.append(str(_SRC_PATH / 'tools/android'))
from colabutils.memory_usage.memory_usage_view import (
MemoryUsageView, TreeNode, _aggregate_nodes, _zip_by_name,
_compare_node_lists, _prettify_size)
_REALISTIC_JSON = """
[
{
"name": "main",
"value": 0,
"children": [
{
"name": "foo",
"value": 0,
"children": [
{
"name": "malloc",
"value": 8
}
]
},
{
"name": "malloc",
"value": 16
},
{
"name": "bar",
"value": 0,
"children": [
{
"name": "malloc",
"value": 48
}
]
}
]
}
]
"""
_SIMPLE_JSON_WITH_DELTAS = """
[
{
"name": "root1",
"value": 100,
"delta": 10,
"children": [
{
"name": "child1_1",
"value": 50,
"delta": 5
},
{
"name": "child1_2",
"value": 30,
"delta": 3
}
]
},
{
"name": "root2",
"value": 200,
"delta": 20
}
]
"""
class MemoryUsageViewTest(unittest.TestCase):
def test_json_preserved(self):
view = MemoryUsageView.from_json(_SIMPLE_JSON_WITH_DELTAS)
dumped_json = view.to_json()
self.assertEqual(json.loads(_SIMPLE_JSON_WITH_DELTAS),
json.loads(dumped_json))
def test_from_json_defaults(self):
json_data = '''[{"name": "root"}]'''
view = MemoryUsageView.from_json(json_data)
self.assertEqual(len(view.roots), 1)
self.assertEqual(view.roots[0].name, 'root')
self.assertEqual(view.roots[0].value, 0)
self.assertEqual(view.roots[0].delta, 0)
self.assertEqual(view.roots[0].children, [])
def test_from_json_empty_name_stays_empty(self):
json_data = '''[{"name": ""}]'''
view = MemoryUsageView.from_json(json_data)
self.assertEqual(len(view.roots), 1)
self.assertEqual(view.roots[0].name, '')
def test_to_json_no_children(self):
node = TreeNode(name='test_node')
view = MemoryUsageView(roots=[node])
dumped_json = json.loads(view.to_json())
expected_json = {'name': 'test_node', 'value': 0, 'delta': 0}
self.assertEqual(dumped_json, [expected_json])
def test_from_df(self):
df_data = {
'callsite_id': [1, 2, 3],
'depth': [0, 0, 1],
'frame_name': ['root_a', 'root_b', 'child_a_1'],
'parent_callsite_id': [None, None, 1],
'total_size_bytes': [100, 200, 50],
}
df = pd.DataFrame(df_data)
view = MemoryUsageView.from_df(df, demangle=False, aggregate=False)
dumped_json = view.to_json()
expected_json = """
[
{
"name": "root_a",
"value": 100,
"delta": 0,
"children": [
{
"name": "child_a_1",
"value": 50,
"delta": 0
}
]
},
{
"name": "root_b",
"value": 200,
"delta": 0
}
]
"""
self.assertEqual(json.loads(expected_json), json.loads(dumped_json))
self.assertEqual(['root_a', 'root_b'], sorted(view.toplevel_names()))
def test_from_df_empty_name_converts_to_unknown(self):
df_data = {
'callsite_id': [1, 2, 3],
'depth': [0, 0, 1],
'frame_name': ['root_a', 'root_b', ''],
'parent_callsite_id': [None, None, 1],
'total_size_bytes': [100, 200, 50],
}
df = pd.DataFrame(df_data)
view = MemoryUsageView.from_df(df, demangle=False, aggregate=True)
self.assertEqual(2, len(view.roots))
root_b_node, root_a_node = view.roots
self.assertEqual('root_a', root_a_node.name)
self.assertEqual(100 + 50, root_a_node.value)
self.assertEqual(1, len(root_a_node.children))
empty_name_node = root_a_node.children[0]
self.assertEqual('unknown', empty_name_node.name)
self.assertEqual(50, empty_name_node.value)
def test_aggregation_sorting(self):
# Check all cumulative values and the order.
view = MemoryUsageView.from_json(_REALISTIC_JSON)
view.roots = _aggregate_nodes(view.roots)
self.assertEqual(1, len(view.roots))
root_node = view.roots[0]
self.assertEqual('main', root_node.name)
self.assertEqual(48 + 16 + 8, root_node.value)
self.assertEqual(3, len(root_node.children))
bar_node, malloc_node, foo_node = root_node.children
self.assertEqual('bar', bar_node.name)
self.assertEqual(48, bar_node.value)
self.assertEqual('malloc', malloc_node.name)
self.assertEqual(16, malloc_node.value)
self.assertEqual('foo', foo_node.name)
self.assertEqual(8, foo_node.value)
def test_aggregation_sorting2(self):
# Change the order of the children and verify that
# it leads to the same order after aggregation.
view = MemoryUsageView.from_json(_REALISTIC_JSON)
foo_node, malloc_node, bar_node = view.roots[0].children
view.roots[0].children = [foo_node, bar_node, malloc_node]
view.roots = _aggregate_nodes(view.roots)
root_node = view.roots[0]
bar_node, malloc_node, foo_node = root_node.children
self.assertEqual('bar', bar_node.name)
self.assertEqual(48, bar_node.value)
self.assertEqual('malloc', malloc_node.name)
self.assertEqual(16, malloc_node.value)
self.assertEqual('foo', foo_node.name)
self.assertEqual(8, foo_node.value)
def test_aggregation_merge_duplicate_names(self):
view = MemoryUsageView.from_json(_REALISTIC_JSON)
# Make two adjacent node names match for merging.
foo_node, _, bar_node = view.roots[0].children
self.assertEqual('bar', bar_node.name)
foo_node.name = 'bar'
view.roots = _aggregate_nodes(view.roots)
self.assertEqual(1, len(view.roots))
root_node = view.roots[0]
self.assertEqual(2, len(root_node.children))
bar_node, malloc_node = root_node.children
self.assertEqual('bar', bar_node.name)
self.assertEqual(48 + 8, bar_node.value)
self.assertEqual('malloc', malloc_node.name)
self.assertEqual(16, malloc_node.value)
def test_zip_by_name_both_empty(self):
self.assertEqual([], list(_zip_by_name([], [])))
def test_zip_by_name_one_empty(self):
node = TreeNode(name='abc')
self.assertEqual([(node, None)], list(_zip_by_name([node], [])))
self.assertEqual([(None, node)], list(_zip_by_name([], [node])))
node2 = TreeNode(name='def')
self.assertEqual([(None, node), (None, node2)],
list(_zip_by_name([], [node, node2])))
def test_zip_by_name_simple(self):
left_node1 = TreeNode(name='l1')
left_node2 = TreeNode(name='l2')
left_nodes = [left_node1, left_node2]
right_node1 = TreeNode(name='r1')
right_node2 = TreeNode(name='r2')
right_nodes = [right_node1, right_node2]
result = list(_zip_by_name(left_nodes, right_nodes))
self.assertEqual(4, len(result))
self.assertIn((left_node1, None), result)
self.assertIn((left_node2, None), result)
self.assertIn((None, right_node1), result)
self.assertIn((None, right_node2), result)
def test_zip_by_name_with_intersection(self):
left_node1 = TreeNode(name='l1')
left_abc = TreeNode(name='abc')
left_nodes = [left_node1, left_abc]
right_abc = TreeNode(name='abc')
right_node2 = TreeNode(name='r2')
right_nodes = [right_abc, right_node2]
result = list(_zip_by_name(left_nodes, right_nodes))
self.assertEqual(3, len(result))
self.assertIn((left_abc, right_abc), result)
self.assertIn((left_node1, None), result)
self.assertIn((None, right_node2), result)
def test_compare_nothing(self):
self.assertEqual([], _compare_node_lists([], []))
def test_compare_one_node_to_nothing(self):
node = TreeNode(name='one', value=123)
result_node = TreeNode(name='one', value=123, delta=-123)
self.assertEqual([result_node], _compare_node_lists([node], []))
node.value = 456
result_node.value = 0
result_node.delta = 456
self.assertEqual([result_node], _compare_node_lists([], [node]))
def test_compare_same_name_nodes(self):
node_base = TreeNode(name='pikachu', value=123)
node_new = TreeNode(name='pikachu', value=456)
result = _compare_node_lists([node_base], [node_new])
self.assertEqual(1, len(result))
result_node = result[0]
self.assertEqual(123, result_node.value)
self.assertEqual('pikachu', result_node.name)
self.assertEqual(456 - 123, result_node.delta)
def test_compare_children(self):
base_root = TreeNode(name='root', value=30)
new_root = TreeNode(name='root', value=80)
base_root.children = [
TreeNode(name='common_child', value=10),
TreeNode(name='base_only_child', value=20),
]
new_root.children = [
TreeNode(name='common_child', value=30),
TreeNode(name='new_only_child', value=50),
]
result_nodes = _compare_node_lists([base_root], [new_root])
self.assertEqual(1, len(result_nodes))
result_root = result_nodes[0]
self.assertEqual('root', result_root.name)
self.assertEqual(30, result_root.value)
self.assertEqual(80 - 30, result_root.delta)
self.assertEqual(3, len(result_root.children))
new_only, common_child, base_only = result_root.children
self.assertEqual('new_only_child', new_only.name)
self.assertEqual(0, new_only.value)
self.assertEqual(50, new_only.delta)
self.assertEqual('common_child', common_child.name)
self.assertEqual(10, common_child.value)
self.assertEqual(30 - 10, common_child.delta)
self.assertEqual('base_only_child', base_only.name)
self.assertEqual(20, base_only.value)
self.assertEqual(-20, base_only.delta)
def test_compare_children_and_single(self):
base_root = TreeNode(name='root', value=30)
new_root = TreeNode(name='root', value=80)
base_root.children = [
TreeNode(name='child10', value=10),
TreeNode(name='child20', value=20),
]
result_nodes = _compare_node_lists([base_root], [new_root])
self.assertEqual(1, len(result_nodes))
result_root = result_nodes[0]
self.assertEqual('root', result_root.name)
self.assertEqual(30, result_root.value)
self.assertEqual(80 - 30, result_root.delta)
self.assertEqual(2, len(result_root.children))
child_10, child_20 = result_root.children
self.assertEqual(10, child_10.value)
self.assertEqual(-10, child_10.delta)
self.assertEqual(20, child_20.value)
self.assertEqual(-20, child_20.delta)
def test_prettify_size(self):
tests = [
(500, 500),
(1024, '1 KiB'),
(1536, '1.5 KiB'),
(1500, '1.46 KiB'),
(1048576, '1 MiB'),
(123456789, '117.74 MiB'),
(1099511627776, '1024 GiB'),
(2000000000000, 2000000000000),
]
test_lines = []
for inp, expected in tests:
self.assertEqual(expected, _prettify_size(inp))
if __name__ == '__main__':
unittest.main()