bootsnap 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []