Skip to content

Commit

Permalink
refactor(tree): Optimize certain leaf access cases (#21595)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexvy86 authored Jun 26, 2024
1 parent d4f660f commit 41ee750
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 20 deletions.
42 changes: 22 additions & 20 deletions packages/dds/tree/src/simple-tree/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type MapTreeNode,
tryGetMapTreeNode,
typeNameSymbol,
isFlexTreeNode,
} from "../feature-libraries/index.js";
import { type Mutable, fail, isReadonlyArray } from "../util/index.js";

Expand Down Expand Up @@ -55,27 +56,28 @@ export function isTreeNode(candidate: unknown): candidate is TreeNode | Unhydrat
* Retrieve the associated proxy for the given field.
* */
export function getProxyForField(field: FlexTreeField): TreeNode | TreeValue | undefined {
function tryToUnboxLeaves(
flexField: FlexTreeTypedField<
FlexFieldSchema<typeof FieldKinds.required | typeof FieldKinds.optional>
>,
): TreeNode | TreeValue | undefined {
const maybeUnboxedContent = flexField.content;
return isFlexTreeNode(maybeUnboxedContent)
? getOrCreateNodeProxy(maybeUnboxedContent)
: maybeUnboxedContent;
}
switch (field.schema.kind) {
case FieldKinds.required: {
const asValue = field as FlexTreeTypedField<FlexFieldSchema<typeof FieldKinds.required>>;

// TODO: Ideally, we would return leaves without first boxing them. However, this is not
// as simple as calling '.content' since this skips the node and returns the FieldNode's
// inner field.
return getOrCreateNodeProxy(asValue.boxedContent);
const typedField = field as FlexTreeTypedField<
FlexFieldSchema<typeof FieldKinds.required>
>;
return tryToUnboxLeaves(typedField);
}
case FieldKinds.optional: {
const asValue = field as FlexTreeTypedField<FlexFieldSchema<typeof FieldKinds.optional>>;

// TODO: Ideally, we would return leaves without first boxing them. However, this is not
// as simple as calling '.content' since this skips the node and returns the FieldNode's
// inner field.

const maybeContent = asValue.boxedContent;

// Normally, empty fields are unreachable due to the behavior of 'tryGetField'. However, the
// root field is a special case where the field is always present (even if empty).
return maybeContent === undefined ? undefined : getOrCreateNodeProxy(maybeContent);
const typedField = field as FlexTreeTypedField<
FlexFieldSchema<typeof FieldKinds.optional>
>;
return tryToUnboxLeaves(typedField);
}
// TODO: Remove if/when 'FieldNode' is removed.
case FieldKinds.sequence: {
Expand All @@ -84,9 +86,9 @@ export function getProxyForField(field: FlexTreeField): TreeNode | TreeValue | u
fail("'sequence' field is unexpected.");
}
case FieldKinds.identifier: {
const identifier = field.boxedAt(0);
assert(identifier !== undefined, 0x91a /* identifier must exist */);
return getOrCreateNodeProxy(identifier);
// Identifier fields are just value fields that hold strings
return (field as FlexTreeTypedField<FlexFieldSchema<typeof FieldKinds.required>>)
.content as string;
}

default:
Expand Down
232 changes: 232 additions & 0 deletions packages/dds/tree/src/test/simple-tree/simpleTree.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
writeWideSimpleTreeNewValue,
type WideTreeNode,
} from "./benchmarkUtilities.js";
import { SchemaFactory } from "../../simple-tree/index.js";
import { hydrate } from "./utils.js";

// number of nodes in test for wide trees
const nodesCountWide = [
Expand Down Expand Up @@ -80,6 +82,236 @@ describe("SimpleTree benchmarks", () => {
},
});
}

