Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
a920d4f
refactor: Extract interface
Josmithr Jul 31, 2025
968c35a
docs: Update comments
Josmithr Jul 31, 2025
cbe3972
refactor: Small cleanup
Josmithr Jul 31, 2025
de9f281
refactor: Unify helpers
Josmithr Jul 31, 2025
9a7634f
style: Line break
Josmithr Jul 31, 2025
37691d7
refactor: Rename variables
Josmithr Jul 31, 2025
6a8b9db
test: Improve validation patterns
Josmithr Jul 31, 2025
e17f456
refactor: Remove duplicate assertion
Josmithr Aug 4, 2025
767fbaa
refactor: Use interface instead of class
Josmithr Aug 4, 2025
8f263ea
test: Add event listeners
Josmithr Aug 4, 2025
be4c707
refactor: Use interface instead of class
Josmithr Aug 4, 2025
3a99c23
refactor: Use common helper base
Josmithr Aug 4, 2025
81976cf
refactor: Remove unnecessary parameters
Josmithr Aug 4, 2025
7764ec1
refactor: Shared interface definitions
Josmithr Aug 4, 2025
7741eef
docs: TODOs
Josmithr Aug 4, 2025
2cfd250
Merge branch 'main' into table-perf-test-updates
Josmithr Aug 11, 2025
7a83a17
refactor: Unify argument names
Josmithr Aug 11, 2025
c9eeebd
refactor: Dedupe option properties
Josmithr Aug 11, 2025
8a946c4
refactor: Small initialization optimization
Josmithr Aug 11, 2025
37b5795
refactor: Add options type and allow sparse table
Josmithr Aug 11, 2025
3539e9e
docs: Add missing comment
Josmithr Aug 11, 2025
c82ed94
fix: Tests
Josmithr Aug 11, 2025
79490c4
refactor: Use single helper
Josmithr Aug 11, 2025
8d042c4
test: Clean up pre-post operation validation
Josmithr Aug 11, 2025
3911f4d
refactor: Remove unneeded return values
Josmithr Aug 11, 2025
81f0cc9
fix: Leverage shared helper
Josmithr Aug 11, 2025
2acc79c
test: Update pattern
Josmithr Aug 11, 2025
e0ec7b2
refactor: Unify variable naming
Josmithr Aug 11, 2025
80a099b
docs: Add comments
Josmithr Aug 11, 2025
ab6796d
Merge branch 'main' into table-perf-test-updates
Josmithr Aug 11, 2025
82a1169
fix: Dispose undo/redo manager
Josmithr Aug 12, 2025
542b029
WIP
Josmithr Aug 12, 2025
1183dc7
Merge branch 'table-perf-test-updates' into tree/table-insert-methods…
Josmithr Aug 12, 2025
c536733
fix: Tests
Josmithr Aug 12, 2025
6a1eb11
refactor: Remove single insert/remove methods and add range removal m…
Josmithr Aug 12, 2025
31695b2
fix: Index querying
Josmithr Aug 12, 2025
bbc3c61
fix: API and input validation
Josmithr Aug 12, 2025
f3a27bd
test: Add by-ID coverage
Josmithr Aug 12, 2025
44431ab
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 13, 2025
1edd336
docs: Update comments
Josmithr Aug 13, 2025
57bc97d
docs: Update comments
Josmithr Aug 13, 2025
cbe042e
test: Add perf tests for batch row and column insertion
Josmithr Aug 13, 2025
e31e385
docs: Update docs
Josmithr Aug 13, 2025
52cc631
docs: Update API report
Josmithr Aug 13, 2025
78eb83a
docs: Fix comment typo
Josmithr Aug 13, 2025
d960faa
fix: Call correct APIs
Josmithr Aug 13, 2025
814acd3
fix: Handling of ranges in transactions
Josmithr Aug 13, 2025
f349dd2
refactor: Use `count` instead of `end`
Josmithr Aug 13, 2025
c6dac4d
docs: Update API report
Josmithr Aug 13, 2025
37108bf
Merge branch 'tree/table-insert-methods-refactoring' of https://githu…
Josmithr Aug 13, 2025
3edc347
improvement(tree): Update `removeColumn` to remove cells as well
Josmithr Aug 13, 2025
480af16
docs: Update changeset
Josmithr Aug 13, 2025
6c6fd1a
improvement: Use removal API
Josmithr Aug 13, 2025
f5cfdbe
improvement: Use index overloads
Josmithr Aug 13, 2025
f0c1a83
fix: Make index parameter optional
Josmithr Aug 13, 2025
2983ee9
docs: Update API reports
Josmithr Aug 13, 2025
73193a3
docs: Add notes about perf concerns
Josmithr Aug 13, 2025
b1173c4
Merge branch 'main' into tree/table-column-deletion
Josmithr Aug 13, 2025
ffffef2
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 13, 2025
94346c3
Merge branch 'tree/table-column-deletion' into tree/table-insert-meth…
Josmithr Aug 13, 2025
716e643
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 15, 2025
daec8e2
refactor: Better index find pattern
Josmithr Aug 15, 2025
6ca61e2
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 15, 2025
8fae8e4
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 18, 2025
eaa244a
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 19, 2025
e008aa5
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 21, 2025
ada4bd5
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 27, 2025
6b5cf52
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 27, 2025
5d283b3
Merge branch 'main' into tree/table-insert-methods-refactoring
Josmithr Aug 28, 2025
9ea66fb
test: Remove `.only`
Josmithr Aug 28, 2025
e5fe282
docs: Remove stale TODO
Josmithr Aug 28, 2025
b335d8c
test: Remove duplicated tests
Josmithr Aug 28, 2025
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
186 changes: 138 additions & 48 deletions packages/dds/tree/src/tableSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { assert, fail } from "@fluidframework/core-utils/internal";
import { fail, Heap, oob } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";

