mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
517 lines
13 KiB
C
517 lines
13 KiB
C
/**********************************************************************
|
|
|
|
objspace_dump.c - Heap dumping ObjectSpace extender for MRI.
|
|
|
|
$Author$
|
|
created at: Sat Oct 11 10:11:00 2013
|
|
|
|
NOTE: This extension library is not expected to exist except C Ruby.
|
|
|
|
All the files in this distribution are covered under the Ruby's
|
|
license (see the file COPYING).
|
|
|
|
**********************************************************************/
|
|
|
|
#include "ruby/io.h"
|
|
#include "internal.h"
|
|
#include "ruby/debug.h"
|
|
#include "gc.h"
|
|
#include "node.h"
|
|
#include "vm_core.h"
|
|
#include "objspace.h"
|
|
|
|
static VALUE sym_output, sym_stdout, sym_string, sym_file;
|
|
static VALUE sym_full;
|
|
|
|
struct dump_config {
|
|
VALUE type;
|
|
FILE *stream;
|
|
VALUE string;
|
|
const char *root_category;
|
|
VALUE cur_obj;
|
|
VALUE cur_obj_klass;
|
|
size_t cur_obj_references;
|
|
unsigned int roots: 1;
|
|
unsigned int full_heap: 1;
|
|
};
|
|
|
|
PRINTF_ARGS(static void dump_append(struct dump_config *, const char *, ...), 2, 3);
|
|
static void
|
|
dump_append(struct dump_config *dc, const char *format, ...)
|
|
{
|
|
va_list vl;
|
|
va_start(vl, format);
|
|
|
|
if (dc->stream) {
|
|
vfprintf(dc->stream, format, vl);
|
|
}
|
|
else if (dc->string)
|
|
rb_str_vcatf(dc->string, format, vl);
|
|
|
|
va_end(vl);
|
|
}
|
|
|
|
static void
|
|
dump_append_string_value(struct dump_config *dc, VALUE obj)
|
|
{
|
|
long i;
|
|
char c;
|
|
const char *value;
|
|
|
|
dump_append(dc, "\"");
|
|
for (i = 0, value = RSTRING_PTR(obj); i < RSTRING_LEN(obj); i++) {
|
|
switch ((c = value[i])) {
|
|
case '\\':
|
|
case '"':
|
|
dump_append(dc, "\\%c", c);
|
|
break;
|
|
case '\0':
|
|
dump_append(dc, "\\u0000");
|
|
break;
|
|
case '\b':
|
|
dump_append(dc, "\\b");
|
|
break;
|
|
case '\t':
|
|
dump_append(dc, "\\t");
|
|
break;
|
|
case '\f':
|
|
dump_append(dc, "\\f");
|
|
break;
|
|
case '\n':
|
|
dump_append(dc, "\\n");
|
|
break;
|
|
case '\r':
|
|
dump_append(dc, "\\r");
|
|
break;
|
|
default:
|
|
if (c <= 0x1f)
|
|
dump_append(dc, "\\u%04d", c);
|
|
else
|
|
dump_append(dc, "%c", c);
|
|
}
|
|
}
|
|
dump_append(dc, "\"");
|
|
}
|
|
|
|
static void
|
|
dump_append_symbol_value(struct dump_config *dc, VALUE obj)
|
|
{
|
|
dump_append(dc, "{\"type\":\"SYMBOL\", \"value\":");
|
|
dump_append_string_value(dc, rb_sym2str(obj));
|
|
dump_append(dc, "}");
|
|
}
|
|
|
|
static inline const char *
|
|
obj_type(VALUE obj)
|
|
{
|
|
switch (BUILTIN_TYPE(obj)) {
|
|
#define CASE_TYPE(type) case T_##type: return #type
|
|
CASE_TYPE(NONE);
|
|
CASE_TYPE(NIL);
|
|
CASE_TYPE(OBJECT);
|
|
CASE_TYPE(CLASS);
|
|
CASE_TYPE(ICLASS);
|
|
CASE_TYPE(MODULE);
|
|
CASE_TYPE(FLOAT);
|
|
CASE_TYPE(STRING);
|
|
CASE_TYPE(REGEXP);
|
|
CASE_TYPE(ARRAY);
|
|
CASE_TYPE(HASH);
|
|
CASE_TYPE(STRUCT);
|
|
CASE_TYPE(BIGNUM);
|
|
CASE_TYPE(FILE);
|
|
CASE_TYPE(FIXNUM);
|
|
CASE_TYPE(TRUE);
|
|
CASE_TYPE(FALSE);
|
|
CASE_TYPE(DATA);
|
|
CASE_TYPE(MATCH);
|
|
CASE_TYPE(SYMBOL);
|
|
CASE_TYPE(RATIONAL);
|
|
CASE_TYPE(COMPLEX);
|
|
CASE_TYPE(IMEMO);
|
|
CASE_TYPE(UNDEF);
|
|
CASE_TYPE(NODE);
|
|
CASE_TYPE(ZOMBIE);
|
|
#undef CASE_TYPE
|
|
}
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
static void
|
|
dump_append_special_const(struct dump_config *dc, VALUE value)
|
|
{
|
|
if (value == Qtrue) {
|
|
dump_append(dc, "true");
|
|
}
|
|
else if (value == Qfalse) {
|
|
dump_append(dc, "false");
|
|
}
|
|
else if (value == Qnil) {
|
|
dump_append(dc, "null");
|
|
}
|
|
else if (FIXNUM_P(value)) {
|
|
dump_append(dc, "%ld", FIX2LONG(value));
|
|
}
|
|
else if (FLONUM_P(value)) {
|
|
dump_append(dc, "%#g", RFLOAT_VALUE(value));
|
|
}
|
|
else if (SYMBOL_P(value)) {
|
|
dump_append_symbol_value(dc, value);
|
|
}
|
|
else {
|
|
dump_append(dc, "{}");
|
|
}
|
|
}
|
|
|
|
static void
|
|
reachable_object_i(VALUE ref, void *data)
|
|
{
|
|
struct dump_config *dc = (struct dump_config *)data;
|
|
|
|
if (dc->cur_obj_klass == ref)
|
|
return;
|
|
|
|
if (dc->cur_obj_references == 0)
|
|
dump_append(dc, ", \"references\":[\"%#"PRIxVALUE"\"", ref);
|
|
else
|
|
dump_append(dc, ", \"%#"PRIxVALUE"\"", ref);
|
|
|
|
dc->cur_obj_references++;
|
|
}
|
|
|
|
static void
|
|
dump_append_string_content(struct dump_config *dc, VALUE obj)
|
|
{
|
|
dump_append(dc, ", \"bytesize\":%ld", RSTRING_LEN(obj));
|
|
if (!STR_EMBED_P(obj) && !STR_SHARED_P(obj) && (long)rb_str_capacity(obj) != RSTRING_LEN(obj))
|
|
dump_append(dc, ", \"capacity\":%"PRIuSIZE, rb_str_capacity(obj));
|
|
|
|
if (is_ascii_string(obj)) {
|
|
dump_append(dc, ", \"value\":");
|
|
dump_append_string_value(dc, obj);
|
|
}
|
|
}
|
|
|
|
static const char *
|
|
imemo_name(int imemo)
|
|
{
|
|
switch(imemo) {
|
|
#define TYPE_STR(t) case(imemo_##t): return #t
|
|
TYPE_STR(env);
|
|
TYPE_STR(cref);
|
|
TYPE_STR(svar);
|
|
TYPE_STR(throw_data);
|
|
TYPE_STR(ifunc);
|
|
TYPE_STR(memo);
|
|
TYPE_STR(ment);
|
|
TYPE_STR(iseq);
|
|
TYPE_STR(tmpbuf);
|
|
TYPE_STR(ast);
|
|
TYPE_STR(parser_strterm);
|
|
default:
|
|
return "unknown";
|
|
#undef TYPE_STR
|
|
}
|
|
}
|
|
|
|
static void
|
|
dump_object(VALUE obj, struct dump_config *dc)
|
|
{
|
|
size_t memsize;
|
|
struct allocation_info *ainfo;
|
|
rb_io_t *fptr;
|
|
ID flags[RB_OBJ_GC_FLAGS_MAX];
|
|
size_t n, i;
|
|
|
|
if (SPECIAL_CONST_P(obj)) {
|
|
dump_append_special_const(dc, obj);
|
|
return;
|
|
}
|
|
|
|
dc->cur_obj = obj;
|
|
dc->cur_obj_references = 0;
|
|
dc->cur_obj_klass = BUILTIN_TYPE(obj) == T_NODE ? 0 : RBASIC_CLASS(obj);
|
|
|
|
if (dc->cur_obj == dc->string)
|
|
return;
|
|
|
|
dump_append(dc, "{\"address\":\"%#"PRIxVALUE"\", \"type\":\"%s\"", obj, obj_type(obj));
|
|
|
|
if (dc->cur_obj_klass)
|
|
dump_append(dc, ", \"class\":\"%#"PRIxVALUE"\"", dc->cur_obj_klass);
|
|
if (rb_obj_frozen_p(obj))
|
|
dump_append(dc, ", \"frozen\":true");
|
|
|
|
switch (BUILTIN_TYPE(obj)) {
|
|
case T_NONE:
|
|
dump_append(dc, "}\n");
|
|
return;
|
|
|
|
case T_IMEMO:
|
|
dump_append(dc, ", \"imemo_type\":\"%s\"", imemo_name(imemo_type(obj)));
|
|
break;
|
|
|
|
case T_SYMBOL:
|
|
dump_append_string_content(dc, rb_sym2str(obj));
|
|
break;
|
|
|
|
case T_STRING:
|
|
if (STR_EMBED_P(obj))
|
|
dump_append(dc, ", \"embedded\":true");
|
|
if (is_broken_string(obj))
|
|
dump_append(dc, ", \"broken\":true");
|
|
if (FL_TEST(obj, RSTRING_FSTR))
|
|
dump_append(dc, ", \"fstring\":true");
|
|
if (STR_SHARED_P(obj))
|
|
dump_append(dc, ", \"shared\":true");
|
|
else
|
|
dump_append_string_content(dc, obj);
|
|
|
|
if (!ENCODING_IS_ASCII8BIT(obj))
|
|
dump_append(dc, ", \"encoding\":\"%s\"", rb_enc_name(rb_enc_from_index(ENCODING_GET(obj))));
|
|
break;
|
|
|
|
case T_HASH:
|
|
dump_append(dc, ", \"size\":%"PRIuSIZE, (size_t)RHASH_SIZE(obj));
|
|
if (FL_TEST(obj, RHASH_PROC_DEFAULT))
|
|
dump_append(dc, ", \"default\":\"%#"PRIxVALUE"\"", RHASH_IFNONE(obj));
|
|
break;
|
|
|
|
case T_ARRAY:
|
|
dump_append(dc, ", \"length\":%ld", RARRAY_LEN(obj));
|
|
if (RARRAY_LEN(obj) > 0 && FL_TEST(obj, ELTS_SHARED))
|
|
dump_append(dc, ", \"shared\":true");
|
|
if (RARRAY_LEN(obj) > 0 && FL_TEST(obj, RARRAY_EMBED_FLAG))
|
|
dump_append(dc, ", \"embedded\":true");
|
|
break;
|
|
|
|
case T_CLASS:
|
|
case T_MODULE:
|
|
if (dc->cur_obj_klass)
|
|
dump_append(dc, ", \"name\":\"%s\"", rb_class2name(obj));
|
|
break;
|
|
|
|
case T_DATA:
|
|
if (RTYPEDDATA_P(obj))
|
|
dump_append(dc, ", \"struct\":\"%s\"", RTYPEDDATA_TYPE(obj)->wrap_struct_name);
|
|
break;
|
|
|
|
case T_FLOAT:
|
|
dump_append(dc, ", \"value\":\"%g\"", RFLOAT_VALUE(obj));
|
|
break;
|
|
|
|
case T_OBJECT:
|
|
dump_append(dc, ", \"ivars\":%u", ROBJECT_NUMIV(obj));
|
|
break;
|
|
|
|
case T_FILE:
|
|
fptr = RFILE(obj)->fptr;
|
|
if (fptr)
|
|
dump_append(dc, ", \"fd\":%d", fptr->fd);
|
|
break;
|
|
|
|
case T_ZOMBIE:
|
|
dump_append(dc, "}\n");
|
|
return;
|
|
}
|
|
|
|
rb_objspace_reachable_objects_from(obj, reachable_object_i, dc);
|
|
if (dc->cur_obj_references > 0)
|
|
dump_append(dc, "]");
|
|
|
|
if ((ainfo = objspace_lookup_allocation_info(obj))) {
|
|
dump_append(dc, ", \"file\":\"%s\", \"line\":%lu", ainfo->path, ainfo->line);
|
|
if (RTEST(ainfo->mid)) {
|
|
VALUE m = rb_sym2str(ainfo->mid);
|
|
dump_append(dc, ", \"method\":\"%s\"", RSTRING_PTR(m));
|
|
}
|
|
dump_append(dc, ", \"generation\":%"PRIuSIZE, ainfo->generation);
|
|
}
|
|
|
|
if ((memsize = rb_obj_memsize_of(obj)) > 0)
|
|
dump_append(dc, ", \"memsize\":%"PRIuSIZE, memsize);
|
|
|
|
if ((n = rb_obj_gc_flags(obj, flags, sizeof(flags))) > 0) {
|
|
dump_append(dc, ", \"flags\":{");
|
|
for (i=0; i<n; i++) {
|
|
dump_append(dc, "\"%s\":true", rb_id2name(flags[i]));
|
|
if (i != n-1) dump_append(dc, ", ");
|
|
}
|
|
dump_append(dc, "}");
|
|
}
|
|
|
|
dump_append(dc, "}\n");
|
|
}
|
|
|
|
static int
|
|
heap_i(void *vstart, void *vend, size_t stride, void *data)
|
|
{
|
|
struct dump_config *dc = (struct dump_config *)data;
|
|
VALUE v = (VALUE)vstart;
|
|
for (; v != (VALUE)vend; v += stride) {
|
|
if (dc->full_heap || RBASIC(v)->flags)
|
|
dump_object(v, dc);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
root_obj_i(const char *category, VALUE obj, void *data)
|
|
{
|
|
struct dump_config *dc = (struct dump_config *)data;
|
|
|
|
if (dc->root_category != NULL && category != dc->root_category)
|
|
dump_append(dc, "]}\n");
|
|
if (dc->root_category == NULL || category != dc->root_category)
|
|
dump_append(dc, "{\"type\":\"ROOT\", \"root\":\"%s\", \"references\":[\"%#"PRIxVALUE"\"", category, obj);
|
|
else
|
|
dump_append(dc, ", \"%#"PRIxVALUE"\"", obj);
|
|
|
|
dc->root_category = category;
|
|
dc->roots = 1;
|
|
}
|
|
|
|
static VALUE
|
|
dump_output(struct dump_config *dc, VALUE opts, VALUE output, const char *filename)
|
|
{
|
|
VALUE tmp;
|
|
|
|
dc->full_heap = 0;
|
|
|
|
if (RTEST(opts)) {
|
|
output = rb_hash_aref(opts, sym_output);
|
|
|
|
if (Qtrue == rb_hash_lookup2(opts, sym_full, Qfalse))
|
|
dc->full_heap = 1;
|
|
}
|
|
|
|
if (output == sym_stdout) {
|
|
dc->stream = stdout;
|
|
dc->string = Qnil;
|
|
}
|
|
else if (output == sym_file) {
|
|
rb_io_t *fptr;
|
|
rb_require("tempfile");
|
|
tmp = rb_assoc_new(rb_str_new_cstr(filename), rb_str_new_cstr(".json"));
|
|
tmp = rb_funcallv(rb_path2class("Tempfile"), rb_intern("create"), 1, &tmp);
|
|
io:
|
|
dc->string = rb_io_get_write_io(tmp);
|
|
rb_io_flush(dc->string);
|
|
GetOpenFile(dc->string, fptr);
|
|
dc->stream = rb_io_stdio_file(fptr);
|
|
}
|
|
else if (output == sym_string) {
|
|
dc->string = rb_str_new_cstr("");
|
|
}
|
|
else if (!NIL_P(tmp = rb_io_check_io(output))) {
|
|
output = sym_file;
|
|
goto io;
|
|
}
|
|
else {
|
|
rb_raise(rb_eArgError, "wrong output option: %"PRIsVALUE, output);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
static VALUE
|
|
dump_result(struct dump_config *dc, VALUE output)
|
|
{
|
|
if (output == sym_string) {
|
|
return rb_str_resurrect(dc->string);
|
|
}
|
|
else if (output == sym_file) {
|
|
rb_io_flush(dc->string);
|
|
return dc->string;
|
|
}
|
|
else {
|
|
return Qnil;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* ObjectSpace.dump(obj[, output: :string]) # => "{ ... }"
|
|
* ObjectSpace.dump(obj, output: :file) # => #<File:/tmp/rubyobj20131125-88733-1xkfmpv.json>
|
|
* ObjectSpace.dump(obj, output: :stdout) # => nil
|
|
*
|
|
* Dump the contents of a ruby object as JSON.
|
|
*
|
|
* This method is only expected to work with C Ruby.
|
|
* This is an experimental method and is subject to change.
|
|
* In particular, the function signature and output format are
|
|
* not guaranteed to be compatible in future versions of ruby.
|
|
*/
|
|
|
|
static VALUE
|
|
objspace_dump(int argc, VALUE *argv, VALUE os)
|
|
{
|
|
static const char filename[] = "rubyobj";
|
|
VALUE obj = Qnil, opts = Qnil, output;
|
|
struct dump_config dc = {0,};
|
|
|
|
rb_scan_args(argc, argv, "1:", &obj, &opts);
|
|
|
|
output = dump_output(&dc, opts, sym_string, filename);
|
|
|
|
dump_object(obj, &dc);
|
|
|
|
return dump_result(&dc, output);
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* ObjectSpace.dump_all([output: :file]) # => #<File:/tmp/rubyheap20131125-88469-laoj3v.json>
|
|
* ObjectSpace.dump_all(output: :stdout) # => nil
|
|
* ObjectSpace.dump_all(output: :string) # => "{...}\n{...}\n..."
|
|
* ObjectSpace.dump_all(output:
|
|
* File.open('heap.json','w')) # => #<File:heap.json>
|
|
*
|
|
* Dump the contents of the ruby heap as JSON.
|
|
*
|
|
* This method is only expected to work with C Ruby.
|
|
* This is an experimental method and is subject to change.
|
|
* In particular, the function signature and output format are
|
|
* not guaranteed to be compatible in future versions of ruby.
|
|
*/
|
|
|
|
static VALUE
|
|
objspace_dump_all(int argc, VALUE *argv, VALUE os)
|
|
{
|
|
static const char filename[] = "rubyheap";
|
|
VALUE opts = Qnil, output;
|
|
struct dump_config dc = {0,};
|
|
|
|
rb_scan_args(argc, argv, "0:", &opts);
|
|
|
|
output = dump_output(&dc, opts, sym_file, filename);
|
|
|
|
/* dump roots */
|
|
rb_objspace_reachable_objects_from_root(root_obj_i, &dc);
|
|
if (dc.roots) dump_append(&dc, "]}\n");
|
|
|
|
/* dump all objects */
|
|
rb_objspace_each_objects(heap_i, &dc);
|
|
|
|
return dump_result(&dc, output);
|
|
}
|
|
|
|
void
|
|
Init_objspace_dump(VALUE rb_mObjSpace)
|
|
{
|
|
#undef rb_intern
|
|
#if 0
|
|
rb_mObjSpace = rb_define_module("ObjectSpace"); /* let rdoc know */
|
|
#endif
|
|
|
|
rb_define_module_function(rb_mObjSpace, "dump", objspace_dump, -1);
|
|
rb_define_module_function(rb_mObjSpace, "dump_all", objspace_dump_all, -1);
|
|
|
|
sym_output = ID2SYM(rb_intern("output"));
|
|
sym_stdout = ID2SYM(rb_intern("stdout"));
|
|
sym_string = ID2SYM(rb_intern("string"));
|
|
sym_file = ID2SYM(rb_intern("file"));
|
|
sym_full = ID2SYM(rb_intern("full"));
|
|
|
|
/* force create static IDs */
|
|
rb_obj_gc_flags(rb_mObjSpace, 0, 0);
|
|
}
|