UNPKG

@itwin/core-frontend

Version:
848 lines • 42.2 kB
"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