majo 0.0.3 → 0.1.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: 30b12103910ff82b8073d3882debca86928bc05d03e318b8c640e0c3b6f4c6ee
4
- data.tar.gz: 851e43d87d7ba4020b5eb04487ae9859522bba5bd7f7a6765d02432efe2b8d74
3
+ metadata.gz: 84aa655c3585c95f2e639690246b2946114208ce08b979d00d46e6a93efef60c
4
+ data.tar.gz: 0073efd2fd643f97219ca17be43259960cfb514ab864cbb6f0b0f9378d3f7a9f
5
5
  SHA512:
6
- metadata.gz: ee86c8d4f321e54646ec82ee79168a8071ff641be47f9fd006ab70c9f254fbd958e6667c1e9dde36b00e911eee17d970a09a785dd53912f9bc4383101e649957
7
- data.tar.gz: 3b707eee86c906409bd26f9b15e0e35aa1e6516e566b7d0f7e526b4fc76d74478110f75aa77b23041133d8b966999882c46d60515617121d7eec9cd41d9c3ac1
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
- TODO: Delete this and the text below, and describe your gem
3
+ A memory profiler focusing on long-lived objects.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/majo`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ## Motivation
6
6
 
7
- ## Installation
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
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ $ gem install majo
18
22
 
19
23
  ## Usage
20
24
 
21
- TODO: Write usage instructions here
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
 
@@ -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) {
@@ -11,6 +11,8 @@ typedef struct {
11
11
  size_t alloc_generation;
12
12
  size_t free_generation;
13
13
  size_t memsize;
14
+
15
+ VALUE result;
14
16
  } majo_allocation_info;
15
17
 
16
18
  VALUE
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
- majo_result *arg = (majo_result *)data;
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, arg);
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
- // TODO
12
- ruby_xfree(arg);
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
- // TODO
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 = {
@@ -0,0 +1,11 @@
1
+ module Majo
2
+ module Colorize
3
+ def blue(str)
4
+ "\e[34m#{str}\e[0m"
5
+ end
6
+
7
+ def cyan(str)
8
+ "\e[36m#{str}\e[0m"
9
+ end
10
+ end
11
+ end
@@ -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.ljust(max_length)} #{row[1]}" }.join("\n")
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
@@ -0,0 +1,15 @@
1
+ module Majo
2
+ module Formatter
3
+ class Monochrome < Color
4
+ private
5
+
6
+ def blue(str)
7
+ str
8
+ end
9
+
10
+ def cyan(str)
11
+ str
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,6 +1,7 @@
1
1
  module Majo
2
2
  module Formatter
3
3
  autoload :Color, 'majo/formatter/color'
4
+ autoload :Monochrome, 'majo/formatter/monochrome'
4
5
  autoload :CSV, 'majo/formatter/csv'
5
6
  end
6
7
  end
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 nil, :color
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Majo
4
- VERSION = "0.0.3"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/majo.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'majo.so'
4
4
  require_relative "majo/version"
5
+ require_relative "majo/colorize"
5
6
  require_relative 'majo/allocation_info'
6
7
  require_relative 'majo/result'
7
8
  require_relative 'majo/formatter'
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.3
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-22 00:00:00.000000000 Z
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.6.0.dev
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: []