From 45a39107244ca832937e07a5690c72bc1ea92a68 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 22 Jul 2026 10:56:24 +0800 Subject: [PATCH] fix: cover legacy parser regressions --- CHANGELOG.md | 3 ++ tests/test_legacy_parsers.py | 66 ++++++++++++++++++++++++++++++++++++ tools/smaps_parser.py | 4 +-- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/test_legacy_parsers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f66cb6a..8f78da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixed +- Restored `[anon:native]` classification in the shared smaps parser so Native PSS and SwapPSS remain visible, and added regression coverage for deep HPROF dominator trees that exceed Python's recursion limit. + ## v1.3.0 - 2026-07-22 ### Added diff --git a/tests/test_legacy_parsers.py b/tests/test_legacy_parsers.py new file mode 100644 index 0000000..abdd9ff --- /dev/null +++ b/tests/test_legacy_parsers.py @@ -0,0 +1,66 @@ +import io +import sys +import tempfile +import unittest +from contextlib import redirect_stdout +from pathlib import Path +from types import SimpleNamespace + +from tools.hprof_parser import HprofParser +from tools import smaps_parser + + +class HprofParserRegressionTests(unittest.TestCase): + def test_retained_size_handles_a_chain_deeper_than_the_recursion_limit(self): + parser = HprofParser("unused.hprof") + depth = sys.getrecursionlimit() + 500 + parser.shallow_sizes = {object_id: 1 for object_id in range(depth)} + + for object_id in range(depth - 1): + parser.dominated_by[object_id].add(object_id + 1) + + parser.calculate_retained_sizes() + + self.assertEqual(depth, parser.retained_sizes[0]) + self.assertEqual(1, parser.retained_sizes[depth - 1]) + self.assertEqual(depth, len(parser.retained_sizes)) + + +class SmapsParserRegressionTests(unittest.TestCase): + def test_anon_native_pss_and_swap_pss_are_reported_as_native(self): + sample = """1000-2000 rw-p 00000000 00:00 0 [anon:native] +Pss: 123 kB +SwapPss: 7 kB +""" + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + smaps_path = root / "smaps" + report_path = root / "report.txt" + smaps_path.write_text(sample, encoding="utf-8") + + with redirect_stdout(io.StringIO()): + smaps_parser.parse_smaps(str(smaps_path)) + smaps_parser.print_result( + SimpleNamespace( + pid=None, + output=str(report_path), + type="ALL", + simple=False, + ) + ) + + report = report_path.read_text(encoding="utf-8") + + self.assertEqual( + smaps_parser.HEAP_NATIVE, + smaps_parser.match_type("[anon:native]", smaps_parser.HEAP_UNKNOWN), + ) + self.assertIn("Native (本地C/C++代码内存) : 0.123 MB", report) + self.assertIn("PSS: 0.123 MB", report) + self.assertIn("SwapPSS: 0.007 MB", report) + self.assertIn("[anon:native] : 123 kB", report) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/smaps_parser.py b/tools/smaps_parser.py index f705c4f..78a1f47 100644 --- a/tools/smaps_parser.py +++ b/tools/smaps_parser.py @@ -339,11 +339,11 @@ def match_type(name, prewhat): # All native allocator patterns should be classified as HEAP_NATIVE # # Supported patterns (per AOSP): - # - [heap]: Traditional native heap (Android 4.x - 10) + # - [heap] / [anon:native]: Generic native heap markers # - [anon:libc_malloc]: libc malloc allocator (Android 5.x - 10) # - [anon:scudo:primary/secondary]: Scudo allocator (Android 11+) # - [anon:GWP-ASan]: GWP-ASan debugging allocator (Android 11+) - if name.startswith("[heap]"): + if name.startswith("[heap]") or name.startswith("[anon:native]"): which_heap = HEAP_NATIVE elif name.startswith("[anon:libc_malloc]"): which_heap = HEAP_NATIVE