Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e772f24
init commit
CalebGerman Jan 17, 2025
4fb6e47
Merge remote-tracking branch 'origin/master' into cgerman/secret-fron…
CalebGerman Feb 10, 2025
ac08bf7
WIP working on RPC
CalebGerman Feb 10, 2025
60f371f
Frontend to backend comms
CalebGerman Feb 11, 2025
8ed3041
Changeset downloaded
CalebGerman Feb 17, 2025
f59683a
Got Affans code working
CalebGerman Feb 17, 2025
22e2410
Frontend has change elements from backend
CalebGerman Feb 20, 2025
7ee0ffd
pushing new changed elements into manager
CalebGerman Feb 25, 2025
716af60
Working all the way
CalebGerman Mar 19, 2025
42bf6b8
Updated group helper
CalebGerman Mar 19, 2025
74b777d
WIP
CalebGerman Mar 19, 2025
a61ca9f
WIP
CalebGerman Mar 19, 2025
a8fb1f3
WIP direct comparison result
CalebGerman Mar 21, 2025
2ce1529
Added optimizations
CalebGerman Mar 24, 2025
8a8f434
Adding timings
CalebGerman Mar 24, 2025
1df5903
Merge remote-tracking branch 'origin/master' into cgerman/secret-fron…
CalebGerman Mar 24, 2025
e2bd4a3
Fixed merged files
CalebGerman Mar 24, 2025
3dece23
Added more logging for timing
CalebGerman Mar 24, 2025
ee65f18
Added experiment.md
CalebGerman Mar 25, 2025
0eda6d0
Updated experiment markdown
CalebGerman Mar 25, 2025
d7e3d7f
Updated table
CalebGerman Mar 25, 2025
a5257eb
updated table
CalebGerman Mar 25, 2025
f3ac4d3
Got new results
CalebGerman Mar 26, 2025
488ad21
Initial pass at domain comparison processors. Allow options to contro…
diegopinate May 14, 2025
983db29
Move transformation to changed elements to frontend, and keep the bac…
diegopinate May 14, 2025
9a78eb4
Add driven property filter, ability to maintain driven element inform…
diegopinate May 19, 2025
59e47d1
Initial handling for selecting driven elements from widget
diegopinate May 23, 2025
0793515
Generalize Rpc interface naming and helpers. Track relationships that…
diegopinate Jul 14, 2025
7ca29e7
Merge branch 'master' of https://github.com/iTwin/changed-elements-re…
diegopinate Jul 14, 2025
fc7d708
Fix visualization after merge conflicts
diegopinate Jul 15, 2025
dc1d9c0
Remove hacked-in driven type logic in favor of using customizable cal…
diegopinate Jul 16, 2025
22b2166
Merge branch 'master' of https://github.com/iTwin/changed-elements-re…
diegopinate Jul 16, 2025
0654647
Revert env file, add environment variable to use direct comparison vi…
diegopinate Jul 16, 2025
7daf85f
Add changeset
diegopinate Jul 16, 2025
bf27bad
Add approved builds, fix build
diegopinate Jul 16, 2025
aba2bba
Update node and pnpm in workflows
diegopinate Jul 16, 2025
e110079
Fix linter and linting problems. Add interfaces for comparison metadata.
diegopinate Jul 16, 2025
8f48a13
Add object-storage-azure to backend deps
diegopinate Jul 16, 2025
d8f56bb
Fix comment
diegopinate Jul 16, 2025
32737f4
Comment clean-up.
diegopinate Jul 17, 2025
582a2a2
Remove unused comment
diegopinate Jul 17, 2025
e877de2
Add docs to ChangedECInstanceCache
diegopinate Jul 17, 2025
181fd51
Move checks above starting logic
diegopinate Jul 17, 2025
097837f
Remove useless try block
diegopinate Jul 17, 2025
2d0e1ab
Add appropriate null checks and remove comment
diegopinate Jul 17, 2025
767831b
Add void
diegopinate Jul 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/wise-readers-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@itwin/changed-elements-react": minor
---

_Frontend Enhancements:_

1. Provide consumers a way to inject their own changes and skip using the changed elements service altogether
2. Provide colorization overrides for any special customization logic
3. Provide a callback when changed instances are selected in the UI

