bootsnap 1.1.8-java → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +103 -0
  3. data/README.md +47 -6
  4. data/exe/bootsnap +5 -0
  5. data/ext/bootsnap/bootsnap.c +217 -88
  6. data/ext/bootsnap/extconf.rb +3 -1
  7. data/lib/bootsnap.rb +17 -8
  8. data/lib/bootsnap/bundler.rb +6 -3
  9. data/lib/bootsnap/cli.rb +246 -0
  10. data/lib/bootsnap/cli/worker_pool.rb +131 -0
  11. data/lib/bootsnap/compile_cache.rb +32 -4
  12. data/lib/bootsnap/compile_cache/iseq.rb +32 -15
  13. data/lib/bootsnap/compile_cache/yaml.rb +94 -40
  14. data/lib/bootsnap/explicit_require.rb +2 -1
  15. data/lib/bootsnap/load_path_cache.rb +35 -9
  16. data/lib/bootsnap/load_path_cache/cache.rb +48 -29
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +36 -29
  18. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +39 -7
  19. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +70 -53
  20. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +18 -0
  21. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +148 -0
  22. data/lib/bootsnap/load_path_cache/path.rb +8 -7
  23. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -39
  24. data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
  25. data/lib/bootsnap/load_path_cache/store.rb +20 -14
  26. data/lib/bootsnap/setup.rb +11 -13
  27. data/lib/bootsnap/version.rb +2 -1
  28. metadata +44 -45
  29. data/.gitignore +0 -17
  30. data/.rubocop.yml +0 -20
  31. data/.travis.yml +0 -4
  32. data/CODE_OF_CONDUCT.md +0 -74
  33. data/CONTRIBUTING.md +0 -21
  34. data/Gemfile +0 -8
  35. data/Rakefile +0 -11
  36. data/bin/console +0 -14
  37. data/bin/setup +0 -8
  38. data/bin/testunit +0 -8
  39. data/bootsnap.gemspec +0 -39
  40. data/dev.yml +0 -10
@@ -1,37 +1,37 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module ChangeObserver
4
- def self.register(observer, arr)
5
- # Re-overriding these methods on an array that already has them would
6
- # cause StackOverflowErrors
7
- return if arr.respond_to?(:push_without_lpc)
8
-
5
+ module ArrayMixin
9
6
  # For each method that adds items to one end or another of the array
10
7
  # (<<, push, unshift, concat), override that method to also notify the
11
8
  # observer of the change.
12
- sc = arr.singleton_class
13
- sc.send(:alias_method, :shovel_without_lpc, :<<)
14
- arr.define_singleton_method(:<<) do |entry|
15
- observer.push_paths(self, entry.to_s)
16
- shovel_without_lpc(entry)
9
+ def <<(entry)
10
+ @lpc_observer.push_paths(self, entry.to_s)
11
+ super
12
+ end
13
+
14
+ def push(*entries)
15
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
16
+ super
17
17
  end
18
18
 
19
- sc.send(:alias_method, :push_without_lpc, :push)
20
- arr.define_singleton_method(:push) do |*entries|
21
- observer.push_paths(self, *entries.map(&:to_s))
22
- push_without_lpc(*entries)
19
+ def unshift(*entries)
20
+ @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
21
+ super
23
22
  end
24
23
 
25
- sc.send(:alias_method, :unshift_without_lpc, :unshift)
26
- arr.define_singleton_method(:unshift) do |*entries|
27
- observer.unshift_paths(self, *entries.map(&:to_s))
28
- unshift_without_lpc(*entries)
24
+ def concat(entries)
25
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
26
+ super
29
27
  end
30
28
 
31
- sc.send(:alias_method, :concat_without_lpc, :concat)
32
- arr.define_singleton_method(:concat) do |entries|
33
- observer.push_paths(self, *entries.map(&:to_s))
34
- concat_without_lpc(entries)
29
+ # uniq! keeps the first occurrence of each path, otherwise preserving
30
+ # order, preserving the effective load path
31
+ def uniq!(*args)
32
+ ret = super
33
+ @lpc_observer.reinitialize if block_given? || !args.empty?
34
+ ret
35
35
  end
36
36
 
37
37
  # For each method that modifies the array more aggressively, override
@@ -41,16 +41,23 @@ module Bootsnap
41
41
  # accounting cost would be greater than the hit from these, since we
42
42
  # actively discourage calling them.
