From 85571cb7124a6193e4c6ae57bfed9c62bcb5de0c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 11 Aug 2018 03:53:36 +0000 Subject: tweak hpb stats destructor output Including the last rb_gc_count() call would be useful, and avoid showing giant SIZE_MAX initializers for acc.min fields. --- ext/mwrap/mwrap.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 138384d..b60c21d 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -1351,21 +1351,26 @@ static void dump_hpb(FILE *fp, unsigned flags) if (flags & DUMP_HPB_STATS) { fprintf(fp, "lifespan_max: %zu\n" - "lifespan_min: %zu\n" + "lifespan_min:%s%zu\n" "lifespan_mean: %0.3f\n" "lifespan_stddev: %0.3f\n" "deathspan_max: %zu\n" - "deathspan_min: %zu\n" + "deathspan_min:%s%zu\n" "deathspan_mean: %0.3f\n" - "deathspan_stddev: %0.3f\n", + "deathspan_stddev: %0.3f\n" + "gc_count: %zu\n", hpb_stats.alive.max, + hpb_stats.alive.min == SIZE_MAX ? " -" : " ", hpb_stats.alive.min, hpb_stats.alive.mean, acc_stddev_dbl(&hpb_stats.alive), hpb_stats.reborn.max, + hpb_stats.reborn.min == SIZE_MAX ? " -" : " ", hpb_stats.reborn.min, hpb_stats.reborn.mean, - acc_stddev_dbl(&hpb_stats.reborn)); + acc_stddev_dbl(&hpb_stats.reborn), + /* n.b.: unsafe to call rb_gc_count() in destructor */ + generation); } if (flags & DUMP_HPB_EACH) { struct alloc_hdr *h; -- cgit v1.2.3-24-ge0c7 From a58386266d4effd90efa840fb0b7d6aa5ce71748 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 11 Aug 2018 03:53:39 +0000 Subject: struct acc: use 64-bit counters Since there's no hope of atomicity here anyways, use 64-bit counters. --- ext/mwrap/mwrap.c | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index b60c21d..cbda436 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -218,14 +218,14 @@ static int has_ec_p(void) } struct acc { - size_t nr; - size_t min; - size_t max; + uint64_t nr; + int64_t min; + int64_t max; double m2; double mean; }; -#define ACC_INIT(name) { .nr=0, .min=SIZE_MAX, .max=0, .m2=0, .mean=0 } +#define ACC_INIT(name) { .nr=0, .min=INT64_MAX, .max=-1, .m2=0, .mean=0 } /* for tracking 16K-aligned heap page bodies (protected by GVL) */ struct { @@ -314,29 +314,35 @@ static void acc_add(struct acc *acc, size_t val) { double delta = val - acc->mean; - size_t nr = ++acc->nr; + uint64_t nr = ++acc->nr; - /* 32-bit overflow, ignore accuracy, just don't divide-by-zero */ + /* just don't divide-by-zero if we ever hit this (unlikely :P) */ if (nr) acc->mean += delta / nr; acc->m2 += delta * (val - acc->mean); - if (val < acc->min) - acc->min = val; - if (val > acc->max) - acc->max = val; + if ((int64_t)val < acc->min) + acc->min = (int64_t)val; + if ((int64_t)val > acc->max) + acc->max = (int64_t)val; } +#if SIZEOF_LONG == 8 +# define INT64toNUM(x) LONG2NUM((long)x) +#elif defined(HAVE_LONG_LONG) && SIZEOF_LONG_LONG == 8 +# define INT64toNUM(x) LL2NUM((LONG_LONG)x) +#endif + static VALUE acc_max(const struct acc *acc) { - return acc->max ? SIZET2NUM(acc->max) : DBL2NUM(HUGE_VAL); + return INT64toNUM(acc->max); } static VALUE acc_min(const struct acc *acc) { - return acc->min == SIZE_MAX ? DBL2NUM(HUGE_VAL) : SIZET2NUM(acc->min); + return acc->min == INT64_MAX ? INT2FIX(-1) : INT64toNUM(acc->min); } static VALUE @@ -1350,22 +1356,22 @@ static void dump_hpb(FILE *fp, unsigned flags) { if (flags & DUMP_HPB_STATS) { fprintf(fp, - "lifespan_max: %zu\n" - "lifespan_min:%s%zu\n" + "lifespan_max: %"PRId64"\n" + "lifespan_min:%s%"PRId64"\n" "lifespan_mean: %0.3f\n" "lifespan_stddev: %0.3f\n" - "deathspan_max: %zu\n" - "deathspan_min:%s%zu\n" + "deathspan_max: %"PRId64"\n" + "deathspan_min:%s%"PRId64"\n" "deathspan_mean: %0.3f\n" "deathspan_stddev: %0.3f\n" "gc_count: %zu\n", hpb_stats.alive.max, - hpb_stats.alive.min == SIZE_MAX ? " -" : " ", + hpb_stats.alive.min == INT64_MAX ? " -" : " ", hpb_stats.alive.min, hpb_stats.alive.mean, acc_stddev_dbl(&hpb_stats.alive), hpb_stats.reborn.max, - hpb_stats.reborn.min == SIZE_MAX ? " -" : " ", + hpb_stats.reborn.min == INT64_MAX ? " -" : " ", hpb_stats.reborn.min, hpb_stats.reborn.mean, acc_stddev_dbl(&hpb_stats.reborn), -- cgit v1.2.3-24-ge0c7 From f2155aca16e5786a1d1189d5eb10458111b78014 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 11 Aug 2018 04:04:42 +0000 Subject: doc: 2.1 pre-release updates Mwrap.total_bytes_allocated/total_bytes_freed API needs docs, and we need to clarify heap page stats are not reset. --- ext/mwrap/mwrap.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index cbda436..2ee04a5 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -1193,11 +1193,17 @@ static VALUE mwrap_quiet(VALUE mod) return rb_ensure(rb_yield, SIZET2NUM(cur), reset_locating, 0); } +/* + * total bytes allocated as tracked by mwrap + */ static VALUE total_inc(VALUE mod) { return SIZET2NUM(total_bytes_inc); } +/* + * total bytes freed as tracked by mwrap + */ static VALUE total_dec(VALUE mod) { return SIZET2NUM(total_bytes_dec); @@ -1338,6 +1344,10 @@ void Init_mwrap(void) * from posix_memalign(3) use in mainline Ruby: * * https://sourceware.org/bugzilla/show_bug.cgi?id=14581 + * + * These statistics are never reset by Mwrap.reset or + * any other method. They only make sense in the context + * of an entire program lifetime. */ hpb = rb_define_class_under(mod, "HeapPageBody", rb_cObject); rb_define_singleton_method(hpb, "stat", hpb_stat, -1); -- cgit v1.2.3-24-ge0c7 From 5120b83117179f81c7c73c41c122da0f6f5cbdb7 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 11 Aug 2018 04:09:57 +0000 Subject: mwrap 2.1.0 - heap_page_body struct tracking This release enables tracking of memalign allocations for "struct heap_page_body" in the Ruby GC. This can be useful for tracking deathspans (time between free and re-allocation) of heap page bodies which can cause fragmentation in some malloc implementations, including glibc. The documentation for it is available at: https://80x24.org/mwrap/Mwrap/HeapPageBody.html And a live demo runs at: https://80x24.org/MWRAP/heap_pages This release also includes global counters for Mwrap.total_bytes_allocated and Mwrap.total_bytes_freed 10 changes since v2.0.0 (2018-07-20): add olddoc.yml to generate links in page footers add .olddoc.yml to MANIFEST gemspec: use "git describe" output for prereleases add global counters for total bytes allocated/freed keep stats for memalign-ed heap_page_body in Ruby remove "memalign:" MWRAP option allow dump_heap: mask via MWRAP env tweak hpb stats destructor output struct acc: use 64-bit counters doc: 2.1 pre-release updates --- mwrap.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mwrap.gemspec b/mwrap.gemspec index 2c01a68..877c80d 100644 --- a/mwrap.gemspec +++ b/mwrap.gemspec @@ -12,7 +12,7 @@ desc = `git describe --abbrev=4 HEAD`.strip.tr('-', '.').delete_prefix('v') Gem::Specification.new do |s| s.name = 'mwrap' - s.version = desc.empty? ? '2.0.0' : desc + s.version = desc.empty? ? '2.1.0' : desc s.homepage = 'https://80x24.org/mwrap/' s.authors = ["Ruby hackers"] s.summary = 'LD_PRELOAD malloc wrapper for Ruby' -- cgit v1.2.3-24-ge0c7 From 4a90540056afce4f73da97300e6709993355fe4f Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Tue, 9 Aug 2022 00:19:11 +0000 Subject: memalign: perform rcu_read_unlock on ENOMEM We must not forget to release RCU read locks even if the process will probably die, soon, due to ENOMEM. I noticed this while working on the Perl5/XS port. Link: https://80x24.org/mwrap-perl/20191102020331.28050-4-e@80x24.org/ --- ext/mwrap/mwrap.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 2ee04a5..4575e34 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -632,9 +632,9 @@ internal_memalign(void **pp, size_t alignment, size_t size, uintptr_t caller) p = ptr_align(p, alignment); h = ptr2hdr(p); alloc_insert_rcu(l, h, size, real); - update_stats_rcu_unlock(l); *pp = p; } + update_stats_rcu_unlock(l); } return real ? 0 : ENOMEM; -- cgit v1.2.3-24-ge0c7 From 32c9a817cab2e28534de62ff62c4686b520fb3fe Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 15 Aug 2022 21:22:17 +0000 Subject: workaround breakage from urcu v0.11.4 urcu v0.11.4+ introduced commit 7ca7fe9c03 (Make temporary variable in _rcu_dereference non-const, 2021-07-29) which conflicts with our use of _LGPL_SOURCE. In retrospect, CMM_LOAD_SHARED and CMM_STORE_SHARED seem sufficient for our use of the `totals' cds_lfht pointer since the constructur should always fire before any threads are running. This is fixed in urcu v0.12.4 and v0.13.2 (released 2022-08-18) but I suspect older versions will live on in enterprise/LTS distros for a long while. Link: https://lore.kernel.org/lttng-dev/20220809181927.GA3718@dcvr/ --- ext/mwrap/mwrap.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 4575e34..477b1cb 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -139,8 +139,8 @@ __attribute__((constructor)) static void resolve_malloc(void) _exit(1); } #endif /* !FreeBSD */ - totals = lfht_new(); - if (!totals) + CMM_STORE_SHARED(totals, lfht_new()); + if (!CMM_LOAD_SHARED(totals)) fprintf(stderr, "failed to allocate totals table\n"); err = pthread_atfork(call_rcu_before_fork, @@ -375,7 +375,7 @@ static struct src_loc *totals_add_rcu(struct src_loc *k) struct cds_lfht *t; again: - t = rcu_dereference(totals); + t = CMM_LOAD_SHARED(totals); if (!t) goto out_unlock; cds_lfht_lookup(t, k->hval, loc_eq, k, &iter); cur = cds_lfht_iter_get_node(&iter); @@ -417,7 +417,7 @@ static struct src_loc *update_stats_rcu_lock(size_t size, uintptr_t caller) static const size_t xlen = sizeof(caller); char *dst; - if (caa_unlikely(!totals)) return 0; + if (caa_unlikely(!CMM_LOAD_SHARED(totals))) return 0; if (locating++) goto out; /* do not recurse into another *alloc */ uatomic_add(&total_bytes_inc, size); @@ -808,7 +808,7 @@ static void *dump_to_file(void *x) ++locating; rcu_read_lock(); - t = rcu_dereference(totals); + t = CMM_LOAD_SHARED(totals); if (!t) goto out_unlock; cds_lfht_for_each_entry(t, &iter, l, hnode) { @@ -877,7 +877,7 @@ static void *totals_reset(void *ign) uatomic_set(&total_bytes_dec, 0); rcu_read_lock(); - t = rcu_dereference(totals); + t = CMM_LOAD_SHARED(totals); cds_lfht_for_each_entry(t, &iter, l, hnode) { uatomic_set(&l->total, 0); uatomic_set(&l->allocations, 0); @@ -945,7 +945,7 @@ static VALUE dump_each_rcu(VALUE x) struct cds_lfht_iter iter; struct src_loc *l; - t = rcu_dereference(totals); + t = CMM_LOAD_SHARED(totals); cds_lfht_for_each_entry(t, &iter, l, hnode) { VALUE v[6]; if (l->total <= a->min) continue; @@ -1049,9 +1049,9 @@ static VALUE mwrap_aref(VALUE mod, VALUE loc) if (!k) return val; + t = CMM_LOAD_SHARED(totals); + if (!t) return val; rcu_read_lock(); - t = rcu_dereference(totals); - if (!t) goto out_unlock; cds_lfht_lookup(t, k->hval, loc_eq, k, &iter); cur = cds_lfht_iter_get_node(&iter); @@ -1059,7 +1059,6 @@ static VALUE mwrap_aref(VALUE mod, VALUE loc) l = caa_container_of(cur, struct src_loc, hnode); val = TypedData_Wrap_Struct(cSrcLoc, &src_loc_type, l); } -out_unlock: rcu_read_unlock(); return val; } -- cgit v1.2.3-24-ge0c7 From a90e237d93256ded3088c83520683fc5835da72d Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 20 Aug 2022 21:35:21 +0000 Subject: constify arg for totals_add_rcu It's not modified by that function, so constify it for ease-of-reading and review. --- ext/mwrap/mwrap.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 477b1cb..8592ea7 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -367,7 +367,7 @@ acc_stddev(const struct acc *acc) return DBL2NUM(acc_stddev_dbl(acc)); } -static struct src_loc *totals_add_rcu(struct src_loc *k) +static struct src_loc *totals_add_rcu(const struct src_loc *k) { struct cds_lfht_iter iter; struct cds_lfht_node *cur; -- cgit v1.2.3-24-ge0c7 From 7ec73b9ccdeaed30e813884f7765281be422bc21 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 20 Aug 2022 21:35:22 +0000 Subject: support Ruby 3.0.x HEAP_PAGE_SIZE no longer estimates malloc overhead in Ruby 3.0.x, but we can now rely on the GC::INTERNAL_CONSTANTS hash to access the true value. We also need to account for Ractors in 3.0+, and thus we need to rely on thread-specific `ruby_current_ec' instead of process-wide `ruby_current_execution_context_ptr' (which no longer exists in 3.0+). Finally, the VM seems prone to making some small immortal allocations in a few places we were not expecting in 2.7 and earlier. Account for that and loosen some exact checks to account for it. Tested on Ruby 3.0.3 and 3.0.4 --- ext/mwrap/extconf.rb | 7 +++++++ ext/mwrap/mwrap.c | 25 +++++++++++++++++++++---- test/test_mwrap.rb | 27 ++++++++++++++++----------- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/ext/mwrap/extconf.rb b/ext/mwrap/extconf.rb index e9dbb1e..254a3bb 100644 --- a/ext/mwrap/extconf.rb +++ b/ext/mwrap/extconf.rb @@ -25,4 +25,11 @@ else abort 'missing __builtin_add_overflow' end +begin + if n = GC::INTERNAL_CONSTANTS[:HEAP_PAGE_SIZE] + $defs << "-DHEAP_PAGE_SIZE=#{n}" + end +rescue NameError +end + create_makefile 'mwrap' diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 8592ea7..cd68eed 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -3,7 +3,7 @@ * License: GPL-2.0+ */ #define _LGPL_SOURCE /* allows URCU to inline some stuff */ -#include +#include /* defines HAVE_RUBY_RACTOR_H on 3.0+ */ #include #include #include @@ -22,10 +22,24 @@ #include #include "jhash.h" +#if __STDC_VERSION__ >= 201112 +# define MWRAP_TSD _Thread_local +#elif defined(__GNUC__) +# define MWRAP_TSD __thread +#else +# error _Thread_local nor __thread supported +#endif + static ID id_uminus; const char *rb_source_location_cstr(int *line); /* requires 2.6.0dev */ extern int __attribute__((weak)) ruby_thread_has_gvl_p(void); + +#ifdef HAVE_RUBY_RACTOR_H /* Ruby 3.0+ */ +extern MWRAP_TSD void * __attribute__((weak)) ruby_current_ec; +#else /* Ruby 2.6-2.7 */ extern void * __attribute__((weak)) ruby_current_execution_context_ptr; +# define ruby_current_ec ruby_current_execution_context_ptr +#endif extern void * __attribute__((weak)) ruby_current_vm_ptr; /* for rb_gc_count */ extern size_t __attribute__((weak)) rb_gc_count(void); extern VALUE __attribute__((weak)) rb_cObject; @@ -40,9 +54,12 @@ static size_t total_bytes_inc, total_bytes_dec; /* match values in Ruby gc.c */ #define HEAP_PAGE_ALIGN_LOG 14 enum { - HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG), + HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG) +#ifndef HEAP_PAGE_SIZE /* Ruby 2.6-2.7 only */ + , REQUIRED_SIZE_BY_MALLOC = (sizeof(size_t) * 5), HEAP_PAGE_SIZE = (HEAP_PAGE_ALIGN - REQUIRED_SIZE_BY_MALLOC) +#endif }; #define IS_HEAP_PAGE_BODY ((struct src_loc *)-1) @@ -75,7 +92,7 @@ static int resolving_malloc; } \ } while (0) -static __thread size_t locating; +static MWRAP_TSD size_t locating; static size_t generation; static size_t page_size; static struct cds_lfht *totals; @@ -214,7 +231,7 @@ static char *int2str(int num, char *dst, size_t * size) static int has_ec_p(void) { return (ruby_thread_has_gvl_p() && ruby_current_vm_ptr && - ruby_current_execution_context_ptr); + ruby_current_ec); } struct acc { diff --git a/test/test_mwrap.rb b/test/test_mwrap.rb index 48fba23..fadaef6 100644 --- a/test/test_mwrap.rb +++ b/test/test_mwrap.rb @@ -29,7 +29,8 @@ class TestMwrap < Test::Unit::TestCase tmp.rewind lines = tmp.readlines line_1 = lines.grep(/\s-e:1\b/)[0].strip - assert_equal '10001', line_1.split(/\s+/)[0] + bytes = line_1.split(/\s+/)[0].to_i + assert_operator bytes, :>=, 10001 end end @@ -42,7 +43,7 @@ class TestMwrap < Test::Unit::TestCase res = system(env, *cmd, { 5 => tmp }) assert res, $?.inspect tmp.rewind - assert_match(/\b10001\s+1\s+-e:1$/, tmp.read) + assert_match(/\b1\d{4}\s+[1-9]\d*\s+-e:1$/, tmp.read) env['MWRAP'] = 'dump_fd:1,dump_min:10000' tmp.rewind @@ -50,14 +51,14 @@ class TestMwrap < Test::Unit::TestCase res = system(env, *cmd, { 1 => tmp }) assert res, $?.inspect tmp.rewind - assert_match(/\b10001\s+1\s+-e:1$/, tmp.read) + assert_match(/\b1\d{4}\s+[1-9]\d*\s+-e:1$/, tmp.read) tmp.rewind tmp.truncate(0) env['MWRAP'] = "dump_path:#{tmp.path},dump_min:10000" res = system(env, *cmd) assert res, $?.inspect - assert_match(/\b10001\s+1\s+-e:1$/, tmp.read) + assert_match(/\b1\d{4}\s+[1-9]\d*\s+-e:1$/, tmp.read) tmp.rewind tmp.truncate(0) @@ -98,7 +99,7 @@ class TestMwrap < Test::Unit::TestCase tmp.rewind buf = tmp.read assert_not_match(/\s+-e:1$/, buf) - assert_match(/\b20001\s+1\s+-e:3$/, buf) + assert_match(/\b2\d{4}\s+[0-9]\d*\s+-e:3$/, buf) end end @@ -176,8 +177,8 @@ class TestMwrap < Test::Unit::TestCase -e GC.disable -e keep=("0"*10000) -e loc=Mwrap["-e:3"] - -e loc.each{|size,gen|p([size,gen,count])} - ) + -e + ) + [ 'loc.each{|size,gen|p([size,gen,count]) if size > 10000}' ] buf = IO.popen(@@env, cmd, &:read) assert_predicate $?, :success? assert_match(/\A\[\s*\d+,\s*\d+,\s*\d+\]\s*\z/s, buf) @@ -230,7 +231,8 @@ class TestMwrap < Test::Unit::TestCase loc.name == k or abort 'SourceLocation#name broken' loc.total >= 10000 or abort 'SourceLocation#total broken' loc.frees == 0 or abort 'SourceLocation#frees broken' - loc.allocations == 1 or abort 'SourceLocation#allocations broken' + loc.allocations >= 1 or + abort "SourceLocation#allocations broken: #{loc.allocations}" seen = false loc.each do |*x| seen = x end seen[1] == loc.total or 'SourceLocation#each broken' @@ -240,7 +242,9 @@ class TestMwrap < Test::Unit::TestCase freed = false until freed freed = true - loc.each do freed = false end + loc.each do |size, gen| + freed = false if size >= 10000 + end end loc.frees == 1 or abort 'SourceLocation#frees broken (after free)' Float === loc.mean_lifespan or abort 'mean_lifespan broken' @@ -264,8 +268,9 @@ class TestMwrap < Test::Unit::TestCase assert_separately(+"#{<<~"begin;"}\n#{<<~'end;'}") begin; require 'mwrap' - before = __LINE__ + before = nil res = Mwrap.quiet do |depth| + before = __LINE__ depth == 1 or abort 'depth is not 1' ('a' * 10000).clear Mwrap.quiet { |d| d == 2 or abort 'depth is not 2' } @@ -304,7 +309,7 @@ class TestMwrap < Test::Unit::TestCase gen <= GC.count && gen >= 0 or abort "bad generation: #{gen}" (0 == (addr & 16383)) or abort "addr not aligned: #{'%x' % addr}" end - nr == ap or abort 'HeapPageBody.each missed page' + nr == ap or abort "HeapPageBody.each missed page #{nr} != #{ap}" 10.times { (1..20000).to_a.map(&:to_s) } 3.times { GC.start } Mwrap::HeapPageBody.stat(h) -- cgit v1.2.3-24-ge0c7 From aa82056594757a75aa0f0b629a8b4612f523c5b0 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 20 Aug 2022 21:35:23 +0000 Subject: disable HeapPageBody count test for Ruby 3.1 Ruby 3.1+ uses mmap on platforms relevant to us, so we currently can't account for it with various malloc wrappers. Tested on Ruby 3.1.2, this is the only change necessary to support 3.1.x so far; but the functionality is gone unless we decide to wrap mmap, as well. --- test/test_mwrap.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_mwrap.rb b/test/test_mwrap.rb index fadaef6..c506554 100644 --- a/test/test_mwrap.rb +++ b/test/test_mwrap.rb @@ -309,7 +309,9 @@ class TestMwrap < Test::Unit::TestCase gen <= GC.count && gen >= 0 or abort "bad generation: #{gen}" (0 == (addr & 16383)) or abort "addr not aligned: #{'%x' % addr}" end - nr == ap or abort "HeapPageBody.each missed page #{nr} != #{ap}" + if RUBY_VERSION.to_f < 3.1 # 3.1+ uses mmap on platforms we care about + nr == ap or abort "HeapPageBody.each missed page #{nr} != #{ap}" + end 10.times { (1..20000).to_a.map(&:to_s) } 3.times { GC.start } Mwrap::HeapPageBody.stat(h) -- cgit v1.2.3-24-ge0c7 From 0f1880761db6b1c819887ba9541e78617d61802e Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 20 Aug 2022 22:55:05 +0000 Subject: quiet uninitialized and unused variable warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These warnings are already suppressed in similar ways in the Perl/XS port. The only warning which remains is related to cfree, which I'm leaving alone, for now, due to lack of use/test-cases: ../../../../ext/mwrap/mwrap.c:689:6: warning: ‘cfree’ specifies less restrictive attributes than its target ‘free’: ‘leaf’, ‘nothrow’ [-Wmissing-attributes] 689 | void cfree(void *) __attribute__((alias("free"))); | ^~~~~ /usr/include/stdlib.h:565:13: note: ‘cfree’ target declared here 565 | extern void free (void *__ptr) __THROW; | ^~~~ --- ext/mwrap/mwrap.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index cd68eed..a097b2e 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -169,11 +169,18 @@ __attribute__((constructor)) static void resolve_malloc(void) --locating; } +#ifdef NDEBUG +#define QUIET_CC_WARNING(var) (void)var; +#else +#define QUIET_CC_WARNING(var) +#endif + static void mutex_lock(pthread_mutex_t *m) { int err = pthread_mutex_lock(m); assert(err == 0); + QUIET_CC_WARNING(err) } static void @@ -181,6 +188,7 @@ mutex_unlock(pthread_mutex_t *m) { int err = pthread_mutex_unlock(m); assert(err == 0); + QUIET_CC_WARNING(err) } #ifndef HAVE_MEMPCPY @@ -660,16 +668,14 @@ internal_memalign(void **pp, size_t alignment, size_t size, uintptr_t caller) static void * memalign_result(int err, void *p) { - if (caa_unlikely(err)) { + if (caa_unlikely(err)) errno = err; - return 0; - } return p; } void *memalign(size_t alignment, size_t size) { - void *p; + void *p = NULL; int err = internal_memalign(&p, alignment, size, RETURN_ADDRESS(0)); return memalign_result(err, p); } @@ -684,7 +690,7 @@ void cfree(void *) __attribute__((alias("free"))); void *valloc(size_t size) { - void *p; + void *p = NULL; int err = internal_memalign(&p, page_size, size, RETURN_ADDRESS(0)); return memalign_result(err, p); } @@ -702,7 +708,7 @@ void *valloc(size_t size) void *pvalloc(size_t size) { size_t alignment = page_size; - void *p; + void *p = NULL; int err; if (add_overflow_p(size, alignment)) { -- cgit v1.2.3-24-ge0c7 From 61f9d94e11e046094c607bdf56c9633938e074b2 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 22 Aug 2022 17:17:57 +0000 Subject: various doc updates Copyright years are unnecessary churn and probably aren't necessary: https://www.linuxfoundation.org/blog/copyright-notices-in-open-source-software-projects/ add POP3 and IMAP archive info, and drop the outdated link to the live demo since I no longer have the resources to run it. Finally, add rdoc (olddoc) + rsync support --- .document | 1 + .gitignore | 3 +++ .olddoc.yml | 4 +++- README | 12 +++++++----- Rakefile | 30 +++++++++++++++++++++++++++++- bin/mwrap | 2 +- ext/mwrap/extconf.rb | 2 +- ext/mwrap/mwrap.c | 2 +- lib/mwrap_rack.rb | 8 +++----- test/test_mwrap.rb | 2 +- 10 files changed, 50 insertions(+), 16 deletions(-) diff --git a/.document b/.document index 4ca33e3..4385cfe 100644 --- a/.document +++ b/.document @@ -1,2 +1,3 @@ ext/mwrap/mwrap.c lib/mwrap_rack.rb +README diff --git a/.gitignore b/.gitignore index aa3606c..f670b62 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ /pkg /*.gem /doc +/NEWS +/NEWS.atom.xml +/LATEST diff --git a/.olddoc.yml b/.olddoc.yml index dac0353..bfecbaf 100644 --- a/.olddoc.yml +++ b/.olddoc.yml @@ -5,4 +5,6 @@ rdoc_url: https://80x24.org/mwrap/ ml_url: https://80x24.org/mwrap-public/ public_email: mwrap-public@80x24.org nntp_url: - - nntp://news.public-inbox.org/inbox.comp.lang.ruby.mwrap +- nntps://news.public-inbox.org/inbox.comp.lang.ruby.mwrap +imap_url: +- imaps://;AUTH=ANONYMOUS@80x24.org/inbox.comp.lang.ruby.mwrap.0 diff --git a/README b/README index 3a20258..f387bc4 100644 --- a/README +++ b/README @@ -67,16 +67,18 @@ first two columns to find the hottest malloc locations. mwrap 2.0.0+ also supports a Rack application endpoint, it is documented at: - https://80x24.org/mwrap/MwrapRack.html +https://80x24.org/mwrap/MwrapRack.html == Known problems * 32-bit machines are prone to overflow (WONTFIX) -== Mail archives and list: +== Public mail archives and contact info: - https://80x24.org/mwrap-public/ - nntp://80x24.org/inbox.comp.lang.ruby.mwrap +* https://80x24.org/mwrap-public/ +* nntps://80x24.org/inbox.comp.lang.ruby.mwrap +* imaps://;AUTH=ANONYMOUS@80x24.org/inbox.comp.lang.ruby.mwrap.0 +* https://80x24.org/mwrap-public/_/text/help/#pop3 No subscription will ever be required to post, but HTML mail will be rejected: @@ -88,7 +90,7 @@ will be rejected: git clone https://80x24.org/mwrap.git Send all patches and pull requests (use "git request-pull" to format) to -the mailing list. We do not use centralized or proprietary messaging +mwrap-public@80x24.org. We do not use centralized or proprietary messaging systems. == License diff --git a/Rakefile b/Rakefile index 50bfa89..255d346 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,4 @@ -# Copyright (C) 2018 mwrap hackers +# Copyright (C) mwrap hackers # License: GPL-2.0+ require 'rake/testtask' begin @@ -14,3 +14,31 @@ task :default => :compile c_files = File.readlines('MANIFEST').grep(%r{ext/.*\.[ch]$}).map!(&:chomp!) task 'compile:mwrap' => c_files + +olddoc = ENV['OLDDOC'] || 'olddoc' +rdoc = ENV['RDOC'] || 'rdoc' +task :rsync_docs do + require 'fileutils' + top = %w(README COPYING LATEST NEWS NEWS.atom.xml) + system("git", "set-file-times") + dest = ENV["RSYNC_DEST"] || "80x24.org:/srv/80x24/mwrap/" + FileUtils.rm_rf('doc') + sh "#{olddoc} prepare" + sh "#{rdoc} -f dark216" # dark216 requires olddoc 1.7+ + File.unlink('doc/created.rid') rescue nil + File.unlink('doc/index.html') rescue nil + FileUtils.cp(top, 'doc') + sh "#{olddoc} merge" + + Dir['doc/**/*'].each do |txt| + st = File.stat(txt) + if st.file? + gz = "#{txt}.gz" + tmp = "#{gz}.#$$" + sh("gzip --rsyncable -9 <#{txt} >#{tmp}") + File.utime(st.atime, st.mtime, tmp) # make nginx gzip_static happy + File.rename(tmp, gz) + end + end + sh("rsync --chmod=Fugo=r #{ENV['RSYNC_OPT']} -av doc/ #{dest}/") +end diff --git a/bin/mwrap b/bin/mwrap index 9f67dab..212078c 100755 --- a/bin/mwrap +++ b/bin/mwrap @@ -1,6 +1,6 @@ #!/usr/bin/ruby # frozen_string_literal: true -# Copyright (C) 2018 mwrap hackers +# Copyright (C) mwrap hackers # License: GPL-2.0+ require 'mwrap' mwrap_so = $".grep(%r{/mwrap\.so\z})[0] or abort "mwrap.so not loaded" diff --git a/ext/mwrap/extconf.rb b/ext/mwrap/extconf.rb index 254a3bb..512ab82 100644 --- a/ext/mwrap/extconf.rb +++ b/ext/mwrap/extconf.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# Copyright (C) 2018 mwrap hackers +# Copyright (C) mwrap hackers # License: GPL-2.0+ require 'mkmf' diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index a097b2e..85b847b 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 mwrap hackers + * Copyright (C) mwrap hackers * License: GPL-2.0+ */ #define _LGPL_SOURCE /* allows URCU to inline some stuff */ diff --git a/lib/mwrap_rack.rb b/lib/mwrap_rack.rb index e45b26d..53380b9 100644 --- a/lib/mwrap_rack.rb +++ b/lib/mwrap_rack.rb @@ -1,4 +1,4 @@ -# Copyright (C) 2018 all contributors +# Copyright (C) all contributors # License: GPL-2.0+ # frozen_string_literal: true require 'mwrap' @@ -17,9 +17,6 @@ require 'cgi' # map('/MWRAP') { run(MwrapRack.new) } # map('/') { run(your_normal_app) } # -# A live demo is available at https://80x24.org/MWRAP/ -# (warning the demo machine is 32-bit, so counters will overflow) -# # This module is only available in mwrap 2.0.0+ class MwrapRack module HtmlResponse # :nodoc: @@ -115,10 +112,11 @@ class MwrapRack end GC_STAT_URL = 'https://docs.ruby-lang.org/en/trunk/GC.html#method-c-stat' - GC_STAT_HELP = <<~"" + GC_STAT_HELP = <<~EOM

