Skip to content

Commit

Permalink
Tree widget: Increase performance consistency when creating filtering…
Browse files Browse the repository at this point in the history
… paths (#1130)

* Update filtering to reduce main thread blockage

* Add changeset

* Adjust implementation based on comments, and fix cache entry modification issue
  • Loading branch information
JonasDov authored Dec 20, 2024
1 parent 3f339ca commit f6b3a56
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Increase performance consistency when creating filtering paths from target items.",
"packageName": "@itwin/tree-widget-react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import type {
GroupingHierarchyNode,
HierarchyDefinition,
HierarchyLevelDefinition,
HierarchyNodeIdentifiersPath,
HierarchyNodesDefinition,
LimitingECSqlQueryExecutor,
NodesQueryClauseFactory,
Expand Down Expand Up @@ -589,30 +588,6 @@ export class ModelsTreeDefinition implements HierarchyDefinition {
}
}

function createSubjectInstanceKeysPath(subjectId: Id64String, idsCache: ModelsTreeIdsCache): Observable<HierarchyNodeIdentifiersPath> {
return from(idsCache.getSubjectAncestorsPath(subjectId)).pipe(map((idsPath) => idsPath.map((id) => ({ className: "BisCore.Subject", id }))));
}

function createModelInstanceKeyPaths(modelId: Id64String, idsCache: ModelsTreeIdsCache): Observable<HierarchyNodeIdentifiersPath> {
return from(idsCache.getModelSubjects(modelId)).pipe(
mergeAll(),
mergeMap((modelSubjectId) =>
createSubjectInstanceKeysPath(modelSubjectId, idsCache).pipe(
map((subjectPath) => [...subjectPath, { className: "BisCore.GeometricModel3d", id: modelId }]),
),
),
);
}

function createCategoryInstanceKeyPaths(categoryId: Id64String, idsCache: ModelsTreeIdsCache): Observable<HierarchyNodeIdentifiersPath> {
return from(idsCache.getCategoryModels(categoryId)).pipe(
mergeAll(),
mergeMap((categoryModelId) =>
createModelInstanceKeyPaths(categoryModelId, idsCache).pipe(map((modelPath) => [...modelPath, { className: "BisCore.SpatialCategory", id: categoryId }])),
),
);
}

