haveapi 0.30.2 → 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/benchmarks/Gemfile +30 -0
- data/benchmarks/Gemfile.lock +296 -0
- data/benchmarks/endpoints.rb +70 -14
- data/benchmarks/performance_compare.rb +112 -13
- data/benchmarks/shell.nix +21 -0
- data/doc/client-examples.md +81 -35
- data/doc/performance.md +99 -61
- 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/version.rb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- 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/performance/comparator_spec.rb +136 -6
- data/spec/performance/event_baseline_spec.rb +156 -28
- data/spec/performance/release_lineage_spec.rb +53 -14
- data/spec/performance/runner_spec.rb +134 -0
- metadata +9 -3
|
@@ -8,6 +8,8 @@ module HaveAPI
|
|
|
8
8
|
end
|
|
9
9
|
require 'digest'
|
|
10
10
|
require 'json'
|
|
11
|
+
require 'open3'
|
|
12
|
+
require 'rbconfig'
|
|
11
13
|
|
|
12
14
|
module HaveAPI
|
|
13
15
|
module Performance
|
|
@@ -18,6 +20,7 @@ module HaveAPI
|
|
|
18
20
|
MAX_ITERATIONS = 100_000
|
|
19
21
|
REQUEST_LIMIT_SECONDS = 30.0
|
|
20
22
|
SETUP_LIMIT_SECONDS = 120.0
|
|
23
|
+
BENCHMARK_INPUTS = %w[servers/ruby/benchmarks/Gemfile servers/ruby/benchmarks/Gemfile.lock servers/ruby/benchmarks/shell.nix].freeze
|
|
21
24
|
|
|
22
25
|
def self.complex_cases(profile)
|
|
23
26
|
sizes = profile == 'weekly' ? [32] : [16]
|
|
@@ -30,6 +33,34 @@ module HaveAPI
|
|
|
30
33
|
RUNTIME_POLICY = { 'rack_env' => 'test', 'locale' => 'C.UTF-8', 'timezone' => 'UTC', 'yjit' => false, 'rjit' => false }.freeze
|
|
31
34
|
class ContractError < StandardError; end
|
|
32
35
|
|
|
36
|
+
def self.gemspec(root, path)
|
|
37
|
+
script = <<~RUBY
|
|
38
|
+
require 'json'
|
|
39
|
+
spec = Gem::Specification.load(File.basename(ARGV.fetch(0)))
|
|
40
|
+
abort 'Cannot load gemspec' unless spec
|
|
41
|
+
puts JSON.generate(ruby: spec.required_ruby_version.to_s,
|
|
42
|
+
dependencies: spec.dependencies.map { |d| [d.name, d.type.to_s, d.requirement.to_s] }.sort)
|
|
43
|
+
RUBY
|
|
44
|
+
environment = (ENV.keys.grep(/\ABUNDLE/) + %w[RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS]).to_h { |key| [key, nil] }
|
|
45
|
+
output, errors, status = Open3.capture3(environment, RbConfig.ruby, '-e', script, path, chdir: File.dirname(File.join(root, path)))
|
|
46
|
+
raise ContractError, "Cannot load source gemspec #{path}: #{errors.strip}" unless status.success?
|
|
47
|
+
|
|
48
|
+
specification = JSON.parse(output)
|
|
49
|
+
unless Gem::Requirement.new(specification.fetch('ruby')).satisfied_by?(Gem::Version.new(RUBY_VERSION))
|
|
50
|
+
raise ContractError, "Benchmark Ruby does not satisfy #{path}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
specification.fetch('dependencies').each do |name, type, requirement|
|
|
54
|
+
next unless type == 'runtime'
|
|
55
|
+
next if %w[haveapi haveapi-client haveapi-rust-client].include?(name)
|
|
56
|
+
|
|
57
|
+
unless Gem::Requirement.new(requirement.split(',').map(&:strip)).satisfied_by?(Gem::Specification.find_by_name(name).version)
|
|
58
|
+
raise ContractError, "Benchmark dependencies do not satisfy #{name} #{requirement}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
specification
|
|
62
|
+
end
|
|
63
|
+
|
|
33
64
|
# One reviewed maintenance mapping, not a general cherry-pick certificate.
|
|
34
65
|
# Both live entry points verify Git; replay verifies the retained raw delta.
|
|
35
66
|
module Release
|
|
@@ -166,7 +197,10 @@ module HaveAPI
|
|
|
166
197
|
receipt = inputs.fetch('release_versions')
|
|
167
198
|
raise ContractError, 'Incomplete release input graph' unless receipt.fetch('contents').keys.sort == paths(profile).sort
|
|
168
199
|
raise ContractError, 'Release input normalization mismatch' unless receipt == capture(receipt.fetch('contents'))
|
|
169
|
-
|
|
200
|
+
|
|
201
|
+
expected = receipt.fetch('normalized_sha256')
|
|
202
|
+
expected = expected.except('flake.nix', 'flake.lock') if inputs.has_key?('benchmark')
|
|
203
|
+
raise ContractError, 'Release normalized graph mismatch' unless inputs.fetch('manifests') == expected
|
|
170
204
|
|
|
171
205
|
receipt.fetch('raw_sha256').each do |path, digest|
|
|
172
206
|
raise ContractError, 'Release input escaped verified source' unless source.fetch('files_sha256').fetch(path) == digest
|
|
@@ -175,7 +209,7 @@ module HaveAPI
|
|
|
175
209
|
end
|
|
176
210
|
|
|
177
211
|
def self.graph(inputs)
|
|
178
|
-
inputs.except('release_versions', 'json_preparation')
|
|
212
|
+
inputs.except('release_versions', 'json_preparation', 'contributor_flakes')
|
|
179
213
|
end
|
|
180
214
|
|
|
181
215
|
def self.html(body, own)
|
|
@@ -211,6 +245,19 @@ module HaveAPI
|
|
|
211
245
|
raise ContractError, 'Weekly baseline must be the fixed reference'
|
|
212
246
|
end
|
|
213
247
|
|
|
248
|
+
def self.push_pair!(selection, baseline:, candidate:)
|
|
249
|
+
return false unless selection['push_pair']
|
|
250
|
+
|
|
251
|
+
expected = { 'event_name' => 'push', 'before' => baseline, 'after' => candidate }
|
|
252
|
+
unless selection['event_name'] == 'push' && selection['profile'] == 'quick' && selection['push_pair'] == expected &&
|
|
253
|
+
selection['baseline_sha'] == baseline && selection['candidate_sha'] == candidate && baseline != candidate &&
|
|
254
|
+
[baseline, candidate].all? { |sha| sha.is_a?(String) && sha.match?(/\A[0-9a-f]{40}\z/) && sha != '0' * 40 }
|
|
255
|
+
raise ContractError, 'Invalid quick push event pair'
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
true
|
|
259
|
+
end
|
|
260
|
+
|
|
214
261
|
# Canonicalization ignores map order only. Arrays and scalar types retain
|
|
215
262
|
# their meaning; duplicate JSON or symbol/string-normalized keys fail closed.
|
|
216
263
|
module Identity
|
|
@@ -270,6 +317,9 @@ module HaveAPI
|
|
|
270
317
|
end
|
|
271
318
|
|
|
272
319
|
class Comparator
|
|
320
|
+
POLICY = 'per-revision-response-stability-v1'.freeze
|
|
321
|
+
SOURCE_SHA256 = Digest::SHA256.file(__FILE__).hexdigest.freeze
|
|
322
|
+
|
|
273
323
|
def initialize(samples, budgets, budget_sha256:)
|
|
274
324
|
@samples = samples
|
|
275
325
|
@budgets = budgets
|
|
@@ -357,6 +407,37 @@ module HaveAPI
|
|
|
357
407
|
raise ContractError, 'Loaded runtime not in verified source tree' unless selected.fetch('files_sha256').fetch(path) == sha256
|
|
358
408
|
end
|
|
359
409
|
inputs = observation.fetch('inputs')
|
|
410
|
+
if inputs.has_key?('benchmark')
|
|
411
|
+
tool = inputs.fetch('benchmark')
|
|
412
|
+
raise ContractError, 'Incomplete benchmark inputs' unless tool.keys.sort == BENCHMARK_INPUTS.sort
|
|
413
|
+
|
|
414
|
+
tool.each do |path, input|
|
|
415
|
+
unless Digest::SHA256.hexdigest(input.fetch('content')) == input.fetch('sha256') &&
|
|
416
|
+
@samples.fetch('inputs').fetch('benchmark').fetch(path) == input
|
|
417
|
+
raise ContractError, 'Benchmark input digest mismatch'
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
unless inputs.fetch('locks') == { 'servers/ruby/benchmarks/Gemfile.lock' => tool.fetch('servers/ruby/benchmarks/Gemfile.lock').fetch('sha256') }
|
|
421
|
+
raise ContractError, 'Benchmark lock identity mismatch'
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
inputs.fetch('contributor_flakes').each do |path, input|
|
|
425
|
+
unless %w[flake.nix flake.lock].include?(path) && Digest::SHA256.hexdigest(input.fetch('content')) == selected.fetch('files_sha256').fetch(path)
|
|
426
|
+
raise ContractError, 'Contributor flake escaped source'
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
unless inputs.fetch('contributor_flakes').keys.sort == %w[flake.lock flake.nix]
|
|
430
|
+
raise ContractError, 'Missing contributor flake provenance'
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
inputs.fetch('manifests').each do |path, digest|
|
|
434
|
+
next if Release::JSON_GEMFILES.include?(path) || lineage
|
|
435
|
+
|
|
436
|
+
raise ContractError, 'Dependency manifest escaped source' unless selected.fetch('files_sha256').fetch(path) == digest
|
|
437
|
+
end
|
|
438
|
+
elsif inputs.has_key?('contributor_flakes')
|
|
439
|
+
raise ContractError, 'Contributor flake separation without benchmark inputs'
|
|
440
|
+
end
|
|
360
441
|
if lineage
|
|
361
442
|
Release.validate_inputs(inputs, selected, @samples.fetch('profile'))
|
|
362
443
|
elsif inputs.has_key?('release_versions')
|
|
@@ -474,14 +555,28 @@ module HaveAPI
|
|
|
474
555
|
end
|
|
475
556
|
end
|
|
476
557
|
|
|
558
|
+
def output_counts(outputs, digest = 'canonical_sha256')
|
|
559
|
+
outputs.each_with_object(Hash.new(0)) do |output, counts|
|
|
560
|
+
identity = JSON.generate(output.values_at('http_status', 'envelope_status', digest))
|
|
561
|
+
counts[identity] += output.fetch('count')
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
477
565
|
def same_outputs(left, right)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
566
|
+
output_counts(left) == output_counts(right)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def response_changes(pairs)
|
|
570
|
+
outputs = %w[baseline candidate].to_h do |role|
|
|
571
|
+
[role, pairs.flat_map { |pair| pair.fetch(role).fetch('outputs') }]
|
|
483
572
|
end
|
|
484
|
-
|
|
573
|
+
bytes = outputs.transform_values do |examples|
|
|
574
|
+
examples.sum { |output| output.fetch('body_bytes') * output.fetch('count') }.to_f / examples.sum { |output| output.fetch('count') }
|
|
575
|
+
end
|
|
576
|
+
delta = bytes.fetch('candidate') - bytes.fetch('baseline')
|
|
577
|
+
{ 'raw_changed' => output_counts(outputs.fetch('baseline'), 'body_sha256') != output_counts(outputs.fetch('candidate'), 'body_sha256'),
|
|
578
|
+
'canonical_changed' => !same_outputs(outputs.fetch('baseline'), outputs.fetch('candidate')),
|
|
579
|
+
'bytes_per_request' => bytes.merge('delta' => delta, 'delta_percent' => 100.0 * delta / bytes.fetch('baseline')) }
|
|
485
580
|
end
|
|
486
581
|
|
|
487
582
|
def compare_case(item, budget)
|
|
@@ -499,7 +594,8 @@ module HaveAPI
|
|
|
499
594
|
raise ContractError, 'Uncalibrated allocation ceiling' unless reference > 0 && ceiling == (reference * 1.25).ceil + 32
|
|
500
595
|
end
|
|
501
596
|
|
|
502
|
-
|
|
597
|
+
outputs_by_source = {}
|
|
598
|
+
canonical_counters = nil
|
|
503
599
|
(self_pairs + pairs).each_with_index do |pair, index|
|
|
504
600
|
expected_order = if index < self_pairs.length
|
|
505
601
|
%w[baseline baseline]
|
|
@@ -530,8 +626,10 @@ module HaveAPI
|
|
|
530
626
|
raise ContractError, 'Worker workload order mismatch'
|
|
531
627
|
end
|
|
532
628
|
|
|
533
|
-
|
|
534
|
-
|
|
629
|
+
outputs_by_source[actual_sha] ||= sample.fetch('outputs')
|
|
630
|
+
unless same_outputs(outputs_by_source.fetch(actual_sha), sample.fetch('outputs'))
|
|
631
|
+
raise ContractError, "Response identity differs within revision #{actual_sha}"
|
|
632
|
+
end
|
|
535
633
|
end
|
|
536
634
|
end
|
|
537
635
|
classifications = %w[wall_seconds cpu_seconds].to_h do |metric|
|
|
@@ -566,9 +664,10 @@ module HaveAPI
|
|
|
566
664
|
'pass'
|
|
567
665
|
end
|
|
568
666
|
item.slice('id', 'dimensions', 'iterations')
|
|
569
|
-
.merge('classification' => classification, 'timing' => classifications,
|
|
667
|
+
.merge('classification' => classification, 'timing' => classifications, 'responses' => response_changes(pairs),
|
|
570
668
|
'correctness' => { 'status' => 'pass', 'validated_samples' => 2 * (self_pairs.length + pairs.length),
|
|
571
|
-
'operations_per_sample' => iterations, 'distinct_responses' =>
|
|
669
|
+
'operations_per_sample' => iterations, 'distinct_responses' => outputs_by_source.fetch(@samples.fetch('baseline_sha')).length,
|
|
670
|
+
'distinct_responses_by_source' => outputs_by_source.transform_values(&:length) },
|
|
572
671
|
'pilot' => item.fetch('pilot', {}).slice('selected_iterations', 'warmup_iterations', 'target_seconds',
|
|
573
672
|
'baseline_batch_seconds', 'adequate_signal', 'capped', 'calibration_adjusted'),
|
|
574
673
|
'allocations' => { 'baseline_median' => base_allocations, 'candidate_median' => candidate_allocations,
|
|
@@ -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.
|
|
@@ -165,7 +196,8 @@ Construction copies and freezes Hash, Array and String data without freezing
|
|
|
165
196
|
the caller's containers. Each request owns its description snapshot.
|
|
166
197
|
`context.with(...)` creates a distinct frozen context, sharing unchanged fields
|
|
167
198
|
by identity and normalizing only explicitly supplied fields. A shallow-frozen
|
|
168
|
-
input container still needs copying; explicitly supplying an existing owned
|
|
199
|
+
input container still needs copying; explicitly supplying an existing owned
|
|
200
|
+
Hash,
|
|
169
201
|
Array or String also copies it. Renderers must treat context-owned data as
|
|
170
202
|
read-only.
|
|
171
203
|
|
|
@@ -185,16 +217,21 @@ overview and every panel before rendering.
|
|
|
185
217
|
|
|
186
218
|
Generated-client adapters belong to their generator gems and consume context
|
|
187
219
|
readers without depending on the server gem. They resolve names using the
|
|
188
|
-
complete private generator model and never run a compiler, formatter or
|
|
189
|
-
|
|
190
|
-
|
|
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.
|
|
191
226
|
Derived contexts within one request share unchanged owned data. Supplying a
|
|
192
|
-
resource, action or sample still copies that supplied subtree, so a resource
|
|
227
|
+
resource, action or sample still copies that supplied subtree, so a resource
|
|
228
|
+
with
|
|
193
229
|
many actions can remain costly to render.
|
|
194
230
|
|
|
195
231
|
## Verification
|
|
196
232
|
|
|
197
|
-
`make test-documentation` checks renderers, contexts, profiles, rendered HTML
|
|
233
|
+
`make test-documentation` checks renderers, contexts, profiles, rendered HTML
|
|
234
|
+
and
|
|
198
235
|
local assets. Prepare the component bundles and JavaScript development
|
|
199
236
|
dependencies with `make release-deps` first. The target builds the JavaScript
|
|
200
237
|
package before compiling TypeScript examples against its public declarations;
|
|
@@ -220,8 +257,10 @@ nix develop .#server-ruby --command bash -c \
|
|
|
220
257
|
'make --no-print-directory benchmark-documentation > /tmp/documentation.json'
|
|
221
258
|
```
|
|
222
259
|
|
|
223
|
-
The target synchronizes shared documentation before running the harness.
|
|
224
|
-
|
|
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.
|
|
225
264
|
No network server, database, generated SDK compilation or formatter is involved.
|
|
226
265
|
The synthetic fixture uses Basic users `admin` and `member`, both with the local
|
|
227
266
|
test password `fixture-only`.
|
|
@@ -229,8 +268,10 @@ test password `fixture-only`.
|
|
|
229
268
|
Defaults measure two shapes with eight ordinary actions each: eight resources
|
|
230
269
|
with one action, and one resource with eight actions. Each ordinary action also
|
|
231
270
|
has a privileged sample; the first resource has an additional admin-only action.
|
|
232
|
-
Context lanes derive a prepared parent with a changed profile or a constant
|
|
233
|
-
|
|
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
|
|
234
275
|
dynamic renderer panels. Use a minimal smoke run or a larger repeated campaign:
|
|
235
276
|
|
|
236
277
|
```sh
|
|
@@ -242,8 +283,10 @@ make benchmark-documentation \
|
|
|
242
283
|
```
|
|
243
284
|
|
|
244
285
|
Run these `make` commands inside the prepared shell. `--shape custom --resources
|
|
245
|
-
N --actions N` controls both dimensions independently; `--lanes context`
|
|
246
|
-
|
|
286
|
+
N --actions N` controls both dimensions independently; `--lanes context`
|
|
287
|
+
isolates
|
|
288
|
+
derivation and `--lanes html` isolates requests. `--user member` selects
|
|
289
|
+
restricted
|
|
247
290
|
help. `--help` lists all options. `--rust` enables the fixture's matching Rust
|
|
248
291
|
profile, includes both Rust variants in HTML, and adds separate async/blocking
|
|
249
292
|
action and authentication adapter lanes. It requires the optional generator's
|
|
@@ -269,13 +312,16 @@ Rust lanes are never silently omitted. See the
|
|
|
269
312
|
|
|
270
313
|
Every lane reports each trial and the median wall seconds, process CPU seconds
|
|
271
314
|
and allocated objects, with the operation count per trial. Wall time uses a
|
|
272
|
-
monotonic clock. A full GC runs before each trial, outside its timer; GC
|
|
315
|
+
monotonic clock. A full GC runs before each trial, outside its timer; GC
|
|
316
|
+
triggered
|
|
273
317
|
by the measured work remains included. Mount/profile construction has separate
|
|
274
318
|
setup metrics. Ownership construction, description selection, warmups, output
|
|
275
319
|
validation, hashing and JSON serialization are outside lane measurements.
|
|
276
320
|
Reports include fixture dimensions, renderer IDs, profile mode, Ruby runtime,
|
|
277
|
-
host, Git HEAD/status/diff digest, loaded source digests, dependency versions
|
|
278
|
-
|
|
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
|
|
279
325
|
SHA-256; failed or missing output causes a nonzero exit.
|
|
280
326
|
|
|
281
327
|
For a before/after comparison, retain one exact harness and fixture and point
|