Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 58 additions & 20 deletions packages/table-core/src/core/cells/constructCell.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { warmInstanceShape } from '../../utils'
import type { Table_Internal } from '../../types/Table'
import type { CellData, RowData } from '../../types/type-utils'
import type { TableFeatures } from '../../types/TableFeatures'
Expand All @@ -6,22 +7,69 @@ import type { Cell } from '../../types/Cell'
import type { Column } from '../../types/Column'
import type { Cell_CoreProperties } from './coreCellsFeature.types'

type CellConstructor<
TFeatures extends TableFeatures,
TData extends RowData,
> = new (
column: Column<TFeatures, TData, any>,
row: Row<TFeatures, TData>,
id: string,
) => Cell_CoreProperties<TFeatures, TData, any>

/**
* Creates or retrieves the cell prototype for a table.
* The prototype is cached on the table and shared by all cell instances.
* Creates or retrieves the cell constructor for a table.
*
* Cells are allocated through a per-table constructor function (rather than
* `Object.create`) so the engine learns the exact number of fields a cell
* needs and stores them in-object. The constructor's prototype carries the
* feature APIs and is shared by all cells.
*/
function getCellPrototype<
function getCellConstructor<
TFeatures extends TableFeatures,
TData extends RowData,
>(table: Table_Internal<TFeatures, TData>): object {
if (!table._cellPrototype) {
table._cellPrototype = { table }
>(table: Table_Internal<TFeatures, TData>): CellConstructor<TFeatures, TData> {
if (!table._cellConstructor) {
const cellPrototype: Record<string, unknown> = { table }
const features = Object.values(table._features)
for (let i = 0; i < features.length; i++) {
features[i]!.assignCellPrototype?.(table._cellPrototype, table)
features[i]!.assignCellPrototype?.(cellPrototype, table)
}

// Every core own property is declared here (memo storage as `undefined`)
// so later writes are value writes that never change the cell's hidden
// class.
function TableCell(
this: any,
column: Column<TFeatures, TData, any>,
row: Row<TFeatures, TData>,
id: string,
) {
this._memoGetContext = undefined
this._memos = undefined
this.column = column
this.id = id
this.row = row
}
TableCell.prototype = cellPrototype

table._cellPrototype = cellPrototype
table._cellConstructor = TableCell as unknown as CellConstructor<
TFeatures,
TData
>

// Discarded warmup cell: pre-marks every declared field as mutable on
// the shared cell shape before any real cell exists or any code
// optimizes against it.
warmInstanceShape(
constructCell(
{ id: '' } as Column<TFeatures, TData, unknown>,
{ id: '' } as Row<TFeatures, TData>,
table,
) as Record<string, unknown>,
)
}
return table._cellPrototype
return table._cellConstructor as CellConstructor<TFeatures, TData>
}

/**
Expand All @@ -38,18 +86,8 @@ export function constructCell<
row: Row<TFeatures, TData>,
table: Table_Internal<TFeatures, TData>,
): Cell<TFeatures, TData, TValue> {
// Create cell with shared prototype for memory efficiency
const cellPrototype = getCellPrototype(table)
const cell = Object.create(cellPrototype) as Cell_CoreProperties<
TFeatures,
TData,
TValue
>

// Only assign instance-specific properties
cell.column = column
cell.id = `${row.id}_${column.id}`
cell.row = row
const CellCtor = getCellConstructor(table)
const cell = new CellCtor(column, row, `${row.id}_${column.id}`)

// Initialize instance-specific data for features that need it
const initFns = table._cellInstanceInitFns
Expand Down
3 changes: 3 additions & 0 deletions packages/table-core/src/core/cells/coreCellsFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const coreCellsFeature: TableFeature = {
cell_getContext: {
fn: (cell) => cell_getContext(cell),
memoDeps: (cell) => [cell],
// Called for every rendered cell; a dedicated slot keeps the memo
// load monomorphic. Declared in constructCell.
memoSlot: '_memoGetContext',
},
})
},
Expand Down
12 changes: 12 additions & 0 deletions packages/table-core/src/core/cells/coreCellsFeature.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export interface Cell_CoreProperties<
in out TData extends RowData,
TValue extends CellData = CellData,
> {
/**
* Dedicated memo slot for the render-hot `getContext` API. Declared at
* construction so creating the memo never changes the cell's hidden class.
* @internal
*/
_memoGetContext?: (...args: Array<any>) => any
/**
* Holder for lazily created memoized API state. Declared at construction so
* creating a memo never changes the cell's hidden class.
* @internal
*/
_memos?: Record<string, (...args: Array<any>) => any>
/**
* The associated Column object for the cell.
*/
Expand Down
4 changes: 3 additions & 1 deletion packages/table-core/src/core/columns/constructColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ export function constructColumn<
TValue
>

// Only assign instance-specific properties
// Only assign instance-specific properties. `_memos` is declared up front
// so memoized API calls never change the column's hidden class.
column._memos = undefined
column.accessorFn = accessorFn
column.columnDef = resolvedColumnDef as ColumnDef<TFeatures, TData, TValue>
column.columns = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export interface Column_CoreProperties<
in out TData extends RowData,
TValue extends CellData = CellData,
> {
/**
* Holder for lazily created memoized API state. Declared at construction so
* creating a memo never changes the column's hidden class.
* @internal
*/
_memos?: Record<string, (...args: Array<any>) => any>
/**
* The resolved accessor function to use when extracting the value for the column from each row. Will only be defined if the column def has a valid accessor key or function defined.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/table-core/src/core/headers/buildHeaderGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ function constructHeaderGroup<
pendingParentHeaders.push(header)
}

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required by the TS version compatibility matrix
headerGroup.headers.push(headerToGroup as Header<TFeatures, TData, unknown>)
headerToGroup.headerGroup = headerGroup
}
Expand Down
4 changes: 3 additions & 1 deletion packages/table-core/src/core/headers/constructHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export function constructHeader<
TValue
>

// Only assign instance-specific properties
// Only assign instance-specific properties. `_memos` is declared up front
// so memoized API calls never change the header's hidden class.
header._memos = undefined
header.colSpan = 0
header.column = column
header.depth = options.depth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export interface Header_CoreProperties<
in out TData extends RowData,
TValue extends CellData = CellData,
> {
/**
* Holder for lazily created memoized API state. Declared at construction so
* creating a memo never changes the header's hidden class.
* @internal
*/
_memos?: Record<string, (...args: Array<any>) => any>
/**
* The col-span for the header.
*/
Expand Down
106 changes: 80 additions & 26 deletions packages/table-core/src/core/rows/constructRow.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,88 @@
import { makeObjectMap } from '../../utils'
import { makeObjectMap, warmInstanceShape } from '../../utils'
import type { Table_Internal } from '../../types/Table'
import type { RowData } from '../../types/type-utils'
import type { TableFeatures } from '../../types/TableFeatures'
import type { Row } from '../../types/Row'
import type { Row_CoreProperties } from './coreRowsFeature.types'

type RowConstructor<
TFeatures extends TableFeatures,
TData extends RowData,
> = new (
id: string,
original: TData,
rowIndex: number,
depth: number,
parentId: string | undefined,
subRows: Array<Row<TFeatures, TData>>,
) => Row_CoreProperties<TFeatures, TData>

/**
* Creates or retrieves the row prototype for a table.
* The prototype is cached on the table and shared by all row instances.
* Creates or retrieves the row constructor for a table.
*
* Rows are allocated through a per-table constructor function (rather than
* `Object.create`) so the engine learns the exact number of fields a row
* needs and stores them in-object, instead of spilling into an out-of-line
* property backing store that reallocates as fields are assigned. The
* constructor's prototype carries the feature APIs and is shared by all rows.
*/
function getRowPrototype<
function getRowConstructor<
TFeatures extends TableFeatures,
TData extends RowData,
>(table: Table_Internal<TFeatures, TData>): object {
if (!table._rowPrototype) {
table._rowPrototype = { table }
>(table: Table_Internal<TFeatures, TData>): RowConstructor<TFeatures, TData> {
if (!table._rowConstructor) {
const rowPrototype: Record<string, unknown> = { table }
const features = Object.values(table._features)
for (let i = 0; i < features.length; i++) {
features[i]!.assignRowPrototype?.(table._rowPrototype, table)
features[i]!.assignRowPrototype?.(rowPrototype, table)
}

// Every core own property is declared here (as `undefined` when it has no
// value yet) so later writes are value writes that never change the row's
// hidden class.
function TableRow(
this: any,
id: string,
original: TData,
rowIndex: number,
depth: number,
parentId: string | undefined,
subRows: Array<Row<TFeatures, TData>>,
) {
this._cellsCache = undefined
this._displayIndexCache = -1
this._memoGetAllCells = undefined
this._memoGetAllCellsByColumnId = undefined
this._memos = undefined
this._uniqueValuesCache = undefined
this._valuesCache = makeObjectMap()
this.depth = depth
this.id = id
this.index = rowIndex
this.original = original
this.originalSubRows = undefined
this.parentId = parentId
this.subRows = subRows
}
TableRow.prototype = rowPrototype

table._rowPrototype = rowPrototype
table._rowConstructor = TableRow as unknown as RowConstructor<
TFeatures,
TData
>

// Discarded warmup row: pre-marks every declared field as mutable on the
// shared row shape before any real row exists or any code optimizes
// against it.
warmInstanceShape(
constructRow(table, '', undefined as unknown as TData, -1, -1) as Record<
string,
unknown
>,
)
}
return table._rowPrototype
return table._rowConstructor as RowConstructor<TFeatures, TData>
}

/**
Expand All @@ -40,23 +102,15 @@ export const constructRow = <
subRows?: Array<Row<TFeatures, TData>>,
parentId?: string,
): Row<TFeatures, TData> => {
// Create row with shared prototype for memory efficiency
const rowPrototype = getRowPrototype(table)
const row = Object.create(rowPrototype) as Row_CoreProperties<
TFeatures,
TData
>

// Only assign instance-specific properties
row._displayIndexCache = -1
row._uniqueValuesCache = makeObjectMap()
row._valuesCache = makeObjectMap()
row.depth = depth
row.id = id
row.index = rowIndex
row.original = original
row.parentId = parentId
row.subRows = subRows ?? []
const RowCtor = getRowConstructor(table)
const row = new RowCtor(
id,
original,
rowIndex,
depth,
parentId,
subRows ?? [],
)

// Initialize instance-specific data (e.g., caches) for features that need it
const initFns = table._rowInstanceInitFns
Expand Down
4 changes: 4 additions & 0 deletions packages/table-core/src/core/rows/coreRowsFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ export const coreRowsFeature: TableFeature = {
row_getAllCellsByColumnId: {
fn: (row) => row_getAllCellsByColumnId(row),
memoDeps: (row) => [row.getAllCells()],
// Called per row by pinned-region cell reads; dedicated slots keep
// these render-hot memo loads monomorphic. Declared in constructRow.
memoSlot: '_memoGetAllCellsByColumnId',
},
row_getAllCells: {
fn: (row) => row_getAllCells(row),
memoDeps: (row) => [row.table.getAllLeafColumns()],
memoSlot: '_memoGetAllCells',
},
row_getLeafRows: {
fn: (row) => row_getLeafRows(row),
Expand Down
19 changes: 18 additions & 1 deletion packages/table-core/src/core/rows/coreRowsFeature.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,24 @@ export interface Row_CoreProperties<
* @internal
*/
_displayIndexCache: number
_uniqueValuesCache: Record<string, unknown>
/**
* Dedicated memo slot for the render-hot `getAllCells` API. Declared at
* construction so creating the memo never changes the row's hidden class.
* @internal
*/
_memoGetAllCells?: (...args: Array<any>) => any
/**
* Dedicated memo slot for the `getAllCellsByColumnId` API.
* @internal
*/
_memoGetAllCellsByColumnId?: (...args: Array<any>) => any
/**
* Holder for lazily created memoized API state. Declared at construction so
* creating a memo never changes the row's hidden class.
* @internal
*/
_memos?: Record<string, (...args: Array<any>) => any>
_uniqueValuesCache?: Record<string, unknown>
_valuesCache: Record<string, unknown>
/**
* The depth of the row (if nested or grouped) relative to the root row array.
Expand Down
17 changes: 11 additions & 6 deletions packages/table-core/src/core/rows/coreRowsFeature.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,13 @@ export function row_getUniqueValues<
TFeatures extends TableFeatures,
TData extends RowData,
>(row: Row<TFeatures, TData>, columnId: string) {
if (hasOwn(row._uniqueValuesCache, columnId)) {
return row._uniqueValuesCache[columnId]
// Allocated on first use: only faceting/grouping paths read unique values,
// so most rows never pay for the map. The slot itself is declared at
// construction, keeping this a value write.
const uniqueValuesCache = (row._uniqueValuesCache ??= makeObjectMap())

if (hasOwn(uniqueValuesCache, columnId)) {
return uniqueValuesCache[columnId]
}

const column = row.table.getColumn(columnId)
Expand All @@ -120,16 +125,16 @@ export function row_getUniqueValues<
}

if (!column.columnDef.getUniqueValues) {
row._uniqueValuesCache[columnId] = [row.getValue(columnId)]
return row._uniqueValuesCache[columnId]
uniqueValuesCache[columnId] = [row.getValue(columnId)]
return uniqueValuesCache[columnId]
}

row._uniqueValuesCache[columnId] = column.columnDef.getUniqueValues(
uniqueValuesCache[columnId] = column.columnDef.getUniqueValues(
row.original,
row.index,
)

return row._uniqueValuesCache[columnId]
return uniqueValuesCache[columnId]
}

/**
Expand Down
Loading
Loading