Non-Infinity lifespans can indicate fragmentation.

See #{GC_STAT_URL} for info on GC.stat values. + EOM def each Mwrap.quiet do diff --git a/test/test_mwrap.rb b/test/test_mwrap.rb index c506554..eaa65cb 100644 --- a/test/test_mwrap.rb +++ b/test/test_mwrap.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# Copyright (C) 2018 mwrap hackers +# Copyright (C) mwrap hackers # License: GPL-2.0+ require 'test/unit' require 'mwrap' -- cgit v1.2.3-24-ge0c7 From e3d506de0aa3e8a574db0724bcc540f4e08f3838 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 22 Aug 2022 18:10:18 +0000 Subject: mwrap 2.2.0 This release adds Ruby 2.7, 3.0, and 3.1 support. This also fixes breakage in urcu v0.11.4, v0.12.3, and v0.13.1. While urcu v0.12.4 and v0.13.2 were released 2022-08-18, old versions will continue to be in distros for a while (and urcu v0.11.x won't be updated). There are also several updates ported from the Perl version. 7 changes since v2.1.0 in 2018: memalign: perform rcu_read_unlock on ENOMEM workaround breakage from urcu v0.11.4 constify arg for totals_add_rcu support Ruby 3.0.x disable HeapPageBody count test for Ruby 3.1 quiet uninitialized and unused variable warnings various doc updates --- mwrap.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mwrap.gemspec b/mwrap.gemspec index 877c80d..48a32b2 100644 --- a/mwrap.gemspec +++ b/mwrap.gemspec @@ -12,9 +12,9 @@ desc = `git describe --abbrev=4 HEAD`.strip.tr('-', '.').delete_prefix('v') Gem::Specification.new do |s| s.name = 'mwrap' - s.version = desc.empty? ? '2.1.0' : desc + s.version = desc.empty? ? '2.2.0' : desc s.homepage = 'https://80x24.org/mwrap/' - s.authors = ["Ruby hackers"] + s.authors = ["mwrap hackers"] s.summary = 'LD_PRELOAD malloc wrapper for Ruby' s.executables = %w(mwrap) s.files = manifest -- cgit v1.2.3-24-ge0c7 From 867b36082111f564f6e55f63cd21fa0c94df2d20 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Tue, 23 Aug 2022 09:00:30 +0000 Subject: support --enabled-shared builds of Ruby Most GNU/Linux distros build Ruby with --enable-shared, so it makes sense to support it properly even if it's not the default favored by ruby-core. __attribute__((weak)) on a local function is not weak enough for the shared library, so we add the extra check for the function's existence to have_ec_p(), instead. Tested with ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux-gnu] on Debian 11 (bullseye) as well as default builds w/o --enable-shared. Note: FreeBSD 12.3 appears broken with mwrap and I no longer have older FreeBSD systems handy. --- ext/mwrap/mwrap.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 85b847b..9d90298 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -32,7 +32,6 @@ static ID id_uminus; const char *rb_source_location_cstr(int *line); /* requires 2.6.0dev */ -extern int __attribute__((weak)) ruby_thread_has_gvl_p(void); #ifdef HAVE_RUBY_RACTOR_H /* Ruby 3.0+ */ extern MWRAP_TSD void * __attribute__((weak)) ruby_current_ec; @@ -45,6 +44,7 @@ extern size_t __attribute__((weak)) rb_gc_count(void); extern VALUE __attribute__((weak)) rb_cObject; extern VALUE __attribute__((weak)) rb_eTypeError; extern VALUE __attribute__((weak)) rb_yield(VALUE); +int __attribute__((weak)) ruby_thread_has_gvl_p(void); static size_t total_bytes_inc, total_bytes_dec; @@ -64,11 +64,6 @@ enum { #define IS_HEAP_PAGE_BODY ((struct src_loc *)-1) -int __attribute__((weak)) ruby_thread_has_gvl_p(void) -{ - return 0; -} - #ifdef __FreeBSD__ void *__malloc(size_t); void __free(void *); @@ -238,8 +233,8 @@ static char *int2str(int num, char *dst, size_t * size) */ static int has_ec_p(void) { - return (ruby_thread_has_gvl_p() && ruby_current_vm_ptr && - ruby_current_ec); + return ruby_thread_has_gvl_p && ruby_thread_has_gvl_p() && + ruby_current_vm_ptr && ruby_current_ec; } struct acc { -- cgit v1.2.3-24-ge0c7 From 7524237b2de98f7e407ea227fb6ca6904153c0a5 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 3 Sep 2022 09:27:39 +0000 Subject: extconf.rb: avoid RDoc errors during gem install RDoc doesn't seem to like <<'' (blank line) as a heredoc terminator. That said, RDoc scanning extconf.rb during "gem install" is unexpected and make no sense to me. Normal `rdoc' invocations seem to ignore extconf.rb. --- ext/mwrap/extconf.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/mwrap/extconf.rb b/ext/mwrap/extconf.rb index 512ab82..1828407 100644 --- a/ext/mwrap/extconf.rb +++ b/ext/mwrap/extconf.rb @@ -11,15 +11,15 @@ have_library 'dl' have_library 'c' have_library 'execinfo' # FreeBSD -if try_link(<<'') +if try_link(< Date: Sat, 3 Sep 2022 09:27:40 +0000 Subject: add --version and --help args These may make things easier for new users, and we'll also with help text if given no args. We'll programmatically generate version based on `git describe', but fallback to a hardcoded version if outside of git. We'll also start appending `-dirty' to the version string to match git.git conventions. --- MANIFEST | 3 +++ VERSION-GEN | 36 ++++++++++++++++++++++++++++++++++++ bin/mwrap | 19 +++++++++++++++++++ lib/mwrap/.gitignore | 1 + mwrap.gemspec | 9 ++++++--- 5 files changed, 65 insertions(+), 3 deletions(-) create mode 100755 VERSION-GEN create mode 100644 lib/mwrap/.gitignore diff --git a/MANIFEST b/MANIFEST index e6d8964..e8ace8b 100644 --- a/MANIFEST +++ b/MANIFEST @@ -5,10 +5,13 @@ COPYING MANIFEST README Rakefile +VERSION-GEN bin/mwrap ext/mwrap/extconf.rb ext/mwrap/jhash.h ext/mwrap/mwrap.c +lib/mwrap/.gitignore lib/mwrap_rack.rb mwrap.gemspec test/test_mwrap.rb +lib/mwrap/version.rb diff --git a/VERSION-GEN b/VERSION-GEN new file mode 100755 index 0000000..161a04f --- /dev/null +++ b/VERSION-GEN @@ -0,0 +1,36 @@ +#!/bin/sh +VF=lib/mwrap/version.rb +DEF_VER=v2.2.0 +VN=$(git describe HEAD 2>/dev/null) +if test $? -eq 0 +then + case "$VN" in + v[0-9]*) + set -e + git update-index -q --refresh + set +e + git diff-index --quiet HEAD -- || VN="$VN-dirty" + set -e + VN=$(echo $VN | tr '-' '.') + ;; + esac +fi +set -e + +case $VN in +'') VN="$DEF_VER" ;; +esac + +VN=$(expr "$VN" : v*'\(.*\)') +VC=unset +if test -r $VF +then + VC="$(cat $VF)" +fi + +new="module Mwrap; VERSION = '$VN'.freeze; end" +if test x"$new" != x"$VC" +then + echo "$new" >$VF +fi +echo $VN diff --git a/bin/mwrap b/bin/mwrap index 212078c..054b3a3 100755 --- a/bin/mwrap +++ b/bin/mwrap @@ -2,6 +2,25 @@ # frozen_string_literal: true # Copyright (C) mwrap hackers # License: GPL-2.0+ +help = </dev/null`.split("\n") +git_ok = $?.success? +git_manifest << 'lib/mwrap/version.rb'.freeze # generated by ./VERSION-GEN manifest = File.exist?('MANIFEST') ? File.readlines('MANIFEST').map!(&:chomp).delete_if(&:empty?) : git_manifest -if git_manifest[0] && manifest != git_manifest +if git_ok && manifest != git_manifest tmp = "MANIFEST.#$$.tmp" File.open(tmp, 'w') { |fp| fp.puts(git_manifest.join("\n")) } File.rename(tmp, 'MANIFEST') system('git add MANIFEST') end -desc = `git describe --abbrev=4 HEAD`.strip.tr('-', '.').delete_prefix('v') +version = `./VERSION-GEN`.chomp +$?.success? or abort './VERSION-GEN failed' Gem::Specification.new do |s| s.name = 'mwrap' - s.version = desc.empty? ? '2.2.0' : desc + s.version = version s.homepage = 'https://80x24.org/mwrap/' s.authors = ["mwrap hackers"] s.summary = 'LD_PRELOAD malloc wrapper for Ruby' -- cgit v1.2.3-24-ge0c7 From d505b403d5d8810bc104425d9296b358f49bddb5 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 3 Sep 2022 09:27:41 +0000 Subject: cleanup some FreeBSD-related workarounds While FreeBSD 12.3 + Ruby 3.0.4p208 remains broken with mwrap (likely due to threading bugs in Ruby) at least get the code more consistently closer to a working state. Thus we'll remember to account for PTHREAD_MUTEX_INITIALIZER requiring malloc for HeapPageBody tracking, as we do with other mutexes. For reference, the Perl5 port of mwrap seems to have no problems on FreeBSD 12.3, so it seems down to misbehavior w.r.t. pthreads usage within Ruby itself. Perl5 on FreeBSD is configured with threads support, but Perl5 doesn't spawn background threads by default like Ruby can. --- README | 2 +- ext/mwrap/mwrap.c | 93 +++++++++++++++++++++++++++++++------------------------ 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/README b/README index f387bc4..539073e 100644 --- a/README +++ b/README @@ -23,7 +23,7 @@ It does not require recompiling or rebuilding Ruby, but only supports Ruby trunk (2.6.0dev+) on a few platforms: * GNU/Linux -* FreeBSD (tested 11.1) +* FreeBSD (tested 11.1 on Ruby 2.6, currently broken with Ruby 3.x) It may work on NetBSD, OpenBSD and DragonFly BSD. diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 9d90298..1d6baec 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -99,9 +99,43 @@ union padded_mutex { /* a round-robin pool of mutexes */ #define MUTEX_NR (1 << 6) #define MUTEX_MASK (MUTEX_NR - 1) +#ifdef __FreeBSD__ +# define STATIC_MTX_INIT_OK (0) +#else /* only tested on Linux + glibc */ +# define STATIC_MTX_INIT_OK (1) +#endif static size_t mutex_i; static union padded_mutex mutexes[MUTEX_NR] = { +#if STATIC_MTX_INIT_OK [0 ... (MUTEX_NR-1)].mtx = PTHREAD_MUTEX_INITIALIZER +#endif +}; + +#define ACC_INIT(name) { .nr=0, .min=INT64_MAX, .max=-1, .m2=0, .mean=0 } +struct acc { + uint64_t nr; + int64_t min; + int64_t max; + double m2; + double mean; +}; + +/* for tracking 16K-aligned heap page bodies (protected by GVL) */ +struct { + pthread_mutex_t lock; + struct cds_list_head bodies; + struct cds_list_head freed; + + struct acc alive; + struct acc reborn; +} hpb_stats = { +#if STATIC_MTX_INIT_OK + .lock = PTHREAD_MUTEX_INITIALIZER, +#endif + .bodies = CDS_LIST_HEAD_INIT(hpb_stats.bodies), + .freed = CDS_LIST_HEAD_INIT(hpb_stats.freed), + .alive = ACC_INIT(hpb_stats.alive), + .reborn = ACC_INIT(hpb_stats.reborn) }; static pthread_mutex_t *mutex_assign(void) @@ -120,12 +154,11 @@ __attribute__((constructor)) static void resolve_malloc(void) int err; ++locating; -#ifdef __FreeBSD__ /* * PTHREAD_MUTEX_INITIALIZER on FreeBSD means lazy initialization, * which happens at pthread_mutex_lock, and that calls calloc */ - { + if (!STATIC_MTX_INIT_OK) { size_t i; for (i = 0; i < MUTEX_NR; i++) { @@ -135,22 +168,28 @@ __attribute__((constructor)) static void resolve_malloc(void) _exit(1); } } + err = pthread_mutex_init(&hpb_stats.lock, 0); + if (err) { + fprintf(stderr, "error: %s\n", strerror(err)); + _exit(1); + } /* initialize mutexes used by urcu-bp */ rcu_read_lock(); rcu_read_unlock(); +#ifndef __FreeBSD__ + } else { + if (!real_malloc) { + resolving_malloc = 1; + real_malloc = dlsym(RTLD_NEXT, "malloc"); + } + real_free = dlsym(RTLD_NEXT, "free"); + if (!real_malloc || !real_free) { + fprintf(stderr, "missing malloc/aligned_alloc/free\n" + "\t%p %p\n", real_malloc, real_free); + _exit(1); + } +#endif /* !__FreeBSD__ */ } -#else /* !FreeBSD (tested on GNU/Linux) */ - if (!real_malloc) { - resolving_malloc = 1; - real_malloc = dlsym(RTLD_NEXT, "malloc"); - } - real_free = dlsym(RTLD_NEXT, "free"); - if (!real_malloc || !real_free) { - fprintf(stderr, "missing malloc/aligned_alloc/free\n" - "\t%p %p\n", real_malloc, real_free); - _exit(1); - } -#endif /* !FreeBSD */ CMM_STORE_SHARED(totals, lfht_new()); if (!CMM_LOAD_SHARED(totals)) fprintf(stderr, "failed to allocate totals table\n"); @@ -237,32 +276,6 @@ static int has_ec_p(void) ruby_current_vm_ptr && ruby_current_ec; } -struct acc { - uint64_t nr; - int64_t min; - int64_t max; - double m2; - double mean; -}; - -#define ACC_INIT(name) { .nr=0, .min=INT64_MAX, .max=-1, .m2=0, .mean=0 } - -/* for tracking 16K-aligned heap page bodies (protected by GVL) */ -struct { - pthread_mutex_t lock; - struct cds_list_head bodies; - struct cds_list_head freed; - - struct acc alive; - struct acc reborn; -} hpb_stats = { - .lock = PTHREAD_MUTEX_INITIALIZER, - .bodies = CDS_LIST_HEAD_INIT(hpb_stats.bodies), - .freed = CDS_LIST_HEAD_INIT(hpb_stats.freed), - .alive = ACC_INIT(hpb_stats.alive), - .reborn = ACC_INIT(hpb_stats.reborn) -}; - /* allocated via real_malloc/real_free */ struct src_loc { pthread_mutex_t *mtx; -- cgit v1.2.3-24-ge0c7 From e6f1dbf1c169b8bd7f2fd3e24d1618eb0ddb2666 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 3 Sep 2022 09:54:40 +0000 Subject: mwrap 2.3.0 This release now supports --enable-shared builds of Ruby which is the common setting for GNU/Linux distros. FreeBSD 12.3 support remains broken, but the code is somewhat revised so it can be more easily fixed and maintained in the future. `mwrap --help' and and `mwrap --version' are now supported, and an RDoc error which only happens during `gem install' is fixed. * support --enabled-shared builds of Ruby * extconf.rb: avoid RDoc errors during gem install * add --version and --help args * cleanup some FreeBSD-related workarounds --- VERSION-GEN | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION-GEN b/VERSION-GEN index 161a04f..ae66e94 100755 --- a/VERSION-GEN +++ b/VERSION-GEN @@ -1,6 +1,6 @@ #!/bin/sh VF=lib/mwrap/version.rb -DEF_VER=v2.2.0 +DEF_VER=v2.3.0 VN=$(git describe HEAD 2>/dev/null) if test $? -eq 0 then -- cgit v1.2.3-24-ge0c7 From fd15a49eb477b345e97ca3bbd6cde135cd0d6583 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 3 Sep 2022 10:50:53 +0000 Subject: Ractor compatibility We can no longer depend on having GVL/GIL when we have a Ruby execution context, so so make `kbuf' thread-specific to avoid data corruption. This was ported from the Perl5 version, since Perl5 has had MVM/Ractor-like abilities with native threads for decades, despite the Perl5 ecosystem largely avoids and discourages threads. --- ext/mwrap/mwrap.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 1d6baec..90f560e 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -308,7 +308,9 @@ struct alloc_hdr { size_t size; }; -static char kbuf[PATH_MAX + INT2STR_MAX + sizeof(struct alloc_hdr) + 2]; +static MWRAP_TSD char kbuf[ + PATH_MAX + INT2STR_MAX + sizeof(struct alloc_hdr) + 2 +]; static struct alloc_hdr *ptr2hdr(void *p) { -- cgit v1.2.3-24-ge0c7 From 13ecb7e5cf4ee769801e53d9df87141c6730e825 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 3 Sep 2022 10:58:20 +0000 Subject: paranoid safety fix to clamp pathnames to PATH_MAX While I doubt Ruby (nor Perl) would store pathnames longer than PATH_MAX by default, it's possible `eval' users to specify whatever path (and line number) they wish to use. Likely was the case with `# line $FILE' directives in Perl5 which prompted this clamping. --- ext/mwrap/mwrap.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 90f560e..08761d6 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -470,6 +470,8 @@ static struct src_loc *update_stats_rcu_lock(size_t size, uintptr_t caller) /* avoid vsnprintf or anything which could call malloc here: */ len = strlen(ptr); + if (len > PATH_MAX) + len = PATH_MAX; k = (void *)kbuf; k->total = size; dst = mempcpy(k->k, ptr, len); -- cgit v1.2.3-24-ge0c7 From a03b72e85011b71e031447b9c8c917e6f2c08c81 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 7 Jan 2023 21:51:14 +0000 Subject: drop heap page support for Ruby <= 3.0 Ruby 3.1 uses mmap, nowadays, and I don't think it's worth the effort to suport it since mmap and munmap don't require the symmetry *memalign + free do. --- ext/mwrap/extconf.rb | 7 -- ext/mwrap/mwrap.c | 327 ++------------------------------------------------- lib/mwrap_rack.rb | 51 -------- test/test_mwrap.rb | 38 ------ 4 files changed, 12 insertions(+), 411 deletions(-) diff --git a/ext/mwrap/extconf.rb b/ext/mwrap/extconf.rb index 1828407..e8d3cc6 100644 --- a/ext/mwrap/extconf.rb +++ b/ext/mwrap/extconf.rb @@ -25,11 +25,4 @@ else abort 'missing __builtin_add_overflow' end -begin - if n = GC::INTERNAL_CONSTANTS[:HEAP_PAGE_SIZE] - $defs << "-DHEAP_PAGE_SIZE=#{n}" - end -rescue NameError -end - create_makefile 'mwrap' diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 08761d6..6875486 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -51,19 +51,6 @@ static size_t total_bytes_inc, total_bytes_dec; /* true for glibc/dlmalloc/ptmalloc, not sure about jemalloc */ #define ASSUMED_MALLOC_ALIGNMENT (sizeof(void *) * 2) -/* match values in Ruby gc.c */ -#define HEAP_PAGE_ALIGN_LOG 14 -enum { - HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG) -#ifndef HEAP_PAGE_SIZE /* Ruby 2.6-2.7 only */ - , - REQUIRED_SIZE_BY_MALLOC = (sizeof(size_t) * 5), - HEAP_PAGE_SIZE = (HEAP_PAGE_ALIGN - REQUIRED_SIZE_BY_MALLOC) -#endif -}; - -#define IS_HEAP_PAGE_BODY ((struct src_loc *)-1) - #ifdef __FreeBSD__ void *__malloc(size_t); void __free(void *); @@ -111,33 +98,6 @@ static union padded_mutex mutexes[MUTEX_NR] = { #endif }; -#define ACC_INIT(name) { .nr=0, .min=INT64_MAX, .max=-1, .m2=0, .mean=0 } -struct acc { - uint64_t nr; - int64_t min; - int64_t max; - double m2; - double mean; -}; - -/* for tracking 16K-aligned heap page bodies (protected by GVL) */ -struct { - pthread_mutex_t lock; - struct cds_list_head bodies; - struct cds_list_head freed; - - struct acc alive; - struct acc reborn; -} hpb_stats = { -#if STATIC_MTX_INIT_OK - .lock = PTHREAD_MUTEX_INITIALIZER, -#endif - .bodies = CDS_LIST_HEAD_INIT(hpb_stats.bodies), - .freed = CDS_LIST_HEAD_INIT(hpb_stats.freed), - .alive = ACC_INIT(hpb_stats.alive), - .reborn = ACC_INIT(hpb_stats.reborn) -}; - static pthread_mutex_t *mutex_assign(void) { return &mutexes[uatomic_add_return(&mutex_i, 1) & MUTEX_MASK].mtx; @@ -168,11 +128,6 @@ __attribute__((constructor)) static void resolve_malloc(void) _exit(1); } } - err = pthread_mutex_init(&hpb_stats.lock, 0); - if (err) { - fprintf(stderr, "error: %s\n", strerror(err)); - _exit(1); - } /* initialize mutexes used by urcu-bp */ rcu_read_lock(); rcu_read_unlock(); @@ -300,9 +255,6 @@ struct alloc_hdr { struct src_loc *loc; } live; struct rcu_head dead; - struct { - size_t at; /* rb_gc_count() */ - } hpb_freed; } as; void *real; /* what to call real_free on */ size_t size; @@ -344,64 +296,6 @@ static int loc_eq(struct cds_lfht_node *node, const void *key) memcmp(k->k, existing->k, loc_size(k)) == 0); } -/* note: not atomic */ -static void -acc_add(struct acc *acc, size_t val) -{ - double delta = val - acc->mean; - uint64_t nr = ++acc->nr; - - /* just don't divide-by-zero if we ever hit this (unlikely :P) */ - if (nr) - acc->mean += delta / nr; - - acc->m2 += delta * (val - acc->mean); - if ((int64_t)val < acc->min) - acc->min = (int64_t)val; - if ((int64_t)val > acc->max) - acc->max = (int64_t)val; -} - -#if SIZEOF_LONG == 8 -# define INT64toNUM(x) LONG2NUM((long)x) -#elif defined(HAVE_LONG_LONG) && SIZEOF_LONG_LONG == 8 -# define INT64toNUM(x) LL2NUM((LONG_LONG)x) -#endif - -static VALUE -acc_max(const struct acc *acc) -{ - return INT64toNUM(acc->max); -} - -static VALUE -acc_min(const struct acc *acc) -{ - return acc->min == INT64_MAX ? INT2FIX(-1) : INT64toNUM(acc->min); -} - -static VALUE -acc_mean(const struct acc *acc) -{ - return DBL2NUM(acc->nr ? acc->mean : HUGE_VAL); -} - -static double -acc_stddev_dbl(const struct acc *acc) -{ - if (acc->nr > 1) { - double variance = acc->m2 / (acc->nr - 1); - return sqrt(variance); - } - return 0.0; -} - -static VALUE -acc_stddev(const struct acc *acc) -{ - return DBL2NUM(acc_stddev_dbl(acc)); -} - static struct src_loc *totals_add_rcu(const struct src_loc *k) { struct cds_lfht_iter iter; @@ -519,7 +413,7 @@ void free(void *p) struct src_loc *l = h->as.live.loc; if (!real_free) return; /* oh well, leak a little */ - if (l && l != IS_HEAP_PAGE_BODY) { + if (l) { size_t age = generation - h->as.live.gen; uatomic_add(&total_bytes_dec, h->size); @@ -534,19 +428,6 @@ void free(void *p) mutex_unlock(l->mtx); call_rcu(&h->as.dead, free_hdr_rcu); - } else if (l == IS_HEAP_PAGE_BODY) { - size_t gen = generation; - size_t age = gen - h->as.live.gen; - - h->as.hpb_freed.at = gen; - - mutex_lock(&hpb_stats.lock); - acc_add(&hpb_stats.alive, age); - - /* hpb_stats.bodies => hpb_stats.freed */ - cds_list_move(&h->anode, &hpb_stats.freed); - - mutex_unlock(&hpb_stats.lock); } else { real_free(h->real); } @@ -614,65 +495,18 @@ internal_memalign(void **pp, size_t alignment, size_t size, uintptr_t caller) return ENOMEM; - if (alignment == HEAP_PAGE_ALIGN && size == HEAP_PAGE_SIZE) { - if (has_ec_p()) generation = rb_gc_count(); - l = IS_HEAP_PAGE_BODY; - } else { - l = update_stats_rcu_lock(size, caller); - } + l = update_stats_rcu_lock(size, caller); - if (l == IS_HEAP_PAGE_BODY) { - void *p; - size_t gen = generation; - - mutex_lock(&hpb_stats.lock); - - /* reuse existing entry */ - if (!cds_list_empty(&hpb_stats.freed)) { - size_t deathspan; - - h = cds_list_first_entry(&hpb_stats.freed, - struct alloc_hdr, anode); - /* hpb_stats.freed => hpb_stats.bodies */ - cds_list_move(&h->anode, &hpb_stats.bodies); - assert(h->size == size); - assert(h->real); - real = h->real; - p = hdr2ptr(h); - assert(ptr_is_aligned(p, alignment)); - - deathspan = gen - h->as.hpb_freed.at; - acc_add(&hpb_stats.reborn, deathspan); - } - else { - real = real_malloc(asize); - if (!real) return ENOMEM; - - p = hdr2ptr(real); - if (!ptr_is_aligned(p, alignment)) - p = ptr_align(p, alignment); - h = ptr2hdr(p); - h->size = size; - h->real = real; - cds_list_add(&h->anode, &hpb_stats.bodies); - } - mutex_unlock(&hpb_stats.lock); - h->as.live.loc = l; - h->as.live.gen = gen; + real = real_malloc(asize); + if (real) { + void *p = hdr2ptr(real); + if (!ptr_is_aligned(p, alignment)) + p = ptr_align(p, alignment); + h = ptr2hdr(p); + alloc_insert_rcu(l, h, size, real); *pp = p; } - else { - real = real_malloc(asize); - if (real) { - void *p = hdr2ptr(real); - if (!ptr_is_aligned(p, alignment)) - p = ptr_align(p, alignment); - h = ptr2hdr(p); - alloc_insert_rcu(l, h, size, real); - *pp = p; - } - update_stats_rcu_unlock(l); - } + update_stats_rcu_unlock(l); return real ? 0 : ENOMEM; } @@ -1243,73 +1077,6 @@ static VALUE total_dec(VALUE mod) return SIZET2NUM(total_bytes_dec); } -static VALUE hpb_each_yield(VALUE ignore) -{ - struct alloc_hdr *h, *next; - - cds_list_for_each_entry_safe(h, next, &hpb_stats.bodies, anode) { - VALUE v[2]; /* [ generation, address ] */ - void *addr = hdr2ptr(h); - assert(ptr_is_aligned(addr, HEAP_PAGE_ALIGN)); - v[0] = LONG2NUM((long)addr); - v[1] = SIZET2NUM(h->as.live.gen); - rb_yield_values2(2, v); - } - return Qnil; -} - -/* - * call-seq: - * - * Mwrap::HeapPageBody.each { |gen, addr| } -> Integer - * - * Yields the generation (GC.count) the heap page body was created - * and address of the heap page body as an Integer. Returns the - * number of allocated pages as an Integer. This return value should - * match the result of GC.stat(:heap_allocated_pages) - */ -static VALUE hpb_each(VALUE mod) -{ - ++locating; - return rb_ensure(hpb_each_yield, Qfalse, reset_locating, 0); -} - -/* - * call-seq: - * - * Mwrap::HeapPageBody.stat -> Hash - * Mwrap::HeapPageBody.stat(hash) -> hash - * - * The maximum lifespan of a heap page body in the Ruby VM. - * This may be Infinity if no heap page bodies were ever freed. - */ -static VALUE hpb_stat(int argc, VALUE *argv, VALUE hpb) -{ - VALUE h; - - rb_scan_args(argc, argv, "01", &h); - if (NIL_P(h)) - h = rb_hash_new(); - else if (!RB_TYPE_P(h, T_HASH)) - rb_raise(rb_eTypeError, "not a hash %+"PRIsVALUE, h); - - ++locating; -#define S(x) ID2SYM(rb_intern(#x)) - rb_hash_aset(h, S(lifespan_max), acc_max(&hpb_stats.alive)); - rb_hash_aset(h, S(lifespan_min), acc_min(&hpb_stats.alive)); - rb_hash_aset(h, S(lifespan_mean), acc_mean(&hpb_stats.alive)); - rb_hash_aset(h, S(lifespan_stddev), acc_stddev(&hpb_stats.alive)); - rb_hash_aset(h, S(deathspan_max), acc_max(&hpb_stats.reborn)); - rb_hash_aset(h, S(deathspan_min), acc_min(&hpb_stats.reborn)); - rb_hash_aset(h, S(deathspan_mean), acc_mean(&hpb_stats.reborn)); - rb_hash_aset(h, S(deathspan_stddev), acc_stddev(&hpb_stats.reborn)); - rb_hash_aset(h, S(resurrects), SIZET2NUM(hpb_stats.reborn.nr)); -#undef S - --locating; - - return h; -} - /* * Document-module: Mwrap * @@ -1328,19 +1095,13 @@ static VALUE hpb_stat(int argc, VALUE *argv, VALUE hpb) * * dump_fd: a writable FD to dump to * * dump_path: a path to dump to, the file is opened in O_APPEND mode * * dump_min: the minimum allocation size (total) to dump - * * dump_heap: mask of heap_page_body statistics to dump * * If both `dump_fd' and `dump_path' are specified, dump_path takes * precedence. - * - * dump_heap bitmask - * * 0x01 - summary stats (same info as HeapPageBody.stat) - * * 0x02 - all live heaps (similar to HeapPageBody.each) - * * 0x04 - skip non-heap_page_body-related output */ void Init_mwrap(void) { - VALUE mod, hpb; + VALUE mod; ++locating; mod = rb_define_module("Mwrap"); @@ -1372,67 +1133,9 @@ void Init_mwrap(void) rb_define_method(cSrcLoc, "max_lifespan", src_loc_max_lifespan, 0); rb_define_method(cSrcLoc, "name", src_loc_name, 0); - /* - * Information about "struct heap_page_body" allocations from - * Ruby gc.c. This can be useful for tracking fragmentation - * from posix_memalign(3) use in mainline Ruby: - * - * https://sourceware.org/bugzilla/show_bug.cgi?id=14581 - * - * These statistics are never reset by Mwrap.reset or - * any other method. They only make sense in the context - * of an entire program lifetime. - */ - hpb = rb_define_class_under(mod, "HeapPageBody", rb_cObject); - rb_define_singleton_method(hpb, "stat", hpb_stat, -1); - rb_define_singleton_method(hpb, "each", hpb_each, 0); - --locating; } -enum { - DUMP_HPB_STATS = 0x1, - DUMP_HPB_EACH = 0x2, - DUMP_HPB_EXCL = 0x4, -}; - -static void dump_hpb(FILE *fp, unsigned flags) -{ - if (flags & DUMP_HPB_STATS) { - fprintf(fp, - "lifespan_max: %"PRId64"\n" - "lifespan_min:%s%"PRId64"\n" - "lifespan_mean: %0.3f\n" - "lifespan_stddev: %0.3f\n" - "deathspan_max: %"PRId64"\n" - "deathspan_min:%s%"PRId64"\n" - "deathspan_mean: %0.3f\n" - "deathspan_stddev: %0.3f\n" - "gc_count: %zu\n", - hpb_stats.alive.max, - hpb_stats.alive.min == INT64_MAX ? " -" : " ", - hpb_stats.alive.min, - hpb_stats.alive.mean, - acc_stddev_dbl(&hpb_stats.alive), - hpb_stats.reborn.max, - hpb_stats.reborn.min == INT64_MAX ? " -" : " ", - hpb_stats.reborn.min, - hpb_stats.reborn.mean, - acc_stddev_dbl(&hpb_stats.reborn), - /* n.b.: unsafe to call rb_gc_count() in destructor */ - generation); - } - if (flags & DUMP_HPB_EACH) { - struct alloc_hdr *h; - - cds_list_for_each_entry(h, &hpb_stats.bodies, anode) { - void *addr = hdr2ptr(h); - - fprintf(fp, "%p\t%zu\n", addr, h->as.live.gen); - } - } -} - /* rb_cloexec_open isn't usable by non-Ruby processes */ #ifndef O_CLOEXEC # define O_CLOEXEC 0 @@ -1446,7 +1149,6 @@ static void mwrap_dump_destructor(void) struct dump_arg a = { .min = 0 }; size_t i; int dump_fd; - unsigned dump_heap = 0; char *dump_path; char *s; @@ -1478,9 +1180,6 @@ static void mwrap_dump_destructor(void) if ((s = strstr(opt, "dump_min:"))) sscanf(s, "dump_min:%zu", &a.min); - if ((s = strstr(opt, "dump_heap:"))) - sscanf(s, "dump_heap:%u", &dump_heap); - switch (dump_fd) { case 0: goto out; case 1: a.fp = stdout; break; @@ -1500,9 +1199,7 @@ static void mwrap_dump_destructor(void) } /* we'll leak some memory here, but this is a destructor */ } - if ((dump_heap & DUMP_HPB_EXCL) == 0) - dump_to_file(&a); - dump_hpb(a.fp, dump_heap); + dump_to_file(&a); out: --locating; } diff --git a/lib/mwrap_rack.rb b/lib/mwrap_rack.rb index 53380b9..c777a78 100644 --- a/lib/mwrap_rack.rb +++ b/lib/mwrap_rack.rb @@ -89,54 +89,6 @@ class MwrapRack end end - class HeapPages # :nodoc: - include HtmlResponse - HEADER = 'addressgeneration' - - def hpb_rows - Mwrap::HeapPageBody.stat(stat = Thread.current[:mwrap_hpb_stat] ||= {}) - %i(lifespan_max lifespan_min lifespan_mean lifespan_stddev - deathspan_max deathspan_min deathspan_mean deathspan_stddev - resurrects - ).map! do |k| - "#{k}#{stat[k]}\n" - end.join - end - - def gc_stat_rows - GC.stat(stat = Thread.current[:mwrap_gc_stat] ||= {}) - %i(count heap_allocated_pages heap_eden_pages heap_tomb_pages - total_allocated_pages total_freed_pages).map do |k| - "GC.stat(:#{k})#{stat[k]}\n" - end.join - end - - GC_STAT_URL = 'https://docs.ruby-lang.org/en/trunk/GC.html#method-c-stat' - GC_STAT_HELP = <<~EOM -

