vicary 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 585c77607d6c331c3cebe14a5da44463a0231f7c3a15e5c1ba68f79d8b9f588b
4
- data.tar.gz: 883c07a66721ce9c67fb0cfe1f1ea16ce486decd6be62164b1ebbbf53fec9bf0
3
+ metadata.gz: 86411ff6192dfe36ba02d8159d4000bc55814d5eb8dd782713f373199d0bef98
4
+ data.tar.gz: 85ad752a1663010614adec4820efb015e0ee9b63ad74bf048f8869760f438246
5
5
  SHA512:
6
- metadata.gz: 060ff017a7a0de4ede9652d3e87ddd14083022725ce225739758ce0c7882fe953d6bfaf27ccf48f4fa8b73b480fce98f623dfaf02406944e19e841763b42c022
7
- data.tar.gz: 0b80b3df83f7399b5118d67abbf2e8c1da589b0ec902d7f2cc04a60be8498e41df9ebdbc569af9c57b487025994bfd5715a58b0d0c2a9a6994e06271fa2e2dcd
6
+ metadata.gz: c2197ab3c0b5759a94bd8027e8c4096c886b3186e37be9cd06e562710db94677450cce228c3a9c106156a43857591a1c0c18fd3dd8eddba617c644fa6f3bee6c
7
+ data.tar.gz: c1a2612a43896411f0462732d37641fb864fb84dc48eef2c81d96e182295bb69c0b4ad8be6c1fa04bc53368ba8a906669bd7e8e58735abc5fd034b7e7821f627
data/README.md CHANGED
@@ -47,15 +47,23 @@ zip reader and a binary read parsed as CSV yields zero rows — a *lower* exposu
47
47
  than the truth, and the wrong direction to fail in silently. The shipped table is
48
48
  gzip, which `zlib` reads, so that hazard does not arise on the default path.
49
49
 
50
- The last three need an essay corpus no package here ships, and this gem measures
51
- them once `VICARY_EVAL_CORPUS_TSV` points at one: 100% carrier recall (29/29
52
- held-out REDACT spans) and 0.60 over-fired spans per essay (15 across 25),
53
- identical to Python and TypeScript, plus its own latency p95 of 8.8–9.9 ms. **That
54
- last one is worth knowing about**: the bar is ≤ 10 ms, and this port runs nearest
55
- it of the three — roughly 4× Python — so the latency gate is a live constraint
56
- here rather than a formality. The carrier essays are built from offsets recorded
57
- in `conformance/carrier.json` rather than from a reimplementation of Python's
58
- RNG, and the suite asserts their sha256.
50
+ Two of the last three read the corpus the repository now ships in
51
+ `conformance/corpora/`, so they measure on a bare checkout with no environment
52
+ set: 100% carrier recall and 8.150 over-fired spans per essay against a ≤ 8.15
53
+ bar, identical to Python and TypeScript. `VICARY_EVAL_CORPUS_TSV` is an override
54
+ for a different corpus, not a requirement.
55
+
56
+ The ninth is latency, and **this port's absolute figure no longer constrains it**.
57
+ The gate was a 10 ms bar, which this port ran nearest of the three; it is now a
58
+ ratio against the last release timed on the same machine, held to ≤ +8%. That
59
+ change matters most here: across three CPU models this port's absolute median
60
+ spreads **31.8%**, the same axis that made the old bar a coin flip, while its
61
+ ratio spreads 0.36 pp. Measured, the ratio holds σ 0.46% — the *widest* margin of
62
+ the three ports, where the absolute figure gave it the narrowest.
63
+
64
+ The carrier essays are built from offsets recorded in `conformance/carrier.json`
65
+ rather than from a reimplementation of Python's RNG, and the suite asserts their
66
+ sha256.
59
67
 
60
68
  The last two need the reference interpreter — run `just py-setup` from the
61
69
  repository root first.
data/lib/vicary/corpus.rb CHANGED
@@ -445,13 +445,23 @@ module Vicary
445
445
  over_fire = 0
446
446
  rewrites = 0
447
447
 
