@itwin/core-frontend
Version:
iTwin.js frontend components
848 lines • 42.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TileAdmin = void 0;
exports.overrideRequestTileTreeProps = overrideRequestTileTreeProps;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Tiles
*/
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const IModelApp_1 = require("../IModelApp");
const IpcApp_1 = require("../IpcApp");
const IModelConnection_1 = require("../IModelConnection");
const Viewport_1 = require("../Viewport");
const internal_1 = require("./internal");
/** Manages [[Tile]]s and [[TileTree]]s on behalf of [[IModelApp]]. Its responsibilities include scheduling requests for tile content via a priority queue;
* keeping track of and imposing limits upon the amount of GPU memory consumed by tiles; and notifying listeners of tile-related events.
* @see [[IModelApp.tileAdmin]] to access the instance of the TileAdmin.
* @see [[TileAdmin.Props]] to configure the TileAdmin at startup.
* @public
* @extensions
*/
class TileAdmin {
_versionInfo;
channels;
_users = new Set();
_requestsPerUser = new Map();
_tileUsagePerUser = new Map();
_selectedAndReady = new Map();
_tileUserSetsForRequests = new internal_1.UniqueTileUserSets();
_maxActiveTileTreePropsRequests;
_defaultTileSizeModifier;
_retryInterval;
_enableInstancing;
/** @internal */
edgeOptions;
/** @internal */
enableImprovedElision;
/** @internal */
enableFrontendScheduleScripts;
/** @internal */
decodeImdlInWorker;
/** @internal */
ignoreAreaPatterns;
/** @internal */
enableExternalTextures;
/** @internal */
disableMagnification;
/** @internal */
percentGPUMemDisablePreload;
/** @internal */
alwaysRequestEdges;
/** @internal */
alwaysSubdivideIncompleteTiles;
/** @internal */
minimumSpatialTolerance;
/** @internal */
maximumMajorTileFormatVersion;
/** @internal */
useProjectExtents;
/** @internal */
expandProjectExtents;
/** @internal */
optimizeBRepProcessing;
/** @internal */
disablePolyfaceDecimation;
/** @internal */
useLargerTiles;
/** @internal */
maximumLevelsToSkip;
/** @internal */
mobileRealityTileMinToleranceRatio;
/** @internal */
tileTreeExpirationTime;
/** @internal */
tileExpirationTime;
/** @internal */
contextPreloadParentDepth;
/** @internal */
contextPreloadParentSkip;
/** @beta */
cesiumIonKey;
_removeIModelConnectionOnCloseListener;
_totalElided = 0;
_rpcInitialized = false;
_nextPruneTime;
_nextPurgeTime;
_tileTreePropsRequests = [];
_cleanup;
_lruList = new internal_1.LRUTileList();
_maxTotalTileContentBytes;
_gpuMemoryLimit = "none";
_isMobile;
_cloudStorage;
/** Create a TileAdmin suitable for passing to [[IModelApp.startup]] via [[IModelAppOptions.tileAdmin]] to customize aspects of
* its behavior.
* @param props Options for customizing the behavior of the TileAdmin.
* @returns the TileAdmin
*/
static async create(props) {
const rpcConcurrency = IpcApp_1.IpcApp.isValid ? (await IpcApp_1.IpcApp.appFunctionIpc.queryConcurrency("cpu")) : undefined;
const isMobile = core_bentley_1.ProcessDetector.isMobileBrowser;
return new TileAdmin(isMobile, rpcConcurrency, props);
}
/** @internal */
get emptyTileUserSet() { return internal_1.UniqueTileUserSets.emptySet; }
/** Returns basic statistics about the TileAdmin's current state. */
get statistics() {
let numActiveTileTreePropsRequests = 0;
for (const req of this._tileTreePropsRequests) {
if (!req.isDispatched)
break;
++numActiveTileTreePropsRequests;
}
return {
...this.channels.statistics,
totalElidedTiles: this._totalElided,
numActiveTileTreePropsRequests,
numPendingTileTreePropsRequests: this._tileTreePropsRequests.length - numActiveTileTreePropsRequests,
};
}
/** Resets the cumulative (per-session) statistics like totalCompletedRequests, totalEmptyTiles, etc. */
resetStatistics() {
this.channels.resetStatistics();
this._totalElided = 0;
}
/** Exposed as public strictly for tests.
* @internal
*/
constructor(isMobile, rpcConcurrency, options) {
this._isMobile = isMobile;
if (undefined === options)
options = {};
this.channels = new internal_1.TileRequestChannels(rpcConcurrency, true === options.cacheTileMetadata);
this._maxActiveTileTreePropsRequests = options.maxActiveTileTreePropsRequests ?? 10;
this._defaultTileSizeModifier = (undefined !== options.defaultTileSizeModifier && options.defaultTileSizeModifier > 0) ? options.defaultTileSizeModifier : 1.0;
this._retryInterval = undefined !== options.retryInterval ? options.retryInterval : 1000;
this._enableInstancing = options.enableInstancing ?? core_common_1.defaultTileOptions.enableInstancing;
this.edgeOptions = {
type: false === options.enableIndexedEdges ? "non-indexed" : "compact",
smooth: options.generateAllPolyfaceEdges ?? true,
};
this.enableImprovedElision = options.enableImprovedElision ?? core_common_1.defaultTileOptions.enableImprovedElision;
this.enableFrontendScheduleScripts = options.enableFrontendScheduleScripts ?? false;
this.decodeImdlInWorker = options.decodeImdlInWorker ?? true;
this.ignoreAreaPatterns = options.ignoreAreaPatterns ?? core_common_1.defaultTileOptions.ignoreAreaPatterns;
this.enableExternalTextures = options.enableExternalTextures ?? core_common_1.defaultTileOptions.enableExternalTextures;
this.disableMagnification = options.disableMagnification ?? core_common_1.defaultTileOptions.disableMagnification;
this.percentGPUMemDisablePreload = Math.max(0, Math.min((options.percentGPUMemDisablePreload === undefined ? 80 : options.percentGPUMemDisablePreload), 80));
this.alwaysRequestEdges = true === options.alwaysRequestEdges;
this.alwaysSubdivideIncompleteTiles = options.alwaysSubdivideIncompleteTiles ?? core_common_1.defaultTileOptions.alwaysSubdivideIncompleteTiles;
this.maximumMajorTileFormatVersion = options.maximumMajorTileFormatVersion ?? core_common_1.defaultTileOptions.maximumMajorTileFormatVersion;
this.useProjectExtents = options.useProjectExtents ?? core_common_1.defaultTileOptions.useProjectExtents;
this.expandProjectExtents = options.expandProjectExtents ?? core_common_1.defaultTileOptions.expandProjectExtents;
this.optimizeBRepProcessing = options.optimizeBRepProcessing ?? core_common_1.defaultTileOptions.optimizeBRepProcessing;
this.disablePolyfaceDecimation = options.disablePolyfaceDecimation ?? core_common_1.defaultTileOptions.disablePolyfaceDecimation;
this.useLargerTiles = options.useLargerTiles ?? core_common_1.defaultTileOptions.useLargerTiles;
this.mobileRealityTileMinToleranceRatio = Math.max(options.mobileRealityTileMinToleranceRatio ?? 3.0, 1.0);
this.cesiumIonKey = options.cesiumIonKey;
this._cloudStorage = options.tileStorage;
const gpuMemoryLimits = options.gpuMemoryLimits;
let gpuMemoryLimit;
if (typeof gpuMemoryLimits === "object")
gpuMemoryLimit = isMobile ? gpuMemoryLimits.mobile : gpuMemoryLimits.nonMobile;
else
gpuMemoryLimit = gpuMemoryLimits;
if (undefined === gpuMemoryLimit)
gpuMemoryLimit = isMobile ? "default" : TileAdmin.nonMobileUndefinedGpuMemoryLimit;
this.gpuMemoryLimit = gpuMemoryLimit;
if (undefined !== options.maximumLevelsToSkip)
this.maximumLevelsToSkip = Math.floor(Math.max(0, options.maximumLevelsToSkip));
else
this.maximumLevelsToSkip = 1;
const minSpatialTol = options.minimumSpatialTolerance;
this.minimumSpatialTolerance = undefined !== minSpatialTol ? Math.max(minSpatialTol, 0) : 0.001;
const clamp = (seconds, min, max) => {
seconds = Math.min(seconds, max);
seconds = Math.max(seconds, min);
return core_bentley_1.BeDuration.fromSeconds(seconds);
};
const ignoreMinimums = true === options.ignoreMinimumExpirationTimes;
const minTileTime = ignoreMinimums ? 0.1 : 5;
const minTreeTime = ignoreMinimums ? 0.1 : 10;
// If unspecified, tile expiration time defaults to 20 seconds.
this.tileExpirationTime = clamp((options.tileExpirationTime ?? 20), minTileTime, 60);
// If unspecified, trees never expire (will change this to use a default later).
this.tileTreeExpirationTime = clamp(options.tileTreeExpirationTime ?? 300, minTreeTime, 3600);
const now = core_bentley_1.BeTimePoint.now();
this._nextPruneTime = now.plus(this.tileExpirationTime);
this._nextPurgeTime = now.plus(this.tileTreeExpirationTime);
this._removeIModelConnectionOnCloseListener = IModelConnection_1.IModelConnection.onClose.addListener((iModel) => this.onIModelClosed(iModel));
// If unspecified preload 2 levels of parents for context tiles.
this.contextPreloadParentDepth = Math.max(0, Math.min((options.contextPreloadParentDepth === undefined ? 2 : options.contextPreloadParentDepth), 8));
// If unspecified skip one level before preloading of parents of context tiles.
this.contextPreloadParentSkip = Math.max(0, Math.min((options.contextPreloadParentSkip === undefined ? 1 : options.contextPreloadParentSkip), 5));
const removals = [
this.onTileLoad.addListener(() => this.invalidateAllScenes()),
this.onTileChildrenLoad.addListener(() => this.invalidateAllScenes()),
this.onTileTreeLoad.addListener(() => {
// A reality model tile tree's range may extend outside of the project extents - we'll want to recompute the extents
// of any spatial view's that may be displaying the reality model.
for (const user of this.tileUsers)
if (user instanceof Viewport_1.Viewport && user.view.isSpatialView())
user.invalidateController();
}),
];
this._cleanup = () => {
removals.forEach((removal) => removal());
};
}
_tileStorage;
async getTileStorage() {
if (this._tileStorage !== undefined)
return this._tileStorage;
// if custom implementation is provided, construct a new TileStorage instance and return it.
if (this._cloudStorage !== undefined) {
this._tileStorage = new internal_1.TileStorage(this._cloudStorage);
return this._tileStorage;
}
const fetchStorage = new internal_1.FetchCloudStorage();
this._tileStorage = new internal_1.TileStorage(fetchStorage);
return this._tileStorage;
}
/** @internal */
get enableInstancing() { return this._enableInstancing; }
/** Given a numeric combined major+minor tile format version (typically obtained from a request to the backend to query the maximum tile format version it supports),
* return the maximum *major* format version to be used to request tile content from the backend.
* @see [[TileAdmin.Props.maximumMajorTileFormatVersion]]
* @see [[CurrentImdlVersion]]
*/
getMaximumMajorTileFormatVersion(formatVersion) {
return (0, core_common_1.getMaximumMajorTileFormatVersion)(this.maximumMajorTileFormatVersion, formatVersion);
}
/** A default multiplier applied to the size in pixels of a [[Tile]] during tile selection for any [[Viewport]].
* Individual Viewports can override this multiplier if desired.
* A value greater than 1.0 causes lower-resolution tiles to be selected; a value < 1.0 selects higher-resolution tiles.
* This can allow an application to sacrifice quality for performance or vice-versa.
* This property is initialized from the value supplied by the [[TileAdmin.Props.defaultTileSizeModifier]] used to initialize the TileAdmin at startup.
* Changing it after startup will change it for all Viewports that do not explicitly override it with their own multiplier.
* This value must be greater than zero.
*/
get defaultTileSizeModifier() { return this._defaultTileSizeModifier; }
set defaultTileSizeModifier(modifier) {
if (modifier !== this._defaultTileSizeModifier && modifier > 0 && !Number.isNaN(modifier)) {
this._defaultTileSizeModifier = modifier;
IModelApp_1.IModelApp.viewManager.invalidateScenes();
}
}
/** The total number of bytes of GPU memory allocated to [[Tile]] contents.
* @see [[gpuMemoryLimit]] to impose limits on how high this can grow.
*/
get totalTileContentBytes() {
return this._lruList.totalBytesUsed;
}
/** The maximum number of bytes of GPU memory that can be allocated to the contents of [[Tile]]s. When this limit is exceeded, the contents of the least-recently-drawn
* tiles are discarded until the total is below this limit or all undisplayed tiles' contents have been discarded.
* @see [[totalTileContentBytes]] for the current GPU memory usage.
* @see [[gpuMemoryLimit]] to adjust this maximum.
*/
get maxTotalTileContentBytes() {
return this._maxTotalTileContentBytes;
}
/** The strategy for limiting the amount of GPU memory allocated to [[Tile]] graphics.
* @see [[TileAdmin.Props.gpuMemoryLimits]] to configure this at startup.
* @see [[maxTotalTileContentBytes]] for the limit as a maximum number of bytes.
*/
get gpuMemoryLimit() {
return this._gpuMemoryLimit;
}
set gpuMemoryLimit(limit) {
if (limit === this.gpuMemoryLimit)
return;
let maxBytes;
if (typeof limit === "number") {
limit = Math.max(0, limit);
maxBytes = limit;
}
else {
switch (limit) {
case "default":
case "aggressive":
case "relaxed":
const spec = this._isMobile ? TileAdmin.mobileGpuMemoryLimits : TileAdmin.nonMobileGpuMemoryLimits;
maxBytes = spec[limit];
break;
default:
limit = "none";
// eslint-disable-next-line no-fallthrough
case "none":
maxBytes = undefined;
break;
}
}
this._gpuMemoryLimit = limit;
this._maxTotalTileContentBytes = maxBytes;
}
/** Returns whether or not preloading for context (reality and map tiles) is currently allowed.
* It is not allowed on mobile devices or if [[TileAdmin.Props.percentGPUMemDisablePreload]] is 0.
* Otherwise it is always allowed if [[GpuMemoryLimit]] is "none".
* Otherwise it is only allowed if current GPU memory utilization is less than [[TileAdmin.Props.percentGPUMemDisablePreload]] of GpuMemoryLimit.
* @internal
*/
get isPreloadingAllowed() {
return !this._isMobile && this.percentGPUMemDisablePreload > 0 && (this._maxTotalTileContentBytes === undefined || this._lruList.totalBytesUsed / this._maxTotalTileContentBytes * 100 < this.percentGPUMemDisablePreload);
}
/** Invoked from the [[ToolAdmin]] event loop to process any pending or active requests for tiles.
* @internal
*/
process() {
this.processQueue();
// Prune expired tiles and purge expired tile trees. This may free up some memory.
this.pruneAndPurge();
// Free up any additional memory as required to keep within our limit.
this.freeMemory();
}
/** Iterate over the tiles that have content loaded but are not in use by any [[TileUser]].
* @alpha
*/
get unselectedLoadedTiles() {
return this._lruList.unselectedTiles;
}
/** Iterate over the tiles that have content loaded and are in use by any [[TileUser]].
* @alpha
*/
get selectedLoadedTiles() {
return this._lruList.selectedTiles;
}
/** Returns the number of pending and active requests associated with the specified viewport. */
getNumRequestsForViewport(vp) {
return this.getNumRequestsForUser(vp);
}
/** Returns the number of pending and active requests associated with the specified user. */
getNumRequestsForUser(user) {
const requests = this.getRequestsForUser(user);
let count = requests?.size ?? 0;
const tiles = this.getTilesForUser(user);
if (tiles)
count += tiles.external.requested;
return count;
}
/** Returns the current set of Tiles requested by the specified TileUser.
* Do not modify the set or the Tiles.
* @internal
*/
getRequestsForUser(user) {
return this._requestsPerUser.get(user);
}
/** Specifies the set of tiles currently requested for use by a TileUser. This set replaces any previously specified for the same user.
* The requests are not actually processed until the next call to [[TileAdmin.process].
* This is typically invoked when a viewport recreates its scene, e.g. in response to camera movement.
* @internal
*/
requestTiles(user, tiles) {
this._requestsPerUser.set(user, tiles);
}
/** Returns two sets of tiles associated with the specified user - typically, a viewport's current scene.
* Do not modify the returned sets.
* @internal
*/
getTilesForUser(user) {
return this._selectedAndReady.get(user);
}
/** Adds the specified tiles to the sets of selected and ready tiles for the specified TileUser.
* The TileAdmin takes ownership of the `ready` set - do not modify it after passing it in.
* @internal
*/
addTilesForUser(user, selected, ready, touched) {
// "selected" are tiles we are drawing.
this._lruList.markUsed(user.tileUserId, selected);
// "ready" are tiles we want to draw but can't yet because, for example, their siblings are not yet ready to be drawn.
this._lruList.markUsed(user.tileUserId, ready);
// "touched" are tiles whose contents we want to keep in memory regardless of whether they are "selected" or "ready".
this._lruList.markUsed(user.tileUserId, touched);
const entry = this.getTilesForUser(user);
if (undefined === entry) {
this._selectedAndReady.set(user, { ready, selected: new Set(selected), external: { selected: 0, requested: 0, ready: 0 } });
return;
}
for (const tile of selected)
entry.selected.add(tile);
for (const tile of ready)
entry.ready.add(tile);
}
/** Disclose statistics about tiles that are handled externally from TileAdmin. At this time, that means OrbitGT point cloud tiles.
* These statistics are included in the return value of [[getTilesForUser]].
* @internal
*/
addExternalTilesForUser(user, statistics) {
const entry = this.getTilesForUser(user);
if (!entry) {
this._selectedAndReady.set(user, { ready: new Set(), selected: new Set(), external: { ...statistics } });
return;
}
entry.external.requested += statistics.requested;
entry.external.selected += statistics.selected;
entry.external.ready += statistics.ready;
}
/** Clears the sets of tiles associated with a TileUser. */
clearTilesForUser(user) {
this._selectedAndReady.delete(user);
this._lruList.clearUsed(user.tileUserId);
}
/** Indicates that the TileAdmin should cease tracking the specified TileUser, e.g. because it is about to be destroyed.
* Any requests which are of interest only to the specified user will be canceled.
*/
forgetUser(user) {
this.onUserIModelClosed(user);
this._users.delete(user);
}
/** Indicates that the TileAdmin should track tile requests for the specified TileUser.
* This is invoked by the Viewport constructor and should be invoked manually for any non-Viewport TileUser.
* [[forgetUser]] must be later invoked to unregister the user.
*/
registerUser(user) {
this._users.add(user);
}
/** Iterable over all TileUsers registered with TileAdmin. This may include [[OffScreenViewport]]s.
* @alpha
*/
get tileUsers() {
return this._users;
}
/** @internal */
invalidateAllScenes() {
for (const user of this.tileUsers)
if (user instanceof Viewport_1.Viewport)
user.invalidateScene();
}
/** @internal */
onShutDown() {
if (this._cleanup) {
this._cleanup();
this._cleanup = undefined;
}
this._removeIModelConnectionOnCloseListener();
this.channels.onShutDown();
for (const req of this._tileTreePropsRequests)
req.abandon();
this._requestsPerUser.clear();
this._tileUserSetsForRequests.clear();
this._tileUsagePerUser.clear();
this._tileTreePropsRequests.length = 0;
this._lruList[Symbol.dispose]();
}
/** Returns the union of the input set and the input TileUser, to be associated with a [[TileRequest]].
* @internal
*/
getTileUserSetForRequest(user, users) {
return this._tileUserSetsForRequests.getTileUserSet(user, users);
}
/** Marks the Tile as "in use" by the specified TileUser, where the tile defines what "in use" means.
* A tile will not be discarded while it is in use by any TileUser.
* @see [[TileTree.prune]]
* @internal
*/
markTileUsed(marker, user) {
let set = this._tileUsagePerUser.get(user);
if (!set)
this._tileUsagePerUser.set(user, set = new Set());
set.add(marker);
}
/** Returns true if the Tile is currently in use by any TileUser.
* @see [[markTileUsed]].
* @internal
*/
isTileInUse(marker) {
for (const [_user, markers] of this._tileUsagePerUser)
if (markers.has(marker))
return true;
return false;
}
/** Indicates that the TileAdmin should reset usage tracking for the specified TileUser, e.g. because the user is a Viewport about
* to recreate its scene. Any tiles currently marked as "in use" by this user no longer will be.
* @internal
*/
clearUsageForUser(user) {
this._tileUsagePerUser.delete(user);
}
/** @internal */
async requestTileTreeProps(iModel, treeId) {
this.initializeRpc();
const requests = this._tileTreePropsRequests;
return new Promise((resolve, reject) => {
const request = new TileTreePropsRequest(iModel, treeId, resolve, reject);
requests.push(request);
if (this._tileTreePropsRequests.length <= this._maxActiveTileTreePropsRequests)
request.dispatch();
});
}
/** Temporary workaround for authoring applications. Usage:
* ```ts
* async function handleModelChanged(modelId: Id64String, iModel: IModelConnection): Promise<void> {
* await iModel.tiles.purgeTileTrees([modelId]);
* IModelApp.viewManager.refreshForModifiedModels(modelId);
* }
* ```
* @internal
*/
async purgeTileTrees(iModel, modelIds) {
this.initializeRpc();
return core_common_1.IModelTileRpcInterface.getClient().purgeTileTrees(iModel.getRpcProps(), modelIds);
}
/** @internal */
async requestCachedTileContent(tile) {
if (tile.iModelTree.iModel.iModelId === undefined)
throw new Error("Provided iModel has no iModelId");
const { guid, tokenProps, treeId } = this.getTileRequestProps(tile);
const content = await (await this.getTileStorage()).downloadTile(tokenProps, tile.iModelTree.iModel.iModelId, tile.iModelTree.iModel.changeset.id, treeId, tile.contentId, guid);
return content;
}
/** @internal */
async generateTileContent(tile) {
this.initializeRpc();
const props = this.getTileRequestProps(tile);
const retrieveMethod = await core_common_1.IModelTileRpcInterface.getClient().generateTileContent(props.tokenProps, props.treeId, props.contentId, props.guid);
if (tile.request?.isCanceled) {
// the content is no longer needed, return an empty array.
return new Uint8Array();
}
if (retrieveMethod === core_common_1.TileContentSource.ExternalCache) {
const tileContent = await this.requestCachedTileContent(tile);
if (tileContent === undefined)
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NoContent, "Failed to fetch generated tile from external cache");
return tileContent;
}
else if (retrieveMethod === core_common_1.TileContentSource.Backend) {
return core_common_1.IModelTileRpcInterface.getClient().retrieveTileContent(props.tokenProps, this.getTileRequestProps(tile));
}
throw new core_common_1.BackendError(core_bentley_1.BentleyStatus.ERROR, "", "Invalid response from RPC backend");
}
/** @internal */
getTileRequestProps(tile) {
const tree = tile.iModelTree;
const tokenProps = tree.iModel.getRpcProps();
let guid = tree.geometryGuid || tokenProps.changeset?.id || "first";
if (tree.contentIdQualifier)
guid = `${guid}_${tree.contentIdQualifier}`;
const contentId = tile.contentId;
const treeId = tree.id;
return { tokenProps, treeId, contentId, guid };
}
/** Request graphics for a single element or geometry stream.
* @see [[readElementGraphics]] to convert the result into a [[RenderGraphic]] for display.
* @public
*/
async requestElementGraphics(iModel, requestProps) {
if (true !== requestProps.omitEdges && undefined === requestProps.edgeType)
requestProps = { ...requestProps, edgeType: "non-indexed" !== this.edgeOptions.type ? 2 : 1 };
// For backwards compatibility, these options default to true in the backend. Explicitly set them to false in (newer) frontends if not supplied.
if (undefined === requestProps.quantizePositions || undefined === requestProps.useAbsolutePositions) {
requestProps = {
...requestProps,
quantizePositions: requestProps.quantizePositions ?? false,
useAbsolutePositions: requestProps.useAbsolutePositions ?? false,
};
}
this.initializeRpc();
const intfc = core_common_1.IModelTileRpcInterface.getClient();
return intfc.requestElementGraphics(iModel.getRpcProps(), requestProps);
}
/** Obtain information about the version/format of the tiles supplied by the backend. */
async queryVersionInfo() {
if (!this._versionInfo) {
this.initializeRpc();
this._versionInfo = await core_common_1.IModelTileRpcInterface.getClient().queryVersionInfo();
}
return this._versionInfo;
}
/** @internal */
onTilesElided(numElided) {
this._totalElided += numElided;
}
/** Invoked when a Tile marks itself as "ready" - i.e., its content is loaded (or determined not to exist, or not to be needed).
* If the tile has content, it is added to the LRU list of tiles with content.
* The `onTileLoad` event will also be raised.
* @internal
*/
onTileContentLoaded(tile) {
// It may already be present if it previously had content - perhaps we're replacing its content.
this._lruList.drop(tile);
this._lruList.add(tile);
this.onTileLoad.raiseEvent(tile);
}
/** Invoked when a Tile's content is disposed of. It will be removed from the LRU list of tiles with content.
* @internal
*/
onTileContentDisposed(tile) {
this._lruList.drop(tile);
}
/** @internal */
terminateTileTreePropsRequest(request) {
const index = this._tileTreePropsRequests.indexOf(request);
if (index >= 0) {
this._tileTreePropsRequests.splice(index, 1);
this.dispatchTileTreePropsRequests();
}
}
/** Event raised when a request to load a tile's content completes. */
onTileLoad = new core_bentley_1.BeEvent();
/** Event raised when a request to load a tile tree completes. */
onTileTreeLoad = new core_bentley_1.BeEvent();
/** Event raised when a request to load a tile's child tiles completes. */
onTileChildrenLoad = new core_bentley_1.BeEvent();
/** Subscribe to [[onTileLoad]], [[onTileTreeLoad]], and [[onTileChildrenLoad]]. */
addLoadListener(callback) {
const tileLoad = this.onTileLoad.addListener((tile) => callback(tile.tree.iModel));
const treeLoad = this.onTileTreeLoad.addListener((tree) => callback(tree.iModel));
const childLoad = this.onTileChildrenLoad.addListener((tile) => callback(tile.tree.iModel));
return () => {
tileLoad();
treeLoad();
childLoad();
};
}
/** Determine what information about the schedule script is needed to produce tiles.
* If no script, or the script doesn't require batching, then no information is needed - normal tiles can be used.
* If possible and enabled, normal tiles can be requested and then processed on the frontend based on the ModelTimeline.
* Otherwise, special tiles must be requested based on the script's sourceId (RenderTimeline or DisplayStyle element).
* @internal
*/
getScriptInfoForTreeId(modelId, script) {
if (!script || !script.script.requiresBatching)
return undefined;
const timeline = script.script.modelTimelines.find((x) => x.modelId === modelId);
if (!timeline || (!timeline.requiresBatching && !timeline.containsTransform))
return undefined;
// Frontend schedule scripts require the element Ids to be included in the script - previously saved views may have omitted them.
if (!core_bentley_1.Id64.isValidId64(script.sourceId) || (this.enableFrontendScheduleScripts && !timeline.omitsElementIds))
return { timeline };
return { animationId: script.sourceId };
}
dispatchTileTreePropsRequests() {
for (let i = 0; i < this._maxActiveTileTreePropsRequests && i < this._tileTreePropsRequests.length; i++)
this._tileTreePropsRequests[i].dispatch();
}
processQueue() {
// Mark all requests as being associated with no users, indicating they are no longer needed.
this._tileUserSetsForRequests.clearAll();
// Notify channels that we are enqueuing new requests.
this.channels.swapPending();
// Repopulate pending requests queue from each user. We do NOT sort by priority while doing so.
this._requestsPerUser.forEach((value, key) => this.processRequests(key, value));
// Ask channels to update their queues and dispatch requests.
this.channels.process();
}
/** Exported strictly for tests. @internal */
freeMemory() {
if (undefined !== this._maxTotalTileContentBytes)
this._lruList.freeMemory(this._maxTotalTileContentBytes);
}
pruneAndPurge() {
const now = core_bentley_1.BeTimePoint.now();
const needPrune = this._nextPruneTime.before(now);
const needPurge = this._nextPurgeTime.before(now);
if (!needPrune && !needPurge)
return;
// Identify all of the TileTrees in use by all of the TileUsers known to the TileAdmin.
// NOTE: A single viewport can display tiles from more than one IModelConnection.
// NOTE: A viewport may be displaying no trees - but we need to record its IModel so we can purge those which are NOT being displayed
// NOTE: That won't catch external tile trees previously used by that viewport.
const trees = new internal_1.DisclosedTileTreeSet();
const treesByIModel = needPurge ? new Map() : undefined;
for (const user of this._users) {
if (!user.iModel.isOpen) // case of closing an IModelConnection while keeping the Viewport open, possibly for reuse with a different IModelConnection.
continue;
user.discloseTileTrees(trees);
if (treesByIModel && undefined === treesByIModel.get(user.iModel))
treesByIModel.set(user.iModel, new Set());
}
if (needPrune) {
// Request that each displayed tile tree discard any tiles and/or tile content that is no longer needed.
for (const tree of trees)
tree.prune();
this._nextPruneTime = now.plus(this.tileExpirationTime);
}
if (treesByIModel) {
for (const tree of trees) {
let set = treesByIModel.get(tree.iModel);
if (undefined === set)
treesByIModel.set(tree.iModel, set = new Set());
set.add(tree);
}
// Discard any tile trees that are no longer in use by any user.
const olderThan = now.minus(this.tileTreeExpirationTime);
for (const entry of treesByIModel)
entry[0].tiles.purge(olderThan, entry[1]);
this._nextPurgeTime = now.plus(this.tileTreeExpirationTime);
}
}
processRequests(user, tiles) {
for (const tile of tiles) {
if (undefined === tile.request) {
// ###TODO: This assertion triggers for AttachmentViewports used for rendering 3d sheet attachments.
// Determine why and fix.
// assert(tile.loadStatus === Tile.LoadStatus.NotLoaded);
if (internal_1.TileLoadStatus.NotLoaded === tile.loadStatus) {
const request = new internal_1.TileRequest(tile, user);
tile.request = request;
(0, core_bentley_1.assert)(this.channels.has(request.channel));
request.channel.append(request);
}
}
else {
const req = tile.request;
(0, core_bentley_1.assert)(undefined !== req);
if (undefined !== req) {
// Request may already be dispatched (in channel's active requests) - if so do not re-enqueue!
if (req.isQueued && 0 === req.users.length)
req.channel.append(req);
req.addUser(user);
(0, core_bentley_1.assert)(0 < req.users.length);
}
}
}
}
// NB: This does *not* remove from this._users - a viewport could later be reused with a different IModelConnection.
onUserIModelClosed(user) {
this.clearUsageForUser(user);
this.clearTilesForUser(user);
// NB: user will be removed from TileUserSets in process() - but if we can establish that only this user wants a given tile, cancel its request immediately.
const tiles = this._requestsPerUser.get(user);
if (undefined !== tiles) {
for (const tile of tiles) {
const request = tile.request;
if (undefined !== request && 1 === request.users.length)
request.cancel();
}
this._requestsPerUser.delete(user);
}
}
onIModelClosed(iModel) {
this._requestsPerUser.forEach((_req, user) => {
if (user.iModel === iModel)
this.onUserIModelClosed(user);
});
// Remove any TileTreeProps requests associated with this iModel.
this._tileTreePropsRequests = this._tileTreePropsRequests.filter((req) => {
if (req.iModel !== iModel)
return true;
req.abandon();
return false;
});
// Dispatch TileTreeProps requests not associated with this iModel.
this.dispatchTileTreePropsRequests();
this.channels.onIModelClosed(iModel);
}
initializeRpc() {
// Would prefer to do this in constructor - but nothing enforces that the app initializes the rpc interfaces before it creates the TileAdmin (via IModelApp.startup()) - so do it on first request instead.
if (this._rpcInitialized)
return;
this._rpcInitialized = true;
const retryInterval = this._retryInterval;
core_common_1.RpcOperation.lookup(core_common_1.IModelTileRpcInterface, "requestTileTreeProps").policy.retryInterval = () => retryInterval;
const policy = core_common_1.RpcOperation.lookup(core_common_1.IModelTileRpcInterface, "generateTileContent").policy;
policy.retryInterval = () => retryInterval;
policy.allowResponseCaching = () => core_common_1.RpcResponseCacheControl.Immutable;
}
}
exports.TileAdmin = TileAdmin;
/** @public */
(function (TileAdmin) {
/** The number of bytes of GPU memory associated with the various [[GpuMemoryLimit]]s for non-mobile devices.
* @see [[TileAdmin.Props.gpuMemoryLimits]] to specify the limit at startup.
* @see [[TileAdmin.gpuMemoryLimit]] to adjust the actual limit after startup.
* @see [[TileAdmin.mobileMemoryLimits]] for mobile devices.
*/
TileAdmin.nonMobileGpuMemoryLimits = {
default: 1024 * 1024 * 1024, // 1 GB
aggressive: 500 * 1024 * 1024, // 500 MB
relaxed: 2.5 * 1024 * 1024 * 1024, // 2.5 GB
};
/** @internal exported for tests */
TileAdmin.nonMobileUndefinedGpuMemoryLimit = 6000 * 1024 * 1024; // 6,000 MB - used when nonMobile limit is undefined
/** The number of bytes of GPU memory associated with the various [[GpuMemoryLimit]]s for mobile devices.
* @see [[TileAdmin.Props.gpuMemoryLimits]] to specify the limit at startup.
* @see [[TileAdmin.gpuMemoryLimit]] to adjust the actual limit after startup.
* @see [[TileAdmin.nonMobileMemoryLimits]] for non-mobile devices.
*/
TileAdmin.mobileGpuMemoryLimits = {
default: 200 * 1024 * 1024, // 200 MB
aggressive: 75 * 1024 * 1024, // 75 MB
relaxed: 500 * 1024 * 1024, // 500 MB
};
})(TileAdmin || (exports.TileAdmin = TileAdmin = {}));
/** Some views contain thousands of models. When we open such a view, the first thing we do is request the IModelTileTreeProps for each model. This involves a http request per model,
* which can exceed the maximum number of simultaneous requests permitted by the browser.
* Similar to how we throttle requests for tile *content*, we throttle requests for IModelTileTreeProps based on `TileAdmin.Props.maxActiveTileTreePropsRequests`, heretofore referred to as `N`.
* TileAdmin maintains a FIFO queue of requests for IModelTileTreeProps. The first N of those requests have been dispatched; the remainder are waiting for their turn.
* When `TileAdmin.requestTileTreeProps` is called, it appends a new request to the queue, and if the queue length < N, dispatches it immediately.
* When a request completes, throws an error, or is canceled, it is removed from the queue, and any not-yet-dispatched requests are dispatched (not exceeding N total in flight).
* When an IModelConnection is closed, any requests associated with that iModel are canceled.
* NOTE: This request queue currently does not interact at all with the tile content request queue.
* NOTE: We rely on TreeOwner to not request the same IModelTileTreeProps multiple times - we do not check the queue for presence of a requested tree before enqeueing it.
*/
class TileTreePropsRequest {
iModel;
_treeId;
_resolve;
_reject;
_isDispatched = false;
constructor(iModel, _treeId, _resolve, _reject) {
this.iModel = iModel;
this._treeId = _treeId;
this._resolve = _resolve;
this._reject = _reject;
}
get isDispatched() { return this._isDispatched; }
dispatch() {
if (this.isDispatched)
return;
this._isDispatched = true;
requestTileTreeProps(this.iModel, this._treeId).then((props) => {
this.terminate();
this._resolve(props);
}).catch((err) => {
this.terminate();
this._reject(err);
});
}
/** The IModelConnection was closed, or IModelApp was shut down. Don't call terminate(), because we don't want to dispatch pending requests as a result.
* Just reject if not yet dispatched.
*/
abandon() {
if (!this.isDispatched) {
// A little white lie that tells the TileTreeOwner it can try to load again later if needed, rather than treating rejection as failure to load.
this._reject(new core_common_1.ServerTimeoutError("requestTileTreeProps cancelled"));
}
}
terminate() {
IModelApp_1.IModelApp.tileAdmin.terminateTileTreePropsRequest(this);
}
}
let requestTileTreePropsOverride;
async function requestTileTreeProps(iModel, treeId) {
if (requestTileTreePropsOverride)
return requestTileTreePropsOverride(iModel, treeId);
return core_common_1.IModelTileRpcInterface.getClient().requestTileTreeProps(iModel.getRpcProps(), treeId);
}
/** Strictly for tests - overrides the call to IModelTileRpcInterface.requestTileTreeProps with a custom function, or clears the override.
* @internal
*/
function overrideRequestTileTreeProps(func) {
requestTileTreePropsOverride = func;
}
//# sourceMappingURL=TileAdmin.js.map