-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix slow hierarchy filtering with large number of filtered paths (#711)
* Add performance test * Remove FilteredChildrenPaths from query * Run extract-api and update extractions * Add changeset * Update changeset comment * Add filterPathsIdentifierPositions * Remove added test * Fix tests and cleanup * Run extract-api, update test name * Fix comments * Add back new lines * Update changeset * Change eCInstanceIdCondition * Adjust Filtering.test.ts * Adjust import order * Update packages/hierarchies/src/hierarchies/imodel/FilteringHierarchyDefinition.ts Co-authored-by: Grigas <[email protected]> * Adjust import order & change ECSQL_COLUMN_NAME_FilterECInstanceId to return id64 string * Fix comments * Update .changeset/tall-comics-compare.md Co-authored-by: Grigas <[email protected]> * Add watch to performance-tests, fix benchmark * Add tests * Fix filtering * Add unit test * Cleanup integration test `filters through instance nodes that are in multiple paths` * Update packages/hierarchies/src/hierarchies/imodel/FilteringHierarchyDefinition.ts Co-authored-by: Grigas <[email protected]> * Create a separate class for handling positions * Remove unnecessarily exported class * Add parentNode to parseNode * Run extract api * Fix comments * Update changeset --------- Co-authored-by: Grigas <[email protected]>
- Loading branch information
Showing
15 changed files
with
625 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
--- | ||
"@itwin/presentation-hierarchies": minor | ||
--- | ||
|
||
Increased the speed of hierarchy filtering with large number of filtered paths. | ||
|
||
| Amount of paths | Before the change | After the change | | ||
| --- | ---------- | --------- | | ||
| 500 | 960.18 ms | 233.65 ms | | ||
| 1k | 2.29 s | 336.81 ms | | ||
| 10k | 232.55 s | 2.17 s | | ||
| 50k | not tested | 13.45 s | | ||
|
||
In addition, changed `NodeParser` (return type of `HierarchyDefinition.parseNode`): | ||
|
||
- It now can return a promise, so instead of just `SourceInstanceHierarchyNode` it can now also return `Promise<SourceInstanceHierarchyNode>`. | ||
- Additionally, it now accepts an optional `parentNode` argument of `HierarchyDefinitionParentNode` type. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
apps/performance-tests/src/hierarchies/Filtering.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Bentley Systems, Incorporated. All rights reserved. | ||
* See LICENSE.md in the project root for license terms and full copyright notice. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import { expect } from "chai"; | ||
import { IModelDb, PhysicalElement, SnapshotDb } from "@itwin/core-backend"; | ||
import { Id64 } from "@itwin/core-bentley"; | ||
import { createNodesQueryClauseFactory, HierarchyFilteringPath, HierarchyNode } from "@itwin/presentation-hierarchies"; | ||
import { createBisInstanceLabelSelectClauseFactory, ECClassHierarchyInspector, ECSchemaProvider } from "@itwin/presentation-shared"; | ||
import { Datasets } from "../util/Datasets"; | ||
import { run } from "../util/TestUtilities"; | ||
import { ProviderOptions, StatelessHierarchyProvider } from "./StatelessHierarchyProvider"; | ||
|
||
describe("filtering", () => { | ||
const totalNumberOfFilteringPaths = 50000; | ||
const physicalElementsSmallestDecimalId = 20; | ||
|
||
run({ | ||
testName: `filters with ${totalNumberOfFilteringPaths} paths`, | ||
setup: (): ProviderOptions => { | ||
const { schemaName, itemsPerGroup, defaultClassName } = Datasets.CUSTOM_SCHEMA; | ||
|
||
const filtering = { | ||
paths: new Array<HierarchyFilteringPath>(), | ||
}; | ||
const parentIdsArr = new Array<number>(); | ||
for (let i = 1; i <= 100; ++i) { | ||
parentIdsArr.push(i + physicalElementsSmallestDecimalId); | ||
for (let j = (i - 1) * 500; j < i * 500; ++j) { | ||
filtering.paths.push([ | ||
{ className: `${schemaName}.${defaultClassName}_0`, id: `0x${physicalElementsSmallestDecimalId.toString(16)}` }, | ||
{ | ||
className: `${schemaName}.${defaultClassName}_${Math.floor(i / itemsPerGroup)}`, | ||
id: `0x${(i + physicalElementsSmallestDecimalId).toString(16)}`, | ||
}, | ||
{ | ||
className: `${schemaName}.${defaultClassName}_${Math.floor(j / itemsPerGroup)}`, | ||
id: `0x${(j + physicalElementsSmallestDecimalId).toString(16)}`, | ||
}, | ||
]); | ||
} | ||
} | ||
|
||
const iModel = SnapshotDb.openFile(Datasets.getIModelPath("50k flat elements")); | ||
const fullClassName = PhysicalElement.classFullName.replace(":", "."); | ||
const createHierarchyLevelDefinition = async (imodelAccess: ECSchemaProvider & ECClassHierarchyInspector, whereClause: (alias: string) => string) => { | ||
const query = createNodesQueryClauseFactory({ | ||
imodelAccess, | ||
instanceLabelSelectClauseFactory: createBisInstanceLabelSelectClauseFactory({ classHierarchyInspector: imodelAccess }), | ||
}); | ||
return [ | ||
{ | ||
fullClassName, | ||
query: { | ||
ecsql: ` | ||
SELECT ${await query.createSelectClause({ | ||
ecClassId: { selector: `this.ECClassId` }, | ||
ecInstanceId: { selector: `this.ECInstanceId` }, | ||
nodeLabel: { selector: `this.UserLabel` }, | ||
})} | ||
FROM ${fullClassName} AS this | ||
${whereClause("this")} | ||
`, | ||
}, | ||
}, | ||
]; | ||
}; | ||
return { | ||
iModel, | ||
rowLimit: "unbounded", | ||
getHierarchyFactory: (imodelAccess) => ({ | ||
async defineHierarchyLevel(props) { | ||
// A hierarchy with this structure is created: | ||
// | ||
// id:21 -> all other BisCore.PhysicalElement | ||
// / . | ||
// id:20 . | ||
// \ . | ||
// id:120 -> all other BisCore.PhysicalElement | ||
// | ||
// We need to split the hierarchy in 100 parts, because we are using 50000 paths and there is a limit of 500 filtering paths for a single parent. | ||
|
||
if (!props.parentNode) { | ||
return createHierarchyLevelDefinition(imodelAccess, (alias) => `WHERE ${alias}.ECInstanceId = ${physicalElementsSmallestDecimalId}`); | ||
} | ||
if ( | ||
props.parentNode && | ||
HierarchyNode.isInstancesNode(props.parentNode) && | ||
props.parentNode.key.instanceKeys.some(({ id }) => Id64.getLocalId(id) === physicalElementsSmallestDecimalId) | ||
) { | ||
return createHierarchyLevelDefinition(imodelAccess, (alias) => `WHERE ${alias}.ECInstanceId IN (${parentIdsArr.join(", ")})`); | ||
} | ||
|
||
if ( | ||
props.parentNode && | ||
HierarchyNode.isInstancesNode(props.parentNode) && | ||
props.parentNode.key.instanceKeys.some(({ id }) => parentIdsArr.includes(Id64.getLocalId(id))) | ||
) { | ||
return createHierarchyLevelDefinition( | ||
imodelAccess, | ||
(alias) => `WHERE ${alias}.ECInstanceId NOT IN (${physicalElementsSmallestDecimalId}, ${parentIdsArr.join(", ")})`, | ||
); | ||
} | ||
|
||
return []; | ||
}, | ||
}), | ||
filtering, | ||
}; | ||
}, | ||
cleanup: (props: { iModel: IModelDb }) => { | ||
props.iModel.close(); | ||
}, | ||
test: async (props) => { | ||
const provider = new StatelessHierarchyProvider(props); | ||
const nodeCount = await provider.loadHierarchy(); | ||
expect(nodeCount).to.eq(totalNumberOfFilteringPaths); | ||
}, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.