Skip to content

Commit

Permalink
Make forest config tree-shake compatible (#23488)
Browse files Browse the repository at this point in the history
## Description

Establish a new pattern for configuration to replace enums with
something compatible with tree shaking.

This has the annoyance that the different options are no longer grouped
under a scope.
It has the benefits that different values for the same option can now
have different release tags,
and that the code supporting the unused options can be removed with tree
shaking (including for users of the public API, which can't see the
alternative options but used to include them in the bundle anyway).

Apply this pattern to ForestType, giving a >7KB reduction in the tree
bundle, as measure by our bundle size tests.

## Breaking Changes

This is an incompatible change to the forest configuration alpha APIs:
users will need to point to the new opaque objects instead of enum
members. All impacted code will fail to build rather than having runtime
errors.
  • Loading branch information
CraigMacomber authored Jan 8, 2025
1 parent 46abe82 commit dababa5
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 63 deletions.
16 changes: 11 additions & 5 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,19 @@ export interface ForestOptions {
readonly forest?: ForestType;
}

// @alpha
export enum ForestType {
Expensive = 2,
Optimized = 1,
Reference = 0
// @alpha @sealed
export interface ForestType extends ErasedType_2<"ForestType"> {
}

// @alpha
export const ForestTypeExpensiveDebug: ForestType;

// @alpha
export const ForestTypeOptimized: ForestType;

// @alpha
export const ForestTypeReference: ForestType;

// @alpha @deprecated
export function getBranch(tree: ITree): BranchableTree;

Expand Down
5 changes: 4 additions & 1 deletion packages/dds/tree/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export {
export {
type ITreeInternal,
type SharedTreeOptions,
ForestType,
type ForestType,
type SharedTreeFormatOptions,
SharedTreeFormatVersion,
Tree,
Expand All @@ -55,6 +55,9 @@ export {
type TransactionResultExt,
type TransactionResultSuccess,
type TransactionResultFailed,
ForestTypeOptimized,
ForestTypeExpensiveDebug,
ForestTypeReference,
} from "./shared-tree/index.js";

export {
Expand Down
5 changes: 4 additions & 1 deletion packages/dds/tree/src/shared-tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ export {
type SharedTreeOptions,
SharedTree,
getBranch,
ForestType,
type ForestType,
type SharedTreeContentSnapshot,
type SharedTreeFormatOptions,
SharedTreeFormatVersion,
buildConfiguredForest,
defaultSharedTreeOptions,
type ForestOptions,
type ITreeInternal,
ForestTypeOptimized,
ForestTypeExpensiveDebug,
ForestTypeReference,
} from "./sharedTree.js";

export {
Expand Down
100 changes: 60 additions & 40 deletions packages/dds/tree/src/shared-tree/sharedTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* Licensed under the MIT License.
*/

import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
import type { IFluidHandle } from "@fluidframework/core-interfaces/internal";
import { assert } from "@fluidframework/core-utils/internal";
import type { ErasedType, IFluidHandle } from "@fluidframework/core-interfaces/internal";
import type {
IChannelAttributes,
IChannelFactory,
Expand Down Expand Up @@ -186,30 +186,6 @@ function getCodecVersions(formatVersion: number): ExplicitCodecVersions {
return versions;
}

/**
* Build and return a forest of the requested type.
*/
export function buildConfiguredForest(
type: ForestType,
schema: TreeStoredSchemaSubscription,
idCompressor: IIdCompressor,
): IEditableForest {
switch (type) {
case ForestType.Optimized:
return buildChunkedForest(
makeTreeChunker(schema, defaultSchemaPolicy),
undefined,
idCompressor,
);
case ForestType.Reference:
return buildForest();
case ForestType.Expensive:
return buildForest(undefined, true);
default:
unreachableCase(type);
}
}

/**
* Shared tree, configured with a good set of indexes and field kinds which will maintain compatibility over time.
*
Expand Down Expand Up @@ -596,26 +572,70 @@ export interface SharedTreeFormatOptions {

/**
* Used to distinguish between different forest types.
* @remarks
* Current options are {@link ForestTypeReference}, {@link ForestTypeOptimized} and {@link ForestTypeExpensiveDebug}.
* @sealed @alpha
*/
export interface ForestType extends ErasedType<"ForestType"> {}

/**
* Reference implementation of forest.
* @remarks
* A simple implementation with minimal complexity and moderate debuggability, validation and performance.
* @privateRemarks
* The "ObjectForest" forest type.
* @alpha
*/
export enum ForestType {
/**
* The "ObjectForest" forest type.
*/
Reference = 0,
/**
* The "ChunkedForest" forest type.
*/
Optimized = 1,
/**
* The "ObjectForest" forest type with expensive asserts for debugging.
*/
Expensive = 2,
export const ForestTypeReference = toForestType(() => buildForest());

/**
* Optimized implementation of forest.
* @remarks
* A complex optimized forest implementation, which has minimal validation and debuggability to optimize for performance.
* Uses an internal representation optimized for size designed to scale to larger datasets with reduced overhead.
* @privateRemarks
* The "ChunkedForest" forest type.
* @alpha
*/
export const ForestTypeOptimized = toForestType(
(schema: TreeStoredSchemaSubscription, idCompressor: IIdCompressor) =>
buildChunkedForest(makeTreeChunker(schema, defaultSchemaPolicy), undefined, idCompressor),
);

/**
* Slow implementation of forest intended only for debugging.
* @remarks
* Includes validation with scales poorly.
* May be asymptotically slower than {@link ForestTypeReference}, and may perform very badly with larger data sizes.
* @privateRemarks
* The "ObjectForest" forest type with expensive asserts for debugging.
* @alpha
*/
export const ForestTypeExpensiveDebug = toForestType(() => buildForest(undefined, true));

type ForestFactory = (
schema: TreeStoredSchemaSubscription,
idCompressor: IIdCompressor,
) => IEditableForest;

function toForestType(factory: ForestFactory): ForestType {
return factory as unknown as ForestType;
}

/**
* Build and return a forest of the requested type.
*/
export function buildConfiguredForest(
factory: ForestType,
schema: TreeStoredSchemaSubscription,
idCompressor: IIdCompressor,
): IEditableForest {
return (factory as unknown as ForestFactory)(schema, idCompressor);
}

export const defaultSharedTreeOptions: Required<SharedTreeOptionsInternal> = {
jsonValidator: noopValidator,
forest: ForestType.Reference,
forest: ForestTypeReference,
treeEncodeType: TreeCompressionStrategy.Compressed,
formatVersion: SharedTreeFormatVersion.v3,
disposeForksAfterTransaction: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ import {
cursorForJsonableTreeNode,
} from "../../../feature-libraries/index.js";
import {
ForestType,
type TreeStoredContent,
type ISharedTreeEditor,
SharedTreeFactory,
Tree,
ForestTypeOptimized,
} from "../../../shared-tree/index.js";
import {
MockTreeCheckout,
Expand Down Expand Up @@ -86,7 +86,7 @@ import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/in

const options = {
jsonValidator: typeboxValidator,
forest: ForestType.Optimized,
forest: ForestTypeOptimized,
summaryEncodeType: TreeCompressionStrategy.Compressed,
};

Expand Down Expand Up @@ -397,7 +397,7 @@ describe("End to end chunked encoding", () => {
it("Initializing tree creates uniform chunks with encoded identifiers", async () => {
const factory = new SharedTreeFactory({
jsonValidator: typeboxValidator,
forest: ForestType.Optimized,
forest: ForestTypeOptimized,
});

const runtime = new MockFluidDataStoreRuntime({
Expand Down
10 changes: 7 additions & 3 deletions packages/dds/tree/src/test/memory/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { testIdCompressor } from "../utils.js";

import {
configuredSharedTree,
ForestType,
ForestTypeOptimized,
ForestTypeReference,
SchemaFactory,
TreeCompressionStrategy,
TreeViewConfiguration,
Expand Down Expand Up @@ -233,11 +234,14 @@ describe("SharedTree memory usage", () => {
) {
describe(title, () => {
for (const numberOfNodes of testNodeCounts) {
for (const forestType of [ForestType.Reference, ForestType.Optimized]) {
for (const [forestName, forestType] of [
["ObjectForest", ForestTypeReference],
["ChunkedForest", ForestTypeOptimized],
] as const) {
benchmarkMemory(
new (class implements IMemoryTestObject {
public readonly title =
`initialize ${numberOfNodes} nodes into tree using ${forestType === 0 ? "ObjectForest" : "ChunkedForest"}`;
`initialize ${numberOfNodes} nodes into tree using ${forestName}`;

private sharedTree: TreeView<typeof schema> | undefined;

Expand Down
12 changes: 7 additions & 5 deletions packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ import {
// eslint-disable-next-line import/no-internal-modules
} from "../../feature-libraries/object-forest/objectForest.js";
import {
ForestType,
ForestTypeExpensiveDebug,
ForestTypeOptimized,
ForestTypeReference,
getBranch,
type ISharedTree,
type SharedTree,
Expand Down Expand Up @@ -100,7 +102,7 @@ const enableSchemaValidation = true;

const DebugSharedTree = configuredSharedTree({
jsonValidator: typeboxValidator,
forest: ForestType.Reference,
forest: ForestTypeReference,
}) as ISharedObjectKind<unknown> as ISharedObjectKind<SharedTree>;

class MockSharedTreeRuntime extends MockFluidDataStoreRuntime {
Expand Down Expand Up @@ -1886,7 +1888,7 @@ describe("SharedTree", () => {
1,
new SharedTreeFactory({
jsonValidator: typeboxValidator,
forest: ForestType.Reference,
forest: ForestTypeReference,
}),
);
const forest = trees[0].checkout.forest;
Expand All @@ -1899,7 +1901,7 @@ describe("SharedTree", () => {
1,
new SharedTreeFactory({
jsonValidator: typeboxValidator,
forest: ForestType.Optimized,
forest: ForestTypeOptimized,
}),
);
assert.equal(trees[0].checkout.forest instanceof ChunkedForest, true);
Expand All @@ -1910,7 +1912,7 @@ describe("SharedTree", () => {
1,
new SharedTreeFactory({
jsonValidator: typeboxValidator,
forest: ForestType.Expensive,
forest: ForestTypeExpensiveDebug,
}),
);
const forest = trees[0].checkout.forest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,19 @@ export interface ForestOptions {
readonly forest?: ForestType;
}

// @alpha
export enum ForestType {
Expensive = 2,
Optimized = 1,
Reference = 0
// @alpha @sealed
export interface ForestType extends ErasedType<"ForestType"> {
}

// @alpha
export const ForestTypeExpensiveDebug: ForestType;

// @alpha
export const ForestTypeOptimized: ForestType;

// @alpha
export const ForestTypeReference: ForestType;

// @alpha @deprecated
export function getBranch(tree: ITree): BranchableTree;

Expand Down

0 comments on commit dababa5

Please sign in to comment.