43
43
  %i(
44
- collect! compact! delete delete_at delete_if fill flatten! insert map!
45
- reject! reverse! select! shuffle! shift slice! sort! sort_by!
46
- ).each do |meth|
47
- sc.send(:alias_method, :"#{meth}_without_lpc", meth)
48
- arr.define_singleton_method(meth) do |*a|
49
- send(:"#{meth}_without_lpc", *a)
50
- observer.reinitialize
44
+ []= clear collect! compact! delete delete_at delete_if fill flatten!
45
+ insert keep_if map! pop reject! replace reverse! rotate! select!
46
+ shift shuffle! slice! sort! sort_by!
47
+ ).each do |method_name|
48
+ define_method(method_name) do |*args, &block|
49
+ ret = super(*args, &block)
50
+ @lpc_observer.reinitialize
51
+ ret
51
52
  end
52
53
  end
53
54
  end
55
+
56
+ def self.register(observer, arr)
57
+ return if arr.frozen? # can't register observer, but no need to.
58
+ arr.instance_variable_set(:@lpc_observer, observer)
59
+ arr.extend(ArrayMixin)
60
+ end
54
61
  end
55
62
  end
56
63
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module CoreExt
@@ -10,6 +11,14 @@ module Bootsnap
10
11
  Thread.current[:without_bootsnap_cache] = prev
11
12
  end
12
13
 
14
+ def self.allow_bootsnap_retry(allowed)
15
+ prev = Thread.current[:without_bootsnap_retry] || false
16
+ Thread.current[:without_bootsnap_retry] = !allowed
17
+ yield
18
+ ensure
19
+ Thread.current[:without_bootsnap_retry] = prev
20
+ end
21
+
13
22
  module ClassMethods
14
23
  def autoload_paths=(o)
15
24
  super
@@ -22,17 +31,25 @@ module Bootsnap
22
31
  Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
23
32
  rescue Bootsnap::LoadPathCache::ReturnFalse
24
33
  nil # doesn't really apply here
34
+ rescue Bootsnap::LoadPathCache::FallbackScan
35
+ nil # doesn't really apply here
25
36
  end
26
37
  end
27
38
 
28
39
  def autoloadable_module?(path_suffix)
29
- Bootsnap::LoadPathCache.autoload_paths_cache.has_dir?(path_suffix)
40
+ Bootsnap::LoadPathCache.autoload_paths_cache.load_dir(path_suffix)
30
41
  end
31
42
 
32
43
  def remove_constant(const)
33
44
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
34
45
  end
35
46
 
47
+ def require_or_load(*)
48
+ CoreExt::ActiveSupport.allow_bootsnap_retry(true) do
49
+ super
50
+ end
51
+ end
52
+
36
53
  # If we can't find a constant using the patched implementation of
37
54
  # search_for_file, try again with the default implementation.
38
55
  #
@@ -40,16 +57,26 @@ module Bootsnap
40
57
  # behaviour. The gymnastics here are a bit awkward, but it prevents
41
58
  # 200+ lines of monkeypatches.
42
59
  def load_missing_constant(from_mod, const_name)
43
- super
60
+ CoreExt::ActiveSupport.allow_bootsnap_retry(false) do
61
+ super
62
+ end
44
63
  rescue NameError => e
64
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
65
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
66
+
67
+ # This function can end up called recursively, we only want to
68
+ # retry at the top-level.
69
+ raise(e) if Thread.current[:without_bootsnap_retry]
70
+ # If we already had cache disabled, there's no use retrying
71
+ raise(e) if Thread.current[:without_bootsnap_cache]
45
72
  # NoMethodError is a NameError, but we only want to handle actual
46
73
  # NameError instances.
47
- raise unless e.class == NameError
74
+ raise(e) unless e.class == NameError
48
75
  # We can only confidently handle cases when *this* constant fails
49
76
  # to load, not other constants referred to by it.
50
- raise unless e.name == const_name
77
+ raise(e) unless e.name == const_name
51
78
  # If the constant was actually loaded, something else went wrong?
52
- raise if from_mod.const_defined?(const_name)
79
+ raise(e) if from_mod.const_defined?(const_name)
53
80
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
54
81
  end
55
82
 
@@ -57,7 +84,12 @@ module Bootsnap
57
84
  # reiterate it with version polymorphism here...
58
85
  def depend_on(*)
59
86
  super
60
- rescue LoadError
87
+ rescue LoadError => e
88
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
89
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
90
+
91
+ # If we already had cache disabled, there's no use retrying
92
+ raise(e) if Thread.current[:without_bootsnap_cache]
61
93
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
62
94
  end
63
95
  end
@@ -69,7 +101,7 @@ end
69
101
  module ActiveSupport
70
102
  module Dependencies
71
103
  class << self
72
- prepend Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods
104
+ prepend(Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods)
73
105
  end
74
106
  end
75
107
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module CoreExt
4
5
  def self.make_load_error(path)
5
- err = LoadError.new("cannot load such file -- #{path}")
6
+ err = LoadError.new(+"cannot load such file -- #{path}")
7
+ err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
6
8
  err.define_singleton_method(:path) { path }
