blob: 8a961b0bb93b535bb3421a678b70d0f083cd51bd [file] [log] [blame]
# 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