majo 0.0.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []