Bound native/shared table loading and rendering; replace full-collection change serialization #52

Open
opened 2026-10-19 12:01:07 +02:00 by michielbdejong · 1 comment

Problem and affected apps

The native table editor has unbounded client-side work as collections grow. This is in the shared application code, not evidence of a fundamental NextGraph engine limit. It also affects the main Atomic-labelled expense editor: both apps call RuntimeExpenseEditor, which passes mode="native" to RuntimeSchemaTables.

Verified against main source matching commit be8938078d97a70519a991ade8b9207d07067c5f.

Current behavior

  • RuntimeSchemaTables.tsx, Projection serializes the complete subscribed row collection with JSON.stringify during render and uses that string as an effect dependency. A single-field update can therefore traverse and allocate a representation of the whole collection. Cost follows serialized data size, roughly rows × fields for uniformly sized rows.
  • useRuntimeShape.ts opens a graph/shape subscription without ordering or paging limits.
  • RuntimeSchemaTables repeatedly materializes row arrays, rebuilds record wrappers and relationship choices, and calls RuntimeAtomicStore.ingest() across the loaded groups. The Atomic mirror is instantiated and ingested even in native mode.
  • TableWorkspace.tsx filters and sorts loaded records in the browser and keeps all table panels mounted, including inactive tabs.
  • SchemaTable.tsx renders every matching row without virtualization. The RDF inspector also constructs all its rows even when its details element is collapsed.

Consequently, work and memory follow loaded records and mounted tables rather than the viewport or changed rows. Removing only JSON.stringify will leave the other bottlenecks.

Requested direction

  1. Replace row-collection JSON comparison with incremental change notifications and stable row identities/snapshots. The pinned ORM exposes added/updated/removed row listeners; verify their behavior for both local writes and incoming updates.
  2. Bound loaded records using ORM ordering/pagination and a retained-page limit. The pinned SDK offers pageSize and maxActivePages; its bounded-page behavior for incoming inserts requires explicit handling/testing. Use the appropriate SDK insertion/deletion APIs for ordered projections.
  3. Make search/sort operate over the intended complete collection through backend queries, not only the current cached page. Clearly expose loading, result scope and paging state.
  4. Virtualize visible rows and avoid eager work for inactive tables, collapsed inspectors and unused Atomic mirrors. Load relationship choices on demand or through bounded queries.
  5. Preserve drafts, baselines, validation errors and focus across paging, virtualization and external updates. Moving a row off screen must not silently discard its draft. Keep schema-wide validation/migration work distinct from ordinary row-render work.

Reproduction and acceptance evidence

  • Add a reproducible seeded-data benchmark with increasing collection sizes, for example 1k, 10k and 100k rows with a fixed field count, plus multiple classes/tabs.
  • Measure initial load, single-field edit, incoming update, search/sort, page/scroll navigation, DOM row count and retained memory. Include a sustained incoming-update case.
  • Demonstrate that ordinary single-row updates no longer serialize/rebuild every loaded collection and that loaded/retained/rendered rows remain bounded by the configured window.
  • Verify creation, deletion and incoming membership/order changes at page boundaries; complete-collection search; draft retention; and lazy inspector behavior.
  • Record hardware/browser, dataset shape, measurements and agreed responsiveness targets. No benchmark has yet established a supported row-count threshold; this issue describes source-level scaling risks, not measured latency or a NextGraph capacity limit.

Related: #51 addresses Atomic table membership and collection discovery. Keep that correctness fix distinct from this shared/native scalability work, while aligning the bounded collection interfaces.

Implementation and remaining latency work (19 October 2026)

PR #61 is merged. Runtime tables retain one 100-record page; the shared grid virtualizes rows, preserves drafts/focus and the #57/#59/#60 reconciliation behavior, and defers inactive tables, inspectors and relationship loading. Native boundary/update tests and 1k/10k/100k benchmark evidence are included.

Keep this issue open for the remaining performance work. The original sub-100ms editing and sub-second query/navigation timings remain measurement goals; they are not grounds to discard the bounded-loading improvements or to declare a supported capacity. Final latency gates need repeated production-build measurements on representative hardware.

The isolated startup profile sampled 54 correct bounded subscriptions. At 100k records per class, a 100-subject ordered subscription has median readiness of 3.44 seconds, about 1.4ms JavaScript row setup, and about 3.43 seconds of main-thread heartbeat delay. Unordered readiness is still 1.77 seconds; the first single-subject case is about 3ms. This prioritizes native multi-subject evaluation and query planning without establishing a particular scan algorithm or a graph-size limit.

Remaining work:

  • Inspect finite-subject evaluation and index use, including other subject positions and data distributions.
  • Investigate keeping engine query/subscription work from blocking editing.
  • Repeat end-to-end production measurements on representative hardware, then agree final latency gates. The existing benchmark continues to assert the original goals and exposes the known 100k failures.

See reproduction and measurements, including pnpm bench:tables and pnpm bench:subscriptions. Broker-backed replication remains outside the local native benchmark's evidence.

Follow-up native startup optimization