448
- # Load the gazetteer before the clock starts. It is a one-time ~207 ms
449
- # cost in this port, and whichever essay happens to be first pays all of
450
- # it: at n=25 that single sample lands at or above p95 and sets the
451
- # gate's answer by itself — 14.3 ms cold against 7.6 ms warm, on a 10 ms
452
- # bar. The number the gate claims is essay-length redaction latency, not
453
- # process startup. Excluded in all three ports alike.
454
- yield(cases.first.base[0, 200], identity) unless cases.empty?
448
+ # Warm up before the clock starts, over the WHOLE corpus rather than one
449
+ # 200-char call. Two costs are being excluded, and the second one is why
450
+ # this grew.
451
+ #
452
+ # The gazetteer load is a one-time ~207 ms cost in this port, and
453
+ # whichever essay happens to be first pays all of it — 14.3 ms cold
454
+ # against 7.6 ms warm.
455
+ #
456
+ # The second belongs to TypeScript, where V8 tiers the redaction path up
457
+ # over roughly the first four essays and runs them at about twice their
458
+ # steady-state cost. This port barely moves under a full warmup and does
459
+ # it anyway: the three ports measure identically or the gate is three
460
+ # different gates.
461
+ cases.each do |kase|
462
+ yield(kase.text, identity)
463
+ yield(kase.base, identity)
464
+ end
455
465
 
456
466
  cases.each do |kase|
457
467
  # The median of LATENCY_REPEATS, not one sample — see that constant.
@@ -6,37 +6,77 @@ require "pathname"
6
6
  module Vicary
7
7
  # Is this build slower than the last release, and is that a fair question here?
8
8
  #
9
- # The latency gate used to hold an absolute number 10 ms which is a claim
10
- # about the machine as much as about the code. It passed on a laptop and failed
11
- # on the CI runner enforcing it, so v0.2.3 published to PyPI and npm and was
12
- # refused by RubyGems on the same commit. This gem is the one that caught it.
9
+ # The gate has asked this three ways. The first two are worth keeping in view,
10
+ # because each looked correct until it decided a release.
13
11
  #
14
- # What replaced it asks a relative question: is this port slower than it was at
15
- # the last release, by more than the tolerance. That only means something
16
- # between measurements taken on comparable hardware, so this module's real work
17
- # is REFUSING to compare when they are not — a machine difference reported as a
18
- # code regression is worse than no gate, because it trains the reader to ignore
19
- # it.
12
+ # **An absolute bar 10 ms.** A claim about the machine as much as about the
13
+ # code. It passed on a laptop and failed on the CI runner enforcing it, so
14
+ # v0.2.3 published to PyPI and npm and was refused by RubyGems on the same
15
+ # commit. This gem is the one that caught it.
20
16
  #
21
- # This port reaches its own verdict from the shared file. It does not read
17
+ # **A stored baseline** record each release's number and compare the next run
18
+ # against it, refusing unless the run claims the profile the baseline was
19
+ # recorded on. Better, and still wrong, for a reason no estimator fixes: the
20
+ # profile `github-ubuntu-latest` is not a machine. Thirty-six processes across
21
+ # six runners per port, on identical code, spread 67% in THIS port — 6.53 ms on
22
+ # an Intel Xeon 6973P-C against 10.63 ms on an EPYC 7763 — 26% in Python and
23
+ # 21% in TypeScript, against an 8% bar. One probe run drew five CPU models from
24
+ # that one label, and two runners of the same model still differed by 26%.
25
+ #
26
+ # **A pair, measured here.** The previous release's code and this checkout,
27
+ # measured on the SAME machine, interleaved and counterbalanced, by
28
+ # `tools/latency_pair.py`. Every property of the machine is common to both
29
+ # sides and cancels; what is left is within-process noise, 1.7% in this port.
30
+ #
31
+ # Which leaves this module the job it has always had: REFUSING to compare when
32
+ # the two sides would not be like for like. What changed is that the refusals
33
+ # are about the pair record — is there one, is it this port's, was it measured
34
+ # on these essays, was it measured for this commit — rather than about the
35
+ # profile of a machine somewhere else.
36
+ #
37
+ # This port reaches its own verdict from the shared record. It does not read
22
38
  # Python's answer.
