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 +4 -4
- data/README.md +17 -9
- data/lib/vicary/corpus.rb +17 -7
- data/lib/vicary/latency_baseline.rb +132 -74
- data/lib/vicary/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86411ff6192dfe36ba02d8159d4000bc55814d5eb8dd782713f373199d0bef98
|
|
4
|
+
data.tar.gz: 85ad752a1663010614adec4820efb015e0ee9b63ad74bf048f8869760f438246
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
identical to Python and TypeScript
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
#
|
|
449
|
-
#
|
|
450
|
-
#
|
|
451
|
-
#
|
|
452
|
-
#
|
|
453
|
-
#
|
|
454
|
-
|
|
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
|
|
10
|
-
#
|
|
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
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
|
|
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, :
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
127
|
+
# Compare the pair measured on this machine, for this port.
|
|
71
128
|
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
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
|
-
|
|
77
|
-
doc = load(dir)
|
|
78
|
-
tolerance = (doc
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
145
|
+
record, why = load_pair(pair_path)
|
|
146
|
+
return declined.call(why || "no paired measurement") if record.nil?
|
|
88
147
|
|
|
89
|
-
|
|
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
|
-
"
|
|
95
|
-
"
|
|
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
|
|
154
|
+
unless record["implementation"] == implementation
|
|
99
155
|
return declined.call(
|
|
100
|
-
"#{
|
|
101
|
-
"
|
|
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
|
-
"
|
|
109
|
-
"
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
"
|
|
117
|
-
"
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
"
|
|
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,
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
153
|
-
"against a %d%% bar",
|
|
154
|
-
c.measured_ms, c.
|
|
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
|
data/lib/vicary/version.rb
CHANGED
|
@@ -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.
|
|
9
|
+
VERSION = "0.2.5"
|
|
10
10
|
end
|