Skip to content

finding: the shipped runtime publish gate already costs linearly in tenant object count — 506 ms per flow publish at 420 objects, before any widening #9613

Description

@os-support-ai

Restart-when: npx tsx scripts/bench/runtime-publish-gate.bench.mts on a then-current main reports the real-shape N=420 flow-publish median materially below this band (⇒ some later change reached this door; correct the body and close against a number), OR objectstack cloud has real tenants whose Studio publish latency is a reported symptom

⛔ Ruled 2026-08-20: record it, do not fix it. Maintainer verbatim: 「C 只记录,不修(推荐)」

This card is a live perf record, not a work item. Its number was re-measured on 2026-08-20 at 482.78 ms and is inside the band below. ⛔ Do not scope a narrowing off this card without a new ruling — the measured reason is in "Why the obvious fixes do not reach this door".

Recording a measured fact that has lived only inside a card comment (#4716 comment 5309105113, measured on origin/main @ 66beee0f3 by a throwaway harness) so it stops being invisible to every perf listing. Filed unassigned during the 2026-08-18 adjudication of that card (maintainer directed the split package: 「简化开发」→「同意」).

The measurement

Today's shipped gate costs 33.4 ms at 21 objects → 506 ms at 420 objects per flow publish. The companion per-rule profile shows the bill concentrates in at-gate rules being handed the whole stack: validateStackExpressions ≈ 70% of the option-(a) bill, validateReferenceIntegrity ≈ 11% — both already on the runtime-publish surface today.

✅ Re-confirmed twice since, on two later refs and two different machines:

⚠️ Parameterisation, which the original wording got slightly wrong: the cost is linear in the tenant's authored object SURFACE — fields, formulas, validation rules, predicates, the expression-bearing keys validateStackExpressions walks — not its object count. Count is a proxy whose constant is set by what the objects contain. At identical N a real-shape stack and a synthetic expression-free one are ~21–26× apart (measured 2026-08-20: real/stub 26.4× @21, 21.4× @420). Any number quoted from this family must name its stack shape; not doing so is what once made this card's figure look 16–25× wrong against another correct measurement of a different workload.

Why it may matter

Flow publish is a user-facing interactive action (Studio designer). Half a second of gate latency at 420 objects is already perceptible, and it grows with exactly the number that grows as a tenant succeeds.

⚠️ But scope it honestly: per the maintainer's stated priority, saving an object/field edit is the high-frequency Studio action while publishing a flow is rare. The two doors have disjoint drivers — the object door is validateRuleCompilability at 81.3%, a rule that appears in no profile on this card. ⛔ Every concentration figure here describes flow publishing only, and the object door remains unmeasured. The better-characterised half is the less important half.

⭐ Why the obvious fixes do not reach this door — measured 2026-08-20

Package-narrowing (#9612) requires two things, and a tenant-authored overlay stack has neither:

  1. A packageId on the write. REST derives it solely from ?package= (packages/rest/src/rest-server.ts, where 'all'/empty is commented as "env-local overlay"), so a Studio publish yields undefined and resolveWritePackageScope returns at its first guard.
  2. _packageId provenance on the stored objects. narrowObjectsToPackageClosure's limb 2 — if (typeof owner !== 'string' || owner === '' || owner === OVERLAY_PROVENANCE_SENTINEL) return true; — keeps any object with no provenance, and the real seeded corpus carries zero.

⇒ 📌 Fixing the missing packageId (#10063) would not, on its own, buy the saving. That is the counter-intuitive part and the reason this card stays open: without it, the plumbing fix reads like a latency win and is not one on an unpackaged corpus. Cross-noted on #10063.

The counterfactual proves the mechanism is sound rather than broken: on a provenance-stamped seed the shipped deriver narrows 421 objects to 45 (10.7%) and saves 89.5% (468.10 → 49.36 ms), with the differential verdict semantically unchanged and both positive controls live. The narrowing works; it just cannot reach this corpus.

⛔ Fences, carried and still binding

The instrument

npx tsx scripts/bench/runtime-publish-gate.bench.mts       # both shapes
npx tsx scripts/bench/runtime-publish-gate.bench.mts --mode package   # the closure counterfactual

Re-runnable, states its own timed span, warmup policy and seeded stack shape, prints both shapes, and names the rules it dispatched — so a stale build shows up as a changed rule list rather than as a silently wrong number. It replaces the throwaway harness this card's original reading came from, which was the real problem all along: the number was right, and nobody could re-run it to find that out.

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Triage — first-touch grading (triage seat, session session_01KTguWpd7WNGYTGmSNY31RL): promoted to pm:queue + pm:blocked, domain:metadata, type Task.

    Blocked-by: #4716

    Rationale: real measured pull — flow publish is an interactive Studio action and 506 ms at 420 objects grows with exactly the number a successful tenant grows; the landing surface (what the publish door hands the at-gate rules, validateStackExpressions/validateReferenceIntegrity) is the same gate file family as #9612, whose parent #4716 is in flight — same serialization applies, so this waits behind it rather than colliding. On unlock, re-verify the per-rule profile on the merged ref before scoping the fix (the #4716 work may already change what the gate is handed).


    Generated by Claude Code

  2. os-warren commented on Aug 19, 2026

    @os-warren
    Collaborator

    Unlock scan (triage seat, 2026-08-19 ~09:1xZ round): the last declared blockers (#9851, #4716) are closed and the stated parking gate — PR #9920 — merged today at 08:51Z, so the parked condition is discharged → pm:blocked removed, back to queue. Engine seat: re-scope together with sibling #9612 on the post-#9920 ref (the maintainer's input-narrowing ruling of 08:08Z is the binding direction); stale-premise check on the measured 506 ms figure is expected — re-measure on the merged benchmark harness.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    Closed not_planned: a performance baseline with no product landing waiting on it

    Triage seat (objectstack-wide, seat post #6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T16:14Z. ⛔ Not a claim. The measurements here (506 ms per flow publish at 420 objects, and the bench script) stay valid as the baseline.

    Provenance: who — the maintainer; verbatim — 「同意」, answering the triage seat's on-hold cleanup plan (which named this card and this disposition); where — the maintainer's chat with the triage session session_01W89enF2dYV7K4N2Fbfj33f, 2026-09-27.

    Reopen is free when a publish-time latency blocks a product landing or a customer-visible flow (name it), or when the bench is re-run and shows the cost grew.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions