upkeep-rails 0.1.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.

Potentially problematic release.


This version of upkeep-rails might be problematic. Click here for more details.

Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +424 -0
  4. data/docs/architecture/ambient-inputs-roadmap.md +306 -0
  5. data/docs/architecture/herb-roadmap.md +324 -0
  6. data/docs/architecture/identity-and-sharing.md +187 -0
  7. data/docs/architecture/query-dependencies.md +230 -0
  8. data/docs/cost-model-roadmap.md +703 -0
  9. data/docs/guides/getting-started.md +282 -0
  10. data/docs/handoff-2026-05-15.md +230 -0
  11. data/docs/production_roadmap.md +372 -0
  12. data/docs/shared-warm-scale-roadmap.md +214 -0
  13. data/docs/single-subscriber-cold-roadmap.md +192 -0
  14. data/docs/testing.md +113 -0
  15. data/lib/generators/upkeep/install/install_generator.rb +90 -0
  16. data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +31 -0
  17. data/lib/generators/upkeep/install/templates/subscription.js +107 -0
  18. data/lib/generators/upkeep/install/templates/upkeep.rb +6 -0
  19. data/lib/upkeep/active_record_query.rb +294 -0
  20. data/lib/upkeep/capture/request.rb +150 -0
  21. data/lib/upkeep/dag/subscription_shape.rb +244 -0
  22. data/lib/upkeep/dag.rb +370 -0
  23. data/lib/upkeep/delivery/action_cable_adapter.rb +43 -0
  24. data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
  25. data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
  26. data/lib/upkeep/delivery/transport.rb +194 -0
  27. data/lib/upkeep/delivery/turbo_streams.rb +275 -0
  28. data/lib/upkeep/delivery.rb +7 -0
  29. data/lib/upkeep/dependencies.rb +466 -0
  30. data/lib/upkeep/herb/developer_report.rb +116 -0
  31. data/lib/upkeep/herb/manifest_cache.rb +83 -0
  32. data/lib/upkeep/herb/manifest_diff.rb +183 -0
  33. data/lib/upkeep/herb/source_instrumenter.rb +84 -0
  34. data/lib/upkeep/herb/template_manifest.rb +377 -0
  35. data/lib/upkeep/invalidation/collection_append.rb +84 -0
  36. data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
  37. data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
  38. data/lib/upkeep/invalidation/collection_remove.rb +57 -0
  39. data/lib/upkeep/invalidation/planner.rb +341 -0
  40. data/lib/upkeep/invalidation.rb +7 -0
  41. data/lib/upkeep/rails/action_view_capture.rb +765 -0
  42. data/lib/upkeep/rails/cable/channel.rb +108 -0
  43. data/lib/upkeep/rails/cable/subscriber_identity.rb +214 -0
  44. data/lib/upkeep/rails/cable.rb +4 -0
  45. data/lib/upkeep/rails/client_subscription.rb +37 -0
  46. data/lib/upkeep/rails/configuration.rb +57 -0
  47. data/lib/upkeep/rails/controller_runtime.rb +137 -0
  48. data/lib/upkeep/rails/install.rb +28 -0
  49. data/lib/upkeep/rails/railtie.rb +36 -0
  50. data/lib/upkeep/rails/replay.rb +176 -0
  51. data/lib/upkeep/rails/testing.rb +36 -0
  52. data/lib/upkeep/rails.rb +276 -0
  53. data/lib/upkeep/replay.rb +408 -0
  54. data/lib/upkeep/runtime.rb +1075 -0
  55. data/lib/upkeep/shared_streams.rb +72 -0
  56. data/lib/upkeep/subscriptions/active_record_store.rb +292 -0
  57. data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +291 -0
  58. data/lib/upkeep/subscriptions/active_registry.rb +93 -0
  59. data/lib/upkeep/subscriptions/async_durable_writer.rb +136 -0
  60. data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
  61. data/lib/upkeep/subscriptions/layered_reverse_index.rb +122 -0
  62. data/lib/upkeep/subscriptions/persistent_reverse_index.rb +144 -0
  63. data/lib/upkeep/subscriptions/registrar.rb +36 -0
  64. data/lib/upkeep/subscriptions/reverse_index.rb +294 -0
  65. data/lib/upkeep/subscriptions/shape.rb +116 -0
  66. data/lib/upkeep/subscriptions/store.rb +159 -0
  67. data/lib/upkeep/subscriptions.rb +7 -0
  68. data/lib/upkeep/targeting.rb +135 -0
  69. data/lib/upkeep/version.rb +5 -0
  70. data/lib/upkeep-rails.rb +3 -0
  71. data/lib/upkeep.rb +14 -0
  72. data/upkeep-rails.gemspec +53 -0
  73. metadata +296 -0
