Class: Bundler::Spinel::Survey

Inherits:
Object
  • Object
show all
Defined in:
lib/bundler/spinel/survey.rb

Overview

Wholesale review: probe a large list of gems and aggregate the results. The point isn’t a pass/fail — it’s the *histogram of rejection reasons*, which directly prioritises what Spinel should support next (see RFC asks).

Embarrassingly parallel: each gem is an independent fetch + compile, both subprocess-bound (Open3 releases the GVL), so a thread pool scales across cores. Verdicts are cached in the ledger, so a survey is resumable and re-runnable; only ledger writes are serialised.

Constant Summary collapse

METAPROG =

Metaprogramming / reflection constructs Spinel treats as out of scope. Mirrors Probe::RISK_TOKENS’ reason vocabulary: whether a construct shows up as a static risk (‘send`) or as an unresolved call (`unresolved:send`), it’s bucketed as metaprog and kept out of the candidate-call list — the point of that list is calls Spinel could plausibly learn, not ones it deliberately won’t.

%w[
  eval instance_eval class_eval define_method method_missing
  respond_to_missing const_missing send public_send objectspace
  tracepoint binding
].freeze
REPORT_CALLS =

How many candidate calls to show inline in the report. The full ranked list always goes to candidates.tsv; 0 shows the whole list inline too.

Integer(ENV.fetch("SPINEL_REPORT_CALLS", "200"))

Instance Method Summary collapse

Constructor Details

#initialize(engine: Engine.new, ledger: Ledger.new, jobs: 4, refresh: false, skip_known: nil) ⇒ Survey

Returns a new instance of Survey.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/bundler/spinel/survey.rb', line 31

def initialize(engine: Engine.new, ledger: Ledger.new, jobs: 4, refresh: false, skip_known: nil)
  @engine = engine
  @ledger = ledger
  @jobs = jobs
  @refresh = refresh
  # Resume fast-path: when a gem already has *any* verdict at this rev,
  # short-circuit `probe_one` so we skip the per-gem `latest_version`
  # (a `gem list -r` HTTPS roundtrip × ~190k = hours of pure waste on a
  # restart). Defaults to !refresh — refresh means re-probe everything,
  # which by definition wants the fresh latest_version too.
  @skip_known = skip_known.nil? ? !refresh : skip_known
  @fetcher = GemFetcher.new
  @probe = Probe.new(@engine, @ledger)
  @mutex = Mutex.new
end

Instance Method Details

#candidates_tsv(names) ⇒ Object

The full ranked candidate-call list as TSV (‘counttcall`, header first) —the long-tail companion to the report’s top-N inline table. Written to candidates.tsv so the whole roadmap signal survives, not just the head.



91
92
93
94
95
# File 'lib/bundler/spinel/survey.rb', line 91

def candidates_tsv(names)
  _, _, reasons = aggregate(names)
  rows = candidate_calls(reasons).map { |call, c| "#{c}\t#{call}" }
  (["count\tcall"] + rows).join("\n") + "\n"
end

#report(names) ⇒ Object

Aggregate a markdown report from ledger verdicts for ‘names` at this rev.

Reads straight from the ledger — no network. The just-run probes already recorded a verdict (with its resolved version) per surveyed gem at this rev, so re-resolving each gem’s latest version online would only repeat work and serialise a 1k-name survey behind 1k ‘gem list -r` calls. We take the last current-rev entry per gem: append-only means a re-probe supersedes, and the survey probes a gem at one (latest) version per run.



83
84
85
86
# File 'lib/bundler/spinel/survey.rb', line 83

def report(names)
  n, counts, reasons = aggregate(names)
  render(n, counts, reasons)
end

#run(names, progress: $stderr) ⇒ Object

names: Array<String>. Probes each at its latest version (ledger-cached). Returns the Array<Ledger::Verdict> for the surveyed set.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/bundler/spinel/survey.rb', line 49

def run(names, progress: $stderr)
  @engine.ensure!
  queue = Queue.new
  names.each { |n| queue << n }
  results = []
  done = 0
  total = names.size

  workers = Array.new([@jobs, total].min) do
    Thread.new do
      until queue.empty?
        name = (queue.pop(true) rescue break)
        v = probe_one(name)
        @mutex.synchronize do
          results << v if v
          done += 1
          progress&.print("\r[survey] #{done}/#{total}  #{name.ljust(30)}")
        end
      end
    end
  end
  workers.each(&:join)
  progress&.puts("\r[survey] #{done}/#{total} done#{' ' * 30}")
  results.compact
end