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
162
163
164
165
166
167
168
169
170
171
172
173
# 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_classes_total.zero?
    # A normal run always loads the bundled core+stdlib RBS (~1300+
    # classes), so zero means the environment failed to build (most
    # often a duplicate declaration in `signature_paths:`) and fell
    # back to empty — type coverage is then near-useless but the run
    # still "succeeds". Surface it loudly so a broken setup is not
    # read as a clean analysis (the 20260620 field trial: redmine
    # would otherwise wire a 0-coverage check into CI).
    out.puts("#{prefix}  WARNING: the RBS environment is empty — it failed to build or loaded no")
    out.puts("#{prefix}           signatures, so type coverage is severely limited (most diagnostics")
    out.puts("#{prefix}           and coverage cannot fire). Usually a duplicate declaration in")
    out.puts("#{prefix}           `signature_paths:` — fix it and re-run; the rigor-doctor skill helps.")
  elsif @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



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/rigor/analysis/run_stats.rb', line 175

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