23
39
  module LatencyBaseline
24
- BASELINE_FILENAME = "latency_baseline.json"
25
-
26
- # Set by CI on the one matrix entry whose language version matches the
27
- # recorded profile. Absent everywhere else on purpose: a developer's laptop
28
- # measures the same commit two to three times faster than the runner, and
29
- # comparing that against a runner baseline reports a phantom improvement.
30
- PROFILE_ENV_VAR = "VICARY_LATENCY_PROFILE"
40
+ # The tolerance and the protocol, in the repository. Not a measurement:
41
+ # nothing is recorded at release time any more, because the comparison point
42
+ # is the previous release's *code*, which the repository already has.
43
+ SPEC_FILENAME = "latency_baseline.json"
44
+
45
+ # Where `tools/latency_pair.py` left the paired measurement. Set by CI in the
46
+ # same job, seconds before the gate runs. Absent on a laptop unless the
47
+ # harness was run there by hand, and that absence is a refusal to compare
48
+ # rather than a pass — measuring one side of a comparison is not a gate.
49
+ PAIR_ENV_VAR = "VICARY_LATENCY_PAIR"
50
+
51
+ # What this reader understands. A record from a future shape is refused
52
+ # rather than half-read: a partly-understood record still yields a number,
53
+ # and a number is exactly what must not be invented here.
54
+ PAIR_DOCUMENT_VERSION = 1
31
55
 
32
56
  IMPLEMENTATION = "ruby"
33
57
 
58
+ # The bar, chosen rather than derived — 8% is what a reviewer is willing to
59
+ # call a regression. What the noise decides is whether the bar is USABLE,
60
+ # and it is: the gate statistic holds **sigma 0.46%** in this port (95% CI
61
+ # 0.34-0.72%, sixteen runs across eight CI runners, fixed head and tag),
62
+ # putting 8% at 17.2 sigma — the widest margin of the three. It was about a
63
+ # third of a sigma under the stored baseline, which is how that one red-lit
64
+ # `main` on unchanged code, and how it refused this port's 0.2.3 while the
65
+ # other two took the same commit.
66
+ #
67
+ # This is also the port where the pairing earns the most. Across three CPU
68
+ # models its absolute figure spreads **31.8%** — the same axis, and nearly
69
+ # the same size, as the 67% that killed the stored baseline — while its ratio
70
+ # spreads 0.36 pp. Same runs, same data. See `tools/latency_pair.py`.
71
+ #
72
+ # It does not catch drift: +5% a release passes every time and compounds.
73
+ # That is deliberate — this gate is for the step change, not the trend.
34
74
  DEFAULT_TOLERANCE_PCT = 8.0
35
75
 
36
76
  # The gate's answer, and — when it declines — why.
37
77
  Comparison = Struct.new(
38
- :measured_ms, :baseline_ms, :regression_pct, :tolerance_pct,
39
- :comparable, :reason,
78
+ :measured_ms, :previous_ms, :current_ms, :regression_pct, :tolerance_pct,
79
+ :against, :comparable, :reason,
40
80
  keyword_init: true
41
81
  ) do
42
82
  def holds?
@@ -47,97 +87,114 @@ module Vicary
47
87
  end
48
88
 
49
89
  class << self
50
- def baseline_path(dir = nil)
90
+ def spec_path(dir = nil)
51
91
  root = dir || Conformance.directory
52
92
  return nil if root.nil?
53
93
 
54
- path = Pathname.new(root).join(BASELINE_FILENAME)
94
+ path = Pathname.new(root).join(SPEC_FILENAME)
55
95
  path.exist? ? path : nil
56
96
  end
57
97
 
58
98
  def load(dir = nil)
59
- path = baseline_path(dir)
99
+ path = spec_path(dir)
60
100
  return nil if path.nil?
61
101
 
62
102
  JSON.parse(path.read)
63
103
  end
64
104
 
