mirror of
https://github.com/scylladb/scylladb.git
synced 2026-06-08 16:03:20 +00:00
scylla-gdb.py: introduce scylla generate_object_graph
When investigating OOM:s a prominent pattern is a size class that is exploded, using up most of the available memory alone. If one is lucky, the objects causing the OOM are instances of some virtual class, making their identification easy. Other times the objects are referenced by instances of some virtual class, allowing their identification with some work. However there are cases where neither these objects nor their direct referrers are instances of virtual classes. This is the case `scylla generate_object_graph` intends to help. scylla generate_object_graph, like its name suggests generates the object graph of the requested object. The object graph is a directed graph, where vertices are objects and edges are references between them, going from referrers to the referee. The vertices contain information, like the address of the object, its size, whether it is a live or not and if applies, the address and symbol name of its vtable. The edges contain the list of offsets the referrer has references at. The generated graph is an image, which allows the visual inspection of the object graph, allowing the developer to notice patterns and hopefully identify the problematic objects. The graph is generated with the help of `graphwiz`. The command generates `.dot` files which can be converted to images with the help of the `dot` utility. The command can do this if the output file is one of the supported image formats (e.g. `png`), otherwise only the `.dot` file is generated, leaving the actual image generation to the user.
This commit is contained in:
159
scylla-gdb.py
159
scylla-gdb.py
@@ -12,6 +12,9 @@ import sys
|
||||
import struct
|
||||
import random
|
||||
import bisect
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def template_arguments(gdb_type):
|
||||
@@ -2554,6 +2557,161 @@ class scylla_memtables(gdb.Command):
|
||||
gdb.write(' (memtable*) 0x%x: total=%d, used=%d, free=%d, flushed=%d\n' % (mt, reg.total(), reg.used(), reg.free(), mt['_flushed_memory']))
|
||||
|
||||
|
||||
class scylla_generate_object_graph(gdb.Command):
|
||||
"""Generate an object graph for an object.
|
||||
|
||||
The object graph is a directed graph, where vertices are objects and edges
|
||||
are references between them, going from referrers to the referee. The
|
||||
vertices contain information, like the address of the object, its size,
|
||||
whether it is a live or not and if applies, the address and symbol name of
|
||||
its vtable. The edges contain the list of offsets the referrer has references
|
||||
at. The generated graph is an image, which allows the visual inspection of the
|
||||
object graph.
|
||||
|
||||
The graph is generated with the help of `graphwiz`. The command
|
||||
generates `.dot` files which can be converted to images with the help of
|
||||
the `dot` utility. The command can do this if the output file is one of
|
||||
the supported image formats (e.g. `png`), otherwise only the `.dot` file
|
||||
is generated, leaving the actual image generation to the user. When that is
|
||||
the case, the generated `.dot` file can be converted to an image with the
|
||||
following command:
|
||||
|
||||
dot -Tpng graph.dot -o graph.png
|
||||
|
||||
The `.dot` file is always generated, regardless of the specified output. This
|
||||
file will contain the full name of vtable symbols. The graph will only contain
|
||||
cropped versions of those to keep the size reasonable.
|
||||
|
||||
See `scylla generate_object_graph --help` for more details on usage.
|
||||
Also see `man dot` for more information on supported output formats.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
gdb.Command.__init__(self, 'scylla generate-object-graph', gdb.COMMAND_USER, gdb.COMPLETE_COMMAND)
|
||||
|
||||
@staticmethod
|
||||
def _traverse_object_graph_breadth_first(address, max_depth, max_vertices, timeout_seconds):
|
||||
vertices = dict() # addr -> obj info (ptr metadata, vtable symbol)
|
||||
edges = defaultdict(set) # (referrer, referee) -> {offset1, offset2...}
|
||||
|
||||
vptr_type = gdb.lookup_type('uintptr_t').pointer()
|
||||
|
||||
current_objects = [address]
|
||||
next_objects = []
|
||||
depth = 0
|
||||
start_time = time.time()
|
||||
stop = False
|
||||
|
||||
while not stop:
|
||||
depth += 1
|
||||
for current_obj in current_objects:
|
||||
for next_obj, next_off in scylla_find.find(current_obj):
|
||||
if timeout_seconds > 0:
|
||||
current_time = time.time()
|
||||
if current_time - start_time > timeout_seconds:
|
||||
stop = True
|
||||
break
|
||||
|
||||
edges[(next_obj, address)].add(next_off)
|
||||
if next_obj in vertices:
|
||||
continue
|
||||
|
||||
ptr_meta = scylla_ptr.analyze(next_obj)
|
||||
symbol_name = resolve(gdb.Value(next_obj).reinterpret_cast(vptr_type).dereference(), cache=False)
|
||||
vertices[next_obj] = (ptr_meta, symbol_name)
|
||||
|
||||
next_objects.append(next_obj)
|
||||
|
||||
if max_vertices > 0 and len(vertices) >= max_vertices:
|
||||
stop = True
|
||||
break;
|
||||
|
||||
if max_depth > 0 and depth == max_depth:
|
||||
stop = True
|
||||
break
|
||||
|
||||
current_objects = next_objects
|
||||
next_objects = []
|
||||
|
||||
return edges, vertices
|
||||
|
||||
@staticmethod
|
||||
def _do_generate_object_graph(address, output_file, max_depth, max_vertices, timeout_seconds):
|
||||
edges, vertices = scylla_generate_object_graph._traverse_object_graph_breadth_first(address, max_depth,
|
||||
max_vertices, timeout_seconds)
|
||||
|
||||
vptr_type = gdb.lookup_type('uintptr_t').pointer()
|
||||
prefix_len = len('vtable for ')
|
||||
vertices[address] = (scylla_ptr.analyze(address),
|
||||
resolve(gdb.Value(address).reinterpret_cast(vptr_type).dereference(), cache=False))
|
||||
|
||||
for addr, obj_info in vertices.items():
|
||||
ptr_meta, vtable_symbol_name = obj_info
|
||||
size = ptr_meta.size
|
||||
state = "L" if ptr_meta.is_live else "F"
|
||||
|
||||
if vtable_symbol_name:
|
||||
symbol_name = vtable_symbol_name[prefix_len:] if len(vtable_symbol_name) > prefix_len else vtable_symbol_name
|
||||
output_file.write('{} [label="0x{:x} ({}, {}) {}"]; // {}\n'.format(addr, addr, size, state,
|
||||
symbol_name[:16], vtable_symbol_name))
|
||||
else:
|
||||
output_file.write('{} [label="0x{:x} ({}, {})"];\n'.format(addr, addr, size, state, ptr_meta))
|
||||
|
||||
for edge, offsets in edges.items():
|
||||
a, b = edge
|
||||
output_file.write('{} -> {} [label="{}"];\n'.format(a, b, offsets))
|
||||
|
||||
@staticmethod
|
||||
def generate_object_graph(address, output_file, max_depth, max_vertices, timeout_seconds):
|
||||
with open(output_file, 'w') as f:
|
||||
f.write('digraph G {\n')
|
||||
scylla_generate_object_graph._do_generate_object_graph(address, f, max_depth, max_vertices, timeout_seconds)
|
||||
f.write('}')
|
||||
|
||||
def invoke(self, arg, from_tty):
|
||||
parser = argparse.ArgumentParser(description="scylla generate-object-graph")
|
||||
parser.add_argument("-o", "--output-file", action="store", type=str, default="graph.dot",
|
||||
help="Output file. Supported extensions are: dot, png, jpg, jpeg, svg and pdf."
|
||||
" Regardless of the extension, a `.dot` file will always be generated."
|
||||
" If the output is one of the graphic formats the command will convert the `.dot` file using the `dot` utility."
|
||||
" In this case the dot utility from the graphwiz suite has to be installed on the machine."
|
||||
" To manually convert the `.dot` file do: `dot -Tpng graph.dot -o graph.png`.")
|
||||
parser.add_argument("-d", "--max-depth", action="store", type=int, default=5,
|
||||
help="Maximum depth to traverse the object graph. Set to -1 for unlimited depth. Default is 5.")
|
||||
parser.add_argument("-v", "--max-vertices", action="store", type=int, default=-1,
|
||||
help="Maximum amount of vertices (objects) to add to the object graph. Set to -1 to unlimited. Default is -1 (unlimited).")
|
||||
parser.add_argument("-t", "--timeout", action="store", type=int, default=-1,
|
||||
help="Maximum amount of seconds to spend building the graph. Set to -1 for no timeout. Default is -1 (unlimited).")
|
||||
parser.add_argument("object", action="store", help="The object that is the starting point of the graph.")
|
||||
|
||||
try:
|
||||
args = parser.parse_args(arg.split())
|
||||
except SystemExit:
|
||||
return
|
||||
|
||||
supported_extensions = {'dot', 'png', 'jpg', 'jpeg', 'svg', 'pdf'}
|
||||
head, tail = os.path.split(args.output_file)
|
||||
filename, extension = tail.split('.')
|
||||
|
||||
if not extension in supported_extensions:
|
||||
raise ValueError("The output file `{}' has unsupported extension `{}'. Supported extensions are: {}".format(
|
||||
args.output_file, extension, supported_extensions))
|
||||
|
||||
if extension != 'dot':
|
||||
dot_file = os.path.join(head, filename + '.dot')
|
||||
else:
|
||||
dot_file = args.output_file
|
||||
|
||||
if args.max_depth == -1 and args.max_vertices == -1 and args.timeout == -1:
|
||||
raise ValueError("The search has to be limited by at least one of: MAX_DEPTH, MAX_VERTICES or TIMEOUT")
|
||||
|
||||
scylla_generate_object_graph.generate_object_graph(int(gdb.parse_and_eval(args.object)), dot_file,
|
||||
args.max_depth, args.max_vertices, args.timeout)
|
||||
|
||||
if extension != 'dot':
|
||||
subprocess.check_call(['dot', '-T' + extension, dot_file, '-o', args.output_file])
|
||||
|
||||
|
||||
class scylla_gdb_func_dereference_lw_shared_ptr(gdb.Function):
|
||||
"""Dereference the pointer guarded by the `seastar::lw_shared_ptr` instance.
|
||||
|
||||
@@ -2661,6 +2819,7 @@ scylla_gms()
|
||||
scylla_cache()
|
||||
scylla_sstables()
|
||||
scylla_memtables()
|
||||
scylla_generate_object_graph()
|
||||
|
||||
|
||||
# Convenience functions
|
||||
|
||||
Reference in New Issue
Block a user