7
9
  err
8
10
  end
@@ -11,78 +13,93 @@ module Bootsnap
11
13
  end
12
14
 
13
15
  module Kernel
14
- alias_method :require_without_cache, :require
15
- def require(path)
16
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
17
- require_without_cache(resolved)
18
- else
19
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
20
- end
21
- rescue Bootsnap::LoadPathCache::ReturnFalse
22
- return false
23
- rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
25
- end
16
+ module_function # rubocop:disable Style/ModuleFunction
26
17
 
27
- alias_method :load_without_cache, :load
28
- def load(path, wrap = false)
29
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
30
- load_without_cache(resolved, wrap)
31
- else
32
- # load also allows relative paths from pwd even when not in $:
33
- relative = File.expand_path(path)
34
- if File.exist?(File.expand_path(path))
35
- return load_without_cache(relative, wrap)
36
- end
37
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
18
+ alias_method(:require_without_bootsnap, :require)
19
+
20
+ # Note that require registers to $LOADED_FEATURES while load does not.
21
+ def require_with_bootsnap_lfi(path, resolved = nil)
22
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
23
+ require_without_bootsnap(resolved || path)
38
24
  end
39
- rescue Bootsnap::LoadPathCache::ReturnFalse
40
- return false
41
- rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
43
25
  end
44
- end
45
26
 
46
- class << Kernel
47
- alias_method :require_without_cache, :require
48
27
  def require(path)
49
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
50
- require_without_cache(resolved)
51
- else
52
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
28
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
29
+
30
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
31
+ return require_with_bootsnap_lfi(path, resolved)
53
32
  end
33
+
34
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
35
+ rescue LoadError => e
36
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
37
+ raise(e)
54
38
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
- return false
39
+ false
56
40
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
41
+ fallback = true
42
+ ensure
43
+ if fallback
44
+ require_with_bootsnap_lfi(path)
45
+ end
58
46
  end
59
47
 
60
- alias_method :load_without_cache, :load
48
+ alias_method(:require_relative_without_bootsnap, :require_relative)
49
+ def require_relative(path)
50
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
51
+ caller_locations(1..1).first.absolute_path, path
52
+ )
53
+ require(realpath)
54
+ end
55
+
56
+ alias_method(:load_without_bootsnap, :load)
61
57
  def load(path, wrap = false)
62
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
63
- load_without_cache(resolved, wrap)
64
- else
65
- # load also allows relative paths from pwd even when not in $:
66
- relative = File.expand_path(path)
67
- if File.exist?(relative)
68
- return load_without_cache(relative, wrap)
69
- end
70
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
58
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
59
+ return load_without_bootsnap(resolved, wrap)
71
60
  end
61
+
62
+ # load also allows relative paths from pwd even when not in $:
63
+ if File.exist?(relative = File.expand_path(path).freeze)
64
+ return load_without_bootsnap(relative, wrap)
65
+ end
66
+
67
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
68
+ rescue LoadError => e
69
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
70
+ raise(e)
72
71
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
- return false
72
+ false
74
73
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
74
+ fallback = true
75
+ ensure
76
+ if fallback
77
+ load_without_bootsnap(path, wrap)
78
+ end
76
79
  end
77
80
  end
78
81
 
79
82
  class Module
80
- alias_method :autoload_without_cache, :autoload
83
+ alias_method(:autoload_without_bootsnap, :autoload)
81
84
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
85
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
86
+ # obvious how to make it work. This feels like a pretty niche case, unclear
87
+ # if it will ever burn anyone.
88
+ #
89
+ # The challenge is that we don't control the point at which the entry gets
90
+ # added to $LOADED_FEATURES and won't be able to hook that modification
91
+ # since it's done in C-land.
92
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
93
+ rescue LoadError => e
94
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
95
+ raise(e)
83
96
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
- return false
97
+ false
85
98
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
99
+ fallback = true
100
+ ensure
101
+ if fallback
102
+ autoload_without_bootsnap(const, path)
103
+ end
87
104
  end
