| # Copyright 2024 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """ | |
| Parse `ffx profile memory --debug-json` output into Python objects. | |
| """ | |
| import itertools | |
| import json | |
| from dataclasses import dataclass, field | |
| from functools import cached_property | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Tuple | |
| from dataclasses_json_lite import Undefined, config, dataclass_json | |
| from multidict import multidict | |
| def table_of(type): | |
| return lambda table: [ | |
| type.from_dict(dict(zip(table[0], row))) for row in table[1:] | |
| ] | |
| @dataclass_json(undefined=Undefined.RAISE) | |
| @dataclass | |
| class Kernel: | |
| total: int | |
| free: int | |
| wired: int | |
| total_heap: int | |
| free_heap: int | |
| vmo: int | |
| mmu: int | |
| ipc: int | |
| other: int | |
| vmo_pager_total: int | |
| vmo_pager_newest: int | |
| vmo_pager_oldest: int | |
| vmo_discardable_locked: int | |
| vmo_discardable_unlocked: int | |
| vmo_reclaim_disabled: int | |
| @dataclass_json(undefined=Undefined.RAISE) | |
| @dataclass | |
| class KmemStatsCompression: | |
| uncompressed_storage_bytes: int | |
| compressed_storage_bytes: int | |
| compressed_fragmentation_bytes: int | |
| compression_time: int | |
| decompression_time: int | |
| total_page_compression_attempts: int | |
| failed_page_compression_attempts: int | |
| total_page_decompressions: int | |
| compressed_page_evictions: int | |
| eager_page_compressions: int | |
| memory_pressure_page_compressions: int | |
| critical_memory_page_compressions: int | |
| pages_decompressed_unit_ns: int | |
| pages_decompressed_within_log_time: List[int] | |
| @dataclass_json(undefined=Undefined.RAISE) | |
| @dataclass | |
| class Process: | |
| koid: int | |
| name: str | |
| vmos_koid: List[int] = field(metadata=config(field_name="vmos")) | |
| @cached_property | |
| def vmos(self) -> Tuple["VMO"]: | |
| return tuple(self._capture.vmo_by_koid[koid] for koid in self.vmos_koid) | |
| @cached_property | |
| def vmos_and_their_ancestors(self) -> Tuple["VMO"]: | |
| koids = set() | |
| for vmo in self.vmos: | |
| koids.add(vmo.koid) | |
| koids.update(a.koid for a in vmo.ancestors) | |
| return tuple(self._capture.vmo_by_koid[koid] for koid in koids) | |
| @dataclass_json(undefined=Undefined.RAISE) | |
| @dataclass | |
| class VMO: | |
| koid: int | |
| name_index: int = field(metadata=config(field_name="name")) | |
| parent_koid: int | |
| committed_bytes: int | |
| allocated_bytes: int | |
| populated_bytes: Optional[int] = None | |
| def __str__(self): | |
| return f"VMO(koid={self.koid!r} name={self.name!r})" | |
| @classmethod | |
| def expand_vmos_with_ancestors(cls, vmos): | |
| koids = set() | |
| for vmo in vmos: | |
| koids.add(vmo.koid) | |
| koids.update(a.koid for a in vmo.ancestors) | |
| return tuple(vmos[0]._capture.vmo_by_koid[koid] for koid in koids) | |
| @cached_property | |
| def name_or_ancestor_name(self): | |
| if not self.name and self.parent: | |
| return self.parent.name_or_ancestor_name | |
| return self.name | |
| @cached_property | |
| def name(self): | |
| return self._capture.vmo_names[self.name_index] | |
| @cached_property | |
| def parent(self): | |
| return ( | |
| self._capture.vmo_by_koid[self.parent_koid] | |
| if self.parent_koid | |
| else None | |
| ) | |
| @cached_property | |
| def processes(self): | |
| return self._capture.processes_by_vmo_koid[self.koid] | |
| @cached_property | |
| def children(self): | |
| return self._capture.vmos_by_parent_vmo_koid[self.koid] | |
| def descendants(self): | |
| for vmo in self.children: | |
| yield vmo | |
| yield from vmo.descendants() | |
| @cached_property | |
| def ancestors(self): | |
| return (self.parent,) + self.parent.ancestors if self.parent else () | |
| @dataclass_json(undefined=Undefined.RAISE) | |
| @dataclass | |
| class Capture: | |
| time: int = field(metadata=config(field_name="Time")) | |
| processes: List[Process] = field( | |
| metadata=config(field_name="Processes", decoder=table_of(Process)) | |
| ) | |
| kernel: Kernel = field(metadata=config(field_name="Kernel")) | |
| vmo_names: List[str] = field(metadata=config(field_name="VmoNames")) | |
| vmos: List[VMO] = field( | |
| metadata=config(field_name="Vmos", decoder=table_of(VMO)) | |
| ) | |
| kmem_stats_compression: Optional[KmemStatsCompression] = None | |
| @cached_property | |
| def vmo_by_koid(self): | |
| return {vmo.koid: vmo for vmo in self.vmos} | |
| @cached_property | |
| def processes_by_vmo_koid(self): | |
| return multidict( | |
| (vmo_koid, process) | |
| for process in self.processes | |
| for vmo_koid in process.vmos_koid | |
| ) | |
| @cached_property | |
| def processes_by_vmo_koid_with_ancestors(self): | |
| return multidict( | |
| (vmo.koid, process) | |
| for process in self.processes | |
| for vmo in VMO.expand_vmos_with_ancestors(process.vmos) | |
| ) | |
| @cached_property | |
| def vmos_by_name(self): | |
| return multidict((vmo.name, vmo) for vmo in self.vmos) | |
| @cached_property | |
| def processes_by_name(self) -> Dict[str, List[Process]]: | |
| return multidict((process.name, process) for process in self.processes) | |
| @cached_property | |
| def process_by_koid(self): | |
| return {process.koid: process for process in self.processes} | |
| @cached_property | |
| def vmos_by_parent_vmo_koid(self): | |
| return multidict((vmo.parent_koid, vmo) for vmo in self.vmos) | |
| @dataclass_json(undefined=Undefined.RAISE) | |
| @dataclass | |
| class Bucket: | |
| event_code: int | |
| name: str | |
| process: str | |
| vmo: str | |
| @dataclass_json(undefined=Undefined.RAISE) | |
| @dataclass | |
| class ProfileMemoryDebug: | |
| capture: Capture = field(metadata=config(field_name="Capture")) | |
| buckets: List[Bucket] = field(metadata=config(field_name="Buckets")) | |
| def parse(path: Path): | |
| """Parses `ffx profile memory --debug-json`""" | |
| with open(path, "rt") as f: | |
| result = ProfileMemoryDebug.from_dict(json.load(f)) | |
| for o in itertools.chain(result.capture.processes, result.capture.vmos): | |
| o._capture = result.capture | |
| return result |