• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Protocol Buffers - Google's data interchange format
2# Copyright 2023 Google Inc.  All rights reserved.
3#
4# Use of this source code is governed by a BSD-style
5# license that can be found in the LICENSE file or at
6# https://developers.google.com/open-source/licenses/bsd
7
8module Google
9  module Protobuf
10    module Internal
11      # A pointer -> Ruby Object cache that keeps references to Ruby wrapper
12      # objects.  This allows us to look up any Ruby wrapper object by the address
13      # of the object it is wrapping. That way we can avoid ever creating two
14      # different wrapper objects for the same C object, which saves memory and
15      # preserves object identity.
16      #
17      # We use WeakMap for the cache. If sizeof(long) > sizeof(VALUE), we also
18      # need a secondary Hash to store WeakMap keys, because our pointer keys may
19      # need to be stored as Bignum instead of Fixnum.  Since WeakMap is weak for
20      # both keys and values, a Bignum key will cause the WeakMap entry to be
21      # collected immediately unless there is another reference to the Bignum.
22      # This happens on 64-bit Windows, on which pointers are 64 bits but longs
23      # are 32 bits. In this case, we enable the secondary Hash to hold the keys
24      # and prevent them from being collected.
25      class ObjectCache
26        def initialize
27          @map = ObjectSpace::WeakMap.new
28          @mutex = Mutex.new
29        end
30
31        def get(key)
32          @map[key]
33        end
34
35        def try_add(key, value)
36          @map[key] || @mutex.synchronize do
37            @map[key] ||= value
38          end
39        end
40      end
41
42      class LegacyObjectCache
43        def initialize
44          @secondary_map = {}
45          @map = ObjectSpace::WeakMap.new
46          @mutex = Mutex.new
47        end
48
49        def get(key)
50          value = if secondary_key = @secondary_map[key]
51            @map[secondary_key]
52          else
53            @mutex.synchronize do
54              @map[(@secondary_map[key] ||= Object.new)]
55            end
56          end
57
58          # GC if we could remove at least 2000 entries or 20% of the table size
59          # (whichever is greater).  Since the cost of the GC pass is O(N), we
60          # want to make sure that we condition this on overall table size, to
61          # avoid O(N^2) CPU costs.
62          cutoff = (@secondary_map.size * 0.2).ceil
63          cutoff = 2_000 if cutoff < 2_000
64          if (@secondary_map.size - @map.size) > cutoff
65            purge
66          end
67
68          value
69        end
70
71        def try_add(key, value)
72          if secondary_key = @secondary_map[key]
73            if old_value = @map[secondary_key]
74              return old_value
75            end
76          end
77
78          @mutex.synchronize do
79            secondary_key ||= (@secondary_map[key] ||= Object.new)
80            @map[secondary_key] ||= value
81          end
82        end
83
84        private
85
86        def purge
87          @mutex.synchronize do
88            @secondary_map.each do |key, secondary_key|
89              unless @map.key?(secondary_key)
90                @secondary_map.delete(key)
91              end
92            end
93          end
94          nil
95        end
96      end
97    end
98  end
99end
100