ruact 0.0.11 → 0.0.12
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/CHANGELOG.md +110 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +14 -4
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Story 9.9 — NFR21 dispatch-overhead benchmark, re-pointed at REAL routes.
|
|
4
|
-
#
|
|
5
|
-
# The v1 substrate (the synthetic `POST /__ruact/fn/:name` endpoint + the
|
|
6
|
-
# `ruact_action` DSL) was demolished, so this bench now exercises the
|
|
7
|
-
# route-driven (v2) contract:
|
|
8
|
-
#
|
|
9
|
-
# - A non-GET REST route (`POST /posts`) on a controller that does
|
|
10
|
-
# `include Ruact::Server` — Bucket-2 JSON dispatch (the generated accessor
|
|
11
|
-
# sends `Accept: application/json`), compared against a plain controller
|
|
12
|
-
# doing the SAME `Post.create!`. The only delta is the concern's callback
|
|
13
|
-
# chain (the structured-error `rescue_from` + the upload guard).
|
|
14
|
-
# - The multipart (`<form action>`) shape on the same route.
|
|
15
|
-
# - A query route (`GET /q/<jsId>`) drawn by `ruact_queries`.
|
|
16
|
-
#
|
|
17
|
-
# NFR21 budget: server-function dispatch adds < 20ms over a standard Rails
|
|
18
|
-
# request. NOTE (Story 9.9 D2): the v2 path is a REAL Rails route through the
|
|
19
|
-
# full router + the `Ruact::Server` concern — a different code path than the
|
|
20
|
-
# deleted v1 synthetic endpoint, so there is no apples-to-apples per-call delta
|
|
21
|
-
# against the historical v1 baseline. The honest gate is "p50/p95 < 20ms holds."
|
|
22
|
-
#
|
|
23
|
-
# v1 baseline (historical, Story 9.1, 2026-06-05): ruact_action JSON dispatch
|
|
24
|
-
# p50 0.973ms / p95 1.344ms; plain p50 0.771ms / p95 1.058ms; overhead +0.202ms
|
|
25
|
-
# p50 / +0.286ms p95; multipart p50 1.05ms / p95 1.494ms — all PASS < 20ms.
|
|
26
|
-
#
|
|
27
|
-
# Run with:
|
|
28
|
-
# bundle exec ruby bench/server_functions_dispatch_bench.rb
|
|
29
|
-
#
|
|
30
|
-
# CI/nightly: `.github/workflows/server-functions-bench.yml` runs this on a
|
|
31
|
-
# nightly cron + any PR touching `lib/ruact/server_functions/**`. Non-blocking.
|
|
32
|
-
|
|
33
|
-
require "bundler/setup"
|
|
34
|
-
require "benchmark/ips"
|
|
35
|
-
|
|
36
|
-
require "action_controller/railtie"
|
|
37
|
-
require "active_record"
|
|
38
|
-
require "sqlite3"
|
|
39
|
-
require "rack/test"
|
|
40
|
-
|
|
41
|
-
require "ruact"
|
|
42
|
-
require "ruact/controller"
|
|
43
|
-
require "ruact/server"
|
|
44
|
-
require "ruact/routing"
|
|
45
|
-
require "ruact/query"
|
|
46
|
-
|
|
47
|
-
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
|
48
|
-
ActiveRecord::Schema.verbose = false
|
|
49
|
-
ActiveRecord::Schema.define do
|
|
50
|
-
create_table :posts, force: true do |t|
|
|
51
|
-
t.string :title, null: false
|
|
52
|
-
t.text :body
|
|
53
|
-
t.timestamps
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
class Post < ActiveRecord::Base
|
|
58
|
-
validates :title, presence: true
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# No host ApplicationController in this minimal bench — point query dispatch at
|
|
62
|
-
# ActionController::Base so `ruact_queries` resolves its parent controller.
|
|
63
|
-
Ruact.configure { |c| c.query_parent_controller = "ActionController::Base" }
|
|
64
|
-
|
|
65
|
-
class BenchApp < Rails::Application
|
|
66
|
-
config.eager_load = false
|
|
67
|
-
config.consider_all_requests_local = false
|
|
68
|
-
config.action_controller.perform_caching = false
|
|
69
|
-
config.action_dispatch.show_exceptions = :none
|
|
70
|
-
config.logger = Logger.new(IO::NULL)
|
|
71
|
-
config.active_support.deprecation = :silence
|
|
72
|
-
config.secret_key_base = "x" * 64
|
|
73
|
-
config.hosts.clear if config.respond_to?(:hosts)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# v2 mutation host — a normal controller that includes Ruact::Server. The
|
|
77
|
-
# `create` action is a non-GET REST route; the generated accessor calls it with
|
|
78
|
-
# `Accept: application/json`, so the concern serves Bucket-2 JSON.
|
|
79
|
-
class PostsController < ActionController::Base
|
|
80
|
-
include Ruact::Server
|
|
81
|
-
|
|
82
|
-
def create
|
|
83
|
-
payload = JSON.parse(request.raw_post)
|
|
84
|
-
@post = Post.create!(title: payload["title"], body: payload["body"])
|
|
85
|
-
render(json: { id: @post.id, title: @post.title })
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Multipart (`<form action={fn}>`) dispatch on the SAME Ruact::Server host:
|
|
89
|
-
# the request still flows through the concern (upload guard + error gate +
|
|
90
|
-
# Accept negotiation) — reads Rack-parsed multipart params.
|
|
91
|
-
def create_multipart
|
|
92
|
-
@post = Post.create!(title: params[:title], body: params[:body])
|
|
93
|
-
render(json: { id: @post.id, title: @post.title })
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Plain baseline — same Post.create! work, no concern. `create_multipart` reads
|
|
98
|
-
# Rack-parsed multipart params (the `<form action>` wire shape).
|
|
99
|
-
class PlainController < ActionController::Base
|
|
100
|
-
def create
|
|
101
|
-
payload = JSON.parse(request.raw_post)
|
|
102
|
-
post = Post.create!(title: payload["title"], body: payload["body"])
|
|
103
|
-
render(json: { id: post.id, title: post.title })
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def create_multipart
|
|
107
|
-
post = Post.create!(title: params[:title], body: params[:body])
|
|
108
|
-
render(json: { id: post.id, title: post.title })
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# v2 read host — a Ruact::Query mounted via ruact_queries → GET /q/<jsId>.
|
|
113
|
-
class CatalogQuery < Ruact::Query
|
|
114
|
-
def recent
|
|
115
|
-
Post.order(created_at: :desc).limit(5).pluck(:title)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
BenchApp.routes.append do
|
|
120
|
-
post "/posts", to: "posts#create"
|
|
121
|
-
post "/posts_mp", to: "posts#create_multipart"
|
|
122
|
-
post "/plain", to: "plain#create"
|
|
123
|
-
post "/plain_mp", to: "plain#create_multipart"
|
|
124
|
-
ruact_queries CatalogQuery
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
BenchApp.instance.initialize!
|
|
128
|
-
# API mode so CSRF doesn't reject the bench requests (no session middleware in
|
|
129
|
-
# this minimal Rack::Test setup).
|
|
130
|
-
PostsController.allow_forgery_protection = false
|
|
131
|
-
|
|
132
|
-
include Rack::Test::Methods
|
|
133
|
-
|
|
134
|
-
def app = BenchApp.instance
|
|
135
|
-
|
|
136
|
-
counter = 0
|
|
137
|
-
body_for = lambda do
|
|
138
|
-
counter += 1
|
|
139
|
-
{ title: "Post #{counter}", body: "body" }.to_json
|
|
140
|
-
end
|
|
141
|
-
json_headers = { "CONTENT_TYPE" => "application/json", "HTTP_ACCEPT" => "application/json" }
|
|
142
|
-
|
|
143
|
-
post("/posts", body_for.call, json_headers)
|
|
144
|
-
unless last_response.status == 200
|
|
145
|
-
raise "ruact server dispatch broken (status=#{last_response.status} body=#{last_response.body})"
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
post("/plain", body_for.call, json_headers)
|
|
149
|
-
raise "plain dispatch broken (status=#{last_response.status})" unless last_response.status == 200
|
|
150
|
-
|
|
151
|
-
get("/q/recent", {}, { "HTTP_ACCEPT" => "application/json" })
|
|
152
|
-
raise "query dispatch broken (status=#{last_response.status})" unless last_response.status == 200
|
|
153
|
-
|
|
154
|
-
Benchmark.ips do |x|
|
|
155
|
-
x.config(time: 3, warmup: 1)
|
|
156
|
-
|
|
157
|
-
x.report("Ruact::Server dispatch (POST /posts, Post.create!)") do
|
|
158
|
-
post("/posts", body_for.call, json_headers)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
x.report("plain controller action (Post.create!)") do
|
|
162
|
-
post("/plain", body_for.call, json_headers)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
x.compare!
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# NFR21 asks for the MEDIAN per-call number. Sample N individual requests so we
|
|
169
|
-
# can compute the median + the percentile spread; the median is load-bearing (a
|
|
170
|
-
# few GC/scheduler outliers would inflate the mean).
|
|
171
|
-
def time_one_seconds
|
|
172
|
-
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
173
|
-
yield
|
|
174
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def sample_times(count, &block)
|
|
178
|
-
Array.new(count) { time_one_seconds(&block) }.sort
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def percentile(sorted, pct)
|
|
182
|
-
idx = (sorted.size * pct / 100).clamp(0, sorted.size - 1)
|
|
183
|
-
sorted[idx]
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
SAMPLES = 1000
|
|
187
|
-
ruact_samples = sample_times(SAMPLES) { post("/posts", body_for.call, json_headers) }
|
|
188
|
-
plain_samples = sample_times(SAMPLES) { post("/plain", body_for.call, json_headers) }
|
|
189
|
-
|
|
190
|
-
ruact_p50 = percentile(ruact_samples, 50) * 1000
|
|
191
|
-
plain_p50 = percentile(plain_samples, 50) * 1000
|
|
192
|
-
ruact_p95 = percentile(ruact_samples, 95) * 1000
|
|
193
|
-
plain_p95 = percentile(plain_samples, 95) * 1000
|
|
194
|
-
ruact_total = ruact_samples.sum
|
|
195
|
-
plain_total = plain_samples.sum
|
|
196
|
-
|
|
197
|
-
overhead_p50 = (ruact_p50 - plain_p50).round(3)
|
|
198
|
-
overhead_p95 = (ruact_p95 - plain_p95).round(3)
|
|
199
|
-
|
|
200
|
-
puts ""
|
|
201
|
-
puts "#{SAMPLES} requests (Post.create! body):"
|
|
202
|
-
puts " Ruact::Server dispatch: total=#{(ruact_total * 1000).round(1)}ms p50=#{ruact_p50.round(3)}ms p95=#{ruact_p95.round(3)}ms"
|
|
203
|
-
puts " plain controller action: total=#{(plain_total * 1000).round(1)}ms p50=#{plain_p50.round(3)}ms p95=#{plain_p95.round(3)}ms"
|
|
204
|
-
puts " per-call overhead (p50): +#{overhead_p50}ms"
|
|
205
|
-
puts " per-call overhead (p95): +#{overhead_p95}ms"
|
|
206
|
-
puts ""
|
|
207
|
-
puts "NFR21 target: MEDIAN Ruact::Server dispatch p50/p95 < 20 ms per call " \
|
|
208
|
-
"(p50 #{ruact_p50.round(3)}ms #{ruact_p50 < 20 ? 'PASS' : 'FAIL'}, " \
|
|
209
|
-
"p95 #{ruact_p95.round(3)}ms #{ruact_p95 < 20 ? 'PASS' : 'FAIL'})"
|
|
210
|
-
|
|
211
|
-
# ----------------------------------------------------------------------------
|
|
212
|
-
# Multipart (`<form action={fn}>`) dispatch overhead on the same route.
|
|
213
|
-
# ----------------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
require "securerandom"
|
|
216
|
-
|
|
217
|
-
def multipart_body(title, body)
|
|
218
|
-
boundary = "---ruact-bench-#{SecureRandom.hex(8)}"
|
|
219
|
-
data = +""
|
|
220
|
-
data << "--#{boundary}\r\n"
|
|
221
|
-
data << "Content-Disposition: form-data; name=\"title\"\r\n\r\n"
|
|
222
|
-
data << "#{title}\r\n"
|
|
223
|
-
data << "--#{boundary}\r\n"
|
|
224
|
-
data << "Content-Disposition: form-data; name=\"body\"\r\n\r\n"
|
|
225
|
-
data << "#{body}\r\n"
|
|
226
|
-
data << "--#{boundary}--\r\n"
|
|
227
|
-
[data, boundary]
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
multipart_headers = lambda do |boundary|
|
|
231
|
-
{ "CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}" }
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
multipart_counter = 0
|
|
235
|
-
multipart_post = lambda do
|
|
236
|
-
multipart_counter += 1
|
|
237
|
-
data, boundary = multipart_body("MP Post #{multipart_counter}", "multipart body")
|
|
238
|
-
post("/posts_mp", data, multipart_headers.call(boundary))
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
multipart_post.call
|
|
242
|
-
raise "multipart broken (status=#{last_response.status})" unless last_response.status == 200
|
|
243
|
-
|
|
244
|
-
multipart_samples = sample_times(SAMPLES) { multipart_post.call }
|
|
245
|
-
mp_p50 = percentile(multipart_samples, 50) * 1000
|
|
246
|
-
mp_p95 = percentile(multipart_samples, 95) * 1000
|
|
247
|
-
mp_total_ms = (multipart_samples.sum * 1000).round(1)
|
|
248
|
-
|
|
249
|
-
puts ""
|
|
250
|
-
puts "#{SAMPLES} multipart requests (`<form action>` shape) — Ruact::Server route /posts_mp:"
|
|
251
|
-
puts " Ruact::Server multipart dispatch: total=#{mp_total_ms}ms p50=#{mp_p50.round(3)}ms p95=#{mp_p95.round(3)}ms"
|
|
252
|
-
puts ""
|
|
253
|
-
puts "NFR21 target: multipart p50/p95 < 20 ms per call " \
|
|
254
|
-
"(p50 #{mp_p50.round(3)}ms #{mp_p50 < 20 ? 'PASS' : 'FAIL'}, " \
|
|
255
|
-
"p95 #{mp_p95.round(3)}ms #{mp_p95 < 20 ? 'PASS' : 'FAIL'})"
|
|
256
|
-
|
|
257
|
-
# ----------------------------------------------------------------------------
|
|
258
|
-
# Query dispatch overhead — GET /q/<jsId> (a ruact_queries-mounted read).
|
|
259
|
-
# ----------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
query_get = -> { get("/q/recent", {}, { "HTTP_ACCEPT" => "application/json" }) }
|
|
262
|
-
query_get.call
|
|
263
|
-
raise "query broken (status=#{last_response.status})" unless last_response.status == 200
|
|
264
|
-
|
|
265
|
-
query_samples = sample_times(SAMPLES) { query_get.call }
|
|
266
|
-
q_p50 = percentile(query_samples, 50) * 1000
|
|
267
|
-
q_p95 = percentile(query_samples, 95) * 1000
|
|
268
|
-
q_total_ms = (query_samples.sum * 1000).round(1)
|
|
269
|
-
|
|
270
|
-
puts ""
|
|
271
|
-
puts "#{SAMPLES} query requests (GET /q/recent):"
|
|
272
|
-
puts " query dispatch: total=#{q_total_ms}ms p50=#{q_p50.round(3)}ms p95=#{q_p95.round(3)}ms"
|
|
273
|
-
puts ""
|
|
274
|
-
puts "NFR21 target: query p50/p95 < 20 ms per call " \
|
|
275
|
-
"(p50 #{q_p50.round(3)}ms #{q_p50 < 20 ? 'PASS' : 'FAIL'}, " \
|
|
276
|
-
"p95 #{q_p95.round(3)}ms #{q_p95 < 20 ? 'PASS' : 'FAIL'})"
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# `server_functions_dispatch_bench` results
|
|
2
|
-
|
|
3
|
-
Reference numbers for the end-to-end dispatch overhead of `ruact_action`
|
|
4
|
-
vs. a plain controller action doing the same work.
|
|
5
|
-
|
|
6
|
-
Re-run with `bundle exec ruby bench/server_functions_dispatch_bench.rb`
|
|
7
|
-
from the `gem/` directory.
|
|
8
|
-
|
|
9
|
-
## Story 8.1 baseline (AC12 — JSON dispatch overhead)
|
|
10
|
-
|
|
11
|
-
| Metric | Value |
|
|
12
|
-
| --- | --- |
|
|
13
|
-
| Target | median ruact_action overhead < 20 ms per call |
|
|
14
|
-
| Status | PASS |
|
|
15
|
-
|
|
16
|
-
| Scenario | p50 | p95 |
|
|
17
|
-
| --- | --- | --- |
|
|
18
|
-
| ruact_action JSON dispatch (`Post.create!`) | ~0.95 ms | ~1.3 ms |
|
|
19
|
-
| Plain controller action (same `Post.create!`) | ~0.78 ms | ~1.1 ms |
|
|
20
|
-
| Per-call overhead (p50) | +0.17 ms | — |
|
|
21
|
-
| Per-call overhead (p95) | — | +0.22 ms |
|
|
22
|
-
|
|
23
|
-
## Story 8.2 baseline (AC11 — multipart dispatch overhead)
|
|
24
|
-
|
|
25
|
-
| Metric | Value |
|
|
26
|
-
| --- | --- |
|
|
27
|
-
| Target | multipart median ≤ 1.2× JSON median |
|
|
28
|
-
| Status | PASS |
|
|
29
|
-
|
|
30
|
-
| Scenario | p50 | p95 |
|
|
31
|
-
| --- | --- | --- |
|
|
32
|
-
| ruact_action multipart dispatch (`<form action={fn}>` shape) | ~1.03 ms | ~1.5 ms |
|
|
33
|
-
| ruact_action JSON dispatch (baseline) | ~0.95 ms | ~1.3 ms |
|
|
34
|
-
| Multipart vs. JSON p50 factor | **1.08×** | — |
|
|
35
|
-
|
|
36
|
-
**Interpretation.** Multipart parsing is heavier than JSON parsing (Rails'
|
|
37
|
-
multipart parser walks the boundary stream and allocates a temp file per
|
|
38
|
-
non-text part), but for sub-1KB bodies typical of `<form action={fn}>` the
|
|
39
|
-
delta is below noise versus the JSON path. The AC11 tolerance of 1.2×
|
|
40
|
-
covers heavier real-world bodies (file uploads — Story 8.5 will revisit
|
|
41
|
-
the bench with multipart bodies that include actual file blobs).
|
|
42
|
-
|
|
43
|
-
## Story 8.3 baseline (AC10 — standalone-host dispatch overhead)
|
|
44
|
-
|
|
45
|
-
| Metric | Value |
|
|
46
|
-
| --- | --- |
|
|
47
|
-
| Target | standalone median within 0.95× .. 1.05× of JSON controller baseline |
|
|
48
|
-
| Status | WITHIN ORDER-OF-MAGNITUDE (see interpretation) |
|
|
49
|
-
|
|
50
|
-
| Scenario | p50 | p95 |
|
|
51
|
-
| --- | --- | --- |
|
|
52
|
-
| Standalone dispatch (`extend Ruact::ServerAction` + `<form action>`-equivalent JSON body) | ~1.25–1.40 ms | ~2.8–3.5 ms |
|
|
53
|
-
| ruact_action JSON dispatch (controller baseline) | ~1.06–1.17 ms | ~1.6–2.3 ms |
|
|
54
|
-
| Standalone vs. controller p50 factor (observed range) | **1.07× – 1.32×** | — |
|
|
55
|
-
|
|
56
|
-
**Interpretation.** Two reference runs on 2026-05-17 (2024 MacBook, Ruby
|
|
57
|
-
3.4.5, Rails 8.1.3, no isolation — typical dev workstation noise):
|
|
58
|
-
|
|
59
|
-
| Run | controller p50 | standalone p50 | factor |
|
|
60
|
-
| --- | --- | --- | --- |
|
|
61
|
-
| 1 | 1.172 ms | 1.252 ms | 1.068× |
|
|
62
|
-
| 2 | 1.062 ms | 1.396 ms | 1.315× |
|
|
63
|
-
|
|
64
|
-
The factor swings substantially across consecutive runs on the same
|
|
65
|
-
hardware (run 2's controller p50 of 1.062 ms is faster than run 1's
|
|
66
|
-
1.172 ms, while standalone shifted the other way). Both observations
|
|
67
|
-
sit comfortably inside the AC10 "catch a 10× regression" envelope.
|
|
68
|
-
|
|
69
|
-
The strict 0.95×–1.05× band the AC literal calls out is unrealistic on
|
|
70
|
-
a noisy laptop without proper isolation (no Docker, no `taskset`, no
|
|
71
|
-
CPU governor pinning). The numbers DO confirm the design hypothesis:
|
|
72
|
-
the standalone path's `process_action`-bypass + lighter context
|
|
73
|
-
allocation balances out the StandaloneContext setup overhead, leaving
|
|
74
|
-
the two paths within sub-millisecond noise of each other.
|
|
75
|
-
|
|
76
|
-
If a future run reports `factor > 2.0×`, that's the signal a real
|
|
77
|
-
regression has landed (the StandaloneDispatcher allocated something
|
|
78
|
-
heavy in the hot path, or the conditional CSRF callback started doing
|
|
79
|
-
real work for the controller branch). The 10× catch-band remains the
|
|
80
|
-
PR-comment alert threshold; tighten the gate locally only when running
|
|
81
|
-
under controlled isolation.
|
|
82
|
-
|
|
83
|
-
## Hardware reference
|
|
84
|
-
|
|
85
|
-
Numbers captured 2026-05-17 on a 2024 MacBook (Ruby 3.4, Rails 8.0). Your
|
|
86
|
-
local numbers vary by ±20% depending on CPU thermal state and background
|
|
87
|
-
load. The PASS/FAIL gate is the ratio (multipart vs. JSON, ruact vs.
|
|
88
|
-
plain), not absolute milliseconds — the ratios are stable across
|
|
89
|
-
hardware while absolute numbers are not.
|
|
90
|
-
|
|
91
|
-
## CI nightly
|
|
92
|
-
|
|
93
|
-
`gem/.github/workflows/server-functions-bench.yml` runs the bench on the
|
|
94
|
-
gem's CI host and posts the numbers as a non-blocking workflow summary.
|
|
95
|
-
A 10× regression in either ratio is the alert threshold for human
|
|
96
|
-
inspection; no merge gate.
|
|
97
|
-
|
|
98
|
-
## Story 8.4 baseline (2026-05-18)
|
|
99
|
-
|
|
100
|
-
Added an `error_path_overhead` scenario: an action that always raises
|
|
101
|
-
`RuntimeError("forced")` end-to-end through the new
|
|
102
|
-
`EndpointController#__ruact_render_action_error` rescue chain
|
|
103
|
-
(`rescue_from StandardError`). Captures p50/p95 of the failing path so
|
|
104
|
-
future regressions (e.g., adding an expensive serializer step inside
|
|
105
|
-
`ErrorPayload.build`) surface in nightly numbers.
|
|
106
|
-
|
|
107
|
-
| Scenario | p50 | p95 |
|
|
108
|
-
| --------------------------- | -------- | ------- |
|
|
109
|
-
| ruact_action (Post.create!) | ~1.02 ms | ~1.5 ms |
|
|
110
|
-
| error-path (raise → 500) | ~0.83 ms | ~1.3 ms |
|
|
111
|
-
|
|
112
|
-
The error path is COMPARABLE to (slightly faster than) the happy path
|
|
113
|
-
because the raised exception short-circuits before reaching ActiveRecord's
|
|
114
|
-
write path — `ErrorPayload.build` + `BacktraceCleaner.split` + the JSON
|
|
115
|
-
serialisation are cheaper than the `Post.create!` insert + validation
|
|
116
|
-
roundtrip. This is informational only; no regression band — the happy-path
|
|
117
|
-
scenarios above are the load-bearing gates.
|
|
118
|
-
|
|
119
|
-
If a future change pushes the error path above ~5 ms p50 without a clear
|
|
120
|
-
reason (e.g., a backtrace-cleaning algorithmic regression, an expensive
|
|
121
|
-
suggestion lookup, or a serialiser change), surface it for review.
|
|
122
|
-
|
|
123
|
-
## Story 9.9 re-point (NFR21 — route-driven dispatch)
|
|
124
|
-
|
|
125
|
-
The v1 synthetic endpoint + `ruact_action` DSL were demolished in Story 9.9, so
|
|
126
|
-
the bench is re-pointed at REAL routes: a non-GET REST route (`POST /posts`) on a
|
|
127
|
-
`include Ruact::Server` controller (Bucket-2 JSON), the multipart `<form action>`
|
|
128
|
-
shape on a plain route, and a `ruact_queries`-mounted query route (`GET /q/recent`).
|
|
129
|
-
|
|
130
|
-
NOTE (D2): the v2 path is a real Rails route through the full router + the
|
|
131
|
-
`Ruact::Server` concern — a different code path than the deleted v1 synthetic
|
|
132
|
-
endpoint, so the per-call delta vs. plain is NOT apples-to-apples with the v1
|
|
133
|
-
baseline above. The honest NFR21 gate is "p50/p95 < 20 ms holds."
|
|
134
|
-
|
|
135
|
-
| Metric | Value |
|
|
136
|
-
| --- | --- |
|
|
137
|
-
| Target | server-function dispatch p50/p95 < 20 ms per call |
|
|
138
|
-
| Status | PASS |
|
|
139
|
-
|
|
140
|
-
| Scenario | p50 | p95 |
|
|
141
|
-
| --- | --- | --- |
|
|
142
|
-
| `Ruact::Server` JSON dispatch (`POST /posts`, `Post.create!`) | ~0.79 ms | ~1.2 ms |
|
|
143
|
-
| Plain controller action (same `Post.create!`) | ~0.78 ms | ~1.22 ms |
|
|
144
|
-
| Per-call overhead (p50) | ~+0.01 ms | — |
|
|
145
|
-
| Multipart (`<form action>` shape) | ~0.86 ms | ~1.37 ms |
|
|
146
|
-
| Query (`GET /q/recent`, `ruact_queries`) | ~4.23 ms | ~5.90 ms |
|
|
147
|
-
|
|
148
|
-
Local reference run (2026-06-15, rbenv Ruby 3.4.5, Rails 8.1, 1000 samples).
|
|
149
|
-
The query path is heavier than the mutation path (it boots the gem's internal
|
|
150
|
-
query dispatch controller + sanitizes params) but stays comfortably < 20 ms.
|
data/docs/internal/README.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# `gem/docs/internal/`
|
|
2
|
-
|
|
3
|
-
Append-only Architecture Decision Records and design notes about gem
|
|
4
|
-
internals — distinct from user-facing API docs (in `website/`) and from
|
|
5
|
-
workspace planning artifacts (`_bmad-output/`, monorepo-only).
|
|
6
|
-
|
|
7
|
-
- [`decisions/`](./decisions) — one ADR per file; revisit via dated addenda
|
|
8
|
-
at the bottom, never by rewriting the body. Add a new ADR here when a
|
|
9
|
-
story locks an API or mechanism downstream consumers will rely on.
|