| # Copyright (C) 2020 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Classes for extracting profiling information from simpleperf record files. |
| |
| Example: |
| analyzer = RecordAnalyzer() |
| analyzer.analyze('perf.data') |
| |
| for event_name, event_count in analyzer.event_counts.items(): |
| print(f'Number of {event_name} events: {event_count}') |
| """ |
| |
| import collections |
| import logging |
| import sys |
| |
| from typing import DefaultDict, Dict, Iterable, Iterator, Optional |
| |
| # Disable import-error as simpleperf_report_lib is not in pylint's `sys.path` |
| # pylint: disable=import-error |
| import simpleperf_report_lib # type: ignore |
| |
| |
| class Instruction: |
| """Instruction records profiling information for an assembly instruction. |
| |
| Attributes: |
| relative_addr (int): The address of an instruction relative to the |
| start of its method. For arm64, the first instruction of a method |
| will be at the relative address 0, the second at the relative |
| address 4, and so on. |
| event_counts (DefaultDict[str, int]): A mapping of event names to their |
| total number of events for this instruction. |
| """ |
| |
| def __init__(self, relative_addr: int) -> None: |
| """Instantiates an Instruction. |
| |
| Args: |
| relative_addr (int): A relative address. |
| """ |
| self.relative_addr = relative_addr |
| |
| self.event_counts: DefaultDict[str, int] = collections.defaultdict(int) |
| |
| def record_sample(self, event_name: str, event_count: int) -> None: |
| """Records profiling information given by a sample. |
| |
| Args: |
| event_name (str): An event name. |
| event_count (int): An event count. |
| """ |
| self.event_counts[event_name] += event_count |
| |
| |
| class Method: |
| """Method records profiling information for a compiled method. |
| |
| Attributes: |
| name (str): A method name. |
| event_counts (DefaultDict[str, int]): A mapping of event names to their |
| total number of events for this method. |
| instructions (Dict[int, Instruction]): A mapping of relative |
| instruction addresses to their Instruction object. |
| """ |
| |
| def __init__(self, name: str) -> None: |
| """Instantiates a Method. |
| |
| Args: |
| name (str): A method name. |
| """ |
| self.name = name |
| |
| self.event_counts: DefaultDict[str, int] = collections.defaultdict(int) |
| self.instructions: Dict[int, Instruction] = {} |
| |
| def record_sample(self, relative_addr: int, event_name: str, |
| event_count: int) -> None: |
| """Records profiling information given by a sample. |
| |
| Args: |
| relative_addr (int): The relative address of an instruction hit. |
| event_name (str): An event name. |
| event_count (int): An event count. |
| """ |
| self.event_counts[event_name] += event_count |
| |
| if relative_addr not in self.instructions: |
| self.instructions[relative_addr] = Instruction(relative_addr) |
| |
| instruction = self.instructions[relative_addr] |
| instruction.record_sample(event_name, event_count) |
| |
| |
| class RecordAnalyzer: |
| """RecordAnalyzer extracts profiling information from simpleperf record |
| files. |
| |
| Multiple record files can be analyzed successively, each containing one or |
| more event types. Samples from odex files are the only ones analyzed, as |
| we're interested by the performance of methods generated by the optimizing |
| compiler. |
| |
| Attributes: |
| event_names (Set[str]): A set of event names to analyze. If empty, all |
| events are analyzed. |
| event_counts (DefaultDict[str, int]): A mapping of event names to their |
| total number of events for the analyzed samples. |
| methods (Dict[str, Method]): A mapping of method names to their Method |
| object. |
| report (simpleperf_report_lib.ReportLib): A ReportLib object. |
| target_arch (str): A target architecture determined from the first |
| record file analyzed. |
| """ |
| |
| def __init__(self, event_names: Optional[Iterable[str]] = None) -> None: |
| """Instantiates a RecordAnalyzer. |
| |
| Args: |
| event_names (Optional[Iterable[str]]): An optional iterable of |
| event names to analyze. If empty or falsy, all events are |
| analyzed. |
| """ |
| if not event_names: |
| event_names = [] |
| |
| self.event_names = set(event_names) |
| |
| self.event_counts: DefaultDict[str, int] = collections.defaultdict(int) |
| self.methods: Dict[str, Method] = {} |
| self.report: simpleperf_report_lib.ReportLib |
| self.target_arch = '' |
| |
| def analyze(self, filename: str) -> None: |
| """Analyzes a perf record file. |
| |
| Args: |
| filename (str): The path to a perf record file. |
| """ |
| # One ReportLib object needs to be instantiated per record file |
| self.report = simpleperf_report_lib.ReportLib() |
| self.report.SetRecordFile(filename) |
| |
| arch = self.report.GetArch() |
| if not self.target_arch: |
| self.target_arch = arch |
| elif self.target_arch != arch: |
| logging.error( |
| 'Record file %s is for the architecture %s, expected %s', |
| filename, arch, self.target_arch) |
| self.report.Close() |
| sys.exit(1) |
| |
| for sample in self.samples(): |
| event = self.report.GetEventOfCurrentSample() |
| if self.event_names and event.name not in self.event_names: |
| continue |
| |
| symbol = self.report.GetSymbolOfCurrentSample() |
| relative_addr = symbol.vaddr_in_file - symbol.symbol_addr |
| self.record_sample(symbol.symbol_name, relative_addr, event.name, |
| sample.period) |
| |
| self.report.Close() |
| logging.info('Analyzed %d event(s) for %d method(s)', |
| len(self.event_counts), len(self.methods)) |
| |
| def samples(self) -> Iterator[simpleperf_report_lib.SampleStruct]: |
| """Iterates over samples for compiled methods located in odex files. |
| |
| Yields: |
| simpleperf_report_lib.SampleStruct: A sample for a compiled method. |
| """ |
| sample = self.report.GetNextSample() |
| while sample: |
| symbol = self.report.GetSymbolOfCurrentSample() |
| if symbol.dso_name.endswith('.odex'): |
| yield sample |
| |
| sample = self.report.GetNextSample() |
| |
| def record_sample(self, method_name: str, relative_addr: int, |
| event_name: str, event_count: int) -> None: |
| """Records profiling information given by a sample. |
| |
| Args: |
| method_name (str): A method name. |
| relative_addr (int): The relative address of an instruction hit. |
| event_name (str): An event name. |
| event_count (int): An event count. |
| """ |
| self.event_counts[event_name] += event_count |
| |
| if method_name not in self.methods: |
| self.methods[method_name] = Method(method_name) |
| |
| method = self.methods[method_name] |
| method.record_sample(relative_addr, event_name, event_count) |