describe("Access to leaves", () => {
/**
* Creates a pair of benchmarks to test accessing leaf values in a tree, one for unhydrated nodes and one for flex
* nodes.
* @param title - The title for the test.
* @param unhydratedNodeInitFunction - Function that returns the test tree with unhydrated nodes.
* @param flexNodeInitFunction - Function that returns the test tree with flex nodes.
* @param treeReadingFunction - Function that reads the leaf value from the tree. It should have no side-effects.
* @param expectedValue - The expected value of the leaf.
*/
function generateBenchmarkPair<RootNode>(
title: string,
unhydratedNodeInitFunction: () => RootNode,
flexNodeInitFunction: () => RootNode,
treeReadingFunction: (tree: RootNode) => number | undefined,
expectedValue: number | undefined,
) {
const unhydratedTree = unhydratedNodeInitFunction();
let readNumber: number | undefined;
benchmark({
type: BenchmarkType.Measurement,
title: `${title} (unhydrated node)`,
benchmarkFn: () => {
readNumber = treeReadingFunction(unhydratedTree);
},
after: () => {
assert.equal(readNumber, expectedValue);
},
});
const flexTree = flexNodeInitFunction();
benchmark({
type: BenchmarkType.Measurement,
title: `${title} (flex node)`,
benchmarkFn: () => {
readNumber = treeReadingFunction(flexTree);
},
after: () => {
assert.equal(readNumber, expectedValue);
},
});
}

describe("Optional object property", () => {
const factory = new SchemaFactory("test");
class MySchema extends factory.object("root", {
value: factory.optional(factory.number),
leafUnion: factory.optional([factory.number, factory.string]),
complexUnion: factory.optional([
factory.number,
factory.object("inner", {
value: factory.optional(factory.number),
}),
]),
}) {}

const testCases = [
{
title: `Read value from leaf`,
initUnhydrated: () => new MySchema({ value: 1 }),
readFunction: (tree: MySchema) => tree.value,
expected: 1,
},
{
title: `Read value from union of two leaves`,
initUnhydrated: () => new MySchema({ leafUnion: 1 }),
readFunction: (tree: MySchema) => tree.leafUnion as number,
expected: 1,
},
{
title: `Read value from union of leaf and non-leaf`,
initUnhydrated: () => new MySchema({ complexUnion: 1 }),
readFunction: (tree: MySchema) => tree.complexUnion as number,
expected: 1,
},
{
title: `Read undefined from leaf`,
initUnhydrated: () => new MySchema({}),
readFunction: (tree: MySchema) => tree.value,
expected: undefined,
},
{
title: `Read undefined from union of two leaves`,
initUnhydrated: () => new MySchema({}),
readFunction: (tree: MySchema) => tree.leafUnion as number,
expected: undefined,
},
{
title: `Read undefined from union of leaf and non-leaf`,
initUnhydrated: () => new MySchema({}),
readFunction: (tree: MySchema) => tree.complexUnion as number,
expected: undefined,
},
];

for (const { title, initUnhydrated, readFunction, expected } of testCases) {
const initFlexNode = () => hydrate(MySchema, initUnhydrated());
generateBenchmarkPair(title, initUnhydrated, initFlexNode, readFunction, expected);
}
});

describe("Required object property", () => {
const factory = new SchemaFactory("test");
class MySchema extends factory.object("root", {
value: factory.number,
leafUnion: [factory.number, factory.string],
complexUnion: [
factory.number,
factory.object("inner", {
value: factory.number,
}),
],
}) {}

const testCases = [
{
title: `Read value from leaf`,
readFunction: (tree: MySchema) => tree.value,
},
{
title: `Read value from union of two leaves`,
readFunction: (tree: MySchema) => tree.leafUnion as number,
},
{
title: `Read value from union of leaf and non-leaf`,
readFunction: (tree: MySchema) => tree.complexUnion as number,
},
];

const initUnhydrated = () => new MySchema({ value: 1, leafUnion: 1, complexUnion: 1 });
const initFlex = () => hydrate(MySchema, initUnhydrated());
for (const { title, readFunction } of testCases) {
generateBenchmarkPair(title, initUnhydrated, initFlex, readFunction, 1);
}
});

describe("Map keys", () => {
const factory = new SchemaFactory("test");
class NumberMap extends factory.map("root", [factory.number]) {}
class NumberStringMap extends factory.map("root", [factory.number, factory.string]) {}
class NumberObjectMap extends factory.map("root", [
factory.number,
factory.object("inner", { value: factory.number }),
]) {}
// Just to simplify typing a bit below in a way that keeps TypeScript happy
type CombinedTypes = NumberMap | NumberStringMap | NumberObjectMap;

const valueTestCases = [
{
title: `Read value from leaf`,
mapType: NumberMap,
},
{
title: `Read value from union of two leaves`,
mapType: NumberStringMap,
},
{
title: `Read value from union of leaf and non-leaf`,
mapType: NumberObjectMap,
},
];

for (const { title, mapType } of valueTestCases) {
const initUnhydrated = () => new mapType([["a", 1]]);
const initFlex = () => hydrate(mapType, initUnhydrated());
const readFunction = (tree: CombinedTypes) => tree.get("a") as number;
generateBenchmarkPair(title, initUnhydrated, initFlex, readFunction, 1);
}

const undefinedTestCases = [
{
title: `Read undefined from leaf`,
mapType: NumberMap,
read: (tree: CombinedTypes) => tree.get("b") as number,
expected: undefined,
},
{
title: `Read undefined from union of two leaves`,
mapType: NumberStringMap,
read: (tree: CombinedTypes) => tree.get("b") as number,
expected: undefined,
},
{
title: `Read undefined from union of leaf and non-leaf`,
mapType: NumberObjectMap,
read: (tree: CombinedTypes) => tree.get("b") as number,
expected: undefined,
},
];

for (const { title, mapType } of undefinedTestCases) {
const initUnhydrated = () => new mapType([["a", 1]]);
const initFlex = () => hydrate(mapType, initUnhydrated());
const readFunction = (tree: CombinedTypes) => tree.get("b") as number;
generateBenchmarkPair(title, initUnhydrated, initFlex, readFunction, undefined);
}
});

describe("Array entries", () => {
const factory = new SchemaFactory("test");
class NumArray extends factory.array("root", [factory.number]) {}
class NumStringArray extends factory.array("root", [factory.number, factory.string]) {}
class NumObjectArray extends factory.array("root", [
factory.number,
factory.object("inner", { value: factory.number }),
]) {}

const testCases = [
{
title: `Read value from leaf`,
arrayType: NumArray,
},
{
title: `Read value from union of two leaves`,
arrayType: NumStringArray,
},
{
title: `Read value from union of leaf and non-leaf`,
arrayType: NumObjectArray,
},
];

for (const { title, arrayType } of testCases) {
const initUnhydrated = () => new arrayType([1]);
const initFlex = () => hydrate(arrayType, initUnhydrated());
const read = (tree: NumArray | NumStringArray | NumObjectArray) => tree[0] as number;
generateBenchmarkPair(title, initUnhydrated, initFlex, read, 1);
}
});
});
});

describe(`Edit SimpleTree`, () => {
Expand Down

0 comments on commit 41ee750

Please sign in to comment.