import { Tree, TreeAlpha } from "./shared-tree/index.js";
Expand Down Expand Up @@ -550,6 +550,21 @@ export namespace System_TableSchema {
TCell extends ImplicitAllowedTypes = ImplicitAllowedTypes,
> = OptionsWithSchemaFactory<TSchemaFactory> & OptionsWithCellSchema<TCell>;

/**
* An index range.
*/
interface IndexRange {
/**
* Start of the range (inclusive).
*/
start: number;

/**
* The number of items.
*/
count: number;
}

/**
* Factory for creating table schema.
* @system @alpha
Expand Down Expand Up @@ -759,42 +774,55 @@ export namespace System_TableSchema {
return [];
}

// Ensure the specified columns exists before starting transaction.
// Improves user-facing error experience.
for (const columnToRemove of columnsToRemove) {
this._assertContainsColumn(columnToRemove);
// To reduce the number of operations we need to perform, we will build up a series of
// contiguous ranges to be removed from the input list and remove each range as an operation
// (rather than each column individually).
const columnIndicesToRemove: number[] = [];
for (const column of columnsToRemove) {
this._assertContainsColumn(column);
const columnIndex =
this._getColumnIndex(column) ?? fail("Expected column to exist.");
columnIndicesToRemove.push(columnIndex);
}

const rangesToRemove = Table._groupIndicesIntoRanges(columnIndicesToRemove);

const removedColumns: ColumnValueType[] = [];
this._applyEditsInBatch(() => {
// Note, throwing an error within a transaction will abort the entire transaction.
// So if we throw an error here for any column, no columns will be removed.
for (const columnToRemove of columnsToRemove) {
const removedColumn = this._removeColumn(columnToRemove);
removedColumns.push(removedColumn);
// #region Remove columns in batches of contiguous ranges

// Note: Each range we remove from the list impacts the indices of the remaining items.
// As a result, we need to adjust each subsequent range we remove to account for the changes.
// `rangesToRemove` is sorted in ascending order, so it is sufficient to just track and modify based on a single offset.
let offset = 0;
for (const range of rangesToRemove) {
const removed = Table._removeRange(
{
index: range.start - offset,
count: range.count,
},
this.columns,
);
removedColumns.push(...removed);
offset = offset + range.count;
}
});
return removedColumns;
}
}

private _removeColumn(columnOrId: string | ColumnValueType): ColumnValueType {
const column = this._getColumn(columnOrId);
const index = this.columns.indexOf(column);
assert(index !== -1, "Column should exist");
// #endregion

this._applyEditsInBatch(() => {
// Remove the corresponding cell from all rows.
for (const row of this.rows) {
// TypeScript is unable to narrow the row type correctly here, hence the cast.
// See: https://github.com/microsoft/TypeScript/issues/52144
(row as RowValueType).removeCell(column);
}
// #region Remove cells corresponding with removed columns from each row

this.columns.removeAt(index);
});
for (const row of this.rows) {
for (const column of removedColumns) {
// TypeScript is unable to narrow the row type correctly here, hence the cast.
// See: https://github.com/microsoft/TypeScript/issues/52144
(row as RowValueType).removeCell(column);
}
}

return column;
// #endregion
});
return removedColumns;
}
}

public removeRows(
Expand Down Expand Up @@ -826,34 +854,41 @@ export namespace System_TableSchema {
return [];
}

// Ensure the specified rows exists before starting transaction.
// Improves user-facing error experience.
for (const rowToRemove of rowsToRemove) {
this._assertContainsRow(rowToRemove);
// To reduce the number of operations we need to perform, we will build up a series of
// contiguous ranges to be removed from the input list and remove each range as an operation
// (rather than each row individually).
const rowIndicesToRemove: number[] = [];
for (const row of rowsToRemove) {
this._assertContainsRow(row);
const rowIndex = this._getRowIndex(row) ?? fail("Expected row to exist.");
rowIndicesToRemove.push(rowIndex);
}

const rangesToRemove = Table._groupIndicesIntoRanges(rowIndicesToRemove);

const removedRows: RowValueType[] = [];
this._applyEditsInBatch(() => {
// Note, throwing an error within a transaction will abort the entire transaction.
// So if we throw an error here for any row, no rows will be removed.
for (const rowToRemove of rowsToRemove) {
const removedRow = this._removeRow(rowToRemove);
removedRows.push(removedRow);
// Note: Each range we remove from the list impacts the indices of the remaining items.
// As a result, we need to adjust each subsequent range we remove to account for the changes.
// `rangesToRemove` is sorted in ascending order, so it is sufficient to just track and modify based on a single offset.
let offset = 0;
for (const range of rangesToRemove) {
// Note, throwing an error within a transaction will abort the entire transaction.
// So if we throw an error here for any column, no columns will be removed.
const removed = Table._removeRange(
{
index: range.start - offset,
count: range.count,
},
this.rows,
);
removedRows.push(...removed);
offset = offset + range.count;
}
});
return removedRows;
}

private _removeRow(rowOrId: string | RowValueType): RowValueType {
const rowToRemove = this._getRow(rowOrId);

const index = this.rows.indexOf(rowToRemove);
assert(index !== -1, "Row should exist");

this.rows.removeAt(index);
return rowToRemove;
}

public removeCell(
key: TableSchema.CellKey<TColumnSchema, TRowSchema>,
): CellValueType | undefined {
Expand Down Expand Up @@ -1078,7 +1113,7 @@ export namespace System_TableSchema {
const { index, count } = range;
const end = index + count; // exclusive

// TypeScript is unable to narrow the array element type correctly here, hence the cast.
// TypeScript is unable to narrow the row type correctly here, hence the cast.
// See: https://github.com/microsoft/TypeScript/issues/52144
const removedRows = array.slice(
index,
Expand All @@ -1089,6 +1124,61 @@ export namespace System_TableSchema {
return removedRows;
}

/**
* Groups the provided list of indices into a list of ordered, contiguous ranges.
*/
private static _groupIndicesIntoRanges(indices: readonly number[]): IndexRange[] {
// We will leverage a set for unique index lookup.
const indexSet = new Set(indices);

// Tracks which indices have already been visited by the walk to ensure
// indices are not double-counted, and to ensure the algorithm remains O(n).
const visited = new Set<number>();

// Tracks the set of disjoint index ranges, sorted in ascending order.
const sortedRanges = new Heap<IndexRange>({
compare: (a, b) => a.start - b.start,
min: {
start: Number.MIN_VALUE,
// Note: the count here is arbitrary - it isn't used by the comparison function,
// but the Heap type requires that "min" be of the same type as the elements being inserted.
count: Number.NaN,
},
});

for (const index of indices) {
if (visited.has(index)) {
continue;
}

let start = index; // inclusive
let end = index; // inclusive

// Expand backward
while (indexSet.has(start - 1)) {
start--;
}

// Expand forward
while (indexSet.has(end + 1)) {
end++;
}

// Mark all indices in this range as visited so we don't consider them again.
for (let i = start; i <= end; i++) {
visited.add(i);
}

sortedRanges.add({ start, count: end - start + 1 });
}

const result: IndexRange[] = [];
while (sortedRanges.peek() !== undefined) {
result.push(sortedRanges.get() ?? oob());
}
return result;
}

/**
* Ensure that the specified index is a valid location for item insertion in the destination list.
* @throws Throws a usage error if the destination is invalid.
Expand Down
8 changes: 4 additions & 4 deletions packages/dds/tree/src/test/memory/tableTree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ describe("SharedTree table APIs memory usage", () => {
},
});

// Test the memory usage of the SharedTree for undoing the insertion of a column and a row in the middle for a given number of times.
// Test the memory usage of undoing the insertion of a column and a row in the middle N times.
runBenchmark({
title: `Undo: ${scenarioName}`,
tableSize,
Expand Down Expand Up @@ -703,7 +703,7 @@ describe("SharedTree table APIs memory usage", () => {
},
});

// Test the memory usage of the SharedTree for undoing the removal of a column and a row in the middle for a given number of times.
// Test the memory usage of undoing the removal of a column and a row in the middle N times.
runBenchmark({
title: `Undo: ${scenarioName}`,
tableSize,
Expand Down Expand Up @@ -880,7 +880,7 @@ describe("SharedTree table APIs memory usage", () => {
},
});

// Test the memory usage of the SharedTree for undoing the setting of a cell value in the middle for a given number of times.
// Test the memory usage of undoing the setting of a cell value in the middle N times.
runBenchmark({
title: `Undo: ${scenarioName}`,
tableSize,
Expand All @@ -906,7 +906,7 @@ describe("SharedTree table APIs memory usage", () => {
},
});

// Test the memory usage of the SharedTree for redoing the setting of a cell value in the middle for a given number of times.
// Test the memory usage of redoing the setting of a cell value in the middle N times.
runBenchmark({
title: `Redo: ${scenarioName}`,
tableSize,
Expand Down
30 changes: 16 additions & 14 deletions packages/dds/tree/src/test/shared-tree/tableTree.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ describe("SharedTree table APIs execution time", () => {
maxBenchmarkDurationSeconds,
});

// Test the execute time of undoing insert a row and a column in the middle for a given number of times.
// Test the execution time of undoing insert a row and a column in the middle of the table N times.
runBenchmark({
title: `Undo: ${scenarioName}`,
tableSize,
Expand Down Expand Up @@ -427,7 +427,7 @@ describe("SharedTree table APIs execution time", () => {
});

// TODO: AB#43364: Enable these tests back after allowing SharedTree to support undo/redo for removing cells when a column is removed.
// Test the execute time of the SharedTree for redoing a remove row and a column in the middle for a given number of times.
// Test the execution time of redoing a remove row and a column in the middle of the table N times.
// runBenchmark({
// title: `Redo: ${scenarioName}`,
// tableSize,
Expand Down Expand Up @@ -473,12 +473,14 @@ describe("SharedTree table APIs execution time", () => {
operation: (table) => {
for (let i = 0; i < count; i++) {
const column = new Column({});
const row = new Row({ cells: {} });
table.insertColumns({
index: Math.floor(table.columns.length / 2),
columns: [column],
});

const row = new Row({ cells: {} });
table.insertRows({ index: Math.floor(table.rows.length / 2), rows: [row] });

table.removeColumns([column]);
table.removeRows([row]);
}
Expand All @@ -487,7 +489,7 @@ describe("SharedTree table APIs execution time", () => {
});

// TODO: AB#43364: Enable these tests back after allowing SharedTree to support undo/redo for removing cells when a column is removed.
// Test the execute time of undoing insert a row and a column and removing them right away for a given number of times.
// Test the execution time of undoing insert a row and a column and removing them right away N times.
// runBenchmark({
// title: `Undo: ${scenarioName}`,
// tableSize,
Expand Down Expand Up @@ -522,7 +524,7 @@ describe("SharedTree table APIs execution time", () => {
// });

// TODO: AB#43364: Enable these tests back after allowing SharedTree to support undo/redo for removing cells when a column is removed.
// Test the execute time of the SharedTree for redoing an insert row and a column and removing them right away for a given number of times.
// Test the execution time of redoing an insert row and a column and removing them right away N times.
// runBenchmark({
// title: `Redo: ${scenarioName}`,
// tableSize,
Expand Down Expand Up @@ -833,18 +835,18 @@ describe("SharedTree table APIs execution time", () => {
});

// TODO: AB#43364: Enable these tests back after allowing SharedTree to support undo/redo for removing cells when a column is removed.
// Test the execute time of undoing insert a row and a column and removing them right away for a given number of times.
// Test the execute time of undoing remove a row and a column in the middle for a given number of times.
// Test the execution time of undoing insert a row and a column and removing them right away N times.
// Test the execution time of undoing remove a row and a column in the middle of the table N times.
// runBenchmark({
// title: `Undo: ${scenarioName}`,
// tableSize,
// initialCellValue,
// beforeOperation: (table, undoRedoManager) => {
// for (let i = 0; i < count; i++) {
// const column = table.columns[Math.floor(table.columns.length / 2)];
// table.removeColumn(column);
// table.removeColumns([column]);
// const row = table.rows[Math.floor(table.rows.length / 2)];
// table.removeRow(row);
// table.removeRows([row]);
// }
// assert(undoRedoManager.canUndo);
// },
Expand All @@ -863,17 +865,17 @@ describe("SharedTree table APIs execution time", () => {
// });

// TODO: AB#43364: Enable these tests back after allowing SharedTree to support undo/redo for removing cells when a column is removed.
// Test the execute time of the SharedTree for redoing a remove row and a column in the middle for a given number of times.
// Test the execution time of redoing a remove row and a column in the middle of the table N times.
// runBenchmark({
// title: `Redo: ${scenarioName}`,
// tableSize,
// initialCellValue,
// beforeOperation: (table, undoRedoManager) => {
// for (let i = 0; i < count; i++) {
// const column = table.columns[Math.floor(table.columns.length / 2)];
// table.removeColumn(column);
// table.removeColumns([column]);
// const row = table.rows[Math.floor(table.rows.length / 2)];
// table.removeRow(row);
// table.removeRows([row]);
// }
// for (let i = 0; i < count; i++) {
// undoRedoManager.undo();
Expand Down Expand Up @@ -920,7 +922,7 @@ describe("SharedTree table APIs execution time", () => {
maxBenchmarkDurationSeconds,
});

// Test the execute time of undoing set a cell value for a given number of times.
// Test the execution time of undoing set a cell value N times.
runBenchmark({
title: `Undo: ${scenarioName}`,
tableSize,
Expand Down Expand Up @@ -950,7 +952,7 @@ describe("SharedTree table APIs execution time", () => {
maxBenchmarkDurationSeconds,
});

// Test the execute time of the SharedTree for redoing a set cell value for a given number of times.
// Test the execution time of redoing a set cell value N times.
runBenchmark({
title: `Redo: ${scenarioName}`,
tableSize,
Expand Down
Loading