Class: Rigor::Analysis::RunStats

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

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.

Returns:

  • (String)
"<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



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/rigor/analysis/run_stats.rb', line 32

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_classesInteger (readonly)

Returns the value of attribute gem_walk_classes.

Returns:

  • (Integer)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def gem_walk_classes
  @gem_walk_classes
end

#gem_walk_gemsInteger (readonly)

Returns the value of attribute gem_walk_gems.

Returns:

  • (Integer)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def gem_walk_gems
  @gem_walk_gems
end

#peak_rss_bytesInteger? (readonly)

Returns the value of attribute peak_rss_bytes.

Returns:

  • (Integer, nil)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def peak_rss_bytes
  @peak_rss_bytes
end

#rbs_attribution_availableBoolean (readonly)

Returns the value of attribute rbs_attribution_available.

Returns:

  • (Boolean)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def rbs_attribution_available
  @rbs_attribution_available
end

#rbs_classes_bundledInteger (readonly)

Returns the value of attribute rbs_classes_bundled.

Returns:

  • (Integer)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def rbs_classes_bundled
  @rbs_classes_bundled
end

#rbs_classes_project_sigInteger (readonly)

Returns the value of attribute rbs_classes_project_sig.

Returns:

  • (Integer)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def rbs_classes_project_sig
  @rbs_classes_project_sig
end

#rbs_classes_totalInteger (readonly)

Returns the value of attribute rbs_classes_total.

Returns:

  • (Integer)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def rbs_classes_total
  @rbs_classes_total
end

#target_filesInteger (readonly)

Returns the value of attribute target_files.

Returns:

  • (Integer)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

def target_files
  @target_files
end

#wall_secondsFloat (readonly)

Returns the value of attribute wall_seconds.

Returns:

  • (Float)


27
28
29
# File 'lib/rigor/analysis/run_stats.rb', line 27

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.

Parameters:

  • class_decl_paths: (Hash[String, String])

Returns:

  • (Boolean)


112
113
114
115
116
# File 'lib/rigor/analysis/run_stats.rb', line 112

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:) ⇒ [Integer, Integer]

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.

Parameters:

  • class_decl_paths: (Hash[String, String])
  • signature_paths: (Array[untyped])

Returns:

  • ([Integer, Integer])


97
98
99
100
101
102
103
104
105
106
107
# File 'lib/rigor/analysis/run_stats.rb', line 97

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_bytesInteger?

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.

Returns:

  • (Integer, nil)


53
54
55
56
57
58
59
60
61
# File 'lib/rigor/analysis/run_stats.rb', line 53

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



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

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



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

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: "") ⇒ void

This method returns an undefined value.

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.

Parameters:

  • out (Object)
  • prefix: (String) (defaults to: "")


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/rigor/analysis/run_stats.rb', line 121

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_hHash[Symbol, untyped]

Returns:

  • (Hash[Symbol, untyped])


152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/rigor/analysis/run_stats.rb', line 152

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