From 589bd7a39610b181c286e88c906a50a3872588a7 Mon Sep 17 00:00:00 2001 From: Alexander Momchilov Date: Wed, 13 May 2026 10:29:47 -0400 Subject: [PATCH] Reduce stat syscalls Don't check `File.file?` before `File.read`. If anything it's a [TOC/TOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use) error, but it's also an extra stat system call. Instead, just try to read, and rescue the exception, if any. --- lib/spoom/context/bundle.rb | 9 +++++++-- lib/spoom/context/sorbet.rb | 9 +++++++-- lib/spoom/file_collector.rb | 10 ++++++++-- lib/spoom/sorbet/metrics/metrics_file_parser.rb | 1 + 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/spoom/context/bundle.rb b/lib/spoom/context/bundle.rb index e7bcafaa..dcee9c2f 100644 --- a/lib/spoom/context/bundle.rb +++ b/lib/spoom/context/bundle.rb @@ -10,12 +10,16 @@ module Bundle #: -> String? def read_gemfile read("Gemfile") + rescue Errno::ENOENT, Errno::EACCES + nil end # Read the contents of the Gemfile.lock in this context directory #: -> String? def read_gemfile_lock read("Gemfile.lock") + rescue Errno::ENOENT, Errno::EACCES + nil end # Set the `contents` of the Gemfile in this context directory @@ -45,9 +49,10 @@ def bundle_exec(command, version: nil, capture_err: true) #: -> Hash[String, Bundler::LazySpecification] def gemfile_lock_specs - return {} unless file?("Gemfile.lock") + lockfile = read_gemfile_lock + return {} unless lockfile - parser = Bundler::LockfileParser.new(read_gemfile_lock) + parser = Bundler::LockfileParser.new(lockfile) parser.specs.to_h { |spec| [spec.name, spec] } end diff --git a/lib/spoom/context/sorbet.rb b/lib/spoom/context/sorbet.rb index 4069015f..35e46165 100644 --- a/lib/spoom/context/sorbet.rb +++ b/lib/spoom/context/sorbet.rb @@ -42,10 +42,15 @@ def srb_metrics(*arg, sorbet_bin: nil, capture_err: true) sorbet_bin: sorbet_bin, capture_err: capture_err, ) - return unless file?(metrics_file) metrics_path = absolute_path_to(metrics_file) - metrics = Spoom::Sorbet::Metrics::MetricsFileParser.parse_file(metrics_path) + + begin + metrics = Spoom::Sorbet::Metrics::MetricsFileParser.parse_file(metrics_path) + rescue Errno::ENOENT, Errno::EACCES + return + end + remove!(metrics_file) metrics end diff --git a/lib/spoom/file_collector.rb b/lib/spoom/file_collector.rb index 2c7b7927..293dda60 100644 --- a/lib/spoom/file_collector.rb +++ b/lib/spoom/file_collector.rb @@ -33,9 +33,15 @@ def visit_path(path) return if excluded_path?(path) - if File.file?(path) + begin + stat = File.stat(path) + rescue Errno::ENOENT, Errno::EACCES + return + end + + if stat.file? visit_file(path) - elsif File.directory?(path) + elsif stat.directory? visit_directory(path) else # rubocop:disable Style/EmptyElse # Ignore aliases, sockets, etc. diff --git a/lib/spoom/sorbet/metrics/metrics_file_parser.rb b/lib/spoom/sorbet/metrics/metrics_file_parser.rb index 71fbd408..5a2ac1f8 100644 --- a/lib/spoom/sorbet/metrics/metrics_file_parser.rb +++ b/lib/spoom/sorbet/metrics/metrics_file_parser.rb @@ -10,6 +10,7 @@ module MetricsFileParser DEFAULT_PREFIX = "ruby_typer.unknown." class << self + # Raises if `path` doesn't point to a valid file that we have access to (see `File.read` for details) #: (String path, ?String prefix) -> Hash[String, Integer] def parse_file(path, prefix = DEFAULT_PREFIX) parse_string(File.read(path), prefix)