Class: Zeitwerk::Cref::Map

Inherits:
Object
  • Object
show all
Defined in:
lib/zeitwerk/cref/map.rb

Overview

This class emulates a hash table whose keys are of type Zeitwerk::Cref.

It is a synchronized 2-level hash. The keys of the top one, stored in ‘@map`, are class and module objects, but their hash code is forced to be their object IDs (see why below). Then, each one of them stores a hash table keyed on constant names as symbols. We finally store the values in those.

For example, if we store values 0, 1, and 2 for the crefs that would correspond to ‘M::X`, `M::Y`, and `N::Z`, the map will look like this:

{ M => { X: 0, :Y => 1 }, N => { Z: 2 } }

This structure is internal, so only the needed interface is implemented.

Why not use tables that map pairs [Module, Symbol] to their values? Because class and module objects are not guaranteed to be hashable, the ‘hash` method may have been overridden:

https://github.com/fxn/zeitwerk/issues/188

We can also use a 1-level hash whose keys are the corresponding class and module names. In the example above it would be:

{ "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }

The gem used this approach for several years.

Another option would be to make crefs hashable. I tried with hash code

real_mod_hash(mod) ^ cname.hash

and the matching eql?, but that was about 1.8x slower.

Finally, I came with this solution which is 1.6x faster than the previous one based on class and module names, even being synchronized. Also, client code feels natural, since crefs are central objects in Zeitwerk’s implementation.

Instance Method Summary collapse

Constructor Details

#initializeMap

:nodoc: all



40
41
42
43
44
# File 'lib/zeitwerk/cref/map.rb', line 40

def initialize
  @map = {}
  @map.compare_by_identity
  @mutex = Mutex.new
end

Instance Method Details

#[](cref) ⇒ Object



55
56
57
58
59
# File 'lib/zeitwerk/cref/map.rb', line 55

def [](cref)
  @mutex.synchronize do
    @map[cref.mod]&.[](cref.cname)
  end
end

#[]=(cref, value) ⇒ Object



47
48
49
50
51
52
# File 'lib/zeitwerk/cref/map.rb', line 47

def []=(cref, value)
  @mutex.synchronize do
    cnames = (@map[cref.mod] ||= {})
    cnames[cref.cname] = value
  end
end

#clearObject



112
113
114
115
116
# File 'lib/zeitwerk/cref/map.rb', line 112

def clear
  @mutex.synchronize do
    @map.clear
  end
end

#delete(cref) ⇒ Object



70
71
72
# File 'lib/zeitwerk/cref/map.rb', line 70

def delete(cref)
  delete_mod_cname(cref.mod, cref.cname)
end

#delete_by_value(value) ⇒ Object



89
90
91
92
93
94
95
96
# File 'lib/zeitwerk/cref/map.rb', line 89

def delete_by_value(value)
  @mutex.synchronize do
    @map.delete_if do |mod, cnames|
      cnames.delete_if { _2 == value }
      cnames.empty?
    end
  end
end

#delete_mod_cname(mod, cname) ⇒ Object

Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer to not create a cref in every call, since that is global.



78
79
80
81
82
83
84
85
86
# File 'lib/zeitwerk/cref/map.rb', line 78

def delete_mod_cname(mod, cname)
  @mutex.synchronize do
    if cnames = @map[mod]
      value = cnames.delete(cname)
      @map.delete(mod) if cnames.empty?
      value
    end
  end
end

#each_keyObject

Order of yielded crefs is undefined.



101
102
103
104
105
106
107
108
109
# File 'lib/zeitwerk/cref/map.rb', line 101

def each_key
  @mutex.synchronize do
    @map.each do |mod, cnames|
      cnames.each_key do |cname|
        yield Zeitwerk::Cref.new(mod, cname)
      end
    end
  end
end

#empty?Boolean

Returns:

  • (Boolean)


119
120
121
122
123
# File 'lib/zeitwerk/cref/map.rb', line 119

def empty? # for tests
  @mutex.synchronize do
    @map.empty?
  end
end

#get_or_set(cref, &block) ⇒ Object



62
63
64
65
66
67
# File 'lib/zeitwerk/cref/map.rb', line 62

def get_or_set(cref, &block)
  @mutex.synchronize do
    cnames = (@map[cref.mod] ||= {})
    cnames.fetch(cref.cname) { cnames[cref.cname] = block.call }
  end
end