Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation for openCheckpoint and do prefetching in IPC/desktop case #6552

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion common/api/core-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ export class CheckpointManager {
export interface CheckpointProps extends TokenArg {
readonly allowPreceding?: boolean;
readonly changeset: ChangesetIdWithIndex;
readonly doPrefetch?: boolean;
// (undocumented)
readonly expectV2?: boolean;
readonly iModelId: GuidString;
Expand Down Expand Up @@ -5716,7 +5717,6 @@ export interface V2CheckpointAccessProps {

// @internal
export class V2CheckpointManager {
// (undocumented)
static attach(checkpoint: CheckpointProps): Promise<{
dbName: string;
container: CloudSqlite.CloudContainer;
Expand Down
1 change: 1 addition & 0 deletions common/api/core-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6210,6 +6210,7 @@ export interface OpenBriefcaseProps extends IModelEncryptionProps, OpenDbKey {
// @beta
export interface OpenCheckpointArgs {
readonly changeset?: ChangesetIndexOrId;
readonly doPrefetch?: boolean;
readonly iModelId: GuidString;
// (undocumented)
readonly iTwinId: GuidString;
Expand Down
2 changes: 2 additions & 0 deletions common/api/core-frontend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ import { NormalMapParams } from '@itwin/core-common';
import { ObservableSet } from '@itwin/core-bentley';
import { OctEncodedNormal } from '@itwin/core-common';
import { OpenBriefcaseProps } from '@itwin/core-common';
import { OpenCheckpointArgs } from '@itwin/core-common';
import { OpenMode } from '@itwin/core-bentley';
import { OrbitGtBlobProps } from '@itwin/core-common';
import { OrbitGtDataManager } from '@itwin/core-orbitgt';
Expand Down Expand Up @@ -2204,6 +2205,7 @@ export class CheckpointConnection extends IModelConnection {
// (undocumented)
protected _isClosed?: boolean;
get iTwinId(): GuidString;
static openFromIpc(args: OpenCheckpointArgs): Promise<CheckpointConnection>;
static openRemote(iTwinId: GuidString, iModelId: GuidString, version?: IModelVersion): Promise<CheckpointConnection>;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-backend",
"comment": "add doPrefetch to allow optional prefetching",
"type": "none"
}
],
"packageName": "@itwin/core-backend"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-common",
"comment": "",
"type": "none"
}
],
"packageName": "@itwin/core-common"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-frontend",
"comment": "add openFromIpc to allow flexibility specifying the changeset version, and optinal prefetching",
"type": "none"
}
],
"packageName": "@itwin/core-frontend"
}
27 changes: 23 additions & 4 deletions core/backend/src/CheckpointManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export interface CheckpointProps extends TokenArg {

/** The number of seconds before the current token expires to attempt to reacquire a new token. Default is 1 hour. */
readonly reattachSafetySeconds?: number;

/** If present, indicates whether to initiate a prefetch operation when the checkpoint is attached. */
readonly doPrefetch?: boolean;
}

/** Return value from [[ProgressFunction]].
Expand Down Expand Up @@ -184,6 +187,21 @@ export class V2CheckpointManager {
return container;
}

/**
* Attaches to a checkpoint and optionally initiates a prefetch of the database blocks.
* Prefetching aims to improve subsequent access times by preloading data, but it should be used strategically to balance initial load time against overall performance improvement.
* Prefetching is enabled when 'Checkpoint/prefetch' is set to true.
* The decision is based on total number of blocks in db and maximum number of blocks(2GB). This limit aims to manage resource usage and ensure that prefetching is beneficial.
* The restriction is meant to mitigate the 'noisy neighbor' problem in cloud environment. However, it does not apply in desktop scenarios,
* where larger prefetches might be ok without impacting resources.
* Prefetching should be considered when:
* - A significant portion of the database will be accessed, making upfront loading more efficient than multiple individual fetches.
* - The application operates in a read-intensive environment where initial longer load time is acceptable for the benefit of faster subsequent access.
* - The total number of blocks does not exceed the maximum block number in cloud environment.
* - (We may change this in the future depends on perf test results)
* @param checkpoint The checkpoint properties
* @returns A promise that resolves with the database name and container.
*/
public static async attach(checkpoint: CheckpointProps): Promise<{ dbName: string, container: CloudSqlite.CloudContainer }> {
let v2props: V2CheckpointAccessProps | undefined;
try {
Expand All @@ -201,13 +219,15 @@ export class V2CheckpointManager {
container.connect(this.cloudCache);
container.checkForChanges();
const dbStats = container.queryDatabase(dbName);
if (IModelHost.appWorkspace.settings.getBoolean("Checkpoints/prefetch", false)) {
const doPrefetch = checkpoint.doPrefetch ?? IModelHost.appWorkspace.settings.getBoolean("Checkpoints/prefetch", false);
if (doPrefetch) {
const getPrefetchConfig = (name: string, defaultVal: number) => IModelHost.appWorkspace.settings.getNumber(`Checkpoints/prefetch/${name}`, defaultVal);
const minRequests = getPrefetchConfig("minRequests", 3);
const maxRequests = getPrefetchConfig("maxRequests", 6);
const timeout = getPrefetchConfig("timeout", 100);
const maxBlocks = getPrefetchConfig("maxBlocks", 500); // default size of 2GB. Assumes a checkpoint block size of 4MB.
if (dbStats?.totalBlocks !== undefined && dbStats.totalBlocks <= maxBlocks && dbStats.nPrefetch === 0) {
// !(this.cloudCache.isDaemon) => desktop/IPC, don't need the maxBlocks restriction.
if (dbStats?.totalBlocks !== undefined && (!(this.cloudCache.isDaemon) || dbStats.totalBlocks <= maxBlocks) && dbStats.nPrefetch === 0) {
const logPrefetch = async (prefetch: CloudSqlite.CloudPrefetch) => {
const stopwatch = new StopWatch(`[${container.containerId}/${dbName}]`, true);
Logger.logInfo(loggerCategory, `Starting prefetch of ${stopwatch.description}`, { minRequests, maxRequests, timeout });
Expand Down Expand Up @@ -461,8 +481,7 @@ export class CheckpointManager {
const changeset = args.changeset ?? await IModelHost.hubAccess.getLatestChangeset({ ...args, accessToken: await IModelHost.getAccessToken() });

return {
iModelId: args.iModelId,
iTwinId: args.iTwinId,
...args,
changeset: {
index: changeset.index,
id: changeset.id ?? (await IModelHost.hubAccess.queryChangeset({ ...args, changeset, accessToken: await IModelHost.getAccessToken() })).id,
Expand Down
13 changes: 11 additions & 2 deletions core/backend/src/IModelDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3048,6 +3048,11 @@ export class SnapshotDb extends IModelDb {
return db;
}

/**
* Attach to a checkpoint and open it. This involves fetching the database name and container from V2CheckpointManager, and opening the file.
* @param checkpoint The checkpoint properties
* @returns A promise that resolves with an instance of SnapshotDb.
*/
private static async attachAndOpenCheckpoint(checkpoint: CheckpointProps): Promise<SnapshotDb> {
const { dbName, container } = await V2CheckpointManager.attach(checkpoint);
const key = CheckpointManager.getKey(checkpoint);
Expand Down Expand Up @@ -3075,11 +3080,15 @@ export class SnapshotDb extends IModelDb {
}

/**
* Open a Checkpoint directly from its cloud container.
* Open a Checkpoint directly from its cloud container. Data for the checkpoint is loaded from the container on demand as it is accessed.
* This method starts a "prefetch" (see [[CloudSqlite.startCloudPrefetch]]) operation to asynchronously download all blocks in the checkpoint by
* default. Prefetching can improve performance when a large portion of the database will be needed, or to allow offline usage.
* To disable prefetching, set `args.doPrefetch = false`;
* @beta
*/
public static async openCheckpoint(args: OpenCheckpointArgs): Promise<SnapshotDb> {
return this.attachAndOpenCheckpoint(await CheckpointManager.toCheckpointProps(args));
// set doPrefetch = true by default
return this.attachAndOpenCheckpoint(await CheckpointManager.toCheckpointProps({ doPrefetch: true, ...args }));
}

/** Used to refresh the container sasToken using the current user's accessToken.
Expand Down
3 changes: 3 additions & 0 deletions core/common/src/BriefcaseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ export interface OpenCheckpointArgs {

/** changeset for the checkpoint. If undefined, attempt to open the checkpoint for the latest changeset. */
readonly changeset?: ChangesetIndexOrId;

/** if present, determines whether to initiate a prefetch when opening a checkpoint */
readonly doPrefetch?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion core/common/src/IpcAppProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export interface IpcAppFunctions {

/** see BriefcaseConnection.openFile */
openBriefcase: (args: OpenBriefcaseProps) => Promise<IModelConnectionProps>;
/** see BriefcaseConnection.openStandalone */
/** see SnapshotDb.openCheckpoint */
openCheckpoint: (args: OpenCheckpointArgs) => Promise<IModelConnectionProps>;
/** see BriefcaseConnection.openStandalone */
openStandalone: (filePath: string, openMode: OpenMode, opts?: StandaloneOpenOptions) => Promise<IModelConnectionProps>;
Expand Down
31 changes: 20 additions & 11 deletions core/frontend/src/CheckpointConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { BentleyError, BentleyStatus, GuidString, Logger } from "@itwin/core-bentley";
import {
IModelConnectionProps, IModelError, IModelReadRpcInterface, IModelRpcOpenProps, IModelVersion, RpcManager, RpcNotFoundResponse, RpcOperation,
IModelConnectionProps, IModelError, IModelReadRpcInterface, IModelRpcOpenProps, IModelVersion, OpenCheckpointArgs, RpcManager, RpcNotFoundResponse, RpcOperation,
RpcRequest, RpcRequestEvent,
} from "@itwin/core-common";
import { FrontendLoggerCategory } from "./common/FrontendLoggerCategory";
Expand Down Expand Up @@ -54,18 +54,27 @@ export class CheckpointConnection extends IModelConnection {
const accessToken = await IModelApp.getAccessToken();
const changeset = await IModelApp.hubAccess.getChangesetFromVersion({ accessToken, iModelId, version });

let connection: CheckpointConnection;
const iModelProps = { iTwinId, iModelId, changeset };
if (IpcApp.isValid) {
connection = new this(await IpcApp.appFunctionIpc.openCheckpoint(iModelProps), true);
} else {
const routingContext = IModelRoutingContext.current || IModelRoutingContext.default;
connection = new this(await this.callOpen(iModelProps, routingContext), false);
RpcManager.setIModel(connection);
connection.routingContext = routingContext;
RpcRequest.notFoundHandlers.addListener(connection._reopenConnectionHandler);
}
if (IpcApp.isValid) // when called from Ipc, use the Ipc implementation
return this.openFromIpc(iModelProps);

const routingContext = IModelRoutingContext.current || IModelRoutingContext.default;
const connection = new this(await this.callOpen(iModelProps, routingContext), false);
RpcManager.setIModel(connection);
connection.routingContext = routingContext;
RpcRequest.notFoundHandlers.addListener(connection._reopenConnectionHandler);

IModelConnection.onOpen.raiseEvent(connection);
return connection;
}

/**
* Open a readonly IModelConnection to a Checkpoint of an iModel from Ipc.
* @note this function is equivalent to [[openRemote]] but allows more flexibility specifying the changeset version,
* and also optionally starting a prefetch operation.
*/
public static async openFromIpc(args: OpenCheckpointArgs): Promise<CheckpointConnection> {
kabentley marked this conversation as resolved.
Show resolved Hide resolved
const connection = new this(await IpcApp.appFunctionIpc.openCheckpoint(args), true);
IModelConnection.onOpen.raiseEvent(connection);
return connection;
}
Expand Down
29 changes: 29 additions & 0 deletions full-stack-tests/backend/src/integration/Checkpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,35 @@ describe("Checkpoints", () => {
IModelHost.appWorkspace.settings.dropDictionary("prefetch");
});

it("should start prefetch in IPC", async () => {
// simulate user being logged in
sinon.stub(IModelHost, "getAccessToken").callsFake(async () => accessToken);

const prefetchSpy = sinon.spy(CloudSqlite, "startCloudPrefetch").withArgs(sinon.match.any, `${testChangeSet.id}.bim`, sinon.match.any); // Need matchers because GCS is also prefetched.
const settingsSpy = sinon.spy(IModelHost.appWorkspace.settings, "getBoolean").withArgs("Checkpoints/prefetch");

expect(prefetchSpy.callCount).equal(0);
expect(settingsSpy.callCount).equal(0);

const iModel = await SnapshotDb.openCheckpoint({
iTwinId: testITwinId,
iModelId: testIModelId,
changeset: testChangeSet,
doPrefetch: true,
});
assert.equal(iModel.iModelId, testIModelId);
assert.equal(iModel.changeset.id, testChangeSet.id);
assert.equal(iModel.iTwinId, testITwinId);
assert.equal(iModel.rootSubject.name, "Stadium Dataset 1");

expect(prefetchSpy.callCount).equal(1);
expect(settingsSpy.callCount).equal(1);

iModel.close();
sinon.restore();
IModelHost.appWorkspace.settings.dropDictionary("prefetch");
});

it("should query bcv stat table", async () => {
const containerSpy = sinon.spy(V2CheckpointManager, "attach");

Expand Down
Loading