_Backend Enhancements:_
1. Initial ChangesRpcInterface and ChangesRpcImpl which aim to allow using the Partial EC Change Unifier in a simplified way
2. The Rpc interface allows the app to provide relationships that they care about and marks any related changed ec instance with what relationships were affected that may drive the element for changes

See VersionCompare initialization options (`changesProvider`, `colorOverrideProvider` and `onInstancesSelected`) for more information.
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"root": true,
"extends": [
"eslint:recommended",
"prettier",
"plugin:@typescript-eslint/recommended-type-checked"
],
"parser": "@typescript-eslint/parser",
Expand All @@ -16,6 +17,7 @@
"no-alert": "warn",
"no-empty": [ "warn", { "allowEmptyCatch": true } ],
"no-eval": "error",
"no-console": "off",
"@typescript-eslint/comma-dangle": [
"warn",
{
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ jobs:
with:
fetch-depth: 0 # Fetch all history for all branches and tags

- name: Install pnpm
- name: Install pnpm@10.12.4
uses: pnpm/action-setup@v2
with:
version: 8
version: 10.12.4
run_install: false

- name: Use Node.js 20
- name: Use Node.js 20.16.0
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 20.16.0
cache: "pnpm"

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependabot-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:

# Install pnpm
- name: Install pnpm
run: npm install -g pnpm
run: npm install -g pnpm@10.12.4

# Install dependencies
- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
version: 10.12.4
run_install: false

- name: Use Node.js 20
Expand Down
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"singleQuote": false,
"trailingComma": "all",
"printWidth": 150,
"tabWidth": 2,
"useTabs": false
}
32 changes: 32 additions & 0 deletions experiment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Changed Elements React Experiment - Direct Comparison Workflow

### HYPOTHESIS:
If we use changeset group processing without processing the changed elements, then the result of the direct processing will be produced faster and will resemble GitHub's diff functionality by displaying a flat list of changes.

### REASON FOR EXPERIMENT
Changed elements undergo extensive processing to ensure a presentation-based summary of changes in an iModel. We aim to understand how a feature like Version Comparison would operate using raw changeset group results instead of presentation ruleset-based property path traversal. We expect faster loading times for Direct Comparison due to reduced processing, while still providing valuable output to the user.

### EXPERIMENT
We conducted multiple experiments to confirm our hypothesis. The steps were:

1. Run the Version Compare V2 workflow (Post/Get/Display) for an iModel with an unprocessed job range. Record the time until the user can interact with the job-related information on the UI.
2. Run the experimental Direct Comparison on the same unprocessed job version. Record the time until the user can interact with the job-related information on the UI.

#### Results Table
We tested across three different iModels of varying sizes in the DEV region to draw better conclusions.

| Itwin | IModel | Number Of Changeset Processed (V2 / Direct Comparison) | V2 Processing Time till interaction in UI (ms) | V2 Number of Changed Elements Found | Direct Comparison Processing Time till interaction in UI (ms) | Direct Comparison Number of Changed Elements Found | % diff between V2 and Direct Processing |
| -- | -- | -- | -- | -- | -- | -- | -- |
| 1036c64d-7fbe-47fd-b03c-4ed7ad7fc829 | c87854bc-1197-4ed9-8d3d-ad9cb5fd1347 | 12 | 22133 | 5342 | 6536 | 28039 | 108.807% |
| 1036c64d-7fbe-47fd-b03c-4ed7ad7fc829 | e657e0d6-fad1-4971-9c22-459bd400534b | 524 | 185067 | 109474 | 71375 | 314907 | 88.6688% |
| 1036c64d-7fbe-47fd-b03c-4ed7ad7fc829 | b8571aeb-dc0b-405f-bf6b-42401af40dd1 | 23 | 109007 | 128803 | 26017 | 22348 | 122.926% |

### RESULTS SUMMARY
The most salient findings of our testing:

1. On iModels with a vast amount of data to process, Direct Comparison is on average 106.8 % faster than V2 Comparison.
2. The UI has more elements to process with the Direct Comparison workflow than with the V2 workflow.
3. The larger the IModel/changeset range. The v2 processing is faster due to multiple agents used for processing.

### CONCLUSION
This experiment proved that the Direct Comparison workflow is viable and may be preferable in some situations for larger iModels due to its processing speed. Direct Comparison may be an efficacious solution to long waiting times if the user does not require the full information provided by property traversal.
16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
"test:components": "npm test --prefix packages/changed-elements-react",
"cover": "run-p --silent cover:*",
"cover:components": "npm run test:cover --prefix packages/changed-elements-react",
"lint": "eslint '**/*.{ts,tsx}'",
"lint": "eslint ./packages/**/src/**/*.{ts,tsx}",
"typecheck": "run-p --silent typecheck:*",
"typecheck:components": "npm run typecheck --prefix packages/changed-elements-react",
"typecheck:backend": "npm run typecheck --prefix packages/test-app-backend",
"typecheck:frontend": "npm run typecheck --prefix packages/test-app-frontend",
"check": "changeset status"
},
"engines": {
"pnpm": ">=8",
"pnpm": ">=10",
"npm": "<0",
"node": ">=20"
},
Expand All @@ -43,6 +43,16 @@
"axios@<1.8.2": ">=1.8.2",
"dompurify@<3.2.4": ">=3.2.4",
"esbuild@<=0.24.2": ">=0.25.0"
}
},
"onlyBuiltDependencies": [
"@bentley/imodeljs-native",
"@parcel/watcher",
"@swc/core",
"esbuild",
"protobufjs"
]
},
"devDependencies": {
"eslint-config-prettier": "^10.1.5"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"searchResults": "Search Results",
"removed": "Removed",
"modified": "Modified",
"driven": "Driven by another change",
"loading": "Loading...",
"versions": "Versions",
"version": "Version",
Expand Down Expand Up @@ -153,8 +154,10 @@
"hiddenProperty": "Hidden Properties",
"placement": "Placement",
"indirect": "Indirect",
"driven": "Driven",
"modifiedIndirectly": "Children modified",
"modelHasChanges": "Model has changed elements",
"modelHasDrivenChanges": "Model was changed indirectly",
"childrenModified": "Child elements were modified",
"childrenChanges": "Children changes",
"childrenAdded": "Children were added",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export function useComparisonJobs(args: UseComparisonJobsArgs): UseComparisonJob
job,
watchJob: async function* (pollingIntervalMs: number, signal?: AbortSignal) {
signal?.throwIfAborted();

while (job.status === "Queued" || job.status === "Started") {
await new Promise((resolve) => setTimeout(resolve, pollingIntervalMs));
const comparisonJob = await getComparisonJob({
Expand Down
102 changes: 102 additions & 0 deletions packages/changed-elements-react/src/api/ChangedECInstanceCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { Id64String } from "@itwin/core-bentley";
import { ChangedECInstance } from "./VersionCompare.js";
import { ChangedElementEntry } from "./ChangedElementEntryCache.js";

/**
* Used to maintain the ChangedECInstances when using a custom changesProvider
* Useful to correlate the ChangedElementEntry with the ChangedECInstance
*/
export class ChangedECInstanceCache {
private readonly _cache: Map<Id64String, ChangedECInstance>;

constructor() {
this._cache = new Map<Id64String, ChangedECInstance>();
}

/**
* Creates a key for the ChangedECInstance based on its Id and Class Id
* @param instance ChangedECInstance to create a key for
* @returns Key string for the ChangedECInstance in the cache, formatted as "instanceId:classId"
*/
private _getKey(instance: ChangedECInstance): string {
return `${instance.ECInstanceId}:${instance.ECClassId}`;
}

/**
* Initializes the cache with the given ChangedECInstances
* The cache will be cleared before adding the instances
* @param instances
*/
public initialize(instances: ChangedECInstance[]): void {
this._cache.clear();
for (const instance of instances) {
this._cache.set(this._getKey(instance), instance);
}
}

/**
* Add instance to the cache
* @param instance ChangedECInstance to add to the cache
*/
public add(instance: ChangedECInstance): void {
this._cache.set(this._getKey(instance), instance);
}

/**
* Gets the ChangedECInstance from the cache based on the instanceId and classId
* @param instanceId Id of the instance to get
* @param classId Class Id of the instance to get
* @returns ChangedECInstance if found, undefined otherwise
*/
public get(instanceId: Id64String, classId: Id64String): ChangedECInstance | undefined {
const key = `${instanceId}:${classId}`;
return this._cache.get(key);
}

/**
* Returns whether the cache contains the ChangedECInstance with the given instanceId and classId
* @param instanceId Id of the instance to check
* @param classId Class Id of the instance to check
* @returns true if the cache contains the instance, false otherwise
*/
public has(instanceId: Id64String, classId: Id64String): boolean {
const key = `${instanceId}:${classId}`;
return this._cache.has(key);
}

/**
* Similar to get, but uses the ChangedElementEntry to find the instance
* @param entry ChangedElementEntry to use for finding the instance
* @returns ChangedECInstance if found, undefined otherwise
*/
public getFromEntry(entry: ChangedElementEntry): ChangedECInstance | undefined {
return this.get(entry.id, entry.classId);
}

/**
* Returns all ChangedECInstances that are present in the cache that match the entries
* @param entries
* @returns
*/
public mapFromEntries(entries: ChangedElementEntry[]): ChangedECInstance[] {
const instances: ChangedECInstance[] = [];
for (const entry of entries) {
const instance = this.get(entry.id, entry.classId);
if (instance) {
instances.push(instance);
}
}
return instances;
}

/**
* Clears the cache of all ChangedECInstances
*/
public clear(): void {
this._cache.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -604,20 +604,16 @@ export class ChangedElementEntryCache {
.map((elem: ChangedElementEntry) => elem.id);

// Find top parents
const numTopParentQueries =
(currentEntryIds.length + targetEntryIds.length) /
this._findTopParentChunkSize +
1;
this._setCurrentLoadingMessage("msg_findingParents", numTopParentQueries);
this._progressCoordinator?.updateProgress(VersionCompareProgressStage.FindParents);
const currentTopParents = await this._findTopParents(
const currentTopParents = !this._manager.skipParentChildRelationships ? await this._findTopParents(
this._currentIModel,
currentEntryIds,
);
const targetTopParents = await this._findTopParents(
) : currentEntryIds;

const targetTopParents = !this._manager.skipParentChildRelationships ? await this._findTopParents(
this._targetIModel,
targetEntryIds,
);
): targetEntryIds;

// Find which parents require querying and which ones are available in entries
const unchangedCurrentTopParents = [];
Expand Down Expand Up @@ -722,7 +718,7 @@ export class ChangedElementEntryCache {

this._progressCoordinator?.updateProgress(VersionCompareProgressStage.FindChildren, 0);
// Load child elements of the root nodes if we are not using fast parent loading
if (this._childrenCache && !VersionCompare.manager?.wantFastParentLoad) {
if (this._childrenCache && !VersionCompare.manager?.wantFastParentLoad && !this._manager.skipParentChildRelationships) {
// Set update function for UI updates
this._childrenCache.updateFunction = (percent: number) => {
this._progressCoordinator?.addProgress(VersionCompareProgressStage.FindChildren, percent);
Expand Down Expand Up @@ -757,7 +753,8 @@ export class ChangedElementEntryCache {
this._overrideEntriesInCache(finalEntries);
// Go through all our entries and use the top parent information
// to create the children arrays of top parents
this._findChildrenOfTopParents();
if (!this._manager.skipParentChildRelationships)
this._findChildrenOfTopParents();

// Create the data provider and load the changed model nodes
if (this._uiDataProvider === undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IModelConnection } from "@itwin/core-frontend";
import { ChangedElementDataCache } from "./ChangedElementDataCache.js";
import { ChangeElementType, type ChangedElement, type ChangedElementEntry } from "./ChangedElementEntryCache.js";
import type { ChangedElementQueryData } from "./ElementQueries.js";
import { VersionCompare } from "./VersionCompare.js";

interface ParentChildData {
directChildren: ChangedElementQueryData[];
Expand Down Expand Up @@ -115,7 +116,7 @@ export class ChangedElementsChildrenCache extends ChangedElementDataCache {
map: Map<string, ChangedElementQueryData[]>,
elementIds: string[],
) => {
if (elementIds.length === 0) {
if (elementIds.length === 0 || VersionCompare.manager?.skipParentChildRelationships) {
return;
}

Expand Down
Loading
Loading