Rubyのメモリを調査したい

# Ruby のメモリの話

## メモリサイズを計測したりしたい

### ps を使う

```rb
rss = `ps -o rss= -p #{Process.pid}`.to_i * 0.001
vsz = `ps -o vsz= -p #{Process.pid}`.to_i * 0.001
puts "Process: #{Process.pid}: RSS = #{rss} MB, VSZ = #{vsz} MB"
```

### オブジェクト数を使う

```rb
puts "#{ObjectSpace.memsize_of_all * 0.001 * 0.001 } MB"
```

### 試してみる

```ps.rb
#!/usr/bin/env ruby

require 'objspace'
require 'benchmark'

def mem_by_ps
  rss = `ps -o rss= -p #{Process.pid}`.to_i * 0.001
  vsz = `ps -o vsz= -p #{Process.pid}`.to_i * 0.001
  puts "Process: #{Process.pid}: RSS = #{sprintf('%0.3f', rss)} MB, VSZ = #{sprintf('%0.3f', vsz)} MB"
  [rss, vsz]
end

def mem_by_obj
  obj = ObjectSpace.memsize_of_all * 0.001 * 0.001
  puts "objspace: #{sprintf('%0.6f', obj)} MB"
  obj
end

def mem_wrap
  puts "Ruby version: #{RUBY_VERSION}"

  before = {
    rss: 0,
    vsz: 0,
    obj: 0,
  }
  after = {
    rss: 0,
    vsz: 0,
    obj: 0,
  }

  before[:rss], before[:vsz] = mem_by_ps
  before[:obj] = mem_by_obj


  bm = Benchmark.measure { yield }

  after[:rss], after[:vsz] = mem_by_ps
  after[:obj] = mem_by_obj

  puts "RSS: #{sprintf('%.3f', after[:rss] - before[:rss])} MB"
  puts "VSZ: #{sprintf('%.3f', after[:vsz] - before[:vsz])} MB"
  puts "OBJ: #{sprintf('%.3f', after[:obj] - before[:obj])} MB"

  puts Benchmark::CAPTION
  puts bm
end

if File.basename($0) == 'ps.rb'
  mem_wrap do
    a = Array.new(100_000_000, 0)
  end
end
```

```
$ rbenv exec ruby ps.rb
Ruby version: 2.7.4
Process: 8353: RSS = 12.984 MB, VSZ = 34651.104 MB
objspace: 2.238455 MB
Process: 8353: RSS = 794.288 MB, VSZ = 35432.356 MB
objspace: 802.231552 MB
RSS: 781.304 MB
VSZ: 781.252 MB
OBJ: 799.993 MB
      user     system      total        real
  0.188704   0.225386   0.414090 (  0.416024)
```

例えば，`gsub!`を見てみる

```gsub.rb
#!/usr/bin/env ruby

require './ps'

str = "a" * ARGV[0].to_i

mem_wrap do
  str.gsub!(/a/, 'b')
end
```

```
$ make gsub1
for v in 2.7.4 2.7.6 3.0.2 3.0.3 3.0.4; do echo $v; RBENV_VERSION=$v time rbenv exec ruby gsub.rb 10000000; done
2.7.4
Ruby version: 2.7.4
Process: 71057: RSS = 23.092 MB, VSZ = 34414.088 MB
objspace: 12.349771 MB
Process: 71057: RSS = 32.900 MB, VSZ = 34423.856 MB
objspace: 22.215229 MB
RSS: 9.808 MB
VSZ: 9.768 MB
OBJ: 9.865 MB
        1.85 real         1.61 user         0.10 sys
2.7.6
Ruby version: 2.7.6
Process: 71163: RSS = 22.772 MB, VSZ = 34684.376 MB
objspace: 12.350140 MB
Process: 71163: RSS = 32.580 MB, VSZ = 34694.144 MB
objspace: 22.215430 MB
RSS: 9.808 MB
VSZ: 9.768 MB
OBJ: 9.865 MB
        1.85 real         1.58 user         0.11 sys
3.0.2
Ruby version: 3.0.2
Process: 71183: RSS = 22.916 MB, VSZ = 34414.440 MB
objspace: 11.647444 MB
Process: 71183: RSS = 32.724 MB, VSZ = 34424.208 MB
objspace: 21.319548 MB
RSS: 9.808 MB
VSZ: 9.768 MB
OBJ: 9.672 MB
        1.81 real         1.56 user         0.10 sys
3.0.3
Ruby version: 3.0.3
Process: 71198: RSS = 22.972 MB, VSZ = 34413.416 MB
objspace: 11.524093 MB
Process: 71198: RSS = 43.188 MB, VSZ = 34428.304 MB
objspace: 21.925663 MB
RSS: 20.216 MB
VSZ: 14.888 MB
OBJ: 10.402 MB
        9.35 real         8.86 user         0.26 sys
3.0.4
Ruby version: 3.0.4
Process: 71277: RSS = 22.724 MB, VSZ = 34545.528 MB
objspace: 11.532189 MB
Process: 71277: RSS = 41.352 MB, VSZ = 34558.368 MB
objspace: 22.665408 MB
RSS: 18.628 MB
VSZ: 12.840 MB
OBJ: 11.133 MB
        9.46 real         8.95 user         0.27 sys
```

<details><summary>他ファイル</summary>

```Makefile:Makefile
VERSIONS = $(shell rbenv versions | perl -lne '/(\d\.\d\.\d)/&&print$$1')

