hecks 2.6.0 → 2.8.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/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
data/lib/hecks/bench/report.rb
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Hecks
|
|
4
|
-
module Bench
|
|
5
|
-
# Renders a `Suite.call` result as Markdown or JSON.
|
|
6
|
-
#
|
|
7
|
-
# The Markdown is what `docs/benchmarks.md` pastes in. It states the machine, the
|
|
8
|
-
# counts and the skipped targets next to the table, so a copied table is never a
|
|
9
|
-
# bare number.
|
|
10
|
-
module Report
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
# Renders the whole report as Markdown.
|
|
14
|
-
#
|
|
15
|
-
# @param result [Hash] the value `Suite.call` returned
|
|
16
|
-
# @return [String] the environment, the configuration, the table and any skipped targets
|
|
17
|
-
def markdown(result)
|
|
18
|
-
[environment_block(result[:environment]), configuration_block(result[:config]),
|
|
19
|
-
table(result[:results]), floor_block(result[:results]), skipped_block(result[:skipped])]
|
|
20
|
-
.compact.join("\n\n") << "\n"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Renders the whole report as JSON.
|
|
24
|
-
#
|
|
25
|
-
# @param result [Hash] the value `Suite.call` returned
|
|
26
|
-
# @return [String] pretty-printed JSON of `result`
|
|
27
|
-
def json(result)
|
|
28
|
-
JSON.pretty_generate(result)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Renders the results table.
|
|
32
|
-
#
|
|
33
|
-
# @param results [Array<Hash>] the `:results` of `Suite.call`
|
|
34
|
-
# @return [String] a Markdown table with one row per domain and target
|
|
35
|
-
def table(results)
|
|
36
|
-
header = ["| domain | target | commands/s | range across runs | p50 (us) | p99 (us) | drift |",
|
|
37
|
-
"|---|---|---:|---:|---:|---:|---:|"]
|
|
38
|
-
(header + results.map { |entry| row(entry) }).join("\n")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Renders one table row.
|
|
42
|
-
#
|
|
43
|
-
# @param entry [Hash] one aggregated result
|
|
44
|
-
# @return [String] the row as Markdown
|
|
45
|
-
def row(entry)
|
|
46
|
-
median = entry[:median]
|
|
47
|
-
low, high = entry[:throughput_range]
|
|
48
|
-
cells = [entry[:domain], entry[:target], number(median[:throughput_per_s], 0),
|
|
49
|
-
"#{number(low, 0)} to #{number(high, 0)}", number(median[:p50_us], 1),
|
|
50
|
-
number(median[:p99_us], 1), number(median[:drift], 2)]
|
|
51
|
-
"| #{cells.join(' | ')} |"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Formats a number with thousands separators.
|
|
55
|
-
#
|
|
56
|
-
# @param value [Numeric, nil] the number
|
|
57
|
-
# @param places [Integer] digits after the decimal point
|
|
58
|
-
# @return [String] the formatted number, or `"n/a"` for nil
|
|
59
|
-
def number(value, places)
|
|
60
|
-
return "n/a" if value.nil?
|
|
61
|
-
|
|
62
|
-
whole, fraction = format("%.#{places}f", value).split(".")
|
|
63
|
-
[whole.gsub(/(\d)(?=(\d{3})+\z)/, '\1,'), fraction].compact.join(".")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Renders the machine and toolchain.
|
|
67
|
-
#
|
|
68
|
-
# @param environment [Hash] the `:environment` of `Suite.call`
|
|
69
|
-
# @return [String] a Markdown bullet list
|
|
70
|
-
def environment_block(environment)
|
|
71
|
-
load = environment[:load_average]
|
|
72
|
-
lines = [
|
|
73
|
-
"- CPU: #{environment[:cpu]} (#{environment[:cores]} cores), #{environment[:memory_gib]} GiB",
|
|
74
|
-
"- OS: #{environment[:os]}",
|
|
75
|
-
"- Ruby: #{environment[:ruby]} (YJIT #{environment[:yjit] ? 'on' : 'off'})",
|
|
76
|
-
"- Rust: #{environment[:rustc]}; #{environment[:cargo]}",
|
|
77
|
-
"- Hecks: #{environment[:hecks]} at #{environment[:commit]}",
|
|
78
|
-
"- Load average (1 min) before and after: #{load[:before]} and #{load[:after]}"
|
|
79
|
-
]
|
|
80
|
-
lines << "- Postgres: #{environment[:postgres]}" if environment[:postgres]
|
|
81
|
-
lines.join("\n")
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Renders how hard the run pushed.
|
|
85
|
-
#
|
|
86
|
-
# @param config [Hash] the `:config` of `Suite.call`
|
|
87
|
-
# @return [String] one sentence with the warmup, iteration and run counts
|
|
88
|
-
def configuration_block(config)
|
|
89
|
-
"Warmup #{config[:warmup]} cycles, then #{config[:iterations]} timed cycles per run, " \
|
|
90
|
-
"median of #{config[:runs]} run(s), domains: #{config[:domains].join(', ')}."
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Renders the Rust round-trip floor, if a Rust target ran.
|
|
94
|
-
#
|
|
95
|
-
# @param results [Array<Hash>] the `:results` of `Suite.call`
|
|
96
|
-
# @return [String, nil] a sentence naming each domain's floor, or nil without a Rust result
|
|
97
|
-
def floor_block(results)
|
|
98
|
-
floors = results.select { |entry| entry[:target] == "rust" }
|
|
99
|
-
.map { |entry| "#{entry[:domain]} #{number(entry[:median][:roundtrip_floor_p50_us], 1)} us" }
|
|
100
|
-
return nil if floors.empty?
|
|
101
|
-
|
|
102
|
-
"Rust pipe round-trip floor (p50, a step refused at once): #{floors.join(', ')}."
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Renders the targets that did not run.
|
|
106
|
-
#
|
|
107
|
-
# @param skipped [Array<Hash>] the `:skipped` of `Suite.call`
|
|
108
|
-
# @return [String, nil] one line per skipped target, or nil when none were skipped
|
|
109
|
-
def skipped_block(skipped)
|
|
110
|
-
return nil if skipped.empty?
|
|
111
|
-
|
|
112
|
-
(["Skipped:"] + skipped.map { |entry| "- #{entry[:target]}: #{entry[:reason]}" }).join("\n")
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
require_relative "run"
|
|
2
|
-
|
|
3
|
-
module Hecks
|
|
4
|
-
module Bench
|
|
5
|
-
# Measures the Ruby runtime, in-process, on one persistence adapter.
|
|
6
|
-
#
|
|
7
|
-
# The domain is booted from a throwaway copy through `Fuzzing::IsolatedBoot`, so a
|
|
8
|
-
# run always starts from an empty store and never touches the example's own data.
|
|
9
|
-
# That is the same isolation `bin/fuzz --adapter` uses. Both Postgres adapters write
|
|
10
|
-
# to a uniquely named schema in the `hecks_bench` database, which is dropped after the
|
|
11
|
-
# run, so a concurrent `bin/fuzz` and its shared `hecks_fuzz` schema are never touched.
|
|
12
|
-
#
|
|
13
|
-
# Only `dispatch_flat` is inside the timer. Boot, setup commands and warmup are not.
|
|
14
|
-
module RubyRunner
|
|
15
|
-
# The adapters this runner can bind, as `IsolatedBoot` names them.
|
|
16
|
-
ADAPTERS = %i[memory sqlite postgres postgres_era].freeze
|
|
17
|
-
|
|
18
|
-
# The adapters that need a server and a scratch schema.
|
|
19
|
-
POSTGRES_ADAPTERS = %i[postgres postgres_era].freeze
|
|
20
|
-
|
|
21
|
-
# The scratch database both Postgres adapters run in.
|
|
22
|
-
DATABASE = "hecks_bench".freeze
|
|
23
|
-
|
|
24
|
-
module_function
|
|
25
|
-
|
|
26
|
-
# Boots the workload's domain on `adapter` and times its commands.
|
|
27
|
-
#
|
|
28
|
-
# @param workload [Workload] the commands to dispatch
|
|
29
|
-
# @param adapter [Symbol] one of `ADAPTERS`
|
|
30
|
-
# @param warmup [Integer] cycles dispatched and discarded before timing starts
|
|
31
|
-
# @param iterations [Integer] cycles timed
|
|
32
|
-
# @return [Run] the timings of the measured cycles
|
|
33
|
-
# @raise [ArgumentError] if `adapter` is not one of `ADAPTERS`
|
|
34
|
-
# @raise [StandardError] whatever refusal a command raises, warmup included, since a
|
|
35
|
-
# benchmark that swallowed one would be timing something other than its workload
|
|
36
|
-
def call(workload, adapter:, warmup:, iterations:)
|
|
37
|
-
unless ADAPTERS.include?(adapter)
|
|
38
|
-
raise ArgumentError, "unknown adapter #{adapter.inspect} — one of #{ADAPTERS.join(', ')}"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
load_dependencies
|
|
42
|
-
schema = "bench_#{Process.pid}_#{rand(1 << 32).to_s(16)}"
|
|
43
|
-
quietly_for_postgres do
|
|
44
|
-
boot(workload, adapter, schema) do |runtime|
|
|
45
|
-
workload.setup.each { |step| dispatch(runtime, step) }
|
|
46
|
-
measure(runtime, workload, warmup: warmup, iterations: iterations)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
ensure
|
|
50
|
-
drop_schema(schema) if schema && POSTGRES_ADAPTERS.include?(adapter)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Boots the workload's domain on `adapter` in an isolated copy.
|
|
54
|
-
#
|
|
55
|
-
# @param workload [Workload] the domain to boot
|
|
56
|
-
# @param adapter [Symbol] one of `ADAPTERS`
|
|
57
|
-
# @param schema [String] the scratch schema name, used only by the Postgres adapters
|
|
58
|
-
# @yield [Hecks::Runtime] the booted domain
|
|
59
|
-
# @return [Object] whatever the block returns
|
|
60
|
-
def boot(workload, adapter, schema)
|
|
61
|
-
Hecks::Fuzzing::IsolatedBoot.call(workload.domain_path, adapter: adapter, database: DATABASE,
|
|
62
|
-
schema: schema, scratch: { database: DATABASE, schema: schema }) do |copy|
|
|
63
|
-
yield Hecks.boot(copy, install_facade: false)
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Keeps Postgres's `schema already exists` notices off stderr while a block runs.
|
|
68
|
-
#
|
|
69
|
-
# Every aggregate opens its own connection and each one repeats the notice, which
|
|
70
|
-
# buries the progress lines. The server setting is restored afterwards.
|
|
71
|
-
#
|
|
72
|
-
# @yield the work to run with notices quieted
|
|
73
|
-
# @return [Object] whatever the block returns
|
|
74
|
-
def quietly_for_postgres
|
|
75
|
-
previous = ENV.fetch("PGOPTIONS", nil)
|
|
76
|
-
ENV["PGOPTIONS"] = [previous, "-c client_min_messages=warning"].compact.join(" ")
|
|
77
|
-
yield
|
|
78
|
-
ensure
|
|
79
|
-
ENV["PGOPTIONS"] = previous
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Dispatches warmup cycles untimed, then times the measured ones.
|
|
83
|
-
#
|
|
84
|
-
# @param runtime [Hecks::Runtime] the booted domain
|
|
85
|
-
# @param workload [Workload] the commands to dispatch
|
|
86
|
-
# @param warmup [Integer] cycles to discard
|
|
87
|
-
# @param iterations [Integer] cycles to time
|
|
88
|
-
# @return [Run] the timings of the measured cycles
|
|
89
|
-
def measure(runtime, workload, warmup:, iterations:)
|
|
90
|
-
warmup.times { |n| workload.cycle(n).each { |step| dispatch(runtime, step) } }
|
|
91
|
-
GC.start
|
|
92
|
-
samples = []
|
|
93
|
-
started = now
|
|
94
|
-
iterations.times do |n|
|
|
95
|
-
workload.cycle(warmup + n).each { |step| samples << [step.verb, time { dispatch(runtime, step) }] }
|
|
96
|
-
end
|
|
97
|
-
Run.new(samples: samples, wall_seconds: now - started)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Sends one step to the runtime.
|
|
101
|
-
#
|
|
102
|
-
# @param runtime [Hecks::Runtime] the booted domain
|
|
103
|
-
# @param step [Workload::Step] the command to dispatch
|
|
104
|
-
# @return [Object] whatever `dispatch_flat` answers
|
|
105
|
-
def dispatch(runtime, step)
|
|
106
|
-
runtime.dispatch_flat(step.verb, step.ruby_args)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Reads the monotonic clock.
|
|
110
|
-
#
|
|
111
|
-
# @return [Float] seconds on the monotonic clock
|
|
112
|
-
def now
|
|
113
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Times a block.
|
|
117
|
-
#
|
|
118
|
-
# @yield the work to time
|
|
119
|
-
# @return [Float] seconds the block took
|
|
120
|
-
def time
|
|
121
|
-
started = now
|
|
122
|
-
yield
|
|
123
|
-
now - started
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Loads what booting a domain on every adapter needs.
|
|
127
|
-
#
|
|
128
|
-
# @return [void]
|
|
129
|
-
def load_dependencies
|
|
130
|
-
require "hecks"
|
|
131
|
-
require "hecks/fuzzing"
|
|
132
|
-
require "hecks/ports/persistence/plugins/era"
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Removes the schema a Postgres run created, so repeated runs do not accumulate.
|
|
136
|
-
#
|
|
137
|
-
# @param schema [String] the schema name this run used
|
|
138
|
-
# @return [void]
|
|
139
|
-
def drop_schema(schema)
|
|
140
|
-
require "pg"
|
|
141
|
-
connection = PG.connect(dbname: DATABASE, connect_timeout: 2)
|
|
142
|
-
connection.exec("SET client_min_messages = warning")
|
|
143
|
-
connection.exec("DROP SCHEMA IF EXISTS #{connection.quote_ident(schema)} CASCADE")
|
|
144
|
-
rescue LoadError, PG::Error
|
|
145
|
-
nil
|
|
146
|
-
ensure
|
|
147
|
-
connection&.close
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
data/lib/hecks/bench/run.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Bench
|
|
3
|
-
# The raw timings of one measured pass over a workload on one runtime.
|
|
4
|
-
#
|
|
5
|
-
# Runners produce these and know nothing about reporting. {#summary} is the only
|
|
6
|
-
# place raw samples become the figures a report prints.
|
|
7
|
-
class Run
|
|
8
|
-
# @return [Array<Array(String, Float)>] each timed command's verb and latency in
|
|
9
|
-
# seconds, in dispatch order
|
|
10
|
-
attr_reader :samples
|
|
11
|
-
|
|
12
|
-
# @return [Float] seconds from the first timed command's start to the last one's end
|
|
13
|
-
attr_reader :wall_seconds
|
|
14
|
-
|
|
15
|
-
# @return [Hash{Symbol => Object}] facts a runner wants reported alongside the timings
|
|
16
|
-
attr_reader :extras
|
|
17
|
-
|
|
18
|
-
# Builds a run.
|
|
19
|
-
#
|
|
20
|
-
# @param samples [Array<Array(String, Float)>] verb and latency seconds per command
|
|
21
|
-
# @param wall_seconds [Float] the wall-clock duration of the whole timed pass
|
|
22
|
-
# @param extras [Hash{Symbol => Object}] runner-specific facts, such as a round-trip floor
|
|
23
|
-
def initialize(samples:, wall_seconds:, extras: {})
|
|
24
|
-
@samples = samples
|
|
25
|
-
@wall_seconds = wall_seconds
|
|
26
|
-
@extras = extras
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Reduces the samples to the figures a report prints.
|
|
30
|
-
#
|
|
31
|
-
# @return [Hash{Symbol => Object}] `:commands`, `:wall_seconds`, `:throughput_per_s`,
|
|
32
|
-
# the `Stats.latency` figures, `:drift`, `:by_verb` (each verb's count, p50 and p99),
|
|
33
|
-
# and any `extras`
|
|
34
|
-
def summary
|
|
35
|
-
seconds = samples.map(&:last)
|
|
36
|
-
{
|
|
37
|
-
commands: samples.size,
|
|
38
|
-
wall_seconds: wall_seconds.round(4),
|
|
39
|
-
throughput_per_s: (samples.size / wall_seconds).round(1),
|
|
40
|
-
**Stats.latency(seconds),
|
|
41
|
-
drift: Stats.drift(seconds),
|
|
42
|
-
by_verb: by_verb
|
|
43
|
-
}.merge(extras)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def by_verb
|
|
49
|
-
samples.group_by(&:first).transform_values do |group|
|
|
50
|
-
Stats.latency(group.map(&:last)).slice(:count, :p50_us, :p99_us)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "open3"
|
|
3
|
-
require_relative "run"
|
|
4
|
-
|
|
5
|
-
module Hecks
|
|
6
|
-
module Bench
|
|
7
|
-
# Measures the native Rust binary through its streaming mode.
|
|
8
|
-
#
|
|
9
|
-
# `rust --serve` reads one JSON step per line and answers one JSON line each, keeping
|
|
10
|
-
# its in-memory store alive between them. This runner writes a step, waits for the
|
|
11
|
-
# answer line, and times the whole round trip, so the figures include two pipe
|
|
12
|
-
# crossings and the binary's own JSON parse and print. Process start-up is outside
|
|
13
|
-
# the timer.
|
|
14
|
-
#
|
|
15
|
-
# Alongside each run it measures a **round-trip floor**: the same round trip for a
|
|
16
|
-
# step the binary refuses immediately, which is what the pipes cost with almost no
|
|
17
|
-
# domain work behind them.
|
|
18
|
-
module RustRunner
|
|
19
|
-
# Where the Rust crate lives.
|
|
20
|
-
RUST_DIR = File.expand_path("../../../rust", __dir__)
|
|
21
|
-
|
|
22
|
-
# How many refused-at-once steps make up the round-trip floor's sample.
|
|
23
|
-
FLOOR_SAMPLES = 500
|
|
24
|
-
|
|
25
|
-
# A `cargo build` that exited non-zero.
|
|
26
|
-
class BuildFailed < StandardError; end
|
|
27
|
-
|
|
28
|
-
# A benchmarked step the binary refused or could not answer.
|
|
29
|
-
class StepRefused < StandardError; end
|
|
30
|
-
|
|
31
|
-
module_function
|
|
32
|
-
|
|
33
|
-
# Says why the Rust target cannot run, if it cannot.
|
|
34
|
-
#
|
|
35
|
-
# @param binary [String, nil] an explicit pre-built binary, which needs no toolchain
|
|
36
|
-
# @return [String, nil] nil when the target can run, otherwise one sentence saying why
|
|
37
|
-
def unavailable_reason(binary: nil)
|
|
38
|
-
return "the binary #{binary} does not exist or is not executable" if binary && !File.executable?(binary)
|
|
39
|
-
return nil if binary || Environment.capture("cargo", "-V") != "unknown"
|
|
40
|
-
|
|
41
|
-
"`cargo` is not on PATH and no --rust-binary was given"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Builds the release binary for one domain's Cargo feature and pins a copy of it.
|
|
45
|
-
#
|
|
46
|
-
# The copy has its own name so building another domain's feature afterwards cannot
|
|
47
|
-
# replace it.
|
|
48
|
-
#
|
|
49
|
-
# @param feature [String] the domain's Cargo feature, its directory name
|
|
50
|
-
# @param rust_dir [String] the Rust crate's directory
|
|
51
|
-
# @return [String] the path of the pinned release binary
|
|
52
|
-
# @raise [BuildFailed] if cargo exits non-zero or leaves no binary
|
|
53
|
-
def build(feature, rust_dir: RUST_DIR)
|
|
54
|
-
output, status = Open3.capture2e("cargo", "build", "--release", "--no-default-features",
|
|
55
|
-
"--features", feature, chdir: rust_dir)
|
|
56
|
-
unless status.success?
|
|
57
|
-
raise BuildFailed,
|
|
58
|
-
"cargo build --release --features #{feature} failed:\n#{output.lines.last(15).join}"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
target = File.join(ENV.fetch("CARGO_TARGET_DIR", File.join(rust_dir, "target")), "release")
|
|
62
|
-
pinned = File.join(target, "rust-bench-#{feature}")
|
|
63
|
-
FileUtils.cp(File.join(target, "rust"), pinned)
|
|
64
|
-
pinned
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Starts the binary in serve mode and times the workload's commands.
|
|
68
|
-
#
|
|
69
|
-
# @param workload [Workload] the commands to send
|
|
70
|
-
# @param binary [String] the path of a binary built for `workload`'s domain
|
|
71
|
-
# @param warmup [Integer] cycles sent and discarded before timing starts
|
|
72
|
-
# @param iterations [Integer] cycles timed
|
|
73
|
-
# @return [Run] the timings, with the round-trip floor in its extras as
|
|
74
|
-
# `:roundtrip_floor_p50_us`
|
|
75
|
-
# @raise [StepRefused] if the binary refuses any step, warmup included
|
|
76
|
-
def call(workload, binary:, warmup:, iterations:)
|
|
77
|
-
IO.popen([binary, "--serve"], "r+") do |pipe|
|
|
78
|
-
pipe.sync = true
|
|
79
|
-
workload.setup.each { |step| ask(pipe, step.to_json_line) }
|
|
80
|
-
warmup.times { |n| workload.cycle(n).each { |step| ask(pipe, step.to_json_line) } }
|
|
81
|
-
run = measure(pipe, workload, warmup: warmup, iterations: iterations)
|
|
82
|
-
Run.new(samples: run.samples, wall_seconds: run.wall_seconds,
|
|
83
|
-
extras: { roundtrip_floor_p50_us: floor(pipe) })
|
|
84
|
-
ensure
|
|
85
|
-
pipe.close_write unless pipe.closed?
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Times the measured cycles.
|
|
90
|
-
#
|
|
91
|
-
# @param pipe [IO] the binary's stdin and stdout
|
|
92
|
-
# @param workload [Workload] the commands to send
|
|
93
|
-
# @param warmup [Integer] cycles already sent, so record names continue from there
|
|
94
|
-
# @param iterations [Integer] cycles to time
|
|
95
|
-
# @return [Run] the timings, without extras
|
|
96
|
-
def measure(pipe, workload, warmup:, iterations:)
|
|
97
|
-
samples = []
|
|
98
|
-
started = now
|
|
99
|
-
iterations.times do |n|
|
|
100
|
-
workload.cycle(warmup + n).each do |step|
|
|
101
|
-
line = step.to_json_line
|
|
102
|
-
began = now
|
|
103
|
-
answer = round_trip(pipe, line)
|
|
104
|
-
samples << [step.verb, now - began]
|
|
105
|
-
check(answer, step.verb)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
Run.new(samples: samples, wall_seconds: now - started)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Measures the pipes with almost no work behind them.
|
|
112
|
-
#
|
|
113
|
-
# @param pipe [IO] the binary's stdin and stdout
|
|
114
|
-
# @return [Float] the p50 microseconds of {FLOOR_SAMPLES} round trips for an empty step,
|
|
115
|
-
# which the binary refuses without touching its store
|
|
116
|
-
def floor(pipe)
|
|
117
|
-
seconds = Array.new(FLOOR_SAMPLES) do
|
|
118
|
-
began = now
|
|
119
|
-
round_trip(pipe, "{}")
|
|
120
|
-
now - began
|
|
121
|
-
end
|
|
122
|
-
Stats.micros(Stats.percentile(seconds, 0.5))
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Sends one step and returns the answer line, untimed.
|
|
126
|
-
#
|
|
127
|
-
# @param pipe [IO] the binary's stdin and stdout
|
|
128
|
-
# @param line [String] the step as one JSON line
|
|
129
|
-
# @return [void]
|
|
130
|
-
# @raise [StepRefused] if the binary refuses the step
|
|
131
|
-
def ask(pipe, line)
|
|
132
|
-
check(round_trip(pipe, line), line[0, 80])
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Writes one line and reads one line back.
|
|
136
|
-
#
|
|
137
|
-
# @param pipe [IO] the binary's stdin and stdout
|
|
138
|
-
# @param line [String] the step as one JSON line
|
|
139
|
-
# @return [String] the answer line, or an empty string if the binary closed its output
|
|
140
|
-
def round_trip(pipe, line)
|
|
141
|
-
pipe.write(line, "\n")
|
|
142
|
-
pipe.gets.to_s
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Refuses an answer that is not a success.
|
|
146
|
-
#
|
|
147
|
-
# @param answer [String] the binary's answer line
|
|
148
|
-
# @param what [String] what was asked, for the error message
|
|
149
|
-
# @return [void]
|
|
150
|
-
# @raise [StepRefused] unless `answer` starts with `{"ok":true`
|
|
151
|
-
def check(answer, what)
|
|
152
|
-
return if answer.start_with?('{"ok":true')
|
|
153
|
-
|
|
154
|
-
raise StepRefused, "#{what} was not accepted: #{answer.strip.empty? ? 'the binary closed its output' : answer.strip}"
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Reads the monotonic clock.
|
|
158
|
-
#
|
|
159
|
-
# @return [Float] seconds on the monotonic clock
|
|
160
|
-
def now
|
|
161
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
data/lib/hecks/bench/stats.rb
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Bench
|
|
3
|
-
# Order statistics over a list of latency samples.
|
|
4
|
-
#
|
|
5
|
-
# Percentiles use the nearest-rank method: the smallest sample with at least
|
|
6
|
-
# that fraction of the samples at or below it. It never interpolates, so every
|
|
7
|
-
# reported figure is a latency that actually happened. With fewer than 100
|
|
8
|
-
# samples the p99 is simply the maximum.
|
|
9
|
-
module Stats
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# Reads one percentile from a list of samples.
|
|
13
|
-
#
|
|
14
|
-
# @param samples [Array<Float>] the samples, in any order
|
|
15
|
-
# @param fraction [Float] the percentile as a fraction, e.g. `0.99`
|
|
16
|
-
# @return [Float, nil] the nearest-rank percentile, or nil when `samples` is empty
|
|
17
|
-
def percentile(samples, fraction)
|
|
18
|
-
return nil if samples.empty?
|
|
19
|
-
|
|
20
|
-
sorted = samples.sort
|
|
21
|
-
sorted[[(fraction * sorted.size).ceil - 1, 0].max]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Finds the middle value of a list, averaging the two middle values of an even one.
|
|
25
|
-
#
|
|
26
|
-
# @param values [Array<Numeric>] the values, in any order
|
|
27
|
-
# @return [Float, nil] the median, or nil when `values` is empty
|
|
28
|
-
def median(values)
|
|
29
|
-
return nil if values.empty?
|
|
30
|
-
|
|
31
|
-
sorted = values.sort
|
|
32
|
-
middle = sorted.size / 2
|
|
33
|
-
sorted.size.odd? ? sorted[middle].to_f : (sorted[middle - 1] + sorted[middle]) / 2.0
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Summarizes one run's latency samples.
|
|
37
|
-
#
|
|
38
|
-
# @param seconds [Array<Float>] per-command latencies in seconds, in dispatch order
|
|
39
|
-
# @return [Hash{Symbol => Numeric}] `:count`, and `:p50_us`, `:p99_us`, `:mean_us` and
|
|
40
|
-
# `:max_us` in microseconds
|
|
41
|
-
def latency(seconds)
|
|
42
|
-
{
|
|
43
|
-
count: seconds.size,
|
|
44
|
-
p50_us: micros(percentile(seconds, 0.50)),
|
|
45
|
-
p99_us: micros(percentile(seconds, 0.99)),
|
|
46
|
-
mean_us: micros(seconds.sum / seconds.size),
|
|
47
|
-
max_us: micros(seconds.max)
|
|
48
|
-
}
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Compares how slow the last tenth of a run was against its first tenth.
|
|
52
|
-
#
|
|
53
|
-
# A ratio well above 1.0 means latency grows as the store fills, which a single
|
|
54
|
-
# p50 over the whole run would hide.
|
|
55
|
-
#
|
|
56
|
-
# @param seconds [Array<Float>] per-command latencies in dispatch order
|
|
57
|
-
# @return [Float, nil] the last tenth's p50 over the first tenth's, or nil when there
|
|
58
|
-
# are fewer than ten samples
|
|
59
|
-
def drift(seconds)
|
|
60
|
-
tenth = seconds.size / 10
|
|
61
|
-
return nil if tenth.zero?
|
|
62
|
-
|
|
63
|
-
(percentile(seconds.last(tenth), 0.5) / percentile(seconds.first(tenth), 0.5)).round(2)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Converts seconds to microseconds, rounded to a tenth.
|
|
67
|
-
#
|
|
68
|
-
# @param seconds [Float, nil] a duration in seconds
|
|
69
|
-
# @return [Float, nil] the duration in microseconds, or nil when `seconds` is nil
|
|
70
|
-
def micros(seconds)
|
|
71
|
-
seconds && (seconds * 1_000_000).round(1)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|