function createGeometricElementInstanceKeyPaths(
imodelAccess: ECClassHierarchyInspector & LimitingECSqlQueryExecutor,
idsCache: ModelsTreeIdsCache,
Expand Down Expand Up @@ -690,10 +665,13 @@ function createGeometricElementInstanceKeyPaths(
releaseMainThreadOnItemsCount(300),
map((row) => parseQueryRow(row, groupInfos, separator, hierarchyConfig.elementClassSpecification)),
mergeMap(({ modelId, elementHierarchyPath, groupingNode }) =>
createModelInstanceKeyPaths(modelId, idsCache).pipe(
from(idsCache.createModelInstanceKeyPaths(modelId)).pipe(
mergeAll(),
map((modelPath) => {
modelPath.pop(); // model is already included in the element hierarchy path
const path = [...modelPath, ...elementHierarchyPath];
// We dont want to modify the original path, we create a copy that we can modify
const newModelPath = [...modelPath];
newModelPath.pop(); // model is already included in the element hierarchy path
const path = [...newModelPath, ...elementHierarchyPath];
if (!groupingNode) {
return path;
}
Expand Down Expand Up @@ -796,11 +774,12 @@ async function createInstanceKeyPathsFromTargetItems({
const elementsLength = ids.elements.length;
return collect(
merge(
from(ids.subjects).pipe(mergeMap((id) => createSubjectInstanceKeysPath(id, idsCache))),
from(ids.models).pipe(mergeMap((id) => createModelInstanceKeyPaths(id, idsCache))),
from(ids.categories).pipe(mergeMap((id) => createCategoryInstanceKeyPaths(id, idsCache))),
from(ids.subjects).pipe(mergeMap((id) => from(idsCache.createSubjectInstanceKeysPath(id)))),
from(ids.models).pipe(mergeMap((id) => from(idsCache.createModelInstanceKeyPaths(id)).pipe(mergeAll()))),
from(ids.categories).pipe(mergeMap((id) => from(idsCache.createCategoryInstanceKeyPaths(id)).pipe(mergeAll()))),
from(ids.elements).pipe(
bufferCount(Math.ceil(elementsLength / Math.ceil(elementsLength / 5000))),
releaseMainThreadOnItemsCount(1),
mergeMap((block) => createGeometricElementInstanceKeyPaths(imodelAccess, idsCache, hierarchyConfig, block), 10),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { bufferTime, filter, firstValueFrom, mergeAll, mergeMap, ReplaySubject,
import { assert } from "@itwin/core-bentley";
import { pushToMap } from "../../common/Utils";

import type { InstanceKey } from "@itwin/presentation-shared";
import type { ModelsTreeDefinition } from "../ModelsTreeDefinition";
import type { Id64Array, Id64Set, Id64String } from "@itwin/core-bentley";
import type { LimitingECSqlQueryExecutor } from "@itwin/presentation-hierarchies";
import type { HierarchyNodeIdentifiersPath, LimitingECSqlQueryExecutor } from "@itwin/presentation-hierarchies";

interface SubjectInfo {
parentSubject: Id64String | undefined;
Expand All @@ -32,12 +33,18 @@ export class ModelsTreeIdsCache {
private _subjectInfos: Promise<Map<Id64String, SubjectInfo>> | undefined;
private _parentSubjectIds: Promise<Id64Array> | undefined; // the list should contain a subject id if its node should be shown as having children
private _modelInfos: Promise<Map<Id64String, ModelInfo>> | undefined;
private _modelKeyPaths: Map<Id64String, Promise<HierarchyNodeIdentifiersPath[]>>;
private _subjectKeyPaths: Map<Id64String, Promise<HierarchyNodeIdentifiersPath>>;
private _categoryKeyPaths: Map<Id64String, Promise<HierarchyNodeIdentifiersPath[]>>;

constructor(
private _queryExecutor: LimitingECSqlQueryExecutor,
private _hierarchyConfig: ModelsTreeHierarchyConfiguration,
) {
this._categoryElementCounts = new ModelCategoryElementsCountCache(async (input) => this.queryCategoryElementCounts(input));
this._modelKeyPaths = new Map();
this._subjectKeyPaths = new Map();
this._categoryKeyPaths = new Map();
}

public [Symbol.dispose]() {
Expand Down Expand Up @@ -215,22 +222,25 @@ export class ModelsTreeIdsCache {
return modelIds;
}

/**
* Returns a list of Subject ancestor ECInstanceIds from root to target Subject as displayed in the
* hierarchy - taking into account `hideInHierarchy` flag.
*/
public async getSubjectAncestorsPath(targetSubjectId: Id64String): Promise<Id64Array> {
const subjectInfos = await this.getSubjectInfos();
const result = new Array<Id64String>();
let currParentId: Id64String | undefined = targetSubjectId;
while (currParentId) {
const parentInfo = subjectInfos.get(currParentId);
if (!parentInfo?.hideInHierarchy) {
result.push(currParentId);
}
currParentId = parentInfo?.parentSubject;
public async createSubjectInstanceKeysPath(targetSubjectId: Id64String): Promise<HierarchyNodeIdentifiersPath> {
let entry = this._subjectKeyPaths.get(targetSubjectId);
if (!entry) {
entry = (async () => {
const subjectInfos = await this.getSubjectInfos();
const result = new Array<InstanceKey>();
let currParentId: Id64String | undefined = targetSubjectId;
while (currParentId) {
const parentInfo = subjectInfos.get(currParentId);
if (!parentInfo?.hideInHierarchy) {
result.push({ className: "BisCore.Subject", id: currParentId });
}
currParentId = parentInfo?.parentSubject;
}
return result.reverse();
})();
this._subjectKeyPaths.set(targetSubjectId, entry);
}
return result.reverse();
return entry;
}

private async *queryModelElementCounts() {
Expand Down Expand Up @@ -297,15 +307,24 @@ export class ModelsTreeIdsCache {
return modelInfos.get(modelId)?.elementCount ?? 0;
}

public async getModelSubjects(modelId: Id64String): Promise<Id64Array> {
const result = new Array<Id64String>();
const subjectInfos = await this.getSubjectInfos();
subjectInfos.forEach((subjectInfo, subjectId) => {
if (subjectInfo.childModels.has(modelId)) {
result.push(subjectId);
}
});
return result;
public async createModelInstanceKeyPaths(modelId: Id64String): Promise<HierarchyNodeIdentifiersPath[]> {
let entry = this._modelKeyPaths.get(modelId);
if (!entry) {
entry = (async () => {
const result = new Array<HierarchyNodeIdentifiersPath>();
const subjectInfos = (await this.getSubjectInfos()).entries();
for (const [modelSubjectId, subjectInfo] of subjectInfos) {
if (subjectInfo.childModels.has(modelId)) {
const subjectPath = await this.createSubjectInstanceKeysPath(modelSubjectId);
result.push([...subjectPath, { className: "BisCore.GeometricModel3d", id: modelId }]);
}
}
return result;
})();

this._modelKeyPaths.set(modelId, entry);
}
return entry;
}

private async queryCategoryElementCounts(
Expand Down Expand Up @@ -352,15 +371,30 @@ export class ModelsTreeIdsCache {
return this._categoryElementCounts.getCategoryElementsCount(modelId, categoryId);
}

public async getCategoryModels(categoryId: Id64String): Promise<Id64Array> {
const result = new Set<Id64String>();
const modelInfos = await this.getModelInfos();
modelInfos?.forEach((modelInfo, modelId) => {
if (modelInfo.categories.has(categoryId)) {
result.add(modelId);
}
});
return [...result];
public async createCategoryInstanceKeyPaths(categoryId: Id64String): Promise<HierarchyNodeIdentifiersPath[]> {
let entry = this._categoryKeyPaths.get(categoryId);
if (!entry) {
entry = (async () => {
const result = new Set<Id64String>();
const modelInfos = await this.getModelInfos();
modelInfos?.forEach((modelInfo, modelId) => {
if (modelInfo.categories.has(categoryId)) {
result.add(modelId);
}
});

const categoryPaths = new Array<HierarchyNodeIdentifiersPath>();
for (const categoryModelId of [...result]) {
const modelPaths = await this.createModelInstanceKeyPaths(categoryModelId);
for (const modelPath of modelPaths) {
categoryPaths.push([...modelPath, { className: "BisCore.SpatialCategory", id: categoryId }]);
}
}
return categoryPaths;
})();
this._categoryKeyPaths.set(categoryId, entry);
}
return entry;
}
}

Expand Down

0 comments on commit f6b3a56

Please sign in to comment.