88
105
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ class << $LOADED_FEATURES
3
+ alias_method(:delete_without_bootsnap, :delete)
4
+ def delete(key)
5
+ Bootsnap::LoadPathCache.loaded_features_index.purge(key)
6
+ delete_without_bootsnap(key)
7
+ end
8
+
9
+ alias_method(:reject_without_bootsnap!, :reject!)
10
+ def reject!(&block)
11
+ backup = dup
12
+
13
+ # FIXME: if no block is passed we'd need to return a decorated iterator
14
+ reject_without_bootsnap!(&block)
15
+
16
+ Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self)
17
+ end
18
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
6
+ # we can't easily obtain an interface to.
7
+ #
8
+ # This works around an issue where, without bootsnap, *ruby* knows that it
9
+ # has already required a file by its short name (e.g. require 'bundler') if
10
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
11
+ # different absolute path. This class makes bootsnap smart enough to
12
+ # realize that it has already loaded 'bundler', and not just
13
+ # '/path/to/bundler'.
14
+ #
15
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
16
+ #
17
+ # 1. `require 'a'`
18
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
19
+ # 3. `require 'a'`
20
+ #
21
+ # Ruby returns false from step 3.
22
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
23
+ # `a.rb`s.
24
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
25
+ # returning false like ruby.
26
+ class LoadedFeaturesIndex
27
+ def initialize
28
+ @lfi = {}
29
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
30
+
31
+ # In theory the user could mutate $LOADED_FEATURES and invalidate our
32
+ # cache. If this ever comes up in practice — or if you, the
33
+ # enterprising reader, feels inclined to solve this problem — we could
34
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
+ # updates to our @lfi.
36
+ $LOADED_FEATURES.each do |feat|
37
+ hash = feat.hash
38
+ $LOAD_PATH.each do |lpe|
39
+ next unless feat.start_with?(lpe)
40
+ # /a/b/lib/my/foo.rb
41
+ # ^^^^^^^^^
42
+ short = feat[(lpe.length + 1)..-1]
43
+ stripped = strip_extension_if_elidable(short)
44
+ @lfi[short] = hash
45
+ @lfi[stripped] = hash
46
+ end
47
+ end
48
+ end
49
+
50
+ # We've optimized for initialize and register to be fast, and purge to be tolerable.
51
+ # If access patterns make this not-okay, we can lazy-invert the LFI on
52
+ # first purge and work from there.
53
+ def purge(feature)
54
+ @mutex.synchronize do
55
+ feat_hash = feature.hash
56
+ @lfi.reject! { |_, hash| hash == feat_hash }
57
+ end
58
+ end
59
+
60
+ def purge_multi(features)
61
+ rejected_hashes = features.map(&:hash).to_set
62
+ @mutex.synchronize do
63
+ @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
64
+ end
65
+ end
66
+
67
+ def key?(feature)
68
+ @mutex.synchronize { @lfi.key?(feature) }
69
+ end
70
+
71
+ # There is a relatively uncommon case where we could miss adding an
72
+ # entry:
73
+ #
74
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
75
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
76
+ # pass `long` (the full expanded absolute path), then we did are not able
77
+ # to confidently add the `bundler.rb` form to @lfi.
78
+ #
79
+ # We could either:
80
+ #
81
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
82
+ # not quite right; or
83
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
+ # entry.
85
+ def register(short, long = nil)
86
+ if long.nil?
87
+ pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
88
+ len = $LOADED_FEATURES.size
89
+ ret = yield
90
+ long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
91
+ else
92
+ ret = yield
93
+ end
94
+
95
+ hash = long.hash
96
+
97
+ # Do we have a filename with an elidable extension, e.g.,
98
+ # 'bundler.rb', or 'libgit2.so'?
99
+ altname = if extension_elidable?(short)
100
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
101
+ strip_extension_if_elidable(short)
102
+ elsif long && (ext = File.extname(long.freeze))
103
+ # We already know the extension of the actual file this
104
+ # resolves to, so put that back on.
105
+ short + ext
106
+ end
107
+
108
+ @mutex.synchronize do
109
+ @lfi[short] = hash
110
+ (@lfi[altname] = hash) if altname
111
+ end
112
+
113
+ ret
114
+ end
115
+
116
+ private
117
+
118
+ STRIP_EXTENSION = /\.[^.]*?$/
119
+ private_constant(:STRIP_EXTENSION)
120
+
121
+ # Might Ruby automatically search for this extension if
122
+ # someone tries to 'require' the file without it? E.g. Ruby
123
+ # will implicitly try 'x.rb' if you ask for 'x'.
124
+ #
125
+ # This is complex and platform-dependent, and the Ruby docs are a little
126
+ # handwavy about what will be tried when and in what order.
127
+ # So optimistically pretend that all known elidable extensions
128
+ # will be tried on all platforms, and that people are unlikely
129
+ # to name files in a way that assumes otherwise.
130
+ # (E.g. It's unlikely that someone will know that their code
131
+ # will _never_ run on MacOS, and therefore think they can get away
132
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
+ #
134
+ # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
135
+ def extension_elidable?(f)
136
+ f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
137
+ end
138
+
139
+ def strip_extension_if_elidable(f)
140
+ if extension_elidable?(f)
141
+ f.sub(STRIP_EXTENSION, '')
142
+ else
143
+ f
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end