@@ -259,17 +259,17 @@ VALUE secondary_map_mutex = Qnil;
259259
260260// Lambda that will GC entries from the secondary map that are no longer present
261261// in the primary map.
262- VALUE gc_secondary_map = Qnil ;
262+ VALUE gc_secondary_map_lambda = Qnil ;
263263ID length ;
264264
265265extern VALUE weak_obj_cache ;
266266
267267static void SecondaryMap_Init () {
268268 rb_gc_register_address (& secondary_map );
269- rb_gc_register_address (& gc_secondary_map );
269+ rb_gc_register_address (& gc_secondary_map_lambda );
270270 rb_gc_register_address (& secondary_map_mutex );
271271 secondary_map = rb_hash_new ();
272- gc_secondary_map = rb_eval_string (
272+ gc_secondary_map_lambda = rb_eval_string (
273273 "->(secondary, weak) {\n"
274274 " secondary.delete_if { |k, v| !weak.key?(v) }\n"
275275 "}\n" );
@@ -287,43 +287,61 @@ static void SecondaryMap_Init() {
287287// secondary map that are no longer present in the WeakMap. The logic of
288288// how often to perform this GC is an artbirary tuning parameter that
289289// represents a straightforward CPU/memory tradeoff.
290+ //
291+ // Requires: secondary_map_mutex is held.
290292static void SecondaryMap_MaybeGC () {
293+ PBRUBY_ASSERT (rb_mutex_locked_p (secondary_map_mutex ) == Qtrue );
291294 size_t weak_len = NUM2ULL (rb_funcall (weak_obj_cache , length , 0 ));
292295 size_t secondary_len = RHASH_SIZE (secondary_map );
296+ if (secondary_len < weak_len ) {
297+ // Logically this case should not be possible: a valid entry cannot exist in
298+ // the weak table unless there is a corresponding entry in the secondary
299+ // table. It should *always* be the case that secondary_len >= weak_len.
300+ //
301+ // However ObjectSpace::WeakMap#length (and therefore weak_len) is
302+ // unreliable: it overreports its true length by including non-live objects.
303+ // However these non-live objects are not yielded in iteration, so we may
304+ // have previously deleted them from the secondary map in a previous
305+ // invocation of SecondaryMap_MaybeGC().
306+ //
307+ // In this case, we can't measure any waste, so we just return.
308+ return ;
309+ }
293310 size_t waste = secondary_len - weak_len ;
294- PBRUBY_ASSERT (secondary_len >= weak_len );
295311 // GC if we could remove at least 2000 entries or 20% of the table size
296312 // (whichever is greater). Since the cost of the GC pass is O(N), we
297313 // want to make sure that we condition this on overall table size, to
298314 // avoid O(N^2) CPU costs.
299315 size_t threshold = PBRUBY_MAX (secondary_len * 0.2 , 2000 );
300316 if (waste > threshold ) {
301- rb_funcall (gc_secondary_map , rb_intern ("call" ), 2 , secondary_map , weak_obj_cache );
317+ rb_funcall (gc_secondary_map_lambda , rb_intern ("call" ), 2 ,
318+ secondary_map , weak_obj_cache );
302319 }
303320}
304321
305- static VALUE SecondaryMap_Get (VALUE key ) {
322+ // Requires: secondary_map_mutex is held by this thread iff create == true.
323+ static VALUE SecondaryMap_Get (VALUE key , bool create ) {
324+ PBRUBY_ASSERT (!create || rb_mutex_locked_p (secondary_map_mutex ) == Qtrue );
306325 VALUE ret = rb_hash_lookup (secondary_map , key );
307- if (ret == Qnil ) {
308- rb_mutex_lock (secondary_map_mutex );
326+ if (ret == Qnil && create ) {
309327 SecondaryMap_MaybeGC ();
310328 ret = rb_eval_string ("Object.new" );
311329 rb_hash_aset (secondary_map , key , ret );
312- rb_mutex_unlock (secondary_map_mutex );
313330 }
314331 return ret ;
315332}
316333
317334#endif
318335
319- static VALUE ObjectCache_GetKey (const void * key ) {
336+ // Requires: secondary_map_mutex is held by this thread iff create == true.
337+ static VALUE ObjectCache_GetKey (const void * key , bool create ) {
320338 char buf [sizeof (key )];
321339 memcpy (& buf , & key , sizeof (key ));
322340 intptr_t key_int = (intptr_t )key ;
323341 PBRUBY_ASSERT ((key_int & 3 ) == 0 );
324342 VALUE ret = LL2NUM (key_int >> 2 );
325343#if USE_SECONDARY_MAP
326- ret = SecondaryMap_Get (ret );
344+ ret = SecondaryMap_Get (ret , create );
327345#endif
328346 return ret ;
329347}
@@ -347,14 +365,20 @@ static void ObjectCache_Init() {
347365
348366void ObjectCache_Add (const void * key , VALUE val ) {
349367 PBRUBY_ASSERT (ObjectCache_Get (key ) == Qnil );
350- VALUE key_rb = ObjectCache_GetKey (key );
368+ #if USE_SECONDARY_MAP
369+ rb_mutex_lock (secondary_map_mutex );
370+ #endif
371+ VALUE key_rb = ObjectCache_GetKey (key , true);
351372 rb_funcall (weak_obj_cache , item_set , 2 , key_rb , val );
373+ #if USE_SECONDARY_MAP
374+ rb_mutex_unlock (secondary_map_mutex );
375+ #endif
352376 PBRUBY_ASSERT (ObjectCache_Get (key ) == val );
353377}
354378
355379// Returns the cached object for this key, if any. Otherwise returns Qnil.
356380VALUE ObjectCache_Get (const void * key ) {
357- VALUE key_rb = ObjectCache_GetKey (key );
381+ VALUE key_rb = ObjectCache_GetKey (key , false );
358382 return rb_funcall (weak_obj_cache , item_get , 1 , key_rb );
359383}
360384
0 commit comments