65
- # `major.minor` of the running Ruby, matching how the profile records it.
66
- def language_version
67
- RUBY_VERSION.split(".").first(2).join(".")
105
+ # The paired measurement, or why there is none to read.
106
+ #
107
+ # An unreadable file and an absent one stay distinguishable: the first is a
108
+ # broken harness and the second is an ordinary laptop, and they should not
109
+ # report the same thing.
110
+ def load_pair(path = nil)
111
+ given = (path || ENV[PAIR_ENV_VAR] || "").strip
112
+ if given.empty?
113
+ return [nil,
114
+ "#{PAIR_ENV_VAR} is unset, so no paired measurement was taken on " \
115
+ "this machine; the gate compares this build against the last " \
116
+ "release measured HERE, and one side of a comparison is not a gate"]
117
+ end
118
+ return [nil, "#{PAIR_ENV_VAR}=#{given.inspect} does not exist"] unless File.exist?(given)
119
+
120
+ begin
121
+ [JSON.parse(File.read(given)), nil]
122
+ rescue StandardError => e
123
+ [nil, "the pair record at #{given} could not be read: #{e.message}"]
124
+ end
68
125
  end
69
126
 
70
- # Compare +measured_ms+ against the recorded baseline for this port.
127
+ # Compare the pair measured on this machine, for this port.
71
128
  #
72
- # Every reason below is a refusal to compare, not a failure to measure: the
73
- # number was measured either way and is reported either way. What is
74
- # withheld is the verdict, because the two sides would not be like for like.
129
+ # +measured_ms+ is this process's own figure. It is reported either way and
130
+ # it is never the verdict: the verdict comes from the two numbers in the
131
+ # pair record, taken back to back on one machine. Mixing this process's
132
+ # measurement with the pair's other side would reintroduce exactly the
133
+ # machine difference the pair exists to cancel.
75
134
  def compare(measured_ms, corpus_id, dir: nil, implementation: IMPLEMENTATION,
76
- observed_language_version: nil, profile_env: nil)
77
- doc = load(dir)
78
- tolerance = (doc && doc["tolerance_pct"] || DEFAULT_TOLERANCE_PCT).to_f
79
- lang = observed_language_version || language_version
80
-
81
- declined = lambda do |reason, baseline_ms = nil|
82
- Comparison.new(measured_ms: measured_ms, baseline_ms: baseline_ms,
83
- regression_pct: nil, tolerance_pct: tolerance,
135
+ pair_path: nil, building_sha: nil)
136
+ doc = load(dir) || {}
137
+ tolerance = (doc["tolerance_pct"] || DEFAULT_TOLERANCE_PCT).to_f
138
+
139
+ declined = lambda do |reason|
140
+ Comparison.new(measured_ms: measured_ms, previous_ms: nil, current_ms: nil,
141
+ regression_pct: nil, tolerance_pct: tolerance, against: nil,
84
142
  comparable: false, reason: reason)
85
143
  end
86
144
 
87
- return declined.call("no #{BASELINE_FILENAME} in this checkout") if doc.nil?
145
+ record, why = load_pair(pair_path)
146
+ return declined.call(why || "no paired measurement") if record.nil?
88
147
 
89
- profile = doc["profile"] || {}
90
- want_profile = profile["id"]
91
- have_profile = (profile_env || ENV[PROFILE_ENV_VAR] || "").strip
92
- if have_profile.empty?
148
+ unless record["document_version"] == PAIR_DOCUMENT_VERSION
93
149
  return declined.call(
94
- "#{PROFILE_ENV_VAR} is unset, so this machine does not claim to be " \
95
- "#{want_profile.inspect}; the baseline was recorded there"
150
+ "the pair record is document_version #{record['document_version']} " \
151
+ "and this reader knows #{PAIR_DOCUMENT_VERSION}"
96
152
  )
97
153
  end
98
- unless have_profile == want_profile
154
+ unless record["implementation"] == implementation
99
155
  return declined.call(
100
- "#{PROFILE_ENV_VAR}=#{have_profile.inspect} but the baseline was " \
101
- "recorded on #{want_profile.inspect}"
156
+ "the pair record measures #{record['implementation'].inspect}, " \
157
+ "not #{implementation.inspect}"
102
158
  )
103
159
  end