Non-Infinity lifespans can indicate fragmentation. -

See #{GC_STAT_URL} for info on GC.stat values. - EOM - - def each - Mwrap.quiet do - yield("heap pages" \ - "

heap pages

" \ - "\n" \ - "#{hpb_rows}" \ - "#{gc_stat_rows}" \ - "
statvalue
\n" \ - "#{GC_STAT_HELP}" \ - "#{HEADER}") - Mwrap::HeapPageBody.each do |addr, generation| - addr = -sprintf('0x%x', addr) - yield(-"\n") - end - yield "
#{addr}#{generation}
\n" - end - end - end - def r404 # :nodoc: [404,{'Content-Type'=>'text/plain'},["Not found\n"]] end @@ -152,15 +104,12 @@ class MwrapRack loc = -CGI.unescape($1) loc = Mwrap[loc] or return r404 EachAt.new(loc).response - when '/heap_pages' - HeapPages.new.response when '/' n = 2000 u = 'https://80x24.org/mwrap/README.html' b = -('Mwrap demo' \ "

allocations >#{n} bytes" \ "

#{u}" \ - "

heap pages" \ "\n") [ 200, {'Content-Type'=>'text/html','Content-Length'=>-b.size.to_s},[b]] else diff --git a/test/test_mwrap.rb b/test/test_mwrap.rb index eaa65cb..6522167 100644 --- a/test/test_mwrap.rb +++ b/test/test_mwrap.rb @@ -59,13 +59,6 @@ class TestMwrap < Test::Unit::TestCase res = system(env, *cmd) assert res, $?.inspect assert_match(/\b1\d{4}\s+[1-9]\d*\s+-e:1$/, tmp.read) - - tmp.rewind - tmp.truncate(0) - env['MWRAP'] = "dump_path:#{tmp.path},dump_heap:5" - res = system(env, *cmd) - assert res, $?.inspect - assert_match %r{lifespan_stddev}, tmp.read end end @@ -295,35 +288,4 @@ class TestMwrap < Test::Unit::TestCase abort 'freed more than allocated' end; end - - def test_heap_page_body - assert_separately(+"#{<<~"begin;"}\n#{<<~'end;'}") - begin; - require 'mwrap' - require 'rubygems' # use up some memory - ap = GC.stat(:heap_allocated_pages) - h = {} - nr = 0 - Mwrap::HeapPageBody.each do |addr, gen| - nr += 1 - gen <= GC.count && gen >= 0 or abort "bad generation: #{gen}" - (0 == (addr & 16383)) or abort "addr not aligned: #{'%x' % addr}" - end - if RUBY_VERSION.to_f < 3.1 # 3.1+ uses mmap on platforms we care about - nr == ap or abort "HeapPageBody.each missed page #{nr} != #{ap}" - end - 10.times { (1..20000).to_a.map(&:to_s) } - 3.times { GC.start } - Mwrap::HeapPageBody.stat(h) - Integer === h[:lifespan_max] or abort 'lifespan_max not recorded' - Integer === h[:lifespan_min] or abort 'lifespan_min not recorded' - Float === h[:lifespan_mean] or abort 'lifespan_mean not recorded' - 3.times { GC.start } - 10.times { (1..20000).to_a.map(&:to_s) } - Mwrap::HeapPageBody.stat(h) - h[:deathspan_min] <= h[:deathspan_max] or - abort 'wrong min/max deathtime' - Float === h[:deathspan_mean] or abort 'deathspan_mean not recorded' - end; - end end -- cgit v1.2.3-24-ge0c7 From 40893ed4ddc5d92cce36ee951e9c7ae67b96f41a Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 7 Jan 2023 21:47:12 +0000 Subject: mwrap_rack: lowercase response headers for Rack 3 Rack 3 requires lowercase headers, and they work with any Rack <=2.x version. --- lib/mwrap_rack.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/mwrap_rack.rb b/lib/mwrap_rack.rb index c777a78..1bd00ac 100644 --- a/lib/mwrap_rack.rb +++ b/lib/mwrap_rack.rb @@ -22,10 +22,10 @@ class MwrapRack module HtmlResponse # :nodoc: def response [ 200, { - 'Expires' => 'Fri, 01 Jan 1980 00:00:00 GMT', - 'Pragma' => 'no-cache', - 'Cache-Control' => 'no-cache, max-age=0, must-revalidate', - 'Content-Type' => 'text/html; charset=UTF-8', + 'expires' => 'Fri, 01 Jan 1980 00:00:00 GMT', + 'pragma' => 'no-cache', + 'cache-control' => 'no-cache, max-age=0, must-revalidate', + 'content-type' => 'text/html; charset=UTF-8', }, self ] end end @@ -90,7 +90,7 @@ class MwrapRack end def r404 # :nodoc: - [404,{'Content-Type'=>'text/plain'},["Not found\n"]] + [404,{'content-type'=>'text/plain'},["Not found\n"]] end # The standard Rack application endpoint for MwrapRack @@ -111,7 +111,7 @@ class MwrapRack "

