pax_global_header00006660000000000000000000000064136250322100014504gustar00rootroot0000000000000052 comment=ce8404376312d6012fa7c8152d2269ae01b6fa80 bootsnap-1.4.6/000077500000000000000000000000001362503221000133415ustar00rootroot00000000000000bootsnap-1.4.6/.github/000077500000000000000000000000001362503221000147015ustar00rootroot00000000000000bootsnap-1.4.6/.github/CODEOWNERS000066400000000000000000000000321362503221000162670ustar00rootroot00000000000000# mvm:maintainer * @burke bootsnap-1.4.6/.github/probots.yml000066400000000000000000000000211362503221000171050ustar00rootroot00000000000000enabled: - cla bootsnap-1.4.6/.gitignore000066400000000000000000000002141362503221000153260ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a *.gem *.db mkmf.log .rubocop-* bootsnap-1.4.6/.rubocop.yml000066400000000000000000000006751362503221000156230ustar00rootroot00000000000000inherit_from: - http://shopify.github.io/ruby-style-guide/rubocop.yml AllCops: Exclude: - 'vendor/**/*' - 'tmp/**/*' TargetRubyVersion: '2.3' # This doesn't take into account retrying from an exception Lint/HandleExceptions: Enabled: false # allow String.new to create mutable strings Style/EmptyLiteral: Enabled: false # allow the use of globals which makes sense in a CLI app like this Style/GlobalVars: Enabled: false bootsnap-1.4.6/.travis.yml000066400000000000000000000003511362503221000154510ustar00rootroot00000000000000language: ruby sudo: false os: - linux - osx rvm: - ruby-2.4 - ruby-2.5 - ruby-head matrix: allow_failures: - rvm: ruby-head include: - rvm: jruby os: linux env: MINIMAL_SUPPORT=1 script: bin/ci bootsnap-1.4.6/CHANGELOG.md000066400000000000000000000060211362503221000151510ustar00rootroot00000000000000# 1.4.5 * MRI 2.7 support * Fixed concurrency bugs # 1.4.4 * Disable ISeq cache in `bootsnap/setup` by default in Ruby 2.5 # 1.4.3 * Fix some cache permissions and umask issues after switch to mkstemp # 1.4.2 * Fix bug when removing features loaded by relative path from `$LOADED_FEATURES` * Fix bug with propagation of `NameError` up from nested calls to `require` # 1.4.1 * Don't register change observers to frozen objects. # 1.4.0 * When running in development mode, always fall back to a full path scan on LoadError, making bootsnap more able to detect newly-created files. (#230) * Respect `$LOADED_FEATURES.delete` in order to support code reloading, for integration with Zeitwerk. (#230) * Minor performance improvement: flow-control exceptions no longer generate backtraces. * Better support for requiring from environments where some features are not supported (especially JRuby). (#226)k * More robust handling of OS errors when creating files. (#225) # 1.3.2 * Fix Spring + Bootsnap incompatibility when there are files with similar names. * Fix `YAML.load_file` monkey patch to keep accepting File objects as arguments. * Fix the API for `ActiveSupport::Dependencies#autoloadable_module?`. * Some performance improvements. # 1.3.1 * Change load path scanning to more correctly follow symlinks. # 1.3.0 * Handle cases where load path entries are symlinked (https://github.com/Shopify/bootsnap/pull/136) # 1.2.1 * Fix method visibility of `Kernel#require`. # 1.2.0 * Add `LoadedFeaturesIndex` to preserve fix a common bug related to `LOAD_PATH` modifications after loading bootsnap. # 1.1.8 * Don't cache YAML documents with `!ruby/object` * Fix cache write mode on Windows # 1.1.7 * Create cache entries as 0775/0664 instead of 0755/0644 * Better handling around cache updates in highly-parallel workloads # 1.1.6 * Assortment of minor bugfixes # 1.1.5 * bugfix re-release of 1.1.4 # 1.1.4 (yanked) * Avoid loading a constant twice by checking if it is already defined # 1.1.3 * Properly resolve symlinked path entries # 1.1.2 * Minor fix: deprecation warning # 1.1.1 * Fix crash in `Native.compile_option_crc32=` on 32-bit platforms. # 1.1.0 * Add `bootsnap/setup` * Support jruby (without compile caching features) * Better deoptimization when Coverage is enabled * Consider `Bundler.bundle_path` to be stable # 1.0.0 * (none) # 0.3.2 * Minor performance savings around checking validity of cache in the presence of relative paths. * When coverage is enabled, skips optimization instead of exploding. # 0.3.1 * Don't whitelist paths under `RbConfig::CONFIG['prefix']` as stable; instead use `['libdir']` (#41). * Catch `EOFError` when reading load-path-cache and regenerate cache. * Support relative paths in load-path-cache. # 0.3.0 * Migrate CompileCache from xattr as a cache backend to a cache directory * Adds support for Linux and FreeBSD # 0.2.15 * Support more versions of ActiveSupport (`depend_on`'s signature varies; don't reiterate it) * Fix bug in handling autoloaded modules that raise NoMethodError bootsnap-1.4.6/CODE_OF_CONDUCT.md000066400000000000000000000062271362503221000161470ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at burke@libbey.me. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ bootsnap-1.4.6/CONTRIBUTING.md000066400000000000000000000014251362503221000155740ustar00rootroot00000000000000# Contributing to Bootsnap We love receiving pull requests! ## Standards * PR should explain what the feature does, and why the change exists. * PR should include any carrier specific documentation explaining how it works. * Code _must_ be tested, including both unit and remote tests where applicable. * Be consistent. Write clean code that follows [Ruby community standards](https://github.com/bbatsov/ruby-style-guide). * Code should be generic and reusable. If you're stuck, ask questions! ## How to contribute 1. Fork it ( https://github.com/Shopify/bootsnap/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request bootsnap-1.4.6/Gemfile000066400000000000000000000002461362503221000146360ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' # Specify your gem's dependencies in bootsnap.gemspec gemspec group :development do gem 'rubocop' end bootsnap-1.4.6/LICENSE.txt000066400000000000000000000020701362503221000151630ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017 Shopify, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bootsnap-1.4.6/README.jp.md000066400000000000000000000330351362503221000152340ustar00rootroot00000000000000# Bootsnap [![Build Status](https://travis-ci.org/Shopify/bootsnap.svg?branch=master)](https://travis-ci.org/Shopify/bootsnap) Bootsnap は RubyVM におけるバイトコード生成やファイルルックアップ等の時間のかかる処理を最適化するためのライブラリです。ActiveSupport や YAML もサポートしています。[内部動作](#内部動作)もご覧ください。 注意書き: このライブラリは英語話者によって管理されています。この README は日本語ですが、日本語でのサポートはしておらず、リクエストにお答えすることもできません。バイリンガルの方がサポートをサポートしてくださる場合はお知らせください!:) ### パフォーマンス * [Discourse](https://github.com/discourse/discourse) では、約6秒から3秒まで、約50%の起動時間短縮が確認されています。 * 小さなアプリケーションでも、50%の改善(3.6秒から1.8秒)が確認されています。 * 非常に巨大でモノリシックなアプリである Shopify のプラットフォームでは、約25秒から6.5秒へと約75%短縮されました。 ## 使用方法 この gem は macOS と Linux で作動します。まずは、`bootsnap` を `Gemfile` に追加します: ```ruby gem 'bootsnap', require: false ``` Rails を使用している場合は、以下のコードを、`config/boot.rb` 内にある `require 'bundler/setup'` の直後に追加してください。 ```ruby require 'bootsnap/setup' ``` 単に `gem 'bootsnap', require: 'bootsnap/setup'` と指定することも技術的には可能ですが、最大限のパフォーマンス改善を得るためには Bootsnap をできるだけ早く読み込むことが重要です。 この require の仕組みは[こちら](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/setup.rb)で確認できます。 Rails を使用していない場合、または、より多くの設定を変更したい場合は、以下のコードを `require 'bundler/setup'` の直後に追加してください(早く読み込まれるほど、より多くのものを最適化することができます)。 ```ruby require 'bootsnap' env = ENV['RAILS_ENV'] || "development" Bootsnap.setup(  cache_dir:           'tmp/cache',         # キャッシュファイルを保存する path  development_mode:     env == 'development', # 現在の作業環境、例えば RACK_ENV, RAILS_ENV など。 load_path_cache: true, # キャッシュで LOAD_PATH を最適化する。  autoload_paths_cache: true,                 # キャッシュで ActiveSupport による autoload を行う。  disable_trace:       true,                 # (アルファ) `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`をセットする。  compile_cache_iseq:   true,                 # ISeq キャッシュをコンパイルする  compile_cache_yaml:   true                 # YAML キャッシュをコンパイルする ) ``` **ヒント**: `require 'bootsnap'` を `BootLib::Require.from_gem('bootsnap', 'bootsnap')` で、 [こちらのトリック](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require)を使って置き換えることができます。こうすると、巨大な`$LOAD_PATH`がある場合でも、起動時間を最短化するのに役立ちます。 注意: Bootsnap と [Spring](https://github.com/rails/spring) は別領域の問題を扱うツールです。Bootsnap は個々のソースファイルの読み込みを高速化します。一方で、Spring は起動されたRailsプロセスのコピーを保持して次回の起動時に起動プロセスの一部を完全にスキップします。2つのツールはうまく連携しており、どちらも新しく生成された Rails アプリケーションにデフォルトで含まれています。 ### 環境 Bootsnapのすべての機能はセットアップ時の設定に従って開発、テスト、プロダクション、および他のすべての環境で有効化されます。Shopify では、この gem を問題なくすべての環境で安全に使用しています。 特定の環境で機能を無効にする場合は、必要に応じて適切な ENV 変数または設定を考慮して設定を変更することをおすすめします。 ## 内部動作 Bootsnap は、処理に時間のかかるメソッドの結果をキャッシュすることで最適化しています。これは、大きく分けて2つのカテゴリに分けられます。 * [Path Pre-Scanning](#path-pre-scanning) * `Kernel#require` と `Kernel#load` を `$LOAD_PATH` フルスキャンを行わないように変更します。 * `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` を `ActiveSupport::Dependencies.autoload_paths` のフルスキャンを行わないようにオーバーライドします。 * [Compilation caching](#compilation-caching) * Ruby バイトコードのコンパイル結果をキャッシュするためのメソッド `RubyVM::InstructionSequence.load_iseq` が実装されています。 * `YAML.load_file` を YAML オブジェクトのロード結果を MessagePack でキャッシュするように変更します。 MessagePack でサポートされていないタイプが使われている場合は Marshal が使われます。 ### Path Pre-Scanning _(このライブラリは [bootscale](https://github.com/byroot/bootscale) という別のライブラリを元に開発されました)_ Bootsnap の初期化時、あるいはパス(例えば、`$LOAD_PATH`)の変更時に、`Bootsnap::LoadPathCache` がキャッシュから必要なエントリーのリストを読み込みます。または、必要に応じてフルスキャンを実行し結果をキャッシュします。 その後、たとえば `require 'foo'` を評価する場合, Ruby は `$LOAD_PATH` `['x', 'y', ...]` のすべてのエントリーを繰り返し評価することで `x/foo.rb`, `y/foo.rb` などを探索します。これに対して Bootsnap は、キャッシュされた require 可能なファイルと `$LOAD_PATH` を見ることで、Rubyが最終的に選択するであろうパスで置き換えます。 この動作によって生成された syscall を見ると、最終的な結果は以前なら次のようになります。 ``` open x/foo.rb # (fail) # (imagine this with 500 $LOAD_PATH entries instead of two) open y/foo.rb # (success) close y/foo.rb open y/foo.rb ... ``` これが、次のようになります: ``` open y/foo.rb ... ``` `autoload_paths_cache` オプションが `Bootsnap.setup` に与えられている場合、`ActiveSupport::Dependencies.autoload_paths` をトラバースする方法にはまったく同じ最適化が使用されます。 `*_path_cache` を機能させるオーバーライドを図にすると、次のようになります。 ![Bootsnapの説明図](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png) Bootsnap は、 `$LOAD_PATH` エントリを安定エントリと不安定エントリの2つのカテゴリに分類します。不安定エントリはアプリケーションが起動するたびにスキャンされ、そのキャッシュは30秒間だけ有効になります。安定エントリーに期限切れはありません。コンテンツがスキャンされると、決して変更されないものとみなされます。 安定していると考えられる唯一のディレクトリは、Rubyのインストールプレフィックス (`RbConfig::CONFIG['prefix']`, または `/usr/local/ruby` や `~/.rubies/x.y.z`)下にあるものと、`Gem.path` (たとえば `~/.gem/ruby/x.y.z`) や `Bundler.bundle_path` 下にあるものです。他のすべては不安定エントリと分類されます。 [`Bootsnap::LoadPathCache::Cache`](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/cache.rb) に加えて次の図では、エントリの解決がどのように機能するかを理解するのに役立つかもしれません。経路探索は以下のようになります。 ![パス探索の仕組み](https://cloud.githubusercontent.com/assets/3074765/25388270/670b5652-299b-11e7-87fb-975647f68981.png) また、`LoadError` のスキャンがどれほど重いかに注意を払うことも大切です。もし Ruby が `require 'something'` を評価し、そのファイルが `$LOAD_PATH` にない場合は、それを知るために `2 * $LOAD_PATH.length` のファイルシステムアスセスが必要になります。Bootsnap は、ファイルシステムにまったく触れずに `LoadError` を投げ、この結果をキャッシュします。 ## Compilation Caching *(このコンセプトのより分かりやすい解説は [yomikomu](https://github.com/ko1/yomikomu) をお読み下さい。)* Ruby には複雑な文法が実装されており、構文解析は簡単なオペレーションではありません。1.9以降、Ruby は Ruby ソースを内部のバイトコードに変換した後、Ruby VM によって実行してきました。2.3.0 以降、[RubyはAPIを公開し](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html)、そのバイトコードをキャッシュすることができるようになりました。これにより、同じファイルが複数ロードされた時の、比較的時間のかかる部分をバイパスすることができます。 また、アプリケーションの起動時に YAML ドキュメントの読み込みに多くの時間を費やしていることを発見しました。そして、 MessagePack と Marshal は deserialization にあたって YAML よりもはるかに高速であるということに気付きました。そこで、YAML ドキュメントを、Ruby バイトコードと同じコンパイルキャッシングの最適化を施すことで、高速化しています。Ruby の "バイトコード" フォーマットに相当するものは MessagePack ドキュメント (あるいは、MessagePack をサポートしていないタイプの YAML ドキュメントの場合は、Marshal stream)になります。 これらのコンパイル結果は、入力ファイル(FNV1a-64)のフルパスのハッシュを取って生成されたファイル名で、キャッシュディレクトリに保存されます。 Bootsnap 無しでは、ファイルを `require` するために生成された syscall の順序は次のようになっていました: ``` open /c/foo.rb -> m fstat64 m close m open /c/foo.rb -> o fstat64 o fstat64 o read o read o ... close o ``` しかし Bootsnap では、次のようになります: ``` open /c/foo.rb -> n fstat64 n close n open /c/foo.rb -> n fstat64 n open (cache) -> m read m read m close m close n ``` これは一見劣化していると思われるかもしれませんが、性能に大きな違いがあります。 *(両方のリストの最初の3つの syscalls -- `open`, `fstat64`, `close` -- は本質的に有用ではありません。[このRubyパッチ](https://bugs.ruby-lang.org/issues/13378)は、Boosnap と組み合わせることによって、それらを最適化しています)* Bootsnap は、64バイトのヘッダーとそれに続くキャッシュの内容を含んだキャッシュファイルを書き込みます。ヘッダーは、次のいくつかのフィールドで構成されるキャッシュキーです。 - `version`、Bootsnapにハードコードされる基本的なスキーマのバージョン - `ruby_platform`、`RUBY_PLATFORM`(x86_64-linux-gnuなど)変数とglibcバージョン(Linuxの場合)またはOSバージョン(BSD、macOSの場合は` uname -v`)のハッシュ - `compile_option`、`RubyVM::InstructionSequence.compile_option` の返り値 - `ruby_revision`、コンパイルされたRubyのバージョン - `size`、ソースファイルのサイズ - `mtime`、コンパイル時のソースファイルの最終変更タイムスタンプ - `data_size`、バッファに読み込む必要のあるヘッダーに続くバイト数。 キーが有効な場合、キャッシュがファイルからロードされます。そうでない場合、キャッシュは再生成され、現在のキャッシュを破棄します。 # 最終的なキャッシュ結果 次のファイル構造があるとします。 ``` / ├── a ├── b └── c └── foo.rb ``` そして、このような `$LOAD_PATH` があるとします。 ``` ["/a", "/b", "/c"] ``` Bootsnap なしで `require 'foo'` を呼び出すと、Ruby は次の順序で syscalls を生成します: ``` open /a/foo.rb -> -1 open /b/foo.rb -> -1 open /c/foo.rb -> n close n open /c/foo.rb -> m fstat64 m close m open /c/foo.rb -> o fstat64 o fstat64 o read o read o ... close o ``` しかし Bootsnap では、次のようになります: ``` open /c/foo.rb -> n fstat64 n close n open /c/foo.rb -> n fstat64 n open (cache) -> m read m read m close m close n ``` Bootsnap なしで `require 'nope'` を呼び出すと、次のようになります: ``` open /a/nope.rb -> -1 open /b/nope.rb -> -1 open /c/nope.rb -> -1 open /a/nope.bundle -> -1 open /b/nope.bundle -> -1 open /c/nope.bundle -> -1 ``` ...そして、Bootsnap で `require 'nope'` を呼び出すと、次のようになります... ``` # (nothing!) ``` bootsnap-1.4.6/README.md000066400000000000000000000267071362503221000146340ustar00rootroot00000000000000# Bootsnap [![Build Status](https://travis-ci.org/Shopify/bootsnap.svg?branch=master)](https://travis-ci.org/Shopify/bootsnap) Bootsnap is a library that plugs into Ruby, with optional support for `ActiveSupport` and `YAML`, to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work). #### Performance - [Discourse](https://github.com/discourse/discourse) reports a boot time reduction of approximately 50%, from roughly 6 to 3 seconds on one machine; - One of our smaller internal apps also sees a reduction of 50%, from 3.6 to 1.8 seconds; - The core Shopify platform -- a rather large monolithic application -- boots about 75% faster, dropping from around 25s to 6.5s. * In Shopify core (a large app), about 25% of this gain can be attributed to `compile_cache_*` features; 75% to path caching, and ~1% to `disable_trace`. This is fairly representative. ## Usage This gem works on macOS and Linux. Add `bootsnap` to your `Gemfile`: ```ruby gem 'bootsnap', require: false ``` If you are using Rails, add this to `config/boot.rb` immediately after `require 'bundler/setup'`: ```ruby require 'bootsnap/setup' ``` Note that bootsnap writes to `tmp/cache`, and that directory *must* be writable. Rails will fail to boot if it is not. If this is unacceptable (e.g. you are running in a read-only container and unwilling to mount in a writable tmpdir), you should remove this line or wrap it in a conditional. **Note also that bootsnap will never clean up its own cache: this is left up to you. Depending on your deployment strategy, you may need to periodically purge `tmp/cache/bootsnap*`. If you notice deploys getting progressively slower, this is almost certainly the cause.** It's technically possible to simply specify `gem 'bootsnap', require: 'bootsnap/setup'`, but it's important to load Bootsnap as early as possible to get maximum performance improvement. You can see how this require works [here](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/setup.rb). If you are not using Rails, or if you are but want more control over things, add this to your application setup immediately after `require 'bundler/setup'` (i.e. as early as possible: the sooner this is loaded, the sooner it can start optimizing things) ```ruby require 'bootsnap' env = ENV['RAILS_ENV'] || "development" Bootsnap.setup( cache_dir: 'tmp/cache', # Path to your cache development_mode: env == 'development', # Current working environment, e.g. RACK_ENV, RAILS_ENV, etc load_path_cache: true, # Optimize the LOAD_PATH with a cache autoload_paths_cache: true, # Optimize ActiveSupport autoloads with cache disable_trace: true, # Set `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }` compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting. compile_cache_yaml: true # Compile YAML into a cache ) ``` **Note that `disable_trace` will break debuggers and tracing.** **Protip:** You can replace `require 'bootsnap'` with `BootLib::Require.from_gem('bootsnap', 'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This will help optimize boot time further if you have an extremely large `$LOAD_PATH`. Note: Bootsnap and [Spring](https://github.com/rails/spring) are orthogonal tools. While Bootsnap speeds up the loading of individual source files, Spring keeps a copy of a pre-booted Rails process on hand to completely skip parts of the boot process the next time it's needed. The two tools work well together, and are both included in a newly-generated Rails applications by default. ### Environments All Bootsnap features are enabled in development, test, production, and all other environments according to the configuration in the setup. At Shopify, we use this gem safely in all environments without issue. If you would like to disable any feature for a certain environment, we suggest changing the configuration to take into account the appropriate ENV var or configuration according to your needs. ## How does this work? Bootsnap optimizes methods to cache results of expensive computations, and can be grouped into two broad categories: * [Path Pre-Scanning](#path-pre-scanning) * `Kernel#require` and `Kernel#load` are modified to eliminate `$LOAD_PATH` scans. * `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` are overridden to eliminate scans of `ActiveSupport::Dependencies.autoload_paths`. * [Compilation caching](#compilation-caching) * `RubyVM::InstructionSequence.load_iseq` is implemented to cache the result of ruby bytecode compilation. * `YAML.load_file` is modified to cache the result of loading a YAML object in MessagePack format (or Marshal, if the message uses types unsupported by MessagePack). ### Path Pre-Scanning *(This work is a minor evolution of [bootscale](https://github.com/byroot/bootscale)).* Upon initialization of bootsnap or modification of the path (e.g. `$LOAD_PATH`), `Bootsnap::LoadPathCache` will fetch a list of requirable entries from a cache, or, if necessary, perform a full scan and cache the result. Later, when we run (e.g.) `require 'foo'`, ruby *would* iterate through every item on our `$LOAD_PATH` `['x', 'y', ...]`, looking for `x/foo.rb`, `y/foo.rb`, and so on. Bootsnap instead looks at all the cached requirables for each `$LOAD_PATH` entry and substitutes the full expanded path of the match ruby would have eventually chosen. If you look at the syscalls generated by this behaviour, the net effect is that what would previously look like this: ``` open x/foo.rb # (fail) # (imagine this with 500 $LOAD_PATH entries instead of two) open y/foo.rb # (success) close y/foo.rb open y/foo.rb ... ``` becomes this: ``` open y/foo.rb ... ``` Exactly the same strategy is employed for methods that traverse `ActiveSupport::Dependencies.autoload_paths` if the `autoload_paths_cache` option is given to `Bootsnap.setup`. The following diagram flowcharts the overrides that make the `*_path_cache` features work. ![Flowchart explaining Bootsnap](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png) Bootsnap classifies path entries into two categories: stable and volatile. Volatile entries are scanned each time the application boots, and their caches are only valid for 30 seconds. Stable entries do not expire -- once their contents has been scanned, it is assumed to never change. The only directories considered "stable" are things under the Ruby install prefix (`RbConfig::CONFIG['prefix']`, e.g. `/usr/local/ruby` or `~/.rubies/x.y.z`), and things under the `Gem.path` (e.g. `~/.gem/ruby/x.y.z`) or `Bundler.bundle_path`. Everything else is considered "volatile". In addition to the [`Bootsnap::LoadPathCache::Cache` source](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/cache.rb), this diagram may help clarify how entry resolution works: ![How path searching works](https://cloud.githubusercontent.com/assets/3074765/25388270/670b5652-299b-11e7-87fb-975647f68981.png) It's also important to note how expensive `LoadError`s can be. If ruby invokes `require 'something'`, but that file isn't on `$LOAD_PATH`, it takes `2 * $LOAD_PATH.length` filesystem accesses to determine that. Bootsnap caches this result too, raising a `LoadError` without touching the filesystem at all. ### Compilation Caching *(A more readable implementation of this concept can be found in [yomikomu](https://github.com/ko1/yomikomu)).* Ruby has complex grammar and parsing it is not a particularly cheap operation. Since 1.9, Ruby has translated ruby source to an internal bytecode format, which is then executed by the Ruby VM. Since 2.3.0, Ruby [exposes an API](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html) that allows caching that bytecode. This allows us to bypass the relatively-expensive compilation step on subsequent loads of the same file. We also noticed that we spend a lot of time loading YAML documents during our application boot, and that MessagePack and Marshal are *much* faster at deserialization than YAML, even with a fast implementation. We use the same strategy of compilation caching for YAML documents, with the equivalent of Ruby's "bytecode" format being a MessagePack document (or, in the case of YAML documents with types unsupported by MessagePack, a Marshal stream). These compilation results are stored in a cache directory, with filenames generated by taking a hash of the full expanded path of the input file (FNV1a-64). Whereas before, the sequence of syscalls generated to `require` a file would look like: ``` open /c/foo.rb -> m fstat64 m close m open /c/foo.rb -> o fstat64 o fstat64 o read o read o ... close o ``` With bootsnap, we get: ``` open /c/foo.rb -> n fstat64 n close n open /c/foo.rb -> n fstat64 n open (cache) -> m read m read m close m close n ``` This may look worse at a glance, but underlies a large performance difference. *(The first three syscalls in both listings -- `open`, `fstat64`, `close` -- are not inherently useful. [This ruby patch](https://bugs.ruby-lang.org/issues/13378) optimizes them out when coupled with bootsnap.)* Bootsnap writes a cache file containing a 64 byte header followed by the cache contents. The header is a cache key including several fields: * `version`, hardcoded in bootsnap. Essentially a schema version; * `ruby_platform`, A hash of `RUBY_PLATFORM` (e.g. x86_64-linux-gnu) variable and glibc version (on Linux) or OS version (`uname -v` on BSD, macOS) * `compile_option`, which changes with `RubyVM::InstructionSequence.compile_option` does; * `ruby_revision`, the version of Ruby this was compiled with; * `size`, the size of the source file; * `mtime`, the last-modification timestamp of the source file when it was compiled; and * `data_size`, the number of bytes following the header, which we need to read it into a buffer. If the key is valid, the result is loaded from the value. Otherwise, it is regenerated and clobbers the current cache. ### Putting it all together Imagine we have this file structure: ``` / ├── a ├── b └── c └── foo.rb ``` And this `$LOAD_PATH`: ``` ["/a", "/b", "/c"] ``` When we call `require 'foo'` without bootsnap, Ruby would generate this sequence of syscalls: ``` open /a/foo.rb -> -1 open /b/foo.rb -> -1 open /c/foo.rb -> n close n open /c/foo.rb -> m fstat64 m close m open /c/foo.rb -> o fstat64 o fstat64 o read o read o ... close o ``` With bootsnap, we get: ``` open /c/foo.rb -> n fstat64 n close n open /c/foo.rb -> n fstat64 n open (cache) -> m read m read m close m close n ``` If we call `require 'nope'` without bootsnap, we get: ``` open /a/nope.rb -> -1 open /b/nope.rb -> -1 open /c/nope.rb -> -1 open /a/nope.bundle -> -1 open /b/nope.bundle -> -1 open /c/nope.bundle -> -1 ``` ...and if we call `require 'nope'` *with* bootsnap, we get... ``` # (nothing!) ``` ## When not to use Bootsnap *Alternative engines*: Bootsnap is pretty reliant on MRI features, and parts are disabled entirely on alternative ruby engines. *Non-local filesystems*: Bootsnap depends on `tmp/cache` (or whatever you set its cache directory to) being on a relatively fast filesystem. If you put it on a network mount, bootsnap is very likely to slow your application down quite a lot. bootsnap-1.4.6/Rakefile000066400000000000000000000004751362503221000150140ustar00rootroot00000000000000# frozen_string_literal: true require('rake/extensiontask') require('bundler/gem_tasks') gemspec = Gem::Specification.load('bootsnap.gemspec') Rake::ExtensionTask.new do |ext| ext.name = 'bootsnap' ext.ext_dir = 'ext/bootsnap' ext.lib_dir = 'lib/bootsnap' ext.gem_spec = gemspec end task(default: :compile) bootsnap-1.4.6/bin/000077500000000000000000000000001362503221000141115ustar00rootroot00000000000000bootsnap-1.4.6/bin/ci000077500000000000000000000002201362503221000144240ustar00rootroot00000000000000#!/bin/bash set -euxo pipefail if [[ "${MINIMAL_SUPPORT-0}" -eq 1 ]]; then exec bin/test-minimal-support else rake exec bin/testunit fi bootsnap-1.4.6/bin/console000077500000000000000000000005701362503221000155030ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require("bundler/setup") require("bootsnap") # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require("irb") IRB.start(__FILE__) bootsnap-1.4.6/bin/setup000077500000000000000000000002031362503221000151720ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here bootsnap-1.4.6/bin/test-minimal-support000077500000000000000000000002131362503221000201500ustar00rootroot00000000000000#!/bin/bash set -euxo pipefail cd test/minimal_support bundle BOOTSNAP_CACHE_DIR=/tmp bundle exec ruby -w -I ../../lib bootsnap_setup.rb bootsnap-1.4.6/bin/testunit000077500000000000000000000003201362503221000157110ustar00rootroot00000000000000#!/bin/bash if [[ $# -eq 0 ]]; then exec ruby -I"test" -w -e 'Dir.glob("./test/**/*_test.rb").each { |f| require f }' -- "$@" else path=$1 exec ruby -I"test" -w -e "require '${path#test/}'" -- "$@" fi bootsnap-1.4.6/bootsnap.gemspec000066400000000000000000000027061362503221000165400ustar00rootroot00000000000000# coding: utf-8 # frozen_string_literal: true lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require('bootsnap/version') Gem::Specification.new do |spec| spec.name = "bootsnap" spec.version = Bootsnap::VERSION spec.authors = ["Burke Libbey"] spec.email = ["burke.libbey@shopify.com"] spec.license = "MIT" spec.summary = "Boot large ruby/rails apps faster" spec.description = spec.summary spec.homepage = "https://github.com/Shopify/bootsnap" spec.metadata = { 'bug_tracker_uri' => 'https://github.com/Shopify/bootsnap/issues', 'changelog_uri' => 'https://github.com/Shopify/bootsnap/blob/master/CHANGELOG.md', 'source_code_uri' => 'https://github.com/Shopify/bootsnap', } spec.files = %x(git ls-files -z).split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end spec.require_paths = %w(lib) spec.required_ruby_version = '>= 2.3.0' if RUBY_PLATFORM =~ /java/ spec.platform = 'java' else spec.platform = Gem::Platform::RUBY spec.extensions = ['ext/bootsnap/extconf.rb'] end spec.add_development_dependency("bundler") spec.add_development_dependency('rake', '~> 10.0') spec.add_development_dependency('rake-compiler', '~> 0') spec.add_development_dependency("minitest", "~> 5.0") spec.add_development_dependency("mocha", "~> 1.2") spec.add_runtime_dependency("msgpack", "~> 1.0") end bootsnap-1.4.6/dev.yml000066400000000000000000000002461362503221000146440ustar00rootroot00000000000000env: BOOTSNAP_PEDANTIC: '1' up: - ruby: 2.6.0 - bundler commands: build: rake compile test: 'rake compile && exec bin/testunit' style: 'exec rubocop -D' bootsnap-1.4.6/ext/000077500000000000000000000000001362503221000141415ustar00rootroot00000000000000bootsnap-1.4.6/ext/bootsnap/000077500000000000000000000000001362503221000157665ustar00rootroot00000000000000bootsnap-1.4.6/ext/bootsnap/bootsnap.c000066400000000000000000000616621362503221000177720ustar00rootroot00000000000000/* * Suggested reading order: * 1. Skim Init_bootsnap * 2. Skim bs_fetch * 3. The rest of everything * * Init_bootsnap sets up the ruby objects and binds bs_fetch to * Bootsnap::CompileCache::Native.fetch. * * bs_fetch is the ultimate caller for for just about every other function in * here. */ #include "bootsnap.h" #include "ruby.h" #include #include #include #include #include #ifndef _WIN32 #include #endif #ifdef __GLIBC__ #include #endif /* 1000 is an arbitrary limit; FNV64 plus some slashes brings the cap down to * 981 for the cache dir */ #define MAX_CACHEPATH_SIZE 1000 #define MAX_CACHEDIR_SIZE 981 #define KEY_SIZE 64 /* * An instance of this key is written as the first 64 bytes of each cache file. * The mtime and size members track whether the file contents have changed, and * the version, ruby_platform, compile_option, and ruby_revision members track * changes to the environment that could invalidate compile results without * file contents having changed. The data_size member is not truly part of the * "key". Really, this could be called a "header" with the first six members * being an embedded "key" struct and an additional data_size member. * * The data_size indicates the remaining number of bytes in the cache file * after the header (the size of the cached artifact). * * After data_size, the struct is padded to 64 bytes. */ struct bs_cache_key { uint32_t version; uint32_t ruby_platform; uint32_t compile_option; uint32_t ruby_revision; uint64_t size; uint64_t mtime; uint64_t data_size; /* not used for equality */ uint8_t pad[24]; } __attribute__((packed)); /* * If the struct padding isn't correct to pad the key to 64 bytes, refuse to * compile. */ #define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__) #define STATIC_ASSERT2(X,L) STATIC_ASSERT3(X,L) #define STATIC_ASSERT3(X,L) STATIC_ASSERT_MSG(X,at_line_##L) #define STATIC_ASSERT_MSG(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1] STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE); /* Effectively a schema version. Bumping invalidates all previous caches */ static const uint32_t current_version = 2; /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a * new OS ABI, etc. */ static uint32_t current_ruby_platform; /* Invalidates cache when switching ruby versions */ static uint32_t current_ruby_revision; /* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */ static uint32_t current_compile_option_crc32 = 0; /* Current umask */ static mode_t current_umask; /* Bootsnap::CompileCache::{Native, Uncompilable} */ static VALUE rb_mBootsnap; static VALUE rb_mBootsnap_CompileCache; static VALUE rb_mBootsnap_CompileCache_Native; static VALUE rb_eBootsnap_CompileCache_Uncompilable; static ID uncompilable; /* Functions exposed as module functions on Bootsnap::CompileCache::Native */ static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v); static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler); /* Helpers */ static uint64_t fnv1a_64(const char *str); static void bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE]); static int bs_read_key(int fd, struct bs_cache_key * key); static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2); static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler); static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance); static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, const char ** errno_provenance); static uint32_t get_ruby_revision(void); static uint32_t get_ruby_platform(void); /* * Helper functions to call ruby methods on handler object without crashing on * exception. */ static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data); static VALUE prot_storage_to_output(VALUE arg); static VALUE prot_input_to_output(VALUE arg); static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag); static VALUE prot_input_to_storage(VALUE arg); static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data); struct s2o_data; struct i2o_data; struct i2s_data; /* https://bugs.ruby-lang.org/issues/13667 */ extern VALUE rb_get_coverages(void); static VALUE bs_rb_coverage_running(VALUE self) { VALUE cov = rb_get_coverages(); return RTEST(cov) ? Qtrue : Qfalse; } /* * Ruby C extensions are initialized by calling Init_. * * This sets up the module hierarchy and attaches functions as methods. * * We also populate some semi-static information about the current OS and so on. */ void Init_bootsnap(void) { rb_mBootsnap = rb_define_module("Bootsnap"); rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache"); rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native"); rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError); current_ruby_revision = get_ruby_revision(); current_ruby_platform = get_ruby_platform(); uncompilable = rb_intern("__bootsnap_uncompilable__"); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 3); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1); current_umask = umask(0777); umask(current_umask); } /* * Bootsnap's ruby code registers a hook that notifies us via this function * when compile_option changes. These changes invalidate all existing caches. * * Note that on 32-bit platforms, a CRC32 can't be represented in a Fixnum, but * can be represented by a uint. */ static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v) { if (!RB_TYPE_P(crc32_v, T_BIGNUM) && !RB_TYPE_P(crc32_v, T_FIXNUM)) { Check_Type(crc32_v, T_FIXNUM); } current_compile_option_crc32 = NUM2UINT(crc32_v); return Qnil; } /* * We use FNV1a-64 to derive cache paths. The choice is somewhat arbitrary but * it has several nice properties: * * - Tiny implementation * - No external dependency * - Solid performance * - Solid randomness * - 32 bits doesn't feel collision-resistant enough; 64 is nice. */ static uint64_t fnv1a_64_iter(uint64_t h, const char *str) { unsigned char *s = (unsigned char *)str; while (*s) { h ^= (uint64_t)*s++; h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40); } return h; } static uint64_t fnv1a_64(const char *str) { uint64_t h = (uint64_t)0xcbf29ce484222325ULL; return fnv1a_64_iter(h, str); } /* * Ruby's revision may be Integer or String. CRuby 2.7 or later uses * Git commit ID as revision. It's String. */ static uint32_t get_ruby_revision(void) { VALUE ruby_revision; ruby_revision = rb_const_get(rb_cObject, rb_intern("RUBY_REVISION")); if (RB_TYPE_P(ruby_revision, RUBY_T_FIXNUM)) { return FIX2INT(ruby_revision); } else { uint64_t hash; hash = fnv1a_64(StringValueCStr(ruby_revision)); return (uint32_t)(hash >> 32); } } /* * When ruby's version doesn't change, but it's recompiled on a different OS * (or OS version), we need to invalidate the cache. * * We actually factor in some extra information here, to be extra confident * that we don't try to re-use caches that will not be compatible, by factoring * in utsname.version. */ static uint32_t get_ruby_platform(void) { uint64_t hash; VALUE ruby_platform; ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM")); hash = fnv1a_64(RSTRING_PTR(ruby_platform)); #ifdef _WIN32 return (uint32_t)(hash >> 32) ^ (uint32_t)GetVersion(); #elif defined(__GLIBC__) hash = fnv1a_64_iter(hash, gnu_get_libc_version()); return (uint32_t)(hash >> 32); #else struct utsname utsname; /* Not worth crashing if this fails; lose extra cache invalidation potential */ if (uname(&utsname) >= 0) { hash = fnv1a_64_iter(hash, utsname.version); } return (uint32_t)(hash >> 32); #endif } /* * Given a cache root directory and the full path to a file being cached, * generate a path under the cache directory at which the cached artifact will * be stored. * * The path will look something like: /12/34567890abcdef */ static void bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE]) { uint64_t hash = fnv1a_64(path); uint8_t first_byte = (hash >> (64 - 8)); uint64_t remainder = hash & 0x00ffffffffffffff; sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder); } /* * Test whether a newly-generated cache key based on the file as it exists on * disk matches the one that was generated when the file was cached (or really * compare any two keys). * * The data_size member is not compared, as it serves more of a "header" * function. */ static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2) { return ( k1->version == k2->version && k1->ruby_platform == k2->ruby_platform && k1->compile_option == k2->compile_option && k1->ruby_revision == k2->ruby_revision && k1->size == k2->size && k1->mtime == k2->mtime ); } /* * Entrypoint for Bootsnap::CompileCache::Native.fetch. The real work is done * in bs_fetch; this function just performs some basic typechecks and * conversions on the ruby VALUE arguments before passing them along. */ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler) { FilePathValue(path_v); Check_Type(cachedir_v, T_STRING); Check_Type(path_v, T_STRING); if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) { rb_raise(rb_eArgError, "cachedir too long"); } char * cachedir = RSTRING_PTR(cachedir_v); char * path = RSTRING_PTR(path_v); char cache_path[MAX_CACHEPATH_SIZE]; /* generate cache path to cache_path */ bs_cache_path(cachedir, path, &cache_path); return bs_fetch(path, path_v, cache_path, handler); } /* * Open the file we want to load/cache and generate a cache key for it if it * was loaded. */ static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance) { struct stat statbuf; int fd; fd = open(path, O_RDONLY); if (fd < 0) { *errno_provenance = "bs_fetch:open_current_file:open"; return fd; } #ifdef _WIN32 setmode(fd, O_BINARY); #endif if (fstat(fd, &statbuf) < 0) { *errno_provenance = "bs_fetch:open_current_file:fstat"; close(fd); return -1; } key->version = current_version; key->ruby_platform = current_ruby_platform; key->compile_option = current_compile_option_crc32; key->ruby_revision = current_ruby_revision; key->size = (uint64_t)statbuf.st_size; key->mtime = (uint64_t)statbuf.st_mtime; return fd; } #define ERROR_WITH_ERRNO -1 #define CACHE_MISSING_OR_INVALID -2 /* * Read the cache key from the given fd, which must have position 0 (e.g. * freshly opened file). * * Possible return values: * - 0 (OK, key was loaded) * - CACHE_MISSING_OR_INVALID (-2) * - ERROR_WITH_ERRNO (-1, errno is set) */ static int bs_read_key(int fd, struct bs_cache_key * key) { ssize_t nread = read(fd, key, KEY_SIZE); if (nread < 0) return ERROR_WITH_ERRNO; if (nread < KEY_SIZE) return CACHE_MISSING_OR_INVALID; return 0; } /* * Open the cache file at a given path, if it exists, and read its key into the * struct. * * Possible return values: * - 0 (OK, key was loaded) * - CACHE_MISSING_OR_INVALID (-2) * - ERROR_WITH_ERRNO (-1, errno is set) */ static int open_cache_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance) { int fd, res; fd = open(path, O_RDONLY); if (fd < 0) { *errno_provenance = "bs_fetch:open_cache_file:open"; if (errno == ENOENT) return CACHE_MISSING_OR_INVALID; return ERROR_WITH_ERRNO; } #ifdef _WIN32 setmode(fd, O_BINARY); #endif res = bs_read_key(fd, key); if (res < 0) { *errno_provenance = "bs_fetch:open_cache_file:read"; close(fd); return res; } return fd; } /* * The cache file is laid out like: * 0...64 : bs_cache_key * 64..-1 : cached artifact * * This function takes a file descriptor whose position is pre-set to 64, and * the data_size (corresponding to the remaining number of bytes) listed in the * cache header. * * We load the text from this file into a buffer, and pass it to the ruby-land * handler with exception handling via the exception_tag param. * * Data is returned via the output_data parameter, which, if there's no error * or exception, will be the final data returnable to the user. */ static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, const char ** errno_provenance) { char * data = NULL; ssize_t nread; int ret; VALUE storage_data; if (data_size > 100000000000) { *errno_provenance = "bs_fetch:fetch_cached_data:datasize"; errno = EINVAL; /* because wtf? */ ret = -1; goto done; } data = ALLOC_N(char, data_size); nread = read(fd, data, data_size); if (nread < 0) { *errno_provenance = "bs_fetch:fetch_cached_data:read"; ret = -1; goto done; } if (nread != data_size) { ret = CACHE_MISSING_OR_INVALID; goto done; } storage_data = rb_str_new_static(data, data_size); *exception_tag = bs_storage_to_output(handler, storage_data, output_data); ret = 0; done: if (data != NULL) xfree(data); return ret; } /* * Like mkdir -p, this recursively creates directory parents of a file. e.g. * given /a/b/c, creates /a and /a/b. */ static int mkpath(char * file_path, mode_t mode) { /* It would likely be more efficient to count back until we * find a component that *does* exist, but this will only run * at most 256 times, so it seems not worthwhile to change. */ char * p; for (p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) { *p = '\0'; #ifdef _WIN32 if (mkdir(file_path) == -1) { #else if (mkdir(file_path, mode) == -1) { #endif if (errno != EEXIST) { *p = '/'; return -1; } } *p = '/'; } return 0; } /* * Write a cache header/key and a compiled artifact to a given cache path by * writing to a tmpfile and then renaming the tmpfile over top of the final * path. */ static int atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, const char ** errno_provenance) { char template[MAX_CACHEPATH_SIZE + 20]; char * tmp_path; int fd, ret; ssize_t nwrite; tmp_path = strncpy(template, path, MAX_CACHEPATH_SIZE); strcat(tmp_path, ".tmp.XXXXXX"); // mkstemp modifies the template to be the actual created path fd = mkstemp(tmp_path); if (fd < 0) { if (mkpath(tmp_path, 0775) < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:mkpath"; return -1; } fd = open(tmp_path, O_WRONLY | O_CREAT, 0664); if (fd < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:open"; return -1; } } #ifdef _WIN32 setmode(fd, O_BINARY); #endif key->data_size = RSTRING_LEN(data); nwrite = write(fd, key, KEY_SIZE); if (nwrite < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:write"; return -1; } if (nwrite != KEY_SIZE) { *errno_provenance = "bs_fetch:atomic_write_cache_file:keysize"; errno = EIO; /* Lies but whatever */ return -1; } nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data)); if (nwrite < 0) return -1; if (nwrite != RSTRING_LEN(data)) { *errno_provenance = "bs_fetch:atomic_write_cache_file:writelength"; errno = EIO; /* Lies but whatever */ return -1; } close(fd); ret = rename(tmp_path, path); if (ret < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:rename"; return -1; } ret = chmod(path, 0664 & ~current_umask); if (ret < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod"; } return ret; } /* Read contents from an fd, whose contents are asserted to be +size+ bytes * long, into a buffer */ static ssize_t bs_read_contents(int fd, size_t size, char ** contents, const char ** errno_provenance) { ssize_t nread; *contents = ALLOC_N(char, size); nread = read(fd, *contents, size); if (nread < 0) { *errno_provenance = "bs_fetch:bs_read_contents:read"; } return nread; } /* * This is the meat of the extension. bs_fetch is * Bootsnap::CompileCache::Native.fetch. * * There are three "formats" in use here: * 1. "input" format, which is what we load from the source file; * 2. "storage" format, which we write to the cache; * 3. "output" format, which is what we return. * * E.g., For ISeq compilation: * input: ruby source, as text * storage: binary string (RubyVM::InstructionSequence#to_binary) * output: Instance of RubyVM::InstructionSequence * * And for YAML: * input: yaml as text * storage: MessagePack or Marshal text * output: ruby object, loaded from yaml/messagepack/marshal * * A handler passed in must support three messages: * * storage_to_output(S) -> O * * input_to_output(I) -> O * * input_to_storage(I) -> S * (input_to_storage may raise Bootsnap::CompileCache::Uncompilable, which * will prevent caching and cause output to be generated with * input_to_output) * * The semantics of this function are basically: * * return storage_to_output(cache[path]) if cache[path] * storage = input_to_storage(input) * cache[path] = storage * return storage_to_output(storage) * * Or expanded a bit: * * - Check if the cache file exists and is up to date. * - If it is, load this data to storage_data. * - return storage_to_output(storage_data) * - Read the file to input_data * - Generate storage_data using input_to_storage(input_data) * - Write storage_data data, with a cache key, to the cache file. * - Return storage_to_output(storage_data) */ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler) { struct bs_cache_key cached_key, current_key; char * contents = NULL; int cache_fd = -1, current_fd = -1; int res, valid_cache = 0, exception_tag = 0; const char * errno_provenance = NULL; VALUE input_data; /* data read from source file, e.g. YAML or ruby source */ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */ VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */ VALUE exception; /* ruby exception object to raise instead of returning */ /* Open the source file and generate a cache key for it */ current_fd = open_current_file(path, ¤t_key, &errno_provenance); if (current_fd < 0) goto fail_errno; /* Open the cache key if it exists, and read its cache key in */ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance); if (cache_fd == CACHE_MISSING_OR_INVALID) { /* This is ok: valid_cache remains false, we re-populate it. */ } else if (cache_fd < 0) { goto fail_errno; } else { /* True if the cache existed and no invalidating changes have occurred since * it was generated. */ valid_cache = cache_key_equal(¤t_key, &cached_key); } if (valid_cache) { /* Fetch the cache data and return it if we're able to load it successfully */ res = fetch_cached_data( cache_fd, (ssize_t)cached_key.data_size, handler, &output_data, &exception_tag, &errno_provenance ); if (exception_tag != 0) goto raise; else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0; else if (res == ERROR_WITH_ERRNO) goto fail_errno; else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */ } close(cache_fd); cache_fd = -1; /* Cache is stale, invalid, or missing. Regenerate and write it out. */ /* Read the contents of the source file into a buffer */ if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno; input_data = rb_str_new_static(contents, current_key.size); /* Try to compile the input_data using input_to_storage(input_data) */ exception_tag = bs_input_to_storage(handler, input_data, path_v, &storage_data); if (exception_tag != 0) goto raise; /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try * to cache anything; just return input_to_output(input_data) */ if (storage_data == uncompilable) { bs_input_to_output(handler, input_data, &output_data, &exception_tag); if (exception_tag != 0) goto raise; goto succeed; } /* If storage_data isn't a string, we can't cache it */ if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data; /* Write the cache key and storage_data to the cache directory */ res = atomic_write_cache_file(cache_path, ¤t_key, storage_data, &errno_provenance); if (res < 0) goto fail_errno; /* Having written the cache, now convert storage_data to output_data */ exception_tag = bs_storage_to_output(handler, storage_data, &output_data); if (exception_tag != 0) goto raise; /* If output_data is nil, delete the cache entry and generate the output * using input_to_output */ if (NIL_P(output_data)) { if (unlink(cache_path) < 0) { errno_provenance = "bs_fetch:unlink"; goto fail_errno; } bs_input_to_output(handler, input_data, &output_data, &exception_tag); if (exception_tag != 0) goto raise; } goto succeed; /* output_data is now the correct return. */ #define CLEANUP \ if (contents != NULL) xfree(contents); \ if (current_fd >= 0) close(current_fd); \ if (cache_fd >= 0) close(cache_fd); succeed: CLEANUP; return output_data; fail_errno: CLEANUP; exception = rb_syserr_new(errno, errno_provenance); rb_exc_raise(exception); __builtin_unreachable(); raise: CLEANUP; rb_jump_tag(exception_tag); __builtin_unreachable(); invalid_type_storage_data: CLEANUP; Check_Type(storage_data, T_STRING); __builtin_unreachable(); #undef CLEANUP } /*****************************************************************************/ /********************* Handler Wrappers **************************************/ /***************************************************************************** * Everything after this point in the file is just wrappers to deal with ruby's * clunky method of handling exceptions from ruby methods invoked from C: * * In order to call a ruby method from C, while protecting against crashing in * the event of an exception, we must call the method with rb_protect(). * * rb_protect takes a C function and precisely one argument; however, we want * to pass multiple arguments, so we must create structs to wrap them up. * * These functions return an exception_tag, which, if non-zero, indicates an * exception that should be jumped to with rb_jump_tag after cleaning up * allocated resources. */ struct s2o_data { VALUE handler; VALUE storage_data; }; struct i2o_data { VALUE handler; VALUE input_data; }; struct i2s_data { VALUE handler; VALUE input_data; VALUE pathval; }; static VALUE prot_storage_to_output(VALUE arg) { struct s2o_data * data = (struct s2o_data *)arg; return rb_funcall(data->handler, rb_intern("storage_to_output"), 1, data->storage_data); } static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data) { int state; struct s2o_data s2o_data = { .handler = handler, .storage_data = storage_data, }; *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state); return state; } static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag) { struct i2o_data i2o_data = { .handler = handler, .input_data = input_data, }; *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag); } static VALUE prot_input_to_output(VALUE arg) { struct i2o_data * data = (struct i2o_data *)arg; return rb_funcall(data->handler, rb_intern("input_to_output"), 1, data->input_data); } static VALUE try_input_to_storage(VALUE arg) { struct i2s_data * data = (struct i2s_data *)arg; return rb_funcall(data->handler, rb_intern("input_to_storage"), 2, data->input_data, data->pathval); } static VALUE rescue_input_to_storage(VALUE arg) { return uncompilable; } static VALUE prot_input_to_storage(VALUE arg) { struct i2s_data * data = (struct i2s_data *)arg; return rb_rescue2( try_input_to_storage, (VALUE)data, rescue_input_to_storage, Qnil, rb_eBootsnap_CompileCache_Uncompilable, 0); } static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data) { int state; struct i2s_data i2s_data = { .handler = handler, .input_data = input_data, .pathval = pathval, }; *storage_data = rb_protect(prot_input_to_storage, (VALUE)&i2s_data, &state); return state; } bootsnap-1.4.6/ext/bootsnap/bootsnap.h000066400000000000000000000001401362503221000177570ustar00rootroot00000000000000#ifndef BOOTSNAP_H #define BOOTSNAP_H 1 /* doesn't expose anything */ #endif /* BOOTSNAP_H */ bootsnap-1.4.6/ext/bootsnap/extconf.rb000066400000000000000000000011371362503221000177630ustar00rootroot00000000000000# frozen_string_literal: true require("mkmf") $CFLAGS << ' -O3 ' $CFLAGS << ' -std=c99' # ruby.h has some -Wpedantic fails in some cases # (e.g. https://github.com/Shopify/bootsnap/issues/15) unless ['0', '', nil].include?(ENV['BOOTSNAP_PEDANTIC']) $CFLAGS << ' -Wall' $CFLAGS << ' -Werror' $CFLAGS << ' -Wextra' $CFLAGS << ' -Wpedantic' $CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is. $CFLAGS << ' -Wno-keyword-macro' # hiding return $CFLAGS << ' -Wno-gcc-compat' # ruby.h 2.6.0 on macos 10.14, dunno end create_makefile("bootsnap/bootsnap") bootsnap-1.4.6/lib/000077500000000000000000000000001362503221000141075ustar00rootroot00000000000000bootsnap-1.4.6/lib/bootsnap.rb000066400000000000000000000026561362503221000162720ustar00rootroot00000000000000# frozen_string_literal: true require_relative('bootsnap/version') require_relative('bootsnap/bundler') require_relative('bootsnap/load_path_cache') require_relative('bootsnap/compile_cache') module Bootsnap InvalidConfiguration = Class.new(StandardError) def self.setup( cache_dir:, development_mode: true, load_path_cache: true, autoload_paths_cache: true, disable_trace: false, compile_cache_iseq: true, compile_cache_yaml: true ) if autoload_paths_cache && !load_path_cache raise(InvalidConfiguration, "feature 'autoload_paths_cache' depends on feature 'load_path_cache'") end setup_disable_trace if disable_trace Bootsnap::LoadPathCache.setup( cache_path: cache_dir + '/bootsnap-load-path-cache', development_mode: development_mode, active_support: autoload_paths_cache ) if load_path_cache Bootsnap::CompileCache.setup( cache_dir: cache_dir + '/bootsnap-compile-cache', iseq: compile_cache_iseq, yaml: compile_cache_yaml ) end def self.setup_disable_trace if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0') warn( "from #{caller_locations(1, 1)[0]}: The 'disable_trace' method is not allowed with this Ruby version. " \ "current: #{RUBY_VERSION}, allowed version: < 2.5.0", ) else RubyVM::InstructionSequence.compile_option = { trace_instruction: false } end end end bootsnap-1.4.6/lib/bootsnap/000077500000000000000000000000001362503221000157345ustar00rootroot00000000000000bootsnap-1.4.6/lib/bootsnap/bundler.rb000066400000000000000000000004321362503221000177130ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap extend(self) def bundler? return false unless defined?(::Bundler) # Bundler environment variable %w(BUNDLE_BIN_PATH BUNDLE_GEMFILE).each do |current| return true if ENV.key?(current) end false end end bootsnap-1.4.6/lib/bootsnap/compile_cache.rb000066400000000000000000000025121362503221000210340ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module CompileCache Error = Class.new(StandardError) PermissionError = Class.new(Error) def self.setup(cache_dir:, iseq:, yaml:) if iseq if supported? require_relative('compile_cache/iseq') Bootsnap::CompileCache::ISeq.install!(cache_dir) elsif $VERBOSE warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby") end end if yaml if supported? require_relative('compile_cache/yaml') Bootsnap::CompileCache::YAML.install!(cache_dir) elsif $VERBOSE warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby") end end end def self.permission_error(path) cpath = Bootsnap::CompileCache::ISeq.cache_dir raise( PermissionError, "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \ "(or, less likely, doesn't have permission to read '#{path}')", ) end def self.supported? # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0 RUBY_ENGINE == 'ruby' && RUBY_PLATFORM =~ /darwin|linux|bsd/ && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0") end end end bootsnap-1.4.6/lib/bootsnap/compile_cache/000077500000000000000000000000001362503221000205075ustar00rootroot00000000000000bootsnap-1.4.6/lib/bootsnap/compile_cache/iseq.rb000066400000000000000000000040601362503221000217750ustar00rootroot00000000000000# frozen_string_literal: true require('bootsnap/bootsnap') require('zlib') module Bootsnap module CompileCache module ISeq class << self attr_accessor(:cache_dir) end def self.input_to_storage(_, path) RubyVM::InstructionSequence.compile_file(path).to_binary rescue SyntaxError raise(Uncompilable, 'syntax error') end def self.storage_to_output(binary) RubyVM::InstructionSequence.load_from_binary(binary) rescue RuntimeError => e if e.message == 'broken binary format' STDERR.puts("[Bootsnap::CompileCache] warning: rejecting broken binary") nil else raise end end def self.input_to_output(_) nil # ruby handles this end module InstructionSequenceMixin def load_iseq(path) # Having coverage enabled prevents iseq dumping/loading. return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running? Bootsnap::CompileCache::Native.fetch( Bootsnap::CompileCache::ISeq.cache_dir, path.to_s, Bootsnap::CompileCache::ISeq ) rescue Errno::EACCES Bootsnap::CompileCache.permission_error(path) rescue RuntimeError => e if e.message =~ /unmatched platform/ puts("unmatched platform for file #{path}") end raise end def compile_option=(hash) super(hash) Bootsnap::CompileCache::ISeq.compile_option_updated end end def self.compile_option_updated option = RubyVM::InstructionSequence.compile_option crc = Zlib.crc32(option.inspect) Bootsnap::CompileCache::Native.compile_option_crc32 = crc end def self.install!(cache_dir) Bootsnap::CompileCache::ISeq.cache_dir = cache_dir Bootsnap::CompileCache::ISeq.compile_option_updated class << RubyVM::InstructionSequence prepend(InstructionSequenceMixin) end end end end end bootsnap-1.4.6/lib/bootsnap/compile_cache/yaml.rb000066400000000000000000000037571362503221000220120ustar00rootroot00000000000000# frozen_string_literal: true require('bootsnap/bootsnap') module Bootsnap module CompileCache module YAML class << self attr_accessor(:msgpack_factory) end def self.input_to_storage(contents, _) raise(Uncompilable) if contents.index("!ruby/object") obj = ::YAML.load(contents) msgpack_factory.packer.write(obj).to_s rescue NoMethodError, RangeError # if the object included things that we can't serialize, fall back to # Marshal. It's a bit slower, but can encode anything yaml can. # NoMethodError is unexpected types; RangeError is Bignums Marshal.dump(obj) end def self.storage_to_output(data) # This could have a meaning in messagepack, and we're being a little lazy # about it. -- but a leading 0x04 would indicate the contents of the YAML # is a positive integer, which is rare, to say the least. if data[0] == 0x04.chr && data[1] == 0x08.chr Marshal.load(data) else msgpack_factory.unpacker.feed(data).read end end def self.input_to_output(data) ::YAML.load(data) end def self.install!(cache_dir) require('yaml') require('msgpack') # MessagePack serializes symbols as strings by default. # We want them to roundtrip cleanly, so we use a custom factory. # see: https://github.com/msgpack/msgpack-ruby/pull/122 factory = MessagePack::Factory.new factory.register_type(0x00, Symbol) Bootsnap::CompileCache::YAML.msgpack_factory = factory klass = class << ::YAML; self; end klass.send(:define_method, :load_file) do |path| begin Bootsnap::CompileCache::Native.fetch( cache_dir, path, Bootsnap::CompileCache::YAML ) rescue Errno::EACCES Bootsnap::CompileCache.permission_error(path) end end end end end end bootsnap-1.4.6/lib/bootsnap/explicit_require.rb000066400000000000000000000027141362503221000216420ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module ExplicitRequire ARCHDIR = RbConfig::CONFIG['archdir'] RUBYLIBDIR = RbConfig::CONFIG['rubylibdir'] DLEXT = RbConfig::CONFIG['DLEXT'] def self.from_self(feature) require_relative("../#{feature}") end def self.from_rubylibdir(feature) require(File.join(RUBYLIBDIR, "#{feature}.rb")) end def self.from_archdir(feature) require(File.join(ARCHDIR, "#{feature}.#{DLEXT}")) end # Given a set of gems, run a block with the LOAD_PATH narrowed to include # only core ruby source paths and these gems -- that is, roughly, # temporarily remove all gems not listed in this call from the LOAD_PATH. # # This is useful before bootsnap is fully-initialized to load gems that it # depends on, without forcing full LOAD_PATH traversals. def self.with_gems(*gems) orig = $LOAD_PATH.dup $LOAD_PATH.clear gems.each do |gem| pat = %r{ / (gems|extensions/[^/]+/[^/]+) # "gems" or "extensions/x64_64-darwin16/2.3.0" / #{Regexp.escape(gem)}-(\h{12}|(\d+\.)) # msgpack-1.2.3 or msgpack-1234567890ab }x $LOAD_PATH.concat(orig.grep(pat)) end $LOAD_PATH << ARCHDIR $LOAD_PATH << RUBYLIBDIR begin yield rescue LoadError $LOAD_PATH.replace(orig) yield end ensure $LOAD_PATH.replace(orig) end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache.rb000066400000000000000000000051721362503221000213440ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache ReturnFalse = Class.new(StandardError) FallbackScan = Class.new(StandardError) DOT_RB = '.rb' DOT_SO = '.so' SLASH = '/' # If a NameError happens several levels deep, don't re-handle it # all the way up the chain: mark it once and bubble it up without # more retries. ERROR_TAG_IVAR = :@__bootsnap_rescued DL_EXTENSIONS = ::RbConfig::CONFIG .values_at('DLEXT', 'DLEXT2') .reject { |ext| !ext || ext.empty? } .map { |ext| ".#{ext}" } .freeze DLEXT = DL_EXTENSIONS[0] # This is nil on linux and darwin, but I think it's '.o' on some other # platform. I'm not really sure which, but it seems better to replicate # ruby's semantics as faithfully as possible. DLEXT2 = DL_EXTENSIONS[1] CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT] class << self attr_reader(:load_path_cache, :autoload_paths_cache, :loaded_features_index, :realpath_cache) def setup(cache_path:, development_mode:, active_support: true) unless supported? warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE return end store = Store.new(cache_path) @loaded_features_index = LoadedFeaturesIndex.new @realpath_cache = RealpathCache.new @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode) require_relative('load_path_cache/core_ext/kernel_require') require_relative('load_path_cache/core_ext/loaded_features') if active_support # this should happen after setting up the initial cache because it # loads a lot of code. It's better to do after +require+ is optimized. require('active_support/dependencies') @autoload_paths_cache = Cache.new( store, ::ActiveSupport::Dependencies.autoload_paths, development_mode: development_mode ) require_relative('load_path_cache/core_ext/active_support') end end def supported? RUBY_ENGINE == 'ruby' && RUBY_PLATFORM =~ /darwin|linux|bsd/ end end end end if Bootsnap::LoadPathCache.supported? require_relative('load_path_cache/path_scanner') require_relative('load_path_cache/path') require_relative('load_path_cache/cache') require_relative('load_path_cache/store') require_relative('load_path_cache/change_observer') require_relative('load_path_cache/loaded_features_index') require_relative('load_path_cache/realpath_cache') end bootsnap-1.4.6/lib/bootsnap/load_path_cache/000077500000000000000000000000001362503221000210125ustar00rootroot00000000000000bootsnap-1.4.6/lib/bootsnap/load_path_cache/cache.rb000066400000000000000000000154011362503221000224030ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../explicit_require') module Bootsnap module LoadPathCache class Cache AGE_THRESHOLD = 30 # seconds def initialize(store, path_obj, development_mode: false) @development_mode = development_mode @store = store @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped. @path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f } @has_relative_paths = nil reinitialize end # What is the path item that contains the dir as child? # e.g. given "/a/b/c/d" exists, and the path is ["/a/b"], load_dir("c/d") # is "/a/b". def load_dir(dir) reinitialize if stale? @mutex.synchronize { @dirs[dir] } end # { 'enumerator' => nil, 'enumerator.so' => nil, ... } BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features| # Builtin features are of the form 'enumerator.so'. # All others include paths. next unless feat.size < 20 && !feat.include?('/') base = File.basename(feat, '.*') # enumerator.so -> enumerator ext = File.extname(feat) # .so features[feat] = nil # enumerator.so features[base] = nil # enumerator next unless [DOT_SO, *DL_EXTENSIONS].include?(ext) DL_EXTENSIONS.each do |dl_ext| features["#{base}#{dl_ext}"] = nil # enumerator.bundle end end.freeze # Try to resolve this feature to an absolute path without traversing the # loadpath. def find(feature) reinitialize if (@has_relative_paths && dir_changed?) || stale? feature = feature.to_s return feature if absolute_path?(feature) return expand_path(feature) if feature.start_with?('./') @mutex.synchronize do x = search_index(feature) return x if x # Ruby has some built-in features that require lies about. # For example, 'enumerator' is built in. If you require it, ruby # returns false as if it were already loaded; however, there is no # file to find on disk. We've pre-built a list of these, and we # return false if any of them is loaded. raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature) # The feature wasn't found on our preliminary search through the index. # We resolve this differently depending on what the extension was. case File.extname(feature) # If the extension was one of the ones we explicitly cache (.rb and the # native dynamic extension, e.g. .bundle or .so), we know it was a # failure and there's nothing more we can do to find the file. # no extension, .rb, (.bundle or .so) when '', *CACHED_EXTENSIONS nil # Ruby allows specifying native extensions as '.so' even when DLEXT # is '.bundle'. This is where we handle that case. when DOT_SO x = search_index(feature[0..-4] + DLEXT) return x if x if DLEXT2 x = search_index(feature[0..-4] + DLEXT2) return x if x end else # other, unknown extension. For example, `.rake`. Since we haven't # cached these, we legitimately need to run the load path search. raise(LoadPathCache::FallbackScan, '', []) end end # In development mode, we don't want to confidently return failures for # cases where the file doesn't appear to be on the load path. We should # be able to detect newly-created files without rebooting the # application. raise(LoadPathCache::FallbackScan, '', []) if @development_mode end if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ def absolute_path?(path) path[1] == ':' end else def absolute_path?(path) path.start_with?(SLASH) end end def unshift_paths(sender, *paths) return unless sender == @path_obj @mutex.synchronize { unshift_paths_locked(*paths) } end def push_paths(sender, *paths) return unless sender == @path_obj @mutex.synchronize { push_paths_locked(*paths) } end def reinitialize(path_obj = @path_obj) @mutex.synchronize do @path_obj = path_obj ChangeObserver.register(self, @path_obj) @index = {} @dirs = {} @generated_at = now push_paths_locked(*@path_obj) end end private def dir_changed? @prev_dir ||= Dir.pwd if @prev_dir == Dir.pwd false else @prev_dir = Dir.pwd true end end def push_paths_locked(*paths) @store.transaction do paths.map(&:to_s).each do |path| p = Path.new(path) @has_relative_paths = true if p.relative? next if p.non_directory? expanded_path = p.expanded_path entries, dirs = p.entries_and_dirs(@store) # push -> low precedence -> set only if unset dirs.each { |dir| @dirs[dir] ||= path } entries.each { |rel| @index[rel] ||= expanded_path } end end end def unshift_paths_locked(*paths) @store.transaction do paths.map(&:to_s).reverse_each do |path| p = Path.new(path) next if p.non_directory? expanded_path = p.expanded_path entries, dirs = p.entries_and_dirs(@store) # unshift -> high precedence -> unconditional set dirs.each { |dir| @dirs[dir] = path } entries.each { |rel| @index[rel] = expanded_path } end end end def expand_path(feature) maybe_append_extension(File.expand_path(feature)) end def stale? @development_mode && @generated_at + AGE_THRESHOLD < now end def now Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i end if DLEXT2 def search_index(f) try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f) end def maybe_append_extension(f) try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f end else def search_index(f) try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f) end def maybe_append_extension(f) try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f end end def try_index(f) if (p = @index[f]) p + '/' + f end end def try_ext(f) f if File.exist?(f) end end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/change_observer.rb000066400000000000000000000041421362503221000244740ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache module ChangeObserver module ArrayMixin # For each method that adds items to one end or another of the array # (<<, push, unshift, concat), override that method to also notify the # observer of the change. def <<(entry) @lpc_observer.push_paths(self, entry.to_s) super end def push(*entries) @lpc_observer.push_paths(self, *entries.map(&:to_s)) super end def unshift(*entries) @lpc_observer.unshift_paths(self, *entries.map(&:to_s)) super end def concat(entries) @lpc_observer.push_paths(self, *entries.map(&:to_s)) super end # uniq! keeps the first occurance of each path, otherwise preserving # order, preserving the effective load path def uniq!(*args) ret = super @lpc_observer.reinitialize if block_given? || !args.empty? ret end # For each method that modifies the array more aggressively, override # the method to also have the observer completely reconstruct its state # after the modification. Many of these could be made to modify the # internal state of the LoadPathCache::Cache more efficiently, but the # accounting cost would be greater than the hit from these, since we # actively discourage calling them. %i( []= clear collect! compact! delete delete_at delete_if fill flatten! insert keep_if map! pop reject! replace reverse! rotate! select! shift shuffle! slice! sort! sort_by! ).each do |method_name| define_method(method_name) do |*args, &block| ret = super(*args, &block) @lpc_observer.reinitialize ret end end end def self.register(observer, arr) return if arr.frozen? # can't register observer, but no need to. arr.instance_variable_set(:@lpc_observer, observer) arr.extend(ArrayMixin) end end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/core_ext/000077500000000000000000000000001362503221000226225ustar00rootroot00000000000000bootsnap-1.4.6/lib/bootsnap/load_path_cache/core_ext/active_support.rb000066400000000000000000000077531362503221000262320ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache module CoreExt module ActiveSupport def self.without_bootsnap_cache prev = Thread.current[:without_bootsnap_cache] || false Thread.current[:without_bootsnap_cache] = true yield ensure Thread.current[:without_bootsnap_cache] = prev end def self.allow_bootsnap_retry(allowed) prev = Thread.current[:without_bootsnap_retry] || false Thread.current[:without_bootsnap_retry] = !allowed yield ensure Thread.current[:without_bootsnap_retry] = prev end module ClassMethods def autoload_paths=(o) super Bootsnap::LoadPathCache.autoload_paths_cache.reinitialize(o) end def search_for_file(path) return super if Thread.current[:without_bootsnap_cache] begin Bootsnap::LoadPathCache.autoload_paths_cache.find(path) rescue Bootsnap::LoadPathCache::ReturnFalse nil # doesn't really apply here rescue Bootsnap::LoadPathCache::FallbackScan nil # doesn't really apply here end end def autoloadable_module?(path_suffix) Bootsnap::LoadPathCache.autoload_paths_cache.load_dir(path_suffix) end def remove_constant(const) CoreExt::ActiveSupport.without_bootsnap_cache { super } end def require_or_load(*) CoreExt::ActiveSupport.allow_bootsnap_retry(true) do super end end # If we can't find a constant using the patched implementation of # search_for_file, try again with the default implementation. # # These methods call search_for_file, and we want to modify its # behaviour. The gymnastics here are a bit awkward, but it prevents # 200+ lines of monkeypatches. def load_missing_constant(from_mod, const_name) CoreExt::ActiveSupport.allow_bootsnap_retry(false) do super end rescue NameError => e raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR) e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) # This function can end up called recursively, we only want to # retry at the top-level. raise(e) if Thread.current[:without_bootsnap_retry] # If we already had cache disabled, there's no use retrying raise(e) if Thread.current[:without_bootsnap_cache] # NoMethodError is a NameError, but we only want to handle actual # NameError instances. raise(e) unless e.class == NameError # We can only confidently handle cases when *this* constant fails # to load, not other constants referred to by it. raise(e) unless e.name == const_name # If the constant was actually loaded, something else went wrong? raise(e) if from_mod.const_defined?(const_name) CoreExt::ActiveSupport.without_bootsnap_cache { super } end # Signature has changed a few times over the years; easiest to not # reiterate it with version polymorphism here... def depend_on(*) super rescue LoadError => e raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR) e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) # If we already had cache disabled, there's no use retrying raise(e) if Thread.current[:without_bootsnap_cache] CoreExt::ActiveSupport.without_bootsnap_cache { super } end end end end end end module ActiveSupport module Dependencies class << self prepend(Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods) end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb000066400000000000000000000060661362503221000261730ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache module CoreExt def self.make_load_error(path) err = LoadError.new(+"cannot load such file -- #{path}") err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) err.define_singleton_method(:path) { path } err end end end end module Kernel module_function # rubocop:disable Style/ModuleFunction alias_method(:require_without_bootsnap, :require) # Note that require registers to $LOADED_FEATURES while load does not. def require_with_bootsnap_lfi(path, resolved = nil) Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do require_without_bootsnap(resolved || path) end end def require(path) return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path) if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)) return require_with_bootsnap_lfi(path, resolved) end raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path)) rescue LoadError => e e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) raise(e) rescue Bootsnap::LoadPathCache::ReturnFalse false rescue Bootsnap::LoadPathCache::FallbackScan require_with_bootsnap_lfi(path) end alias_method(:require_relative_without_bootsnap, :require_relative) def require_relative(path) realpath = Bootsnap::LoadPathCache.realpath_cache.call( caller_locations(1..1).first.absolute_path, path ) require(realpath) end alias_method(:load_without_bootsnap, :load) def load(path, wrap = false) if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)) return load_without_bootsnap(resolved, wrap) end # load also allows relative paths from pwd even when not in $: if File.exist?(relative = File.expand_path(path)) return load_without_bootsnap(relative, wrap) end raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path)) rescue LoadError => e e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) raise(e) rescue Bootsnap::LoadPathCache::ReturnFalse false rescue Bootsnap::LoadPathCache::FallbackScan load_without_bootsnap(path, wrap) end end class Module alias_method(:autoload_without_bootsnap, :autoload) def autoload(const, path) # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately # obvious how to make it work. This feels like a pretty niche case, unclear # if it will ever burn anyone. # # The challenge is that we don't control the point at which the entry gets # added to $LOADED_FEATURES and won't be able to hook that modification # since it's done in C-land. autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path) rescue LoadError => e e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) raise(e) rescue Bootsnap::LoadPathCache::ReturnFalse false rescue Bootsnap::LoadPathCache::FallbackScan autoload_without_bootsnap(const, path) end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb000066400000000000000000000010061362503221000262720ustar00rootroot00000000000000# frozen_string_literal: true class << $LOADED_FEATURES alias_method(:delete_without_bootsnap, :delete) def delete(key) Bootsnap::LoadPathCache.loaded_features_index.purge(key) delete_without_bootsnap(key) end alias_method(:reject_without_bootsnap!, :reject!) def reject!(&block) backup = dup # FIXME: if no block is passed we'd need to return a decorated iterator reject_without_bootsnap!(&block) Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self) end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/loaded_features_index.rb000066400000000000000000000125161362503221000256610ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache # LoadedFeaturesIndex partially mirrors an internal structure in ruby that # we can't easily obtain an interface to. # # This works around an issue where, without bootsnap, *ruby* knows that it # has already required a file by its short name (e.g. require 'bundler') if # a new instance of bundler is added to the $LOAD_PATH which resolves to a # different absolute path. This class makes bootsnap smart enough to # realize that it has already loaded 'bundler', and not just # '/path/to/bundler'. # # If you disable LoadedFeaturesIndex, you can see the problem this solves by: # # 1. `require 'a'` # 2. Prepend a new $LOAD_PATH element containing an `a.rb` # 3. `require 'a'` # # Ruby returns false from step 3. # With bootsnap but with no LoadedFeaturesIndex, this loads two different # `a.rb`s. # With bootsnap and with LoadedFeaturesIndex, this skips the second load, # returning false like ruby. class LoadedFeaturesIndex def initialize @lfi = {} @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped. # In theory the user could mutate $LOADED_FEATURES and invalidate our # cache. If this ever comes up in practice — or if you, the # enterprising reader, feels inclined to solve this problem — we could # parallel the work done with ChangeObserver on $LOAD_PATH to mirror # updates to our @lfi. $LOADED_FEATURES.each do |feat| hash = feat.hash $LOAD_PATH.each do |lpe| next unless feat.start_with?(lpe) # /a/b/lib/my/foo.rb # ^^^^^^^^^ short = feat[(lpe.length + 1)..-1] stripped = strip_extension_if_elidable(short) @lfi[short] = hash @lfi[stripped] = hash end end end # We've optimized for initialize and register to be fast, and purge to be tolerable. # If access patterns make this not-okay, we can lazy-invert the LFI on # first purge and work from there. def purge(feature) @mutex.synchronize do feat_hash = feature.hash @lfi.reject! { |_, hash| hash == feat_hash } end end def purge_multi(features) rejected_hashes = features.map(&:hash).to_set @mutex.synchronize do @lfi.reject! { |_, hash| rejected_hashes.include?(hash) } end end def key?(feature) @mutex.synchronize { @lfi.key?(feature) } end # There is a relatively uncommon case where we could miss adding an # entry: # # If the user asked for e.g. `require 'bundler'`, and we went through the # `FallbackScan` pathway in `kernel_require.rb` and therefore did not # pass `long` (the full expanded absolute path), then we did are not able # to confidently add the `bundler.rb` form to @lfi. # # We could either: # # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but # not quite right; or # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching # entry. def register(short, long = nil) if long.nil? pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$} len = $LOADED_FEATURES.size ret = yield long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat } else ret = yield end hash = long.hash # Do we have a filename with an elidable extension, e.g., # 'bundler.rb', or 'libgit2.so'? altname = if extension_elidable?(short) # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'. strip_extension_if_elidable(short) elsif long && (ext = File.extname(long)) # We already know the extension of the actual file this # resolves to, so put that back on. short + ext end @mutex.synchronize do @lfi[short] = hash (@lfi[altname] = hash) if altname end ret end private STRIP_EXTENSION = /\.[^.]*?$/ private_constant(:STRIP_EXTENSION) # Might Ruby automatically search for this extension if # someone tries to 'require' the file without it? E.g. Ruby # will implicitly try 'x.rb' if you ask for 'x'. # # This is complex and platform-dependent, and the Ruby docs are a little # handwavy about what will be tried when and in what order. # So optimistically pretend that all known elidable extensions # will be tried on all platforms, and that people are unlikely # to name files in a way that assumes otherwise. # (E.g. It's unlikely that someone will know that their code # will _never_ run on MacOS, and therefore think they can get away # with callling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.) # # See . def extension_elidable?(f) f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib') end def strip_extension_if_elidable(f) if extension_elidable?(f) f.sub(STRIP_EXTENSION, '') else f end end end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/path.rb000066400000000000000000000067501362503221000223030ustar00rootroot00000000000000# frozen_string_literal: true require_relative('path_scanner') module Bootsnap module LoadPathCache class Path # A path is considered 'stable' if it is part of a Gem.path or the ruby # distribution. When adding or removing files in these paths, the cache # must be cleared before the change will be noticed. def stable? stability == STABLE end # A path is considered volatile if it doesn't live under a Gem.path or # the ruby distribution root. These paths are scanned for new additions # more frequently. def volatile? stability == VOLATILE end attr_reader(:path) def initialize(path) @path = path.to_s end # True if the path exists, but represents a non-directory object def non_directory? !File.stat(path).directory? rescue Errno::ENOENT, Errno::ENOTDIR false end def relative? !path.start_with?(SLASH) end # Return a list of all the requirable files and all of the subdirectories # of this +Path+. def entries_and_dirs(store) if stable? # the cached_mtime field is unused for 'stable' paths, but is # set to zero anyway, just in case we change the stability heuristics. _, entries, dirs = store.get(expanded_path) return [entries, dirs] if entries # cache hit entries, dirs = scan! store.set(expanded_path, [0, entries, dirs]) return [entries, dirs] end cached_mtime, entries, dirs = store.get(expanded_path) current_mtime = latest_mtime(expanded_path, dirs || []) return [[], []] if current_mtime == -1 # path does not exist return [entries, dirs] if cached_mtime == current_mtime entries, dirs = scan! store.set(expanded_path, [current_mtime, entries, dirs]) [entries, dirs] end def expanded_path File.expand_path(path) end private def scan! # (expensive) returns [entries, dirs] PathScanner.call(expanded_path) end # last time a directory was modified in this subtree. +dirs+ should be a # list of relative paths to directories under +path+. e.g. for /a/b and # /a/b/c, pass ('/a/b', ['c']) def latest_mtime(path, dirs) max = -1 ["", *dirs].each do |dir| curr = begin File.mtime("#{path}/#{dir}").to_i rescue Errno::ENOENT, Errno::ENOTDIR -1 end max = curr if curr > max end max end # a Path can be either stable of volatile, depending on how frequently we # expect its contents may change. Stable paths aren't rescanned nearly as # often. STABLE = :stable VOLATILE = :volatile # Built-in ruby lib stuff doesn't change, but things can occasionally be # installed into sitedir, which generally lives under libdir. RUBY_LIBDIR = RbConfig::CONFIG['libdir'] RUBY_SITEDIR = RbConfig::CONFIG['sitedir'] def stability @stability ||= begin if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) } STABLE elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s) STABLE elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR) STABLE else VOLATILE end end end end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/path_scanner.rb000066400000000000000000000031511362503221000240040ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../explicit_require') module Bootsnap module LoadPathCache module PathScanner ALL_FILES = "/{,**/*/**/}*" REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO) ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/ BUNDLE_PATH = if Bootsnap.bundler? (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze else '' end def self.call(path) path = path.to_s relative_slice = (path.size + 1)..-1 # If the bundle path is a descendent of this path, we do additional # checks to prevent recursing into the bundle path as we recurse # through this path. We don't want to scan the bundle path because # anything useful in it will be present on other load path items. # # This can happen if, for example, the user adds '.' to the load path, # and the bundle path is '.bundle'. contains_bundle_path = BUNDLE_PATH.start_with?(path) dirs = [] requirables = [] Dir.glob(path + ALL_FILES).each do |absolute_path| next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH) relative_path = absolute_path.slice(relative_slice) if File.directory?(absolute_path) dirs << relative_path elsif REQUIRABLE_EXTENSIONS.include?(File.extname(relative_path)) requirables << relative_path end end [requirables, dirs] end end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/realpath_cache.rb000066400000000000000000000013051362503221000242610ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache class RealpathCache def initialize @cache = Hash.new { |h, k| h[k] = realpath(*k) } end def call(*key) @cache[key] end private def realpath(caller_location, path) base = File.dirname(caller_location) file = find_file(File.expand_path(path, base)) dir = File.dirname(file) File.join(dir, File.basename(file)) end def find_file(name) ['', *CACHED_EXTENSIONS].each do |ext| filename = "#{name}#{ext}" return File.realpath(filename) if File.exist?(filename) end name end end end end bootsnap-1.4.6/lib/bootsnap/load_path_cache/store.rb000066400000000000000000000050741362503221000225010ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../explicit_require') Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') } Bootsnap::ExplicitRequire.from_rubylibdir('fileutils') module Bootsnap module LoadPathCache class Store NestedTransactionError = Class.new(StandardError) SetOutsideTransactionNotAllowed = Class.new(StandardError) def initialize(store_path) @store_path = store_path # TODO: Remove conditional once Ruby 2.2 support is dropped. @txn_mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new @dirty = false load_data end def get(key) @data[key] end def fetch(key) raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned? v = get(key) unless v @dirty = true v = yield @data[key] = v end v end def set(key, value) raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned? if value != @data[key] @dirty = true @data[key] = value end end def transaction raise(NestedTransactionError) if @txn_mutex.owned? @txn_mutex.synchronize do begin yield ensure commit_transaction end end end private def commit_transaction if @dirty dump_data @dirty = false end end def load_data @data = begin MessagePack.load(File.binread(@store_path)) # handle malformed data due to upgrade incompatability rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError {} rescue ArgumentError => e e.message =~ /negative array size/ ? {} : raise end end def dump_data # Change contents atomically so other processes can't get invalid # caches if they read at an inopportune time. tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp" FileUtils.mkpath(File.dirname(tmp)) exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY # `encoding:` looks redundant wrt `binwrite`, but necessary on windows # because binary is part of mode. File.binwrite(tmp, MessagePack.dump(@data), mode: exclusive_write, encoding: Encoding::BINARY) FileUtils.mv(tmp, @store_path) rescue Errno::EEXIST retry end end end end bootsnap-1.4.6/lib/bootsnap/setup.rb000066400000000000000000000025001362503221000174160ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../bootsnap') env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV'] development_mode = ['', nil, 'development'].include?(env) cache_dir = ENV['BOOTSNAP_CACHE_DIR'] unless cache_dir config_dir_frame = caller.detect do |line| line.include?('/config/') end unless config_dir_frame $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:") $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or") $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR") raise("couldn't infer bootsnap cache directory") end path = config_dir_frame.split(/:\d+:/).first path = File.dirname(path) until File.basename(path) == 'config' app_root = File.dirname(path) cache_dir = File.join(app_root, 'tmp', 'cache') end ruby_version = Gem::Version.new(RUBY_VERSION) iseq_cache_enabled = ruby_version < Gem::Version.new('2.5.0') || ruby_version >= Gem::Version.new('2.6.0') Bootsnap.setup( cache_dir: cache_dir, development_mode: development_mode, load_path_cache: true, autoload_paths_cache: true, # assume rails. open to PRs to impl. detection disable_trace: false, compile_cache_iseq: iseq_cache_enabled, compile_cache_yaml: true, ) bootsnap-1.4.6/lib/bootsnap/version.rb000066400000000000000000000001061362503221000177430ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap VERSION = "1.4.6" end bootsnap-1.4.6/shipit.rubygems.yml000066400000000000000000000000001362503221000172060ustar00rootroot00000000000000bootsnap-1.4.6/test/000077500000000000000000000000001362503221000143205ustar00rootroot00000000000000bootsnap-1.4.6/test/bundler_test.rb000066400000000000000000000022341362503221000173400ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class BundlerTest < Minitest::Test def test_bundler_with_bundle_bin_path_env without_required_env_keys do ENV['BUNDLE_BIN_PATH'] = 'foo' assert_predicate(Bootsnap, :bundler?) end end def test_bundler_with_bundle_gemfile_env without_required_env_keys do ENV['BUNDLE_GEMFILE'] = 'foo' assert_predicate(Bootsnap, :bundler?) end end def test_bundler_without_bundler_const without_bundler do refute_predicate(Bootsnap, :bundler?) end end def test_bundler_without_required_env_keys without_required_env_keys do assert(defined?(::Bundler)) refute_predicate(Bootsnap, :bundler?) end end private def without_bundler b = ::Bundler begin Object.send(:remove_const, :Bundler) yield ensure Object.send(:const_set, :Bundler, b) end end def without_required_env_keys original_env = {} begin %w(BUNDLE_BIN_PATH BUNDLE_GEMFILE).each do |k| original_env[k] = ENV[k] ENV[k] = nil end yield ensure original_env.each { |k, v| ENV[k] = v } end end end bootsnap-1.4.6/test/compile_cache_handler_errors_test.rb000066400000000000000000000054261362503221000235570ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class CompileCacheHandlerErrorsTest < Minitest::Test include(TmpdirHelper) # now test three failure modes of each handler method: # 1. unexpected type # 2. invalid instance of expected type # 3. exception def test_input_to_storage_unexpected_type path = Help.set_file('a.rb', 'a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).returns(nil) # this could be made slightly more obvious though. assert_raises(TypeError) { load(path) } end def test_input_to_storage_invalid_instance_of_expected_type path = Help.set_file('a.rb', 'a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).returns('broken') Bootsnap::CompileCache::ISeq.expects(:input_to_output).with('a = 3').returns('whatever') _, err = capture_subprocess_io do load(path) end assert_match(/broken binary/, err) end def test_input_to_storage_raises path = Help.set_file('a.rb', 'a = 3', 100) klass = Class.new(StandardError) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).raises(klass, 'oops') assert_raises(klass) { load(path) } end def test_storage_to_output_unexpected_type path = Help.set_file('a.rb', 'a = a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).returns(Object.new) # It seems like ruby doesn't really care. load(path) end # not really a thing. Really, we just return whatever. It's a problem with # the handler if that's invalid. # def test_storage_to_output_invalid_instance_of_expected_type def test_storage_to_output_raises path = Help.set_file('a.rb', 'a = a = 3', 100) klass = Class.new(StandardError) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).raises(klass, 'oops') assert_raises(klass) { load(path) } # called from two paths; this tests the second. assert_raises(klass) { load(path) } end def test_input_to_output_unexpected_type path = Help.set_file('a.rb', 'a = a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).raises(Bootsnap::CompileCache::Uncompilable) Bootsnap::CompileCache::ISeq.expects(:input_to_output).returns(Object.new) # It seems like ruby doesn't really care. load(path) end # not really a thing. Really, we just return whatever. It's a problem with # the handler if that's invalid. # def test_input_to_output_invalid_instance_of_expected_type def test_input_to_output_raises path = Help.set_file('a.rb', 'a = 3', 100) klass = Class.new(StandardError) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).raises(Bootsnap::CompileCache::Uncompilable) Bootsnap::CompileCache::ISeq.expects(:input_to_output).raises(klass, 'oops') assert_raises(klass) { load(path) } end end bootsnap-1.4.6/test/compile_cache_key_format_test.rb000066400000000000000000000044551362503221000227070ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('tempfile') require('tmpdir') require('fileutils') class CompileCacheKeyFormatTest < Minitest::Test include(TmpdirHelper) R = { version: 0...4, ruby_platform: 4...8, compile_option: 8...12, ruby_revision: 12...16, size: 16...24, mtime: 24...32, data_size: 32...40, } def test_key_version key = cache_key_for_file(__FILE__) exp = [2].pack("L") assert_equal(exp, key[R[:version]]) end def test_key_compile_option_stable k1 = cache_key_for_file(__FILE__) k2 = cache_key_for_file(__FILE__) RubyVM::InstructionSequence.compile_option = { tailcall_optimization: true } k3 = cache_key_for_file(__FILE__) assert_equal(k1[R[:compile_option]], k2[R[:compile_option]]) refute_equal(k1[R[:compile_option]], k3[R[:compile_option]]) ensure RubyVM::InstructionSequence.compile_option = { tailcall_optimization: false } end def test_key_ruby_revision key = cache_key_for_file(__FILE__) exp = if RUBY_REVISION.is_a?(String) [Help.fnv1a_64(RUBY_REVISION) >> 32].pack("L") else [RUBY_REVISION].pack("L") end assert_equal(exp, key[R[:ruby_revision]]) end def test_key_size key = cache_key_for_file(__FILE__) exp = File.size(__FILE__) act = key[R[:size]].unpack("Q")[0] assert_equal(exp, act) end def test_key_mtime key = cache_key_for_file(__FILE__) exp = File.mtime(__FILE__).to_i act = key[R[:mtime]].unpack("Q")[0] assert_equal(exp, act) end def test_fetch actual = Bootsnap::CompileCache::Native.fetch(@tmp_dir, '/dev/null', TestHandler) assert_equal('NEATO /DEV/NULL', actual) data = File.read("#{@tmp_dir}/8c/d2d180bbd995df") assert_equal("neato /dev/null", data.force_encoding(Encoding::BINARY)[64..-1]) actual = Bootsnap::CompileCache::Native.fetch(@tmp_dir, '/dev/null', TestHandler) assert_equal('NEATO /DEV/NULL', actual) end def test_unexistent_fetch assert_raises(Errno::ENOENT) do Bootsnap::CompileCache::Native.fetch(@tmp_dir, '123', Bootsnap::CompileCache::ISeq) end end private def cache_key_for_file(file) Bootsnap::CompileCache::Native.fetch(@tmp_dir, file, TestHandler) data = File.read(Help.cache_path(@tmp_dir, file)) Help.binary(data[0..31]) end end bootsnap-1.4.6/test/compile_cache_test.rb000066400000000000000000000073651362503221000204720ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class CompileCacheTest < Minitest::Test include(TmpdirHelper) def test_compile_option_crc32 # Just assert that this works. Bootsnap::CompileCache::Native.compile_option_crc32 = 0xffffffff assert_raises(RangeError) do Bootsnap::CompileCache::Native.compile_option_crc32 = 0xffffffff + 1 end end def test_coverage_running? refute(Bootsnap::CompileCache::Native.coverage_running?) require('coverage') begin Coverage.start assert(Bootsnap::CompileCache::Native.coverage_running?) ensure Coverage.result end end def test_no_write_permission_to_cache path = Help.set_file('a.rb', 'a = 3', 100) folder = File.dirname(Help.cache_path(@tmp_dir, path)) FileUtils.mkdir_p(folder) FileUtils.chmod(0400, folder) assert_raises(Bootsnap::CompileCache::PermissionError) { load(path) } end def test_can_open_read_only_cache path = Help.set_file('a.rb', 'a = a = 3', 100) # Load once to create the cache file load(path) FileUtils.chmod(0400, path) # Loading again after the file is marked read-only should still succeed load(path) end def test_file_is_only_read_once path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) # This doesn't really *prove* the file is only read once, but # it at least asserts the input is only cached once. Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(1).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) load(path) end def test_raises_syntax_error path = Help.set_file('a.rb', 'a = (3', 100) assert_raises(SyntaxError) do # SyntaxError emits directly to stderr in addition to raising, it seems. capture_io { load(path) } end end def test_no_recache_when_mtime_and_size_same path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(1).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) Help.set_file(path, 'a = a = 4', 100) load(path) end def test_recache_when_mtime_different path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) # Totally lies the second time but that's not the point. Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(2).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) Help.set_file(path, 'a = a = 2', 101) load(path) end def test_recache_when_size_different path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) # Totally lies the second time but that's not the point. Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(2).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) Help.set_file(path, 'a = 33', 100) load(path) end def test_invalid_cache_file path = Help.set_file('a.rb', 'a = a = 3', 100) cp = Help.cache_path(@tmp_dir, path) FileUtils.mkdir_p(File.dirname(cp)) File.write(cp, 'nope') load(path) assert(File.size(cp) > 32) # cache was overwritten end end bootsnap-1.4.6/test/helper_test.rb000066400000000000000000000004541362503221000171660ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class HelperTest < MiniTest::Test include(TmpdirHelper) def test_validate_cache_path path = Help.set_file('a.rb', 'a = a = 3', 100) cp = Help.cache_path(@tmp_dir, path) load(path) assert_equal(true, File.file?(cp)) end end bootsnap-1.4.6/test/load_path_cache/000077500000000000000000000000001362503221000173765ustar00rootroot00000000000000bootsnap-1.4.6/test/load_path_cache/cache_test.rb000066400000000000000000000117331362503221000220320ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class CacheTest < MiniTest::Test def setup @dir1 = File.realpath(Dir.mktmpdir) @dir2 = File.realpath(Dir.mktmpdir) FileUtils.touch("#{@dir1}/a.rb") FileUtils.mkdir_p("#{@dir1}/foo/bar") FileUtils.touch("#{@dir1}/foo/bar/baz.rb") FileUtils.touch("#{@dir2}/b.rb") FileUtils.touch("#{@dir1}/conflict.rb") FileUtils.touch("#{@dir2}/conflict.rb") FileUtils.touch("#{@dir1}/dl#{DLEXT}") FileUtils.touch("#{@dir1}/both.rb") FileUtils.touch("#{@dir1}/both#{DLEXT}") end def teardown FileUtils.rm_rf(@dir1) FileUtils.rm_rf(@dir2) end # dev.yml specifies 2.3.3 and this test assumes it. Failures on other # versions aren't a big deal, but feel free to fix the test. def test_builtin_features cache = Cache.new(NullCache, []) assert_raises(ReturnFalse) { cache.find('thread') } assert_raises(ReturnFalse) { cache.find('thread.rb') } assert_raises(ReturnFalse) { cache.find('enumerator') } assert_raises(ReturnFalse) { cache.find('enumerator.so') } if RUBY_PLATFORM =~ /darwin/ assert_raises(ReturnFalse) { cache.find('enumerator.bundle') } else assert_raises(FallbackScan) { cache.find('enumerator.bundle') } end bundle = RUBY_PLATFORM =~ /darwin/ ? 'bundle' : 'so' refute(cache.find('thread.' + bundle)) refute(cache.find('enumerator.rb')) refute(cache.find('encdb.' + bundle)) end def test_simple po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/a.rb", cache.find('a')) cache.push_paths(po, @dir2) assert_equal("#{@dir2}/b.rb", cache.find('b')) end def test_extension_append_for_relative_paths po = [@dir1] cache = Cache.new(NullCache, po) Dir.chdir(@dir1) do assert_equal("#{@dir1}/a.rb", cache.find('./a')) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find('./dl')) assert_equal("#{@dir1}/enoent", cache.find('./enoent')) end end def test_unshifted_paths_have_higher_precedence po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict')) cache.unshift_paths(po, @dir2) assert_equal("#{@dir2}/conflict.rb", cache.find('conflict')) end def test_pushed_paths_have_lower_precedence po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict')) cache.push_paths(po, @dir2) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict')) end def test_directory_caching cache = Cache.new(NullCache, [@dir1]) assert_equal(@dir1, cache.load_dir("foo")) assert_equal(@dir1, cache.load_dir("foo/bar")) assert_nil(cache.load_dir("bar")) end def test_extension_permutations cache = Cache.new(NullCache, [@dir1]) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find('dl')) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find("dl#{DLEXT}")) assert_equal("#{@dir1}/both.rb", cache.find("both")) assert_equal("#{@dir1}/both.rb", cache.find("both.rb")) assert_equal("#{@dir1}/both#{DLEXT}", cache.find("both#{DLEXT}")) end def test_relative_paths_rescanned Dir.chdir(@dir2) do cache = Cache.new(NullCache, %w(foo)) refute(cache.find('bar/baz')) Dir.chdir(@dir1) do # one caveat here is that you get the actual path back when # resolving relative paths. On darwin, this means that # /var/folders/... comes back as /private/var/folders/... -- In # production, this should be fine, but for this test to pass, we # have to resolve it. assert_equal(File.realpath("#{@dir1}/foo/bar/baz.rb"), cache.find('bar/baz')) end end end def test_development_mode time = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i # without development_mode, no refresh dev_no_cache = Cache.new(NullCache, [@dir1], development_mode: false) dev_yes_cache = Cache.new(NullCache, [@dir1], development_mode: true) FileUtils.touch("#{@dir1}/new.rb") dev_no_cache.stubs(:now).returns(time + 31) refute(dev_no_cache.find('new')) dev_yes_cache.stubs(:now).returns(time + 28) assert_raises(Bootsnap::LoadPathCache::FallbackScan) do dev_yes_cache.find('new') end dev_yes_cache.stubs(:now).returns(time + 31) assert(dev_yes_cache.find('new')) end def test_path_obj_equal? path_obj = [] cache = Cache.new(NullCache, path_obj) path_obj.unshift(@dir1) assert_equal("#{@dir1}/a.rb", cache.find('a')) end end end end bootsnap-1.4.6/test/load_path_cache/change_observer_test.rb000066400000000000000000000026521362503221000241230ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class ChangeObserverTest < MiniTest::Test def setup @observer = Object.new @arr = [] ChangeObserver.register(@observer, @arr) end def test_observes_changes @observer.expects(:push_paths).with(@arr, 'a') @arr << 'a' @observer.expects(:push_paths).with(@arr, 'b', 'c') @arr.push('b', 'c') @observer.expects(:unshift_paths).with(@arr, 'd', 'e') @arr.unshift('d', 'e') @observer.expects(:push_paths).with(@arr, 'f', 'g') @arr.concat(%w(f g)) @observer.expects(:reinitialize).times(4) @arr.delete(3) @arr.compact! @arr.map!(&:upcase) assert_equal('G', @arr.pop) assert_equal(%w(D E A B C F), @arr) end def test_register_frozen # just assert no crash ChangeObserver.register(@observer, @arr.dup.freeze) end def test_register_twice_observes_once ChangeObserver.register(@observer, @arr) @observer.expects(:push_paths).with(@arr, 'a').once @arr << 'a' assert_equal(%w(a), @arr) end def test_uniq_without_block @observer.expects(:reinitialize).never @arr.uniq! end def test_uniq_with_block @observer.expects(:reinitialize).once @arr.uniq! {} end end end end bootsnap-1.4.6/test/load_path_cache/loaded_features_index_test.rb000066400000000000000000000120701362503221000252770ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class LoadedFeaturesIndexTest < MiniTest::Test def setup @index = LoadedFeaturesIndex.new # not really necessary but let's just make it a clean slate @index.instance_variable_set(:@lfi, {}) end def test_successful_addition refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', '/a/b/bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_no_add_on_raise refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) assert_raises(RuntimeError) do @index.register('bundler', '/a/b/bundler.rb') { raise } end refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_infer_base_from_ext refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_only_strip_elidable_ext # It is only valid to strip a '.rb' or shared library extension from the # end of a filename, not anything else. # # E.g. 'descriptor.pb.rb' if required via 'descriptor.pb' # should never be shortened to merely 'descriptor'! refute(@index.key?('descriptor.pb')) refute(@index.key?('descriptor.pb.rb')) refute(@index.key?('descriptor.rb')) refute(@index.key?('descriptor')) refute(@index.key?('foo')) @index.register('descriptor.pb.rb') {} assert(@index.key?('descriptor.pb')) assert(@index.key?('descriptor.pb.rb')) refute(@index.key?('descriptor.rb')) refute(@index.key?('descriptor')) refute(@index.key?('foo')) end def test_shared_library_ext_considered_elidable # Check that '.dylib' (token shared library extension) is treated as elidable, # and doesn't get mixed up with Ruby '.rb' files. refute(@index.key?('libgit2.dylib')) refute(@index.key?('libgit2.dylib.rb')) refute(@index.key?('descriptor.rb')) refute(@index.key?('descriptor')) refute(@index.key?('foo')) @index.register('libgit2.dylib') {} assert(@index.key?('libgit2.dylib')) refute(@index.key?('libgit2.dylib.rb')) refute(@index.key?('libgit2.rb')) refute(@index.key?('foo')) end def test_cannot_infer_ext_from_base # Current limitation refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler') {} assert(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_purge_loaded_feature refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', '/a/b/bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.purge('/a/b/bundler.rb') refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_purge_multi_loaded_feature refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', '/a/b/bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.purge_multi(['/a/b/bundler.rb', '/a/b/does-not-exist.rb']) refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_register_finds_correct_feature refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', nil) { $LOADED_FEATURES << '/a/b/bundler.rb' } assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.purge('/a/b/bundler.rb') refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_derives_initial_state_from_loaded_features index = LoadedFeaturesIndex.new assert(index.key?('minitest/autorun')) assert(index.key?('minitest/autorun.rb')) refute(index.key?('minitest/autorun.so')) end def test_works_with_pathname path = '/tmp/bundler.rb' pathname = Pathname.new(path) @index.register(pathname, path) { true } assert(@index.key?(pathname)) end end end end bootsnap-1.4.6/test/load_path_cache/path_scanner_test.rb000066400000000000000000000024401362503221000234270ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class PathScannerTest < MiniTest::Test DLEXT = RbConfig::CONFIG['DLEXT'] OTHER_DLEXT = DLEXT == 'bundle' ? 'so' : 'bundle' def test_scans_requirables_and_dirs Dir.mktmpdir do |dir| FileUtils.mkdir_p("#{dir}/ruby/a") FileUtils.mkdir_p("#{dir}/ruby/b/c") FileUtils.mkdir_p("#{dir}/support/h/i") FileUtils.mkdir_p("#{dir}/ruby/l") FileUtils.mkdir_p("#{dir}/support/l/m") FileUtils.touch("#{dir}/ruby/d.rb") FileUtils.touch("#{dir}/ruby/e.#{DLEXT}") FileUtils.touch("#{dir}/ruby/f.#{OTHER_DLEXT}") FileUtils.touch("#{dir}/ruby/a/g.rb") FileUtils.touch("#{dir}/support/h/j.rb") FileUtils.touch("#{dir}/support/h/i/k.rb") FileUtils.touch("#{dir}/support/l/m/n.rb") FileUtils.ln_s("#{dir}/support/h", "#{dir}/ruby/h") FileUtils.ln_s("#{dir}/support/l/m", "#{dir}/ruby/l/m") entries, dirs = PathScanner.call("#{dir}/ruby") assert_equal(["a/g.rb", "d.rb", "e.#{DLEXT}", "h/i/k.rb", "h/j.rb", "l/m/n.rb"], entries.sort) assert_equal(["a", "b", "b/c", "h", "h/i", "l", "l/m"], dirs.sort) end end end end end bootsnap-1.4.6/test/load_path_cache/path_test.rb000066400000000000000000000102031362503221000217120ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('bootsnap/load_path_cache') module Bootsnap module LoadPathCache class PathTest < MiniTest::Test def setup @cache = Object.new end def test_stability require('time') time_file = Time.method(:rfc2822).source_location[0] volatile = Path.new(__FILE__) stable = Path.new(time_file) unknown = Path.new('/who/knows') lib = Path.new(RbConfig::CONFIG['libdir'] + '/a') site = Path.new(RbConfig::CONFIG['sitedir'] + '/b') bundler = Path.new('/bp/3') Bundler.stubs(:bundle_path).returns('/bp') assert(stable.stable?, "The stable path #{stable.path.inspect} was unexpectedly not stable.") refute(stable.volatile?, "The stable path #{stable.path.inspect} was unexpectedly volatile.") assert(volatile.volatile?, "The volatile path #{volatile.path.inspect} was unexpectedly not volatile.") refute(volatile.stable?, "The volatile path #{volatile.path.inspect} was unexpectedly stable.") assert(unknown.volatile?, "The unknown path #{unknown.path.inspect} was unexpectedly not volatile.") refute(unknown.stable?, "The unknown path #{unknown.path.inspect} was unexpectedly stable.") assert(lib.stable?, "The lib path #{lib.path.inspect} was unexpectedly not stable.") refute(site.stable?, "The site path #{site.path.inspect} was unexpectedly stable.") assert(bundler.stable?, "The bundler path #{bundler.path.inspect} was unexpectedly not stable.") end def test_non_directory? refute(Path.new('/dev').non_directory?) refute(Path.new('/nope').non_directory?) assert(Path.new('/dev/null').non_directory?) assert(Path.new('/etc/hosts').non_directory?) end def test_volatile_cache_valid_when_mtime_has_not_changed with_caching_fixtures do |dir, _a, _a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp @cache.expects(:get).with(dir).returns([100, entries, dirs]) path.entries_and_dirs(@cache) end end def test_volatile_cache_invalid_when_mtime_changed with_caching_fixtures do |dir, _a, a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp FileUtils.touch(a_b, mtime: Time.at(101)) @cache.expects(:get).with(dir).returns([100, entries, dirs]) @cache.expects(:set).with(dir, [101, entries, dirs]) # next read doesn't regen @cache.expects(:get).with(dir).returns([101, entries, dirs]) path.entries_and_dirs(@cache) path.entries_and_dirs(@cache) end end def test_volatile_cache_generated_when_missing with_caching_fixtures do |dir, _a, _a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp @cache.expects(:get).with(dir).returns(nil) @cache.expects(:set).with(dir, [100, entries, dirs]) path.entries_and_dirs(@cache) end end def test_stable_cache_does_not_notice_when_mtime_changes with_caching_fixtures do |dir, _a, a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp path.expects(:stable?).returns(true) FileUtils.touch(a_b, mtime: Time.at(101)) # It's unfortunate that we're stubbing the impl of #fetch here. PathScanner.expects(:call).never @cache.expects(:get).with(dir).returns([100, entries, dirs]) path.entries_and_dirs(@cache) end end private def with_caching_fixtures Dir.mktmpdir do |dir| a = "#{dir}/a" a_b = "#{dir}/a/b" a_b_c = "#{dir}/a/b/c.rb" FileUtils.mkdir_p(a_b) [a_b_c, a_b, a, dir].each { |f| FileUtils.touch(f, mtime: Time.at(100)) } yield(dir, a, a_b, a_b_c) end end end end end bootsnap-1.4.6/test/load_path_cache/realpath_cache_test.rb000066400000000000000000000055031362503221000237100ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class RealpathCacheTest < MiniTest::Test EXTENSIONS = ['', *CACHED_EXTENSIONS] def setup @cache = RealpathCache.new @base_dir = File.realpath(Dir.mktmpdir) @absolute_dir = "#{@base_dir}/absolute" Dir.mkdir(@absolute_dir) @symlinked_dir = "#{@base_dir}/symlink" FileUtils.ln_s(@absolute_dir, @symlinked_dir) real_caller = File.new("#{@absolute_dir}/real_caller.rb", 'w').path symlinked_caller = "#{@absolute_dir}/symlinked_caller.rb" FileUtils.ln_s(real_caller, symlinked_caller) EXTENSIONS.each do |ext| real_required = File.new("#{@absolute_dir}/real_required#{ext}", 'w').path symlinked_required = "#{@absolute_dir}/symlinked_required#{ext}" FileUtils.ln_s(real_required, symlinked_required) end end def teardown FileUtils.remove_entry(@base_dir) end def remove_required(extensions) extensions.each do |ext| FileUtils.rm("#{@absolute_dir}/real_required#{ext}") FileUtils.rm("#{@absolute_dir}/symlinked_required#{ext}") end end variants = %w(absolute symlink).product(%w(absolute symlink), %w(real_caller symlinked_caller), %w(real_required symlinked_required)) variants.each do |caller_dir, required_dir, caller_file, required_file| method_name = "test_with_#{caller_dir}_caller_dir_" \ "#{required_dir}_require_dir_" \ "#{caller_file}_#{required_file}" define_method(method_name) do caller_path = "#{@base_dir}/#{caller_dir}/#{caller_file}" require_path = "../#{required_dir}/#{required_file}.rb" expected = "#{@absolute_dir}/real_required.rb" assert(@cache.call(caller_path, require_path).eql?(expected)) end (EXTENSIONS.size - 1).times do |n| removing = EXTENSIONS[0..n] define_method("#{method_name}_no#{removing.join('_')}_extensions") do caller_path = "#{@base_dir}/#{caller_dir}/#{caller_file}" require_path = "../#{required_dir}/#{required_file}" remove_required(removing) expected = "#{@absolute_dir}/real_required#{EXTENSIONS[n + 1]}" assert(@cache.call(caller_path, require_path).eql?(expected)) end end define_method("#{method_name}_no_files") do caller_path = "#{@base_dir}/#{caller_dir}/#{caller_file}" require_path = "../#{required_dir}/#{required_file}" remove_required(EXTENSIONS) expected = "#{@base_dir}/#{required_dir}/#{required_file}" assert(@cache.call(caller_path, require_path).eql?(expected)) end end end end end bootsnap-1.4.6/test/load_path_cache/store_test.rb000066400000000000000000000040161362503221000221170ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('tmpdir') require('fileutils') module Bootsnap module LoadPathCache class StoreTest < MiniTest::Test def setup @dir = Dir.mktmpdir @path = "#{@dir}/store" @store = Store.new(@path) end def teardown FileUtils.rm_rf(@dir) end attr_reader(:store) def test_persistence store.transaction { store.set('a', 'b') } store2 = Store.new(@path) assert_equal('b', store2.get('a')) end def test_modification store.transaction { store.set('a', 'b') } store2 = Store.new(@path) assert_equal('b', store2.get('a')) store.transaction { store.set('a', 'c') } store3 = Store.new(@path) assert_equal('c', store3.get('a')) end def test_stores_arrays store.transaction { store.set('a', [1234, %w(a b)]) } store2 = Store.new(@path) assert_equal([1234, %w(a b)], store2.get('a')) end def test_transaction_required_to_set assert_raises(Store::SetOutsideTransactionNotAllowed) do store.set('a', 'b') end assert_raises(Store::SetOutsideTransactionNotAllowed) do store.fetch('a') { 'b' } end end def test_nested_transaction_fails assert_raises(Store::NestedTransactionError) do store.transaction { store.transaction } end end def test_no_commit_unless_dirty store.transaction { store.set('a', nil) } refute(File.exist?(@path)) store.transaction { store.set('a', 1) } assert(File.exist?(@path)) end def test_retry_on_collision retries = sequence('retries') File.expects(:binwrite).in_sequence(retries).raises(Errno::EEXIST.new("File exists @ rb_sysopen")) File.expects(:binwrite).in_sequence(retries).returns(1) FileUtils.expects(:mv).in_sequence(retries) store.transaction { store.set('a', 1) } end end end end bootsnap-1.4.6/test/minimal_support/000077500000000000000000000000001362503221000175425ustar00rootroot00000000000000bootsnap-1.4.6/test/minimal_support/bootsnap_setup.rb000066400000000000000000000001211362503221000231260ustar00rootroot00000000000000# frozen_string_literal: true require('bundler/setup') require('bootsnap/setup') bootsnap-1.4.6/test/test_helper.rb000066400000000000000000000034271362503221000171710ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__)) require('bundler/setup') require('bootsnap') require('tmpdir') require('fileutils') require('minitest/autorun') require('mocha/minitest') cache_dir = File.expand_path('../../tmp/bootsnap-compile-cache', __FILE__) Bootsnap::CompileCache.setup(cache_dir: cache_dir, iseq: true, yaml: false) module TestHandler def self.input_to_storage(_i, p) 'neato ' + p end def self.storage_to_output(d) d.upcase end def self.input_to_output(_d) raise('but why tho') end end module NullCache def self.get(*) end def self.set(*) end def self.transaction(*) yield end def self.fetch(*) yield end end module MiniTest class Test module Help class << self def binary(str) str.force_encoding(Encoding::BINARY) end def cache_path(dir, file) hex = fnv1a_64(file).to_s(16) "#{dir}/#{hex[0..1]}/#{hex[2..-1]}" end def fnv1a_64(data) hash = 0xcbf29ce484222325 data.bytes.each do |byte| hash = hash ^ byte hash = (hash * 0x100000001b3) % (2**64) end hash end def set_file(path, contents, mtime) File.write(path, contents) FileUtils.touch(path, mtime: mtime) path end end end end end module TmpdirHelper def setup super @prev_dir = Dir.pwd @tmp_dir = Dir.mktmpdir('bootsnap-test') Dir.chdir(@tmp_dir) @prev = Bootsnap::CompileCache::ISeq.cache_dir Bootsnap::CompileCache::ISeq.cache_dir = @tmp_dir end def teardown super Dir.chdir(@prev_dir) FileUtils.remove_entry(@tmp_dir) Bootsnap::CompileCache::ISeq.cache_dir = @prev end end