Class: Rigor::Analysis::RunStats

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/analysis/run_stats.rb

Overview

End-of-run telemetry for the ‘rigor check` CLI’s ‘–stats` output. Captures four cheap-to-measure groups:

  • **Check targets** — the Ruby files the analyser actually walks for diagnostics (‘expand_paths` output).

  • **Type universe** — RBS class/module declarations the analyser had visibility of, broken down by source: ‘project_sig` (declarations whose source file lives under the configured `signature_paths`) vs `bundled` (RBS core, stdlib libraries, gem-bundled RBS — everything outside the project’s own ‘sig/` tree).

  • **Gem source-walk** — the ADR-10 ‘dependencies.source_inference` catalogue. Reports the class count and the number of opt-in gems contributing.

  • Process — wall-clock seconds + peak resident set size.

The split between “check targets” and “type universe” makes explicit that the analyser’s diagnostic surface is bounded by the user-controlled ‘paths:` configuration; the (typically much larger) RBS class universe is symbol-discovery, not a diagnostic surface.

Stats collection is intentionally cheap: wall + RSS are single syscalls, target file count is already in ‘expand_paths`, gem source-walk uses `Index#class_to_gem.size`, and the RBS class breakdown walks `class_decl_paths` (a frozen `Hash<String, String>` populated once per environment by the RBS loader; ~1000-2000 entries × one `String#start_with?`).

Constant Summary collapse

CACHED_SENTINEL =

Source-attribution sentinel produced by ‘RBS::Environment` entries restored from a cached blob (Marshal-loaded `RBS::Environment` loses real file-path attribution; every buffer reports `“<cached>”`). When every entry carries this sentinel the partition_classes routine returns `[0, total]` AND `attribution_available: false`, which the format routine consumes to suppress the misleading breakdown row.

"<cached>"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(wall_seconds:, peak_rss_bytes:, target_files:, rbs_classes_total:, rbs_classes_project_sig:, rbs_classes_bundled:, gem_walk_classes:, gem_walk_gems:, rbs_attribution_available: true) ⇒ RunStats

rubocop:disable Metrics/ParameterLists



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/rigor/analysis/run_stats.rb', line 42

def initialize(wall_seconds:, peak_rss_bytes:, # rubocop:disable Metrics/ParameterLists
               target_files:,
               rbs_classes_total:, rbs_classes_project_sig:, rbs_classes_bundled:,
               gem_walk_classes:, gem_walk_gems:,
               rbs_attribution_available: true)
  @wall_seconds = wall_seconds
  @peak_rss_bytes = peak_rss_bytes
  @target_files = target_files
  @rbs_classes_total = rbs_classes_total
  @rbs_classes_project_sig = rbs_classes_project_sig
  @rbs_classes_bundled = rbs_classes_bundled
  @gem_walk_classes = gem_walk_classes
  @gem_walk_gems = gem_walk_gems
  @rbs_attribution_available = rbs_attribution_available
  freeze
end

Instance Attribute Details

#gem_walk_classesObject (readonly)

Returns the value of attribute gem_walk_classes.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def gem_walk_classes
  @gem_walk_classes
end

#gem_walk_gemsObject (readonly)

Returns the value of attribute gem_walk_gems.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def gem_walk_gems
  @gem_walk_gems
end

#peak_rss_bytesObject (readonly)

Returns the value of attribute peak_rss_bytes.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def peak_rss_bytes
  @peak_rss_bytes
end

#rbs_attribution_availableObject (readonly)

Returns the value of attribute rbs_attribution_available.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def rbs_attribution_available
  @rbs_attribution_available
end

#rbs_classes_bundledObject (readonly)

Returns the value of attribute rbs_classes_bundled.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def rbs_classes_bundled
  @rbs_classes_bundled
end

#rbs_classes_project_sigObject (readonly)

Returns the value of attribute rbs_classes_project_sig.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def rbs_classes_project_sig
  @rbs_classes_project_sig
end

#rbs_classes_totalObject (readonly)

Returns the value of attribute rbs_classes_total.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def rbs_classes_total
  @rbs_classes_total
end

#target_filesObject (readonly)

Returns the value of attribute target_files.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def target_files
  @target_files
end

#wall_secondsObject (readonly)

Returns the value of attribute wall_seconds.



37
38
39
# File 'lib/rigor/analysis/run_stats.rb', line 37

def wall_seconds
  @wall_seconds
end

Class Method Details

.attribution_available?(class_decl_paths:) ⇒ Boolean

True when at least one entry in ‘class_decl_paths` carries a real source file path (i.e. not the cached-sentinel marker). Used by callers to decide whether the `project_sig` / `bundled` split is meaningful.

Returns:

  • (Boolean)


131
132
133
134
135
# File 'lib/rigor/analysis/run_stats.rb', line 131

def self.attribution_available?(class_decl_paths:)
  return false if class_decl_paths.empty?

  class_decl_paths.each_value.any? { |path| path != CACHED_SENTINEL }
end

.partition_classes(class_decl_paths:, signature_paths:) ⇒ Object

Computes ‘(project_sig, bundled)` counts from a frozen `Hash<class_name => source_path>` snapshot and the configured `signature_paths`. `project_sig` is the count of classes whose source path begins with any of the signature path prefixes (after expansion to absolute paths); `bundled` is the remainder.



115
116
117
118
119
120
121
122
123
124
125
# File 'lib/rigor/analysis/run_stats.rb', line 115

def self.partition_classes(class_decl_paths:, signature_paths:)
  prefixes = Array(signature_paths).map { |p| File.expand_path(p.to_s) }
  return [0, class_decl_paths.size] if prefixes.empty?

  project = 0
  class_decl_paths.each_value do |path|
    expanded = File.expand_path(path)
    project += 1 if prefixes.any? { |prefix| expanded.start_with?("#{prefix}/") || expanded == prefix }
  end
  [project, class_decl_paths.size - project]
end

.peak_rss_bytesObject

Reports the process’s resident set size in bytes. Source ordering: ‘/proc/self/status` (Linux — reads `VmHWM:`, the peak RSS the kernel records) first; otherwise `ps -o rss= -p <pid>` (macOS / BSD — reports CURRENT RSS, the closest universally-available proxy). Returns nil when neither route works so the formatter can render `unavailable` instead of misleading zero.



66
67
68
69
70
71
72
73
74
# File 'lib/rigor/analysis/run_stats.rb', line 66

def self.peak_rss_bytes
  from_proc = read_vmhwm_from_proc
  return from_proc unless from_proc.nil?

  from_ps = read_rss_via_ps
  return from_ps unless from_ps.nil?

  nil
end

.read_rss_via_psObject



90
91
92
93
94
95
96
97
# File 'lib/rigor/analysis/run_stats.rb', line 90

def self.read_rss_via_ps
  out = `ps -o rss= -p #{Process.pid} 2>/dev/null`.strip
  return nil if out.empty?

  Integer(out) * 1024
rescue StandardError
  nil
end

.read_vmhwm_from_procObject



76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/rigor/analysis/run_stats.rb', line 76

def self.read_vmhwm_from_proc
  return nil unless File.readable?("/proc/self/status")

  File.foreach("/proc/self/status") do |line|
    next unless line.start_with?("VmHWM:")

    kb_token = line.split.find { |token| token.match?(/\A\d+\z/) }
    return Integer(kb_token) * 1024 if kb_token
  end
  nil
rescue StandardError
  nil
end

Instance Method Details

#format(out, prefix: "") ⇒ Object

Writes a human-facing rendering of the stats to ‘out` (typically `$stderr` from the CLI). Format is intentionally plain text — JSON consumers should parse the structured output of `rigor check –format=json` and consult `stats` there.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/rigor/analysis/run_stats.rb', line 142

def format(out, prefix: "")
  out.puts("#{prefix}Check targets")
  out.puts("#{prefix}  Ruby source files: #{@target_files}")
  out.puts("#{prefix}Type universe (symbol discovery; not analyzed for diagnostics)")
  out.puts("#{prefix}  RBS classes available: #{@rbs_classes_total}")
  if @rbs_attribution_available
    out.puts("#{prefix}    project sig/:        #{@rbs_classes_project_sig}")
    out.puts("#{prefix}    bundled (core+stdlib+gems): #{@rbs_classes_bundled}")
  elsif @rbs_classes_total.positive?
    out.puts("#{prefix}    (source attribution unavailable on cache-hit runs; --no-cache surfaces it)")
  end
  if @gem_walk_gems.positive?
    out.puts("#{prefix}  Gem source-walk classes: #{@gem_walk_classes} " \
             "(across #{@gem_walk_gems} #{@gem_walk_gems == 1 ? 'gem' : 'gems'} " \
             "via dependencies.source_inference)")
  end
  out.puts("#{prefix}Process")
  out.puts("#{prefix}  Wall time:   #{Kernel.format('%.2fs', @wall_seconds)}")
  out.puts("#{prefix}  Memory peak: #{format_bytes(@peak_rss_bytes)}")
end

#to_hObject



163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/rigor/analysis/run_stats.rb', line 163

def to_h
  {
    target_files: @target_files,
    rbs_classes_total: @rbs_classes_total,
    rbs_classes_project_sig: @rbs_classes_project_sig,
    rbs_classes_bundled: @rbs_classes_bundled,
    rbs_attribution_available: @rbs_attribution_available,
    gem_walk_classes: @gem_walk_classes,
    gem_walk_gems: @gem_walk_gems,
    wall_seconds: @wall_seconds,
    peak_rss_bytes: @peak_rss_bytes
  }
end