allocations >#{n} bytes" \ "

#{u}" \ "\n") - [ 200, {'Content-Type'=>'text/html','Content-Length'=>-b.size.to_s},[b]] + [ 200, {'content-type'=>'text/html','content-length'=>-b.size.to_s},[b]] else r404 end -- cgit v1.2.3-24-ge0c7 From 2c25edb01139365f4754985c1e3494765dd1e5a7 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 7 Jan 2023 20:13:13 +0000 Subject: undefine Mwrap::SourceLocation.allocate This quiets `undefining the allocator of T_DATA class Mwrap::SourceLocation' warnings. --- ext/mwrap/mwrap.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index 6875486..160007f 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -1115,6 +1115,7 @@ void Init_mwrap(void) * This class is only available since mwrap 2.0.0+. */ cSrcLoc = rb_define_class_under(mod, "SourceLocation", rb_cObject); + rb_undef_alloc_func(cSrcLoc); rb_define_singleton_method(mod, "dump", mwrap_dump, -1); rb_define_singleton_method(mod, "reset", mwrap_reset, 0); rb_define_singleton_method(mod, "clear", mwrap_clear, 0); -- cgit v1.2.3-24-ge0c7 From 22c89c3a4ef6be25758e5d02488ba2d0d186e04f Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 18:24:28 +0000 Subject: update manifest --- MANIFEST | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MANIFEST b/MANIFEST index e8ace8b..4c5be8b 100644 --- a/MANIFEST +++ b/MANIFEST @@ -7,11 +7,21 @@ README Rakefile VERSION-GEN bin/mwrap +ext/mwrap/check.h +ext/mwrap/dlmalloc_c.h ext/mwrap/extconf.rb +ext/mwrap/gcc.h +ext/mwrap/httpd.h ext/mwrap/jhash.h ext/mwrap/mwrap.c +ext/mwrap/mwrap_core.h +ext/mwrap/mymalloc.h +ext/mwrap/picohttpparser.h +ext/mwrap/picohttpparser_c.h lib/mwrap/.gitignore lib/mwrap_rack.rb mwrap.gemspec +t/httpd.t +t/test_common.perl test/test_mwrap.rb lib/mwrap/version.rb -- cgit v1.2.3-24-ge0c7 From 649a0d3e3578cb385e1a20579c16aca306650b30 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 18:36:56 +0000 Subject: httpd: undefine ruby_snprintf alias for non-Ruby processes ruby/subst.h (included by ruby.h) replaces `snprintf' with `ruby_snprintf'. This only works in processes linked to Ruby, but won't work in subprocesses spawned by Ruby. --- ext/mwrap/httpd.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index 03aef9f..da7ff6d 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -43,6 +43,14 @@ #define TYPE_CSV "text/csv" #define TYPE_PLAIN "text/plain" +/* + * C ruby defines snprintf to ruby_snprintf, we can't have that in + * non-ruby processes spawned by C ruby + */ +#if MWRAP_RUBY && defined(snprintf) +# undef snprintf +#endif + enum mw_qev { MW_QEV_IGNORE = 0, MW_QEV_RD = POLLIN, -- cgit v1.2.3-24-ge0c7 From 8ae75563b2ef53be721338faada7b29717b56541 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 21:40:52 +0000 Subject: test_mwrap: fix test reliability Not sure what drugs I was on when I wrote this :x --- test/test_mwrap.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_mwrap.rb b/test/test_mwrap.rb index 29bbdd2..176dca6 100644 --- a/test/test_mwrap.rb +++ b/test/test_mwrap.rb @@ -5,6 +5,7 @@ require 'test/unit' require 'mwrap' require 'rbconfig' require 'tempfile' +require 'io/wait' class TestMwrap < Test::Unit::TestCase RB = "#{RbConfig::CONFIG['bindir']}/#{RbConfig::CONFIG['RUBY_INSTALL_NAME']}" @@ -128,10 +129,12 @@ class TestMwrap < Test::Unit::TestCase assert_equal "\n", r.gets buf = +'' 10.times { Process.kill(:USR1, pid) } - while IO.select([r], nil, nil, 0.1) + while r.wait_readable(0.1) case tmp = r.read_nonblock(1000, exception: false) when String buf << tmp + when nil + break end end w2.close -- cgit v1.2.3-24-ge0c7 From babee7d4dfbf0771621f51dc1e438fad019efe1e Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 05:20:44 +0000 Subject: drop dlsym use for rb_stderr A weak symbol works fine, here. --- ext/mwrap/mwrap.c | 3 ++- ext/mwrap/mwrap_core.h | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/mwrap/mwrap.c b/ext/mwrap/mwrap.c index d88fee6..a45bb38 100644 --- a/ext/mwrap/mwrap.c +++ b/ext/mwrap/mwrap.c @@ -6,6 +6,7 @@ #include "mwrap_core.h" static ID id_uminus; +extern VALUE __attribute__((weak)) rb_stderr; extern VALUE __attribute__((weak)) rb_cObject; extern VALUE __attribute__((weak)) rb_eTypeError; extern VALUE __attribute__((weak)) rb_yield(VALUE); @@ -33,7 +34,7 @@ static VALUE mwrap_dump(int argc, VALUE *argv, VALUE mod) if (NIL_P(io)) /* library may be linked w/o Ruby */ - io = *((VALUE *)dlsym(RTLD_DEFAULT, "rb_stderr")); + io = rb_stderr; a.min = NIL_P(min) ? 0 : NUM2SIZET(min); io = rb_io_get_io(io); diff --git a/ext/mwrap/mwrap_core.h b/ext/mwrap/mwrap_core.h index c0eea2f..721e5d3 100644 --- a/ext/mwrap/mwrap_core.h +++ b/ext/mwrap/mwrap_core.h @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include -- cgit v1.2.3-24-ge0c7 From afa6a2ea602fb26a16c6f0b9408339086f2790bd Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 21:44:44 +0000 Subject: Rakefile: fix Ruby-only test case name --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index cf4311e..efb62b1 100644 --- a/Rakefile +++ b/Rakefile @@ -8,7 +8,7 @@ rescue LoadError warn 'rake-compiler not available, cross compiling disabled' end -Rake::TestTask.new(:test) +Rake::TestTask.new('test-ruby') task 'test-ruby' => :compile task :default => :compile -- cgit v1.2.3-24-ge0c7 From c5b0ca668258f0e386bcb0b12cf3628899d6bc12 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 23:43:21 +0000 Subject: make Ruby headers less intrusive By including them at the bottom. This will be done for Perl headers in the future, too, since they break assert(). --- ext/mwrap/httpd.h | 8 ---- ext/mwrap/mwrap_core.h | 104 +++++++++++++++++++++++++------------------------ 2 files changed, 53 insertions(+), 59 deletions(-) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index da7ff6d..03aef9f 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -43,14 +43,6 @@ #define TYPE_CSV "text/csv" #define TYPE_PLAIN "text/plain" -/* - * C ruby defines snprintf to ruby_snprintf, we can't have that in - * non-ruby processes spawned by C ruby - */ -#if MWRAP_RUBY && defined(snprintf) -# undef snprintf -#endif - enum mw_qev { MW_QEV_IGNORE = 0, MW_QEV_RD = POLLIN, diff --git a/ext/mwrap/mwrap_core.h b/ext/mwrap/mwrap_core.h index 721e5d3..48669d5 100644 --- a/ext/mwrap/mwrap_core.h +++ b/ext/mwrap/mwrap_core.h @@ -46,13 +46,6 @@ # include "ppport.h" #endif -#if MWRAP_RUBY -# undef _GNU_SOURCE /* ruby.h redefines it */ -# include /* defines HAVE_RUBY_RACTOR_H on 3.0+ */ -# include -# include -#endif - /* * XXH3 (truncated to 32-bits) seems to provide a ~2% speedup. * XXH32 doesn't show improvements over jhash despite rculfhash @@ -90,44 +83,11 @@ static size_t *root_locating; /* determines if PL_curcop is our thread */ #endif /* MWRAP_PERL */ #if MWRAP_RUBY -const char *rb_source_location_cstr(int *line); /* requires 2.6.0dev or later */ - -# ifdef HAVE_RUBY_RACTOR_H /* Ruby 3.0+ */ -extern MWRAP_TSD void * __attribute__((weak)) ruby_current_ec; -# else /* Ruby 2.6-2.7 */ -extern void * __attribute__((weak)) ruby_current_execution_context_ptr; -# define ruby_current_ec ruby_current_execution_context_ptr -# endif /* HAVE_RUBY_RACTOR_H */ - -extern void * __attribute__((weak)) ruby_current_vm_ptr; /* for rb_gc_count */ -extern size_t __attribute__((weak)) rb_gc_count(void); -int __attribute__((weak)) ruby_thread_has_gvl_p(void); - -/* - * rb_source_location_cstr relies on GET_EC(), and it's possible - * to have a native thread but no EC during the early and late - * (teardown) phases of the Ruby process - */ -static int has_ec_p(void) -{ - return ruby_thread_has_gvl_p && ruby_thread_has_gvl_p() && - ruby_current_vm_ptr && ruby_current_ec; -} - -static void set_generation(size_t *gen, size_t size) -{ - if (rb_gc_count) { - uatomic_add_return(&total_bytes_inc, size); - if (has_ec_p()) - *gen = rb_gc_count(); - } else { - *gen = uatomic_add_return(&total_bytes_inc, size); - } -} -# define SET_GENERATION(gen, size) set_generation(gen, size) +static void mw_ruby_set_generation(size_t *, size_t); +# define SET_GENERATION(gen, size) mw_ruby_set_generation(gen, size) #endif /* MWRAP_RUBY */ -#ifndef SET_GENERATION +#ifndef SET_GENERATION /* C-only builds w/o Perl|Ruby */ # define SET_GENERATION(gen, size) \ *gen = uatomic_add_return(&total_bytes_inc, size) #endif /* !SET_GENERATION */ @@ -438,14 +398,7 @@ static const char *mw_perl_src_file_cstr(unsigned *lineno) #endif /* MWRAP_PERL */ #if MWRAP_RUBY -static const char *mw_ruby_src_file_cstr(unsigned *lineno) -{ - if (!has_ec_p()) return NULL; - int line; - const char *fn = rb_source_location_cstr(&line); - *lineno = line < 0 ? UINT_MAX : (unsigned)line; - return fn; -} +static const char *mw_ruby_src_file_cstr(unsigned *lineno); # define SRC_FILE_CSTR(lineno) mw_ruby_src_file_cstr(lineno) #endif /* MWRAP_RUBY */ @@ -1088,3 +1041,52 @@ __attribute__((constructor)) static void mwrap_ctor(void) } --locating; } + +#if MWRAP_RUBY +# undef _GNU_SOURCE /* ruby.h redefines it */ +# include /* defines HAVE_RUBY_RACTOR_H on 3.0+ */ +# include +# include +# ifdef HAVE_RUBY_RACTOR_H /* Ruby 3.0+ */ +extern MWRAP_TSD void * __attribute__((weak)) ruby_current_ec; +# else /* Ruby 2.6-2.7 */ +extern void * __attribute__((weak)) ruby_current_execution_context_ptr; +# define ruby_current_ec ruby_current_execution_context_ptr +# endif /* HAVE_RUBY_RACTOR_H */ + +extern void * __attribute__((weak)) ruby_current_vm_ptr; /* for rb_gc_count */ +extern size_t __attribute__((weak)) rb_gc_count(void); +int __attribute__((weak)) ruby_thread_has_gvl_p(void); + +const char *rb_source_location_cstr(int *line); /* requires 2.6.0dev or later */ +/* + * rb_source_location_cstr relies on GET_EC(), and it's possible + * to have a native thread but no EC during the early and late + * (teardown) phases of the Ruby process + */ +static int has_ec_p(void) +{ + return ruby_thread_has_gvl_p && ruby_thread_has_gvl_p() && + ruby_current_vm_ptr && ruby_current_ec; +} + +static void mw_ruby_set_generation(size_t *gen, size_t size) +{ + if (rb_gc_count) { + uatomic_add_return(&total_bytes_inc, size); + if (has_ec_p()) + *gen = rb_gc_count(); + } else { + *gen = uatomic_add_return(&total_bytes_inc, size); + } +} + +static const char *mw_ruby_src_file_cstr(unsigned *lineno) +{ + if (!has_ec_p()) return NULL; + int line; + const char *fn = rb_source_location_cstr(&line); + *lineno = line < 0 ? UINT_MAX : (unsigned)line; + return fn; +} +#endif /* !MWRAP_RUBY */ -- cgit v1.2.3-24-ge0c7 From b344bcc056e4c0dde8385bd7b4c60e20e04d9263 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 21:55:08 +0000 Subject: test spawning non-Ruby process with socket_dir This tests commit 649a0d3e3578 (httpd: undefine ruby_snprintf alias for non-Ruby processes, 2023-01-08) --- test/test_mwrap.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test_mwrap.rb b/test/test_mwrap.rb index 176dca6..e04fab5 100644 --- a/test/test_mwrap.rb +++ b/test/test_mwrap.rb @@ -6,6 +6,7 @@ require 'mwrap' require 'rbconfig' require 'tempfile' require 'io/wait' +require 'tmpdir' class TestMwrap < Test::Unit::TestCase RB = "#{RbConfig::CONFIG['bindir']}/#{RbConfig::CONFIG['RUBY_INSTALL_NAME']}" @@ -79,6 +80,15 @@ class TestMwrap < Test::Unit::TestCase assert_match(/\b0x[a-f0-9]+\b/s, dump, 'dump output has addresses') end + def test_spawn_non_ruby + Dir.mktmpdir do |dir| + sockdir = "#{dir}/sockdir" + env = @@env.merge('MWRAP' => "socket_dir:#{sockdir}") + out = IO.popen(env, %w(ls -alR), { chdir: dir }, &:read) + assert_match(/\b\d+\.sock\b/, out) + end + end + def test_clear cmd = @@cmd + %w( -e ("0"*10000).clear -- cgit v1.2.3-24-ge0c7 From 9039f3518498a6f08f63d59342c4c6aa6c1696d6 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 23:43:24 +0000 Subject: httpd: show GC count We can't call rb_gc_count() safely outside of Ruby threads (especially during startup/teardown), but we can share it's last-known value safely. --- ext/mwrap/httpd.h | 3 +++ ext/mwrap/mwrap_core.h | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index 03aef9f..0ef6cd9 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -548,6 +548,9 @@ static void show_stats(FILE *fp) "/ files: %zu / locations: %zu", inc , inc - dec, uatomic_read(&nr_file), uatomic_read(&nr_src_loc)); +#if MWRAP_RUBY + fprintf(fp, " / GC: %zu", uatomic_read(&last_gc_count)); +#endif } /* /$PID/at/$LOCATION endpoint */ diff --git a/ext/mwrap/mwrap_core.h b/ext/mwrap/mwrap_core.h index 48669d5..827ee7b 100644 --- a/ext/mwrap/mwrap_core.h +++ b/ext/mwrap/mwrap_core.h @@ -85,6 +85,7 @@ static size_t *root_locating; /* determines if PL_curcop is our thread */ #if MWRAP_RUBY static void mw_ruby_set_generation(size_t *, size_t); # define SET_GENERATION(gen, size) mw_ruby_set_generation(gen, size) +static size_t last_gc_count; /* for httpd which runs in a non-GVL thread */ #endif /* MWRAP_RUBY */ #ifndef SET_GENERATION /* C-only builds w/o Perl|Ruby */ @@ -1074,8 +1075,10 @@ static void mw_ruby_set_generation(size_t *gen, size_t size) { if (rb_gc_count) { uatomic_add_return(&total_bytes_inc, size); - if (has_ec_p()) + if (has_ec_p()) { *gen = rb_gc_count(); + uatomic_set(&last_gc_count, *gen); + } } else { *gen = uatomic_add_return(&total_bytes_inc, size); } -- cgit v1.2.3-24-ge0c7 From 043831f7eba50524416b97a25314a52f2506fb77 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 8 Jan 2023 23:43:26 +0000 Subject: httpd: different URLs for Ruby vs Perl versions They're different projects, still, I guess... --- ext/mwrap/httpd.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index 0ef6cd9..cea79f7 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -38,7 +38,11 @@ #include "picohttpparser_c.h" #include #include -#define URL "https://80x24.org/mwrap-perl.git/about" +#if MWRAP_PERL +# define URL "https://80x24.org/mwrap-perl.git/" +#else +# define URL "https://80x24.org/mwrap.git/" +#endif #define TYPE_HTML "text/html; charset=UTF-8" #define TYPE_CSV "text/csv" #define TYPE_PLAIN "text/plain" -- cgit v1.2.3-24-ge0c7 From ee995d418cc8cfcb148159d84c4f1d6f40108b6d Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Jan 2023 05:52:27 +0000 Subject: httpd: s/X-Mwrap-BT-Depth/X-Mwrap-BT/ This is more consistent with the `MWRAP=bt:' use, since adding `-Depth' seems unnecessary and makes curl commands too long. --- ext/mwrap/httpd.h | 2 +- t/httpd.t | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index cea79f7..5c3b83f 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -899,7 +899,7 @@ static enum mw_qev h1_parse_harder(struct mw_h1 *h1, struct mw_h1req *h1r, * request bodies, so let pico handle parameters in * HTTP request headers, instead. */ - if (NAME_EQ(hdr, "X-Mwrap-BT-Depth")) { + if (NAME_EQ(hdr, "X-Mwrap-BT")) { errno = 0; depth = strtol(hdr->value, &end, 10); if (errno || !valid_end(end)) diff --git a/t/httpd.t b/t/httpd.t index 9a0fae6..76fe7d1 100644 --- a/t/httpd.t +++ b/t/httpd.t @@ -174,12 +174,12 @@ SKIP: { $rc = system(@curl, qw(-d x=y), "http://0/$pid/reset"); is($rc, 0, 'curl /reset'); - $rc = system(@curl, qw(-HX-Mwrap-BT-Depth:10 -XPOST), + $rc = system(@curl, qw(-HX-Mwrap-BT:10 -XPOST), "http://0/$pid/ctl"); - is($rc, 0, 'curl /ctl (X-Mwrap-BT-Depth)'); + is($rc, 0, 'curl /ctl (X-Mwrap-BT)'); like(slurp($cout), qr/\bMWRAP=bt:10\b/, 'changed bt depth'); - $rc = system(@curl, qw(-HX-Mwrap-BT-Depth:10 -d blah http://0/ctl)); + $rc = system(@curl, qw(-HX-Mwrap-BT:10 -d blah http://0/ctl)); is($rc >> 8, 22, '404 w/o PID prefix'); }; -- cgit v1.2.3-24-ge0c7 From 4106dfd813ce42678e0395eda42e36149d1ac851 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Jan 2023 05:52:38 +0000 Subject: various doc updates, add mwrap(1) manpage FreeBSD 12.x doesn't seem to work with the `pkg install'-ed ruby --- Documentation/.gitignore | 2 + Documentation/GNUmakefile | 63 ++++++++++++++++++++++++ Documentation/mwrap.pod | 123 ++++++++++++++++++++++++++++++++++++++++++++++ MANIFEST | 4 +- README | 38 ++++++++------ mwrap.gemspec | 11 ++++- 6 files changed, 223 insertions(+), 18 deletions(-) create mode 100644 Documentation/.gitignore create mode 100644 Documentation/GNUmakefile create mode 100644 Documentation/mwrap.pod diff --git a/Documentation/.gitignore b/Documentation/.gitignore new file mode 100644 index 0000000..1b0e502 --- /dev/null +++ b/Documentation/.gitignore @@ -0,0 +1,2 @@ +mwrap.txt +mwrap.1 diff --git a/Documentation/GNUmakefile b/Documentation/GNUmakefile new file mode 100644 index 0000000..14480da --- /dev/null +++ b/Documentation/GNUmakefile @@ -0,0 +1,63 @@ +# Copyright (C) all contributors +# License: GPL-3.0+ +all:: + +INSTALL = install +POD2MAN = pod2man +VERSION := $(shell cd .. && ./VERSION-GEN) +release := mwrap $(VERSION) +POD2MAN_OPTS = -v -r '$(release)' --stderr -d 1993-10-02 -c 'mwrap user manual' +pod2man = $(POD2MAN) $(POD2MAN_OPTS) +POD2TEXT = pod2text +POD2TEXT_OPTS = --stderr +pod2text = $(POD2TEXT) $(POD2TEXT_OPTS) + +m1 = mwrap +m5 = +m7 = + +man1 := $(addsuffix .1, $(m1)) +man5 := $(addsuffix .5, $(m5)) +man7 := $(addsuffix .7, $(m7)) + +all:: man + +man: $(man1) $(man5) $(man7) + +prefix ?= $(HOME) +mandir ?= $(prefix)/share/man +man1dir = $(mandir)/man1 +man5dir = $(mandir)/man5 +man7dir = $(mandir)/man7 + +gem-man: man + $(INSTALL) -d -m 755 ../man + test -z "$(man1)" || $(INSTALL) -m 644 $(man1) ../man + test -z "$(man5)" || $(INSTALL) -m 644 $(man5) ../man + test -z "$(man7)" || $(INSTALL) -m 644 $(man7) ../man + +install-man: man + $(INSTALL) -d -m 755 $(DESTDIR)$(man1dir) + $(INSTALL) -d -m 755 $(DESTDIR)$(man5dir) + test -z "$(man7)" || $(INSTALL) -d -m 755 $(DESTDIR)$(man7dir) + $(INSTALL) -m 644 $(man1) $(DESTDIR)$(man1dir) + $(INSTALL) -m 644 $(man5) $(DESTDIR)$(man5dir) + test -z "$(man7)" || $(INSTALL) -m 644 $(man7) $(DESTDIR)$(man7dir) + +%.1 %.5 %.7 : %.pod + $(pod2man) -s $(subst .,,$(suffix $@)) $< $@+ && mv $@+ $@ + +mantxt = $(addsuffix .txt, $(m1) $(m5) $(m7)) + +txt :: $(mantxt) + +all :: txt + +%.txt : %.pod + $(pod2text) $< $@+ + touch -r $< $@+ + mv $@+ $@ + +clean:: + $(RM) $(man1) $(man5) $(man7) + $(RM) $(addsuffix .txt.gz, $(m1) $(m5) $(m7)) diff --git a/Documentation/mwrap.pod b/Documentation/mwrap.pod new file mode 100644 index 0000000..6832430 --- /dev/null +++ b/Documentation/mwrap.pod @@ -0,0 +1,123 @@ +=head1 NAME + +mwrap - run any command under mwrap + +=head1 SYNOPSIS + + # to trace a long-running program and access it via $DIR/$PID.sock: + MWRAP=socket_dir:$DIR mwrap COMMAND + + # to trace a short-lived command and dump its output to a log: + MWRAP=dump_path:$FILENAME mwrap COMMAND + +=head1 DESCRIPTION + +mwrap is a command-line to automatically add mwrap.so as an +LD_PRELOAD for any command. It will resolve malloc-family calls +to a Ruby file and line number, and it can also provide a backtrace +of native (C/C++) functions for non-Ruby programs. + +=head1 ENVIRONMENT + +C is the only environment variable read. It contains multiple +options delimited by C<,> with names and values delimited by C<:> + +=over 4 + +=item socket_dir:$DIR + +This launches an embedded HTTP server in each process and binds it +to C<$DIR/$PID.sock>. C +or L (from L) may +be used to access various endpoints in the HTTP server. + +=item bt:$DEPTH + +The backtrace depth for L in addition to the Perl +file and line number where C<$DEPTH> is a non-negative number. + +The maximum allowed value is 32, though values of 5 or less are +typically useful. Increasing this to even 2 or 3 can significantly +increase the amount of memory mwrap (and liburcu) itself uses. + +This is only useful in conjunction with C + +This may be changed via POST request (see below). + +Default: 0 + +=item dump_path:$FILENAME + +Dumps the output at exit to a given filename: + + total_bytes call_count location + +In the future, dumping to a self-describing CSV will be supported. + +=item dump_fd:$DESCRIPTOR + +As with dump_path, but dumps the output to a given file descriptor. + +=back + +=head1 HTTP POST API + +In addition to the various GET endpoints linked via C, +there are some POST endpoints which are typically accessed via +C + +=over 4 + +=item POST http://0/$PID/reset + +C + +Reset all internal counters. This is not done atomically and does +not release any memory. + +=item POST http://0/$PID/trim + +C + +Runs L with a 0 pad value to release unused memory +back to the kernel. In our malloc implementation, this is done +lazily to avoid contention and does not happen unless sleeping threads. + +=item POST http://0/$PID/ctl + +Set various internal knobs. Currently, C is the +only knob supported: + +C + +Using the C header allows changing the aforementioned +C value to a specified depth level. As with C, only make small +adjustments as the memory cost can increase exponentially with each step. + +It is typically a good idea to reset (C) after changing +the depth on a running process. + +Headers other than C may be accepted in the future to +tweak other settings. + +=back + +=head1 CONTACT + +Feedback welcome via plain-text mail to L + +Mail archives are hosted at L + +=head1 COPYRIGHT + +Copyright all contributors L + +License: GPL-3.0+ L + +Source code is at L + +=head1 SEE ALSO + +L, L, L + +=cut diff --git a/MANIFEST b/MANIFEST index 4c5be8b..8d4cdd6 100644 --- a/MANIFEST +++ b/MANIFEST @@ -2,6 +2,9 @@ .gitignore .olddoc.yml COPYING +Documentation/.gitignore +Documentation/GNUmakefile +Documentation/mwrap.pod MANIFEST README Rakefile @@ -24,4 +27,3 @@ mwrap.gemspec t/httpd.t t/test_common.perl test/test_mwrap.rb -lib/mwrap/version.rb diff --git a/README b/README index 382c5a0..761f87e 100644 --- a/README +++ b/README @@ -8,8 +8,8 @@ mwrap wraps all malloc-family calls to trace the Ruby source location of such calls and bytes allocated at each callsite. As of mwrap 2.0.0, it can also function as a leak detector and show live allocations at every call site. Depending on -your application and workload, the overhead is roughly a 50% -increase memory and runtime. +your application and workload, the overhead is roughly a 50-100% +increase memory and runtime with default settings. It works best for allocations under GVL, but tries to track numeric caller addresses for allocations made without GVL so you @@ -22,15 +22,13 @@ Userspace RCU project: https://liburcu.org/ It does not require recompiling or rebuilding Ruby, but only supports Ruby 2.7.0 or later on a few platforms: -* GNU/Linux -* FreeBSD +* GNU/Linux (only tested --without-jemalloc, mwrap 3.x provides its own) -It may work on NetBSD, OpenBSD and DragonFly BSD. +It may work on FreeBSD, NetBSD, OpenBSD and DragonFly BSD if given +appropriate build options. == Install - # FreeBSD: pkg install liburcu - # Debian-based systems: apt-get liburcu-dev # Install mwrap via RubyGems.org @@ -64,24 +62,33 @@ or an address retrieved by backtrace_symbols(3). It is recommended to use the sort(1) command on either of the first two columns to find the hottest malloc locations. -mwrap 2.0.0+ also supports a Rack application endpoint, +mwrap 3.0.0+ also supports an embedded HTTP server it is documented at: -https://80x24.org/mwrap/MwrapRack.html +https://80x24.org/mwrap.git/tree/Documentation/mwrap.pod == Known problems * 32-bit machines are prone to overflow (WONTFIX) -== Public mail archives and contact info: +* signalfd(2)-reliant code will need latest URCU with commit + ea3a28a3f71dd02f (Disable signals in URCU background threads, 2022-09-23) + This doesn't affect C Ruby itself, and signalfd(2) use is rare + 3rd-party processes. + +* Ruby source files over 16.7 million lines long are not supported :P + +== Public mail archives (HTTP, Atom feeds, IMAP mailbox, NNTP group, POP3): * https://80x24.org/mwrap-public/ * nntps://80x24.org/inbox.comp.lang.ruby.mwrap * imaps://;AUTH=ANONYMOUS@80x24.org/inbox.comp.lang.ruby.mwrap.0 * https://80x24.org/mwrap-public/_/text/help/#pop3 -No subscription will ever be required to post, but HTML mail -will be rejected: +No subscription nor real identities will ever be required to obtain support, +but HTML mail is rejected. Memory usage reductions start with you; +only send plain-text mail to us and do not top-post. HTML mail and +top-posting costs everybody memory and bandwidth. mwrap-public@80x24.org @@ -89,9 +96,10 @@ will be rejected: git clone https://80x24.org/mwrap.git -Send all patches and pull requests (use "git request-pull" to format) to -mwrap-public@80x24.org. We do not use centralized or proprietary messaging -systems. +Send all patches ("git format-patch" + "git send-email") and +pull requests (use "git request-pull" to format) via email +to mwrap-perl@80x24.org. We do not and will not use +proprietary messaging systems. == License diff --git a/mwrap.gemspec b/mwrap.gemspec index dc99924..b6f9e71 100644 --- a/mwrap.gemspec +++ b/mwrap.gemspec @@ -1,6 +1,5 @@ git_manifest = `git ls-files 2>/dev/null`.split("\n") git_ok = $?.success? -git_manifest << 'lib/mwrap/version.rb'.freeze # generated by ./VERSION-GEN manifest = File.exist?('MANIFEST') ? File.readlines('MANIFEST').map!(&:chomp).delete_if(&:empty?) : git_manifest if git_ok && manifest != git_manifest @@ -12,11 +11,19 @@ end version = `./VERSION-GEN`.chomp $?.success? or abort './VERSION-GEN failed' +manifest << 'lib/mwrap/version.rb'.freeze + +if system(*%w(make -C Documentation man)) || + system(*%w(gmake -C Documentation man)) + manifest.concat(%w(Documentation/mwrap.1)) +else + warn 'failed to build man-page(s), proceeding without them' +end Gem::Specification.new do |s| s.name = 'mwrap' s.version = version - s.homepage = 'https://80x24.org/mwrap/' + s.homepage = 'https://80x24.org/mwrap.git/' s.authors = ["mwrap hackers"] s.summary = 'LD_PRELOAD malloc wrapper for Ruby' s.executables = %w(mwrap) -- cgit v1.2.3-24-ge0c7 From f617762da0be5b489d81b5176b2b06a0b164e3c8 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Jan 2023 05:58:22 +0000 Subject: httpd: do not sort CSV output by default CSV output is intended to be loaded by something else (e.g. SQLite, spreadsheet program, etc), so sorting it is likely a waste of time. --- ext/mwrap/httpd.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index 5c3b83f..17fb187 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -609,8 +609,13 @@ static enum mw_qev each_gt(struct mw_h1 *h1, struct mw_h1req *h1r, unsigned long min, bool csv) { static const char default_sort[] = "bytes"; - const char *sort = default_sort; - size_t sort_len = sizeof(default_sort) - 1; + const char *sort; + size_t sort_len = 0; + + if (!csv) { + sort = default_sort; + sort_len = sizeof(default_sort) - 1; + } if (h1r->qstr && h1r->qlen > 5 && !memcmp(h1r->qstr, "sort=", 5)) { sort = h1r->qstr + 5; -- cgit v1.2.3-24-ge0c7 From 316732f4b0f853394085ee2355b3b9b19f04f68f Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Jan 2023 06:21:35 +0000 Subject: httpd: add notes about arenas and object heaps We don't users being confused if an innocuous-looking line of code allocates unnexpectedly large values. --- ext/mwrap/httpd.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index 17fb187..fe4fe2f 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -600,7 +600,12 @@ static enum mw_qev each_at(struct mw_h1 *h1, struct mw_h1req *h1r) size, h->as.live.gen, h->real); } rcu_read_unlock(); - FPUTS("", fp); + FPUTS("

