Skip to content

Commit f6b3a56

Browse files
authored
Tree widget: Increase performance consistency when creating filtering paths (#1130)
* Update filtering to reduce main thread blockage * Add changeset * Adjust implementation based on comments, and fix cache entry modification issue
1 parent 3f339ca commit f6b3a56

File tree

3 files changed

+85
-65
lines changed

3 files changed

+85
-65
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Increase performance consistency when creating filtering paths from target items.",
4+
"packageName": "@itwin/tree-widget-react",
5+
"email": "100586436+JonasDov@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/itwin/tree-widget/src/components/trees/models-tree/ModelsTreeDefinition.ts

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import type {
3636
GroupingHierarchyNode,
3737
HierarchyDefinition,
3838
HierarchyLevelDefinition,
39-
HierarchyNodeIdentifiersPath,
4039
HierarchyNodesDefinition,
4140
LimitingECSqlQueryExecutor,
4241
NodesQueryClauseFactory,
@@ -589,30 +588,6 @@ export class ModelsTreeDefinition implements HierarchyDefinition {
589588
}
590589
}
591590

592-
function createSubjectInstanceKeysPath(subjectId: Id64String, idsCache: ModelsTreeIdsCache): Observable<HierarchyNodeIdentifiersPath> {
593-
return from(idsCache.getSubjectAncestorsPath(subjectId)).pipe(map((idsPath) => idsPath.map((id) => ({ className: "BisCore.Subject", id }))));
594-
}
595-
596-
function createModelInstanceKeyPaths(modelId: Id64String, idsCache: ModelsTreeIdsCache): Observable<HierarchyNodeIdentifiersPath> {
597-
return from(idsCache.getModelSubjects(modelId)).pipe(
598-
mergeAll(),
599-
mergeMap((modelSubjectId) =>
600-
createSubjectInstanceKeysPath(modelSubjectId, idsCache).pipe(
601-
map((subjectPath) => [...subjectPath, { className: "BisCore.GeometricModel3d", id: modelId }]),
602-
),
603-
),
604-
);
605-
}
606-
607-
function createCategoryInstanceKeyPaths(categoryId: Id64String, idsCache: ModelsTreeIdsCache): Observable<HierarchyNodeIdentifiersPath> {
608-
return from(idsCache.getCategoryModels(categoryId)).pipe(
609-
mergeAll(),
610-
mergeMap((categoryModelId) =>
611-
createModelInstanceKeyPaths(categoryModelId, idsCache).pipe(map((modelPath) => [...modelPath, { className: "BisCore.SpatialCategory", id: categoryId }])),
612-
),
613-
);
614-
}
615-
616591
function createGeometricElementInstanceKeyPaths(
617592
imodelAccess: ECClassHierarchyInspector & LimitingECSqlQueryExecutor,
618593
idsCache: ModelsTreeIdsCache,
@@ -690,10 +665,13 @@ function createGeometricElementInstanceKeyPaths(
690665
releaseMainThreadOnItemsCount(300),
691666
map((row) => parseQueryRow(row, groupInfos, separator, hierarchyConfig.elementClassSpecification)),
692667
mergeMap(({ modelId, elementHierarchyPath, groupingNode }) =>
693-
createModelInstanceKeyPaths(modelId, idsCache).pipe(
668+
from(idsCache.createModelInstanceKeyPaths(modelId)).pipe(
669+
mergeAll(),
694670
map((modelPath) => {
695-
modelPath.pop(); // model is already included in the element hierarchy path
696-
const path = [...modelPath, ...elementHierarchyPath];
671+
// We dont want to modify the original path, we create a copy that we can modify
672+
const newModelPath = [...modelPath];
673+
newModelPath.pop(); // model is already included in the element hierarchy path
674+
const path = [...newModelPath, ...elementHierarchyPath];
697675
if (!groupingNode) {
698676
return path;
699677
}
@@ -796,11 +774,12 @@ async function createInstanceKeyPathsFromTargetItems({
796774
const elementsLength = ids.elements.length;
797775
return collect(
798776
merge(
799-
from(ids.subjects).pipe(mergeMap((id) => createSubjectInstanceKeysPath(id, idsCache))),
800-
from(ids.models).pipe(mergeMap((id) => createModelInstanceKeyPaths(id, idsCache))),
801-
from(ids.categories).pipe(mergeMap((id) => createCategoryInstanceKeyPaths(id, idsCache))),
777+
from(ids.subjects).pipe(mergeMap((id) => from(idsCache.createSubjectInstanceKeysPath(id)))),
778+
from(ids.models).pipe(mergeMap((id) => from(idsCache.createModelInstanceKeyPaths(id)).pipe(mergeAll()))),
779+
from(ids.categories).pipe(mergeMap((id) => from(idsCache.createCategoryInstanceKeyPaths(id)).pipe(mergeAll()))),
802780
from(ids.elements).pipe(
803781
bufferCount(Math.ceil(elementsLength / Math.ceil(elementsLength / 5000))),
782+
releaseMainThreadOnItemsCount(1),
804783
mergeMap((block) => createGeometricElementInstanceKeyPaths(imodelAccess, idsCache, hierarchyConfig, block), 10),
805784
),
806785
),

packages/itwin/tree-widget/src/components/trees/models-tree/internal/ModelsTreeIdsCache.ts

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { bufferTime, filter, firstValueFrom, mergeAll, mergeMap, ReplaySubject,
88
import { assert } from "@itwin/core-bentley";
99
import { pushToMap } from "../../common/Utils";
1010

11+
import type { InstanceKey } from "@itwin/presentation-shared";
1112
import type { ModelsTreeDefinition } from "../ModelsTreeDefinition";
1213
import type { Id64Array, Id64Set, Id64String } from "@itwin/core-bentley";
13-
import type { LimitingECSqlQueryExecutor } from "@itwin/presentation-hierarchies";
14+
import type { HierarchyNodeIdentifiersPath, LimitingECSqlQueryExecutor } from "@itwin/presentation-hierarchies";
1415

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

3640
constructor(
3741
private _queryExecutor: LimitingECSqlQueryExecutor,
3842
private _hierarchyConfig: ModelsTreeHierarchyConfiguration,
3943
) {
4044
this._categoryElementCounts = new ModelCategoryElementsCountCache(async (input) => this.queryCategoryElementCounts(input));
45+
this._modelKeyPaths = new Map();
46+
this._subjectKeyPaths = new Map();
47+
this._categoryKeyPaths = new Map();
4148
}
4249

4350
public [Symbol.dispose]() {
@@ -215,22 +222,25 @@ export class ModelsTreeIdsCache {
215222
return modelIds;
216223
}
217224

218-
/**
219-
* Returns a list of Subject ancestor ECInstanceIds from root to target Subject as displayed in the
220-
* hierarchy - taking into account `hideInHierarchy` flag.
221-
*/
222-
public async getSubjectAncestorsPath(targetSubjectId: Id64String): Promise<Id64Array> {
223-
const subjectInfos = await this.getSubjectInfos();
224-
const result = new Array<Id64String>();
225-
let currParentId: Id64String | undefined = targetSubjectId;
226-
while (currParentId) {
227-
const parentInfo = subjectInfos.get(currParentId);
228-
if (!parentInfo?.hideInHierarchy) {
229-
result.push(currParentId);
230-
}
231-
currParentId = parentInfo?.parentSubject;
225+
public async createSubjectInstanceKeysPath(targetSubjectId: Id64String): Promise<HierarchyNodeIdentifiersPath> {
226+
let entry = this._subjectKeyPaths.get(targetSubjectId);
227+
if (!entry) {
228+
entry = (async () => {
229+
const subjectInfos = await this.getSubjectInfos();
230+
const result = new Array<InstanceKey>();
231+
let currParentId: Id64String | undefined = targetSubjectId;
232+
while (currParentId) {
233+
const parentInfo = subjectInfos.get(currParentId);
234+
if (!parentInfo?.hideInHierarchy) {
235+
result.push({ className: "BisCore.Subject", id: currParentId });
236+
}
237+
currParentId = parentInfo?.parentSubject;
238+
}
239+
return result.reverse();
240+
})();
241+
this._subjectKeyPaths.set(targetSubjectId, entry);
232242
}
233-
return result.reverse();
243+
return entry;
234244
}
235245

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

300-
public async getModelSubjects(modelId: Id64String): Promise<Id64Array> {
301-
const result = new Array<Id64String>();
302-
const subjectInfos = await this.getSubjectInfos();
303-
subjectInfos.forEach((subjectInfo, subjectId) => {
304-
if (subjectInfo.childModels.has(modelId)) {
305-
result.push(subjectId);
306-
}
307-
});
308-
return result;
310+
public async createModelInstanceKeyPaths(modelId: Id64String): Promise<HierarchyNodeIdentifiersPath[]> {
311+
let entry = this._modelKeyPaths.get(modelId);
312+
if (!entry) {
313+
entry = (async () => {
314+
const result = new Array<HierarchyNodeIdentifiersPath>();
315+
const subjectInfos = (await this.getSubjectInfos()).entries();
316+
for (const [modelSubjectId, subjectInfo] of subjectInfos) {
317+
if (subjectInfo.childModels.has(modelId)) {
318+
const subjectPath = await this.createSubjectInstanceKeysPath(modelSubjectId);
319+
result.push([...subjectPath, { className: "BisCore.GeometricModel3d", id: modelId }]);
320+
}
321+
}
322+
return result;
323+
})();
324+
325+
this._modelKeyPaths.set(modelId, entry);
326+
}
327+
return entry;
309328
}
310329

311330
private async queryCategoryElementCounts(
@@ -352,15 +371,30 @@ export class ModelsTreeIdsCache {
352371
return this._categoryElementCounts.getCategoryElementsCount(modelId, categoryId);
353372
}
354373

355-
public async getCategoryModels(categoryId: Id64String): Promise<Id64Array> {
356-
const result = new Set<Id64String>();
357-
const modelInfos = await this.getModelInfos();
358-
modelInfos?.forEach((modelInfo, modelId) => {
359-
if (modelInfo.categories.has(categoryId)) {
360-
result.add(modelId);
361-
}
362-
});
363-
return [...result];
374+
public async createCategoryInstanceKeyPaths(categoryId: Id64String): Promise<HierarchyNodeIdentifiersPath[]> {
375+
let entry = this._categoryKeyPaths.get(categoryId);
376+
if (!entry) {
377+
entry = (async () => {
378+
const result = new Set<Id64String>();
379+
const modelInfos = await this.getModelInfos();
380+
modelInfos?.forEach((modelInfo, modelId) => {
381+
if (modelInfo.categories.has(categoryId)) {
382+
result.add(modelId);
383+
}
384+
});
385+
386+
const categoryPaths = new Array<HierarchyNodeIdentifiersPath>();
387+
for (const categoryModelId of [...result]) {
388+
const modelPaths = await this.createModelInstanceKeyPaths(categoryModelId);
389+
for (const modelPath of modelPaths) {
390+
categoryPaths.push([...modelPath, { className: "BisCore.SpatialCategory", id: categoryId }]);
391+
}
392+
}
393+
return categoryPaths;
394+
})();
395+
this._categoryKeyPaths.set(categoryId, entry);
396+
}
397+
return entry;
364398
}
365399
}
366400

0 commit comments

Comments
 (0)