python scripts/GC_Graph/generate_gc_graph.py -h
usage: generate_gc_graph.py [-h] [-r] [-t] [-n TYPENAME] [--extra-info EXTRA_INFO] [-l LIMIT] FILENAME
Generate GC Graphs on Cytoscape.
positional arguments:
FILENAME Path of the json file to Generate GC Graphs
optional arguments:
-h, --help show this help message and exit
-r, --reverse Reverse order of the Generated GC Graphs
-t, --traverse-only Generate GC Graphs without internal routines
-n TYPENAME, --typename TYPENAME
Select only nodes with the provided type name [Exact] and their parent and childs
--extra-info EXTRA_INFO
Add extra info from additional json files
-l LIMIT, --limit LIMIT
Maximum number of nodes per graph- Add helpers (GraalPy or CPython):
static inline int
call_traverse(traverseproc traverse, PyObject *op, visitproc visit, void *arg) {
return traverse(op, (visitproc)visit, arg);
}
#ifndef GRAALVM_PYTHON
#define CALL_TRAVERSE(traverse, op, visit_fun, ctx) ((void) call_traverse((traverse), (op), (visitproc)(visit_fun), (ctx)))
#define UNTAG(gc) gc
#define is_managed(gc) 0
#define IMPL_NAME "cpy_"
#else
#define IMPL_NAME "graalpy_"
#endif
static const char* graph_legend = "\"legend\": {" \
"\"n\": \"name\", " \
"\"gc\": \"gc address\", " \
"\"t\": \"type name\", " \
"\"ntv\": \"is native\", " \
"\"rc\": \"ref count\", " \
"\"gr\": \"gc refcount\", " \
"\"nr\": \"is unreachable\", " \
"\"ct\": \"is collecting\"" \
"},\n";
static long log_time_stamp = NULL;
static int log_enabled = -1;
static FILE *gc_graph_file = NULL;
static int is_gc_graph_enabled() {
if (log_enabled == -1) {
log_enabled = getenv("GC_GRAPH") == NULL ? 0 : 1;
}
return log_enabled;
}
static FILE *
get_gc_graph_file() {
if(is_gc_graph_enabled() && !gc_graph_file) {
if (!log_time_stamp) {
log_time_stamp = time(NULL);
}
char *fname;
asprintf(&fname, IMPL_NAME "gc_graph_%ld.json", log_time_stamp);
gc_graph_file = fopen(fname, "a");
fprintf(gc_graph_file, "{");
fprintf(gc_graph_file, graph_legend);
fprintf(gc_graph_file, "\"runs\": [[\"gc graphs\"]\n");
}
return gc_graph_file;
}
static void close_log() {
if(is_gc_graph_enabled()) {
fclose(gc_graph_file);
gc_graph_file = NULL;
}
}
#define BASIC_INFO 1
#define REFCOUNT_INFO 2
#define GC_REFCOUNT_INFO 4
#define IS_REACHABLE_INFO 8
#define IS_COLLECTING_INFO 16
static void
write_item_ptr(FILE *fp, int add_comma, const char* key, void* val) {
fprintf(fp, "%s\"%s\": %p ", add_comma ? "" : ",", key, val);
}
static void
write_item_bool(FILE *fp, int add_comma, const char* key, int val) {
fprintf(fp, "%s\"%s\": %d ", add_comma ? "" : ",", key, val);
}
static void
write_item_str(FILE *fp, int add_comma, const char* key, const char* val) {
fprintf(fp, "%s\"%s\": %s ", add_comma ? "" : ",", key, val);
}
static void
write_item_size_t(FILE *fp, int add_comma, const char* key, Py_ssize_t val) {
fprintf(fp, "%s\"%s\": %ld ", add_comma ? "" : ",", key, val);
}
static void
print_node(PyGC_Head *gc, int is_first_node, int opt) {
FILE *fp = get_gc_graph_file();
PyObject *op = FROM_GC(gc);
fprintf(fp, "%s\"%p\": { ", is_first_node ? "" : ",", op);
int is_first_info = 1;
if ((opt & BASIC_INFO) != 0) {
write_item_ptr(fp, is_first_info, "n", op);
is_first_info = 0;
write_item_ptr(fp, is_first_info, "gc", gc);
write_item_str(fp, is_first_info, "t", Py_TYPE(op)->tp_name);
write_item_bool(fp, is_first_info, "ntv", !is_managed(gc) ? 1 : 0);
}
if ((opt & REFCOUNT_INFO) != 0) {
write_item_size_t(fp, is_first_info, "rc", Py_REFCNT(op));
is_first_info = 0;
}
if ((opt & GC_REFCOUNT_INFO) != 0) {
write_item_size_t(fp, is_first_info, "gr", gc_get_refs(gc));
is_first_info = 0;
}
if ((opt & IS_REACHABLE_INFO) != 0) {
int is_unreachable = (UNTAG(gc)->_gc_next & NEXT_MASK_UNREACHABLE) != 0 ? 1 : 0;
write_item_bool(fp, is_first_info, "nr", is_unreachable);
is_first_info = 0;
}
if ((opt & IS_COLLECTING_INFO) != 0) {
write_item_bool(fp, is_first_info, "ct", gc_is_collecting(gc) ? 1 : 0);
is_first_info = 0;
}
fprintf(fp, "}\n");
fflush(fp);
}
static void
print_edge(int is_first, PyObject *src, PyObject *dst) {
fprintf(get_gc_graph_file(), "%s[\"%p\", \"%p\"]\n", is_first ? "" : ",", src, dst);
fflush(get_gc_graph_file());
}
static void print_section(const char* section, const char* name, int new_gc_collect, int is_start, int is_array) {
FILE *fp = get_gc_graph_file();
if(is_gc_graph_enabled()) {
if (is_start) {
if (new_gc_collect) {
fprintf(fp, ",[\"%s\", \"%s\"\n", section, name);
} else {
fprintf(fp, ",[\"%s\", \"%s\", %s\n", section, name, is_array ? "[" : "{");
}
} else {
if (new_gc_collect) {
fprintf(fp, "]\n");
} else {
fprintf(fp, "%s\n]\n", is_array ? "]" : "}");
}
}
fflush(fp);
}
}
static int
visit_discover_edges(PyObject *op, void *parent)
{
if (_PyObject_IS_GC(op)) {
print_edge(0, (PyObject *)parent, op);
}
return 0;
}
static void
traverse_gc_objects(PyGC_Head *containers, const char* name) {
if(is_gc_graph_enabled() && containers && GC_NEXT(containers)) {
traverseproc traverse;
PyGC_Head *gc;
print_section("nodes", name, 0, 1, 0);
int is_first_node = 1;
for (gc = GC_NEXT(containers); gc != containers; gc = GC_NEXT(gc)) {
PyObject *op = FROM_GC(gc);
print_node(gc, is_first_node, BASIC_INFO | REFCOUNT_INFO);
is_first_node = 0;
}
print_section("nodes", name, 0, 0, 0);
print_section("edges", name, 0, 1, 1);
print_edge(1, NULL, NULL);
for (gc = GC_NEXT(containers); gc != containers; gc = GC_NEXT(gc)) {
PyObject *op = FROM_GC(gc);
traverse = Py_TYPE(op)->tp_traverse;
CALL_TRAVERSE(traverse, op, visit_discover_edges, (void *)op);
}
print_section("edges", name, 0, 0, 1);
}
}
#define GC_NEXT_UNMASK_UNREACHABLE(container) (PyGC_Head*)(UNTAG(container)->_gc_next & ~NEXT_MASK_UNREACHABLE);
static void
query_gc_objects(PyGC_Head *containers, int do_unmask_unreachable, const char* location, const char* name, int info_opt) {
if(is_gc_graph_enabled() && containers) {
print_section(location, name, 0, 1, 0);
if (!gc_list_is_empty(containers)) {
PyGC_Head *next, *gc = !do_unmask_unreachable? GC_NEXT(containers) : GC_NEXT_UNMASK_UNREACHABLE(containers);
int is_first_node = 1;
while (gc != containers) {
next = !do_unmask_unreachable? GC_NEXT(gc) : GC_NEXT_UNMASK_UNREACHABLE(gc);
print_node(gc, is_first_node, info_opt);
gc = next;
is_first_node = 0;
}
}
print_section(location, name, 0, 0, 0);
}
}- GraalPy patch
@@ -1037,6 +1214,8 @@ move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers)
gc_list_move(gc, finalizers);
}
}
+ query_gc_objects(unreachable, 0, "move_legacy_finalizers", "unreachable", GC_REFCOUNT_INFO);
+ query_gc_objects(finalizers, 0, "move_legacy_finalizers", "finalizers", GC_REFCOUNT_INFO);
}
static inline void
@@ -1243,6 +1422,7 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old)
* dict, leaving no other references to the weakref (excepting
* ours).
*/
+ printf("[%p][%ld] %s\n", op, Py_REFCNT(op), Py_TYPE(op)->tp_name);fflush(stdout);
Py_DECREF(op);
if (wrcb_to_call._gc_next == (uintptr_t)gc) {
/* object is still alive -- move it */
@@ -1448,7 +1628,9 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
* set are taken into account).
*/
update_refs(base); // gc_prev is used for gc_refs
+ query_gc_objects(base, 0, "update_refs", "base", GC_REFCOUNT_INFO | IS_COLLECTING_INFO);
subtract_refs(base);
+ query_gc_objects(base, 0, "subtract_refs", "base", GC_REFCOUNT_INFO);
/* Leave everything reachable from outside base in base, and move
* everything else (in base) to unreachable.
@@ -1488,6 +1670,9 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
gc_list_init(unreachable);
gc_list_init(&weak_candidates);
move_unreachable(base, unreachable, &weak_candidates); // gc_prev is pointer again
+ query_gc_objects(base, 0, "move_unreachable", "base", GC_REFCOUNT_INFO | IS_REACHABLE_INFO);
+ query_gc_objects(unreachable, 1, "move_unreachable", "unreachable", GC_REFCOUNT_INFO | IS_REACHABLE_INFO);
+ query_gc_objects(&weak_candidates, 1, "move_unreachable", "weak_candidates", GC_REFCOUNT_INFO | IS_REACHABLE_INFO);
validate_list(base, collecting_clear_unreachable_clear);
validate_list(&weak_candidates, collecting_clear_unreachable_clear);
validate_list(unreachable, collecting_set_unreachable_set);
@@ -1511,6 +1696,7 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
update_refs(base); // gc_prev is used for gc_refs
subtract_refs(base);
move_weak_reachable(base, &weak_candidates);
+ query_gc_objects(&weak_candidates, 1, "move_weak_reachable", "weak_candidates", GC_REFCOUNT_INFO | IS_REACHABLE_INFO);
commit_weak_candidate(&weak_candidates);
}
}
@@ -1543,6 +1729,8 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable,
deduce_unreachable(resurrected, still_unreachable);
clear_unreachable_mask(still_unreachable);
+ query_gc_objects(resurrected, 0, "handle_resurrected_objects", "resurrected", GC_REFCOUNT_INFO);
+ query_gc_objects(still_unreachable, 0, "handle_resurrected_objects", "still_unreachable", GC_REFCOUNT_INFO);
// Move the resurrected objects to the old generation for future collection.
gc_list_merge(resurrected, old_generation);
}
@@ -1554,6 +1742,7 @@ gc_collect_main(PyThreadState *tstate, int generation,
Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
int nofail)
{
+ is_gc_graph_enabled();
int i;
Py_ssize_t m = 0; /* # objects collected */
Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
@@ -1570,6 +1759,8 @@ gc_collect_main(PyThreadState *tstate, int generation,
return m + n;
}
+ print_section("gc_collect_main", "", 1, 1, 0);
+
// gc_collect_main() must not be called before _PyGC_Init
// or after _PyGC_Fini()
assert(gcstate->garbage != NULL);
@@ -1604,6 +1795,7 @@ gc_collect_main(PyThreadState *tstate, int generation,
old = young;
validate_list(old, collecting_clear_unreachable_clear);
+ traverse_gc_objects(young, "young");
deduce_unreachable(young, &unreachable);
untrack_tuples(young);
@@ -1689,6 +1881,8 @@ gc_collect_main(PyThreadState *tstate, int generation,
*/
handle_legacy_finalizers(tstate, gcstate, &finalizers, old);
validate_list(old, collecting_clear_unreachable_clear);
+ print_section("gc_collect_main", "gc_collect_main", 1, 0, 0);
+ log_enabled = -1;
/* Clear free list only during the collection of the highest
* generation */
- CPython 3.11.7 patch
@@ -690,6 +873,8 @@ move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers)
gc_list_move(gc, finalizers);
}
}
+ query_gc_objects(unreachable, 0, "move_legacy_finalizers", "unreachable", GC_REFCOUNT_INFO);
+ query_gc_objects(finalizers, 0, "move_legacy_finalizers", "finalizers", GC_REFCOUNT_INFO);
}
static inline void
@@ -1097,7 +1282,9 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
* set are taken into account).
*/
update_refs(base); // gc_prev is used for gc_refs
+ query_gc_objects(base, 0, "update_refs", "base", GC_REFCOUNT_INFO | IS_COLLECTING_INFO);
subtract_refs(base);
+ query_gc_objects(base, 0, "subtract_refs", "base", GC_REFCOUNT_INFO);
/* Leave everything reachable from outside base in base, and move
* everything else (in base) to unreachable.
@@ -1136,6 +1323,8 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
*/
gc_list_init(unreachable);
move_unreachable(base, unreachable); // gc_prev is pointer again
+ query_gc_objects(base, 0, "move_unreachable", "base", GC_REFCOUNT_INFO | IS_REACHABLE_INFO);
+ query_gc_objects(unreachable, 1, "move_unreachable", "unreachable", GC_REFCOUNT_INFO | IS_REACHABLE_INFO);
validate_list(base, collecting_clear_unreachable_clear);
validate_list(unreachable, collecting_set_unreachable_set);
}
@@ -1168,6 +1357,8 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable,
deduce_unreachable(resurrected, still_unreachable);
clear_unreachable_mask(still_unreachable);
+ query_gc_objects(resurrected, 0, "handle_resurrected_objects", "resurrected", GC_REFCOUNT_INFO);
+ query_gc_objects(still_unreachable, 0, "handle_resurrected_objects", "still_unreachable", GC_REFCOUNT_INFO);
// Move the resurrected objects to the old generation for future collection.
gc_list_merge(resurrected, old_generation);
}
@@ -1179,6 +1370,7 @@ gc_collect_main(PyThreadState *tstate, int generation,
Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
int nofail)
{
+ is_gc_graph_enabled();
int i;
Py_ssize_t m = 0; /* # objects collected */
Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
@@ -1190,6 +1382,7 @@ gc_collect_main(PyThreadState *tstate, int generation,
_PyTime_t t1 = 0; /* initialize to prevent a compiler warning */
GCState *gcstate = &tstate->interp->gc;
+ print_section("gc_collect_main", "", 1, 1, 0);
// gc_collect_main() must not be called before _PyGC_Init
// or after _PyGC_Fini()
assert(gcstate->garbage != NULL);
@@ -1223,6 +1416,7 @@ gc_collect_main(PyThreadState *tstate, int generation,
old = young;
validate_list(old, collecting_clear_unreachable_clear);
+ traverse_gc_objects(young, "young");
deduce_unreachable(young, &unreachable);
untrack_tuples(young);
@@ -1306,6 +1500,8 @@ gc_collect_main(PyThreadState *tstate, int generation,
*/
handle_legacy_finalizers(tstate, gcstate, &finalizers, old);
validate_list(old, collecting_clear_unreachable_clear);
+ print_section("gc_collect_main", "gc_collect_main", 1, 0, 0);
+ log_enabled = -1;
/* Clear free list only during the collection of the highest
* generation */Enable GC graph generator around the area of interest in the code by setting environment GC_GRAPH. This will help reduce the size of the generated graph and provide a faster lookup for the objects that need to be investigated.
e.g.
import os
...
os.environ['GC_GRAPH'] = '1'
# do something
os.environ.pop('GC_GRAPH')After executing the program, a json file will be produced *py_gc_graph_####.json that can be processed using generate_gc_graph.py:
python scripts/GC_Graph/generate_gc_graph.py /path/to/*py_gc_graph_####.jsonTo add more information about objects, e.g. size, the target package need to also be modified/patched to generate json files that maps objects address with the additional information.
e.g.
{
"0x55e90cf22868": { "size": 3397386240 },
...
}an example of patch that produce size information:
- PyTorch 2.2.1 patch
diff --git a/torch/csrc/autograd/python_variable.cpp b/torch/csrc/autograd/python_variable.cpp
index ba0e913896..e76a1a9ca0 100644
--- a/torch/csrc/autograd/python_variable.cpp
+++ b/torch/csrc/autograd/python_variable.cpp
@@ -1869,6 +1869,38 @@ void THPVariable_subclass_dealloc(PyObject* self) {
Py_DECREF(type);
}
+#if defined(GRAALVM_PYTHON)
+ #define IMPL_NAME "graalpy_"
+#else
+ #define IMPL_NAME "cpy_"
+#endif
+
+long log_time_stamp = 0;
+int log_enabled = -1;
+int obj_size = 1024 * 1024 * 512; // > 512 MB
+FILE *pytorch_file = NULL;
+static int is_gc_graph_enabled() {
+ if (log_enabled == -1) {
+ log_enabled = getenv("GC_GRAPH") == NULL ? 0 : 1;
+ obj_size = getenv("TENSOR_SIZE") == NULL ? obj_size : atoi(getenv("TENSOR_SIZE"));
+ }
+ return log_enabled;
+}
+
+static FILE *
+get_pytorch_file() {
+ if(log_enabled && !pytorch_file) {
+ if (!log_time_stamp) {
+ log_time_stamp = time(NULL);
+ }
+ char *fname;
+ asprintf(&fname, IMPL_NAME "pytorch_%ld.json", log_time_stamp);
+ pytorch_file = fopen(fname, "a");
+ fprintf(pytorch_file, "%s", "{\n");
+ }
+ return pytorch_file;
+}
+
// Creates a new Python object for a Variable. The status parameter
// specifies what the interpreter tag status on the object is; for
// example, if you ran check_pyobj, the return optional of this object
@@ -1978,6 +2010,13 @@ static PyObject* THPVariable_NewWithVar(
var.unsafeGetTensorImpl()->set_python_dispatch(true);
}
}
+
+ size_t size = THPVariable_Unpack(v).nbytes();
+ if (is_gc_graph_enabled() && size > obj_size) {
+ fprintf(get_pytorch_file(), "\t\"%p\": { \"size\": \"%ld\" },\n", obj, size);
+ fflush(get_pytorch_file());
+ }
+
}
return obj;
}Then the produced json *py_pytorch_#####.json can then be passed using --extra-info:
python scripts/GC_Graph/generate_gc_graph.py --extra-info /path/to/*py_pytorch_#####.json /path/to/*py_gc_graph_####.json