PR #63 was merged on 20 October 2026 after the measured performance and small-dataset tradeoff were accepted. It uses bounded single-subject SDK pages, with four startups per task, rather than one native 100-subject subscription. The application window still retains 100 records and renders 13 grid rows. Pending reads, later batches and watchers are canceled/closed together when a window changes or fails.

At 100k, isolated window startup has a 316ms median across nine samples at first/middle/last positions; maximum heartbeat delay is 32ms while membership polling is paused. All 27 window cases at 1k/10k/100k pass. At every size, 100 incoming updates generate exactly 100 row notifications and zero whole-window notifications, and agree with a fresh read.

The follow-up end-to-end 100k run improves initial readiness from 5.17s to 1.91s, sort from 8.38s to 2.09s, and Next from 4.17s to 1.019s. Search remains 2.13s. Small graphs pay additional subscription overhead: 1k initial readiness is 946ms versus the original 480ms, while measured 1k/10k search/sort/paging stay below one second.

This PR does not complete #52 or relax its original goals. The strict benchmark still fails the three 100k search/sort/Next assertions. Complete-collection query/polling costs and main-thread blocking remain follow-up work; final latency gates still need representative production measurements. The verifier continues to retain its full graph.

Validation: 362 unit tests, 79 browser tests with four existing skips, six typechecks, twelve production app builds and generated-artifact checks pass. Broker-backed replication was not rerun. Reproduction and raw measurements are included in the PR's docs/table-scaling.md and docs/benchmarks/ changes.