DOCKER_COMPOSE = docker compose
DOCKER_COMPOSE_RUN = $(DOCKER_COMPOSE) run --rm app

RUBY = rbenv exec ruby

define RUBY_RUNS
docker-$(1):
	$(DOCKER_COMPOSE_RUN) make $(1)
$(1):
	for v in $$(VERSIONS); do echo $$$$v; RBENV_VERSION=$$$$v $$(RUBY) $(2); done
endef

# require space for split
define EXEC_DEFINES
  version:-v
  ps:ps.rb
  gsub1:gsub.rb:10000000
  gsub5:gsub.rb:50000000
endef

default: docker-version

$(foreach kv,$(EXEC_DEFINES),$(eval $(call RUBY_RUNS,$(shell echo $(kv) | cut -d: -f1),$(shell echo $(kv) | cut -d: -f2- | sed -e 's/:/ /g'))))
```

```compose.yml
version: '3'
services:
  app:
    build:
      context: .
    volumes:
      - .:/usr/src/app
    environment:
      - RBENV_VERSION
```

```Dockerfile:Dockerfile
FROM debian:bookworm-20231120

ARG RUBY_VERSIONS='2.7.6 3.0.4 3.1.2'
ARG RUBY_BUILD_VERSION=v20231114

WORKDIR /usr/src/app

RUN apt-get update && apt-get install -y \
rbenv \
git \
procps \
&& rm -rf /var/lib/apt/lists/*

RUN git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build \
&& git -C  "$(rbenv root)"/plugins/ruby-build switch -c ${RUBY_BUILD_VERSION} ${RUBY_BUILD_VERSION}

RUN for v in ${RUBY_VERSIONS}; do rbenv install $v; done
```

</details>

## もう少し調べてみる

memprof2 を追加しました

<details><summary>追加した</summary>

```ps.rb
#!/usr/bin/env ruby

require 'objspace'
require 'benchmark'
require 'memprof2'

def mem_by_ps
  rss = `ps -o rss= -p #{Process.pid}`.to_i * 0.001
  vsz = `ps -o vsz= -p #{Process.pid}`.to_i * 0.001
  puts "Process: #{Process.pid}: RSS = #{sprintf('%0.3f', rss)} MB, VSZ = #{sprintf('%0.3f', vsz)} MB"
  [rss, vsz]
end

def mem_by_obj
  obj = ObjectSpace.memsize_of_all * 0.001 * 0.001
  puts "objspace: #{sprintf('%0.6f', obj)} MB"
  obj
end

def mem_wrap
  puts "Ruby version: #{RUBY_VERSION}"

  before = {
    rss: 0,
    vsz: 0,
    obj: 0,
  }
  after = {
    rss: 0,
    vsz: 0,
    obj: 0,
  }

  before[:rss], before[:vsz] = mem_by_ps
  before[:obj] = mem_by_obj


  bm = Benchmark.measure do
    Memprof2.start

    yield

    Memprof2.report
    Memprof2.stop
  end

  after[:rss], after[:vsz] = mem_by_ps
  after[:obj] = mem_by_obj

  puts "RSS: #{sprintf('%.3f', after[:rss] - before[:rss])} MB"
  puts "VSZ: #{sprintf('%.3f', after[:vsz] - before[:vsz])} MB"
  puts "OBJ: #{sprintf('%.3f', after[:obj] - before[:obj])} MB"

  puts Benchmark::CAPTION
  puts bm
end

if File.basename($0) == 'ps.rb'
  mem_wrap do
    a = Array.new(100_000_000, 0)
  end
end
```

</details>

結果

```
$ make docker-gsub1
docker compose run --rm app make gsub1
for v in 2.7.6 3.0.4 3.1.2; do echo $v; RBENV_VERSION=$v rbenv exec ruby gsub.rb 10000000; done
2.7.6
Ruby version: 2.7.6
Process: 86: RSS = 22.784 MB, VSZ = 83.672 MB
objspace: 12.304341 MB
10000121 gsub.rb:8:String
336 gsub.rb:8:MatchData
Process: 86: RSS = 32.640 MB, VSZ = 93.592 MB
objspace: 22.296744 MB
RSS: 9.856 MB
VSZ: 9.920 MB
OBJ: 9.992 MB
      user     system      total        real
  1.656802   0.004753   1.661555 (  1.661600)
3.0.4
Ruby version: 3.0.4
Process: 114: RSS = 23.168 MB, VSZ = 84.272 MB
objspace: 11.504425 MB
231336 gsub.rb:8:MatchData
10014925 gsub.rb:8:String
Process: 114: RSS = 36.748 MB, VSZ = 97.728 MB
objspace: 22.434818 MB
RSS: 13.580 MB
VSZ: 13.456 MB
OBJ: 10.930 MB
      user     system      total        real
 12.587906   0.038369  12.626275 ( 12.626842)
3.1.2
Ruby version: 3.1.2
Process: 142: RSS = 23.680 MB, VSZ = 84.664 MB
objspace: 11.903804 MB
582624 gsub.rb:8:MatchData
10090841 gsub.rb:8:String
Process: 142: RSS = 37.172 MB, VSZ = 98.248 MB
objspace: 22.871224 MB
RSS: 13.492 MB
VSZ: 13.584 MB
OBJ: 10.967 MB
      user     system      total        real
 12.153679   0.042324  12.196003 ( 12.196610)
```

- MatchData が増えているのがわかる

# Refs

https://qiita.com/sonots/items/c14b3e3ca8e6f7dfb651

https://blog.freedom-man.com/measure-ruby-memory-usage