Repository navigation
Description
coordinator#1 · hotlong · session_6eb9ec2e0e3f410b91239dab7a337b9e
立卡门类别:① 已复现缺陷(reach: 公开入口实测,staging 客户列表页,见「已实测」)
Reader: domain:server 车道。packages/rest/src/security-routes.ts(批量循环)与 packages/plugins/plugin-security/src/security-plugin-explain-access.ts / explain-engine.ts(对象级判定只算一次、记录级按 id;或用一次带 id in (...) 的写过滤查询回答每条记录)。ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts 只作备选(菜单打开时再问)。建议档 p2,由分诊定。
Dedupe: repo:objectstack-ai/objectstack is:issue security/explain slow total=23(无同题,最近 #26754 closed 是测试时长);explain batch recordIds per id loop total=6(#22571、#22792 closed,语义卡);explain latency record visible total=40(无同题)。最相关的已关卡:#8326(批量形态的来源)、#27626(客户端去重;其正文说明 explain ×2 是 update / delete 两个问题)。
业务理由:每个打开 HotCRM 列表页的用户,行级「编辑 / 删除」菜单要等这两次判定(约 3–4 s)才有结论;托管运行时一个容器同时跑多个环境的内核,这 2×3 s 的 CPU 让同容器其他租户的请求排在后面。不修,列表页永远以 explain 收尾,环境越多越慢。
来源
2026-11-02 staging 页面性能评估(cloud main cd4e2e1f,objectstack pin 6cbdef1e;控制面 cloud.objectos.app,租户 os-2ph4c1.objectos.app / HotCRM 6.0.0;内置浏览器面板 zh-CN,读 resource timing 与 v2.7.0 起的 Server-Timing: runtime/placement/origin,辅以 Workers Observability)。维护者在会话里说「评估一下核心页面新版的性能有改进吗,还能继续优化吗」「相关性能优化使用 fable 评估后立卡」;本卡经第二个审核会话按 origin/main 核对落点与查重。
已实测
- 客户列表页(
/apps/app.objectstack.hotcrm/crm_account,31 行)resource timing:两个POST /api/v1/security/explain在 6246 ms 才发出(前一层结束之后),各自Server-Timing为runtime=3396 origin=3473与runtime=2986 origin=3062;同页其他接口 runtime 在 50–920 ms。 - Workers:新版窗口
/api/v1/security/explainp50 3257 / p90 3661 ms(n=12,每页 2 次)。旧版(v2.6.0)窗口 p90 3568,说明批量在旧 pin 就是这个代价,不是两 pin 间的回退(旧窗口的 p50 较低,是因为当时记录页还单独发便宜的单条 explain,样本构成不同)。
落点
- 调用方:
ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts:364-440,每页按 (object, operation) 各发一个批量请求,请求体{ object, operation: 'update'|'delete', recordIds: [整页行 id] }(上限 200)。两次 = update + delete。 - 服务端:
packages/rest/src/security-routes.ts:256-281先做一次对象级svc.explain(singularRequest),再for (const id of new Set(recordIds)) { await svc.explain({ ...singularRequest, recordId: id }) }:逐 id 串行跑完整的单条判定(每次重新解析权限集、对象安全元数据、RLS、共享规则、写探针、两次按 id 读行)。注释写明这是「单条判定的传输合并,不是新语义」,批量 ≡ N 次单条由测试钉住。 - 记录页已经不单独发 explain:feat(rest,console): one request opens a record page #27713 把 update / delete / transfer 判定装进
POST /data/:object/:id/page;但服务端record-page-routes.ts:393-405对这几个判定也是串行 await、各自重算对象级部分,修批量时顺手让它们共享对象级判定并并行,不另开卡。
验收
- 50 个 id 的批量请求:本地隔离 CPU ≤ 单条的 3 倍(或每 id 边际 ≤ 10 ms);staging
Server-Timing runtime≤ 500 ms。 - 既有「批量 ≡ N 次单条」一致性测试保持通过。
- staging 客户列表页瀑布里 explain 不再是最后一层的长尾。
相关
cloud#2914(控制台打开的串行瀑布,父单)、objectstack#8326、objectstack#27626、objectstack#27713
🤖 Generated with Claude Code
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 (priority added) · 2026-11-02T08:21Z
⛔ Not a claim.
security/explainre-reads per record (N+1). A performance defect on a named server path: p2,domain:serveras filed. Routing and the pm state the filer set are unchanged.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt
Claim: PM loop round 1 · card thread · 2026-11-02T08:30Z
Session:
session_01ADHkmQxwoGb5fSnrFkpxmt(the seat); the card thread issession_01ADHkmNqE95va9aNi7nYajPAccount:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-3fwdg3(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: the maintainer, verbatim: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」 Card: none from the maintainer on this card; triage graded p2 (6572449390).
File surface: the batch loop in
packages/rest/src/security-routes.ts;packages/plugins/plugin-security/src/security-plugin-explain-access.ts/explain-engine.ts(object-level part computed once);record-page-routes.tsonly if the same change covers it; tests; changeset. ⛔ Noui/**.Container & model:
M,mode:thread,model: opus(TIER_DEFAULT;--tiernames no path mandate: PM's call),effort: highClause-②: no (expected: same answers, faster; batch ≡ N singles stays pinned)
Metadata-protocol: none (expected)
Responsibility: the code is the batch form of
POST /security/explainin@objectstack/restand the explain engine of@objectstack/plugin-security· the platform path is a list page whose two batch explains re-run a full single explain per row (≈3 s each) · reached by every list page with row edit/delete menusThread-read: 6572449390
Premise: holds (the named seam read on
origin/mainby the seat this act; the dev re-measures red first)Serial constraints cleared: none — no open claim on a
domain:serverpm:dispatchedcard names these files (read this act)Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-steve · session_01ADHkmNqE95va9aNi7nYajP
os-dev-report
{ "issue": 29112, "status": "done", "branch": "claude/project-thread-3fwdg3", "pr": "https://github.com/objectstack-ai/objectstack/pull/29208", "session": "session_01ADHkmNqE95va9aNi7nYajP", "premise_still_valid": true, "summary": "The batch form of security/explain ran a whole singular explanation per id. The explain engine is split (no change in what it computes) into an object-level pass, a record pass and a decision builder; a new explainRecordsForAsker runs the object-level pass once and the record pass per distinct id. The security service honours request.recordIds, settling the caller admission and explained context once; the REST door hands the batch over in one call and keeps the per-id path for a service that predates it. Local rig, 50 ids, update: queries 505 -> 231, CPU 284 -> 147 ms; per-id margin about 2.9 ms (card bound 10 ms met); the 3x-single arm is not met (about 4.5 per-record queries per id remain). record-page-routes.ts unchanged (three different operations share no object pass).", "tests": "head fc99ac4f. plugin-security test: 239 files, 5171 passed/45 skipped; rest test: 367 files, 6966 passed/332 skipped; both typecheck green (rest after test-layer fix fc99ac4f). New explain-batch-parity.test.ts 44/44 (40 AGREEMENT cases: 4 askers x 5 verbs x 2 objects, mixed set incl. shared, unshared, RLS-locked, missing, duplicate ids, when-conditioned field entry, unit depth w/o hierarchy resolver; 2 one-object-pass read-count pins). Red first on main sources: 42 failed/2 passed (the 2 read-count pins pass vacuously there). REST suites on main source: 4 failed/48. Ablation 1 (engine: object pass per record): read-count pins red 2/2, findOne sys_user 1->6 and 2->6, restored blob==HEAD. Ablation 2 (REST ignores batch answer): 2/25 red, restored. Ablations ran against src (tests import src directly; no dist path).", "gates": "dispatch-gates --commands on fc99ac4f: 65; --ran: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN. 62 exit 0 first pass; check-plugin-teardown-shape --self-test exit 3 (shallow fixture) -> exit 0 after fetching the pinned commit; check:i18n exit 3 (CLI unbuilt) -> exit 0 after its named closure build; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (whole-workspace dist) = NOT MEASURED locally, CI Lint and Repo Gates runs it. eslint --no-inline-config on the 6 touched files exit 0. PR check-runs: in_progress at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read x2 (card comments), mcp__github__pull_request_read x1 (PR read-back); no write tools", "api_writes": "2 — relay pr_create (PR #29208, 11114 bytes sent = stored), post-stamped os-dev-report comment on #29112", "line_budget": "7 files, +682 / -113 (under the 3000 human-merge threshold)", "files_changed": [ ".changeset/29112-batch-explain-one-object-pass.md", "packages/plugins/plugin-security/src/explain-batch-parity.test.ts", "packages/plugins/plugin-security/src/explain-engine.ts", "packages/plugins/plugin-security/src/security-plugin-explain-access.ts", "packages/rest/src/security-explain-envelope.test.ts", "packages/rest/src/security-routes.test.ts", "packages/rest/src/security-routes.ts" ], "deviations": [ "PR body: the harness attribution block takes lines 1-2, so Fixes #29112 is line 3 (os-dev allows the harness attribution ahead of Fixes)", "Forced test edits named in the PR body: security-routes.test.ts first call now carries recordIds; security-explain-envelope.test.ts batch row's asked request carries recordIds", "check:dual-build-cjs-loads NOT MEASURED locally (needs whole-workspace build); declared to CI", "test:repo not run locally (diff touches no repo-project file)" ], "open_questions": [ { "question": "The service-level batch rides request.recordIds, which the wire schema (ExplainRequestSchema) and ExplainDecisionSchema.records already declare but the TS contract ExplainAccessRequest (packages/spec/src/contracts/security-service.ts) does not. Declare it there?", "options": [ "A: follow-up spec card adds optional recordIds to ExplainAccessRequest (no new key name; the wire key already exists), Clause-2 stays no", "B: leave the TS contract as is; the door already passes it untyped" ], "recommendation": "A, so the contract type says what the reference service honours; it needs a packages/spec edit this card's delta did not name" }, { "question": "Staging target (Server-Timing runtime <= 500 ms for ~31 ids) is probably not reached by this PR alone (per-id queries ~10 -> ~4.5). Next step if cloud's reading confirms?", "options": [ "A: batch the record pass's per-record reads (one id-in read of the rows, one share listing for all ids) with a parity table like this one", "B: bounded concurrency of record passes, after making the per-request context stamps (__rlsMembershipStaged, __verifiedPlatformOwner) concurrency-safe", "C: accept the halving; no further card" ], "recommendation": "Wait for cloud's staging reading after release; if over 500 ms, A, because it removes round trips without touching the once-per-request staging that makes B unsafe today" } ], "out_of_scope_findings": [ "carrier: none · noted, not filed: SecurityPlugin per-request context stamps (__rlsMembershipStaged set before its resolution awaits) are not safe for concurrent use of one context object; no present caller does that, so no defect today (dormant)" ] }Stamped 2026-11-02T09:27Z.
Generated by Claude Code