Skip to content

Commit 0acc628

Browse files
authored
Merge pull request #4859 from udecode/codex/table-perf-prev-check
2 parents d16ea6d + 42a14bb commit 0acc628

File tree

7 files changed

+1768
-100
lines changed

7 files changed

+1768
-100
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Block Selection Core Migration Design Draft
2+
3+
## Background
4+
5+
- The current block selection implementation lives in [BlockSelectionPlugin.tsx](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/BlockSelectionPlugin.tsx).
6+
- The implementation is already on the right track: state is lightweight, the core data is `selectedIds: Set<string>`, and it provides per-id selectors.
7+
- In contrast, table selection takes a heavier path:
8+
- [useSelectedCells.ts](/Users/felixfeng/Desktop/udecode/plate/packages/table/src/react/components/TableElement/useSelectedCells.ts) recomputes the table/cell grid and writes `selectedCells` / `selectedTables`
9+
- [useIsCellSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/table/src/react/components/TableCellElement/useIsCellSelected.ts) makes every cell subscribe to the entire selection state
10+
- The long-term direction: selection becomes a core editor capability, and block selection / table selection both build on top of it.
11+
- Phase 1 does not address table selection. It only completes the block selection migration, but the design must leave room for table extension.
12+
13+
## Problem Definition
14+
15+
Although the current block selection works, it is still "a complete selection state and behavior set owned by a plugin", which causes several structural issues:
16+
17+
1. Selection is a feature-level capability, not a core-level capability
18+
2. Other selection modes cannot reuse the same underlying model
19+
3. Table selection will likely grow another parallel state set instead of reusing unified selection infrastructure
20+
21+
We need to take the first step: move block selection state ownership down to core, without immediately changing the base semantics of `editor.selection`.
22+
23+
## Goals
24+
25+
- Make block selection a core-backed capability
26+
- Keep existing block selection interactions and external behavior unchanged
27+
- Do not modify `editor.selection` in Phase 1
28+
- Introduce a core model that can accommodate future non-text selection
29+
- Keep Phase 1 within a scope that can be landed and verified independently
30+
31+
## Non-Goals
32+
33+
- Do not redo table selection in this phase
34+
- Do not change Slate's `editor.selection` to `Range | Range[]`
35+
- Do not require all transforms to understand multi-range discrete ranges in Phase 1
36+
- Do not detail table merge / border implementation in this document
37+
38+
## Phase 1 Scope
39+
40+
Phase 1 does one thing: migrate block selection from "plugin-owned state" to "core-owned state".
41+
42+
Expected outcome after this phase:
43+
44+
- Block selection state is registered and managed by core
45+
- Block selection UI can remain in the selection package
46+
- Existing block selection commands continue to work, but delegate to core internally
47+
- External callers no longer treat block plugin options as the ultimate source of truth
48+
49+
## Proposed Core State Model
50+
51+
The Phase 1 model should be minimal — do not prematurely include the full table shape.
52+
53+
```ts
54+
type EditorSelectionState = {
55+
primary: Range | null;
56+
block: {
57+
anchorId: string | null;
58+
selectedIds: Set<string>;
59+
isSelecting: boolean;
60+
isSelectionAreaVisible: boolean;
61+
};
62+
};
63+
```
64+
65+
Notes:
66+
67+
- `primary` continues to correspond to the current text selection
68+
- `block` is a new core selection channel, not a plugin-private store
69+
- `selectedIds` continues to use `Set<string>` because it is already the correct data shape: cheap per-id lookups, low-cost membership checks
70+
- Phase 1 does not add a table descriptor, but the state boundary must not be hardcoded to "only block as an extra selection type"
71+
72+
## API Direction
73+
74+
Core should expose a thin selection API layer, and block selection adapts on top of it.
75+
76+
```ts
77+
editor.api.selection.getPrimary()
78+
editor.api.selection.setPrimary(range)
79+
80+
editor.api.selection.block.get()
81+
editor.api.selection.block.clear()
82+
editor.api.selection.block.set(ids)
83+
editor.api.selection.block.add(ids)
84+
editor.api.selection.block.delete(ids)
85+
editor.api.selection.block.has(id)
86+
editor.api.selection.block.isSelecting()
87+
```
88+
89+
Existing block-facing helpers can be retained, but their semantics should change:
90+
91+
- `editor.getApi(BlockSelectionPlugin).blockSelection.add(...)`
92+
- `editor.getApi(BlockSelectionPlugin).blockSelection.clear()`
93+
- `editor.getApi(BlockSelectionPlugin).blockSelection.getNodes(...)`
94+
95+
These helpers should become compatibility wrappers rather than continuing to hold their own real state.
96+
97+
## Rendering Layer Direction
98+
99+
Phase 1 does not need to rewrite block selection visual interactions — only migrate state ownership.
100+
101+
- Block selection area UI can remain in the selection package
102+
- [useBlockSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/hooks/useBlockSelected.ts) switches to reading a core-backed selector
103+
- `BlockSelectionPlugin` shrinks to an adapter: event wiring, render integration, and compatibility layer API
104+
105+
This approach carries significantly lower risk than "rewriting the entire interaction model at once".
106+
107+
## Migration Steps
108+
109+
### Step 1: Introduce core block selection state
110+
111+
- Add block selection state structure in core
112+
- Expose minimal selectors and mutators
113+
- Keep `editor.selection` behavior unchanged
114+
115+
### Step 2: Redirect block selection API
116+
117+
- Redirect reads/writes of `selectedIds`, `anchorId`, `isSelecting`, etc. behind the core API
118+
- Continue exposing the existing block selection command surface externally
119+
120+
### Step 3: Redirect hooks and render
121+
122+
- Hooks like [useBlockSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/hooks/useBlockSelected.ts) switch to consuming the core-backed selector
123+
- UI behavior remains unchanged
124+
125+
### Step 4: Reduce plugin state ownership
126+
127+
- `BlockSelectionPlugin` retains:
128+
- Event wiring
129+
- Adapter APIs
130+
- Rendering integration
131+
- Core becomes the sole state owner
132+
133+
## Compatibility Strategy
134+
135+
To keep the blast radius under control, Phase 1 should adhere to these rules:
136+
137+
- `editor.selection` continues to be `Range | null`
138+
- Not all editing commands are required to understand block selection immediately
139+
- Block-specific operations continue to explicitly read block selection state
140+
- Avoid introducing large-scale type modifications in Phase 1
141+
142+
This allows the migration to be incremental rather than affecting the entire Slate / Plate command surface at once.
143+
144+
## Why This Step First
145+
146+
The value of this phase:
147+
148+
- Reclaim selection ownership into core
149+
- Remove a feature-level state owner
150+
- Provide a unified foundation for other future selection modes
151+
152+
It is also low-risk because block selection's current data model is already relatively healthy — the main issue is "where the state lives", not "what the state looks like".
153+
154+
## Table Direction as Design Constraint Only
155+
156+
Table is not in Phase 1 scope, but Phase 1 design must avoid blocking future table work.
157+
158+
Phase 1 should explicitly avoid:
159+
160+
- Hardcoding core selection to serve only block ids
161+
- Treating "flat id set" as the only non-text selection shape
162+
- Letting future table selection still depend on materialized node arrays
163+
164+
Future table selection will likely need:
165+
166+
- A table-scoped descriptor instead of `selectedCells: TElement[]`
167+
- Keyed selectors instead of each cell subscribing to the entire selection
168+
- Expressive power for non-contiguous / grid-shaped selection semantics
169+
170+
No need to detail the table design here — just ensure Phase 1 state boundaries and API do not prevent Phase 2 from extending in this direction.
171+
172+
## Open Questions
173+
174+
- Should core selection expose a channeled model (`block` / `table` / `primary`) or a more generic descriptor registry?
175+
- After migration, which `BlockSelectionPlugin` APIs are still worth keeping as public interfaces?
176+
- Should block selection render logic stay in `packages/selection` long-term, or continue moving toward core?
177+
178+
## Phase 1 Acceptance Criteria
179+
180+
- Block selection state is owned by core
181+
- Existing block selection interaction behavior remains consistent
182+
- `useBlockSelected` and related selectors switch to reading core-backed state
183+
- Existing block selection commands continue to work, delegating to core via compatibility wrappers
184+
- Phase 1 does not require any changes to table selection behavior
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# dev/table-perf 性能快照
2+
3+
## 环境
4+
5+
- 测试时间:2026-03-10 23:36:30 CST
6+
- 页面地址:`http://localhost:3002/dev/table-perf`
7+
- 应用:`apps/www`,Next.js 16.1.6(Turbopack dev)
8+
- 浏览器:`agent-browser` + Chromium 138.0.7204.15
9+
- 机器:macOS 15.7.3
10+
- 页面错误:`agent-browser errors` 未发现报错
11+
12+
## 采样方法
13+
14+
- 阅读 `apps/www/src/app/dev/table-perf/page.tsx`,确认页面内置了两套测试:
15+
- benchmark:`5` 次 warmup + `20` 次 measured remount
16+
- input latency:`10` 次 warmup + `50` 次 measured inserts
17+
- 使用 `agent-browser` 打开 `/dev/table-perf`
18+
- 读取页面 Metrics 面板和 console 输出
19+
- 本次记录两组数据:
20+
- 默认 `10 x 10`(100 cells)
21+
- 压力 `60 x 60`(3600 cells)
22+
23+
## 结果
24+
25+
| 配置 | Cells | Initial render | Benchmark mean | Benchmark median | Benchmark p95 | Input mean | Input median | Input p95 |
26+
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
27+
| 10 x 10 | 100 | 209.30 ms | 99.32 ms | 83.80 ms | 147.50 ms | 28.07 ms | 28.40 ms | 30.50 ms |
28+
| 60 x 60 | 3600 | 2663.30 ms | 2499.24 ms | 2453.30 ms | 2848.70 ms | 390.48 ms | 379.40 ms | 443.70 ms |
29+
30+
## 对比
31+
32+
- `60 x 60` 相比 `10 x 10`
33+
- Initial render:`12.72x`
34+
- Benchmark mean:`25.16x`
35+
- Input mean:`13.91x`
36+
37+
## 结论
38+
39+
- `10 x 10` 基线可接受。input latency 均值约 `28 ms`,交互感觉应当是顺的。
40+
- `60 x 60` 仍可跑完,但已经明显进入高延迟区间:
41+
- 初始挂载约 `2.66 s`
42+
- benchmark 均值约 `2.50 s`
43+
- 单次输入延迟均值约 `390 ms`
44+
- 以当前页面表现看,大表场景下主要问题不是偶发尖峰,而是整体延迟已经稳定抬高到肉眼可感知的程度。
45+
46+
## 解读注意点
47+
48+
- 切换 preset 后点击 `Generate Table`,页面会刷新当前表格和基础 metrics。
49+
- `Benchmark Results` 会被清空后重新计算。
50+
- `Input Latency` 结果不会在 `Generate Table` 时自动清空;如果切到新 preset,必须重新跑一次 input latency 才能读到当前配置的数据。
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# dev/table-perf Performance Snapshot
2+
3+
## Environment
4+
5+
- Test time: 2026-03-12 21:46:54 CST
6+
- Page URL: `http://localhost:3000/dev/table-perf`
7+
- App: `apps/www`
8+
- Commit: `98ae3116f`
9+
- Browser: Playwright + Chromium `138.0.7204.15`
10+
- Page accessibility check: confirmed `/dev/table-perf` loads via `agent-browser`
11+
12+
## Sampling Method
13+
14+
- Read `apps/www/src/app/dev/table-perf/page.tsx`, confirmed the page has two built-in tests:
15+
- Benchmark: `5` warmup + `20` measured remount iterations
16+
- Input latency: `10` warmup + `50` measured inserts
17+
- Used `Large (1600)` preset, corresponding to `40 x 40`
18+
- Click sequence:
19+
- `Generate Table`
20+
- `Run Benchmark (20 iter)`
21+
- `Test Input Latency (50 samples)`
22+
- Metrics read from the page Metrics panel
23+
24+
## Results
25+
26+
### 40 x 40 (1600 cells)
27+
28+
| Category | Metric | Value |
29+
| --- | --- | ---: |
30+
| Metrics | Initial render | 1002.00 ms |
31+
| Metrics | Re-render count | 3 |
32+
| Metrics | Last render | 0.40 ms |
33+
| Metrics | Avg render | 351.27 ms |
34+
| Metrics | Render median | 51.40 ms |
35+
| Metrics | Render p95 | 1002.00 ms |
36+
| Benchmark Results | Mean | 841.44 ms |
37+
| Benchmark Results | Median | 827.10 ms |
38+
| Benchmark Results | P95 | 959.30 ms |
39+
| Benchmark Results | P99 | 959.30 ms |
40+
| Benchmark Results | Min | 804.00 ms |
41+
| Benchmark Results | Max | 959.30 ms |
42+
| Benchmark Results | Std Dev | 31.64 ms |
43+
| Input Latency | Mean | 40.74 ms |
44+
| Input Latency | Median | 38.70 ms |
45+
| Input Latency | P95 | 52.70 ms |
46+
| Input Latency | Min | 28.00 ms |
47+
| Input Latency | Max | 61.60 ms |
48+
49+
## Conclusion
50+
51+
- The `40 x 40` remount benchmark is still in the high-cost range, with a mean of ~`841 ms`.
52+
- The `40 x 40` input latency mean is ~`41 ms`, median ~`39 ms`, well below the `100+ ms` threshold where noticeable lag occurs.
53+
- This snapshot is better suited for tracking large-table input performance; if the focus shifts to resize/hover interactions, a separate drag/hover profiling session is recommended.
54+
55+
## Interpretation Notes
56+
57+
- `Initial render`, `Re-render count`, `Last render`, and `Avg render / Median / P95` come from the left-side Metrics panel.
58+
- `Benchmark Results` are the remount benchmark statistics.
59+
- `Input Latency` results do not auto-clear when switching presets or clicking `Generate Table`; re-run after changing configuration.

0 commit comments

Comments
 (0)