majo 0.0.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c68eebecd1591a5579a3c44135c7d29c7b46743c0f35c65f8ebd3b557bd78f4a
4
- data.tar.gz: 5742f4feabbead870b4becbeb8c40f5591eff79541f3f70dd4aef285c1de5f0d
3
+ metadata.gz: 5db0056e26ba9974d00512d31930aad34d35af8e5ed9f880eda7665bdb9c2f59
4
+ data.tar.gz: 7489e069356907943bdf68de696cd3c9071aa4c2918743a5c05ba3b4ce286bf4
5
5
  SHA512:
6
- metadata.gz: e1e165da4e081ca6886db4631f1052bab452551b073784c90507da80d1f35672edc0a9b7ead753b32c5d1fc7884e8fe87778c76b38c0b8dbadbde417362902db
7
- data.tar.gz: 24cfe454a1f38b70f7c1b12d6f4bd3a1cf94384b9f18eabe10e27907e93c5b0e8417e53d405582086571e4cd649ca14d6d4cdd05e5e5b50794ff2994cc1f077f
6
+ metadata.gz: 3ab8e2fa890a01d0ac7a36c7be6e2adbe492fbd58319888636e6c733459a0f76dbf3d8aedc08cbe68d488f3a11ff54be758f661b0866afa31b2b4c6ce75abae0
7
+ data.tar.gz: 42ce9c56b603b4f50ff1504a47bf8f1616d384c4e135034be6b7f9d13c9afa6b5f278fde5683b2011ae9a3b024c9dc2f2b87da955b9dfdaad8fd44bb6fd08e1b
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,25 @@
1
+ # Architecture of Majo
2
+
3
+ Majo uses internal TracePoint hooks, `RUBY_INTERNAL_EVENT_NEWOBJ` and `RUBY_INTERNAL_EVENT_FREEOBJ`, to trace object allocation and free events.
4
+ We can use `ObjectSpace.trace_object_allocations_start` method in the Ruby-level, but this API is not suitable for Majo because it does not provide information about freed objects. Therefore, Majo uses these hooks in the C-level.
5
+
6
+ ## On allocation event
7
+
8
+ When an object is allocated, Majo records the object into a st table with the path, line number, and so on.
9
+
10
+ See `newobj_i` function in `majo.c`.
11
+
12
+ ## On free event
13
+
14
+ When an object is freed, Majo moves the allocation information to an array from the st table if it's a long-lived object.
15
+ It determines whether the object is long-lived by checking the GC count. If the object is not swept by a GC sweeping phase, it's a long-lived object.
16
+
17
+ See `freeobj_i` function in `majo.c`.
18
+
19
+ ## Output format
20
+
21
+ Majo provides three output formats: `color`, `monochrome`, and `csv`.
22
+
23
+ `color` and `monochrome` are for human-readable output. They provide overview information about long-lived objects.
24
+
25
+ `csv` is for machine-readable output. It provides the raw information. You can use this format to analyze the result with other tools.
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # CHANGELOG
2
+
3
+ ## v1.0.0 - 2024-07-26
4
+
5
+ The first stable release of Majo!
6
+
7
+ * Report retained objects [#32](https://github.com/pocke/majo/pull/32)
8
+ * Add options for upper and lower threshold of lifetime [#34](https://github.com/pocke/majo/pull/34)
9
+ * Display class path for singleton method call [#37](https://github.com/pocke/majo/pull/37)
10
+
11
+ ## v0.1.0 - 2024-07-25
12
+
13
+ * Initial release
data/README.md CHANGED
@@ -37,10 +37,10 @@ result.report
37
37
 
38
38
  You can pass the following options to `report` method.
39
39
 
40
- | Name | Description | Default | Type |
41
- | ----------- | ------------------------ | --------- | ------------------------------------------------------------------------ |
42
- | `out` | Output file or IO | `$stdout` | `IO`, `String` or an object having `to_path` method (such as `Pathname`) |
43
- | `formatter` | The format of the result | `:color` | `:color`, or `:csv` |
40
+ | Name | Description | Default | Type |
41
+ | ----------- | ------------------------ | ------------------------ | ------------------------------------------------------------------------ |
42
+ | `out` | Output file or IO | `$stdout` | `IO`, `String` or an object having `to_path` method (such as `Pathname`) |
43
+ | `formatter` | The format of the result | `:color` or `monochrome` | `:color`, `:monochrome`, or `:csv` |
44
44
 
45
45
  For example:
46
46
 
@@ -54,7 +54,7 @@ result.report(out: "majo-result.csv", formatter: :csv)
54
54
 
55
55
  ### Result Example
56
56
 
57
- The result contains only long-lived objects, which are collected by the major GC.
57
+ The result contains only long-lived objects, which are collected by the major GC or retained.
58
58
 
59
59
  The example is as follows:
60
60
 
@@ -102,15 +102,53 @@ Objects by class
102
102
  22435 RBS::Location
103
103
  11144 RBS::TypeName
104
104
  (snip)
105
+
106
+ Retained Memory by file
107
+ -----------------------------------
108
+ 7040 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/parser_aux.rb
109
+ 256 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/resolver/type_name_resolver.rb
110
+ (snip)
111
+
112
+ Retained Memory by location
113
+ -----------------------------------
114
+ 7040 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/parser_aux.rb:20
115
+ 256 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/resolver/type_name_resolver.rb:22
116
+ (snip)
117
+
118
+ Retained Memory by class
119
+ -----------------------------------
120
+ 6920 String
121
+ 256 Hash
122
+ 120 Symbol
123
+ (snip)
124
+
125
+ Retained Objects by file
126
+ -----------------------------------
127
+ 160 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/parser_aux.rb
128
+ 1 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/resolver/type_name_resolver.rb
129
+ (snip)
130
+
131
+ Retained Objects by location
132
+ -----------------------------------
133
+ 160 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/parser_aux.rb:20
134
+ 1 /path/to/gems/3.3.0/gems/rbs-3.5.1/lib/rbs/resolver/type_name_resolver.rb:22
135
+ (snip)
136
+
137
+ Retained Objects by class
138
+ -----------------------------------
139
+ 157 String
140
+ 3 Symbol
141
+ 1 Hash
142
+ (snip)
105
143
  ```
106
144
 
107
145
  The CSV format is as follows:
108
146
 
109
147
  ```csv
110
- Object class path,Class path,Method ID,Path,Line,Alloc generation,Free generation,Memsize,Count
111
- Hash,,_parse_signature,/path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb,20,20,22,160,3
112
- Hash,RBS::EnvironmentLoader,each_signature,/path/to/gems/rbs-3.5.1/lib/rbs/environment_loader.rb,159,20,22,160,1
113
- Hash,,_parse_signature,/path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb,20,21,23,160,1
148
+ Object class path,Class path,Method ID,Singleton method,Path,Line,Alloc generation,Free generation,Memsize,Count
149
+ Hash,RBS::Parser,_parse_signature,true,/path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb,20,20,22,160,3
150
+ Hash,RBS::EnvironmentLoader,each_signature,false,/path/to/gems/rbs-3.5.1/lib/rbs/environment_loader.rb,159,20,22,160,1
151
+ Hash,RBS::Parser,_parse_signature,true,/path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb,20,21,23,160,1
114
152
  (snip)
115
153
  ```
116
154
 
@@ -118,17 +156,18 @@ You can find the raw data in the CSV format. It is useful for further analysis.
118
156
 
119
157
  The columns are as follows:
120
158
 
121
- | Column name | Description |
122
- | ------------------- | ------------------------------------------------------------------ |
123
- | `Object class path` | The class name of the allocated object |
124
- | `Class path` | The class name of the receiver of the method allocating the object |
125
- | `Method ID` | The method name allocating the object |
126
- | `Path` | The file path of the method allocating the object |
127
- | `Line` | The line number of the method allocating the object |
128
- | `Alloc generation` | The GC generation number when the object is allocated |
129
- | `Free generation` | The GC generation number when the object is freed |
130
- | `Memsize` | The memory size of the object in bytes |
131
- | `Count` | Number of objects allocated with the same conditions |
159
+ | Column name | Description |
160
+ | ------------------- | ----------------------------------------------------------------------------------- |
161
+ | `Object class path` | The class name of the allocated object |
162
+ | `Class path` | The class name of the receiver of the method allocating the object |
163
+ | `Method ID` | The method name allocating the object |
164
+ | `Singleton method` | `true` if the method call is for a singleton method |
165
+ | `Path` | The file path of the method allocating the object |
166
+ | `Line` | The line number of the method allocating the object |
167
+ | `Alloc generation` | The GC generation number when the object is allocated |
168
+ | `Free generation` | The GC generation number when the object is freed. It's empty for retained objects. |
169
+ | `Memsize` | The memory size of the object in bytes |
170
+ | `Count` | Number of objects allocated with the same conditions |
132
171
 
133
172
  ## Name
134
173
 
@@ -2,13 +2,12 @@
2
2
 
3
3
  static void majo_allocation_info_mark(void *ptr)
4
4
  {
5
- // TODO
6
5
  majo_allocation_info *info = (majo_allocation_info*)ptr;
6
+ rb_gc_mark(info->result);
7
7
  }
8
8
 
9
9
  static void majo_allocation_info_free(majo_allocation_info *info) {
10
- // TODO
11
- ruby_xfree(info);
10
+ free(info);
12
11
  }
13
12
 
14
13
  static size_t majo_allocation_info_memsize(const void *ptr) {
@@ -85,6 +84,12 @@ allocation_info_method_id(VALUE self) {
85
84
  return info->mid;
86
85
  }
87
86
 
87
+ static VALUE
88
+ allocation_info_singleton_p(VALUE self) {
89
+ majo_allocation_info *info = majo_check_allocation_info(self);
90
+ return info->singleton_p ? Qtrue : Qfalse;
91
+ }
92
+
88
93
  static VALUE
89
94
  allocation_info_alloc_generation(VALUE self) {
90
95
  majo_allocation_info *info = majo_check_allocation_info(self);
@@ -94,6 +99,9 @@ allocation_info_alloc_generation(VALUE self) {
94
99
  static VALUE
95
100
  allocation_info_free_generation(VALUE self) {
96
101
  majo_allocation_info *info = majo_check_allocation_info(self);
102
+ if (info->free_generation == 0) {
103
+ return Qnil;
104
+ }
97
105
  return SIZET2NUM(info->free_generation);
98
106
  }
99
107
 
@@ -111,6 +119,7 @@ majo_init_allocation_info() {
111
119
  rb_define_method(rb_cMajo_AllocationInfo, "path", allocation_info_path, 0);
112
120
  rb_define_method(rb_cMajo_AllocationInfo, "class_path", allocation_info_class_path, 0);
113
121
  rb_define_method(rb_cMajo_AllocationInfo, "method_id", allocation_info_method_id, 0);
122
+ rb_define_method(rb_cMajo_AllocationInfo, "singleton?", allocation_info_singleton_p, 0);
114
123
  rb_define_method(rb_cMajo_AllocationInfo, "line", allocation_info_line, 0);
115
124
  rb_define_method(rb_cMajo_AllocationInfo, "object_class_path", allocation_info_object_class_path, 0);
116
125
  rb_define_method(rb_cMajo_AllocationInfo, "alloc_generation", allocation_info_alloc_generation, 0);
@@ -7,10 +7,13 @@ typedef struct {
7
7
  const char *object_class_path;
8
8
  unsigned long line;
9
9
  VALUE mid;
10
+ bool singleton_p;
10
11
 
11
12
  size_t alloc_generation;
12
13
  size_t free_generation;
13
14
  size_t memsize;
15
+
16
+ VALUE result;
14
17
  } majo_allocation_info;
15
18
 
16
19
  VALUE
@@ -0,0 +1,26 @@
1
+ #include "majo.h"
2
+
3
+ #if RUBY_API_VERSION_MAJOR == 3 && RUBY_API_VERSION_MINOR == 1
4
+
5
+ static ID id_attached;
6
+
7
+ VALUE
8
+ majo_attached_object(VALUE klass) {
9
+ if (!FL_TEST(klass, FL_SINGLETON)) {
10
+ rb_raise(rb_eTypeError, "`%"PRIsVALUE"' is not a singleton class", klass);
11
+ }
12
+
13
+ return rb_attr_get(klass, id_attached);
14
+ }
15
+
16
+ void
17
+ majo_init_attached_object() {
18
+ id_attached = rb_intern_const("__attached__");
19
+ }
20
+
21
+ #else
22
+
23
+ void
24
+ majo_init_attached_object() {}
25
+
26
+ #endif
@@ -0,0 +1,13 @@
1
+ #if RUBY_API_VERSION_MAJOR == 3 && RUBY_API_VERSION_MINOR == 1
2
+
3
+ VALUE
4
+ majo_attached_object(VALUE klass);
5
+
6
+ #else
7
+
8
+ #define majo_attached_object(klass) rb_class_attached_object(klass)
9
+
10
+ #endif
11
+
12
+ void
13
+ majo_init_attached_object();
data/ext/majo/majo.c CHANGED
@@ -36,10 +36,23 @@ internal_object_p(VALUE obj)
36
36
  }
