├── .autotest ├── .gitignore ├── CHANGELOG.rdoc ├── Gemfile ├── Manifest.txt ├── README.markdown ├── Rakefile ├── bin └── heap2raster.rb ├── demo.gif ├── ext └── heapfrag │ ├── extconf.rb │ └── heapfrag.c └── test └── test_heapfrag.rb /.autotest: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'autotest/restart' 4 | 5 | Autotest.add_hook :initialize do |at| 6 | at.testlib = 'minitest/autorun' 7 | at.find_directories = ARGV unless ARGV.empty? 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /tmp 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | === 1.0.0 / 2016-03-16 2 | 3 | * 1 major enhancement 4 | 5 | * Birthday! 6 | 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | gem "opengl" 2 | gem "glu" 3 | gem "glut" 4 | 5 | gem "hoe" 6 | gem "rake-compiler" 7 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | .autotest 2 | CHANGELOG.rdoc 3 | Manifest.txt 4 | README.rdoc 5 | Rakefile 6 | bin/heap2raster.rb 7 | ext/heapfrag/extconf.rb 8 | ext/heapfrag/heapfrag.c 9 | lib/heapfrag.bundle 10 | test/test_heapfrag.rb 11 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Heapfrag 2 | 3 | * https://github.com/tenderlove/heapfrag 4 | 5 | ## DESCRIPTION: 6 | 7 | This is a library for dumping and visualizing your heap in MRI. This code is 8 | not for the feint of heart. I don't know what versions of Ruby this will work 9 | on, and I make no guarantees about which versions I support or anything. If 10 | it doesn't work for you, please send a PR! 11 | 12 | This. Is. A. Huge. Hack. 13 | 14 | ## FEATURES/PROBLEMS: 15 | 16 | * All of them 17 | 18 | One problem is that the heap dumped as JSON is really big. I'm having 19 | problems dumping it fast enough. You can adjust the timer interval when you 20 | call `Heapfrag.start`, so if you have a large heap, then a slower interval 21 | might be better. 22 | 23 | ## SYNOPSIS: 24 | 25 | In one terminal: 26 | 27 | ``` 28 | $ ruby bin/heap2raster.rb /tmp/sock 29 | ``` 30 | 31 | In a different terminal: 32 | 33 | ``` 34 | $ ruby --disable-gem -Ilib -rsocket -rheapfrag -S irb 35 | irb(main):001:0> Heapfrag.start(500_000, UNIXSocket.open("/tmp/sock")) 36 | => true 37 | irb(main):002:0> GC.start 38 | => nil 39 | irb(main):003:0> x = 5000.times.map { Object.new }; nil 40 | => nil 41 | ``` 42 | 43 | Hopefully this gif will help: 44 | 45 | ![demo](demo.gif) 46 | 47 | ## REQUIREMENTS: 48 | 49 | * opengl 50 | * glu 51 | * glut 52 | 53 | ## INSTALL: 54 | 55 | You should use this from Git. 56 | 57 | ``` 58 | bundle install 59 | rake compile:heapfrag 60 | ``` 61 | 62 | ## LICENSE: 63 | 64 | (The MIT License) 65 | 66 | Copyright (c) 2016 Aaron Patterson 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining 69 | a copy of this software and associated documentation files (the 70 | 'Software'), to deal in the Software without restriction, including 71 | without limitation the rights to use, copy, modify, merge, publish, 72 | distribute, sublicense, and/or sell copies of the Software, and to 73 | permit persons to whom the Software is furnished to do so, subject to 74 | the following conditions: 75 | 76 | The above copyright notice and this permission notice shall be 77 | included in all copies or substantial portions of the Software. 78 | 79 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 80 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 82 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 83 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 84 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 85 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'rubygems' 4 | require 'hoe' 5 | 6 | Hoe.plugins.delete :rubyforge 7 | Hoe.plugin :minitest 8 | Hoe.plugin :gemspec # `gem install hoe-gemspec` 9 | Hoe.plugin :git # `gem install hoe-git` 10 | 11 | gem 'rake-compiler', '>= 0.4.1' 12 | require "rake/extensiontask" 13 | 14 | Hoe.spec 'heapfrag' do 15 | developer('Aaron Patterson', 'tenderlove@ruby-lang.org') 16 | self.readme_file = 'README.markdown' 17 | self.history_file = 'CHANGELOG.rdoc' 18 | self.extra_rdoc_files = FileList['*.rdoc'] 19 | 20 | self.spec_extras[:extensions] = ["ext/heapfrag/extconf.rb"] 21 | Rake::ExtensionTask.new "heapfrag", spec do |ext| 22 | ext.lib_dir = File.join(*['lib', ENV['FAT_DIR']].compact) 23 | end 24 | end 25 | 26 | # vim: syntax=ruby 27 | -------------------------------------------------------------------------------- /bin/heap2raster.rb: -------------------------------------------------------------------------------- 1 | require 'opengl' 2 | require 'glu' 3 | require 'glut' 4 | require 'json' 5 | require 'socket' 6 | 7 | include Gl,Glu,Glut 8 | 9 | HEIGHT = 200 10 | SQ_PX = 2 11 | 12 | BitMap = Struct.new :width, :height, :young, :oldies 13 | 14 | def make_bitmap heap 15 | heap = heap.find_all { |p| p.key? 'page' }.map do |page| 16 | page['heap'] 17 | end 18 | 19 | max_page_size = heap.map(&:length).max 20 | heap.each { |page| (max_page_size - page.length).times { page << 0 } } 21 | 22 | columns = heap 23 | rows = columns.transpose 24 | 25 | youngs = [] 26 | olds = [] 27 | 28 | rows.each do |row| 29 | unless row.length % SQ_PX == 0 30 | row += [0] * (SQ_PX - (row.length % SQ_PX)) 31 | end 32 | 33 | translated_row = row.each_slice(8 / SQ_PX).map { |slice| 34 | slice.inject(0) { |acc,i| 35 | acc <<= 2 36 | if i == 1 37 | acc += 0b11 38 | end 39 | acc 40 | } 41 | } 42 | SQ_PX.times { youngs.concat translated_row } 43 | 44 | translated_row = row.each_slice(8 / SQ_PX).map { |slice| 45 | slice.inject(0) { |acc,i| 46 | acc <<= 2 47 | if i == 2 48 | acc += 0b11 49 | end 50 | acc 51 | } 52 | } 53 | SQ_PX.times { olds.concat translated_row } 54 | end 55 | rasters = youngs.flatten.pack('C*') 56 | oldies = olds.flatten.pack('C*') 57 | BitMap.new(columns.length * SQ_PX, max_page_size * SQ_PX, rasters, oldies) 58 | end 59 | 60 | $bitmap = BitMap.new 0, 0, '', '' 61 | 62 | def init 63 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1) 64 | glClearColor(0.0, 0.0, 0.0, 0.0) 65 | end 66 | 67 | display = Proc.new do 68 | glClear(GL_COLOR_BUFFER_BIT) 69 | glColor3f(0, 1.0, 0) 70 | glRasterPos2i(0, 0) 71 | glBitmap($bitmap.width, $bitmap.height, 0.0, 0.0, 0.0, 0.0, $bitmap.young) 72 | glColor3f(1.0, 0.0, 0) 73 | glRasterPos2i(0, 0) 74 | glBitmap($bitmap.width, $bitmap.height, 0.0, 0.0, 0.0, 0.0, $bitmap.oldies) 75 | glutSwapBuffers() 76 | end 77 | 78 | reshape = Proc.new do |w, h| 79 | glViewport(0, 0, w, h) 80 | glMatrixMode(GL_PROJECTION) 81 | glLoadIdentity() 82 | glOrtho(0, w, 0, h, -1.0, 1.0) 83 | glMatrixMode(GL_MODELVIEW) 84 | end 85 | 86 | queue = Queue.new 87 | 88 | Thread.new do 89 | server = UNIXServer.new ARGV[0] 90 | sock = server.accept 91 | puts "CONNECTED" 92 | while z = sock.readline 93 | heap = JSON.parse z 94 | $bitmap = make_bitmap heap 95 | queue << :updated 96 | end 97 | end 98 | 99 | # Main Loop 100 | # Open window with initial window size, title bar, 101 | # RGBA display mode, and handle input events. 102 | glutInit 103 | glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB) 104 | glutInitWindowSize(200, 820) 105 | glutInitWindowPosition(100, 100) 106 | glutCreateWindow($0) 107 | init() 108 | glutReshapeFunc(reshape) 109 | glutDisplayFunc(display) 110 | glutIdleFunc(lambda { 111 | queue.pop 112 | glutPostRedisplay 113 | }) 114 | glutMainLoop() 115 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tenderlove/heapfrag/f2be8a368bd2f3adcb7edbd884bc6586d7223b2d/demo.gif -------------------------------------------------------------------------------- /ext/heapfrag/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | create_makefile 'heapfrag' 4 | -------------------------------------------------------------------------------- /ext/heapfrag/heapfrag.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct heap_info { 6 | int pages_seen; 7 | int fd; 8 | }; 9 | 10 | int object_itr(void * start, void * finish, size_t step, void * data) 11 | { 12 | VALUE v = (VALUE)start; 13 | struct heap_info *info = (struct heap_info *)data; 14 | size_t n; 15 | ID flags[5]; 16 | 17 | dprintf(info->fd, "{\"page\":%d,\"heap\":[", info->pages_seen); 18 | for(; v != (VALUE)finish; v += step) { 19 | switch (BUILTIN_TYPE(v)) { 20 | default: { 21 | if((n = rb_obj_gc_flags(v, flags, sizeof(flags))) > 0) { 22 | ID ID_old = rb_intern("old"); 23 | int is_old = 0; 24 | for(size_t i = 0; i < n; i++) { 25 | if(ID_old == flags[i]) is_old = 1; 26 | } 27 | if (is_old) { 28 | dprintf(info->fd, "2"); 29 | } else { 30 | dprintf(info->fd, "1"); 31 | } 32 | } else { 33 | dprintf(info->fd, "1"); 34 | } 35 | break; 36 | } 37 | case T_NONE: 38 | dprintf(info->fd, "0"); 39 | break; 40 | } 41 | if ((v + step) != (VALUE)finish) { 42 | dprintf(info->fd, ","); 43 | } 44 | } 45 | dprintf(info->fd, "]},"); 46 | info->pages_seen++; 47 | return 0; 48 | } 49 | 50 | static int write_fd; 51 | 52 | static void realtime_handler(int signum, siginfo_t *info, void *context) 53 | { 54 | static int in_signal = 0; 55 | struct heap_info ruby_info; 56 | 57 | if (in_signal) return; 58 | 59 | in_signal++; 60 | ruby_info.pages_seen = 0; 61 | ruby_info.fd = write_fd; 62 | 63 | dprintf(ruby_info.fd, "["); 64 | rb_objspace_each_objects(object_itr, &ruby_info); 65 | dprintf(ruby_info.fd, "{}]\n"); 66 | in_signal--; 67 | } 68 | 69 | static VALUE heapfrag_start(VALUE mod, VALUE usec, VALUE fd) 70 | { 71 | struct sigaction act; 72 | struct itimerval itv; 73 | 74 | rb_iv_set(mod, "file", fd); /* prevent GC */ 75 | 76 | write_fd = NUM2INT(rb_funcall(fd, rb_intern("to_i"), 0)); 77 | 78 | act.sa_sigaction = realtime_handler; 79 | act.sa_flags = SA_RESTART | SA_SIGINFO; 80 | sigemptyset(&act.sa_mask); 81 | sigaction(SIGALRM, &act, NULL); 82 | 83 | itv.it_interval.tv_sec = 0; 84 | itv.it_interval.tv_usec = NUM2LONG(usec); 85 | itv.it_value.tv_sec = 0; 86 | itv.it_value.tv_usec = NUM2LONG(usec); 87 | setitimer(ITIMER_REAL, &itv, NULL); 88 | 89 | return Qtrue; 90 | } 91 | 92 | static VALUE heapfrag_stop(VALUE mod) 93 | { 94 | return Qfalse; 95 | } 96 | 97 | void Init_heapfrag() 98 | { 99 | VALUE mHeapfrag = rb_define_module("Heapfrag"); 100 | rb_define_singleton_method(mHeapfrag, "start", heapfrag_start, 2); 101 | rb_define_singleton_method(mHeapfrag, "stop", heapfrag_stop, 0); 102 | } 103 | /* vim: set noet sws=4 sw=4: */ 104 | -------------------------------------------------------------------------------- /test/test_heapfrag.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'heapfrag' 3 | 4 | class TestHeapfrag < Minitest::Test 5 | def test_sanity 6 | flunk "omg" 7 | end 8 | end 9 | --------------------------------------------------------------------------------