@@ -0,0 +1,372 @@
1
+ # Upkeep Rails Production Roadmap
2
+
3
+ This repo contains the Rails-native reactive rendering gem and its in-repo
4
+ dogfood apps.
5
+ The production path is organized around structural proof, not host-maintained
6
+ configuration. The framework must derive dependency and identity inputs from
7
+ the render and request surfaces it observes.
8
+
9
+ ## Runtime Boundary
10
+
11
+ Upkeep keeps three production concerns separate:
12
+
13
+ - Subscription store: the queryable graph and reverse index used to decide
14
+ which subscribers and targets are affected by a lifecycle event. The current
15
+ production store is `config.upkeep.subscription_store = :active_record`.
16
+ `:memory` is an explicit development/test store only.
17
+ - Dispatcher: the async execution boundary that moves delivery work off the
18
+ request thread. The current dispatcher is process-local batching. A future
19
+ ActiveJob dispatcher can use Solid Queue, Redis-backed queues, or whichever
20
+ queue adapter the host app has configured.
21
+ - Broadcast bus: the final delivery path to browser connections. Upkeep uses
22
+ ActionCable; multi-worker deployments need a shared ActionCable adapter such
23
+ as Redis or Solid Cable.
24
+
25
+ Missing production subscription tables are a boot/configuration error, not a
26
+ reason to fall back to memory.
27
+
28
+ ## Design Boundary
29
+
30
+ Rails runtime renderers are the authority for render shape. Upkeep should hook
31
+ the normalized renderer choke points Rails already owns:
32
+
33
+ - `ActionView::TemplateRenderer` for template/page frames.
34
+ - `ActionView::PartialRenderer` and `ActionView::ObjectRenderer` for partial
35
+ and object-derived partial frames.
36
+ - `ActionView::CollectionRenderer` for collection render-site frames and child
37
+ partial frames.
38
+ - `ActionView::Renderer` only as an entry boundary when a lower hook cannot
39
+ prove the needed frame.
40
+
41
+ Herb is an address and validation layer. It can produce stable source-location
42
+ ids, verify single-root fragment eligibility, and improve static DOM tagging.
43
+ It must not become a catalog of every Rails render spelling. If support for a
44
+ render shape requires syntax-specific cases, the hook is at the wrong layer or
45
+ the runtime proof is incomplete.
46
+
47
+ ## Production Gates
48
+
49
+ ### Gate 1: Gem-Shaped Runtime
50
+
51
+ - Gem specification, load paths, `Upkeep::Rails` namespace, and `Railtie`.
52
+ - Idempotent install hooks for ActionView, Active Record, ActionDispatch,
53
+ Warden, and `ActiveSupport::CurrentAttributes`.
54
+ - Runtime code separated from proof-only templates and in-memory domain models.
55
+ - Test runner that exercises both the proof harness and Rails integration tests.
56
+
57
+ Exit criteria:
58
+
59
+ - A Rails app can require the gem and boot with hooks installed.
60
+ - Hooks are controlled by environment/config.
61
+ - `bin/test` runs the gem tests and the proof runner.
62
+
63
+ ### Gate 2: ActionView Graph Capture
64
+
65
+ - Template, partial, object partial, and collection render paths create graph
66
+ frames from Rails-resolved templates and locals.
67
+ - Collection renders create render-site frames without parsing render syntax.
68
+ - Active Record attribute and collection dependencies attach to the current
69
+ frame.
70
+ - Current user, Warden user, CurrentAttributes, session, cookie, and request
71
+ dependencies attach to the frame that reads them.
72
+
73
+ Exit criteria:
74
+
75
+ - Tests prove several render spellings produce equivalent graph shape through
76
+ Rails renderer hooks.
77
+ - The graph report explains selected targets through frame nodes, dependency
78
+ nodes, owner edges, and containment edges.
79
+
80
+ ### Gate 3: Canonical Replay Inputs
81
+
82
+ - Page replay stores controller/action, params, request environment, assigns,
83
+ lookup context, variants, formats, and observed ambient identity inputs.
84
+ - Render-site replay stores the Rails-resolved collection boundary and enough
85
+ canonical inputs to rerender membership for one subscriber.
86
+ - Fragment replay stores Rails-resolved template identity, locals, variants,
87
+ formats, and identity inputs.
88
+ - Replay never depends on host-declared dependency lists.
89
+
90
+ Exit criteria:
91
+
92
+ - Page, render-site, and fragment recipes render the selected target bytes in a
93
+ real Rails app.
94
+ - Replay fails closed when canonical inputs are not structurally available.
95
+
96
+ ### Gate 4: Subscription Storage And Reverse Index
97
+
98
+ - Store per-subscriber graph, identity signature, replay recipes, and selected
99
+ target metadata in the configured subscription store.
100
+ - Build reverse indexes from lifecycle-selectable dependency keys to
101
+ subscriber graph owners.
102
+ - Walk from changed dependency to affected frames across stored graphs.
103
+ - Deduplicate by subscriber, target id, and identity signature.
104
+ - Fail fast when production is configured for `:active_record` but the Upkeep
105
+ subscription tables are missing.
106
+
107
+ Exit criteria:
108
+
109
+ - An Active Record commit selects affected subscriber frames without scanning
110
+ every stored graph.
111
+ - Identity-partitioned targets produce separate payloads unless identity
112
+ signatures prove equivalence.
113
+
114
+ ### Gate 5: Delivery Pipeline
115
+
116
+ - Generate Turbo Stream replacement payloads for selected targets.
117
+ - Partition payloads by identity signature.
118
+ - Handle disconnect cleanup, retries, and backpressure.
119
+ - Keep delivery separate from correctness proof; delivery consumes selected
120
+ target payloads and does not decide who is eligible.
121
+
122
+ Exit criteria:
123
+
124
+ - Benchmark app tests prove no cross-subscriber payload sharing when identity
125
+ observations differ.
126
+ - Delivery reports expose selected dependency, frame, identity, and payload
127
+ digest evidence.
128
+
129
+ ### Gate 6: Production Hardening
130
+
131
+ - Fail-closed handling for unknown dependencies, missing replay inputs, and
132
+ unaddressable DOM roots.
133
+ - Benchmark workload coverage for the maintained Rails apps.
134
+ - Memory, fan-out, and replay latency measurements.
135
+ - Compatibility matrix across supported Rails versions.
136
+
137
+ Exit criteria:
138
+
139
+ - The gem can be used by the maintained benchmark app with no proof-only code
140
+ paths.
141
+ - The green bar includes unit tests, Rails integration tests, proof runner, and
142
+ benchmark app tests.
143
+
144
+ ## First Session
145
+
146
+ The first session targets Gates 1 and 2:
147
+
148
+ 1. Add gem/Railtie structure with idempotent install hooks.
149
+ 2. Add an ActionView capture proof that renders through Rails, not the in-memory
150
+ proof harness.
151
+ 3. Prove multiple render spellings produce graph frames through renderer hooks.
152
+ 4. Keep subscription storage and Turbo delivery out of the session unless the
153
+ renderer proof lands cleanly.
154
+
155
+ The session is complete when the repo has commits for the roadmap, gem
156
+ structure, and Rails-renderer graph capture with passing tests.
157
+
158
+ ## Second Session
159
+
160
+ The second session targets the first Gate 3 proof:
161
+
162
+ 1. Attach replay recipes to Rails ActionView page, fragment, and render-site
163
+ frames.
164
+ 2. Replay Rails-resolved templates with captured locals and fresh Active Record
165
+ records or relations.
166
+ 3. Prove fragment replay reflects a changed record attribute.
167
+ 4. Prove render-site replay reflects a changed collection membership.
168
+ 5. Keep controller/request replay and delivery out of the session unless the
169
+ ActionView replay proof lands cleanly.
170
+
171
+ The session is complete when Rails renderer tests prove selected ActionView
172
+ targets can be rerendered from frame recipes without a full-page extraction.
173
+
174
+ Current coverage:
175
+
176
+ - Page recipes rerender Rails templates with fresh Active Record relations.
177
+ - Render-site recipes rerender Rails collection boundaries with fresh
178
+ membership.
179
+ - Fragment recipes rerender Rails-resolved partial templates with fresh Active
180
+ Record records.
181
+
182
+ ## Third Session
183
+
184
+ The third session completes the first controller/request replay proof:
185
+
186
+ 1. Derive page replay from the controller attached to the Rails view context.
187
+ 2. Capture controller class, action name, request method, path, query string,
188
+ and path-parameter keys in the page frame metadata.
189
+ 3. Replay the page by re-entering `controller.class.action(action).call(env)`.
190
+ 4. Prove controller page replay reruns the action with request parameters and
191
+ fresh Active Record state.
192
+ 5. Keep durable subscription storage and delivery out of the session.
193
+
194
+ Current coverage:
195
+
196
+ - Controller-backed page recipes rerun the controller action.
197
+ - Request query parameters are preserved for replay.
198
+ - Controller replay uses fresh Active Record state through the action's own
199
+ query path.
200
+ - Path parameter values are carried in the replay environment; route-level
201
+ dispatch through a full Rails application remains a later Gate 3 step.
202
+
203
+ ## Fourth Session
204
+
205
+ The fourth session targets Gate 4:
206
+
207
+ 1. Store subscriber render graphs with their replay recipes and identity
208
+ signatures.
209
+ 2. Build a reverse index from observed dependency keys to graph owners.
210
+ 3. Plan invalidations from committed Active Record changes by walking from
211
+ matched dependency owners to containing frames.
212
+ 4. Deduplicate planned work by subscriber, target, and identity signature.
213
+ 5. Prove that identity-partitioned targets keep separate payloads and that
214
+ ambient identity observations stay attached to the replay target.
215
+
216
+ Current coverage:
217
+
218
+ - Active Record collection membership changes select render-site targets across
219
+ stored subscriptions through the reverse index.
220
+ - Active Record attribute changes select fragment targets without scanning each
221
+ stored graph.
222
+ - Duplicate subscriptions for the same subscriber, target, and identity
223
+ signature collapse to one planned target.
224
+ - `Current.user` identity observations partition fragment payloads for users
225
+ with different visibility.
226
+ - CurrentAttributes, session, cookie, request, and Warden observations are
227
+ preserved on page targets that read those surfaces.
228
+ - ActiveRecord is the explicit production subscription store. Memory storage is
229
+ still available for development/test, but it is no longer selected as a
230
+ hidden fallback.
231
+
232
+ ## Fifth Session
233
+
234
+ The fifth session targets the first Gate 5 delivery pass:
235
+
236
+ 1. Convert planned targets into Turbo Stream replacement payloads.
237
+ 2. Address page, fragment, and render-site targets through their observed
238
+ Upkeep DOM attributes.
239
+ 3. Partition payloads by subscriber, target, identity signature, and rendered
240
+ payload digest.
241
+ 4. Share rendered work across subscribers only when the target, identity
242
+ signature, sharing inputs, and replay recipe prove equivalence; merge
243
+ separately rendered payloads when the payload bytes also match.
244
+ 5. Report target, subscriber, identity, dependency, and payload digest evidence
245
+ for each stream and envelope.
246
+
247
+ Current coverage:
248
+
249
+ - Public fragment payloads and equivalent shared render-site payloads are
250
+ grouped before replay when the action, target, identity signature, sharing
251
+ signature, replay recipe, and deoptimization reason match.
252
+ - Render-site payloads target the collection wrapper through a Turbo Stream
253
+ `targets` selector.
254
+ - Identity-partitioned fragment payloads produce separate subscriber envelopes
255
+ when visible bytes differ.
256
+ - Delivery reports expose the selected target, identity signature, matched
257
+ dependency keys, subscriber ids, and payload digest.
258
+
259
+ Remaining Gate 5 work:
260
+
261
+ - Keep the Rails runtime path on the broadcast transport and the lower-level
262
+ connection transport covered as separate delivery boundaries.
263
+
264
+ ## Sixth Session
265
+
266
+ The sixth session targets the Gate 5 transport boundary:
267
+
268
+ 1. Track active subscriber connections by subscriber id.
269
+ 2. Deliver subscriber envelopes through an adapter protocol.
270
+ 3. Clean queued retry work when a subscriber disconnects.
271
+ 4. Retry failed sends up to a bounded attempt count.
272
+ 5. Apply per-connection backpressure with a bounded retry queue.
273
+ 6. Report delivery outcomes without selecting or filtering eligible targets.
274
+
275
+ Current coverage:
276
+
277
+ - Connected subscribers receive only their own envelopes.
278
+ - Disconnected subscribers are reported without invoking an adapter.
279
+ - Failed sends queue retry work and retry delivery preserves the original
280
+ envelope.
281
+ - Repeated failures drop after the configured retry limit.
282
+ - A full retry queue reports backpressure without growing retained work.
283
+ - The Rails runtime delivery path broadcasts through ActionCable subscriber
284
+ streams without depending on process-local WebSocket connection ownership.
285
+
286
+ Open Gate 5 work:
287
+
288
+ - Keep delivery outcome reporting aligned across the connection transport and
289
+ the broadcast transport.
290
+ - Add an ActiveJob dispatcher mode after the storage boundary is stable, so apps
291
+ can route async delivery through Solid Queue or another configured job
292
+ adapter without changing the subscription store.
293
+
294
+ ## Seventh Session
295
+
296
+ The seventh session targets the Rails cable broadcast adapter:
297
+
298
+ 1. Add an ActionCable delivery adapter for the transport boundary.
299
+ 2. Broadcast each subscriber envelope body to a canonical subscriber stream.
300
+ 3. Hide raw subscriber ids from stream names.
301
+ 4. Emit delivery notifications with stream, subscriber, digest, and byte-size
302
+ evidence.
303
+ 5. Prove ActionCable broadcast failures flow through transport retry handling.
304
+
305
+ Current coverage:
306
+
307
+ - The adapter broadcasts envelope bodies through `ActionCable.server.broadcast`
308
+ compatible servers.
309
+ - Subscriber stream names are derived from a digest of the subscriber id.
310
+ - `deliver.upkeep` notifications expose broadcast evidence without carrying
311
+ rendered payload bytes.
312
+ - Transport retries failed ActionCable broadcasts through the existing retry
313
+ queue.
314
+ - The Rails cable subscription side derives server identity from the
315
+ ActionCable connection and streams from each canonical identity stream.
316
+ - The maintained Upkeep benchmark app covers streamed delivery through the
317
+ derived subscriber stream.
318
+ - The install generator creates subscription storage, an initializer, an
319
+ ActionCable route mount, importmap wiring, and the browser subscription
320
+ bootstrap.
321
+ - The browser subscription bootstrap reads injected subscription markers,
322
+ subscribes through ActionCable, and applies Turbo Stream payloads.
323
+
324
+ ## Benchmark Layout
325
+
326
+ The maintained benchmark apps live under `benchmark/upkeep-app` and
327
+ `benchmark/turbo-app` in this repo. The committed benchmark tree contains
328
+ source and harness files; generated results, logs, temporary files, runtime
329
+ databases, Bundler lockfiles, Playwright install artifacts, and credentials are
330
+ excluded.
331
+
332
+ Current coverage:
333
+
334
+ - The Herb surface probe scans the in-repo benchmark apps.
335
+ - `benchmark/bin/run` defaults to the `matrix` family, which targets the
336
+ in-repo `upkeep-app` and `turbo-app`.
337
+ - The gem test suite verifies the benchmark tree points at the in-repo apps
338
+ and excludes generated runtime artifacts.
339
+ - `bin/test` runs the gem tests, both benchmark app test suites, and the proof
340
+ runner as one gate.
341
+ - The benchmark app suites cover authenticated board rendering, room rendering,
342
+ shared feed rendering, authorization boundaries, and helper-hidden render
343
+ idioms.
344
+ - The Rails cable subscription boundary derives subscriber identity from
345
+ ActionCable connection identifiers and rejects unidentified connections.
346
+ - Subscription graphs persist graph snapshots and reverse-index rows before
347
+ registration returns, then register into the active in-process reverse index
348
+ on the request path.
349
+ - Persisted Active Record subscription rows rehydrate recipes that render after
350
+ a fresh store load.
351
+ - Controller requests automatically capture render graphs, register successful
352
+ HTML responses, inject the client subscription marker, and drain committed
353
+ Active Record changes into streamed delivery.
354
+ - The Upkeep benchmark app covers automatic subscription registration and
355
+ streamed delivery through the canonical subscriber stream.
356
+ - Active Record collection dependencies derive table and column coverage from
357
+ the relation's Arel shape. Proven relations index concrete table/column
358
+ lookup keys; unrelated column writes do not select those collections; opaque
359
+ predicates and table sources are refused instead of registering broad
360
+ invalidation dependencies.
361
+ - Identity, request, session, and cookie dependencies stay on the stored graph
362
+ for replay and sharing, but do not create lifecycle invalidation lookup rows.
363
+ - Collection append delivery is gated by Active Record relation shape and
364
+ remains a delivery optimization over the canonical render-site replacement
365
+ recipe.
366
+
367
+ Open work:
368
+
369
+ - Add compatibility matrix runs, memory/fan-out measurements, and replay
370
+ latency gates for the maintained benchmark workloads.
371
+ - Add a multi-worker benchmark gate with the ActiveRecord subscription store
372
+ and a shared ActionCable adapter.
@@ -0,0 +1,214 @@
1
+ # Shared Warm Scale Roadmap
2
+
3
+ This roadmap targets the shape where Upkeep should outperform Turbo: many
4
+ subscribers share an identity-free page and one mutation fans out to all of
5
+ them.
6
+
7
+ Last updated: 2026-05-21.
8
+
9
+ Current code state:
10
+
11
+ - Base runtime/docs commit: `474fb9c` (`Instrument capture and document Upkeep Rails`).
12
+ - Current pass adds gated request/action profiling, operation-scoped capture
13
+ metrics, mutation action/delivery timings, client/server phase correlation,
14
+ benchmark phase labels, lightweight subscription-shape tracing, static
15
+ template metadata caching, shared-stream signature memoization, and queued
16
+ dependency flushing.
17
+ - Latest verification: `bundle exec rake test` passed with `186` runs and
18
+ `1172` assertions.
19
+ - Latest shared benchmark data is the 2026-05-21 ramped 200-subscriber report
20
+ below.
21
+
22
+ ## Environment
23
+
24
+ From a fresh zsh shell in any worktree:
25
+
26
+ ```sh
27
+ cd /path/to/upkeep-rails
28
+ eval "$(mise activate zsh)"
29
+ ruby -v
30
+ bundle exec rake test
31
+ ```
32
+
33
+ Do not use system Ruby. The repo expects Ruby from mise, currently `3.4.7`.
34
+
35
+ ## Baseline
36
+
37
+ Latest ramped identity-free 200-subscriber report:
38
+
39
+ - Report: `benchmark/results/identity-free-feed-compare-20260521113822.md`
40
+ - Upkeep setup p95: `58ms`
41
+ - Turbo setup p95: `38.05ms`
42
+ - Upkeep page render p95: `47ms`
43
+ - Turbo page render p95: `24.05ms`
44
+ - Upkeep WebSocket connect p95: `9ms`
45
+ - Turbo WebSocket connect p95: `11ms`
46
+ - Upkeep subscribe call p95: `6ms`
47
+ - Turbo subscribe call p95: `2ms`
48
+ - Upkeep subscribe ack p95: `5ms`
49
+ - Turbo subscribe ack p95: `1.05ms`
50
+ - Upkeep write POST p95: `58.90ms`
51
+ - Turbo write POST p95: `58.69ms`
52
+ - Upkeep update-settled p95: `191ms`
53
+ - Turbo update-settled p95: `342.10ms`
54
+ - Upkeep render groups: `1`
55
+ - Upkeep represented subscribers: `200`
56
+ - Turbo refresh GETs: `200`
57
+ - Upkeep render dedup savings: `199`
58
+ - Upkeep plans: `1`
59
+ - Upkeep stream batches: `1`
60
+ - Subscription shape cache: `199` hits, `1` miss, `0` bypasses
61
+ - Shape timing: hit key p95 `0.031ms`; hit total p95 `0.049ms`; miss total
62
+ `4.324ms`
63
+ - Setup page server timing: Upkeep p95 `34.28ms`; Turbo p95 `10.12ms`
64
+ - Write request server timing: Upkeep p95 `15.74ms`; Turbo p95 `34.74ms`
65
+ - `GET /feed` request-capture timing: total p95 `33.081ms`; action p95
66
+ `14.896ms`; view p95 `15.456ms`; template p95 `11.104ms`; collection render
67
+ p95 `4.362ms`; SQL p95 `0.191ms`; register p95 `3.814ms`
68
+ - `GET /feed` recorder timing: dependency p95 `3.916ms`; frame p95 `1.364ms`;
69
+ shape trace p95 `3.588ms`
70
+ - Subscribe channel server timing: total p95 `1.884ms`; activation p95
71
+ `1.776ms`
72
+ - Upkeep live deopts: `0`
73
+
74
+ The shared primitive is working: one anonymous shape, one plan, one render, and
75
+ one stream batch fan out to 200 subscribers. This pass removed the full-recorder
76
+ shape key from the cache path, removed the O(n) cohort rebuild from each
77
+ subscriber activation, moved subscription-shape identity into DAG/runtime, and
78
+ made shape hits use a recorder-side rolling trace digest. This pass then
79
+ trimmed the trace and dependency path further by removing unused trace snapshots,
80
+ excluding duplicate manifest data from shape terms, memoizing render-site stream
81
+ signatures, caching static template metadata, and flushing dependencies in
82
+ batches per frame/request. Shape hits and server-side subscription activation
83
+ are no longer shared-path bottlenecks. The next work is reducing Action View
84
+ template/page capture overhead while preserving the one-render fanout path.
85
+
86
+ ## Priorities
87
+
88
+ 1. Close page/capture fixed cost
89
+
90
+ Upkeep update delivery is much faster, and shape hits are no longer a
91
+ meaningful cost. In the latest shared report, setup page server p95 is
92
+ `34.28ms` versus Turbo `10.12ms`. `GET /feed` request-capture total p95 is
93
+ `33.081ms`, action p95 is `14.896ms`, view p95 is `15.456ms`, and
94
+ registration p95 is `3.814ms`.
95
+
96
+ Candidate work:
97
+
98
+ - Reduce Action View capture overhead around template and collection render
99
+ instrumentation. SQL is only `0.273ms` p95 in the shared report.
100
+ - Continue reducing Action View template capture overhead; template p95 is
101
+ now the largest named subphase at `11.104ms`.
102
+ - Keep dependency flushing batched and use `recorder_dependency_flush_count`
103
+ to separate read count from unique graph inserts.
104
+ - Investigate whether page/layout frame capture can be represented with one
105
+ page boundary without losing layout identity safety.
106
+ - Preserve the rolling trace digest as order-sensitive: false cache misses
107
+ are acceptable, unsafe sharing is not.
108
+
109
+ Target outcome:
110
+
111
+ - 200-subscriber `GET /feed` request-capture total p95 falls from
112
+ `33.081ms` toward `15-20ms`.
113
+ - Page render p95 closes most of the remaining shared setup gap.
114
+
115
+ 2. Explain client setup gap
116
+
117
+ Server-side subscribe p95 is `1.884ms` while client-observed subscribe ack
118
+ p95 is `5ms`, and setup p95 is `58ms`. Server/client phase correlation
119
+ now shows that the largest shared setup gap is setup page request time:
120
+ Upkeep `34.28ms` versus Turbo `10.12ms`.
121
+
122
+ Candidate work:
123
+
124
+ - Keep phase labels on page setup, Turbo refresh, write POST, WebSocket
125
+ connect, cable open, subscription registration, and confirmation.
126
+ - Separate browser/k6 scheduling delay from Rails server time when comparing
127
+ setup p95.
128
+ - Filter health checks out of request-capture operation reporting so
129
+ `GET /feed` stays the visible target.
130
+
131
+ Target outcome:
132
+
133
+ - The report continues to explain the difference between `33.081ms`
134
+ server capture p95 and `58ms` client setup p95 without mixing Turbo
135
+ refresh GETs into setup-page timings.
136
+
137
+ 3. Explain and reduce write-path variance
138
+
139
+ Latest write POST p95 is `58.90ms` for Upkeep and `58.69ms` for Turbo.
140
+ Server-side write request p95 is `15.74ms` for Upkeep and `34.74ms` for
141
+ Turbo. Keep this instrumented so future fanout work does not hide write-side
142
+ regressions.
143
+
144
+ Candidate work:
145
+
146
+ - Split the current mutation action timing into Active Record write,
147
+ invalidation planning, stream build, ActionCable enqueue, and response
148
+ return.
149
+ - Separate writer VU timing from subscriber delivery timing in the report.
150
+ - Verify the write regression is not caused by shape-cache bookkeeping.
151
+
152
+ Target outcome:
153
+
154
+ - The report identifies whether write latency is database, planning, delivery
155
+ enqueue, or benchmark noise.
156
+
157
+ 4. Fanout packing
158
+
159
+ Upkeep still has one unavoidable frame per subscriber, but payload and
160
+ envelope work should happen once per render group.
161
+
162
+ Candidate work:
163
+
164
+ - Serialize payload once.
165
+ - Build frame/envelope once per stream group when possible.
166
+ - Keep per-connection work to stream lookup and socket write.
167
+
168
+ Target outcome:
169
+
170
+ - 500/1000 subscriber runs preserve one render group and avoid a new CPU
171
+ bottleneck in payload packing.
172
+
173
+ 5. Benchmark/report hygiene
174
+
175
+ The ramped benchmark is now the right warm shared shape. The report should
176
+ continue to make fixed-cost and shared-cost claims separately.
177
+
178
+ Candidate work:
179
+
180
+ - Keep setup ramp/window fields, `steady_state_setup_leaks`, plans, stream
181
+ batches, represented subscribers, and shape-cache counters in markdown and
182
+ JSON.
183
+ - Keep shape-cache timing, request-capture timing, and server-side subscribe
184
+ timing in markdown and JSON.
185
+ - Keep client setup phase correlation and mutation write phase timing.
186
+ - Keep request-capture timings grouped by operation.
187
+ - Keep cold burst capacity and single-subscriber cold setup as separate
188
+ reports.
189
+
190
+ ## Success Criteria
191
+
192
+ - 200-subscriber update-settled p95 remains materially faster than Turbo.
193
+ - 200-subscriber setup p95 explains and narrows the current `58ms` versus
194
+ Turbo `38.05ms` gap while preserving one-render fanout.
195
+ - 500-subscriber report shows one render group, high delivery/render ratio, and
196
+ no setup leaks.
197
+ - Planning/build batch count for the one-write shared feed path stays at one.
198
+ - Shape cache has one miss and N-1 hits for identical anonymous pages.
199
+ - Anonymous deopts remain zero for identity-free pages.
200
+ - Live delivery deopts remain zero for the common feed create case.
201
+
202
+ ## Validation
203
+
204
+ Use the mise environment in every shell:
205
+
206
+ ```sh
207
+ eval "$(mise activate zsh)"
208
+ bundle exec rake test
209
+ BENCH_FAMILY=render_dedup BENCH_WORKLOAD=identity_free_feed_compare BENCH_TIER=report BENCH_VUS=200 ruby benchmark/bin/run
210
+ BENCH_FAMILY=render_dedup BENCH_WORKLOAD=identity_free_feed_compare BENCH_TIER=report BENCH_VUS=500 ruby benchmark/bin/run
211
+ ```
212
+
213
+ Use the warm shared benchmark for shared-update economics. Do not use cold
214
+ burst failure or accept-queue behavior to judge render dedup performance.