diff --git a/src/Makefile b/src/Makefile index 8b996f28aeee0..989d562aadf99 100644 --- a/src/Makefile +++ b/src/Makefile @@ -45,7 +45,7 @@ SRCS := \ dlload sys init task array dump staticdata toplevel jl_uv datatype \ simplevector runtime_intrinsics precompile jloptions \ threading partr stackwalk gc gc-debug gc-pages gc-stacks gc-alloc-profiler method \ - jlapi signal-handling safepoint timing subtype rtutils \ + jlapi signal-handling safepoint timing subtype rtutils gc-heap-snapshot \ crc32c APInt-C processor ircode opaque_closure codegen-stubs coverage runtime_ccall RT_LLVMLINK := @@ -293,7 +293,9 @@ $(BUILDDIR)/disasm.o $(BUILDDIR)/disasm.dbg.obj: $(SRCDIR)/debuginfo.h $(SRCDIR) $(BUILDDIR)/dump.o $(BUILDDIR)/dump.dbg.obj: $(addprefix $(SRCDIR)/,common_symbols1.inc common_symbols2.inc builtin_proto.h serialize.h) $(BUILDDIR)/gc-debug.o $(BUILDDIR)/gc-debug.dbg.obj: $(SRCDIR)/gc.h $(BUILDDIR)/gc-pages.o $(BUILDDIR)/gc-pages.dbg.obj: $(SRCDIR)/gc.h -$(BUILDDIR)/gc.o $(BUILDDIR)/gc.dbg.obj: $(SRCDIR)/gc.h $(SRCDIR)/gc-alloc-profiler.h +$(BUILDDIR)/gc.o $(BUILDDIR)/gc.dbg.obj: $(SRCDIR)/gc.h $(SRCDIR)/gc-heap-snapshot.h $(SRCDIR)/gc-alloc-profiler.h +$(BUILDDIR)/gc-heap-snapshot.o $(BUILDDIR)/gc-heap-snapshot.dbg.obj: $(SRCDIR)/gc.h $(SRCDIR)/gc-heap-snapshot.h +$(BUILDDIR)/gc-alloc-profiler.o $(BUILDDIR)/gc-alloc-profiler.dbg.obj: $(SRCDIR)/gc.h $(SRCDIR)/gc-alloc-profiler.h $(BUILDDIR)/init.o $(BUILDDIR)/init.dbg.obj: $(SRCDIR)/builtin_proto.h $(BUILDDIR)/interpreter.o $(BUILDDIR)/interpreter.dbg.obj: $(SRCDIR)/builtin_proto.h $(BUILDDIR)/jitlayers.o $(BUILDDIR)/jitlayers.dbg.obj: $(SRCDIR)/jitlayers.h $(SRCDIR)/codegen_shared.h $(SRCDIR)/debug-registry.h diff --git a/src/gc-debug.c b/src/gc-debug.c index 5d42da196ccf8..040502c805448 100644 --- a/src/gc-debug.c +++ b/src/gc-debug.c @@ -1227,20 +1227,17 @@ void gc_count_pool(void) jl_safe_printf("************************\n"); } -int gc_slot_to_fieldidx(void *obj, void *slot) +int gc_slot_to_fieldidx(void *obj, void *slot, jl_datatype_t *vt) JL_NOTSAFEPOINT { - jl_datatype_t *vt = (jl_datatype_t*)jl_typeof(obj); int nf = (int)jl_datatype_nfields(vt); - for (int i = 0; i < nf; i++) { - void *fieldaddr = (char*)obj + jl_field_offset(vt, i); - if (fieldaddr >= slot) { - return i; - } + for (int i = 1; i < nf; i++) { + if (slot < (void*)((char*)obj + jl_field_offset(vt, i))) + return i - 1; } - return -1; + return nf - 1; } -int gc_slot_to_arrayidx(void *obj, void *_slot) +int gc_slot_to_arrayidx(void *obj, void *_slot) JL_NOTSAFEPOINT { char *slot = (char*)_slot; jl_datatype_t *vt = (jl_datatype_t*)jl_typeof(obj); @@ -1258,8 +1255,6 @@ int gc_slot_to_arrayidx(void *obj, void *_slot) } else if (vt->name == jl_array_typename) { jl_array_t *a = (jl_array_t*)obj; - if (!a->flags.ptrarray) - return -1; start = (char*)a->data; len = jl_array_len(a); elsize = a->elsize; diff --git a/src/gc-heap-snapshot.cpp b/src/gc-heap-snapshot.cpp new file mode 100644 index 0000000000000..2349a33cebae3 --- /dev/null +++ b/src/gc-heap-snapshot.cpp @@ -0,0 +1,506 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "gc-heap-snapshot.h" + +#include "julia_internal.h" +#include "gc.h" + +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/DenseMap.h" + +#include +#include +#include + +using std::vector; +using std::string; +using std::ostringstream; +using std::pair; +using std::make_pair; +using llvm::StringMap; +using llvm::DenseMap; +using llvm::StringRef; + +// https://stackoverflow.com/a/33799784/751061 +void print_str_escape_json(ios_t *stream, StringRef s) +{ + ios_printf(stream, "\""); + for (auto c = s.begin(); c != s.end(); c++) { + switch (*c) { + case '"': ios_printf(stream, "\\\""); break; + case '\\': ios_printf(stream, "\\\\"); break; + case '\b': ios_printf(stream, "\\b"); break; + case '\f': ios_printf(stream, "\\f"); break; + case '\n': ios_printf(stream, "\\n"); break; + case '\r': ios_printf(stream, "\\r"); break; + case '\t': ios_printf(stream, "\\t"); break; + default: + if ('\x00' <= *c && *c <= '\x1f') { + ios_printf(stream, "\\u%04x", (int)*c); + } + else { + ios_printf(stream, "%c", *c); + } + } + } + ios_printf(stream, "\""); +} + + +// Edges +// "edge_fields": +// [ "type", "name_or_index", "to_node" ] +// mimicking https://github.com/nodejs/node/blob/5fd7a72e1c4fbaf37d3723c4c81dce35c149dc84/deps/v8/src/profiler/heap-snapshot-generator.cc#L2598-L2601 + +struct Edge { + size_t type; // These *must* match the Enums on the JS side; control interpretation of name_or_index. + size_t name_or_index; // name of the field (for objects/modules) or index of array + size_t to_node; +}; + +// Nodes +// "node_fields": +// [ "type", "name", "id", "self_size", "edge_count", "trace_node_id", "detachedness" ] +// mimicking https://github.com/nodejs/node/blob/5fd7a72e1c4fbaf37d3723c4c81dce35c149dc84/deps/v8/src/profiler/heap-snapshot-generator.cc#L2568-L2575 + +const int k_node_number_of_fields = 7; +struct Node { + size_t type; // index into snapshot->node_types + size_t name; + size_t id; // This should be a globally-unique counter, but we use the memory address + size_t self_size; + size_t trace_node_id; // This is ALWAYS 0 in Javascript heap-snapshots. + // whether the from_node is attached or dettached from the main application state + // https://github.com/nodejs/node/blob/5fd7a72e1c4fbaf37d3723c4c81dce35c149dc84/deps/v8/include/v8-profiler.h#L739-L745 + int detachedness; // 0 - unknown, 1 - attached, 2 - detached + vector edges; + + ~Node() JL_NOTSAFEPOINT = default; +}; + +struct StringTable { + StringMap map; + vector strings; + + size_t find_or_create_string_id(StringRef key) JL_NOTSAFEPOINT { + auto val = map.insert(make_pair(key, map.size())); + if (val.second) + strings.push_back(val.first->first()); + return val.first->second; + } + + void print_json_array(ios_t *stream, bool newlines) { + ios_printf(stream, "["); + bool first = true; + for (const auto &str : strings) { + if (first) { + first = false; + } + else { + ios_printf(stream, newlines ? ",\n" : ","); + } + print_str_escape_json(stream, str); + } + ios_printf(stream, "]"); + } +}; + +struct HeapSnapshot { + vector nodes; + // edges are stored on each from_node + + StringTable names; + StringTable node_types; + StringTable edge_types; + DenseMap node_ptr_to_index_map; + + size_t num_edges = 0; // For metadata, updated as you add each edge. Needed because edges owned by nodes. +}; + +// global heap snapshot, mutated by garbage collector +// when snapshotting is on. +int gc_heap_snapshot_enabled = 0; +HeapSnapshot *g_snapshot = nullptr; +extern jl_mutex_t heapsnapshot_lock; + +void serialize_heap_snapshot(ios_t *stream, HeapSnapshot &snapshot, char all_one); +static inline void _record_gc_edge(const char *node_type, const char *edge_type, + jl_value_t *a, jl_value_t *b, size_t name_or_index) JL_NOTSAFEPOINT; +void _record_gc_just_edge(const char *edge_type, Node &from_node, size_t to_idx, size_t name_or_idx) JL_NOTSAFEPOINT; +void _add_internal_root(HeapSnapshot *snapshot); + + +JL_DLLEXPORT void jl_gc_take_heap_snapshot(ios_t *stream, char all_one) +{ + HeapSnapshot snapshot; + _add_internal_root(&snapshot); + + jl_mutex_lock(&heapsnapshot_lock); + + // Enable snapshotting + g_snapshot = &snapshot; + gc_heap_snapshot_enabled = true; + + // Do a full GC mark (and incremental sweep), which will invoke our callbacks on `g_snapshot` + jl_gc_collect(JL_GC_FULL); + + // Disable snapshotting + gc_heap_snapshot_enabled = false; + g_snapshot = nullptr; + + jl_mutex_unlock(&heapsnapshot_lock); + + // When we return, the snapshot is full + // Dump the snapshot + serialize_heap_snapshot((ios_t*)stream, snapshot, all_one); +} + +// adds a node at id 0 which is the "uber root": +// a synthetic node which points to all the GC roots. +void _add_internal_root(HeapSnapshot *snapshot) +{ + Node internal_root{ + snapshot->node_types.find_or_create_string_id("synthetic"), + snapshot->names.find_or_create_string_id(""), // name + 0, // id + 0, // size + 0, // size_t trace_node_id (unused) + 0, // int detachedness; // 0 - unknown, 1 - attached; 2 - detached + vector() // outgoing edges + }; + snapshot->nodes.push_back(internal_root); +} + +// mimicking https://github.com/nodejs/node/blob/5fd7a72e1c4fbaf37d3723c4c81dce35c149dc84/deps/v8/src/profiler/heap-snapshot-generator.cc#L597-L597 +// returns the index of the new node +size_t record_node_to_gc_snapshot(jl_value_t *a) JL_NOTSAFEPOINT +{ + auto val = g_snapshot->node_ptr_to_index_map.insert(make_pair(a, g_snapshot->nodes.size())); + if (!val.second) { + return val.first->second; + } + + ios_t str_; + bool ios_need_close = 0; + + // Insert a new Node + size_t self_size = 0; + StringRef name = ""; + StringRef node_type = "object"; + + jl_datatype_t *type = (jl_datatype_t*)jl_typeof(a); + + if (jl_is_string(a)) { + node_type = "string"; + name = jl_string_data(a); + self_size = jl_string_len(a); + } + else if (jl_is_symbol(a)) { + node_type = "symbol"; + name = jl_symbol_name((jl_sym_t*)a); + self_size = name.size(); + } + else if (jl_is_simplevector(a)) { + node_type = "array"; + name = "SimpleVector"; + self_size = sizeof(jl_svec_t) + sizeof(void*) * jl_svec_len(a); + } + else if (jl_is_module(a)) { + name = "Module"; + self_size = sizeof(jl_module_t); + } + else if (jl_is_task(a)) { + name = "Task"; + self_size = sizeof(jl_task_t); + } + else { + self_size = jl_is_array_type(type) + ? sizeof(jl_array_t) + : (size_t)jl_datatype_size(type); + + // print full type into ios buffer and get StringRef to it. + // The ios is cleaned up below. + ios_need_close = 1; + ios_mem(&str_, 0); + JL_STREAM* str = (JL_STREAM*)&str_; + jl_static_show(str, (jl_value_t*)type); + + name = StringRef((const char*)str_.buf, str_.size); + } + + g_snapshot->nodes.push_back(Node{ + g_snapshot->node_types.find_or_create_string_id(node_type), // size_t type; + g_snapshot->names.find_or_create_string_id(name), // size_t name; + (size_t)a, // size_t id; + // We add 1 to self-size for the type tag that all heap-allocated objects have. + // Also because the Chrome Snapshot viewer ignores size-0 leaves! + sizeof(void*) + self_size, // size_t self_size; + 0, // size_t trace_node_id (unused) + 0, // int detachedness; // 0 - unknown, 1 - attached; 2 - detached + vector() // outgoing edges + }); + + if (ios_need_close) + ios_close(&str_); + + return val.first->second; +} + +static size_t record_pointer_to_gc_snapshot(void *a, size_t bytes, StringRef name) JL_NOTSAFEPOINT +{ + auto val = g_snapshot->node_ptr_to_index_map.insert(make_pair(a, g_snapshot->nodes.size())); + if (!val.second) { + return val.first->second; + } + + g_snapshot->nodes.push_back(Node{ + g_snapshot->node_types.find_or_create_string_id( "object"), // size_t type; + g_snapshot->names.find_or_create_string_id(name), // size_t name; + (size_t)a, // size_t id; + bytes, // size_t self_size; + 0, // size_t trace_node_id (unused) + 0, // int detachedness; // 0 - unknown, 1 - attached; 2 - detached + vector() // outgoing edges + }); + + return val.first->second; +} + +static string _fieldpath_for_slot(void *obj, void *slot) JL_NOTSAFEPOINT +{ + string res; + jl_datatype_t *objtype = (jl_datatype_t*)jl_typeof(obj); + + while (1) { + int i = gc_slot_to_fieldidx(obj, slot, objtype); + + if (jl_is_tuple_type(objtype) || jl_is_namedtuple_type(objtype)) { + ostringstream ss; + ss << "[" << i << "]"; + res += ss.str(); + } + else { + jl_svec_t *field_names = jl_field_names(objtype); + jl_sym_t *name = (jl_sym_t*)jl_svecref(field_names, i); + res += jl_symbol_name(name); + } + + if (!jl_field_isptr(objtype, i)) { + // Tail recurse + res += "."; + obj = (void*)((char*)obj + jl_field_offset(objtype, i)); + objtype = (jl_datatype_t*)jl_field_type_concrete(objtype, i); + } + else { + return res; + } + } +} + + +void _gc_heap_snapshot_record_root(jl_value_t *root, char *name) JL_NOTSAFEPOINT +{ + record_node_to_gc_snapshot(root); + + auto &internal_root = g_snapshot->nodes.front(); + auto to_node_idx = g_snapshot->node_ptr_to_index_map[root]; + auto edge_label = g_snapshot->names.find_or_create_string_id(name); + + _record_gc_just_edge("internal", internal_root, to_node_idx, edge_label); +} + +// Add a node to the heap snapshot representing a Julia stack frame. +// Each task points at a stack frame, which points at the stack frame of +// the function it's currently calling, forming a linked list. +// Stack frame nodes point at the objects they have as local variables. +size_t _record_stack_frame_node(HeapSnapshot *snapshot, void *frame) JL_NOTSAFEPOINT +{ + auto val = g_snapshot->node_ptr_to_index_map.insert(make_pair(frame, g_snapshot->nodes.size())); + if (!val.second) { + return val.first->second; + } + + snapshot->nodes.push_back(Node{ + snapshot->node_types.find_or_create_string_id("synthetic"), + snapshot->names.find_or_create_string_id("(stack frame)"), // name + (size_t)frame, // id + 1, // size + 0, // size_t trace_node_id (unused) + 0, // int detachedness; // 0 - unknown, 1 - attached; 2 - detached + vector() // outgoing edges + }); + + return val.first->second; +} + +void _gc_heap_snapshot_record_frame_to_object_edge(void *from, jl_value_t *to) JL_NOTSAFEPOINT +{ + auto from_node_idx = _record_stack_frame_node(g_snapshot, (jl_gcframe_t*)from); + auto to_idx = record_node_to_gc_snapshot(to); + Node &from_node = g_snapshot->nodes[from_node_idx]; + + auto name_idx = g_snapshot->names.find_or_create_string_id("local var"); + _record_gc_just_edge("internal", from_node, to_idx, name_idx); +} + +void _gc_heap_snapshot_record_task_to_frame_edge(jl_task_t *from, void *to) JL_NOTSAFEPOINT +{ + auto from_node_idx = record_node_to_gc_snapshot((jl_value_t*)from); + auto to_node_idx = _record_stack_frame_node(g_snapshot, to); + Node &from_node = g_snapshot->nodes[from_node_idx]; + + auto name_idx = g_snapshot->names.find_or_create_string_id("stack"); + _record_gc_just_edge("internal", from_node, to_node_idx, name_idx); +} + +void _gc_heap_snapshot_record_frame_to_frame_edge(jl_gcframe_t *from, jl_gcframe_t *to) JL_NOTSAFEPOINT +{ + auto from_node_idx = _record_stack_frame_node(g_snapshot, from); + auto to_node_idx = _record_stack_frame_node(g_snapshot, to); + Node &from_node = g_snapshot->nodes[from_node_idx]; + + auto name_idx = g_snapshot->names.find_or_create_string_id("next frame"); + _record_gc_just_edge("internal", from_node, to_node_idx, name_idx); +} + +void _gc_heap_snapshot_record_array_edge(jl_value_t *from, jl_value_t *to, size_t index) JL_NOTSAFEPOINT +{ + _record_gc_edge("array", "element", from, to, index); +} + +void _gc_heap_snapshot_record_object_edge(jl_value_t *from, jl_value_t *to, void *slot) JL_NOTSAFEPOINT +{ + string path = _fieldpath_for_slot(from, slot); + _record_gc_edge("object", "property", from, to, + g_snapshot->names.find_or_create_string_id(path)); +} + +void _gc_heap_snapshot_record_module_to_binding(jl_module_t* module, jl_binding_t* binding) JL_NOTSAFEPOINT +{ + auto from_node_idx = record_node_to_gc_snapshot((jl_value_t*)module); + auto to_node_idx = record_pointer_to_gc_snapshot(binding, sizeof(jl_binding_t), jl_symbol_name(binding->name)); + + jl_value_t *value = jl_atomic_load_relaxed(&binding->value); + auto value_idx = value ? record_node_to_gc_snapshot(value) : 0; + jl_value_t *ty = jl_atomic_load_relaxed(&binding->ty); + auto ty_idx = ty ? record_node_to_gc_snapshot(ty) : 0; + jl_value_t *globalref = jl_atomic_load_relaxed(&binding->globalref); + auto globalref_idx = globalref ? record_node_to_gc_snapshot(globalref) : 0; + + auto &from_node = g_snapshot->nodes[from_node_idx]; + auto &to_node = g_snapshot->nodes[to_node_idx]; + from_node.type = g_snapshot->node_types.find_or_create_string_id("object"); + + _record_gc_just_edge("property", from_node, to_node_idx, g_snapshot->names.find_or_create_string_id("")); + if (value_idx) _record_gc_just_edge("internal", to_node, value_idx, g_snapshot->names.find_or_create_string_id("value")); + if (ty_idx) _record_gc_just_edge("internal", to_node, ty_idx, g_snapshot->names.find_or_create_string_id("ty")); + if (globalref_idx) _record_gc_just_edge("internal", to_node, globalref_idx, g_snapshot->names.find_or_create_string_id("globalref")); +} + +void _gc_heap_snapshot_record_internal_array_edge(jl_value_t *from, jl_value_t *to) JL_NOTSAFEPOINT +{ + _record_gc_edge("object", "internal", from, to, + g_snapshot->names.find_or_create_string_id("")); +} + +void _gc_heap_snapshot_record_hidden_edge(jl_value_t *from, void* to, size_t bytes) JL_NOTSAFEPOINT +{ + size_t name_or_idx = g_snapshot->names.find_or_create_string_id(""); + + auto from_node_idx = record_node_to_gc_snapshot(from); + auto to_node_idx = record_pointer_to_gc_snapshot(to, bytes, ""); + + auto &from_node = g_snapshot->nodes[from_node_idx]; + from_node.type = g_snapshot->node_types.find_or_create_string_id("native"); + + _record_gc_just_edge("hidden", from_node, to_node_idx, name_or_idx); +} + +static inline void _record_gc_edge(const char *node_type, const char *edge_type, + jl_value_t *a, jl_value_t *b, size_t name_or_idx) JL_NOTSAFEPOINT +{ + auto from_node_idx = record_node_to_gc_snapshot(a); + auto to_node_idx = record_node_to_gc_snapshot(b); + + auto &from_node = g_snapshot->nodes[from_node_idx]; + from_node.type = g_snapshot->node_types.find_or_create_string_id(node_type); + + _record_gc_just_edge(edge_type, from_node, to_node_idx, name_or_idx); +} + +void _record_gc_just_edge(const char *edge_type, Node &from_node, size_t to_idx, size_t name_or_idx) JL_NOTSAFEPOINT +{ + from_node.edges.push_back(Edge{ + g_snapshot->edge_types.find_or_create_string_id(edge_type), + name_or_idx, // edge label + to_idx // to + }); + + g_snapshot->num_edges += 1; +} + +void serialize_heap_snapshot(ios_t *stream, HeapSnapshot &snapshot, char all_one) +{ + // mimicking https://github.com/nodejs/node/blob/5fd7a72e1c4fbaf37d3723c4c81dce35c149dc84/deps/v8/src/profiler/heap-snapshot-generator.cc#L2567-L2567 + ios_printf(stream, "{\"snapshot\":{"); + ios_printf(stream, "\"meta\":{"); + ios_printf(stream, "\"node_fields\":[\"type\",\"name\",\"id\",\"self_size\",\"edge_count\",\"trace_node_id\",\"detachedness\"],"); + ios_printf(stream, "\"node_types\":["); + snapshot.node_types.print_json_array(stream, false); + ios_printf(stream, ","); + ios_printf(stream, "\"string\", \"number\", \"number\", \"number\", \"number\", \"number\"],"); + ios_printf(stream, "\"edge_fields\":[\"type\",\"name_or_index\",\"to_node\"],"); + ios_printf(stream, "\"edge_types\":["); + snapshot.edge_types.print_json_array(stream, false); + ios_printf(stream, ","); + ios_printf(stream, "\"string_or_number\",\"from_node\"]"); + ios_printf(stream, "},\n"); // end "meta" + ios_printf(stream, "\"node_count\":%zu,", snapshot.nodes.size()); + ios_printf(stream, "\"edge_count\":%zu", snapshot.num_edges); + ios_printf(stream, "},\n"); // end "snapshot" + + ios_printf(stream, "\"nodes\":["); + bool first_node = true; + for (const auto &from_node : snapshot.nodes) { + if (first_node) { + first_node = false; + } + else { + ios_printf(stream, ","); + } + // ["type","name","id","self_size","edge_count","trace_node_id","detachedness"] + ios_printf(stream, "%zu", from_node.type); + ios_printf(stream, ",%zu", from_node.name); + ios_printf(stream, ",%zu", from_node.id); + ios_printf(stream, ",%zu", all_one ? (size_t)1 : from_node.self_size); + ios_printf(stream, ",%zu", from_node.edges.size()); + ios_printf(stream, ",%zu", from_node.trace_node_id); + ios_printf(stream, ",%d", from_node.detachedness); + ios_printf(stream, "\n"); + } + ios_printf(stream, "],\n"); + + ios_printf(stream, "\"edges\":["); + bool first_edge = true; + for (const auto &from_node : snapshot.nodes) { + for (const auto &edge : from_node.edges) { + if (first_edge) { + first_edge = false; + } + else { + ios_printf(stream, ","); + } + ios_printf(stream, "%zu", edge.type); + ios_printf(stream, ",%zu", edge.name_or_index); + ios_printf(stream, ",%zu", edge.to_node * k_node_number_of_fields); + ios_printf(stream, "\n"); + } + } + ios_printf(stream, "],\n"); // end "edges" + + ios_printf(stream, "\"strings\":"); + + snapshot.names.print_json_array(stream, true); + + ios_printf(stream, "}"); +} diff --git a/src/gc-heap-snapshot.h b/src/gc-heap-snapshot.h new file mode 100644 index 0000000000000..96cdaf6a9a866 --- /dev/null +++ b/src/gc-heap-snapshot.h @@ -0,0 +1,108 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#ifndef JL_GC_HEAP_SNAPSHOT_H +#define JL_GC_HEAP_SNAPSHOT_H + +#include "julia.h" +#include "ios.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +// --------------------------------------------------------------------- +// Functions to call from GC when heap snapshot is enabled +// --------------------------------------------------------------------- +void _gc_heap_snapshot_record_root(jl_value_t *root, char *name) JL_NOTSAFEPOINT; +void _gc_heap_snapshot_record_frame_to_object_edge(void *from, jl_value_t *to) JL_NOTSAFEPOINT; +void _gc_heap_snapshot_record_task_to_frame_edge(jl_task_t *from, void *to) JL_NOTSAFEPOINT; +void _gc_heap_snapshot_record_frame_to_frame_edge(jl_gcframe_t *from, jl_gcframe_t *to) JL_NOTSAFEPOINT; +void _gc_heap_snapshot_record_array_edge(jl_value_t *from, jl_value_t *to, size_t index) JL_NOTSAFEPOINT; +void _gc_heap_snapshot_record_object_edge(jl_value_t *from, jl_value_t *to, void* slot) JL_NOTSAFEPOINT; +void _gc_heap_snapshot_record_module_to_binding(jl_module_t* module, jl_binding_t* binding) JL_NOTSAFEPOINT; +// Used for objects managed by GC, but which aren't exposed in the julia object, so have no +// field or index. i.e. they're not reacahable from julia code, but we _will_ hit them in +// the GC mark phase (so we can check their type tag to get the size). +void _gc_heap_snapshot_record_internal_array_edge(jl_value_t *from, jl_value_t *to) JL_NOTSAFEPOINT; +// Used for objects manually allocated in C (outside julia GC), to still tell the heap snapshot about the +// size of the object, even though we're never going to mark that object. +void _gc_heap_snapshot_record_hidden_edge(jl_value_t *from, void* to, size_t bytes) JL_NOTSAFEPOINT; + + +extern int gc_heap_snapshot_enabled; +extern int prev_sweep_full; + +int gc_slot_to_fieldidx(void *_obj, void *slot, jl_datatype_t *vt) JL_NOTSAFEPOINT; +int gc_slot_to_arrayidx(void *_obj, void *begin) JL_NOTSAFEPOINT; + +static inline void gc_heap_snapshot_record_frame_to_object_edge(void *from, jl_value_t *to) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_frame_to_object_edge(from, to); + } +} +static inline void gc_heap_snapshot_record_task_to_frame_edge(jl_task_t *from, void *to) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_task_to_frame_edge(from, to); + } +} +static inline void gc_heap_snapshot_record_frame_to_frame_edge(jl_gcframe_t *from, jl_gcframe_t *to) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_frame_to_frame_edge(from, to); + } +} +static inline void gc_heap_snapshot_record_root(jl_value_t *root, char *name) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_root(root, name); + } +} +static inline void gc_heap_snapshot_record_array_edge(jl_value_t *from, jl_value_t **to) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_array_edge(from, *to, gc_slot_to_arrayidx(from, to)); + } +} +static inline void gc_heap_snapshot_record_object_edge(jl_value_t *from, jl_value_t **to) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_object_edge(from, *to, to); + } +} + +static inline void gc_heap_snapshot_record_module_to_binding(jl_module_t* module, jl_binding_t* binding) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_module_to_binding(module, binding); + } +} + +static inline void gc_heap_snapshot_record_internal_array_edge(jl_value_t *from, jl_value_t *to) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_internal_array_edge(from, to); + } +} + +static inline void gc_heap_snapshot_record_hidden_edge(jl_value_t *from, void* to, size_t bytes) JL_NOTSAFEPOINT +{ + if (__unlikely(gc_heap_snapshot_enabled && prev_sweep_full)) { + _gc_heap_snapshot_record_hidden_edge(from, to, bytes); + } +} + +// --------------------------------------------------------------------- +// Functions to call from Julia to take heap snapshot +// --------------------------------------------------------------------- +JL_DLLEXPORT void jl_gc_take_heap_snapshot(ios_t *stream, char all_one); + + +#ifdef __cplusplus +} +#endif + + +#endif // JL_GC_HEAP_SNAPSHOT_H diff --git a/src/gc.c b/src/gc.c index 4cd692fb66f2f..fd88634383aad 100644 --- a/src/gc.c +++ b/src/gc.c @@ -130,6 +130,9 @@ STATIC_INLINE void import_gc_state(jl_ptls_t ptls, jl_gc_mark_sp_t *sp) { static jl_mutex_t finalizers_lock; static uv_mutex_t gc_cache_lock; +// mutex for gc-heap-snapshot. +jl_mutex_t heapsnapshot_lock; + // Flag that tells us whether we need to support conservative marking // of objects. static _Atomic(int) support_conservative_marking = 0; @@ -679,7 +682,7 @@ static int mark_reset_age = 0; static int64_t scanned_bytes; // young bytes scanned while marking static int64_t perm_scanned_bytes; // old bytes scanned while marking -static int prev_sweep_full = 1; +int prev_sweep_full = 1; #define inc_sat(v,s) v = (v) >= s ? s : (v)+1 @@ -1885,9 +1888,11 @@ STATIC_INLINE int gc_mark_scan_objarray(jl_ptls_t ptls, jl_gc_mark_sp_t *sp, (void)jl_assume(objary == (gc_mark_objarray_t*)sp->data); for (; begin < end; begin += objary->step) { *pnew_obj = *begin; - if (*pnew_obj) + if (*pnew_obj) { verify_parent2("obj array", objary->parent, begin, "elem(%d)", gc_slot_to_arrayidx(objary->parent, begin)); + gc_heap_snapshot_record_array_edge(objary->parent, begin); + } if (!gc_try_setmark(*pnew_obj, &objary->nptr, ptag, pbits)) continue; begin += objary->step; @@ -1921,9 +1926,11 @@ STATIC_INLINE int gc_mark_scan_array8(jl_ptls_t ptls, jl_gc_mark_sp_t *sp, for (; elem_begin < elem_end; elem_begin++) { jl_value_t **slot = &begin[*elem_begin]; *pnew_obj = *slot; - if (*pnew_obj) + if (*pnew_obj) { verify_parent2("array", ary8->elem.parent, slot, "elem(%d)", gc_slot_to_arrayidx(ary8->elem.parent, begin)); + gc_heap_snapshot_record_array_edge(ary8->elem.parent, slot); + } if (!gc_try_setmark(*pnew_obj, &ary8->elem.nptr, ptag, pbits)) continue; elem_begin++; @@ -1969,9 +1976,11 @@ STATIC_INLINE int gc_mark_scan_array16(jl_ptls_t ptls, jl_gc_mark_sp_t *sp, for (; elem_begin < elem_end; elem_begin++) { jl_value_t **slot = &begin[*elem_begin]; *pnew_obj = *slot; - if (*pnew_obj) + if (*pnew_obj) { verify_parent2("array", ary16->elem.parent, slot, "elem(%d)", gc_slot_to_arrayidx(ary16->elem.parent, begin)); + gc_heap_snapshot_record_array_edge(ary16->elem.parent, slot); + } if (!gc_try_setmark(*pnew_obj, &ary16->elem.nptr, ptag, pbits)) continue; elem_begin++; @@ -2015,9 +2024,11 @@ STATIC_INLINE int gc_mark_scan_obj8(jl_ptls_t ptls, jl_gc_mark_sp_t *sp, gc_mark for (; begin < end; begin++) { jl_value_t **slot = &((jl_value_t**)parent)[*begin]; *pnew_obj = *slot; - if (*pnew_obj) + if (*pnew_obj) { verify_parent2("object", parent, slot, "field(%d)", - gc_slot_to_fieldidx(parent, slot)); + gc_slot_to_fieldidx(parent, slot, (jl_datatype_t*)jl_typeof(parent))); + gc_heap_snapshot_record_object_edge((jl_value_t*)parent, slot); + } if (!gc_try_setmark(*pnew_obj, &obj8->nptr, ptag, pbits)) continue; begin++; @@ -2048,9 +2059,11 @@ STATIC_INLINE int gc_mark_scan_obj16(jl_ptls_t ptls, jl_gc_mark_sp_t *sp, gc_mar for (; begin < end; begin++) { jl_value_t **slot = &((jl_value_t**)parent)[*begin]; *pnew_obj = *slot; - if (*pnew_obj) + if (*pnew_obj) { verify_parent2("object", parent, slot, "field(%d)", - gc_slot_to_fieldidx(parent, slot)); + gc_slot_to_fieldidx(parent, slot, (jl_datatype_t*)jl_typeof(parent))); + gc_heap_snapshot_record_object_edge((jl_value_t*)parent, slot); + } if (!gc_try_setmark(*pnew_obj, &obj16->nptr, ptag, pbits)) continue; begin++; @@ -2081,9 +2094,11 @@ STATIC_INLINE int gc_mark_scan_obj32(jl_ptls_t ptls, jl_gc_mark_sp_t *sp, gc_mar for (; begin < end; begin++) { jl_value_t **slot = &((jl_value_t**)parent)[*begin]; *pnew_obj = *slot; - if (*pnew_obj) + if (*pnew_obj) { verify_parent2("object", parent, slot, "field(%d)", - gc_slot_to_fieldidx(parent, slot)); + gc_slot_to_fieldidx(parent, slot, (jl_datatype_t*)jl_typeof(parent))); + gc_heap_snapshot_record_object_edge((jl_value_t*)parent, slot); + } if (!gc_try_setmark(*pnew_obj, &obj32->nptr, ptag, pbits)) continue; begin++; @@ -2370,13 +2385,16 @@ stack: { } if (!gc_try_setmark(new_obj, &nptr, &tag, &bits)) continue; + gc_heap_snapshot_record_frame_to_object_edge(s, new_obj); i++; if (i < nr) { // Haven't done with this one yet. Update the content and push it back stack->i = i; gc_repush_markdata(&sp, gc_mark_stackframe_t); } + // TODO stack addresses needs copy stack handling else if ((s = (jl_gcframe_t*)gc_read_stack(&s->prev, offset, lb, ub))) { + gc_heap_snapshot_record_frame_to_frame_edge(stack->s, s); stack->s = s; stack->i = 0; uintptr_t new_nroots = gc_read_stack(&s->nroots, offset, lb, ub); @@ -2387,7 +2405,9 @@ stack: { goto mark; } s = (jl_gcframe_t*)gc_read_stack(&s->prev, offset, lb, ub); + // walk up one stack frame if (s != 0) { + gc_heap_snapshot_record_frame_to_frame_edge(stack->s, s); stack->s = s; i = 0; uintptr_t new_nroots = gc_read_stack(&s->nroots, offset, lb, ub); @@ -2419,6 +2439,7 @@ excstack: { size_t njlvals = jl_bt_num_jlvals(bt_entry); while (jlval_index < njlvals) { new_obj = jl_bt_entry_jlvalue(bt_entry, jlval_index); + gc_heap_snapshot_record_frame_to_object_edge(bt_entry, new_obj); uintptr_t nptr = 0; jlval_index += 1; if (gc_try_setmark(new_obj, &nptr, &tag, &bits)) { @@ -2433,6 +2454,7 @@ excstack: { } // The exception comes last - mark it new_obj = jl_excstack_exception(excstack, itr); + gc_heap_snapshot_record_frame_to_object_edge(excstack, new_obj); itr = jl_excstack_next(excstack, itr); bt_index = 0; jlval_index = 0; @@ -2471,6 +2493,8 @@ module_binding: { } void *vb = jl_astaggedvalue(b); verify_parent1("module", binding->parent, &vb, "binding_buff"); + // Record the size used for the box for non-const bindings + gc_heap_snapshot_record_module_to_binding(binding->parent, b); (void)vb; jl_value_t *value = jl_atomic_load_relaxed(&b->value); jl_value_t *globalref = jl_atomic_load_relaxed(&b->globalref); @@ -2608,6 +2632,7 @@ mark: { if (flags.how == 1) { void *val_buf = jl_astaggedvalue((char*)a->data - a->offset * a->elsize); verify_parent1("array", new_obj, &val_buf, "buffer ('loc' addr is meaningless)"); + gc_heap_snapshot_record_hidden_edge(new_obj, jl_valueof(val_buf), jl_array_nbytes(a)); (void)val_buf; gc_setmark_buf_(ptls, (char*)a->data - a->offset * a->elsize, bits, jl_array_nbytes(a)); @@ -2616,6 +2641,7 @@ mark: { if (update_meta || foreign_alloc) { objprofile_count(jl_malloc_tag, bits == GC_OLD_MARKED, jl_array_nbytes(a)); + gc_heap_snapshot_record_hidden_edge(new_obj, a->data, jl_array_nbytes(a)); if (bits == GC_OLD_MARKED) { ptls->gc_cache.perm_scanned_bytes += jl_array_nbytes(a); } @@ -2627,6 +2653,7 @@ mark: { else if (flags.how == 3) { jl_value_t *owner = jl_array_data_owner(a); uintptr_t nptr = (1 << 2) | (bits & GC_OLD); + gc_heap_snapshot_record_internal_array_edge(new_obj, owner); int markowner = gc_try_setmark(owner, &nptr, &tag, &bits); gc_mark_push_remset(ptls, new_obj, nptr); if (markowner) { @@ -2723,8 +2750,13 @@ mark: { } #ifdef COPY_STACKS void *stkbuf = ta->stkbuf; - if (stkbuf && ta->copy_stack) + if (stkbuf && ta->copy_stack) { gc_setmark_buf_(ptls, stkbuf, bits, ta->bufsz); + // For gc_heap_snapshot_record: + // TODO: attribute size of stack + // TODO: edge to stack data + // TODO: synthetic node for stack data (how big is it?) + } #endif jl_gcframe_t *s = ta->gcstack; size_t nroots; @@ -2743,12 +2775,15 @@ mark: { #endif if (s) { nroots = gc_read_stack(&s->nroots, offset, lb, ub); + gc_heap_snapshot_record_task_to_frame_edge(ta, s); + assert(nroots <= UINT32_MAX); gc_mark_stackframe_t stackdata = {s, 0, (uint32_t)nroots, offset, lb, ub}; gc_mark_stack_push(&ptls->gc_cache, &sp, gc_mark_laddr(stack), &stackdata, sizeof(stackdata), 1); } if (ta->excstack) { + gc_heap_snapshot_record_task_to_frame_edge(ta, ta->excstack); gc_setmark_buf_(ptls, ta->excstack, bits, sizeof(jl_excstack_t) + sizeof(uintptr_t)*ta->excstack->reserved_size); gc_mark_excstack_t stackdata = {ta->excstack, ta->excstack->top, 0, 0}; @@ -2845,14 +2880,24 @@ mark: { static void jl_gc_queue_thread_local(jl_gc_mark_cache_t *gc_cache, jl_gc_mark_sp_t *sp, jl_ptls_t ptls2) { - gc_mark_queue_obj(gc_cache, sp, jl_atomic_load_relaxed(&ptls2->current_task)); + jl_value_t *current_task = (jl_value_t*)jl_atomic_load_relaxed(&ptls2->current_task); + gc_mark_queue_obj(gc_cache, sp, current_task); + gc_heap_snapshot_record_root(current_task, "current task"); gc_mark_queue_obj(gc_cache, sp, ptls2->root_task); - if (ptls2->next_task) + + gc_heap_snapshot_record_root((jl_value_t*)ptls2->root_task, "root task"); + if (ptls2->next_task) { gc_mark_queue_obj(gc_cache, sp, ptls2->next_task); - if (ptls2->previous_task) // shouldn't be necessary, but no reason not to + gc_heap_snapshot_record_root((jl_value_t*)ptls2->next_task, "next task"); + } + if (ptls2->previous_task) { // shouldn't be necessary, but no reason not to gc_mark_queue_obj(gc_cache, sp, ptls2->previous_task); - if (ptls2->previous_exception) + gc_heap_snapshot_record_root((jl_value_t*)ptls2->previous_task, "previous task"); + } + if (ptls2->previous_exception) { gc_mark_queue_obj(gc_cache, sp, ptls2->previous_exception); + gc_heap_snapshot_record_root((jl_value_t*)ptls2->previous_exception, "previous exception"); + } } extern jl_value_t *cmpswap_names JL_GLOBALLY_ROOTED; @@ -2862,6 +2907,7 @@ static void mark_roots(jl_gc_mark_cache_t *gc_cache, jl_gc_mark_sp_t *sp) { // modules gc_mark_queue_obj(gc_cache, sp, jl_main_module); + gc_heap_snapshot_record_root((jl_value_t*)jl_main_module, "main_module"); // invisible builtin values if (jl_an_empty_vec_any != NULL) @@ -2871,16 +2917,19 @@ static void mark_roots(jl_gc_mark_cache_t *gc_cache, jl_gc_mark_sp_t *sp) for (size_t i = 0; i < jl_current_modules.size; i += 2) { if (jl_current_modules.table[i + 1] != HT_NOTFOUND) { gc_mark_queue_obj(gc_cache, sp, jl_current_modules.table[i]); + gc_heap_snapshot_record_root((jl_value_t*)jl_current_modules.table[i], "top level module"); } } gc_mark_queue_obj(gc_cache, sp, jl_anytuple_type_type); for (size_t i = 0; i < N_CALL_CACHE; i++) { jl_typemap_entry_t *v = jl_atomic_load_relaxed(&call_cache[i]); - if (v != NULL) + if (v != NULL) { gc_mark_queue_obj(gc_cache, sp, v); + } } - if (jl_all_methods != NULL) + if (jl_all_methods != NULL) { gc_mark_queue_obj(gc_cache, sp, jl_all_methods); + } if (_jl_debug_method_invalidation != NULL) gc_mark_queue_obj(gc_cache, sp, _jl_debug_method_invalidation); @@ -3103,6 +3152,7 @@ static int _jl_gc_collect(jl_ptls_t ptls, jl_gc_collection_t collection) // 2.2. mark every thread local root jl_gc_queue_thread_local(gc_cache, &sp, ptls2); // 2.3. mark any managed objects in the backtrace buffer + // TODO: treat these as roots for gc_heap_snapshot_record jl_gc_queue_bt_buf(gc_cache, &sp, ptls2); } @@ -3213,7 +3263,7 @@ static int _jl_gc_collect(jl_ptls_t ptls, jl_gc_collection_t collection) if (gc_sweep_always_full) { sweep_full = 1; } - if (collection == JL_GC_FULL) { + if (collection == JL_GC_FULL && !prev_sweep_full) { sweep_full = 1; recollect = 1; } @@ -3466,6 +3516,7 @@ void jl_init_thread_heap(jl_ptls_t ptls) // System-wide initializations void jl_gc_init(void) { + JL_MUTEX_INIT(&heapsnapshot_lock); JL_MUTEX_INIT(&finalizers_lock); uv_mutex_init(&gc_cache_lock); uv_mutex_init(&gc_perm_lock); diff --git a/src/gc.h b/src/gc.h index 00c3d48b52935..d0a5a5375cb97 100644 --- a/src/gc.h +++ b/src/gc.h @@ -24,6 +24,7 @@ #endif #endif #include "julia_assert.h" +#include "gc-heap-snapshot.h" #include "gc-alloc-profiler.h" #ifdef __cplusplus @@ -646,8 +647,10 @@ extern int gc_verifying; #define verify_parent2(ty,obj,slot,arg1,arg2) do {} while (0) #define gc_verifying (0) #endif -int gc_slot_to_fieldidx(void *_obj, void *slot); -int gc_slot_to_arrayidx(void *_obj, void *begin); + + +int gc_slot_to_fieldidx(void *_obj, void *slot, jl_datatype_t *vt) JL_NOTSAFEPOINT; +int gc_slot_to_arrayidx(void *_obj, void *begin) JL_NOTSAFEPOINT; NOINLINE void gc_mark_loop_unwind(jl_ptls_t ptls, jl_gc_mark_sp_t sp, int pc_offset); #ifdef GC_DEBUG_ENV diff --git a/src/staticdata.c b/src/staticdata.c index 9d2881b281e73..89d3cf83a7491 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -1330,7 +1330,7 @@ static inline uintptr_t get_item_for_reloc(jl_serializer_state *s, uintptr_t bas assert(offset < deser_sym.len && deser_sym.items[offset] && "corrupt relocation item id"); return (uintptr_t)deser_sym.items[offset]; case BindingRef: - return jl_buff_tag | GC_OLD_MARKED; + return jl_buff_tag | GC_OLD; case TagRef: if (offset == 0) return (uintptr_t)s->ptls->root_task; @@ -2163,7 +2163,7 @@ static void jl_restore_system_image_from_stream(ios_t *f) JL_GC_DISABLED jl_gc_set_permalloc_region((void*)sysimg_base, (void*)(sysimg_base + sysimg.size)); s.s = &sysimg; - jl_read_relocations(&s, GC_OLD_MARKED); // gctags + jl_read_relocations(&s, GC_OLD); // gctags size_t sizeof_tags = ios_pos(&relocs); (void)sizeof_tags; jl_read_relocations(&s, 0); // general relocs diff --git a/stdlib/Profile/docs/src/index.md b/stdlib/Profile/docs/src/index.md index 8701dded0d427..e67c1d3a6fdc3 100644 --- a/stdlib/Profile/docs/src/index.md +++ b/stdlib/Profile/docs/src/index.md @@ -107,3 +107,24 @@ Profile.Allocs.fetch Profile.Allocs.start Profile.Allocs.stop ``` + +## Heap Snapshots + +```@docs +Profile.take_heap_snapshot +``` + +The methods in `Profile` are not exported and need to be called e.g. as `Profile.take_heap_snapshot()`. + +```julia-repl +julia> using Profile + +julia> Profile.take_heap_snapshot("snapshot.heapsnapshot") +``` + +Traces and records julia objects on the heap. This only records objects known to the Julia +garbage collector. Memory allocated by external libraries not managed by the garbage +collector will not show up in the snapshot. + +The resulting heap snapshot file can be uploaded to chrome devtools to be viewed. +For more information, see the [chrome devtools docs](https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/#view_snapshots). diff --git a/stdlib/Profile/src/Profile.jl b/stdlib/Profile/src/Profile.jl index 3621fe63bcaac..0f94d54a9c1aa 100644 --- a/stdlib/Profile/src/Profile.jl +++ b/stdlib/Profile/src/Profile.jl @@ -1239,6 +1239,27 @@ function warning_empty(;summary = false) end end + +""" + Profile.take_heap_snapshot(io::IOStream, all_one::Bool=false) + Profile.take_heap_snapshot(filepath::String, all_one::Bool=false) + +Write a snapshot of the heap, in the JSON format expected by the Chrome +Devtools Heap Snapshot viewer (.heapsnapshot extension), to the given +file path or IO stream. If all_one is true, then report the size of +every object as one so they can be easily counted. Otherwise, report +the actual size. +""" +function take_heap_snapshot(io::IOStream, all_one::Bool=false) + @Base._lock_ios(io, ccall(:jl_gc_take_heap_snapshot, Cvoid, (Ptr{Cvoid}, Cchar), io.handle, Cchar(all_one))) +end +function take_heap_snapshot(filepath::String, all_one::Bool=false) + open(filepath, "w") do io + take_heap_snapshot(io, all_one) + end +end + + include("Allocs.jl") end # module diff --git a/stdlib/Profile/test/runtests.jl b/stdlib/Profile/test/runtests.jl index 86c391d573e50..c543298ef9e4c 100644 --- a/stdlib/Profile/test/runtests.jl +++ b/stdlib/Profile/test/runtests.jl @@ -272,4 +272,13 @@ end @test only(node.down).first == lidict[8] end +@testset "HeapSnapshot" begin + fname = tempname() + Profile.take_heap_snapshot(fname) + + open(fname) do fs + @test readline(fs) != "" + end +end + include("allocs.jl")