report_tree.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright (c) 2021 Project CHIP Authors
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. """Generate a tree representation of memory use.
  18. This program reads memory usage information produces a textual tree
  19. showing aggregate and/or individual memory usage by section.
  20. Use `--collect-method=help` to see available collection methods.
  21. Use `--limit=size` to truncate the tree.
  22. """
  23. import os
  24. import sys
  25. from typing import Dict, Optional, Sequence
  26. import anytree # type: ignore
  27. import memdf.collect
  28. import memdf.name
  29. import memdf.report
  30. import memdf.select
  31. from memdf import Config, DFs, SymbolDF
  32. class SourceTree:
  33. """Representation of source tree with associated size."""
  34. class Node(anytree.NodeMixin):
  35. """Represents a source file, directory, or other user of memory."""
  36. def __init__(self,
  37. name: str,
  38. size: int = 0,
  39. parent: Optional['SourceTree.Node'] = None):
  40. self.name = name
  41. self.parent = parent
  42. self.size = size
  43. def percentage(self) -> float:
  44. """Return percentage size of this node within its parent."""
  45. if not self.parent or self.parent.size == 0:
  46. return 100.0
  47. return 100 * self.size / self.parent.size
  48. def __init__(self, name: str):
  49. self.name = name
  50. self.root = self.Node(memdf.name.TOTAL)
  51. self.source_to_node: Dict[str, 'SourceTree.Node'] = {}
  52. self.symbol_to_node: Dict[str, 'SourceTree.Node'] = {}
  53. def source_node(self, source: str) -> 'SourceTree.Node':
  54. """Create a SourceTree.Node for a source file."""
  55. if not source or source == os.path.sep:
  56. return self.root
  57. head, tail = os.path.split(source)
  58. if not tail:
  59. return self.root
  60. if source not in self.source_to_node:
  61. self.source_to_node[source] = self.Node(
  62. tail, size=0, parent=self.source_node(head))
  63. return self.source_to_node[source]
  64. def symbol_node(self, source: str, symbol: str,
  65. size: int) -> 'SourceTree.Node':
  66. """Create a SourceTree node for a symbol."""
  67. if source == symbol:
  68. parent = self.root
  69. else:
  70. parent = self.source_node(source)
  71. node = self.Node(symbol, size, parent=parent)
  72. self.symbol_to_node[symbol] = node
  73. return node
  74. def calculate_sizes(self) -> None:
  75. """Modify a newly read tree with sizes of non-leaf nodes."""
  76. for node in anytree.iterators.PostOrderIter(self.root):
  77. child_sizes = [child.size for child in node.children]
  78. child_size = sum(child_sizes)
  79. node.size += child_size
  80. def truncate(self, limit: int) -> None:
  81. """Truncate tree at size limit."""
  82. if limit:
  83. for node in anytree.iterators.PostOrderIter(self.root):
  84. if node.children:
  85. shown_count = 0
  86. hidden_size = 0
  87. for child in node.children:
  88. if child.size > limit:
  89. shown_count += 1
  90. else:
  91. hidden_size += child.size
  92. child.parent = None
  93. if shown_count and hidden_size:
  94. self.Node(memdf.name.OTHER,
  95. size=hidden_size,
  96. parent=node)
  97. @staticmethod
  98. def from_symbols(config: Config, symbols: SymbolDF,
  99. tree_name: str) -> 'SourceTree':
  100. """Construct a SourceTree from a Memory Map DataFrame."""
  101. tree = SourceTree(tree_name)
  102. for row in symbols.itertuples():
  103. symbol = row.symbol
  104. if config['report.demangle']:
  105. symbol = memdf.report.demangle(symbol)
  106. tree.symbol_node(row.cu, symbol, row.size)
  107. tree.calculate_sizes()
  108. return tree
  109. def print(self) -> None:
  110. """Print tree hierarchically."""
  111. print(self.name)
  112. for pre, _, node in anytree.render.RenderTree(
  113. self.root, childiter=self._render_iter):
  114. print('{}{:2.0f}% {} {}'.format(pre, node.percentage(), node.size,
  115. node.name))
  116. @staticmethod
  117. def _render_iter(nodes: Sequence['SourceTree.Node']
  118. ) -> Sequence['SourceTree.Node']:
  119. """Order for displaying child nodes: decreasing size, others at end."""
  120. return sorted(
  121. nodes,
  122. key=lambda n: -1 if n.name == memdf.name.OTHER else n.size,
  123. reverse=True)
  124. def main(argv):
  125. status = 0
  126. try:
  127. config = memdf.collect.parse_args(
  128. {
  129. **memdf.select.CONFIG,
  130. **memdf.report.REPORT_CONFIG,
  131. **memdf.report.REPORT_BY_CONFIG,
  132. }, argv)
  133. config['args.need_cu'] = True
  134. dfs: DFs = memdf.collect.collect_files(config)
  135. symbols = dfs[SymbolDF.name]
  136. symbols = symbols[~(
  137. symbols.symbol.str.startswith(memdf.name.UNUSED_PREFIX)
  138. | symbols.symbol.str.startswith(memdf.name.OVERLAP_PREFIX))]
  139. by = config['report.by']
  140. for name in symbols[by].unique():
  141. tree = SourceTree.from_symbols(config,
  142. symbols.loc[symbols[by] == name],
  143. name)
  144. limit = (memdf.select.get_limit(config, by, name))
  145. tree.truncate(limit)
  146. print(f'\n{by.upper()}: ', end='')
  147. tree.print()
  148. except Exception as exception:
  149. raise exception
  150. return status
  151. if __name__ == '__main__':
  152. sys.exit(main(sys.argv))