From e56b6e4bd75c7ccec69f20b17c1e2d48b7c0892c Mon Sep 17 00:00:00 2001 From: Kristofer Jonsson Date: Thu, 29 Sep 2022 11:52:22 +0200 Subject: Ethos-U PMU monitor Add Python script demonstrating how to download performance data from device. Write baremetal PMU events to Event Recorder ring buffer and increase the systick sample rate. Change-Id: Ib73c56100a8de2d7b74c455d8f80cda0b59383da --- scripts/ethosu_monitor.py | 149 ++++++++++++++++++++++++++++++++ scripts/ethosumonitor/elf.py | 47 +++++++++++ scripts/ethosumonitor/inputs.py | 173 ++++++++++++++++++++++++++++++++++++++ scripts/ethosumonitor/outputs.py | 83 ++++++++++++++++++ scripts/ethosumonitor/sample.json | 19 +++++ scripts/ethosumonitor/types.py | 129 ++++++++++++++++++++++++++++ 6 files changed, 600 insertions(+) create mode 100755 scripts/ethosu_monitor.py create mode 100644 scripts/ethosumonitor/elf.py create mode 100644 scripts/ethosumonitor/inputs.py create mode 100644 scripts/ethosumonitor/outputs.py create mode 100644 scripts/ethosumonitor/sample.json create mode 100644 scripts/ethosumonitor/types.py (limited to 'scripts') diff --git a/scripts/ethosu_monitor.py b/scripts/ethosu_monitor.py new file mode 100755 index 0000000..60f0bdf --- /dev/null +++ b/scripts/ethosu_monitor.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +# +# SPDX-FileCopyrightText: Copyright 2022 Arm Limited and/or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +# +# 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. +# + +from ethosumonitor.inputs import * +from ethosumonitor.outputs import * +from sys import stderr, exit + +def eventLoop(input: InputInterface, output: OutputInterface): + count = 0 + + try: + while(True): + for record in input.readEventRecord(): + output.writeEventRecord(record) + count = count + 1 + except KeyboardInterrupt: + stderr.write(f'count={count}, input={input}\n') + pass + except EOFError: + pass + + output.flush() + +def getDAPLink(args): + return InputDAPLink(args.elf) + +def getMem(args): + return InputMem(args.elf, args.memory_map) + +def getFile(args): + return InputFile(args.file) + +def getOutput(args): + if args.output_format == 'binary': + return OutputBinary(args.output) + else: + return OutputJson(args.output) + +def addOutputArguments(parser): + parser.add_argument('--output-format', choices=['binary', 'json'], default='json', help='Output format.') + parser.add_argument('-o', '--output', default='/dev/stdout', help='Output file.') + +def main(): + import argparse + + parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, + description='Ethos-U monitor downloading profiling data.', + epilog=''' +Event Recorder: + The Event Recorder library is used to write performance data to a ring buffer + in memory. The ring buffer has a limited size and must be continuously + streamed to a host machine before it overflows. + + This script demonstrates how to stream performance data using DAPLink or + /dev/mem. Support for other technologies can be added implementing the + InputInterface class in inputs.py. +''') + subparsers = parser.add_subparsers() + + subparser = subparsers.add_parser('daplink', + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Download performance data using DAPLink.', + epilog=''' +DAPLink: + Arm Mbed DAPLink is an open source project that enables programming and + debugging application software running on an Arm Cortex CPU. A host machine + can connect to the target device using for example USB or JTAG. + + This script demonstrates how DAPLink can be used to stream Event Recorder + data from a target device. The ELF file passed to the script must be the + same application that is running on the device, and is used to find the + location of the Event Recorder ring buffer. + + $ ethosu_monitor.py daplink --target mps3_an540 myapplication.elf +''') + subparser.set_defaults(getInput=getDAPLink) + subparser.add_argument('--target', default='mps3_an540', help='DAPLink target platform.') + subparser.add_argument('elf', help='Elf file running on the target.') + addOutputArguments(subparser) + + subparser = subparsers.add_parser('memory', + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Download performance data using /dev/mem.', + epilog=''' +/dev/mem: + For a Linux based system the Event Recorder buffer should be stored in shared + memory accessible from Linux. This allows Linux to read device the Event + Recorder ring buffer using /dev/mem. + + The address of the Event Recorder ring buffer is found parsing the ELF + file. Because the device and Linux do not share the same address space a + memory map is required to translate device addresses into host addresses. + Please see sample.json for reference. + + $ ethosu_monitor.py memory --memory-map config.json myapplication.elf +''') + subparser.set_defaults(getInput=getMem) + subparser.add_argument('--memory-map', required=True, help='JSON file describing physical memory map of target.') + subparser.add_argument('elf', help='Elf file running on the target.') + addOutputArguments(subparser) + + subparser = subparsers.add_parser('file', + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Replay performance data stored in binary file.', + epilog=''' +file: + Event Recorder data can be written in binary format for later processing. + This will likely have less latency than the default JSON format, reducing + the risk over ring buffer overflows. + + $ ethosu_monitor.py daplink --output-format binary --output samples.bin myapplication.elf + + The binary data can later be unpacked to JSON. + + $ ethosu_monitor.py file samples.bin --output-format json +''') + subparser.set_defaults(getInput=getFile) + subparser.add_argument('file', help='Binary file containing recorded performance data.') + addOutputArguments(subparser) + + args = parser.parse_args() + + if 'getInput' not in args: + parser.print_help() + exit(2) + + input = args.getInput(args) + output = getOutput(args) + eventLoop(input, output) + +if __name__ == '__main__': + main() diff --git a/scripts/ethosumonitor/elf.py b/scripts/ethosumonitor/elf.py new file mode 100644 index 0000000..bf9ab39 --- /dev/null +++ b/scripts/ethosumonitor/elf.py @@ -0,0 +1,47 @@ +# +# SPDX-FileCopyrightText: Copyright 2022 Arm Limited and/or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +# +# 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. +# + +def elfFindSymbol(elf, name): + from elftools.elf.sections import SymbolTableSection + + for section in elf.iter_sections(): + if isinstance(section, SymbolTableSection): + symbol = section.get_symbol_by_name(name) + if symbol: + return symbol[0] + + return None + +def elfGetData(elf, address, size): + for section in elf.iter_sections(): + if address >= section.header['sh_addr'] and \ + (address + size) < (section.header['sh_addr'] + section.header['sh_size']): + offset = address - section.header['sh_addr'] + return bytearray(section.data()[offset:offset+size]) + + return None + +def elfGetSymbolData(elf, name): + from sys import stderr + + symbol = elfFindSymbol(elf, name) + if not symbol: + stderr.write(f'Failed to find symbol {name}\n') + return None + + return elfGetData(elf, symbol.entry.st_value, symbol.entry.st_size) diff --git a/scripts/ethosumonitor/inputs.py b/scripts/ethosumonitor/inputs.py new file mode 100644 index 0000000..60fb8ed --- /dev/null +++ b/scripts/ethosumonitor/inputs.py @@ -0,0 +1,173 @@ +# +# SPDX-FileCopyrightText: Copyright 2022 Arm Limited and/or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +# +# 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. +# + +from .elf import * +from .types import * +import json +import mmap +import os +from sys import stderr + +class InputInterface: + def readEventRecord(self) -> EventRecord_t: + ... + +class InputFile(InputInterface): + def __init__(self, fname): + self.file = open(fname, 'rb') + + def readEventRecord(self): + data = self.file.read(EventRecord_t.SIZE) + if len(data) == 0: + raise EOFError + + yield data + +class InputRingBuffer(InputInterface): + def __init__(self, elfFile): + from elftools.elf.elffile import ELFFile + + with open(elfFile, 'rb') as f: + elf = ELFFile(f) + symbol = elfFindSymbol(elf, 'EventRecorderInfo') + elfInfo = EventRecorderInfo_t(elfGetData(elf, symbol.entry.st_value, symbol.entry.st_size)) + memInfo = EventRecorderInfo_t(self.read(symbol.entry.st_value, symbol.entry.st_size)) + + # Validate EventRecorder info + if elfInfo.protocolType != memInfo.protocolType or \ + elfInfo.protocolVersion != memInfo.protocolVersion or \ + elfInfo.eventBuffer != memInfo.eventBuffer or \ + elfInfo.eventStatus != memInfo.eventStatus: + raise Exception(f'EventRecorder info mismatch. elf={elfInfo}, mem={memInfo}') + + self.info = elfInfo + status = EventStatus_t(self.read(self.info.eventStatus, EventStatus_t.SIZE)) + self.timestamp = status.tsLast + self.recordIndex = status.recordIndex + self.overflow = 0 + + def readEventRecord(self): + # Read status and use timestamp to detect if there are new samples + status = EventStatus_t(self.read(self.info.eventStatus, EventStatus_t.SIZE)) + if self.timestamp == status.tsLast: + return None + + self.timestamp = status.tsLast + + # Detect firmware reset + if self.recordIndex > status.recordIndex: + self.recordIndex = 0 + + # Detect of recordIndex has overflowed the ring buffer + if status.recordIndex - self.recordIndex > self.info.recordCount: + stderr.write('Warning: Ring buffer overflow\n') + self.overflow = self.overflow + 1 + self.recordIndex = status.recordIndex + + # Generate data for each event record + for i in range(self.recordIndex, status.recordIndex): + i = i % self.info.recordCount + yield self.read(self.info.eventBuffer + EventRecord_t.SIZE * i, EventRecord_t.SIZE) + + self.recordIndex = status.recordIndex + + def read(self, address, size) -> bytearray: + ... + +class InputDAPLink(InputRingBuffer): + def __init__(self, elfFile): + self._open() + super().__init__(elfFile) + self.target.reset() + + def _open(self): + from pyocd.core.helpers import ConnectHelper + + self.session = ConnectHelper.session_with_chosen_probe() + self.board = self.session.board + self.target = self.board.target + + self.session.open() + + def read(self, address, size): + from pyocd.core.exceptions import Error + + for i in range(1000): + try: + return bytearray(self.target.read_memory_block8(address, size)) + except Error: + pass + +class InputMem(InputRingBuffer): + def __init__(self, elfFile, jsonFile): + with open(jsonFile, 'r') as f: + jsonDoc = json.loads(f.read()) + + self.memoryMap = [] + for memoryMap in jsonDoc['memoryMap']: + host = int(memoryMap['host'], 16) + device = int(memoryMap['device'], 16) + size = int(memoryMap['size'], 16) + self.memoryMap.append(DevMemDevice(host, device, size)) + + super().__init__(elfFile) + + def read(self, device, size): + for memoryMap in self.memoryMap: + data = memoryMap.read(device, size) + if data: + return data + + stderr.write(f'Warning: No mapping found for device address {hex(device)} size {size}.\n') + return None + +class DevMem: + def __init__(self, address, size): + self.base_address = address & ~(mmap.PAGESIZE - 1) + self.offset = address - self.base_address + self.size = size + self.offset + + self.fd = os.open('/dev/mem', os.O_RDWR | os.O_SYNC) + self.mem = mmap.mmap(self.fd, self.size, mmap.MAP_SHARED, mmap.PROT_READ, + offset=self.base_address) + + def __del__(self): + os.close(self.fd) + + def read(self, offset, size): + self.mem.seek(self.offset + offset) + + data = bytearray(size) + for i in range(size): + data[i] = self.mem.read_byte() + + return data + +class DevMemDevice(DevMem): + def __init__(self, host, device, size): + super().__init__(host, size) + + self.device = device + self.size = size + + def read(self, device, size): + offset = device - self.device + if offset < 0 or (offset + size) > self.size: + return None + + return super().read(offset, size) diff --git a/scripts/ethosumonitor/outputs.py b/scripts/ethosumonitor/outputs.py new file mode 100644 index 0000000..5a4101c --- /dev/null +++ b/scripts/ethosumonitor/outputs.py @@ -0,0 +1,83 @@ +# +# SPDX-FileCopyrightText: Copyright 2022 Arm Limited and/or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +# +# 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. +# + +from .types import * +from sys import stderr + +class OutputInterface: + def flush(self): + ... + + def writeEventRecord(self, data: bytearray): + ... + +class OutputBinary(OutputInterface): + def __init__(self, fname): + self.file = open(fname, 'wb') + + def flush(self): + self.file.flush() + + def writeEventRecord(self, data: bytearray): + self.file.write(data) + +class OutputJson(OutputInterface): + def __init__(self, fname): + self.file = open(fname, 'w') + + self.count = 0 + self.nextId = 0 + self.timestamp = 0 + self.event = [] + + def flush(self): + self.file.flush() + + def writeEventRecord(self, data: bytearray): + record = EventRecord_t(data) + + if record.first(): + # Drop messages that don't originate from Ethos-U + if record.component() != EventRecord_t.ETHOSU_CID: + return + + self.nextId = 0 + self.timestamp = record.timestamp + self.eventConfig = [] + self.eventCount = [] + + messageIndex = record.message() + + if self.nextId != messageIndex or self.timestamp != record.timestamp: + stderr.write(f'Expected record id {self.nextId} and timestamp {self.timestamp} but got {messageIndex} and {record.timestamp}. count={self.count}, locked={record.locked()}, valid={record.valid()}\n') + stderr.write(f'record={record}\n') + return + + self.nextId = messageIndex + 1 + + if messageIndex == 0: + self.cycleCount = record.val2 << 32 | record.val1 + elif messageIndex == 1: + self.qread = record.val1 + self.status = record.val2 + else: + self.eventConfig.append(record.val1) + self.eventCount.append(record.val2) + + if record.last(): + self.file.write(f'{{ "timestamp": {self.timestamp}, "qread": {self.qread}, "status": {self.status}, "cycleCount": {self.cycleCount}, "eventConfig": [ {", ".join(map(str, self.eventConfig))} ], "eventCount": [ {", ".join(map(str, self.eventCount))} ] }}\n') diff --git a/scripts/ethosumonitor/sample.json b/scripts/ethosumonitor/sample.json new file mode 100644 index 0000000..537e95a --- /dev/null +++ b/scripts/ethosumonitor/sample.json @@ -0,0 +1,19 @@ +{ + "__comment__": [ + "'host' is the Linux physical address accessible from /dev/mem.", + "'device' is the corresponding device address.", + "'size' is the size in bytes of the memory region." + ], + "memoryMap": [ + { + "host": "0x6cf00000", + "device": "0x00000000", + "size": "0x00400000" + }, + { + "host": "0x84000000", + "device": "0x64000000", + "size": "0x01000000" + } + ] +} diff --git a/scripts/ethosumonitor/types.py b/scripts/ethosumonitor/types.py new file mode 100644 index 0000000..5116955 --- /dev/null +++ b/scripts/ethosumonitor/types.py @@ -0,0 +1,129 @@ +# +# SPDX-FileCopyrightText: Copyright 2022 Arm Limited and/or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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 +# +# 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. +# + +import struct + +class EventRecord_t: + SIZE = 16 + + INFO_ID_MASK = 0x0000FFFF + INFO_MESSAGE_MASK = 0x000000FF + INFO_COMPONENT_MASK = 0x0000FF00 + INFO_COMPONENT_POS = 8 + INFO_FIRST = 0x01000000 + INFO_LAST = 0x02000000 + INFO_LOCKED = 0x04000000 + INFO_VALID = 0x08000000 + INFO_MSB_TS = 0x10000000 + INFO_MSB_VAL1 = 0x20000000 + INFO_MSB_VAL2 = 0x40000000 + INFO_TBIT = 0x80000000 + + # Component identifiers + EVENT_CID = 0xFF + ETHOSU_CID = 0x00 + + # Message identifiers + EVENT_MID_INIT = 0x00 + EVENT_MID_START = 0x01 + EVENT_MID_STOP = 0x02 + EVENT_MID_CLOCK = 0x03 + + EVENT_ID_INIT = (EVENT_CID << 8) | EVENT_MID_INIT + EVENT_ID_START = (EVENT_CID << 8) | EVENT_MID_START + EVENT_ID_STOP = (EVENT_CID << 8) | EVENT_MID_STOP + EVENT_ID_CLOCK = (EVENT_CID << 8) | EVENT_MID_CLOCK + + def __init__(self, data): + self.data = data + + # Unpack the struct and restore the MSB from info to timestamp, val1 and val2 + t = struct.unpack('IIII', data) + self.timestamp = t[0] & ~EventRecord_t.INFO_TBIT | (t[3] & EventRecord_t.INFO_MSB_TS) << 3 + self.val1 = t[1] & ~EventRecord_t.INFO_TBIT | (t[3] & EventRecord_t.INFO_MSB_VAL1) << 2 + self.val2 = t[2] & ~EventRecord_t.INFO_TBIT | (t[3] & EventRecord_t.INFO_MSB_VAL2) << 1 + self.info = t[3] + + def first(self): + return self.info & EventRecord_t.INFO_FIRST != 0 + + def last(self): + return self.info & EventRecord_t.INFO_LAST != 0 + + def component(self): + return (self.info & EventRecord_t.INFO_COMPONENT_MASK) >> EventRecord_t.INFO_COMPONENT_POS + + def message(self): + return self.info & EventRecord_t.INFO_MESSAGE_MASK + + def id(self): + return self.info & EventRecord_t.INFO_ID_MASK + + def locked(self): + return self.info & EventRecord_t.INFO_LOCKED != 0 + + def valid(self): + return self.info & EventRecord_t.INFO_VALID != 0 + + def __str__(self): + return f'{{ "timestamp": {hex(self.timestamp)}, "val1": {hex(self.val1)}, "val2": {hex(self.val2)}, "info": "{hex(self.info)}" }}' + +class EventStatus_t: + SIZE = 36 + + def __init__(self, data): + t = struct.unpack('BBHIIIIIIII', data) + + self.state = t[0] + self.context = t[1] + self.infoCrc = t[2] + self.recordIndex = t[3] + self.recordsWritten = t[4] + self.recordsDumped = t[5] + self.tsOverflow = t[6] + self.tsFreq = t[7] + self.tsLast = t[8] + self.initCount = t[9] + self.signature = t[10] + + def __str__(self): + return f'{{ state={self.state}, context={self.context}, info_crc={self.infoCrc}, ' \ + f'record_index={self.recordIndex}, records_written={self.recordsWritten}, records_dumped={self.recordsDumped}, ' \ + f'ts_overflow={self.tsOverflow}, ts_freq={self.tsFreq}, ts_last={self.tsLast}, ' \ + f'init_count={self.initCount}, signature={self.signature} }}' + +class EventRecorderInfo_t: + SIZE = 24 + + def __init__(self, data): + t = struct.unpack('BBHIIIIBBBB', data) + + self.protocolType = t[0] + # self._reserved = t[1] + self.protocolVersion = t[2] + self.recordCount = t[3] + self.eventBuffer = t[4] + self.eventFilter = t[5] + self.eventStatus = t[6] + self.tsSource = t[7] + # self._reserved1 = t[8] + # self._reserved2 = t[9] + # self._reserved3 = t[10] + + def __str__(self): + return f'{{ protocolType={hex(self.protocolType)}, protocolVersion={hex(self.protocolVersion)}, recordCount={self.recordCount}, eventBuffer={hex(self.eventBuffer)}, eventFilter={hex(self.eventFilter)}, eventStatus={hex(self.eventStatus)}, tsSource={self.tsSource} }}' -- cgit v1.2.1