\nNotes:\n"
+"* 16344-byte (64-bit) or 16344-byte (32-bit) allocations in\n"
+"  Ruby <= 3.0 aligned to 0x4000 are likely for object heap slots.\n"
+"* 4080-byte allocations in Perl 5 are likely for arenas\n"
+"  (set via the PERL_ARENA_SIZE compile-time macro)"
+"
", fp); return h1_200(h1, &html, TYPE_HTML); } -- cgit v1.2.3-24-ge0c7 From e8eb1187f958179482ebc0efd2d51719f44cc8a0 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Jan 2023 06:21:36 +0000 Subject: httpd: add CSV download link to /each/$MIN HTML This makes the .CSV download discoverable so I don't have to document it in the manpage \o/ --- ext/mwrap/httpd.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/mwrap/httpd.h b/ext/mwrap/httpd.h index fe4fe2f..ef4d83c 100644 --- a/ext/mwrap/httpd.h +++ b/ext/mwrap/httpd.h @@ -654,7 +654,8 @@ static enum mw_qev each_gt(struct mw_h1 *h1, struct mw_h1req *h1r, fprintf(fp, "mwrap each >%lu" "

mwrap each >%lu " "(change `%lu' in URL to adjust filtering) - " - "MWRAP=bt:%u", min, min, min, depth); + "MWRAP=bt:%u .csv", + min, min, min, depth, min); show_stats(fp); /* need borders to distinguish multi-level traces */ if (depth) -- cgit v1.2.3-24-ge0c7 From 0de44f7bc1e6cc873ac24c57c4ce62c0759194c5 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Jan 2023 07:01:42 +0000 Subject: fix test_sigusr1_works `p' will deadlock even with `STDOUT.sync=true', apparently :< --- test/test_mwrap.rb | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/test/test_mwrap.rb b/test/test_mwrap.rb index e04fab5..58c9743 100644 --- a/test/test_mwrap.rb +++ b/test/test_mwrap.rb @@ -126,33 +126,41 @@ class TestMwrap < Test::Unit::TestCase # some URCU flavors use USR1, ensure the one we choose does not def test_sigusr1_works + err = Tempfile.new('dump') cmd = @@cmd + %w( -e STDOUT.sync=true - -e trap(:USR1){p("HELLO_WORLD")} + -e trap(:USR1){STDOUT.syswrite("HELLO_WORLD\n")} -e END{Mwrap.dump} - -e puts -e STDIN.read) + -e puts("HI") + -e STDIN.read) IO.pipe do |r, w| IO.pipe do |r2, w2| - pid = spawn(@@env, *cmd, in: r2, out: w, err: '/dev/null') + pid = spawn(@@env, *cmd, in: r2, out: w, err: err) r2.close w.close - assert_equal "\n", r.gets + assert_equal "HI\n", r.gets, '#puts HI fired' buf = +'' 10.times { Process.kill(:USR1, pid) } - while r.wait_readable(0.1) + Thread.pass # sched_yield + while r.wait_readable(0.5) case tmp = r.read_nonblock(1000, exception: false) - when String - buf << tmp - when nil - break + when String; buf << tmp; break + when nil; break + else + warn "Unexpected read_nonblock result: #{tmp.inspect}" end end - w2.close - Process.wait(pid) - assert_predicate $?, :success?, $?.inspect - assert_equal(["\"HELLO_WORLD\"\n"], buf.split(/^/).uniq) + w2.close # break from STDERR.read + _, st = Process.wait2(pid) + warn "# buf=#{buf.inspect}" if $DEBUG + assert_predicate(st, :success?, + "#{st.inspect} is success buf=#{buf.inspect} "\ + "err=#{err.rewind;err.read.inspect}") + assert_equal(["HELLO_WORLD\n"], buf.split(/^/).uniq) end end + ensure + err.close! if err end def test_reset -- cgit v1.2.3-24-ge0c7 From 458c801f687fc102984fba591112b492e89c067c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Jan 2023 07:03:58 +0000 Subject: 3.0.0-pre1 --- VERSION-GEN | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION-GEN b/VERSION-GEN index ae66e94..35dab9d 100755 --- a/VERSION-GEN +++ b/VERSION-GEN @@ -1,6 +1,6 @@ #!/bin/sh VF=lib/mwrap/version.rb -DEF_VER=v2.3.0 +DEF_VER=v3.0.0-pre1 VN=$(git describe HEAD 2>/dev/null) if test $? -eq 0 then -- cgit v1.2.3-24-ge0c7