## Problem and affected apps The native table editor has unbounded client-side work as collections grow. This is in the shared application code, not evidence of a fundamental NextGraph engine limit. It also affects the main Atomic-labelled expense editor: both apps call `RuntimeExpenseEditor`, which passes `mode="native"` to `RuntimeSchemaTables`. Verified against main source matching commit `be8938078d97a70519a991ade8b9207d07067c5f`. ## Current behavior - [`RuntimeSchemaTables.tsx`, `Projection`](https://git.elfaconsortium.eu/ELFA/database-apps/src/commit/be8938078d97a70519a991ade8b9207d07067c5f/shared/schema/RuntimeSchemaTables.tsx#L16) serializes the complete subscribed row collection with `JSON.stringify` during render and uses that string as an effect dependency. A single-field update can therefore traverse and allocate a representation of the whole collection. Cost follows serialized data size, roughly rows × fields for uniformly sized rows. - [`useRuntimeShape.ts`](https://git.elfaconsortium.eu/ELFA/database-apps/src/commit/be8938078d97a70519a991ade8b9207d07067c5f/shared/schema/useRuntimeShape.ts) opens a graph/shape subscription without ordering or paging limits. - `RuntimeSchemaTables` repeatedly materializes row arrays, rebuilds record wrappers and relationship choices, and calls `RuntimeAtomicStore.ingest()` across the loaded groups. The Atomic mirror is instantiated and ingested even in native mode. - [`TableWorkspace.tsx`](https://git.elfaconsortium.eu/ELFA/database-apps/src/commit/be8938078d97a70519a991ade8b9207d07067c5f/shared/ui/TableWorkspace.tsx) filters and sorts loaded records in the browser and keeps all table panels mounted, including inactive tabs. - [`SchemaTable.tsx`](https://git.elfaconsortium.eu/ELFA/database-apps/src/commit/be8938078d97a70519a991ade8b9207d07067c5f/shared/ui/SchemaTable.tsx) renders every matching row without virtualization. The RDF inspector also constructs all its rows even when its details element is collapsed. Consequently, work and memory follow loaded records and mounted tables rather than the viewport or changed rows. Removing only `JSON.stringify` will leave the other bottlenecks. ## Requested direction 1. Replace row-collection JSON comparison with incremental change notifications and stable row identities/snapshots. The pinned ORM exposes added/updated/removed row listeners; verify their behavior for both local writes and incoming updates. 2. Bound loaded records using ORM ordering/pagination and a retained-page limit. The pinned SDK offers `pageSize` and `maxActivePages`; its bounded-page behavior for incoming inserts requires explicit handling/testing. Use the appropriate SDK insertion/deletion APIs for ordered projections. 3. Make search/sort operate over the intended complete collection through backend queries, not only the current cached page. Clearly expose loading, result scope and paging state. 4. Virtualize visible rows and avoid eager work for inactive tables, collapsed inspectors and unused Atomic mirrors. Load relationship choices on demand or through bounded queries. 5. Preserve drafts, baselines, validation errors and focus across paging, virtualization and external updates. Moving a row off screen must not silently discard its draft. Keep schema-wide validation/migration work distinct from ordinary row-render work. ## Reproduction and acceptance evidence - Add a reproducible seeded-data benchmark with increasing collection sizes, for example 1k, 10k and 100k rows with a fixed field count, plus multiple classes/tabs. - Measure initial load, single-field edit, incoming update, search/sort, page/scroll navigation, DOM row count and retained memory. Include a sustained incoming-update case. - Demonstrate that ordinary single-row updates no longer serialize/rebuild every loaded collection and that loaded/retained/rendered rows remain bounded by the configured window. - Verify creation, deletion and incoming membership/order changes at page boundaries; complete-collection search; draft retention; and lazy inspector behavior. - Record hardware/browser, dataset shape, measurements and agreed responsiveness targets. No benchmark has yet established a supported row-count threshold; this issue describes source-level scaling risks, not measured latency or a NextGraph capacity limit. Related: #51 addresses Atomic table membership and collection discovery. Keep that correctness fix distinct from this shared/native scalability work, while aligning the bounded collection interfaces. ## Implementation and remaining latency work (19 October 2026) [PR #61](https://git.elfaconsortium.eu/ELFA/database-apps/pulls/61) is merged. Runtime tables retain one 100-record page; the shared grid virtualizes rows, preserves drafts/focus and the #57/#59/#60 reconciliation behavior, and defers inactive tables, inspectors and relationship loading. Native boundary/update tests and 1k/10k/100k benchmark evidence are included. Keep this issue open for the remaining performance work. The original sub-100ms editing and sub-second query/navigation timings remain measurement goals; they are not grounds to discard the bounded-loading improvements or to declare a supported capacity. Final latency gates need repeated production-build measurements on representative hardware. The isolated startup profile sampled 54 correct bounded subscriptions. At 100k records per class, a 100-subject ordered subscription has median readiness of 3.44 seconds, about 1.4ms JavaScript row setup, and about 3.43 seconds of main-thread heartbeat delay. Unordered readiness is still 1.77 seconds; the first single-subject case is about 3ms. This prioritizes native multi-subject evaluation and query planning without establishing a particular scan algorithm or a graph-size limit. Remaining work: - Inspect finite-subject evaluation and index use, including other subject positions and data distributions. - Investigate keeping engine query/subscription work from blocking editing. - Repeat end-to-end production measurements on representative hardware, then agree final latency gates. The existing benchmark continues to assert the original goals and exposes the known 100k failures. See [reproduction and measurements](https://git.elfaconsortium.eu/ELFA/database-apps/src/branch/main/docs/table-scaling.md), including `pnpm bench:tables` and `pnpm bench:subscriptions`. Broker-backed replication remains outside the local native benchmark's evidence. ## Follow-up native startup optimization [PR #63](https://git.elfaconsortium.eu/ELFA/database-apps/pulls/63) was merged on 20 October 2026 after the measured performance and small-dataset tradeoff were accepted. It uses bounded single-subject SDK pages, with four startups per task, rather than one native 100-subject subscription. The application window still retains 100 records and renders 13 grid rows. Pending reads, later batches and watchers are canceled/closed together when a window changes or fails. At 100k, isolated window startup has a 316ms median across nine samples at first/middle/last positions; maximum heartbeat delay is 32ms while membership polling is paused. All 27 window cases at 1k/10k/100k pass. At every size, 100 incoming updates generate exactly 100 row notifications and zero whole-window notifications, and agree with a fresh read. The follow-up end-to-end 100k run improves initial readiness from 5.17s to 1.91s, sort from 8.38s to 2.09s, and Next from 4.17s to 1.019s. Search remains 2.13s. Small graphs pay additional subscription overhead: 1k initial readiness is 946ms versus the original 480ms, while measured 1k/10k search/sort/paging stay below one second. This PR does not complete #52 or relax its original goals. The strict benchmark still fails the three 100k search/sort/Next assertions. Complete-collection query/polling costs and main-thread blocking remain follow-up work; final latency gates still need representative production measurements. The verifier continues to retain its full graph. Validation: 362 unit tests, 79 browser tests with four existing skips, six typechecks, twelve production app builds and generated-artifact checks pass. Broker-backed replication was not rerun. Reproduction and raw measurements are included in the PR's `docs/table-scaling.md` and `docs/benchmarks/` changes.
Author
Owner

Proposed first step in #58: use the published useDeepSignal React adapter in useRuntimeShape and remove the complete row-collection JSON snapshot from Projection. React dependencies now follow changing proxy identities, while graph/schema subscription replacement and error handling are preserved.

All 332 unit tests, six typechecks, 12 production builds and 28 browser tests passed locally. New browser coverage checks incoming scalar edits, nested set changes and row creation/deletion through distinct actual ORM subscriptions using the test engine.

This addresses the serialization/React bridge portion only. Bounded loading/rendering, relationship options, Atomic ingestion and performance measurements remain outstanding, so #52 stays open.

Proposed first step in https://git.elfaconsortium.eu/ELFA/database-apps/pulls/58: use the published `useDeepSignal` React adapter in `useRuntimeShape` and remove the complete row-collection JSON snapshot from `Projection`. React dependencies now follow changing proxy identities, while graph/schema subscription replacement and error handling are preserved. All 332 unit tests, six typechecks, 12 production builds and 28 browser tests passed locally. New browser coverage checks incoming scalar edits, nested set changes and row creation/deletion through distinct actual ORM subscriptions using the test engine. This addresses the serialization/React bridge portion only. Bounded loading/rendering, relationship options, Atomic ingestion and performance measurements remain outstanding, so #52 stays open.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ELFA/database-apps#52
No description provided.