bootsnap 0.2.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.
@@ -0,0 +1,56 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ 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
+
9
+ # For each method that adds items to one end or another of the array
10
+ # (<<, push, unshift, concat), override that method to also notify the
11
+ # 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)
16
+ shovel_without_lpc(entry)
17
+ end
18
+
19
+ sc.send(:alias_method, :push_without_lpc, :push)
20
+ arr.define_singleton_method(:push) do |*entries|
21
+ observer.push_paths(self, *entries)
22
+ push_without_lpc(*entries)
23
+ end
24
+
25
+ sc.send(:alias_method, :unshift_without_lpc, :unshift)
26
+ arr.define_singleton_method(:unshift) do |*entries|
27
+ observer.unshift_paths(self, *entries)
28
+ unshift_without_lpc(*entries)
29
+ end
30
+
31
+ sc.send(:alias_method, :concat_without_lpc, :concat)
32
+ arr.define_singleton_method(:concat) do |entries|
33
+ observer.push_paths(self, *entries)
34
+ concat_without_lpc(entries)
35
+ end
36
+
37
+ # For each method that modifies the array more aggressively, override
38
+ # the method to also have the observer completely reconstruct its state
39
+ # after the modification. Many of these could be made to modify the
40
+ # internal state of the LoadPathCache::Cache more efficiently, but the
41
+ # accounting cost would be greater than the hit from these, since we
42
+ # actively discourage calling them.
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
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,68 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ module CoreExt
4
+ module ActiveSupport
5
+ def self.with_bootsnap_fallback(error)
6
+ yield
7
+ rescue error
8
+ without_bootsnap_cache { yield }
9
+ end
10
+
11
+ def self.without_bootsnap_cache
12
+ prev = Thread.current[:without_bootsnap_cache] || false
13
+ Thread.current[:without_bootsnap_cache] = true
14
+ yield
15
+ ensure
16
+ Thread.current[:without_bootsnap_cache] = prev
17
+ end
18
+
19
+ module ClassMethods
20
+ def autoload_paths=(o)
21
+ r = super
22
+ Bootsnap::LoadPathCache.autoload_paths_cache.reinitialize(o)
23
+ r
24
+ end
25
+
26
+ def search_for_file(path)
27
+ return super if Thread.current[:without_bootsnap_cache]
28
+ begin
29
+ Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
30
+ rescue Bootsnap::LoadPathCache::ReturnFalse
31
+ nil # doesn't really apply here
32
+ end
33
+ end
34
+
35
+ def autoloadable_module?(path_suffix)
36
+ Bootsnap::LoadPathCache.autoload_paths_cache.has_dir?(path_suffix)
37
+ end
38
+
39
+ def remove_constant(const)
40
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
41
+ end
42
+
43
+ # If we can't find a constant using the patched implementation of
44
+ # search_for_file, try again with the default implementation.
45
+ #
46
+ # These methods call search_for_file, and we want to modify its
47
+ # behaviour. The gymnastics here are a bit awkward, but it prevents
48
+ # 200+ lines of monkeypatches.
49
+ def load_missing_constant(from_mod, const_name)
50
+ CoreExt::ActiveSupport.with_bootsnap_fallback(NameError) { super }
51
+ end
52
+
53
+ def depend_on(file_name, message = "No such file to load -- %s.rb")
54
+ CoreExt::ActiveSupport.with_bootsnap_fallback(LoadError) { super }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ module ActiveSupport
63
+ module Dependencies
64
+ class << self
65
+ prepend Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,88 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ module CoreExt
4
+ def self.make_load_error(path)
5
+ err = LoadError.new("cannot load such file -- #{path}")
6
+ err.define_singleton_method(:path) { path }
7
+ err
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ 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
26
+
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)
38
+ end
39
+ rescue Bootsnap::LoadPathCache::ReturnFalse
40
+ return false
41
+ rescue Bootsnap::LoadPathCache::FallbackScan
42
+ load_without_cache(path, wrap)
43
+ end
44
+ end
45
+
46
+ class << Kernel
47
+ alias_method :require_without_cache, :require
48
+ 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)
53
+ end
54
+ rescue Bootsnap::LoadPathCache::ReturnFalse
55
+ return false
56
+ rescue Bootsnap::LoadPathCache::FallbackScan
57
+ require_without_cache(path)
58
+ end
59
+
60
+ alias_method :load_without_cache, :load
61
+ 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)
71
+ end
72
+ rescue Bootsnap::LoadPathCache::ReturnFalse
73
+ return false
74
+ rescue Bootsnap::LoadPathCache::FallbackScan
75
+ load_without_cache(path, wrap)
76
+ end
77
+ end
78
+
79
+ class Module
80
+ alias_method :autoload_without_cache, :autoload
81
+ def autoload(const, path)
82
+ autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
83
+ rescue Bootsnap::LoadPathCache::ReturnFalse
84
+ return false
85
+ rescue Bootsnap::LoadPathCache::FallbackScan
86
+ autoload_without_cache(const, path)
87
+ end
88
+ end
@@ -0,0 +1,93 @@
1
+ require_relative 'path_scanner'
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ class Path
6
+ # A path is considered 'stable' if it is part of a Gem.path or the ruby
7
+ # distribution. When adding or removing files in these paths, the cache
8
+ # must be cleared before the change will be noticed.
9
+ def stable?
10
+ stability == STABLE
11
+ end
12
+
13
+ # A path is considered volatile if it doesn't live under a Gem.path or
14
+ # the ruby distribution root. These paths are scanned for new additions
15
+ # more frequently.
16
+ def volatile?
17
+ stability == VOLATILE
18
+ end
19
+
20
+ attr_reader :path
21
+
22
+ def initialize(path)
23
+ @path = path
24
+ end
25
+
26
+ # Return a list of all the requirable files and all of the subdirectories
27
+ # of this +Path+.
28
+ def entries_and_dirs(store)
29
+ if stable?
30
+ # the cached_mtime field is unused for 'stable' paths, but is
31
+ # set to zero anyway, just in case we change the stability heuristics.
32
+ _, entries, dirs = store.get(path)
33
+ return [entries, dirs] if entries # cache hit
34
+ entries, dirs = scan!
35
+ store.set(path, [0, entries, dirs])
36
+ return [entries, dirs]
37
+ end
38
+
39
+ cached_mtime, entries, dirs = store.get(path)
40
+
41
+ current_mtime = latest_mtime(path, dirs || [])
42
+ return [[], []] if current_mtime == -1 # path does not exist
43
+ return [entries, dirs] if cached_mtime == current_mtime
44
+
45
+ entries, dirs = scan!
46
+ store.set(path, [current_mtime, entries, dirs])
47
+ [entries, dirs]
48
+ end
49
+
50
+ private
51
+
52
+ def scan! # (expensive) returns [entries, dirs]
53
+ PathScanner.call(path)
54
+ end
55
+
56
+ # last time a directory was modified in this subtree. +dirs+ should be a
57
+ # list of relative paths to directories under +path+. e.g. for /a/b and
58
+ # /a/b/c, pass ('/a/b', ['c'])
59
+ def latest_mtime(path, dirs)
60
+ max = -1
61
+ ["", *dirs].each do |dir|
62
+ curr = begin
63
+ File.mtime("#{path}/#{dir}").to_i
64
+ rescue Errno::ENOENT
65
+ -1
66
+ end
67
+ max = curr if curr > max
68
+ end
69
+ max
70
+ end
71
+
72
+ # a Path can be either stable of volatile, depending on how frequently we
73
+ # expect its contents may change. Stable paths aren't rescanned nearly as
74
+ # often.
75
+ STABLE = :stable
76
+ VOLATILE = :volatile
77
+
78
+ RUBY_PREFIX = RbConfig::CONFIG['prefix']
79
+
80
+ def stability
81
+ @stability ||= begin
82
+ if Gem.path.detect { |p| path.start_with?(p) }
83
+ STABLE
84
+ elsif path.start_with?(RUBY_PREFIX)
85
+ STABLE
86
+ else
87
+ VOLATILE
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../load_path_cache'
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ module PathScanner
6
+ RelativePathNotSupported = Class.new(StandardError)
7
+
8
+ REQUIRABLES_AND_DIRS = "/**/*{#{DOT_RB},#{DL_EXTENSIONS.join(',')},/}"
9
+ IS_DIR = %r{(.*)/\z}
10
+ NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
11
+ ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
12
+ BUNDLE_PATH = (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
13
+
14
+ def self.call(path)
15
+ raise RelativePathNotSupported unless path.start_with?(SLASH)
16
+
17
+ relative_slice = (path.size + 1)..-1
18
+ # If the bundle path is a descendent of this path, we do additional
19
+ # checks to prevent recursing into the bundle path as we recurse
20
+ # through this path. We don't want to scan the bundle path because
21
+ # anything useful in it will be present on other load path items.
22
+ #
23
+ # This can happen if, for example, the user adds '.' to the load path,
24
+ # and the bundle path is '.bundle'.
25
+ contains_bundle_path = BUNDLE_PATH.start_with?(path)
26
+
27
+ dirs = []
28
+ requirables = []
29
+
30
+ Dir.glob(path + REQUIRABLES_AND_DIRS).each do |absolute_path|
31
+ next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
32
+ relative_path = absolute_path.slice!(relative_slice)
33
+
34
+ if md = relative_path.match(IS_DIR)
35
+ dirs << md[1]
36
+ else
37
+ requirables << relative_path
38
+ end
39
+ end
40
+ [requirables, dirs]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ require_relative '../explicit_require'
2
+
3
+ Bootsnap::ExplicitRequire.with_gems('snappy') { require 'snappy' }
4
+ Bootsnap::ExplicitRequire.with_gems('msgpack') { require 'msgpack' }
5
+ Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
6
+
7
+ module Bootsnap
8
+ module LoadPathCache
9
+ class Store
10
+ NestedTransactionError = Class.new(StandardError)
11
+ SetOutsideTransactionNotAllowed = Class.new(StandardError)
12
+
13
+ def initialize(store_path)
14
+ @store_path = store_path
15
+ load_data
16
+ end
17
+
18
+ def get(key)
19
+ @data[key]
20
+ end
21
+
22
+ def fetch(key)
23
+ raise SetOutsideTransactionNotAllowed unless @in_txn
24
+ v = get(key)
25
+ unless v
26
+ @dirty = true
27
+ v = yield
28
+ @data[key] = v
29
+ end
30
+ v
31
+ end
32
+
33
+ def set(key, value)
34
+ raise SetOutsideTransactionNotAllowed unless @in_txn
35
+ if value != @data[key]
36
+ @dirty = true
37
+ @data[key] = value
38
+ end
39
+ end
40
+
41
+ def transaction
42
+ raise NestedTransactionError if @in_txn
43
+ @in_txn = true
44
+ yield
45
+ ensure
46
+ commit_transaction
47
+ @in_txn = false
48
+ end
49
+
50
+ private
51
+
52
+ def commit_transaction
53
+ if @dirty
54
+ dump_data
55
+ @dirty = false
56
+ end
57
+ end
58
+
59
+ def load_data
60
+ @data = begin
61
+ MessagePack.load(Snappy.inflate(File.binread(@store_path)))
62
+ rescue Errno::ENOENT, Snappy::Error
63
+ {}
64
+ end
65
+ end
66
+
67
+ def dump_data
68
+ # Change contents atomically so other processes can't get invalid
69
+ # caches if they read at an inopportune time.
70
+ tmp = "#{@store_path}.#{(rand * 100000).to_i}.tmp"
71
+ FileUtils.mkpath(File.dirname(tmp))
72
+ File.binwrite(tmp, Snappy.deflate(MessagePack.dump(@data)))
73
+ FileUtils.mv(tmp, @store_path)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module Bootsnap
2
+ VERSION = "0.2.0"
3
+ end
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bootsnap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Burke Libbey
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake-compiler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mocha
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: msgpack
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: snappy
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.0.15
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.0.15
111
+ description: wip.
112
+ email:
113
+ - burke.libbey@shopify.com
114
+ executables: []
115
+ extensions:
116
+ - ext/bootsnap/extconf.rb
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - ".rubocop.yml"
121
+ - CONTRIBUTING.md
122
+ - Gemfile
123
+ - README.md
124
+ - Rakefile
125
+ - bin/console
126
+ - bin/setup
127
+ - bin/testunit
128
+ - bootsnap.gemspec
129
+ - dev.yml
130
+ - ext/bootsnap/bootsnap.c
131
+ - ext/bootsnap/bootsnap.h
132
+ - ext/bootsnap/crc32.c
133
+ - ext/bootsnap/extconf.rb
134
+ - lib/bootsnap.rb
135
+ - lib/bootsnap/compile_cache.rb
136
+ - lib/bootsnap/compile_cache/iseq.rb
137
+ - lib/bootsnap/compile_cache/yaml.rb
138
+ - lib/bootsnap/explicit_require.rb
139
+ - lib/bootsnap/load_path_cache.rb
140
+ - lib/bootsnap/load_path_cache/cache.rb
141
+ - lib/bootsnap/load_path_cache/change_observer.rb
142
+ - lib/bootsnap/load_path_cache/core_ext/active_support.rb
143
+ - lib/bootsnap/load_path_cache/core_ext/kernel_require.rb
144
+ - lib/bootsnap/load_path_cache/path.rb
145
+ - lib/bootsnap/load_path_cache/path_scanner.rb
146
+ - lib/bootsnap/load_path_cache/store.rb
147
+ - lib/bootsnap/version.rb
148
+ homepage: https://github.com/Shopify/bootsnap
149
+ licenses:
150
+ - MIT
151
+ metadata: {}
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubyforge_project:
168
+ rubygems_version: 2.6.10
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: wip
172
+ test_files: []