haveapi 0.30.1 → 0.31.0
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/.gitignore +1 -0
- data/Gemfile +1 -0
- data/benchmarks/Gemfile +30 -0
- data/benchmarks/Gemfile.lock +296 -0
- data/benchmarks/documentation.rb +277 -0
- data/benchmarks/endpoints.rb +786 -0
- data/benchmarks/performance_budgets.json +1354 -0
- data/benchmarks/performance_compare.rb +678 -0
- data/benchmarks/shell.nix +21 -0
- data/doc/client-examples.md +179 -26
- data/doc/performance.md +310 -0
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/client_examples/ts_generated_client.rb +36 -0
- data/lib/haveapi/documentation_clients.rb +49 -6
- data/lib/haveapi/documentation_context.rb +42 -12
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- data/spec/client_examples/context_performance_spec.rb +72 -0
- data/spec/client_examples/context_spec.rb +206 -0
- data/spec/client_examples/profile_mount_spec.rb +45 -0
- data/spec/client_examples/ts_generated_client_spec.rb +53 -0
- data/spec/client_examples/typescript_profiles_spec.rb +113 -0
- data/spec/documentation/client_examples_spec.rb +2 -2
- data/spec/documentation/performance_spec.rb +79 -0
- data/spec/performance/comparator_spec.rb +472 -0
- data/spec/performance/endpoint_contract_spec.rb +226 -0
- data/spec/performance/endpoint_scaling_spec.rb +82 -0
- data/spec/performance/event_baseline_spec.rb +407 -0
- data/spec/performance/json_activation_spec.rb +151 -0
- data/spec/performance/release_lineage_spec.rb +470 -0
- data/spec/performance/runner_spec.rb +610 -0
- data/spec/performance/sqlite_spec.rb +194 -0
- data/spec/performance/support/live_worker.rb +44 -0
- data/spec/performance/support/samples.rb +72 -0
- data/test_support/documentation_performance_fixture.rb +106 -0
- data/test_support/performance_endpoint_fixture.rb +350 -0
- data/test_support/performance_sqlite_fixture.rb +384 -0
- metadata +29 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
let
|
|
2
|
+
pkgs = import (builtins.fetchTree {
|
|
3
|
+
type = "github";
|
|
4
|
+
owner = "NixOS";
|
|
5
|
+
repo = "nixpkgs";
|
|
6
|
+
rev = "02c087f3fe673c9e317f206a627cb6b910336dbe";
|
|
7
|
+
narHash = "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=";
|
|
8
|
+
}) { };
|
|
9
|
+
in
|
|
10
|
+
pkgs.mkShell {
|
|
11
|
+
name = "haveapi-benchmarks";
|
|
12
|
+
packages = with pkgs; [ ruby_3_4 git gnumake pkg-config openssl libffi ];
|
|
13
|
+
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ];
|
|
14
|
+
shellHook = ''
|
|
15
|
+
for name in ''${!BUNDLE@}; do unset "$name"; done
|
|
16
|
+
unset RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS
|
|
17
|
+
export GEM_HOME="''${HAVEAPI_BENCHMARK_GEM_HOME:-${toString ./.}/.gems}"
|
|
18
|
+
export GEM_PATH="$GEM_HOME:${pkgs.ruby_3_4}/lib/ruby/gems/3.4.0"
|
|
19
|
+
export PATH="$GEM_HOME/bin:$PATH"
|
|
20
|
+
'';
|
|
21
|
+
}
|
data/doc/client-examples.md
CHANGED
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
The Ruby server renders examples from the current user's API description.
|
|
4
4
|
JavaScript and TypeScript examples cover callback and async clients; Python
|
|
5
|
-
examples cover sync and async clients. These clients need no server
|
|
5
|
+
examples cover sync and async clients. These clients need no server
|
|
6
|
+
configuration.
|
|
6
7
|
Ruby, PHP, CLI, curl and HTTP examples are also available. A sample
|
|
7
8
|
that a client cannot represent shows a limitation instead of dropping input or
|
|
8
9
|
inventing a successful result.
|
|
9
10
|
|
|
10
|
-
Go and
|
|
11
|
-
are independent of HaveAPI and the API version. Without a
|
|
11
|
+
Go, Rust and generated TypeScript tabs require a profile for a generated SDK.
|
|
12
|
+
Their package versions are independent of HaveAPI and the API version. Without a
|
|
13
|
+
profile, the server
|
|
12
14
|
hides those clients from the overview and all initialization, authentication and
|
|
13
15
|
action panels. Install the matching generator gem only when configuring its
|
|
14
|
-
profile; ordinary servers do not depend on
|
|
16
|
+
profile; ordinary servers do not depend on these generators.
|
|
15
17
|
|
|
16
18
|
## Configure a generated SDK
|
|
17
19
|
|
|
18
|
-
Register profiles before mounting the server. For an SDK that matches the
|
|
20
|
+
Register profiles before mounting the server. For an SDK that matches the
|
|
21
|
+
current
|
|
19
22
|
API, configure its package identity:
|
|
20
23
|
|
|
21
24
|
```ruby
|
|
@@ -25,25 +28,34 @@ api.documentation_clients('1.0',
|
|
|
25
28
|
},
|
|
26
29
|
rust: {
|
|
27
30
|
crate_name: 'widget-client'
|
|
31
|
+
},
|
|
32
|
+
typescript: {
|
|
33
|
+
package_name: '@example/widget-client'
|
|
28
34
|
}
|
|
29
35
|
)
|
|
30
36
|
api.mount('/api/')
|
|
31
37
|
```
|
|
32
38
|
|
|
33
39
|
`generator_version` defaults to the server's `HaveAPI::VERSION`. Install the
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
matching generator in the server's Ruby environment: `haveapi-go-client`,
|
|
41
|
+
`haveapi-rust-client`, or the source-distributed `haveapi-typescript-client`.
|
|
42
|
+
See the [TypeScript generator guide](../clients/typescript/README.md) for its
|
|
43
|
+
setup. An explicit `generator_version` requires the installed generator to
|
|
44
|
+
report
|
|
45
|
+
that version; the server never downloads one. This version is independent of the
|
|
46
|
+
SDK and API versions.
|
|
47
|
+
|
|
48
|
+
At the end of mount, the server builds one private structural model per
|
|
49
|
+
configured
|
|
50
|
+
API version. The model includes authentication resources, routes and changes
|
|
51
|
+
from
|
|
41
52
|
post-mount hooks, so canonical names and parent arguments use the complete API
|
|
42
53
|
declaration. The private copy excludes examples, prose and parameter defaults.
|
|
43
54
|
HTML requests use the current user's authorized description, fields and samples;
|
|
44
55
|
they never expose the private model or its initialization diagnostics. Without
|
|
45
|
-
a configured profile, the server loads no optional generator and
|
|
46
|
-
Rust panels.
|
|
56
|
+
a configured profile, the server loads no optional generator and hides the Go,
|
|
57
|
+
Rust and generated TypeScript panels. The callback and async TypeScript
|
|
58
|
+
examples remain separate and need no profile.
|
|
47
59
|
|
|
48
60
|
Automatic profiles assume that the installed SDK matches the current server's
|
|
49
61
|
generation contract. A package name or version does not prove its contents.
|
|
@@ -53,8 +65,9 @@ schema override below when the SDK represents an older or restricted API.
|
|
|
53
65
|
|
|
54
66
|
### Optional SDK versions and sources
|
|
55
67
|
|
|
56
|
-
Set `module_version: 'v1.2.3'` for Go
|
|
57
|
-
|
|
68
|
+
Set `module_version: 'v1.2.3'` for Go, `crate_version: '1.2.3'` for Rust or
|
|
69
|
+
`package_version: '1.2.3'` for TypeScript when a specific package version is
|
|
70
|
+
needed. Omit the field or use nil to let the user's
|
|
58
71
|
package manager select a release. The server does not query a registry or claim
|
|
59
72
|
to have resolved a version. Omitted versions produce these installation hints:
|
|
60
73
|
|
|
@@ -63,15 +76,25 @@ go get example.org/widget/client@latest
|
|
|
63
76
|
cargo add widget-client
|
|
64
77
|
# Blocking Rust variant:
|
|
65
78
|
cargo add widget-client --features blocking
|
|
79
|
+
npm install @example/widget-client haveapi-client@0.30.2
|
|
66
80
|
```
|
|
67
81
|
|
|
68
82
|
Go resolves `@latest` by its normal rules. Cargo selects a compatible release
|
|
69
83
|
according to its resolver, manifest and toolchain; existing lockfiles still
|
|
70
84
|
apply. Explicit Cargo versions remain version requirements, without an added
|
|
71
|
-
exact `=` pin. Latest selection can change over time; supply a version to
|
|
85
|
+
exact `=` pin. Latest selection can change over time; supply a version to
|
|
86
|
+
control
|
|
72
87
|
the selection. Cargo never receives a literal `version = "latest"`. Rust
|
|
73
88
|
examples also show their Tokio and serde_json requirements.
|
|
74
89
|
|
|
90
|
+
TypeScript accepts ordinary lowercase npm package names, including scoped names,
|
|
91
|
+
and optional concrete SemVer versions. Tags, version ranges, Git URLs, local
|
|
92
|
+
paths
|
|
93
|
+
and custom factories are not profile options. Configure a package that users can
|
|
94
|
+
install; the server does not check its availability or publish it. Generated
|
|
95
|
+
snippets use the package's public `createClient` export and the published
|
|
96
|
+
`haveapi-client` 0.30.2 runtime.
|
|
97
|
+
|
|
75
98
|
For an unpublished Rust SDK, supply an HTTPS Git URL and a full commit SHA:
|
|
76
99
|
|
|
77
100
|
```ruby
|
|
@@ -118,13 +141,20 @@ api.documentation_clients('1.0',
|
|
|
118
141
|
snapshot_path: '/srv/widget-sdk/api-1.0.snapshot.json',
|
|
119
142
|
generator_config_path: '/srv/widget-sdk/generator.toml',
|
|
120
143
|
generator_version: '0.30.0'
|
|
144
|
+
},
|
|
145
|
+
typescript: {
|
|
146
|
+
package_name: '@example/widget-client',
|
|
147
|
+
package_version: '1.2.3',
|
|
148
|
+
snapshot_path: '/srv/widget-sdk/api-1.0.snapshot.json',
|
|
149
|
+
generator_version: '0.30.0'
|
|
121
150
|
}
|
|
122
151
|
)
|
|
123
152
|
```
|
|
124
153
|
|
|
125
154
|
Paths name trusted absolute local files. `description_path` is Go's saved
|
|
126
|
-
machine-readable description, not HTML.
|
|
127
|
-
and capture metadata.
|
|
155
|
+
machine-readable description, not HTML. For Rust and TypeScript, `snapshot_path`
|
|
156
|
+
selects the saved schema and capture metadata. These files select an exact SDK
|
|
157
|
+
shape in place of the current
|
|
128
158
|
server declaration. Keep the files private and retain them with the SDK release.
|
|
129
159
|
|
|
130
160
|
Rust's legacy `generator_config_path` supplies the crate name/version and full
|
|
@@ -138,7 +168,8 @@ Saved files are read and validated during registration. Automatic models are
|
|
|
138
168
|
initialized at the end of mount. Missing files or generators, actual generator
|
|
139
169
|
version mismatches, unknown fields, conflicting identities and invalid schemas
|
|
140
170
|
fail startup. Mounting also rejects unavailable API versions and mismatched Rust
|
|
141
|
-
snapshot versions. Failed mounting publishes no profiles and
|
|
171
|
+
and TypeScript snapshot versions. Failed mounting publishes no profiles and
|
|
172
|
+
leaves no usable
|
|
142
173
|
server application. An older explicitly pinned generator can serve a suitable
|
|
143
174
|
legacy file profile, but automatic/inline Rust mode requires the new factory
|
|
144
175
|
interface and never guesses a fallback.
|
|
@@ -161,8 +192,21 @@ Existing renderer positional constructors and `init`, `auth(method, desc)` and
|
|
|
161
192
|
The registry binds a request-local `HaveAPI::ClientExample::Context` through
|
|
162
193
|
`with_context`. It holds the selected version, URLs including the API mount,
|
|
163
194
|
the filtered description, and the current resource, action and sample.
|
|
164
|
-
|
|
165
|
-
|
|
195
|
+
Construction copies and freezes Hash, Array and String data without freezing
|
|
196
|
+
the caller's containers. Each request owns its description snapshot.
|
|
197
|
+
`context.with(...)` creates a distinct frozen context, sharing unchanged fields
|
|
198
|
+
by identity and normalizing only explicitly supplied fields. A shallow-frozen
|
|
199
|
+
input container still needs copying; explicitly supplying an existing owned
|
|
200
|
+
Hash,
|
|
201
|
+
Array or String also copies it. Renderers must treat context-owned data as
|
|
202
|
+
read-only.
|
|
203
|
+
|
|
204
|
+
The profile is a shared reference; registered profiles and their adapters are
|
|
205
|
+
frozen separately. Other object types retain their existing reference semantics,
|
|
206
|
+
so context construction is not an arbitrary-object deep-freezing contract.
|
|
207
|
+
Subclasses of `Context` continue deriving through their constructors, preserving
|
|
208
|
+
custom initialization and extra keywords; the sharing optimization applies to
|
|
209
|
+
the exact base class.
|
|
166
210
|
|
|
167
211
|
Renderers may return a String or nil through their existing methods. New
|
|
168
212
|
renderers return either `{status: :code, code: source}` or
|
|
@@ -173,13 +217,21 @@ overview and every panel before rendering.
|
|
|
173
217
|
|
|
174
218
|
Generated-client adapters belong to their generator gems and consume context
|
|
175
219
|
readers without depending on the server gem. They resolve names using the
|
|
176
|
-
complete private generator model and never run a compiler, formatter or
|
|
177
|
-
|
|
178
|
-
|
|
220
|
+
complete private generator model and never run a compiler, formatter or
|
|
221
|
+
generator
|
|
222
|
+
in an HTML request. Keep adapter rendering read-only: configured adapters are
|
|
223
|
+
frozen
|
|
224
|
+
and shared, while authorized description snapshots belong to individual
|
|
225
|
+
requests.
|
|
226
|
+
Derived contexts within one request share unchanged owned data. Supplying a
|
|
227
|
+
resource, action or sample still copies that supplied subtree, so a resource
|
|
228
|
+
with
|
|
229
|
+
many actions can remain costly to render.
|
|
179
230
|
|
|
180
231
|
## Verification
|
|
181
232
|
|
|
182
|
-
`make test-documentation` checks renderers, contexts, profiles, rendered HTML
|
|
233
|
+
`make test-documentation` checks renderers, contexts, profiles, rendered HTML
|
|
234
|
+
and
|
|
183
235
|
local assets. Prepare the component bundles and JavaScript development
|
|
184
236
|
dependencies with `make release-deps` first. The target builds the JavaScript
|
|
185
237
|
package before compiling TypeScript examples against its public declarations;
|
|
@@ -190,6 +242,107 @@ against the real request encoder. It requires the locked Python environment
|
|
|
190
242
|
prepared by `make release-deps` and is included in `make test`. The ordinary
|
|
191
243
|
Ruby renderer checks run without the Python package environment.
|
|
192
244
|
|
|
245
|
+
Deterministic context and HTML regressions run in the ordinary server suite.
|
|
246
|
+
They check description sharing, allocation growth and isolation between users
|
|
247
|
+
without a wall-clock threshold. The Rust adapter suite separately checks that
|
|
248
|
+
normalization avoids unrelated description descendants.
|
|
249
|
+
|
|
250
|
+
### Documentation benchmark
|
|
251
|
+
|
|
252
|
+
Prepare the server bundle in its declared Nix environment, then run the opt-in
|
|
253
|
+
harness from the repository root:
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
nix develop .#server-ruby --command bash -c \
|
|
257
|
+
'make --no-print-directory benchmark-documentation > /tmp/documentation.json'
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The target synchronizes shared documentation before running the harness.
|
|
261
|
+
Standard
|
|
262
|
+
output is JSON; shell/dependency preparation happens before the redirected
|
|
263
|
+
command.
|
|
264
|
+
No network server, database, generated SDK compilation or formatter is involved.
|
|
265
|
+
The synthetic fixture uses Basic users `admin` and `member`, both with the local
|
|
266
|
+
test password `fixture-only`.
|
|
267
|
+
|
|
268
|
+
Defaults measure two shapes with eight ordinary actions each: eight resources
|
|
269
|
+
with one action, and one resource with eight actions. Each ordinary action also
|
|
270
|
+
has a privileged sample; the first resource has an additional admin-only action.
|
|
271
|
+
Context lanes derive a prepared parent with a changed profile or a constant
|
|
272
|
+
small
|
|
273
|
+
sample. The HTML lane performs one real Rack request per trial, with all
|
|
274
|
+
available
|
|
275
|
+
dynamic renderer panels. Use a minimal smoke run or a larger repeated campaign:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
make benchmark-documentation \
|
|
279
|
+
BENCHMARK_DOCUMENTATION_ARGS='--shape resources --size 1 --warmup 0 --trials 1 --iterations 1'
|
|
280
|
+
make benchmark-documentation \
|
|
281
|
+
BENCHMARK_DOCUMENTATION_ARGS='--shape both --size 64 --samples 2 --parameters 8 --warmup 2 --trials 7 --iterations 500' \
|
|
282
|
+
> /tmp/documentation-large.json
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Run these `make` commands inside the prepared shell. `--shape custom --resources
|
|
286
|
+
N --actions N` controls both dimensions independently; `--lanes context`
|
|
287
|
+
isolates
|
|
288
|
+
derivation and `--lanes html` isolates requests. `--user member` selects
|
|
289
|
+
restricted
|
|
290
|
+
help. `--help` lists all options. `--rust` enables the fixture's matching Rust
|
|
291
|
+
profile, includes both Rust variants in HTML, and adds separate async/blocking
|
|
292
|
+
action and authentication adapter lanes. It requires the optional generator's
|
|
293
|
+
Ruby dependencies to be available; ordinary benchmark runs need none of them.
|
|
294
|
+
Prepare both component environments using `nix develop .#server-ruby` and
|
|
295
|
+
`nix develop .#client-rust`. The server bundle alone does not activate the Rust
|
|
296
|
+
dependencies. In the top-level Nix shell, combine the prepared local gem stores:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
env -u RUBYOPT -u RUBYLIB -u BUNDLE_BIN_PATH -u BUNDLE_GEMFILE \
|
|
300
|
+
-u BUNDLE_LOCKFILE -u BUNDLE_PATH -u BUNDLE_FROZEN -u BUNDLER_SETUP \
|
|
301
|
+
GEM_PATH="$PWD/.gems:$PWD/clients/rust/.gems:$(ruby -e 'puts Gem.default_dir')" \
|
|
302
|
+
make benchmark-documentation BENCHMARK_DOCUMENTATION_RUBY=ruby \
|
|
303
|
+
BENCHMARK_DOCUMENTATION_ARGS='--rust --shape both --size 8 --warmup 1 --trials 3 --iterations 100' \
|
|
304
|
+
> /tmp/documentation-rust.json
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
This command runs outside Bundler and records the gem versions actually
|
|
308
|
+
activated. Retain that list alongside both component locks and use the same
|
|
309
|
+
prepared inputs for both revisions. Missing optional dependencies fail the run;
|
|
310
|
+
Rust lanes are never silently omitted. See the
|
|
311
|
+
[Rust adapter guide](../clients/rust/docs/documentation.md#verification).
|
|
312
|
+
|
|
313
|
+
Every lane reports each trial and the median wall seconds, process CPU seconds
|
|
314
|
+
and allocated objects, with the operation count per trial. Wall time uses a
|
|
315
|
+
monotonic clock. A full GC runs before each trial, outside its timer; GC
|
|
316
|
+
triggered
|
|
317
|
+
by the measured work remains included. Mount/profile construction has separate
|
|
318
|
+
setup metrics. Ownership construction, description selection, warmups, output
|
|
319
|
+
validation, hashing and JSON serialization are outside lane measurements.
|
|
320
|
+
Reports include fixture dimensions, renderer IDs, profile mode, Ruby runtime,
|
|
321
|
+
host, Git HEAD/status/diff digest, loaded source digests, dependency versions
|
|
322
|
+
and
|
|
323
|
+
available lockfile/harness/fixture digests. HTML and Rust outputs include
|
|
324
|
+
status, bytes and
|
|
325
|
+
SHA-256; failed or missing output causes a nonzero exit.
|
|
326
|
+
|
|
327
|
+
For a before/after comparison, retain one exact harness and fixture and point
|
|
328
|
+
them at each implementation with `--source-root /absolute/checkout`. Set up
|
|
329
|
+
equivalent dependencies and runtime first. An explicit `--source-revision LABEL`
|
|
330
|
+
can supplement the detected HEAD and is required for an exported source tree.
|
|
331
|
+
Retain exact heads, commands, raw JSON, lock/input identities and host identity.
|
|
332
|
+
Compare dimensions, renderer modes, statuses, bytes and body digests before
|
|
333
|
+
comparing medians. Context `description_shared` deliberately distinguishes the
|
|
334
|
+
old copying implementation. Investigate any output difference; record exact
|
|
335
|
+
volatile-field normalization if needed rather than silently dropping mismatches.
|
|
336
|
+
|
|
337
|
+
These measurements describe the synthetic workload on the recorded host. They
|
|
338
|
+
do not establish production latency. The opt-in harness has no timing threshold;
|
|
339
|
+
the [paired endpoint performance suite](performance.md) supplies required
|
|
340
|
+
quick/weekly CI timing, allocation and query gates, including documentation
|
|
341
|
+
OPTIONS and both HTML shapes. It retains baseline/self noise and blocks
|
|
342
|
+
inconclusive results. Ownership, traversal and allocation regressions cover this
|
|
343
|
+
failure class, while concentrated-resource costs and other future regressions
|
|
344
|
+
still need representative measurements.
|
|
345
|
+
|
|
193
346
|
Each client also has acceptance commands that check executable snippets against
|
|
194
347
|
installed packages or a generated SDK. These use
|
|
195
348
|
rendered source against disposable fixtures; syntax checks alone do not
|
data/doc/performance.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Endpoint performance checks
|
|
2
|
+
|
|
3
|
+
The Ruby performance suite sends real Rack requests through routing,
|
|
4
|
+
authentication, authorization, typed validation and serialization. It covers
|
|
5
|
+
public, Basic and token requests, valid and invalid typed input, nested resources,
|
|
6
|
+
lists, SQLite pagination and eager loading, and documentation OPTIONS/HTML.
|
|
7
|
+
Deterministic specs check response contracts, isolation, allocation growth and
|
|
8
|
+
query/authorization counts. Paired timing runs compare exact baseline and
|
|
9
|
+
candidate sources with one neutral harness and third-party dependency graph.
|
|
10
|
+
|
|
11
|
+
These are synthetic in-process workloads. SQLite results describe the fixture's
|
|
12
|
+
query behavior; they do not measure production database, network server or
|
|
13
|
+
vpsAdmin latency. The checks detect specific regressions and do not guarantee
|
|
14
|
+
every future performance characteristic.
|
|
15
|
+
|
|
16
|
+
## Local commands
|
|
17
|
+
|
|
18
|
+
The benchmark has its own third-party tool bundle and pinned Nix shell in
|
|
19
|
+
`servers/ruby/benchmarks/`. This keeps contributor Nix updates separate from the
|
|
20
|
+
calibrated measurement runtime. Prepare the ordinary frozen bundle once:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
export HAVEAPI_BENCHMARK_GEM_HOME="$PWD/servers/ruby/benchmarks/.gems"
|
|
24
|
+
nix-shell servers/ruby/benchmarks/shell.nix --run '
|
|
25
|
+
set -e
|
|
26
|
+
gem install bundler --version 4.0.23 --no-document
|
|
27
|
+
BUNDLE_IGNORE_CONFIG=1 BUNDLE_FROZEN=1 BUNDLE_PATH__SYSTEM=1 \
|
|
28
|
+
BUNDLE_GEMFILE="$PWD/servers/ruby/benchmarks/Gemfile" bundle _4.0.23_ install
|
|
29
|
+
'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The tool Gemfile installs no HaveAPI package. Its tracked lock is an application
|
|
33
|
+
lock; component library locks remain local contributor inputs. Launch the runner
|
|
34
|
+
outside Bundler so both exported sources use the same Ruby and gem paths. The
|
|
35
|
+
shell clears inherited Bundler settings, `RUBYOPT`, `RUBYLIB` and
|
|
36
|
+
`RUBYGEMS_GEMDEPS`.
|
|
37
|
+
|
|
38
|
+
Run deterministic specs in the prepared contributor shell with
|
|
39
|
+
`make test-performance`. For a comparison, keep the neutral harness in a candidate
|
|
40
|
+
checkout containing both exact Git objects. Export the baseline and candidate
|
|
41
|
+
separately, retain their archive hashes and manifests, and run `make doc` in each
|
|
42
|
+
exported root. Then run from the candidate checkout:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
nix-shell servers/ruby/benchmarks/shell.nix --run '
|
|
46
|
+
make performance PERFORMANCE_ARGS="--baseline-root /absolute/baseline --baseline-sha BASELINE_FULL_SHA --candidate-root /absolute/candidate --candidate-sha CANDIDATE_FULL_SHA --profile quick --budgets servers/ruby/benchmarks/performance_budgets.json --output /absolute/new-evidence-directory"
|
|
47
|
+
'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Replace paths and full 40-character lowercase SHAs. Use `--profile weekly` for
|
|
51
|
+
the expanded workload. The runner checks the exact calibrated Ruby and available
|
|
52
|
+
dependency versions before timing, then requires each worker's complete activated
|
|
53
|
+
graph to match the weekly calibration. Source and loaded-file checks and
|
|
54
|
+
response validation must also pass. An unsupported declaration or runtime change
|
|
55
|
+
requires a separately reviewed environment migration; it does not update the
|
|
56
|
+
reference or budgets.
|
|
57
|
+
|
|
58
|
+
To reclassify retained samples without making requests:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
make performance-compare \
|
|
62
|
+
PERFORMANCE_ARGS='--input /absolute/evidence/samples.json --budgets servers/ruby/benchmarks/performance_budgets.json --output /absolute/new-comparison-directory'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The commands invoke `servers/ruby/benchmarks/endpoints.rb run` and `compare`
|
|
66
|
+
directly. `run` retains `samples.json`, `report.json` and process diagnostics.
|
|
67
|
+
`compare` reads compact samples format version 2; unsupported versions are
|
|
68
|
+
rejected without conversion. `report.json` retains the original measurement's
|
|
69
|
+
`harness_sha256`. Its `comparison` metadata identifies policy
|
|
70
|
+
`per-revision-response-stability-v1`, the loaded comparator file's SHA-256 and
|
|
71
|
+
the SHA-256 of the exact consumed `samples.json` bytes. Error reports retain
|
|
72
|
+
these identities when available; they omit unavailable measurement/input fields
|
|
73
|
+
instead of identifying old measurements with the current harness. The existing
|
|
74
|
+
`budget_sha256` identifies the budget. Replaying samples classifies the recorded
|
|
75
|
+
measurements under the identified comparison policy. It makes no requests and
|
|
76
|
+
leaves the original result unchanged.
|
|
77
|
+
`report.json` contains source/profile identity, overall outcome and per-workload
|
|
78
|
+
results, without raw samples or response bodies.
|
|
79
|
+
Exit codes are 0 for a pass
|
|
80
|
+
or an inconclusive timing result, 1 for a regression and 3 for errors, including
|
|
81
|
+
missing workloads, invalid or unstable responses, inputs or provenance. An
|
|
82
|
+
inconclusive report remains explicitly inconclusive; a successful process exit
|
|
83
|
+
does not establish a measured performance pass. Deterministic failures and regressions block CI.
|
|
84
|
+
Keep the original evidence and diagnose failures or insufficient timing signal;
|
|
85
|
+
there is no automatic retry loop or baseline update.
|
|
86
|
+
|
|
87
|
+
## Timing and work gates
|
|
88
|
+
|
|
89
|
+
Quick workloads include seven API cases, SQLite page/includes cases, and
|
|
90
|
+
documentation OPTIONS plus both HTML shapes at size 8. List and includes quick
|
|
91
|
+
cases use 32 rows; pagination uses page size 32 over 128 rows. Weekly adds
|
|
92
|
+
member/admin variants, larger payloads, list/includes output up to 1,024 rows,
|
|
93
|
+
page size 32 over up to 10,000 SQLite rows, documentation shapes at size 16 and
|
|
94
|
+
Rust async/blocking action/authentication examples. An output of 1,024 rows uses
|
|
95
|
+
an unpaginated relation; it is not an invalid request above the pagination limit.
|
|
96
|
+
|
|
97
|
+
Both profiles also render complete Rust-enabled HTML pages for admin and member
|
|
98
|
+
users. Quick uses 16 resources; weekly uses 32. Each resource has four actions,
|
|
99
|
+
two samples per action and four parameters. These cases validate the full page,
|
|
100
|
+
Rust panels, authorization boundaries and operation counts. They use relative
|
|
101
|
+
allocation and deterministic checks without inventing fixed-reference allocation
|
|
102
|
+
ceilings for the new shapes. Existing calibrated workload budgets remain intact.
|
|
103
|
+
|
|
104
|
+
These shapes exercise complete pages with several megabytes of HTML, so a
|
|
105
|
+
single request provides substantial measurement work. The weekly shape doubles
|
|
106
|
+
the resource count. Actual batch durations and output sizes belong in each
|
|
107
|
+
run's evidence; these synthetic shapes do not define production latency targets.
|
|
108
|
+
|
|
109
|
+
| Measurement policy | Quick | Weekly |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| Baseline request work per batch | 1 second | 2 seconds |
|
|
112
|
+
| Minimum adequate observed batch | 750 ms | 1.5 seconds |
|
|
113
|
+
| Baseline/self pairs | 3 | 3 |
|
|
114
|
+
| Baseline/candidate pairs | 5 | 7 |
|
|
115
|
+
| Required agreement | 4 of 5 | 6 of 7 |
|
|
116
|
+
| Relative wall/CPU regression budget | 20% | 10% |
|
|
117
|
+
| Minimum detectable batch increase | 50 ms | 50 ms |
|
|
118
|
+
|
|
119
|
+
A baseline pilot selects a common count of 1–100,000 requests for both revisions.
|
|
120
|
+
One bounded calibration adjustment can increase an undersized baseline batch.
|
|
121
|
+
Reports retain observed duration, chosen count, adjustment and any cap limitation;
|
|
122
|
+
a capped batch below the adequacy threshold is insufficient timing evidence.
|
|
123
|
+
Cheap workloads have two warmup batches of about 100 ms each, outside the
|
|
124
|
+
measurement. If the baseline pilot already meets the profile's work target with
|
|
125
|
+
one expensive request, both revisions use count 1 and no extra request warmups.
|
|
126
|
+
That case measures the first complete request after equivalent fixture setup;
|
|
127
|
+
the candidate cannot choose a different warmup policy.
|
|
128
|
+
|
|
129
|
+
Wall and process CPU time are compared separately after normalization per request.
|
|
130
|
+
The minimum detectable increase is the larger of the profile's relative budget
|
|
131
|
+
and 50 ms divided by the common request count. Paired baseline/self observations
|
|
132
|
+
retain the measured noise. A consistent increase beyond the budget and noise
|
|
133
|
+
fails. Reports distinguish ordinary variation from insufficient timing signal.
|
|
134
|
+
Inconclusive timing remains labeled as such; it does not trigger automatic
|
|
135
|
+
workflow retries. The agreement rules are engineering thresholds, not statistical
|
|
136
|
+
confidence levels.
|
|
137
|
+
|
|
138
|
+
Candidate order alternates. Setup, mount, schema, seeds, token issuance, reset,
|
|
139
|
+
response parsing/digests and counter checks stay outside the timed loop. GC
|
|
140
|
+
remains enabled, including collections triggered by measured requests. A
|
|
141
|
+
30-second watchdog surrounds each request batch, including pilot and warmups.
|
|
142
|
+
This also bounds every individual request without instrumenting the cheap request
|
|
143
|
+
loop. It is supplementary runaway protection, not a regression budget. Setup has
|
|
144
|
+
a separate 120-second timeout. Supervision records source, workload and phase and terminates
|
|
145
|
+
and reaps stuck workers.
|
|
146
|
+
|
|
147
|
+
Within each measurement or warmup, the worker validates each distinct raw body
|
|
148
|
+
once for that scenario and retains an immutable proof of its parsed values and
|
|
149
|
+
output semantics. Every operation still checks the raw digest and exact bytes,
|
|
150
|
+
HTTP status and content type, called index, and execution/authorization/query
|
|
151
|
+
records. A hash collision cannot reuse a proof. Proofs are discarded at the
|
|
152
|
+
sample boundary and never shared between users, scenarios or source revisions.
|
|
153
|
+
Samples retain metrics, one uniform validated counter record with its operation
|
|
154
|
+
count, and distinct response examples with occurrence counts and raw bodies.
|
|
155
|
+
Replay checks those counts, counters, examples and raw/canonical identities,
|
|
156
|
+
including comparison mode, verified own version and source/environment/inputs.
|
|
157
|
+
It compares canonical response identities and occurrence totals across samples
|
|
158
|
+
from the same verified source SHA. Both columns of a baseline/self pair belong
|
|
159
|
+
to the baseline SHA. JSON map order may vary; raw byte stability is not required.
|
|
160
|
+
It does not retain request order or independently reprove each live
|
|
161
|
+
authorization/counter check.
|
|
162
|
+
The compact artifact is a summary, not an execution transcript.
|
|
163
|
+
|
|
164
|
+
Responses may differ between baseline and candidate without failing or skipping
|
|
165
|
+
the performance comparison. Each workload reports `raw_changed` and
|
|
166
|
+
`canonical_changed`, together with baseline/candidate bytes per request and
|
|
167
|
+
their byte and percentage changes. These diagnostics use the
|
|
168
|
+
occurrence-weighted output distributions from baseline/candidate pairs only;
|
|
169
|
+
baseline/self pairs do not affect the size comparison. Reports retain no raw
|
|
170
|
+
bodies. Response growth alone does not fail the comparison or change its timing
|
|
171
|
+
and allocation limits. Equal response lengths do not prove correctness. The CI
|
|
172
|
+
step summary shows these diagnostics beside wall, CPU, allocation and overall
|
|
173
|
+
outcomes, including regressions and inconclusive timing.
|
|
174
|
+
|
|
175
|
+
Allocation growth above baseline median plus the larger of 10% or 32 objects
|
|
176
|
+
per request fails. Additional fixed-reference ceilings use measured accepted
|
|
177
|
+
counts with 25% headroom plus 32 objects/request. Exact SQL query and required
|
|
178
|
+
authorization counts prevent a fast but incomplete operation from passing.
|
|
179
|
+
Scaling bounds reflect actual output work; growing responses need not allocate
|
|
180
|
+
a constant amount. Budget changes require measured justification and review.
|
|
181
|
+
Missing or uncalibrated budgets for the existing calibrated workloads fail rather
|
|
182
|
+
than disabling the gate.
|
|
183
|
+
|
|
184
|
+
Both profiles use the recorded weekly Rust dependency closure as their expected
|
|
185
|
+
runtime identity. Quick includes the Rust manifests, gemspec and loaded
|
|
186
|
+
dependencies. Both profiles retain the benchmark Gemfile, lock and shell bytes
|
|
187
|
+
and hashes. Baseline and candidate must match that environment exactly;
|
|
188
|
+
unavailable or changed identities fail. The shared
|
|
189
|
+
comparison entry point activates the recorded native JSON 2.21.2 before its
|
|
190
|
+
first require, including in unbundled workers. A newer visible or already loaded
|
|
191
|
+
JSON must not silently replace that dependency; a missing or conflicting version
|
|
192
|
+
fails during activation. Historical
|
|
193
|
+
quick allocation calibration and all existing reference/budget bytes are retained
|
|
194
|
+
unchanged, without claiming that the original quick calibration loaded Rust.
|
|
195
|
+
|
|
196
|
+
The root, Ruby server and Rust generator development Gemfiles declare JSON
|
|
197
|
+
2.21.2 so fresh setup installs the recorded native dependency. Comparisons
|
|
198
|
+
accept only that exact declaration change in these three files. They retain
|
|
199
|
+
the original manifest contents, hashes and declaration evidence, then compare
|
|
200
|
+
normalized contents. Replay verifies the transformation from the retained raw
|
|
201
|
+
inputs. Other dependency requirements and manifest contents must match.
|
|
202
|
+
|
|
203
|
+
## GitHub workflow and baseline selection
|
|
204
|
+
|
|
205
|
+
The `servers/ruby performance` workflow runs on pushes, ordinary pull requests,
|
|
206
|
+
manual dispatch and Sunday at 03:00 UTC. It always creates the `Performance gate`
|
|
207
|
+
aggregate check. Relevant-path detection happens after event resolution, inside
|
|
208
|
+
the workflow; an irrelevant valid change can succeed without running benchmarks.
|
|
209
|
+
Source/dependency/fixture/budget/workflow/flake changes trigger measurement.
|
|
210
|
+
Missing event/source evidence fails even when no relevant path can be established.
|
|
211
|
+
|
|
212
|
+
| Event | Candidate | Baseline |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| Pull request | Exact event head SHA | Merge-base of head and exact event base SHA |
|
|
215
|
+
| Existing branch push, quick | Exact event `after` | Exact nonzero event `before`, on every branch |
|
|
216
|
+
| New branch push, quick (zero `before`) | Exact event `after` | Merge-base with captured exact default-branch SHA |
|
|
217
|
+
| Weekly | Exact scheduled default-branch event SHA | Fixed accepted reference below |
|
|
218
|
+
| Manual with a complete explicit pair | Supplied full candidate SHA | Supplied full baseline SHA |
|
|
219
|
+
| Manual without either SHA | Captured default SHA | Fixed accepted reference below |
|
|
220
|
+
|
|
221
|
+
The immutable accepted reference is
|
|
222
|
+
`b85ec364057f536940d689b09e612512ffffaf82`. Required ancestry is checked. The
|
|
223
|
+
reference and budgets never move automatically. Deleted branches, malformed or
|
|
224
|
+
missing commits, unavailable new-branch fallback, partial manual pairs and equal
|
|
225
|
+
endpoints fail. PRs use the head commit, never the synthetic merge commit. An
|
|
226
|
+
existing branch push uses `before`, never `HEAD^` or a guessed fallback. A quick
|
|
227
|
+
push can compare a nonancestor before/after pair after a force push; its exact
|
|
228
|
+
event pair is carried through all ancestry guards. Direct CLI use of that
|
|
229
|
+
exception requires `--push-event selection.json` from the event resolver. Other
|
|
230
|
+
PR, manual and weekly ancestry restrictions remain. Relevance uses the direct
|
|
231
|
+
before/after tree diff, including the benchmark setup files.
|
|
232
|
+
The weekly profile always requires that fixed baseline, including an explicit
|
|
233
|
+
manual pair. An explicit quick pair may select another ancestor.
|
|
234
|
+
|
|
235
|
+
### Maintained 0.30 release comparisons
|
|
236
|
+
|
|
237
|
+
The original fixed reference remains the measured baseline for weekly runs.
|
|
238
|
+
Its maintenance counterpart is
|
|
239
|
+
`8b7abc097db49893da9cc8fa4395648901fec71d`. The resolver and direct runner accept
|
|
240
|
+
this one nonliteral ancestry relationship only when the counterpart is a literal
|
|
241
|
+
candidate ancestor and the complete binary tree delta from the original reference
|
|
242
|
+
equals the published 0.30.1 metadata commit
|
|
243
|
+
`1c3c98f0d77e0b23c8da1a8937f25c5799864a3b` relative to its parent. They verify
|
|
244
|
+
the exact four trees and canonical full-index delta hash, retain both raw diffs
|
|
245
|
+
and bind the receipt to
|
|
246
|
+
the candidate SHA. Cherry-pick trailers do not establish this proof. A different
|
|
247
|
+
mapping needs a new review; the reference and budget calibration never change.
|
|
248
|
+
Diff rendering uses full blob IDs and explicit formatting options, independent
|
|
249
|
+
of repository-dependent abbreviation and user diff display settings. Ordinary
|
|
250
|
+
master ancestry is checked before probing the maintenance mapping, so a master
|
|
251
|
+
clone does not need maintenance objects for that path.
|
|
252
|
+
Unrelated direct quick baselines still fail. Existing maintenance branch pushes
|
|
253
|
+
use their exact pushed pair; new branches retain the captured-default fallback
|
|
254
|
+
and reason. A `haveapi-0.30` push still requires verified maintenance lineage.
|
|
255
|
+
|
|
256
|
+
Both sources in a maintenance comparison use the prepared benchmark environment.
|
|
257
|
+
In addition to the JSON preparation rule above, these comparisons normalize the
|
|
258
|
+
shared `0.30.x` patch identity, matching local Ruby component VERSION assignments
|
|
259
|
+
and exactly synchronized internal `haveapi-client` requirements. All remaining
|
|
260
|
+
bytes in dependency manifests, gemspecs and Ruby requirements must match, as must
|
|
261
|
+
the resolved third-party runtime graph and benchmark lock bytes. Source `flake.nix` and `flake.lock` remain raw provenance
|
|
262
|
+
with source hashes; they do not supply the benchmark runtime and may differ.
|
|
263
|
+
Every other supported declaration remains strict. The consumed benchmark shell
|
|
264
|
+
and lock are retained separately and must match across workers. Raw manifest
|
|
265
|
+
contents and hashes remain bound to each verified source tree alongside their
|
|
266
|
+
normalized hashes. Replay
|
|
267
|
+
recomputes the transformation and rejects missing, inconsistent or changed inputs.
|
|
268
|
+
|
|
269
|
+
The HTML comparison may normalize the own-version segment of the documentation
|
|
270
|
+
templates' `https://webui.haveapi.org/v<own-version>/#...` anchor hrefs and
|
|
271
|
+
the version in the exact framework-generated HaveAPI footer. The footer must
|
|
272
|
+
occur once and match the verified source's own VERSION; literal examples, other
|
|
273
|
+
footer text and URLs remain exact. Validation runs after the timed loop. Complete
|
|
274
|
+
raw bodies, byte counts and hashes remain in the samples, together with the
|
|
275
|
+
normalized comparison digest and source version. Replay checks these again from
|
|
276
|
+
the raw body. This normalization applies only to the HTML documentation workloads
|
|
277
|
+
on the proven maintenance lineage. JSON/API
|
|
278
|
+
values, other URLs and hosts, example code, authorization markers, panels and all
|
|
279
|
+
other output are not normalized. Cross-revision differences remain diagnostic;
|
|
280
|
+
within-revision stability and raw/canonical integrity still apply.
|
|
281
|
+
|
|
282
|
+
The workflow uses ephemeral GitHub-hosted Ubuntu workers, `contents: read`,
|
|
283
|
+
disabled checkout credential persistence, ordinary `pull_request` events and no
|
|
284
|
+
secrets, publication context or PR comments. The comparison job has a hard
|
|
285
|
+
15-minute quick limit and 45-minute weekly limit. Measured page sizes and sample
|
|
286
|
+
counts must leave room for preparation, ordinary workloads and evidence
|
|
287
|
+
validation within those limits. Actual hosted campaign duration requires its own
|
|
288
|
+
run receipt; the page probe alone does not prove it. A timeout blocks and needs
|
|
289
|
+
diagnosis. Superseded runs are cancelled only within
|
|
290
|
+
the same event/branch or PR concurrency group.
|
|
291
|
+
|
|
292
|
+
Success, regression, inconclusive and preparation-failure artifacts are retained
|
|
293
|
+
for 30 days. They include event selection, exact SHAs and source trees/archive
|
|
294
|
+
hashes, matched dependency manifests, resolved locks, runtime/input graphs, raw
|
|
295
|
+
samples, reports, process diagnostics and runner exit. The aggregate rejects
|
|
296
|
+
failed, cancelled or unexpectedly skipped required work and requires the retained
|
|
297
|
+
report. A completed comparison can retain inconclusive timing diagnostics while
|
|
298
|
+
its deterministic checks pass; the report keeps that distinction visible.
|
|
299
|
+
Repository rules determine whether this aggregate check is required for merging;
|
|
300
|
+
the workflow does not change branch protection or repository permissions.
|
|
301
|
+
|
|
302
|
+
A newly added workflow's manual dispatch may be unavailable until its definition
|
|
303
|
+
exists on the default branch. Preserve a dispatch refusal and run the same weekly
|
|
304
|
+
CLI locally if needed; disclose that hosted weekly execution remains unverified.
|
|
305
|
+
Workflow enablement alone is not a reason to integrate source into the default
|
|
306
|
+
branch. Push verification still checks the exact published candidate.
|
|
307
|
+
|
|
308
|
+
The separate [documentation benchmark](client-examples.md#documentation-benchmark)
|
|
309
|
+
remains available for diagnosis and custom page shapes. Its JSON trials/medians
|
|
310
|
+
are useful measurements; the paired endpoint runner owns required timing gates.
|
data/haveapi.gemspec
CHANGED
|
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
|
|
16
16
|
|
|
17
17
|
s.add_dependency 'activesupport', '>= 7.1'
|
|
18
|
-
s.add_dependency 'haveapi-client', '~> 0.
|
|
18
|
+
s.add_dependency 'haveapi-client', '~> 0.31.0'
|
|
19
19
|
s.add_dependency 'i18n', '>= 1.6', '< 2'
|
|
20
20
|
s.add_dependency 'json'
|
|
21
21
|
s.add_dependency 'mail'
|