37
37
  }
38
38
 
39
+ static const char*
40
+ to_class_path(VALUE klass, st_table *str_table)
41
+ {
42
+ if (FL_TEST(klass, FL_SINGLETON)) {
43
+ return to_class_path(majo_attached_object(klass), str_table);
44
+ }
45
+
46
+ VALUE class_path = (RTEST(klass) && !OBJ_FROZEN(klass)) ? rb_class_path_cached(klass) : Qnil;
47
+ const char *class_path_cstr = RTEST(class_path) ? majo_make_unique_str(str_table, RSTRING_PTR(class_path), RSTRING_LEN(class_path)) : 0;
48
+ return class_path_cstr;
49
+ }
50
+
39
51
  static void
40
52
  newobj_i(VALUE tpval, void *data)
41
53
  {
42
- majo_result *arg = (majo_result *)data;
54
+ VALUE res = (VALUE)data;
55
+ majo_result *arg = majo_check_result(res);
43
56
 
44
57
  rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
45
58
  VALUE obj = rb_tracearg_object(tparg);
@@ -53,22 +66,19 @@ newobj_i(VALUE tpval, void *data)
53
66
  VALUE mid = rb_tracearg_method_id(tparg);
54
67
  VALUE klass = rb_tracearg_defined_class(tparg);
55
68
 
56
- // TODO: when the st already has an entry for the value
57
69
  majo_allocation_info *info = (majo_allocation_info *)malloc(sizeof(majo_allocation_info));
58
70
 
59
71
  const char *path_cstr = RTEST(path) ? majo_make_unique_str(arg->str_table, RSTRING_PTR(path), RSTRING_LEN(path)) : 0;
60
-
61
- VALUE class_path = (RTEST(klass) && !OBJ_FROZEN(klass)) ? rb_class_path_cached(klass) : Qnil;
62
- const char *class_path_cstr = RTEST(class_path) ? majo_make_unique_str(arg->str_table, RSTRING_PTR(class_path), RSTRING_LEN(class_path)) : 0;
72
+ const char *class_path_cstr = to_class_path(klass, arg->str_table);
63
73
 
64
74
  VALUE obj_class = rb_obj_class(obj);
65
- VALUE obj_class_path = (RTEST(obj_class) && !OBJ_FROZEN(obj_class)) ? rb_class_path_cached(obj_class) : Qnil;
66
- const char *obj_class_path_cstr = RTEST(obj_class_path) ? majo_make_unique_str(arg->str_table, RSTRING_PTR(obj_class_path), RSTRING_LEN(obj_class_path)) : 0;
67
-
75
+ const char *obj_class_path_cstr = to_class_path(obj_class, arg->str_table);
68
76
 
77
+ info->result = res;
69
78
  info->path = path_cstr;
70
79
  info->line = NUM2INT(line);
71
80
  info->mid = mid;
81
+ info->singleton_p = FL_TEST(klass, FL_SINGLETON);
72
82
  info->object_class_path = obj_class_path_cstr;
73
83
 
74
84
  info->class_path = class_path_cstr;
@@ -84,12 +94,14 @@ freeobj_i(VALUE tpval, void *data)
84
94
  st_data_t obj = (st_data_t)rb_tracearg_object(tparg);
85
95
  st_data_t v;
86
96
 
87
- // TODO refcount of the strings
88
97
  if (st_delete(arg->object_table, &obj, &v)) {
89
98
  majo_allocation_info *info = (majo_allocation_info *)v;
90
99
  size_t gc_count = rb_gc_count();
91
- // Reject it for majo
92
- if (info->alloc_generation < gc_count-1) {
100
+ int lifetime = (int)(gc_count - info->alloc_generation - 1);
101
+ if (
102
+ (NIL_P(arg->upper_lifetime) || lifetime <= NUM2INT(arg->upper_lifetime)) &&
103
+ (NIL_P(arg->lower_lifetime) || NUM2INT(arg->lower_lifetime) <= lifetime)
104
+ ) {
93
105
  info->memsize = rb_obj_memsize_of((VALUE)obj);
94
106
  info->free_generation = gc_count;
95
107
 
@@ -103,14 +115,16 @@ freeobj_i(VALUE tpval, void *data)
103
115
  }
104
116
 
105
117
  static VALUE
106
- start(VALUE self) {
118
+ start(VALUE self, VALUE upper_lifetime, VALUE lower_lifetime) {
107
119
  VALUE res = majo_new_result();
108
120
  majo_result *arg = majo_check_result(res);
121
+ arg->upper_lifetime = upper_lifetime;
122
+ arg->lower_lifetime = lower_lifetime;
109
123
 
110
124
  VALUE stack = rb_ivar_get(rb_mMajo, running_tracer_stack);
111
125
  rb_ary_push(stack, res);
112
126
 
113
- arg->newobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, arg);
127
+ arg->newobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, (void *)res);
114
128
  arg->freeobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_FREEOBJ, freeobj_i, arg);
115
129
 
116
130
  rb_tracepoint_enable(arg->newobj_trace);
@@ -137,7 +151,7 @@ Init_majo(void)
137
151
  {
138
152
  rb_mMajo = rb_define_module("Majo");
139
153
 
140
- rb_define_module_function(rb_mMajo, "__start", start, 0);
154
+ rb_define_module_function(rb_mMajo, "__start", start, 2);
141
155
  rb_define_module_function(rb_mMajo, "__stop", stop, 0);
142
156
 
143
157
  running_tracer_stack = rb_intern("running_tracer_stack");
@@ -145,4 +159,5 @@ Init_majo(void)
145
159
 
146
160
  majo_init_result();
147
161
  majo_init_allocation_info();
162
+ majo_init_attached_object();
148
163
  }
data/ext/majo/majo.h CHANGED
@@ -4,6 +4,7 @@
4
4
  #include <stdbool.h>
5
5
 
6
6
  #include "ruby.h"
7
+ #include "ruby/version.h"
7
8
  #include "ruby/debug.h"
8
9
  #include "ruby/internal/gc.h"
9
10
 
@@ -12,6 +13,7 @@
12
13
  #include "allocation_info.h"
13
14
  #include "result.h"
14
15
  #include "unique_str.h"
16
+ #include "attached_object.h"
15
17
 
16
18
  extern VALUE rb_mMajo;
17
19
  extern VALUE rb_cMajo_Result;
data/ext/majo/result.c CHANGED
@@ -5,16 +5,40 @@ static void majo_result_mark(void *ptr)
5
5
  majo_result *arg = (majo_result*)ptr;
6
6
  rb_gc_mark(arg->newobj_trace);
7
7
  rb_gc_mark(arg->freeobj_trace);
8
+ rb_gc_mark(arg->retained);
9
+ rb_gc_mark(arg->upper_lifetime);
10
+ rb_gc_mark(arg->lower_lifetime);
11
+ }
12
+
13
+ static int
14
+ free_st_key(st_data_t key, st_data_t value, st_data_t data)
15
+ {
16
+ free((void *)key);
17
+ return ST_CONTINUE;
18
+ }
19
+
20
+ static int
21
+ free_st_value(st_data_t key, st_data_t value, st_data_t data)
22
+ {
23
+ free((void *)value);
24
+ return ST_CONTINUE;
8
25
  }
9
26
 
10
27
  static void majo_result_free(majo_result *arg) {
11
- // TODO
12
- ruby_xfree(arg);
28
+ st_foreach(arg->object_table, free_st_value, 0);
29
+ st_free_table(arg->object_table);
30
+
31
+ st_foreach(arg->str_table, free_st_key, 0);
32
+ st_free_table(arg->str_table);
33
+
34
+ rb_darray_free(arg->olds);
35
+
36
+ free(arg);
13
37
  }
14
38
 
15
39
  static size_t majo_result_memsize(const void *ptr) {
16
- // TODO
17
- return sizeof(majo_result);
40
+ majo_result *res = (majo_result*)ptr;
41
+ return sizeof(majo_result) + st_memsize(res->object_table) + st_memsize(res->str_table) + rb_darray_capa(res->olds) * sizeof(majo_allocation_info);
18
42
  }
19
43
 
20
44
  static rb_data_type_t result_type = {
@@ -29,6 +53,7 @@ static VALUE result_alloc(VALUE klass) {
29
53
  arg->object_table = st_init_numtable();
30
54
  arg->str_table = st_init_strtable();
31
55
  arg->olds = NULL;
56
+ arg->retained = rb_ary_new();
32
57
 
33
58
  return obj;
34
59
  }
@@ -48,7 +73,7 @@ majo_result_append_info(majo_result *res, majo_allocation_info info) {
48
73
  rb_darray_append(&res->olds, info);
49
74
  }
50
75
 
51
- VALUE
76
+ static VALUE
52
77
  majo_result_allocations(VALUE self) {
53
78
  majo_result *res = majo_check_result(self);
54
79
  VALUE ary = rb_ary_new_capa(rb_darray_size(res->olds));
@@ -62,10 +87,37 @@ majo_result_allocations(VALUE self) {
62
87
  return ary;
63
88
  }
64
89
 
90
+ static VALUE
91
+ majo_result_retained(VALUE self) {
92
+ majo_result *res = majo_check_result(self);
93
+ return res->retained;
94
+ }
95
+
96
+ static VALUE
97
+ majo_result_store_retained_object(VALUE self, VALUE obj) {
98
+ majo_result *res = majo_check_result(self);
99
+
100
+ st_data_t value;
101
+ if (st_lookup(res->object_table, (st_data_t)obj, &value)) {
102
+ majo_allocation_info *info = (majo_allocation_info *)value;
103
+ int lifetime = (int)(rb_gc_count() - info->alloc_generation - 1);
104
+ if (NIL_P(res->upper_lifetime) || lifetime <= NUM2INT(res->upper_lifetime)) {
105
+ info->memsize = rb_obj_memsize_of(obj);
106
+
107
+ VALUE info_r = majo_new_allocation_info(info);
108
+ rb_ary_push(res->retained, info_r);
109
+ }
110
+ }
111
+
112
+ return Qnil;
113
+ }
114
+
65
115
  void
66
116
  majo_init_result() {
67
117
  rb_cMajo_Result = rb_define_class_under(rb_mMajo, "Result", rb_cObject);
68
118
  rb_define_alloc_func(rb_cMajo_Result, result_alloc);
69
119
 
70
120
  rb_define_method(rb_cMajo_Result, "allocations", majo_result_allocations, 0);
121
+ rb_define_method(rb_cMajo_Result, "retained", majo_result_retained, 0);
122
+ rb_define_method(rb_cMajo_Result, "store_retained_object", majo_result_store_retained_object, 1);
71
123
  }
data/ext/majo/result.h CHANGED
@@ -7,6 +7,10 @@ typedef struct {
7
7
  rb_darray(majo_allocation_info) olds;
8
8
  VALUE newobj_trace;
9
9
  VALUE freeobj_trace;
10
+ VALUE retained;
11
+
12
+ VALUE upper_lifetime;
13
+ VALUE lower_lifetime;
10
14
  } majo_result;
11
15
 
12
16
  VALUE
@@ -8,32 +8,60 @@ module Majo
8
8
  end
9
9
 
10
10
  def call
11
+ allocs = result.allocations
12
+ retained = result.retained
13
+
11
14
  <<~RESULT
12
- Total #{total_memory} bytes (#{total_objects} objects)
15
+ Total #{total_memory(allocs)} bytes (#{total_objects(allocs)} objects)
16
+ Total retained #{total_memory(retained)} bytes (#{total_objects(retained)} objects)
13
17
 
14
18
  Memory by file
15
19
  #{bar}
16
- #{format_two_columns(memory_by_file)}
20
+ #{format_two_columns(memory_by_file(allocs))}
17
21
 
18
22
  Memory by location
19
23
  #{bar}
20
- #{format_two_columns(memory_by_location)}
24
+ #{format_two_columns(memory_by_location(allocs))}
21
25
 
22
26
  Memory by class
23
27
  #{bar}
24
- #{format_two_columns(memory_by_class)}
28
+ #{format_two_columns(memory_by_class(allocs))}
25
29
 
26
30
  Objects by file
27
31
  #{bar}
28
- #{format_two_columns(objects_by_file)}
32
+ #{format_two_columns(objects_by_file(allocs))}
29
33
 
30
34
  Objects by location
31
35
  #{bar}
32
- #{format_two_columns(objects_by_location)}
36
+ #{format_two_columns(objects_by_location(allocs))}
33
37
 
34
38
  Objects by class
35
39
  #{bar}
36
- #{format_two_columns(objects_by_class)}
40
+ #{format_two_columns(objects_by_class(allocs))}
41
+
42
+ Retained Memory by file
43
+ #{bar}
44
+ #{format_two_columns(memory_by_file(retained))}
45
+
46
+ Retained Memory by location
47
+ #{bar}
48
+ #{format_two_columns(memory_by_location(retained))}
49
+
50
+ Retained Memory by class
51
+ #{bar}
52
+ #{format_two_columns(memory_by_class(retained))}
53
+
54
+ Retained Objects by file
55
+ #{bar}
56
+ #{format_two_columns(objects_by_file(retained))}
57
+
58
+ Retained Objects by location
59
+ #{bar}
60
+ #{format_two_columns(objects_by_location(retained))}
61
+
62
+ Retained Objects by class
63
+ #{bar}
64
+ #{format_two_columns(objects_by_class(retained))}
37
65
  RESULT
38
66
  end
39
67
 
@@ -45,45 +73,45 @@ module Majo
45
73
  cyan '-----------------------------------'
46
74
  end
47
75
 
48
- def total_objects
76
+ def total_objects(allocs)
49
77
  allocs.size
50
78
  end
51
79
 
52
- def total_memory
80
+ def total_memory(allocs)
53
81
  allocs.sum(&:memsize)
54
82
  end
55
83
 
56
- def memory_by_file
84
+ def memory_by_file(allocs)
57
85
  allocs.group_by(&:path).map do |path, allocations|
58
86
  [allocations.sum(&:memsize), path]
59
87
  end.sort_by(&:first).reverse
60
88
  end
61
89
 
62
- def memory_by_location
90
+ def memory_by_location(allocs)
63
91
  allocs.group_by { |a| "#{a.path}:#{a.line}" }.map do |location, allocations|
64
92
  [allocations.sum(&:memsize), location]
65
93
  end.sort_by(&:first).reverse
66
94
  end
67
95
 
68
- def memory_by_class
96
+ def memory_by_class(allocs)
69
97
  allocs.group_by(&:object_class_path).map do |class_path, allocations|
70
98
  [allocations.sum(&:memsize), class_path]
71
99
  end.sort_by(&:first).reverse
72
100
  end
73
101
 
74
- def objects_by_file
102
+ def objects_by_file(allocs)
75
103
  allocs.group_by(&:path).map do |path, allocations|
76
104
  [allocations.size, path]
77
105
  end.sort_by(&:first).reverse
78
106
  end
79
107
 
80
- def objects_by_location
108
+ def objects_by_location(allocs)
81
109
  allocs.group_by { |a| "#{a.path}:#{a.line}" }.map do |location, allocations|
82
110
  [allocations.size, location]
83
111
  end.sort_by(&:first).reverse
84
112
  end
85
113
 
86
- def objects_by_class
114
+ def objects_by_class(allocs)
87
115
  allocs.group_by(&:object_class_path).map do |class_path, allocations|
88
116
  [allocations.size, class_path]
89
117
  end.sort_by(&:first).reverse
@@ -95,10 +123,6 @@ module Majo
95
123
  max_length = data.max_by { |row| row[0].to_s.size }[0].to_s.size
96
124
  data.map { |row| "#{blue(row[0].to_s.rjust(max_length))} #{row[1]}" }.join("\n")
97
125
  end
98
-
99
- def allocs
100
- @allocs ||= result.allocations
101
- end
102
126
  end
103
127
  end
104
128
  end
@@ -9,15 +9,15 @@ module Majo
9
9
 
10
10
  def call
11
11
  ::CSV.generate do |csv|
12
- csv << ['Object class path', 'Class path', 'Method ID', 'Path', 'Line', 'Alloc generation', 'Free generation', 'Memsize', "Count"]
12
+ csv << ['Object class path', 'Class path', 'Method ID', 'Singleton method', 'Path', 'Line', 'Alloc generation', 'Free generation', 'Memsize', "Count"]
13
13
  groups.each do |row, count|
14
- csv << [row.object_class_path, row.class_path, row.method_id, row.path, row.line, row.alloc_generation, row.free_generation, row.memsize, count]
14
+ csv << [row.object_class_path, row.class_path, row.method_id, row.singleton?, row.path, row.line, row.alloc_generation, row.free_generation, row.memsize, count]
15
15
  end
16
16
  end
17
17
  end
18
18
 
19
19
  def groups
20
- @result.allocations.tally
20
+ [*@result.allocations, *@result.retained].tally
21
21
  end
22
22
  end
23
23
  end
data/lib/majo/result.rb CHANGED
@@ -28,7 +28,7 @@ module Majo
28
28
 
29
29
  def with_output(out)
30
30
  case
31
- when out.is_a?(IO)
31
+ when out.is_a?(IO) || (defined?(StringIO) && out.is_a?(StringIO))
32
32
  yield out
33
33
  when out.is_a?(String)
34
34
  File.open(out, "w") { |io| yield io }
data/lib/majo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Majo
4
- VERSION = "0.0.4"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/majo.rb CHANGED
@@ -10,22 +10,27 @@ require_relative 'majo/formatter'
10
10
  module Majo
11
11
  class Error < StandardError; end
12
12
 
13
- def self.start
13
+ def self.start(upper_lifetime: nil, lower_lifetime: 1)
14
14
  GC.start
15
15
  GC.start
16
16
  GC.start
17
- __start
17
+ __start(upper_lifetime, lower_lifetime)
18
18
  end
19
19
 
20
20
  def self.stop
21
21
  GC.start
22
22
  GC.start
23
23
  GC.start
24
- __stop
24
+ res = __stop
25
+
26
+ # Collect retained objcects
27
+ ObjectSpace.each_object do |obj|
28
+ res.store_retained_object(obj)
29
+ end
25
30
  end
26
31
 
27
- def self.run
28
- r = start
32
+ def self.run(upper_lifetime: nil, lower_lifetime: 1)
33
+ r = start(upper_lifetime:, lower_lifetime:)
29
34
  yield
30
35
  r
31
36
  ensure
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: majo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masataka Pocke Kuwabara
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 2024-07-22 00:00:00.000000000 Z
11
+ date: 2024-07-26 00:00:00.000000000 Z
11
12
  dependencies: []
12
13
  description: A memory profiler focusing on long-lived objects.
13
14
  email:
@@ -19,11 +20,15 @@ extra_rdoc_files: []
19
20
  files:
20
21
  - ".rspec"
21
22
  - ".rubocop.yml"
23
+ - ARCHITECTURE.md
24
+ - CHANGELOG.md
22
25
  - LICENSE
23
26
  - README.md
24
27
  - Rakefile
25
28
  - ext/majo/allocation_info.c
26
29
  - ext/majo/allocation_info.h
30
+ - ext/majo/attached_object.c
31
+ - ext/majo/attached_object.h
27
32
  - ext/majo/darray.h
28
33
  - ext/majo/extconf.rb
29
34
  - ext/majo/majo.c
@@ -48,6 +53,8 @@ licenses:
48
53
  metadata:
49
54
  homepage_uri: https://github.com/pocke/majo
50
55
  source_code_uri: https://github.com/pocke/majo
56
+ changelog_uri: https://github.com/pocke/majo/blob/master/CHANGELOG.md
57
+ post_install_message:
51
58
  rdoc_options: []
52
59
  require_paths:
53
60
  - lib
@@ -62,7 +69,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
69
  - !ruby/object:Gem::Version
63
70
  version: '0'
64
71
  requirements: []
65
- rubygems_version: 3.6.0.dev
72
+ rubygems_version: 3.5.11
73
+ signing_key:
66
74
  specification_version: 4
67
75
  summary: A memory profiler focusing on long-lived objects.
68
76
  test_files: []