104
-
105
- want_lang = (profile["language_versions"] || {})[implementation]
106
- if !want_lang.nil? && want_lang.to_s != lang
160
+ unless record["corpus"] == corpus_id
107
161
  return declined.call(
108
- "#{implementation} #{lang} is not the #{want_lang} the baseline was " \
109
- "recorded on; interpreter versions differ by more than the bar"
162
+ "the pair was measured on corpus #{record['corpus'].inspect} and this " \
163
+ "run is #{corpus_id.inspect}; latency scales with essay length"
110
164
  )
111
165
  end
112
166
 
113
- want_corpus = doc["corpus"]
114
- if !want_corpus.nil? && want_corpus != corpus_id
167
+ # Only where there is something to check against. `GITHUB_SHA` names the
168
+ # commit the job is building, so a record left over from an earlier
169
+ # commit is caught here rather than being read as this build's verdict.
170
+ # Locally there is no such witness and no such risk: the harness is run
171
+ # by hand, minutes before, on the tree in front of you.
172
+ building = (building_sha || ENV["GITHUB_SHA"] || "").strip
173
+ head = record["head_sha"].to_s
174
+ if !building.empty? && !head.empty? && building != head
115
175
  return declined.call(
116
- "corpus #{corpus_id.inspect} is not the #{want_corpus.inspect} the " \
117
- "baseline was recorded on; latency scales with essay length"
176
+ "the pair was measured for commit #{head[0, 12]} and this job is " \
177
+ "building #{building[0, 12]}; the record is stale"
118
178
  )
119
179
  end
120
180
 
121
- entry = (doc["implementations"] || {})[implementation] || {}
122
- recorded = entry["pooled_median_ms"]
123
- if recorded.nil?
124
- return declined.call(
125
- "no baseline recorded for #{implementation} yet — the next release " \
126
- "records one"
127
- )
181
+ previous = record["previous_ms"]
182
+ current = record["current_ms"]
183
+ unless previous.is_a?(Numeric) && current.is_a?(Numeric)
184
+ return declined.call("the pair record carries no pair of measurements")
128
185
  end
129
-
130
- recorded = recorded.to_f
131
- if recorded <= 0
186
+ if previous <= 0
132
187
  return declined.call(
133
- "recorded baseline for #{implementation} is not positive", recorded
188
+ "the previous release measured #{previous} ms, which is not positive"
134
189
  )
135
190
  end
136
191
 
137
192
  Comparison.new(
138
- measured_ms: measured_ms, baseline_ms: recorded,
139
- regression_pct: (measured_ms / recorded - 1.0) * 100.0,
140
- tolerance_pct: tolerance, comparable: true, reason: nil
193
+ measured_ms: measured_ms, previous_ms: previous.to_f,
194
+ current_ms: current.to_f,
195
+ regression_pct: (current.to_f / previous.to_f - 1.0) * 100.0,
196
+ tolerance_pct: tolerance, against: (record["against"] || {})["ref"],
197
+ comparable: true, reason: nil
141
198
  )
142
199
  end
143
200
 
@@ -149,9 +206,10 @@ module Vicary
149
206
  end
150
207
 
151
208
  sign = c.regression_pct >= 0 ? "+" : ""
152
- format("latency %.3f ms vs %.3f ms at the last release %s%.2f%% " \
153
- "against a %d%% bar",
154
- c.measured_ms, c.baseline_ms, sign, c.regression_pct, c.tolerance_pct)
209
+ format("latency %.3f ms here; paired on this machine, %.3f ms against " \
210
+ "%s's %.3f ms — %s%.2f%% against a %d%% bar",
211
+ c.measured_ms, c.current_ms, c.against || "the last release",
212
+ c.previous_ms, sign, c.regression_pct, c.tolerance_pct)
155
213
  end
156
214
 
157
215
  # The keyword arguments Gates.measure wants. Returns the *detail* rather
@@ -6,5 +6,5 @@ module Vicary
6
6
  # Shared across all three front doors on purpose: one detector, one number. A
7
7
  # gem 0.3.0 that corresponds to nothing on PyPI cannot be reasoned about, and
8
8
  # the parity claim is between *versions*, not between package names.
9
- VERSION = "0.2.4"
9
+ VERSION = "0.2.5"
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vicary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Thomas