Skip to content

Commit

Permalink
Reduce getClass calls for property grouping (#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasDov authored Feb 15, 2024
1 parent 9b84039 commit edd180e
Show file tree
Hide file tree
Showing 17 changed files with 275 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ProcessedHierarchyNode,
ProcessedInstanceHierarchyNode,
} from "./HierarchyNode";
import { getClass } from "./internal/Common";
import { getClass } from "./internal/GetClass";
import { parseFullClassName } from "./Metadata";
import { ECSqlQueryDef } from "./queries/ECSqlCore";
import { Id64String, InstanceKey } from "./values/Values";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ import {
ProcessedHierarchyNode,
ProcessedInstanceHierarchyNode,
} from "./HierarchyNode";
import { LOGGING_NAMESPACE as CommonLoggingNamespace, getClass, hasChildren } from "./internal/Common";
import { LOGGING_NAMESPACE as CommonLoggingNamespace, hasChildren } from "./internal/Common";
import { FilteringHierarchyLevelDefinitionsFactory } from "./internal/FilteringHierarchyLevelDefinitionsFactory";
import { getClass } from "./internal/GetClass";
import { createDetermineChildrenOperator } from "./internal/operators/DetermineChildren";
import { createGroupingOperator } from "./internal/operators/Grouping";
import { createHideIfNoChildrenOperator } from "./internal/operators/HideIfNoChildren";
Expand Down
52 changes: 21 additions & 31 deletions packages/hierarchy-builder/src/hierarchy-builder/internal/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import naturalCompare from "natural-compare-lite";
import { assert } from "@itwin/core-bentley";
import { ECClass, ECSchema, IMetadataProvider } from "../ECMetadata";
import { ECClass, IMetadataProvider } from "../ECMetadata";
import {
HierarchyNodeKey,
HierarchyNodeLabelGroupingParams,
Expand All @@ -15,40 +15,11 @@ import {
ProcessedCustomHierarchyNode,
ProcessedInstanceHierarchyNode,
} from "../HierarchyNode";
import { getLogger } from "../Logging";
import { parseFullClassName } from "../Metadata";
import { getClass } from "./GetClass";

/** @internal */
export const LOGGING_NAMESPACE = "Presentation.HierarchyBuilder";

/** @internal */
export async function getClass(metadata: IMetadataProvider, fullClassName: string): Promise<ECClass> {
const { schemaName, className } = parseFullClassName(fullClassName);
let schema: ECSchema | undefined;
try {
schema = await metadata.getSchema(schemaName);
} catch (e) {
assert(e instanceof Error);
getLogger().logError(`${LOGGING_NAMESPACE}`, `Failed to get schema "${schemaName} with error ${e.message}."`);
}
if (!schema) {
throw new Error(`Invalid schema "${schemaName}"`);
}

let nodeClass: ECClass | undefined;
try {
nodeClass = await schema.getClass(className);
} catch (e) {
assert(e instanceof Error);
getLogger().logError(`${LOGGING_NAMESPACE}`, `Failed to get schema "${schemaName} with error ${e.message}."`);
}
if (!nodeClass) {
throw new Error(`Invalid class "${className}" in schema "${schemaName}"`);
}

return nodeClass;
}

function mergeNodeHandlingParams(
lhs: InstanceHierarchyNodeProcessingParams | undefined,
rhs: InstanceHierarchyNodeProcessingParams | undefined,
Expand Down Expand Up @@ -168,3 +139,22 @@ export function mergeSortedArrays<TLhsItem, TRhsItem>(
export function compareNodesByLabel<TLhsNode extends { label: string }, TRhsNode extends { label: string }>(lhs: TLhsNode, rhs: TRhsNode): number {
return naturalCompare(lhs.label.toLocaleLowerCase(), rhs.label.toLocaleLowerCase());
}

/** @internal */
export class BaseClassChecker {
private _map = new Map<string, boolean>();
private _metadataProvider: IMetadataProvider;
public constructor(metadataProvider: IMetadataProvider) {
this._metadataProvider = metadataProvider;
}

public async isECClassOfBaseECClass(ecClassNameToCheck: string, baseECClass: ECClass): Promise<boolean> {
let isCurrentNodeClassOfBase = this._map.get(`${ecClassNameToCheck}${baseECClass.fullName}`);
if (isCurrentNodeClassOfBase === undefined) {
const currentNodeECClass = await getClass(this._metadataProvider, ecClassNameToCheck);
isCurrentNodeClassOfBase = await currentNodeECClass.is(baseECClass);
this._map.set(`${ecClassNameToCheck}${baseECClass.fullName}`, isCurrentNodeClassOfBase);
}
return isCurrentNodeClassOfBase;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
ProcessedHierarchyNode,
} from "../HierarchyNode";
import { InstanceKey } from "../values/Values";
import { getClass } from "./Common";
import { getClass } from "./GetClass";
import { defaultNodesParser } from "./TreeNodesReader";

/** @internal */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { assert } from "@itwin/core-bentley";
import { ECClass, ECSchema, IMetadataProvider } from "../ECMetadata";
import { getLogger } from "../Logging";
import { parseFullClassName } from "../Metadata";
import { LOGGING_NAMESPACE } from "./Common";

/** @internal */
export async function getClass(metadata: IMetadataProvider, fullClassName: string): Promise<ECClass> {
const { schemaName, className } = parseFullClassName(fullClassName);
let schema: ECSchema | undefined;
try {
schema = await metadata.getSchema(schemaName);
} catch (e) {
assert(e instanceof Error);
getLogger().logError(`${LOGGING_NAMESPACE}`, `Failed to get schema "${schemaName} with error ${e.message}."`);
}
if (!schema) {
throw new Error(`Invalid schema "${schemaName}"`);
}

let nodeClass: ECClass | undefined;
try {
nodeClass = await schema.getClass(className);
} catch (e) {
assert(e instanceof Error);
getLogger().logError(`${LOGGING_NAMESPACE}`, `Failed to get schema "${schemaName} with error ${e.message}."`);
}
if (!nodeClass) {
throw new Error(`Invalid class "${className}" in schema "${schemaName}"`);
}

return nodeClass;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { concatMap, from, Observable, of, tap, toArray } from "rxjs";
import { IMetadataProvider } from "../../ECMetadata";
import { HierarchyNode, HierarchyNodeKey, ProcessedGroupingHierarchyNode, ProcessedHierarchyNode, ProcessedInstanceHierarchyNode } from "../../HierarchyNode";
import { getLogger } from "../../Logging";
import { IPrimitiveValueFormatter } from "../../values/Formatting";
import { compareNodesByLabel, createOperatorLoggingNamespace, mergeSortedArrays } from "../Common";
import { BaseClassChecker, compareNodesByLabel, createOperatorLoggingNamespace, mergeSortedArrays } from "../Common";
import { assignAutoExpand } from "./grouping/AutoExpand";
import { createBaseClassGroupingHandlers } from "./grouping/BaseClassGrouping";
import { createClassGroups } from "./grouping/ClassGrouping";
Expand Down Expand Up @@ -128,10 +127,12 @@ export async function createGroupingHandlers(
localizedStrings: PropertiesGroupingLocalizedStrings,
): Promise<GroupingHandler[]> {
const groupingHandlers: GroupingHandler[] = new Array<GroupingHandler>();
const baseClassChecker = new BaseClassChecker(metadata);
groupingHandlers.push(
...(await createBaseClassGroupingHandlers(
metadata,
nodes.filter((n): n is ProcessedInstanceHierarchyNode => HierarchyNode.isInstancesNode(n)),
baseClassChecker,
)),
);
groupingHandlers.push(async (allNodes) => createClassGroups(metadata, allNodes));
Expand All @@ -141,6 +142,7 @@ export async function createGroupingHandlers(
nodes.filter((n): n is ProcessedInstanceHierarchyNode => HierarchyNode.isInstancesNode(n)),
valueFormatter,
localizedStrings,
baseClassChecker,
)),
);
groupingHandlers.push(async (allNodes) => createLabelGroups(allNodes));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import { omit } from "@itwin/core-bentley";
import { ECClass, IMetadataProvider } from "../../../ECMetadata";
import { ClassGroupingNodeKey, ProcessedInstanceHierarchyNode } from "../../../HierarchyNode";
import { getClass } from "../../Common";
import { BaseClassChecker } from "../../Common";
import { getClass } from "../../GetClass";
import { GroupingHandler, GroupingHandlerResult } from "../Grouping";

/** @internal */
Expand All @@ -23,13 +24,12 @@ export async function getBaseClassGroupingECClasses(metadata: IMetadataProvider,

/** @internal */
export async function createBaseClassGroupsForSingleBaseClass(
metadata: IMetadataProvider,
nodes: ProcessedInstanceHierarchyNode[],
baseECClass: ECClass,
baseClassChecker: BaseClassChecker,
): Promise<GroupingHandlerResult> {
const groupedNodes = new Array<ProcessedInstanceHierarchyNode>();
const ungroupedNodes = new Array<ProcessedInstanceHierarchyNode>();
const classesAreBaseClass = new Map<string, boolean>();
for (const node of nodes) {
if (
!node.processingParams?.grouping?.byBaseClasses ||
Expand All @@ -40,17 +40,7 @@ export async function createBaseClassGroupsForSingleBaseClass(
}
const fullCurrentNodeClassName = node.key.instanceKeys[0].className;

let isCurrentNodeClassOfBase = classesAreBaseClass.get(fullCurrentNodeClassName);
if (isCurrentNodeClassOfBase === undefined) {
const currentNodeECClass = await getClass(metadata, fullCurrentNodeClassName);
if (await currentNodeECClass.is(baseECClass)) {
classesAreBaseClass.set(fullCurrentNodeClassName, true);
isCurrentNodeClassOfBase = true;
} else {
classesAreBaseClass.set(fullCurrentNodeClassName, false);
isCurrentNodeClassOfBase = false;
}
}
const isCurrentNodeClassOfBase = await baseClassChecker.isECClassOfBaseECClass(fullCurrentNodeClassName, baseECClass);

if (isCurrentNodeClassOfBase) {
groupedNodes.push(node);
Expand Down Expand Up @@ -111,7 +101,11 @@ async function sortByBaseClass(classes: ECClass[]): Promise<ECClass[]> {
}

/** @internal */
export async function createBaseClassGroupingHandlers(metadata: IMetadataProvider, nodes: ProcessedInstanceHierarchyNode[]): Promise<GroupingHandler[]> {
export async function createBaseClassGroupingHandlers(
metadata: IMetadataProvider,
nodes: ProcessedInstanceHierarchyNode[],
baseClassChecker: BaseClassChecker,
): Promise<GroupingHandler[]> {
const baseClassGroupingECClasses = await getBaseClassGroupingECClasses(metadata, nodes);
return baseClassGroupingECClasses.map((baseECClass) => async (allNodes) => createBaseClassGroupsForSingleBaseClass(metadata, allNodes, baseECClass));
return baseClassGroupingECClasses.map((baseECClass) => async (allNodes) => createBaseClassGroupsForSingleBaseClass(allNodes, baseECClass, baseClassChecker));
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { omit } from "@itwin/core-bentley";
import { IMetadataProvider } from "../../../ECMetadata";
import { ClassGroupingNodeKey, ProcessedInstanceHierarchyNode } from "../../../HierarchyNode";
import { getClass } from "../../Common";
import { getClass } from "../../GetClass";
import { GroupingHandlerResult, ProcessedInstancesGroupingHierarchyNode } from "../Grouping";
import { sortNodesByLabel } from "../Sorting";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
import { OmitOverUnion } from "../../../Utils";
import { IPrimitiveValueFormatter } from "../../../values/Formatting";
import { TypedPrimitiveValue } from "../../../values/Values";
import { getClass } from "../../Common";
import { BaseClassChecker } from "../../Common";
import { getClass } from "../../GetClass";
import { GroupingHandler, GroupingHandlerResult, ProcessedInstancesGroupingHierarchyNode } from "../Grouping";
import { sortNodesByLabel } from "../Sorting";

Expand Down Expand Up @@ -47,11 +48,11 @@ export type PreviousPropertiesGroupingInfo = Array<{ propertiesClassName: string

/** @internal */
export async function createPropertyGroups(
metadata: IMetadataProvider,
nodes: ProcessedInstanceHierarchyNode[],
handlerGroupingParams: PropertyGroupInfo,
valueFormatter: IPrimitiveValueFormatter,
localizedStrings: PropertiesGroupingLocalizedStrings,
baseClassChecker: BaseClassChecker,
): Promise<GroupingHandlerResult> {
const groupings: PropertyGroupingInformation = { ungrouped: [], grouped: new Map() };
for (const node of nodes) {
Expand All @@ -60,7 +61,7 @@ export async function createPropertyGroups(
groupings.ungrouped.push(node);
continue;
}
if (!(await shouldCreatePropertyGroup(metadata, handlerGroupingParams, byProperties, node.key.instanceKeys[0].className))) {
if (!(await shouldCreatePropertyGroup(handlerGroupingParams, byProperties, node.key.instanceKeys[0].className, baseClassChecker))) {
groupings.ungrouped.push(node);
continue;
}
Expand Down Expand Up @@ -262,10 +263,10 @@ function getRangesAsString(ranges?: HierarchyNodePropertyValueRange[]): string {
}

async function shouldCreatePropertyGroup(
metadata: IMetadataProvider,
handlerGroupingParams: PropertyGroupInfo,
nodePropertyGroupingParams: HierarchyNodePropertiesGroupingParams,
nodeFullClassName: string,
baseClassChecker: BaseClassChecker,
): Promise<boolean> {
if (
nodePropertyGroupingParams.propertiesClassName !== handlerGroupingParams.ecClass.fullName ||
Expand All @@ -283,11 +284,7 @@ async function shouldCreatePropertyGroup(
if (!doPreviousPropertiesMatch(handlerGroupingParams.previousPropertiesGroupingInfo, nodePropertyGroupingParams)) {
return false;
}
const nodeClass = await getClass(metadata, nodeFullClassName);
if (!(await nodeClass.is(handlerGroupingParams.ecClass))) {
return false;
}
return true;
return baseClassChecker.isECClassOfBaseECClass(nodeFullClassName, handlerGroupingParams.ecClass);
}

/** @internal */
Expand Down Expand Up @@ -339,9 +336,10 @@ export async function createPropertiesGroupingHandlers(
nodes: ProcessedInstanceHierarchyNode[],
valueFormatter: IPrimitiveValueFormatter,
localizedStrings: PropertiesGroupingLocalizedStrings,
baseClassChecker: BaseClassChecker,
): Promise<GroupingHandler[]> {
const propertiesGroupInfo = await getUniquePropertiesGroupInfo(metadata, nodes);
return propertiesGroupInfo.map(
(propertyInfo) => async (allNodes) => createPropertyGroups(metadata, allNodes, propertyInfo, valueFormatter, localizedStrings),
(propertyInfo) => async (allNodes) => createPropertyGroups(allNodes, propertyInfo, valueFormatter, localizedStrings, baseClassChecker),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { IMetadataProvider } from "../ECMetadata";
import { getClass } from "../internal/Common";
import { getClass } from "../internal/GetClass";
import { createConcatenatedValueJsonSelector, createRawPropertyValueSelector, TypedValueSelectClauseProps } from "./ecsql-snippets/ECSqlValueSelectorSnippets";

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
PropertyFilterRuleOperator,
PropertyFilterValue,
} from "../GenericInstanceFilter";
import { getClass } from "../internal/Common";
import { getClass } from "../internal/GetClass";
import { RelationshipPath } from "../Metadata";
import { Id64String, PrimitiveValue } from "../values/Values";
import { createRelationshipPathJoinClause, JoinRelationshipPath } from "./ecsql-snippets/ECSqlJoinSnippets";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { ECClass, ECNavigationProperty, ECRelationshipClass, IMetadataProvider } from "../../ECMetadata";
import { getClass } from "../../internal/Common";
import { getClass } from "../../internal/GetClass";
import { RelationshipPath, RelationshipPathStep } from "../../Metadata";
import { createRawPropertyValueSelector } from "./ECSqlValueSelectorSnippets";

Expand Down
4 changes: 2 additions & 2 deletions packages/hierarchy-builder/src/test/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ProcessedInstanceHierarchyNode,
} from "../hierarchy-builder/HierarchyNode";
import { HierarchyProviderLocalizedStrings } from "../hierarchy-builder/HierarchyProvider";
import * as common from "../hierarchy-builder/internal/Common";
import * as getClass from "../hierarchy-builder/internal/GetClass";
import { parseFullClassName } from "../hierarchy-builder/Metadata";
import { ECSqlQueryReader } from "../hierarchy-builder/queries/ECSqlCore";
import { InstanceKey } from "../hierarchy-builder/values/Values";
Expand Down Expand Up @@ -139,7 +139,7 @@ export interface ClassStubs {
stub: sinon.SinonStub<[metadata: IMetadataProvider, fullClassName: string], Promise<ECClass>>;
}
export function createClassStubs(schemas: IMetadataProvider): ClassStubs {
const stub = sinon.stub(common, "getClass");
const stub = sinon.stub(getClass, "getClass");
const createFullClassNameMatcher = (props: { schemaName: string; className: string }) =>
sinon.match((candidate: string) => {
const { schemaName, className } = parseFullClassName(candidate);
Expand Down
Loading

0 comments on commit edd180e

Please sign in to comment.