Skip to content
Snippets Groups Projects
Commit fadaa7fa authored by Yorick Peterse's avatar Yorick Peterse
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Pipeline #
Gemfile.lock
tmp
pkg
*.so
lib/liballocations.*
source 'https://rubygems.org'
gemspec
LICENSE 0 → 100644
Copyright (c) 2015 GitLab B.V.
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.
# README
A Gem for counting the amount of objects that have been allocated but not
released yet. Tracking this makes it easier to see if objects are being leaked
over time and if so what kind of objects. This Gem does _not_ provide any
insight into where objects are being leaked or why.
This Gem can _only_ be used on CRuby, it does not work on Rubinius due to
TracePoint not being supported on Rubinius.
## Why
The usual approach of getting object counts is by using
`ObjectSpace.each_object`. For example:
counts = Hash.new(0)
ObjectSpace.each_object(Object) do |obj|
counts[obj.class] += 1
end
Sadly this approach is rather slow (e.g. for GitLab this would take roughly
800 ms) and (seems to) force a garbage collection run after every call. In other
words, this isn't something you'd want to run in a production environment.
The allocations Gem on the other hand doesn't suffer from this problem as it
counts objects whenever they're allocated and released. This does mean that
allocating objects is slightly slower than usual, but the overhead should be
small enough to use this Gem in a production environment.
Another big difference between ObjectSpace and this Gem is that the former gives
an overview of _all_ currently retained objects whereas the allocations Gem only
tracks objects that have been allocated since it was enabled.
## Usage
Load the Gem:
require 'allocations'
Enable it:
Allocations.start
Getting a snapshot of the current statistics:
Allocations.to_hash
This will return a Hash with the keys set to various classes and the values to
the amount of instances (of each class) that have not yet been released by the
garbage collector.
Disable it again and clear any existing statistics:
Allocations.stop
## Thread Safety
The C extension uses a mutex to ensure the various methods provided by this Gem
can be used in different threads simultaneously. Each call to
`Allocations.to_hash` returns a new Hash containing a copy of the current
statistics (instead of just referring to a single global Hash).
Do note that calling `Allocation.start` and `Allocations.stop` affects _all_
running threads instead of only the current thread.
## License
All source code in this repository is subject to the terms of the MIT license,
unless stated otherwise. A copy of this license can be found the file "LICENSE".
Rakefile 0 → 100644
require 'bundler'
require 'bundler/gem_tasks'
require 'rake/clean'
require 'rake/extensiontask'
if Gem.win_platform?
task :devkit do
begin
require 'devkit'
rescue LoadError
warn 'Failed to load devkit, installation might fail'
end
end
task :compile => [:devkit]
end
GEMSPEC = Gem::Specification.load('allocations.gemspec')
Rake::ExtensionTask.new('liballocations', GEMSPEC)
CLEAN.include('coverage', 'yardoc', 'tmp', 'lib/liballocations.*')
Dir['./task/*.rake'].each do |task|
import(task)
end
task :default => :test
require File.expand_path('../lib/allocations/version', __FILE__)
Gem::Specification.new do |gem|
gem.name = 'allocations'
gem.version = Allocations::VERSION
gem.authors = ['Yorick Peterse']
gem.email = 'yorickpeterse@gmail.com'
gem.summary = 'Tracking of retained objects in CRuby'
gem.homepage = 'https://gitlab.com/gitlab/allocations'
gem.description = gem.summary
gem.license = 'MIT'
gem.files = Dir.glob([
'lib/**/*.rb',
'ext/**/*',
'README.md',
'LICENSE',
'*.gemspec'
]).select { |file| File.file?(file) }
gem.has_rdoc = 'yard'
gem.required_ruby_version = '>= 2.1.0'
gem.add_development_dependency 'rake'
gem.add_development_dependency 'rake-compiler'
gem.add_development_dependency 'benchmark-ips', ['~> 2.0']
gem.add_development_dependency 'rspec', ['~> 3.0']
end
require 'mkmf'
if RbConfig::CONFIG['CC'] =~ /clang|gcc/
$CFLAGS << ' -pedantic'
end
if ENV['DEBUG']
$CFLAGS << ' -O0 -g'
end
create_makefile('liballocations')
#include "liballocations.h"
st_table *object_counts;
VALUE mutex;
VALUE allocation_tracer;
VALUE free_tracer;
ID id_enabled;
/**
* Called whenever a new Ruby object is allocated.
*/
void newobj_callback(VALUE tracepoint, void* data) {
rb_trace_arg_t *trace_arg = rb_tracearg_from_tracepoint(tracepoint);
st_data_t count = 0;
VALUE obj = rb_tracearg_object(trace_arg);
VALUE klass = RBASIC_CLASS(obj);
/* These aren't actually allocated so there's no point in tracking them. */
if ( klass == Qtrue || klass == Qfalse || klass == Qnil ) {
return;
}
rb_mutex_lock(mutex);
st_lookup(object_counts, (st_data_t) klass, &count);
st_insert(object_counts, (st_data_t) klass, count + 1);
rb_mutex_unlock(mutex);
}
/**
* Called whenever a Ruby object is about to be released.
*
* Important: any Ruby allocations in this function will cause CRuby to
* segfault.
*/
void freeobj_callback(VALUE tracepoint, void* data) {
rb_trace_arg_t *trace_arg = rb_tracearg_from_tracepoint(tracepoint);
st_data_t count;
VALUE obj = rb_tracearg_object(trace_arg);
VALUE klass = RBASIC_CLASS(obj);
rb_mutex_lock(mutex);
if ( st_lookup(object_counts, (st_data_t) klass, &count) ) {
if ( count > 0 && (count - 1) > 0) {
st_insert(object_counts, (st_data_t) klass, count - 1);
}
/* Remove the entry if the count is now 0 */
else {
st_delete(object_counts, (st_data_t*) &klass, NULL);
}
}
rb_mutex_unlock(mutex);
}
/**
* Copies every value in an st_table to a given Ruby Hash.
*/
static int each_count(st_data_t key, st_data_t value, st_data_t hash_ptr) {
rb_hash_aset((VALUE) hash_ptr, (VALUE) key, INT2NUM(value));
return ST_CONTINUE;
}
/**
* Returns a Hash containing the current allocation statistics.
*
* The returned Hash contains its own copy of the statistics, any further object
* allocations/frees will not modify said Hash.
*
* call-seq:
* Allocations.to_hash -> Hash
*/
VALUE allocations_to_hash(VALUE self) {
st_table *local_counts;
VALUE hash;
rb_mutex_lock(mutex);
if ( !object_counts ) {
rb_mutex_unlock(mutex);
return rb_hash_new();
}
local_counts = st_copy(object_counts);
rb_mutex_unlock(mutex);
hash = rb_hash_new();
st_foreach(local_counts, each_count, (st_data_t) hash);
st_free_table(local_counts);
return hash;
}
/**
* Starts the counting of object allocations.
*
* call-seq:
* Allocations.start -> nil
*/
VALUE allocations_start(VALUE self) {
rb_mutex_lock(mutex);
if ( rb_ivar_get(self, id_enabled) == Qtrue ) {
rb_mutex_unlock(mutex);
return Qnil;
}
object_counts = st_init_numtable();
rb_ivar_set(self, id_enabled, Qtrue);
rb_mutex_unlock(mutex);
allocation_tracer = rb_tracepoint_new(Qnil, RUBY_INTERNAL_EVENT_NEWOBJ,
newobj_callback, NULL);
free_tracer = rb_tracepoint_new(Qnil, RUBY_INTERNAL_EVENT_FREEOBJ,
freeobj_callback, NULL);
rb_tracepoint_enable(allocation_tracer);
rb_tracepoint_enable(free_tracer);
return Qnil;
}
/**
* Stops the counting of object allocations and clears the current statistics.
*
* call-seq:
* Allocations.stop -> nil
*/
VALUE allocations_stop(VALUE self) {
rb_mutex_lock(mutex);
if ( rb_ivar_get(self, id_enabled) != Qtrue ) {
rb_mutex_unlock(mutex);
return Qnil;
}
if ( allocation_tracer && free_tracer ) {
rb_tracepoint_disable(allocation_tracer);
rb_tracepoint_disable(free_tracer);
}
if ( object_counts ) {
st_free_table(object_counts);
}
object_counts = NULL;
rb_ivar_set(self, id_enabled, Qfalse);
rb_mutex_unlock(mutex);
return Qnil;
}
/**
* Returns true if tracking allocations has been enabled, false otherwise.
*
* call-seq:
* Allocations.enabled? -> true/false
*/
VALUE allocations_enabled_p(VALUE self) {
VALUE enabled = Qfalse;
rb_mutex_lock(mutex);
if ( rb_ivar_get(self, id_enabled) == Qtrue ) {
enabled = Qtrue;
}
rb_mutex_unlock(mutex);
return enabled;
}
void Init_liballocations() {
VALUE mAllocations = rb_define_module_under(rb_cObject, "Allocations");
mutex = rb_mutex_new();
id_enabled = rb_intern("enabled");
rb_define_singleton_method(mAllocations, "to_hash", allocations_to_hash, 0);
rb_define_singleton_method(mAllocations, "start", allocations_start, 0);
rb_define_singleton_method(mAllocations, "stop", allocations_stop, 0);
rb_define_singleton_method(mAllocations, "enabled?", allocations_enabled_p, 0);
rb_define_const(mAllocations, "MUTEX", mutex);
}
#ifndef LIBALLOCATIONS_H
#define LIBALLOCATIONS_H
#include <ruby.h>
#include <ruby/debug.h>
#include <ruby/st.h>
void Init_liballocations();
#endif
require 'liballocations'
require 'allocations/version'
module Allocations
VERSION = '1.0.0'
end
require 'spec_helper'
describe Allocations do
after do
described_class.stop
end
describe '.start' do
it 'starts the counting of allocations' do
described_class.start
expect(described_class).to be_enabled
end
it 'allows consecutive calls' do
described_class.start
described_class.start
expect(described_class).to be_enabled
end
end
describe '.stop' do
it 'stops the counting of allocations' do
described_class.start
described_class.stop
expect(described_class).to_not be_enabled
end
end
describe '.enabled?' do
it 'returns true when counting is enabled' do
described_class.start
expect(described_class.enabled?).to eq(true)
end
it 'returns false when counting is disabled' do
expect(described_class.enabled?).to eq(false)
end
end
describe '.to_hash' do
it 'returns a Hash containing object counts' do
described_class.start
foo = 'foo'
hash = described_class.to_hash
expect(hash).to be_an_instance_of(Hash)
expect(hash[String] >= 1).to eq(true)
end
end
end
require_relative '../lib/allocations'
require 'rspec'
RSpec.configure do |c|
c.color = true
end
desc 'Runs the tests'
task :test => [:compile] do
sh 'rspec spec'
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment