majo 0.0.3 → 0.1.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 +4 -4
- data/ARCHITECTURE.md +25 -0
- data/README.md +126 -8
- data/ext/majo/allocation_info.c +2 -3
- data/ext/majo/allocation_info.h +2 -0
- data/ext/majo/majo.c +4 -5
- data/ext/majo/result.c +25 -4
- data/lib/majo/colorize.rb +11 -0
- data/lib/majo/formatter/color.rb +14 -2
- data/lib/majo/formatter/monochrome.rb +15 -0
- data/lib/majo/formatter.rb +1 -0
- data/lib/majo/result.rb +9 -1
- data/lib/majo/version.rb +1 -1
- data/lib/majo.rb +1 -0
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84aa655c3585c95f2e639690246b2946114208ce08b979d00d46e6a93efef60c
|
4
|
+
data.tar.gz: 0073efd2fd643f97219ca17be43259960cfb514ab864cbb6f0b0f9378d3f7a9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d0cb81a092b6c0db17c5db53d2a2404fc554c1545bf8ab7b6546679ba7bd9a64e603af30e2fe7d1d5f90fe0848aebba490918d5101a179b6ac309bf21e3b878
|
7
|
+
data.tar.gz: 630052b3e6c3558c7ce2628a17528518e3770188fca26b26ff1f51a69c6d3d14e339b03814a93173ed134e6565d4e6f1aa1a07249961a70360c8903949943889
|
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/README.md
CHANGED
@@ -1,24 +1,142 @@
|
|
1
|
-
# Majo
|
1
|
+
# Majo🧙♀️
|
2
2
|
|
3
|
-
|
3
|
+
A memory profiler focusing on long-lived objects.
|
4
4
|
|
5
|
-
|
5
|
+
## Motivation
|
6
6
|
|
7
|
-
|
7
|
+
I created this gem because I wanted to reduce the maximum memory usage of a Ruby program.
|
8
|
+
|
9
|
+
The existing memory profiler, `memory_profiler` gem, focuses on allocated and retained memory. This tool is useful to reduce allocations and retain memory, but it is not suitable for reducing the maximum memory usage.
|
8
10
|
|
9
|
-
|
11
|
+
This gem solves this problem by focusing on long-lived objects. Long-lived objects are objects that are not garbage collected for a long time. These objects are the main cause of the maximum memory usage.
|
12
|
+
|
13
|
+
## Installation
|
10
14
|
|
11
15
|
Install the gem and add to the application's Gemfile by executing:
|
12
16
|
|
13
|
-
$ bundle add
|
17
|
+
$ bundle add majo --group development
|
14
18
|
|
15
19
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
20
|
|
17
|
-
$ gem install
|
21
|
+
$ gem install majo
|
18
22
|
|
19
23
|
## Usage
|
20
24
|
|
21
|
-
|
25
|
+
Wrap the code you want to profile with `Majo.run`.
|
26
|
+
Then, call `report` method to display the result.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
result = Majo.run do
|
30
|
+
code_to_profile
|
31
|
+
end
|
32
|
+
|
33
|
+
result.report
|
34
|
+
```
|
35
|
+
|
36
|
+
### Options
|
37
|
+
|
38
|
+
You can pass the following options to `report` method.
|
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` or `monochrome` | `:color`, `:monochrome`, or `:csv` |
|
44
|
+
|
45
|
+
For example:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
result = Majo.run do
|
49
|
+
code_to_profile
|
50
|
+
end
|
51
|
+
|
52
|
+
result.report(out: "majo-result.csv", formatter: :csv)
|
53
|
+
```
|
54
|
+
|
55
|
+
### Result Example
|
56
|
+
|
57
|
+
The result contains only long-lived objects, which are collected by the major GC.
|
58
|
+
|
59
|
+
The example is as follows:
|
60
|
+
|
61
|
+
```
|
62
|
+
Total 15055372 bytes (159976 objects)
|
63
|
+
|
64
|
+
Memory by file
|
65
|
+
-----------------------------------
|
66
|
+
10431760 /path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb
|
67
|
+
1966348 /path/to/gems/rbs-3.5.1/lib/rbs/environment_loader.rb
|
68
|
+
980344 /path/to/gems/rbs-3.5.1/lib/rbs/types.rb
|
69
|
+
(snip)
|
70
|
+
|
71
|
+
Memory by location
|
72
|
+
-----------------------------------
|
73
|
+
10431760 /path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb:20
|
74
|
+
1942812 /path/to/gems/rbs-3.5.1/lib/rbs/environment_loader.rb:159
|
75
|
+
249920 /path/to/gems/rbs-3.5.1/lib/rbs/types.rb:994
|
76
|
+
(snip)
|
77
|
+
|
78
|
+
Memory by class
|
79
|
+
-----------------------------------
|
80
|
+
4274028 String
|
81
|
+
3556632 RBS::Location
|
82
|
+
2197840 Array
|
83
|
+
(snip)
|
84
|
+
|
85
|
+
Objects by file
|
86
|
+
-----------------------------------
|
87
|
+
109126 /path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb
|
88
|
+
20813 /path/to/gems/rbs-3.5.1/lib/rbs/types.rb
|
89
|
+
12236 /path/to/gems/rbs-3.5.1/lib/rbs/environment.rb
|
90
|
+
(snip)
|
91
|
+
|
92
|
+
Objects by location
|
93
|
+
-----------------------------------
|
94
|
+
109126 /path/to/gems/rbs-3.5.1/lib/rbs/parser_aux.rb:20
|
95
|
+
4458 /path/to/gems/rbs-3.5.1/lib/rbs/namespace.rb:24
|
96
|
+
4132 /path/to/gems/rbs-3.5.1/lib/rbs/types.rb:374
|
97
|
+
(snip)
|
98
|
+
|
99
|
+
Objects by class
|
100
|
+
-----------------------------------
|
101
|
+
52495 Array
|
102
|
+
22435 RBS::Location
|
103
|
+
11144 RBS::TypeName
|
104
|
+
(snip)
|
105
|
+
```
|
106
|
+
|
107
|
+
The CSV format is as follows:
|
108
|
+
|
109
|
+
```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
|
114
|
+
(snip)
|
115
|
+
```
|
116
|
+
|
117
|
+
You can find the raw data in the CSV format. It is useful for further analysis. For example: [A spreadsheet for Majo result](https://docs.google.com/spreadsheets/d/1Qe6ZSJ58bNfLbA_eSuL9FJy89taNPt325qAJnLDorOE/edit?gid=533761210#gid=533761210)
|
118
|
+
|
119
|
+
The columns are as follows:
|
120
|
+
|
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 |
|
132
|
+
|
133
|
+
## Name
|
134
|
+
|
135
|
+
The name "Majo" is a Japanese word `魔女` that means "witch". It is a wordplay on the word "major" in "major GC".
|
136
|
+
|
137
|
+
## Thanks
|
138
|
+
|
139
|
+
This gem is inspired by [memory_profiler](https://github.com/SamSaffron/memory_profiler) gem.
|
22
140
|
|
23
141
|
## Development
|
24
142
|
|
data/ext/majo/allocation_info.c
CHANGED
@@ -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
|
-
|
11
|
-
ruby_xfree(info);
|
10
|
+
free(info);
|
12
11
|
}
|
13
12
|
|
14
13
|
static size_t majo_allocation_info_memsize(const void *ptr) {
|
data/ext/majo/allocation_info.h
CHANGED
data/ext/majo/majo.c
CHANGED
@@ -39,7 +39,8 @@ internal_object_p(VALUE obj)
|
|
39
39
|
static void
|
40
40
|
newobj_i(VALUE tpval, void *data)
|
41
41
|
{
|
42
|
-
|
42
|
+
VALUE res = (VALUE)data;
|
43
|
+
majo_result *arg = majo_check_result(res);
|
43
44
|
|
44
45
|
rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
|
45
46
|
VALUE obj = rb_tracearg_object(tparg);
|
@@ -53,7 +54,6 @@ newobj_i(VALUE tpval, void *data)
|
|
53
54
|
VALUE mid = rb_tracearg_method_id(tparg);
|
54
55
|
VALUE klass = rb_tracearg_defined_class(tparg);
|
55
56
|
|
56
|
-
// TODO: when the st already has an entry for the value
|
57
57
|
majo_allocation_info *info = (majo_allocation_info *)malloc(sizeof(majo_allocation_info));
|
58
58
|
|
59
59
|
const char *path_cstr = RTEST(path) ? majo_make_unique_str(arg->str_table, RSTRING_PTR(path), RSTRING_LEN(path)) : 0;
|
@@ -66,6 +66,7 @@ newobj_i(VALUE tpval, void *data)
|
|
66
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
67
|
|
68
68
|
|
69
|
+
info->result = res;
|
69
70
|
info->path = path_cstr;
|
70
71
|
info->line = NUM2INT(line);
|
71
72
|
info->mid = mid;
|
@@ -84,11 +85,9 @@ freeobj_i(VALUE tpval, void *data)
|
|
84
85
|
st_data_t obj = (st_data_t)rb_tracearg_object(tparg);
|
85
86
|
st_data_t v;
|
86
87
|
|
87
|
-
// TODO refcount of the strings
|
88
88
|
if (st_delete(arg->object_table, &obj, &v)) {
|
89
89
|
majo_allocation_info *info = (majo_allocation_info *)v;
|
90
90
|
size_t gc_count = rb_gc_count();
|
91
|
-
// Reject it for majo
|
92
91
|
if (info->alloc_generation < gc_count-1) {
|
93
92
|
info->memsize = rb_obj_memsize_of((VALUE)obj);
|
94
93
|
info->free_generation = gc_count;
|
@@ -110,7 +109,7 @@ start(VALUE self) {
|
|
110
109
|
VALUE stack = rb_ivar_get(rb_mMajo, running_tracer_stack);
|
111
110
|
rb_ary_push(stack, res);
|
112
111
|
|
113
|
-
arg->newobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i,
|
112
|
+
arg->newobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, (void *)res);
|
114
113
|
arg->freeobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_FREEOBJ, freeobj_i, arg);
|
115
114
|
|
116
115
|
rb_tracepoint_enable(arg->newobj_trace);
|
data/ext/majo/result.c
CHANGED
@@ -7,14 +7,35 @@ static void majo_result_mark(void *ptr)
|
|
7
7
|
rb_gc_mark(arg->freeobj_trace);
|
8
8
|
}
|
9
9
|
|
10
|
+
static int
|
11
|
+
free_st_key(st_data_t key, st_data_t value, st_data_t data)
|
12
|
+
{
|
13
|
+
free((void *)key);
|
14
|
+
return ST_CONTINUE;
|
15
|
+
}
|
16
|
+
|
17
|
+
static int
|
18
|
+
free_st_value(st_data_t key, st_data_t value, st_data_t data)
|
19
|
+
{
|
20
|
+
free((void *)value);
|
21
|
+
return ST_CONTINUE;
|
22
|
+
}
|
23
|
+
|
10
24
|
static void majo_result_free(majo_result *arg) {
|
11
|
-
|
12
|
-
|
25
|
+
st_foreach(arg->object_table, free_st_value, 0);
|
26
|
+
st_free_table(arg->object_table);
|
27
|
+
|
28
|
+
st_foreach(arg->str_table, free_st_key, 0);
|
29
|
+
st_free_table(arg->str_table);
|
30
|
+
|
31
|
+
rb_darray_free(arg->olds);
|
32
|
+
|
33
|
+
free(arg);
|
13
34
|
}
|
14
35
|
|
15
36
|
static size_t majo_result_memsize(const void *ptr) {
|
16
|
-
|
17
|
-
return sizeof(majo_result);
|
37
|
+
majo_result *res = (majo_result*)ptr;
|
38
|
+
return sizeof(majo_result) + st_memsize(res->object_table) + st_memsize(res->str_table) + rb_darray_capa(res->olds) * sizeof(majo_allocation_info);
|
18
39
|
}
|
19
40
|
|
20
41
|
static rb_data_type_t result_type = {
|
data/lib/majo/formatter/color.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Majo
|
2
2
|
module Formatter
|
3
3
|
class Color
|
4
|
+
include Colorize
|
5
|
+
|
4
6
|
def initialize(result)
|
5
7
|
@result = result
|
6
8
|
end
|
@@ -10,21 +12,27 @@ module Majo
|
|
10
12
|
Total #{total_memory} bytes (#{total_objects} objects)
|
11
13
|
|
12
14
|
Memory by file
|
15
|
+
#{bar}
|
13
16
|
#{format_two_columns(memory_by_file)}
|
14
17
|
|
15
18
|
Memory by location
|
19
|
+
#{bar}
|
16
20
|
#{format_two_columns(memory_by_location)}
|
17
21
|
|
18
22
|
Memory by class
|
23
|
+
#{bar}
|
19
24
|
#{format_two_columns(memory_by_class)}
|
20
25
|
|
21
26
|
Objects by file
|
27
|
+
#{bar}
|
22
28
|
#{format_two_columns(objects_by_file)}
|
23
29
|
|
24
30
|
Objects by location
|
31
|
+
#{bar}
|
25
32
|
#{format_two_columns(objects_by_location)}
|
26
33
|
|
27
34
|
Objects by class
|
35
|
+
#{bar}
|
28
36
|
#{format_two_columns(objects_by_class)}
|
29
37
|
RESULT
|
30
38
|
end
|
@@ -33,6 +41,10 @@ module Majo
|
|
33
41
|
|
34
42
|
attr_reader :result
|
35
43
|
|
44
|
+
def bar
|
45
|
+
cyan '-----------------------------------'
|
46
|
+
end
|
47
|
+
|
36
48
|
def total_objects
|
37
49
|
allocs.size
|
38
50
|
end
|
@@ -80,8 +92,8 @@ module Majo
|
|
80
92
|
def format_two_columns(data)
|
81
93
|
return "" if data.empty?
|
82
94
|
|
83
|
-
max_length = data.max_by { |row| row[0].to_s.size }[0].size
|
84
|
-
data.map { |row| "#{row[0].to_s.
|
95
|
+
max_length = data.max_by { |row| row[0].to_s.size }[0].to_s.size
|
96
|
+
data.map { |row| "#{blue(row[0].to_s.rjust(max_length))} #{row[1]}" }.join("\n")
|
85
97
|
end
|
86
98
|
|
87
99
|
def allocs
|
data/lib/majo/formatter.rb
CHANGED
data/lib/majo/result.rb
CHANGED
@@ -3,10 +3,18 @@ module Majo
|
|
3
3
|
def report(out: $stdout, formatter: nil)
|
4
4
|
fmt =
|
5
5
|
case formatter
|
6
|
-
when
|
6
|
+
when :color
|
7
7
|
Formatter::Color
|
8
8
|
when :csv
|
9
9
|
Formatter::CSV
|
10
|
+
when :monochrome
|
11
|
+
Formatter::Monochrome
|
12
|
+
when nil
|
13
|
+
if out.respond_to?(:tty?) && out.tty?
|
14
|
+
Formatter::Color
|
15
|
+
else
|
16
|
+
Formatter::Monochrome
|
17
|
+
end
|
10
18
|
else
|
11
19
|
raise "unknown formatter: #{formatter.inspect}"
|
12
20
|
end
|
data/lib/majo/version.rb
CHANGED
data/lib/majo.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: majo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.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-
|
11
|
+
date: 2024-07-25 00:00:00.000000000 Z
|
11
12
|
dependencies: []
|
12
|
-
description:
|
13
|
+
description: A memory profiler focusing on long-lived objects.
|
13
14
|
email:
|
14
15
|
- kuwabara@pocke.me
|
15
16
|
executables: []
|
@@ -19,6 +20,7 @@ extra_rdoc_files: []
|
|
19
20
|
files:
|
20
21
|
- ".rspec"
|
21
22
|
- ".rubocop.yml"
|
23
|
+
- ARCHITECTURE.md
|
22
24
|
- LICENSE
|
23
25
|
- README.md
|
24
26
|
- Rakefile
|
@@ -34,9 +36,11 @@ files:
|
|
34
36
|
- ext/majo/unique_str.h
|
35
37
|
- lib/majo.rb
|
36
38
|
- lib/majo/allocation_info.rb
|
39
|
+
- lib/majo/colorize.rb
|
37
40
|
- lib/majo/formatter.rb
|
38
41
|
- lib/majo/formatter/color.rb
|
39
42
|
- lib/majo/formatter/csv.rb
|
43
|
+
- lib/majo/formatter/monochrome.rb
|
40
44
|
- lib/majo/result.rb
|
41
45
|
- lib/majo/version.rb
|
42
46
|
- sig/majo.rbs
|
@@ -46,6 +50,7 @@ licenses:
|
|
46
50
|
metadata:
|
47
51
|
homepage_uri: https://github.com/pocke/majo
|
48
52
|
source_code_uri: https://github.com/pocke/majo
|
53
|
+
post_install_message:
|
49
54
|
rdoc_options: []
|
50
55
|
require_paths:
|
51
56
|
- lib
|
@@ -60,7 +65,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
65
|
- !ruby/object:Gem::Version
|
61
66
|
version: '0'
|
62
67
|
requirements: []
|
63
|
-
rubygems_version: 3.
|
68
|
+
rubygems_version: 3.5.11
|
69
|
+
signing_key:
|
64
70
|
specification_version: 4
|
65
|
-
summary:
|
71
|
+
summary: A memory profiler focusing on long-lived objects.
|
66
72
|
test_files: []
|