blob: a0490e8bd153efefe56027345afa671ca9657f70 [file] [log] [blame]
#
# Copyright (C) 2022 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.
"""
Command line tool to process minidump files and print what was logged by GfxApiLogger.
For more details see:
design: go/bstar-gfx-logging
g3doc: http://g3doc/play/g3doc/games/battlestar/kiwivm/graphics-tips.md#gfx-logs
C++: http://source/play-internal/battlestar/aosp/device/generic/vulkan-cereal/utils/include/utils/GfxApiLogger.h
Usage:
python3 print_gfx_logs.py <path to minidump file>
"""
from __future__ import annotations
import argparse
import ctypes
import sys
from datetime import datetime
import mmap
import textwrap
from . import command_printer
from typing import NamedTuple, Optional, List
import traceback
class Header(ctypes.Structure):
"""The C struct that we use to represent the data in memory
Keep in sync with GfxApiLogger.h
"""
_fields_ = [('signature', ctypes.c_char * 10),
('version', ctypes.c_uint16),
('thread_id', ctypes.c_uint32),
('last_written_time', ctypes.c_uint64),
('write_index', ctypes.c_uint32),
('committed_index', ctypes.c_uint32),
('capture_id', ctypes.c_uint64),
('data_size', ctypes.c_uint32)]
class Command(NamedTuple):
"""A single command in the stream"""
timestamp: int # Unix timestamp when command was recorded, in microseconds
opcode: int
original_size: int
data: bytes
class Stream(NamedTuple):
"""Stream of commands received from the guest"""
pos_in_file: int # Location of this stream in the minidump file, useful for debugging
timestamp: int # Unix timestamp of last command received, in microseconds
thread_id: int
capture_id: int
commands: List[Command]
error_message: Optional[str] # `None` if there were no errors parsing this stream
@staticmethod
def error(pos_in_file: int, error_message: str) -> Stream:
return Stream(
pos_in_file=pos_in_file, timestamp=0, thread_id=0, capture_id=0, commands=[],
error_message=error_message)
def timestampToUnixUs(timestamp: int) -> int:
# Convert Windows' GetSystemTimeAsFileTime to Unix timestamp in microseconds
# https://stackoverflow.com/questions/1695288/getting-the-current-time-in-milliseconds-from-the-system-clock-in-windows
timestamp_us = int(timestamp / 10 - 11644473600000000)
if timestamp_us <= 0: timestamp_us = 0
return timestamp_us
def read_uint32(buf: bytes, pos: int) -> int:
"""Reads a single uint32 from buf at a given position"""
assert pos + 4 <= len(buf)
return int.from_bytes(buf[pos:pos + 4], byteorder='little', signed=False)
def read_uint64(buf: bytes, pos: int) -> int:
"""Reads a single uint32 from buf at a given position"""
assert pos + 8 <= len(buf)
return int.from_bytes(buf[pos:pos + 8], byteorder='little', signed=False)
def process_command(buf: bytes, version: int) -> Command:
pos = 0
if version >= 3:
timestamp_us = read_uint64(buf, pos)
pos += 8
else:
timestamp_us = 0
opcode = read_uint32(buf, pos)
pos += 4
size = read_uint32(buf, pos)
pos += 4
return Command(timestamp_us, opcode, size, bytes(buf[pos:]))
def process_stream(file_bytes: mmap, file_pos: int) -> Stream:
# Read the header
file_bytes.seek(file_pos)
header = Header()
header_bytes = file_bytes.read(ctypes.sizeof(header))
ctypes.memmove(ctypes.addressof(header), header_bytes, ctypes.sizeof(header))
if header.signature != b'GFXAPILOG':
return Stream.error(file_pos, error_message="Signature doesn't match")
if header.version < 2:
return Stream.error(
file_pos,
error_message=(
"This script can only process version 2 or later of the graphics API logs, but the "
+ "dump file uses version {} ").format(header.version))
if header.version == 2:
timestamp_us = timestampToUnixUs(int(header.last_written_time))
else:
timestamp_us = int(header.last_written_time)
# Sanity check the size
if header.data_size > 5_000_000:
return Stream.error(
file_pos,
error_message="data size is larger than 5MB. This likely indicates garbage/corrupted " +
"data")
if header.committed_index >= header.data_size:
return Stream.error(
file_pos,
error_message="index is larger than buffer size. Likely indicates garbage/corrupted " +
"data")
file_bytes.seek(file_pos + ctypes.sizeof(header))
data = file_bytes.read(header.data_size)
# Reorder the buffer so that we can read it in a single pass from back to front
buf = data[header.committed_index:] + data[:header.committed_index]
commands = []
i = len(buf)
while i >= 4:
i -= 4
size = read_uint32(buf, i)
if size == 0 or size > i:
# We reached the end of the stream
break
cmd = process_command(buf[i - size:i], header.version)
commands.append(cmd)
i -= size
commands.reverse() # so that they're sorted from oldest to most recent
return Stream(file_pos, timestamp_us, header.thread_id, header.capture_id, commands, None)
def process_minidump(mm: mmap) -> List[Stream]:
"""
Extracts a list of commands streams from a minidump file
"""
streams = []
pos = 0
while True:
pos = mm.find(b'GFXAPILOG', pos)
if pos == -1:
break
streams.append(process_stream(mm, pos))
pos += 1
return streams
def main():
parser = argparse.ArgumentParser(description="""Command line tool to process crash reports and print out the
commands logged by GfxApiLogger""")
parser.add_argument('dump_file', help="Path to minidump file")
args = parser.parse_args()
streams = None
with open(args.dump_file, "r+b") as f:
with mmap.mmap(f.fileno(), 0) as mm:
streams = process_minidump(mm)
streams.sort(key=lambda s: s.timestamp)
total_commands = 0
num_errors = 0
for stream_idx, stream in enumerate(streams):
print(textwrap.dedent("""
=======================================================
GfxApiLog command stream #{} at offset {} in dump
- Timestamp: {}
- Thread id: {}
- Capture id: {}""".format(stream_idx, stream.pos_in_file,
datetime.fromtimestamp(stream.timestamp / 1000000.0),
stream.thread_id,
stream.capture_id)))
if stream.error_message:
print("Could not decode stream. Error: ", stream.error_message)
continue
subdecode_size = 0
for cmd_idx, cmd in enumerate(stream.commands):
total_commands += 1
cmd_printer = command_printer.CommandPrinter(
cmd.opcode, cmd.original_size, cmd.data, cmd.timestamp, stream_idx, cmd_idx)
try:
cmd_printer.print_cmd()
except:
num_errors += 1
# Print stack trace and continue
traceback.print_exc(file=sys.stdout)
if subdecode_size > 0:
subdecode_size -= cmd.original_size
assert subdecode_size >= 0
if subdecode_size == 0:
print("\n--- end of subdecode ---")
if cmd_printer.cmd_name() == "OP_vkQueueFlushCommandsGOOGLE":
assert subdecode_size == 0
subdecode_size = cmd.original_size - 36
print("\n--- start of subdecode, size = {} bytes ---".format(subdecode_size))
print("\nDone: {} commands, {} errors".format(total_commands, num_errors))
if num_errors > 0:
print("""
NOTE: This script uses some simplifying assumptions to decode the commands. All
decoding errors are almost certainly a bug with this script, NOT a sign of bad
or corrupted data.""")
if __name__ == '__main__':
main()