diff --git a/heapy/.DS_Store b/heapy/.DS_Store new file mode 100644 index 0000000000..2663c78152 Binary files /dev/null and b/heapy/.DS_Store differ diff --git a/heapy/.gitignore b/heapy/.gitignore new file mode 100644 index 0000000000..c7d90259ec --- /dev/null +++ b/heapy/.gitignore @@ -0,0 +1,11 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +tmp +.DS_Store diff --git a/heapy/.rspec b/heapy/.rspec new file mode 100644 index 0000000000..8c18f1abdd --- /dev/null +++ b/heapy/.rspec @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/heapy/.travis.yml b/heapy/.travis.yml new file mode 100644 index 0000000000..643e21b688 --- /dev/null +++ b/heapy/.travis.yml @@ -0,0 +1,4 @@ +language: ruby +rvm: + - 2.3.2 +before_install: gem install bundler -v 1.10.6 diff --git a/heapy/CHANGELOG.md b/heapy/CHANGELOG.md new file mode 100644 index 0000000000..2180273631 --- /dev/null +++ b/heapy/CHANGELOG.md @@ -0,0 +1,15 @@ +## 0.1.4 - 2018-07-25 + +- Bundler is no longer required so heapy can now be used via a simple +gem install. + +## 0.1.3 - 2017-09-07 + +- I'm really bad at keeping a log of changes on this project sorry +- Printing out the memory size for a generation +- Printing out the total number of objects for the heap in the summary + +## 0.1.1 - 2015-10-15 + +- Less memory retention when parsing large heap dumps. +- Improved output format. diff --git a/heapy/CODE_OF_CONDUCT.md b/heapy/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..ce9bee75b0 --- /dev/null +++ b/heapy/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) diff --git a/heapy/Gemfile b/heapy/Gemfile new file mode 100644 index 0000000000..b193ab29d4 --- /dev/null +++ b/heapy/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in heapy.gemspec +gemspec diff --git a/heapy/LICENSE.txt b/heapy/LICENSE.txt new file mode 100644 index 0000000000..d68e51b6e9 --- /dev/null +++ b/heapy/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 schneems + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/heapy/README.md b/heapy/README.md new file mode 100644 index 0000000000..ae5733c14e --- /dev/null +++ b/heapy/README.md @@ -0,0 +1,101 @@ +# Heapy (Ruby Heap Dump Inspector) +[![Help Contribute to Open Source](https://www.codetriage.com/schneems/heapy/badges/users.svg)](https://www.codetriage.com/schneems/heapy) ![Supports Ruby 2.3+](https://img.shields.io/badge/ruby-2.3+-green.svg) + +A CLI for analyzing Ruby Heap dumps. Thanks to [Sam Saffron](http://samsaffron.com/archive/2015/03/31/debugging-memory-leaks-in-ruby) for the idea and initial code. + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'heapy' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install heapy + +## Usage + +Step 1) Generate a heap dump. You could [do this manually](http://samsaffron.com/archive/2015/03/31/debugging-memory-leaks-in-ruby). Or you can use a tool like [derailed_benchmarks](https://github.com/schneems/derailed_benchmarks) + +Step 2) Once you've got the heap dump, you can analyze it using this CLI: + +``` +$ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump + +Generation: nil object count: 209191 +Generation: 14 object count: 407 +Generation: 15 object count: 638 +Generation: 16 object count: 748 +Generation: 17 object count: 1023 +Generation: 18 object count: 805 +# ... +``` + +NOTE: The reason you may be getting a "nil" generation is these objects were loaded into memory before your code began tracking the allocations. To ensure all allocations are tracked you can execute your ruby script this trick. First create a file `trace.rb` that only starts allocation tracing: + +``` +# trace.rb +require 'objspace' + +ObjectSpace.trace_object_allocations_start +``` + +Now make sure this command is loaded before you run your script, you can use Ruby's `-I` to specify a load path and `-r` to specify a library to require, in this case our trace file + +``` +$ ruby -I ./ -r trace script_name.rb +``` + +If the last line of your file is invalid JSON, make sure that you are closing the file after writing the ruby heap dump to it. + +## Digging into a Generation + +You can drill down into a specific generation. In the previous example, the 17'th generation looks strangely large, you can drill into it: + + +``` +$ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump 17 + Analyzing Heap (Generation: 17) + ------------------------------- + + allocated by memory (44061517) (in bytes) + ============================== + 39908512 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/timeout.rb:79 + 1284993 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/openssl/buffering.rb:182 + 201068 /app/vendor/bundle/ruby/2.2.0/gems/json-1.8.3/lib/json/common.rb:223 + 189272 /app/vendor/bundle/ruby/2.2.0/gems/newrelic_rpm-3.13.2.302/lib/new_relic/agent/stats_engine/stats_hash.rb:39 + 172531 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/net/http/header.rb:172 + 92200 /app/vendor/bundle/ruby/2.2.0/gems/activesupport-4.2.3/lib/active_support/core_ext/numeric/conversions.rb:131 +``` + +### Reviewing all generations + +If you want to read all generations you can use the "all" directive + +``` +$ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump all +``` + +You can also use T-Lo's online JS based [Heap Analyzer](http://tenderlove.github.io/heap-analyzer/) for visualizations. + +## Development + +After checking out the repo, run `$ bundle install` to install dependencies. Then, run `rake spec` to run the tests. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/schneems/heapy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct. + + +## License + +The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). + diff --git a/heapy/Rakefile b/heapy/Rakefile new file mode 100644 index 0000000000..303c40f549 --- /dev/null +++ b/heapy/Rakefile @@ -0,0 +1,7 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec +task :test => :spec \ No newline at end of file diff --git a/heapy/bin/heapy b/heapy/bin/heapy new file mode 100755 index 0000000000..39af775319 --- /dev/null +++ b/heapy/bin/heapy @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +require "bundler/setup" if defined?(Bundler) +require "heapy" + + +Heapy::CLI.new(ARGV).run diff --git a/heapy/heapy.gemspec b/heapy/heapy.gemspec new file mode 100644 index 0000000000..580a0b8e9d --- /dev/null +++ b/heapy/heapy.gemspec @@ -0,0 +1,27 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'heapy/version' + +Gem::Specification.new do |spec| + spec.name = "heapy" + spec.version = Heapy::VERSION + spec.authors = ["schneems"] + spec.email = ["richard.schneeman@gmail.com"] + + spec.summary = %q{Inspects Ruby heap dumps} + spec.description = %q{Got a heap dump? Great. Use this tool to see what's in it!} + spec.homepage = "https://github.com/schneems/heapy" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.bindir = "bin" + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.10" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec" + + spec.required_ruby_version = '~> 2.3' +end diff --git a/heapy/lib/heapy.rb b/heapy/lib/heapy.rb new file mode 100644 index 0000000000..5abf15b227 --- /dev/null +++ b/heapy/lib/heapy.rb @@ -0,0 +1,70 @@ +require 'json' + +require "heapy/version" + +module Heapy + + class CLI + def initialize(argv) + @cmd = argv.shift + @file = argv.shift + @number = argv.shift + @args = argv + end + + def help + puts <<-HALP +$ heapy read + +When run with only a file, it will output the generation and count pairs: + + $ heapy read tmp/2015-09-30-heap.dump + Generation: nil object count: 209191 + Generation: 14 object count: 407 + Generation: 15 object count: 638 + Generation: 16 object count: 748 + Generation: 17 object count: 1023 + Generation: 18 object count: 805 + +When run with a file and a number it will output detailed information for that +generation: + + $ heapy read tmp/2015-09-30-heap.dump 17 + + Analyzing Heap (Generation: 17) + ------------------------------- + +allocated by memory (44061517) (in bytes) +============================== + 39908512 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/timeout.rb:79 + 1284993 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/openssl/buffering.rb:182 + 201068 /app/vendor/bundle/ruby/2.2.0/gems/json-1.8.3/lib/json/common.rb:223 + 189272 /app/vendor/bundle/ruby/2.2.0/gems/newrelic_rpm-3.13.2.302/lib/new_relic/agent/stats_engine/stats_hash.rb:39 + 172531 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/net/http/header.rb:172 + 92200 /app/vendor/bundle/ruby/2.2.0/gems/activesupport-4.2.3/lib/active_support/core_ext/numeric/conversions.rb:131 +HALP + end + + def run + + case @cmd + when "--help" + help + when nil + help + when "read" + if @number + Analyzer.new(@file).drill_down(@number) + else + Analyzer.new(@file).analyze + end + else + help + end + end + end +end + +require 'heapy/analyzer' +require 'heapy/alive' + diff --git a/heapy/lib/heapy/alive.rb b/heapy/lib/heapy/alive.rb new file mode 100644 index 0000000000..208849cdb2 --- /dev/null +++ b/heapy/lib/heapy/alive.rb @@ -0,0 +1,269 @@ +require 'objspace' +require 'stringio' + +module Heapy + + # This is an experimental module and likely to change. Don't use in production. + # + # Use at your own risk. APIs are not stable. + # + # == What + # + # You can use it to trace objects to see if they are still "alive" in memory. + # Unlike the heapy CLI this is meant to be used in live running code. + # + # This works by retaining an object's address in memory, then running GC + # and taking a heap dump. If the object exists in the heap dump, it is retained. + # Since we have the whole heap dump we can also do things like find what is retaining + # your object preventing it from being collected. + # + # == Use It + # + # You need to first start tracing objects: + # + # Heapy::Alive.start_object_trace!(heap_file: "./tmp/heap.json") + # + # Next in your code you want to specify the object ato trace + # + # string = "hello world" + # Heapy::Alive.trace_without_retain(string) + # + # When the code is done executing you can get a reference to all "tracer" + # objects by running: + # + # Heapy::Alive.traced_objects.each do |tracer| + # puts tracer.raw_json_hash if tracer.object_retained? + # end + # + # A few helpful methods on `tracer` objects: + # + # - `raw_json_hash` returns the hash of the object from the heap dump. + # - `object_retained?` returns truthy if the object was still present in the heap dump. + # - `address` a string of the memory address of the object you're tracing. + # - `tracked_to_s` a string that represents the object you're tracing (default + # is result of calling inspect on the method). You can pass in a custom representation + # when initializing the object. Can be useful for when `inspect` on the object you + # are tracing is too verbose. + # - `id2ref` returns the original object being traced (if it is still in memory). + # - `root?` returns false if the tracer isn't the root object. + # + # See `ObjectTracker` for more methods. + # + # If you want to see what retains an object, you can use `ObectTracker#retained_by` + # method (caution this is extremely expensive and requires re-walking the whole heap dump: + # + # Heapy::Alive.traced_objects.each do |tracer| + # if tracer.object_retained? + # puts "Traced: #{tracer.raw_json_hash}" + # tracer.retained_by.each do |retainer| + # puts " Retained by: #{retainer.raw_json_hash}" + # end + # end + # end + # + # You can iterate up the whole retained tree by using the `retained_by` method on tracers + # returned. But again it's expensive. If you have large heap dump or if you're tracing a bunch + # of objects, continuously calling `retained_by` will take lots of time. We also don't + # do any circular dependency detection so if you have two objects that depend on each other, + # you may hit an infinite loop. + # + # If you know that you'll need the retained objects of the main objects you're tracing you can + # save re-walking the heap the first N times by using the `retained_by` flag: + # + # Heapy::Alive.traced_objects(retained_by: true) do |tracer| + # # ... + # end + # + # This will pre-fetch the first level of "parents" for each object you're tracing. + # + # Did I mention this is all experimental and may change? + module Alive + @mutex = Mutex.new + @retain_hash = {} + @heap_file = nil + @started = false + + def self.address_to_object(address) + obj_id = address.to_i(16) / 2 + ObjectSpace._id2ref(obj_id) + rescue RangeError + nil + end + + def self.start_object_trace!(heap_file: "./tmp/heap.json") + @mutex.synchronize do + @started ||= true && ObjectSpace.trace_object_allocations_start + @heap_file ||= heap_file + end + end + + def self.trace_without_retain(object, to_s: nil) + tracker = ObjectTracker.new(object_id: object.object_id, to_s: to_s || object.inspect) + @mutex.synchronize do + @retain_hash[tracker.address] = tracker + end + end + + def self.retained_by(tracer: nil, address: nil) + target_address = address || tracer.address + tracer = tracer || @retain_hash[address] + + raise "not a valid address #{target_address}" if target_address.nil? + + retainer_array = [] + Analyzer.new(@heap_file).read do |json_hash| + retainers_from_json_hash(json_hash, target_address: target_address, retainer_array: retainer_array) + end + + retainer_array + end + + class << self + private def retainers_from_json_hash(json_hash, retainer_array:, target_address:) + references = json_hash["references"] + return unless references + + references.each do |address| + next unless address == target_address + + if json_hash["root"] + retainer = RootTracker.new(json_hash) + else + address = json_hash["address"] + representation = self.address_to_object(address)&.inspect || "object not traced".freeze + retainer = ObjectTracker.new(address: address, to_s: representation) + retainer.raw_json_hash = json_hash + end + + retainer_array << retainer + end + end + end + + private + @string_io = StringIO.new + # GIANT BALL OF HACKS || THERE BE DRAGONS + # + # There is so much I don't understand on why I need to do the things + # I'm doing in this method. + # + # Also see `living_dead` https://github.com/schneems/living_dead + def self.gc_start + # During debugging I found calling "puts" made some things + # mysteriously work, I have no idea why. If you remove this line + # then (more) tests fail. Maybe it has something to do with the way + # GC interacts with IO? I seriously have no idea. + # + @string_io.puts "==" + + # Calling flush so we don't create a memory leak. + # Funny enough maybe calling flush without `puts` also works? + # IDK + # + @string_io.flush + + # Calling GC multiple times fixes a different class of things + # Specifically the singleton_class.instance_eval tests. + # It might also be related to calling GC in a block, but changing + # to 1.times brings back failures. + # + # Calling 2 times results in eventual failure https://twitter.com/schneems/status/804369346910896128 + # Calling 5 times results in eventual failure https://twitter.com/schneems/status/804382968307445760 + # Trying 10 times + # + 10.times { GC.start } + end + public + + def self.traced_objects(retained_by: false) + raise "You aren't tracing anything call Heapy::Alive.trace_without_retain first" if @retain_hash.empty? + self.gc_start + + ObjectSpace.dump_all(output: File.open(@heap_file,'w')) + + retainer_address_array_hash = {} + + Analyzer.new(@heap_file).read do |json_hash| + address = json_hash["address"] + tracer = @retain_hash[address] + next unless tracer + tracer.raw_json_hash = json_hash + + if retained_by + retainers_from_json_hash(json_hash, target_address: address, retainer_array: tracer.retained_by) + end + end + @retain_hash.values + end + + class RootTracker + def initialize(json) + @raw_json_hash = json + end + + def references + [] + end + + def id2ref + raise "cannot turn root object into an object" + end + + def root? + true + end + + def address + raise "root does not have an address" + end + + def object_retained? + true + end + + def tracked_to_s + "ROOT" + end + end + + class ObjectTracker + attr_reader :address, :tracked_to_s + + def initialize(object_id: nil, address: nil, to_s: ) + if object_id + @address = "0x#{ (object_id << 1).to_s(16) }" + else + @address = address + end + + raise "must provide address: #{@address.inspect}" if @address.nil? + + @tracked_to_s = to_s.dup + @retained_by = nil + end + + def id2ref + Heapy::Alive.address_to_object(address) + end + + def root? + false + end + + def object_retained? + raw_json_hash && raw_json_hash["address"] + end + + def retainer_array + @retained_by ||= [] + @retained_by + end + + def retained_by + @retained_by || Heapy::Alive.retained_by(tracer: self) + end + + attr_accessor :raw_json_hash + end + end +end \ No newline at end of file diff --git a/heapy/lib/heapy/analyzer.rb b/heapy/lib/heapy/analyzer.rb new file mode 100644 index 0000000000..0fd97867a1 --- /dev/null +++ b/heapy/lib/heapy/analyzer.rb @@ -0,0 +1,151 @@ +module Heapy + class Analyzer + def initialize(filename) + @filename = filename + end + + def read + File.open(@filename) do |f| + f.each_line do |line| + begin + parsed = JSON.parse(line) + yield parsed + rescue JSON::ParserError + puts "Could not parse #{line}" + end + end + end + end + + def drill_down(generation_to_inspect) + puts "" + puts "Analyzing Heap (Generation: #{generation_to_inspect})" + puts "-------------------------------" + puts "" + + generation_to_inspect = Integer(generation_to_inspect) unless generation_to_inspect == "all" + + # + memsize_hash = Hash.new { |h, k| h[k] = 0 } + count_hash = Hash.new { |h, k| h[k] = 0 } + string_count = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = 0 } } + + reference_hash = Hash.new { |h, k| h[k] = 0 } + + read do |parsed| + generation = parsed["generation"] || 0 + if generation_to_inspect == "all".freeze || generation == generation_to_inspect + next unless parsed["file"] + + key = "#{ parsed["file"] }:#{ parsed["line"] }" + memsize_hash[key] += parsed["memsize"] || 0 + count_hash[key] += 1 + + if parsed["type"] == "STRING".freeze + string_count[parsed["value"]][key] += 1 if parsed["value"] + end + + if parsed["references"] + reference_hash[key] += parsed["references"].length + end + end + end + + raise "not a valid Generation: #{generation_to_inspect.inspect}" if memsize_hash.empty? + + total_memsize = memsize_hash.inject(0){|count, (k, v)| count += v} + + # /Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1"=>[{"address"=>"0x7f8a4fbf2328", "type"=>"STRING", "class"=>"0x7f8a4d5dec68", "bytesize"=>223051, "capacity"=>376832, "encoding"=>"UTF-8", "file"=>"/Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim", "line"=>1, "method"=>"new", "generation"=>36, "memsize"=>377065, "flags"=>{"wb_protected"=>true, "old"=>true, "long_lived"=>true, "marked"=>true}}]} + puts "allocated by memory (#{total_memsize}) (in bytes)" + puts "==============================" + memsize_hash = memsize_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50) + longest = memsize_hash.first[1].to_s.length + memsize_hash.each do |file_line, memsize| + puts " #{memsize.to_s.rjust(longest)} #{file_line}" + end + + total_count = count_hash.inject(0){|count, (k, v)| count += v} + + puts "" + puts "object count (#{total_count})" + puts "==============================" + count_hash = count_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50) + longest = count_hash.first[1].to_s.length + count_hash.each do |file_line, memsize| + puts " #{memsize.to_s.rjust(longest)} #{file_line}" + end + + puts "" + puts "High Ref Counts" + puts "==============================" + puts "" + + reference_hash = reference_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50) + longest = count_hash.first[1].to_s.length + + reference_hash.each do |file_line, count| + puts " #{count.to_s.rjust(longest)} #{file_line}" + end + + if !string_count.empty? + puts "" + puts "Duplicate strings" + puts "==============================" + puts "" + + value_count = {} + + string_count.each do |string, location_count_hash| + value_count[string] = location_count_hash.values.inject(&:+) + end + + value_count = value_count.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50) + longest = value_count.first[1].to_s.length + + value_count.each do |string, c1| + + puts " #{c1.to_s.rjust(longest)} #{string.inspect}" + string_count[string].sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.each do |file_line, c2| + puts " #{c2.to_s.rjust(longest)} #{file_line}" + end + puts "" + end + end + + end + + def analyze + puts "" + puts "Analyzing Heap" + puts "==============" + default_key = "nil".freeze + + # generation number is key, value is count + data = Hash.new {|h, k| h[k] = 0 } + mem = Hash.new {|h, k| h[k] = 0 } + total_count = 0 + total_mem = 0 + + read do |parsed| + data[parsed["generation"] || 0] += 1 + mem[parsed["generation"] || 0] += parsed["memsize"] || 0 + end + + data = data.sort {|(k1,v1), (k2,v2)| k1 <=> k2 } + max_length = [data.last[0].to_s.length, default_key.length].max + data.each do |generation, count| + generation = default_key if generation == 0 + total_count += count + total_mem += mem[generation] + puts "Generation: #{ generation.to_s.rjust(max_length) } object count: #{ count }, mem: #{(mem[generation].to_f / 1024).round(1)} kb" + end + + puts "" + puts "Heap total" + puts "==============" + puts "Generations (active): #{data.length}" + puts "Count: #{total_count}" + puts "Memory: #{(total_mem.to_f / 1024).round(1)} kb" + end + end +end diff --git a/heapy/lib/heapy/version.rb b/heapy/lib/heapy/version.rb new file mode 100644 index 0000000000..fd807629c1 --- /dev/null +++ b/heapy/lib/heapy/version.rb @@ -0,0 +1,3 @@ +module Heapy + VERSION = "0.1.4" +end diff --git a/heapy/scratch.rb b/heapy/scratch.rb new file mode 100644 index 0000000000..1fadacf9df --- /dev/null +++ b/heapy/scratch.rb @@ -0,0 +1,64 @@ + + + +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../lib"))) + +load File.expand_path(File.join(__FILE__, "../lib/heapy.rb")) + + +# class Foo +# end + +# class Bar +# end + +# class Baz +# end + +# def run +# foo = Foo.new +# Heapy::Alive.trace_without_retain(foo) +# foo.singleton_class +# foo = nil + +# bar = Bar.new +# Heapy::Alive.trace_without_retain(bar) +# bar.singleton_class +# bar = nil + +# baz = Baz.new +# Heapy::Alive.trace_without_retain(baz) +# baz.singleton_class +# baz = nil +# nil +# end + +# Heapy::Alive.start_object_trace! + +# run + +# objects = Heapy::Alive.traced_objects.each do |obj| +# puts "Address: #{obj.address} #{obj.tracked_to_s}\n #{obj.raw_json_hash || "not found" }" +# end + +Heapy::Alive.start_object_trace! + +def run + foo = "" + Heapy::Alive.trace_without_retain(foo) + b = [] + b << foo + b +end + +c = run + +objects = Heapy::Alive.traced_objects.each do |tracer| + puts "== Address: #{tracer.address} #{tracer.tracked_to_s}\n #{tracer.raw_json_hash || "not found" }" + # tracer.raw_json_hash["references"].each do |address| + # puts Heapy::Alive.address_to_object(address) + # end + Heapy::Alive.retained_by(tracer: tracer).each do |obj| + puts obj.inspect + end +end diff --git a/heapy/trace.rb b/heapy/trace.rb new file mode 100644 index 0000000000..1bc93e54a5 --- /dev/null +++ b/heapy/trace.rb @@ -0,0 +1,3 @@ +require 'objspace' + +ObjectSpace.trace_object_allocations_start diff --git a/heapy/weird_memory/run.rb b/heapy/weird_memory/run.rb new file mode 100644 index 0000000000..521c944845 --- /dev/null +++ b/heapy/weird_memory/run.rb @@ -0,0 +1,31 @@ +arg = ARGV.shift + +@fail_count = 0 + +require 'fileutils' +FileUtils.mkdir_p("tmp") + +def run(file, fail_count: @fail_count) + cmd = "bundle exec ruby #{file}" + puts " $ #{ cmd }" + result = `#{cmd}` + @fail_count += 1 if result.match(/FAIL/) + puts " " + result +end + +if arg.nil? || arg.downcase == "all" + puts "== Running all directories (#{`ruby -v`.strip})" + Dir.glob("weird_memory/**/*.rb").each do |file| + next if file == __FILE__ + run(file) + end +else + puts "== Running examples in `#{arg}` directory (#{`ruby -v`.strip})" + + Dir.glob("weird_memory/#{arg}/**/*.rb").each do |file| + run(file) + end +end + +puts +puts "Total failed: #{@fail_count}" diff --git a/heapy/weird_memory/singleton_class/singleton_class.rb b/heapy/weird_memory/singleton_class/singleton_class.rb new file mode 100644 index 0000000000..6d3cd031fd --- /dev/null +++ b/heapy/weird_memory/singleton_class/singleton_class.rb @@ -0,0 +1,26 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class + string + + return nil +end + +run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/singleton_class/singleton_class_in_class.rb b/heapy/weird_memory/singleton_class/singleton_class_in_class.rb new file mode 100644 index 0000000000..fbe1ac224e --- /dev/null +++ b/heapy/weird_memory/singleton_class/singleton_class_in_class.rb @@ -0,0 +1,29 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +class Runner + + def run + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class + string + + return nil + end +end + +Runner.new.run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/singleton_class/singleton_class_in_proc.rb b/heapy/weird_memory/singleton_class/singleton_class_in_proc.rb new file mode 100644 index 0000000000..8aace6a4d2 --- /dev/null +++ b/heapy/weird_memory/singleton_class/singleton_class_in_proc.rb @@ -0,0 +1,28 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + -> { + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class + string + }.call + + return nil +end + +run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/singleton_class/singleton_class_method_in_proc.rb b/heapy/weird_memory/singleton_class/singleton_class_method_in_proc.rb new file mode 100644 index 0000000000..5c7503af53 --- /dev/null +++ b/heapy/weird_memory/singleton_class/singleton_class_method_in_proc.rb @@ -0,0 +1,26 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class + string + + return nil +end + +-> { run }.call + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval.rb b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval.rb new file mode 100644 index 0000000000..459aab389f --- /dev/null +++ b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval.rb @@ -0,0 +1,27 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class.instance_eval do + end + string + + return nil +end + +run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_class.rb b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_class.rb new file mode 100644 index 0000000000..50064f1389 --- /dev/null +++ b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_class.rb @@ -0,0 +1,29 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +class Runner + def run + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class.instance_eval do + end + string + + return nil + end +end + +Runner.new.run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_proc.rb b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_proc.rb new file mode 100644 index 0000000000..2e98c8738e --- /dev/null +++ b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_proc.rb @@ -0,0 +1,29 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + -> { + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class.instance_eval do + end + string + }.call + + return nil +end + +run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_method_in_proc.rb b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_method_in_proc.rb new file mode 100644 index 0000000000..76e5feada9 --- /dev/null +++ b/heapy/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_method_in_proc.rb @@ -0,0 +1,27 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + string = "" + Heapy::Alive.trace_without_retain(string) + string.singleton_class.instance_eval do + end + string + + return nil +end + +-> { run }.call + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/string/string.rb b/heapy/weird_memory/string/string.rb new file mode 100644 index 0000000000..37d746c6ea --- /dev/null +++ b/heapy/weird_memory/string/string.rb @@ -0,0 +1,25 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + string = "" + Heapy::Alive.trace_without_retain(string) + string + + return nil +end + +run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/string/string_in_class.rb b/heapy/weird_memory/string/string_in_class.rb new file mode 100644 index 0000000000..c23088200a --- /dev/null +++ b/heapy/weird_memory/string/string_in_class.rb @@ -0,0 +1,27 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +class Runner + def run + string = "" + Heapy::Alive.trace_without_retain(string) + string + + return nil + end +end + +Runner.new.run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/string/string_in_proc.rb b/heapy/weird_memory/string/string_in_proc.rb new file mode 100644 index 0000000000..25f5d29c05 --- /dev/null +++ b/heapy/weird_memory/string/string_in_proc.rb @@ -0,0 +1,26 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + -> { + string = "" + Heapy::Alive.trace_without_retain(string) + string + }.call + return nil +end + +run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/string/string_method_in_proc.rb b/heapy/weird_memory/string/string_method_in_proc.rb new file mode 100644 index 0000000000..ae989f02b4 --- /dev/null +++ b/heapy/weird_memory/string/string_method_in_proc.rb @@ -0,0 +1,25 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + string = "" + Heapy::Alive.trace_without_retain(string) + string + + return nil +end + +-> {run}.call + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/times_map/times_map.rb b/heapy/weird_memory/times_map/times_map.rb new file mode 100644 index 0000000000..647aeaac70 --- /dev/null +++ b/heapy/weird_memory/times_map/times_map.rb @@ -0,0 +1,28 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + array = 1.times.map { + string = "" + Heapy::Alive.trace_without_retain(string) + string + } + array = nil + return nil +end + +run + + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/times_map/times_map_in_class.rb b/heapy/weird_memory/times_map/times_map_in_class.rb new file mode 100644 index 0000000000..6539f098f7 --- /dev/null +++ b/heapy/weird_memory/times_map/times_map_in_class.rb @@ -0,0 +1,29 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +class Runner + def run + array = 1.times.map { + string = "" + Heapy::Alive.trace_without_retain(string) + string + } + array = nil + return nil + end +end + +Runner.new.run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/times_map/times_map_in_proc.rb b/heapy/weird_memory/times_map/times_map_in_proc.rb new file mode 100644 index 0000000000..c31e52bd4f --- /dev/null +++ b/heapy/weird_memory/times_map/times_map_in_proc.rb @@ -0,0 +1,30 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + -> { + array = 1.times.map { + string = "" + Heapy::Alive.trace_without_retain(string) + string + } + array = nil + }.call + + return nil +end + +run + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}" diff --git a/heapy/weird_memory/times_map/times_map_method_in_proc.rb b/heapy/weird_memory/times_map/times_map_method_in_proc.rb new file mode 100644 index 0000000000..e34c6845aa --- /dev/null +++ b/heapy/weird_memory/times_map/times_map_method_in_proc.rb @@ -0,0 +1,29 @@ +$LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib"))) + +require 'heapy' + + +Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' }) + +def run + array = 1.times.map { + string = "" + Heapy::Alive.trace_without_retain(string) + string + } + array = nil + return nil +end + +-> { run }.call + +alive_count = Heapy::Alive.traced_objects.select {|tracer| + tracer.object_retained? +}.length +# should return 0, no traced objects are returned + + +expected = 0 +actual = alive_count +result = expected == actual ? "PASS" : "FAIL" +puts "#{result}: expected: #{expected}, actual: #{actual}"