/
TileAdmin.ts
1368 lines (1195 loc) · 63.4 KB
/
TileAdmin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*---------------------------------------------------------------------------------------------
* 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
*/
import {
assert, BeDuration, BeEvent, BentleyStatus, BeTimePoint, Id64, Id64Array, Id64String, IModelStatus, ProcessDetector,
} from "@itwin/core-bentley";
import {
BackendError, defaultTileOptions, EdgeOptions, ElementGraphicsRequestProps, getMaximumMajorTileFormatVersion, IModelError, IModelTileRpcInterface,
IModelTileTreeProps, RenderSchedule, RpcOperation, RpcResponseCacheControl, ServerTimeoutError, TileContentSource, TileVersionInfo,
} from "@itwin/core-common";
import { IModelApp } from "../IModelApp";
import { IpcApp } from "../IpcApp";
import { IModelConnection } from "../IModelConnection";
import { Viewport } from "../Viewport";
import {
DisclosedTileTreeSet, FetchCloudStorage, IModelTileTree, LRUTileList, ReadonlyTileUserSet, Tile, TileContentDecodingStatistics, TileLoadStatus,
TileRequest, TileRequestChannels, TileStorage, TileTree, TileTreeOwner, TileUsageMarker, TileUser, UniqueTileUserSets,
} from "./internal";
import type { FrontendStorage } from "@itwin/object-storage-core/lib/frontend";
/** Details about any tiles not handled by [[TileAdmin]]. At this time, that means OrbitGT point cloud tiles.
* Used for bookkeeping by SelectedAndReadyTiles
* @internal
*/
export interface ExternalTileStatistics {
requested: number;
selected: number;
ready: number;
}
/** Describes two sets of tiles associated with a viewport's current scene.
* @internal
*/
export interface SelectedAndReadyTiles {
/** The tiles actually selected for the viewport's scene. This includes tiles drawn to the screen; it may also include tiles selected for the shadow map.
* These represent the "best available" tiles for the current view - some may have been selected as placeholders while more appropriate tiles are loaded.
*/
readonly selected: Set<Tile>;
/** The tiles that are considered appropriate for the current view and that are ready to draw. Some may not have actually been selected for drawing in the
* current view, e.g., because sibling tiles are not yet ready to draw.
*/
readonly ready: Set<Tile>;
/** Details about any tiles not handled by [[TileAdmin]]. At this time, that means OrbitGT point cloud tiles and tiles for view attachments. */
readonly external: ExternalTileStatistics;
}
/** Describes a strategy for imposing limits upon the amount of GPU memory consumed by [[Tile]] content.
*
* For a given view, a set of tiles is required to display its contents. As the user navigates the view by panning, rotating, zooming, etc, that set of tiles changes.
* Previously-displayed tiles can remain in memory for a short while so that if they are subsequently required for display again they will be immediately available.
* Keeping too many tiles in memory can consume excessive amounts of GPU memory; in extreme cases, more GPU memory can be consumed than is available, resulting in loss of
* the WebGL context, which causes all rendering to stop.
*
* Over-consumption of GPU memory can be prevented by imposing a limit on the maximum amount that can be in use at any one time. When the limit is exceeded, the contents
* of [[Tile]]s that are not currently being displayed by any [[Viewport]] are discarded, freeing up memory, until the limit is satisfied or all
* undisplayed tiles' contents have been discarded. The least-recently-displayed tiles' contents are discarded first, under the assumption that they are the least likely to
* be displayed again in the near future. Contents of tiles that are currently being displayed by at least one viewport will not be discarded.
*
* WebGL provides no direct access to the GPU, so the amount of memory actually being consumed can only be estimated based on the amount of memory
* requested from it; the actual amount will invariably be larger - sometimes much larger.
*
* The number of bytes corresponding to the various limits is estimated at run-time based on whether the client is running on a mobile device or not - tighter limits
* are imposed on mobile devices due to their tighter resource constraints.
*
* In addition to the memory limits, tile contents are naturally discarded after a certain length of time during which they have been displayed by no viewports based on
* [[TileAdmin.Props.tileExpirationTime]].
*
* The options are:
* - "none" - no limits are imposed. Tile contents are discarded solely based on [[TileAdmin.Props.tileExpirationTime]].
* - "aggressive" - a small limit resulting in tile contents being more aggressively discarded.
* - "default" - a moderate limit that strives to balance limiting memory consumption while keeping tiles likely to be displayed again in memory.
* - "relaxed" - a larger limit that may be appropriate for devices equipped with ample GPU memory.
* - number - an explicit limit, in number of bytes. Because of the vagaries of actual GPU memory consumption under WebGL, this should be a conservative estimate - no more than perhaps 1/4 of the total GPU memory available, depending on the device.
* @see [[TileAdmin.Props.gpuMemoryLimits]] to configure the limit at startup.
* @see [[TileAdmin.gpuMemoryLimit]] to adjust the limit after startup.
* @see [[TileAdmin.totalTileContentBytes]] for the current amount of GPU memory being used for tile contents.
* @public
* @extensions
*/
export type GpuMemoryLimit = "none" | "default" | "aggressive" | "relaxed" | number;
/** Defines separate [[GpuMemoryLimit]]s for mobile and desktop clients.
* @see [[TileAdmin.Props.gpuMemoryLimits]] to configure the limit at startup.
* @see [[GpuMemoryLimit]] for a description of how the available limits and how they are imposed.
* @public
* @extensions
*/
export interface GpuMemoryLimits {
/** Limits applied to clients running on mobile devices. Defaults to "default" if undefined. */
mobile?: GpuMemoryLimit;
/** Limits applied to clients running on non-mobile devices. Defaults to 6,000 MB if undefined. */
nonMobile?: GpuMemoryLimit;
}
/** 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
*/
export class TileAdmin {
private _versionInfo?: TileVersionInfo;
public readonly channels: TileRequestChannels;
private readonly _users = new Set<TileUser>();
private readonly _requestsPerUser = new Map<TileUser, Set<Tile>>();
private readonly _tileUsagePerUser = new Map<TileUser, Set<TileUsageMarker>>();
private readonly _selectedAndReady = new Map<TileUser, SelectedAndReadyTiles>();
private readonly _tileUserSetsForRequests = new UniqueTileUserSets();
private readonly _maxActiveTileTreePropsRequests: number;
private _defaultTileSizeModifier: number;
private readonly _retryInterval: number;
private readonly _enableInstancing: boolean;
/** @internal */
public readonly edgeOptions: EdgeOptions;
/** @internal */
public readonly enableImprovedElision: boolean;
/** @internal */
public readonly enableFrontendScheduleScripts: boolean;
/** @internal */
public readonly decodeImdlInWorker: boolean;
/** @internal */
public readonly ignoreAreaPatterns: boolean;
/** @internal */
public readonly enableExternalTextures: boolean;
/** @internal */
public readonly disableMagnification: boolean;
/** @internal */
public readonly percentGPUMemDisablePreload: number;
/** @internal */
public readonly alwaysRequestEdges: boolean;
/** @internal */
public readonly alwaysSubdivideIncompleteTiles: boolean;
/** @internal */
public readonly minimumSpatialTolerance: number;
/** @internal */
public readonly maximumMajorTileFormatVersion: number;
/** @internal */
public readonly useProjectExtents: boolean;
/** @internal */
public readonly optimizeBRepProcessing: boolean;
/** @internal */
public readonly useLargerTiles: boolean;
/** @internal */
public readonly maximumLevelsToSkip: number;
/** @internal */
public readonly mobileRealityTileMinToleranceRatio: number;
/** @internal */
public readonly tileTreeExpirationTime: BeDuration;
/** @internal */
public readonly tileExpirationTime: BeDuration;
/** @internal */
public readonly contextPreloadParentDepth: number;
/** @internal */
public readonly contextPreloadParentSkip: number;
/** @beta */
public readonly cesiumIonKey?: string;
private readonly _removeIModelConnectionOnCloseListener: () => void;
private _totalElided = 0;
private _rpcInitialized = false;
private _nextPruneTime: BeTimePoint;
private _nextPurgeTime: BeTimePoint;
private _tileTreePropsRequests: TileTreePropsRequest[] = [];
private _cleanup?: () => void;
private readonly _lruList = new LRUTileList();
private _maxTotalTileContentBytes?: number;
private _gpuMemoryLimit: GpuMemoryLimit = "none";
private readonly _isMobile: boolean;
private readonly _cloudStorage?: FrontendStorage;
/** 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
*/
public static async create(props?: TileAdmin.Props): Promise<TileAdmin> {
const rpcConcurrency = IpcApp.isValid ? (await IpcApp.appFunctionIpc.queryConcurrency("cpu")) : undefined;
const isMobile = ProcessDetector.isMobileBrowser;
return new TileAdmin(isMobile, rpcConcurrency, props);
}
/** @internal */
public get emptyTileUserSet(): ReadonlyTileUserSet { return UniqueTileUserSets.emptySet; }
/** Returns basic statistics about the TileAdmin's current state. */
public get statistics(): TileAdmin.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. */
public resetStatistics(): void {
this.channels.resetStatistics();
this._totalElided = 0;
}
/** Exposed as public strictly for tests.
* @internal
*/
public constructor(isMobile: boolean, rpcConcurrency: number | undefined, options?: TileAdmin.Props) {
this._isMobile = isMobile;
if (undefined === options)
options = {};
this.channels = new 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 ?? defaultTileOptions.enableInstancing;
this.edgeOptions = {
type: false === options.enableIndexedEdges ? "non-indexed" : "compact",
smooth: options.generateAllPolyfaceEdges ?? true,
};
this.enableImprovedElision = options.enableImprovedElision ?? defaultTileOptions.enableImprovedElision;
this.enableFrontendScheduleScripts = options.enableFrontendScheduleScripts ?? false;
this.decodeImdlInWorker = options.decodeImdlInWorker ?? true;
this.ignoreAreaPatterns = options.ignoreAreaPatterns ?? defaultTileOptions.ignoreAreaPatterns;
this.enableExternalTextures = options.enableExternalTextures ?? defaultTileOptions.enableExternalTextures;
this.disableMagnification = options.disableMagnification ?? 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 ?? defaultTileOptions.alwaysSubdivideIncompleteTiles;
this.maximumMajorTileFormatVersion = options.maximumMajorTileFormatVersion ?? defaultTileOptions.maximumMajorTileFormatVersion;
this.useProjectExtents = options.useProjectExtents ?? defaultTileOptions.useProjectExtents;
this.optimizeBRepProcessing = options.optimizeBRepProcessing ?? defaultTileOptions.optimizeBRepProcessing;
this.useLargerTiles = options.useLargerTiles ?? 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: GpuMemoryLimit | undefined;
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: number, min: number, max: number): BeDuration => {
seconds = Math.min(seconds, max);
seconds = Math.max(seconds, min);
return 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 = BeTimePoint.now();
this._nextPruneTime = now.plus(this.tileExpirationTime);
this._nextPurgeTime = now.plus(this.tileTreeExpirationTime);
this._removeIModelConnectionOnCloseListener = 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 && user.view.isSpatialView())
user.invalidateController();
}),
];
this._cleanup = () => {
removals.forEach((removal) => removal());
};
}
private _tileStorage?: TileStorage;
private async getTileStorage(): Promise<TileStorage> {
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 TileStorage(this._cloudStorage);
return this._tileStorage;
}
const fetchStorage = new FetchCloudStorage();
this._tileStorage = new TileStorage(fetchStorage);
return this._tileStorage;
}
/** @internal */
public 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]]
*/
public getMaximumMajorTileFormatVersion(formatVersion?: number): number {
return 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.
*/
public get defaultTileSizeModifier() { return this._defaultTileSizeModifier; }
public set defaultTileSizeModifier(modifier: number) {
if (modifier !== this._defaultTileSizeModifier && modifier > 0 && !Number.isNaN(modifier)) {
this._defaultTileSizeModifier = modifier;
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.
*/
public get totalTileContentBytes(): number {
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.
*/
public get maxTotalTileContentBytes(): number | undefined {
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.
*/
public get gpuMemoryLimit(): GpuMemoryLimit {
return this._gpuMemoryLimit;
}
public set gpuMemoryLimit(limit: GpuMemoryLimit) {
if (limit === this.gpuMemoryLimit)
return;
let maxBytes: number | undefined;
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
*/
public get isPreloadingAllowed(): boolean {
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
*/
public process(): void {
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
*/
public get unselectedLoadedTiles(): Iterable<Tile> {
return this._lruList.unselectedTiles;
}
/** Iterate over the tiles that have content loaded and are in use by any [[TileUser]].
* @alpha
*/
public get selectedLoadedTiles(): Iterable<Tile> {
return this._lruList.selectedTiles;
}
/** Returns the number of pending and active requests associated with the specified viewport. */
public getNumRequestsForViewport(vp: Viewport): number {
return this.getNumRequestsForUser(vp);
}
/** Returns the number of pending and active requests associated with the specified user. */
public getNumRequestsForUser(user: TileUser): number {
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
*/
public getRequestsForUser(user: TileUser): Set<Tile> | undefined {
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
*/
public requestTiles(user: TileUser, tiles: Set<Tile>): void {
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
*/
public getTilesForUser(user: TileUser): SelectedAndReadyTiles | undefined {
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
*/
public addTilesForUser(user: TileUser, selected: Tile[], ready: Set<Tile>, touched: Set<Tile>): void {
// "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<Tile>(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
*/
public addExternalTilesForUser(user: TileUser, statistics: ExternalTileStatistics): void {
const entry = this.getTilesForUser(user);
if (!entry) {
this._selectedAndReady.set(user, { ready: new Set<Tile>(), selected: new Set<Tile>(), 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. */
public clearTilesForUser(user: TileUser): void {
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.
*/
public forgetUser(user: TileUser): void {
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.
*/
public registerUser(user: TileUser): void {
this._users.add(user);
}
/** Iterable over all TileUsers registered with TileAdmin. This may include [[OffScreenViewport]]s.
* @alpha
*/
public get tileUsers(): Iterable<TileUser> {
return this._users;
}
/** @internal */
public invalidateAllScenes() {
for (const user of this.tileUsers)
if (user instanceof Viewport)
user.invalidateScene();
}
/** @internal */
public onShutDown(): void {
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.dispose();
}
/** Returns the union of the input set and the input TileUser, to be associated with a [[TileRequest]].
* @internal
*/
public getTileUserSetForRequest(user: TileUser, users?: ReadonlyTileUserSet): ReadonlyTileUserSet {
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
*/
public markTileUsed(marker: TileUsageMarker, user: TileUser): void {
let set = this._tileUsagePerUser.get(user);
if (!set)
this._tileUsagePerUser.set(user, set = new Set<TileUsageMarker>());
set.add(marker);
}
/** Returns true if the Tile is currently in use by any TileUser.
* @see [[markTileUsed]].
* @internal
*/
public isTileInUse(marker: TileUsageMarker): boolean {
// eslint-disable-next-line @typescript-eslint/naming-convention
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
*/
public clearUsageForUser(user: TileUser): void {
this._tileUsagePerUser.delete(user);
}
/** @internal */
public async requestTileTreeProps(iModel: IModelConnection, treeId: string): Promise<IModelTileTreeProps> {
this.initializeRpc();
const requests = this._tileTreePropsRequests;
return new Promise<IModelTileTreeProps>((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
*/
public async purgeTileTrees(iModel: IModelConnection, modelIds: Id64Array | undefined): Promise<void> {
this.initializeRpc();
return IModelTileRpcInterface.getClient().purgeTileTrees(iModel.getRpcProps(), modelIds);
}
/** @internal */
public async requestCachedTileContent(tile: { iModelTree: IModelTileTree, contentId: string }): Promise<Uint8Array | undefined> {
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 */
public async generateTileContent(tile: { iModelTree: IModelTileTree, contentId: string, request?: { isCanceled: boolean } }): Promise<Uint8Array> {
this.initializeRpc();
const props = this.getTileRequestProps(tile);
const retrieveMethod = await 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 === TileContentSource.ExternalCache) {
const tileContent = await this.requestCachedTileContent(tile);
if (tileContent === undefined)
throw new IModelError(IModelStatus.NoContent, "Failed to fetch generated tile from external cache");
return tileContent;
} else if (retrieveMethod === TileContentSource.Backend) {
return IModelTileRpcInterface.getClient().retrieveTileContent(props.tokenProps, this.getTileRequestProps(tile));
}
throw new BackendError(BentleyStatus.ERROR, "", "Invalid response from RPC backend");
}
/** @internal */
public getTileRequestProps(tile: { iModelTree: IModelTileTree, contentId: string }) {
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
*/
public async requestElementGraphics(iModel: IModelConnection, requestProps: ElementGraphicsRequestProps): Promise<Uint8Array | undefined> {
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 = IModelTileRpcInterface.getClient();
return intfc.requestElementGraphics(iModel.getRpcProps(), requestProps);
}
/** Obtain information about the version/format of the tiles supplied by the backend. */
public async queryVersionInfo(): Promise<Readonly<TileVersionInfo>> {
if (!this._versionInfo) {
this.initializeRpc();
this._versionInfo = await IModelTileRpcInterface.getClient().queryVersionInfo();
}
return this._versionInfo;
}
/** @internal */
public onTilesElided(numElided: number) {
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
*/
public onTileContentLoaded(tile: Tile): void {
// 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
*/
public onTileContentDisposed(tile: Tile): void {
this._lruList.drop(tile);
}
/** @internal */
public terminateTileTreePropsRequest(request: TileTreePropsRequest): void {
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. */
public readonly onTileLoad = new BeEvent<(tile: Tile) => void>();
/** Event raised when a request to load a tile tree completes. */
public readonly onTileTreeLoad = new BeEvent<(tileTree: TileTreeOwner) => void>();
/** Event raised when a request to load a tile's child tiles completes. */
public readonly onTileChildrenLoad = new BeEvent<(parentTile: Tile) => void>();
/** Subscribe to [[onTileLoad]], [[onTileTreeLoad]], and [[onTileChildrenLoad]]. */
public addLoadListener(callback: (imodel: IModelConnection) => void): () => void {
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
*/
public getScriptInfoForTreeId(modelId: Id64String, script: RenderSchedule.ScriptReference | undefined): { timeline?: RenderSchedule.ModelTimeline, animationId?: Id64String } | undefined {
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 (!Id64.isValidId64(script.sourceId) || (this.enableFrontendScheduleScripts && !timeline.omitsElementIds))
return { timeline };
return { animationId: script.sourceId };
}
private dispatchTileTreePropsRequests(): void {
for (let i = 0; i < this._maxActiveTileTreePropsRequests && i < this._tileTreePropsRequests.length; i++)
this._tileTreePropsRequests[i].dispatch();
}
private processQueue(): void {
// 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 */
public freeMemory(): void {
if (undefined !== this._maxTotalTileContentBytes)
this._lruList.freeMemory(this._maxTotalTileContentBytes);
}
private pruneAndPurge(): void {
const now = 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 DisclosedTileTreeSet();
const treesByIModel = needPurge ? new Map<IModelConnection, Set<TileTree>>() : 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<TileTree>());
}
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<TileTree>());
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);
}
}
private processRequests(user: TileUser, tiles: Set<Tile>): void {
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 (TileLoadStatus.NotLoaded === tile.loadStatus) {
const request = new TileRequest(tile, user);
tile.request = request;
assert(this.channels.has(request.channel));
request.channel.append(request);
}
} else {
const req = tile.request;
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);
assert(0 < req.users.length);
}
}
}
}
// NB: This does *not* remove from this._users - a viewport could later be reused with a different IModelConnection.
private onUserIModelClosed(user: TileUser): void {
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);
}
}
private onIModelClosed(iModel: IModelConnection): void {
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);
}
private initializeRpc(): void {
// 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;
RpcOperation.lookup(IModelTileRpcInterface, "requestTileTreeProps").policy.retryInterval = () => retryInterval;
const policy = RpcOperation.lookup(IModelTileRpcInterface, "generateTileContent").policy;
policy.retryInterval = () => retryInterval;
policy.allowResponseCaching = () => RpcResponseCacheControl.Immutable; // eslint-disable-line deprecation/deprecation
}
}
/** @public */
export namespace TileAdmin { // eslint-disable-line no-redeclare
/** Statistics regarding the current and cumulative state of the [[TileAdmin]]. Useful for monitoring performance and diagnosing problems.
* @public
*/
export interface Statistics {
/** The number of requests in the queue which have not yet been dispatched. */
numPendingRequests: number;
/** The number of requests which have been dispatched but not yet completed. */
numActiveRequests: number;
/** The number of requests canceled during the most recent update. */
numCanceled: number;
/** The total number of completed requests during this session. */
totalCompletedRequests: number;
/** The total number of failed requests during this session. */
totalFailedRequests: number;
/** The total number of timed-out requests during this session. */
totalTimedOutRequests: number;
/** The total number of completed requests during this session which produced an empty tile. These tiles also contribute to totalCompletedRequests, but not to totalUndisplayableTiles. */
totalEmptyTiles: number;
/** The total number of completed requests during this session which produced an undisplayable tile. These tiles also contribute to totalCompletedRequests, but not to totalEmptyTiles. */
totalUndisplayableTiles: number;
/** The total number of tiles whose contents were not requested during this session because their volumes were determined to be empty. */
totalElidedTiles: number;
/** The total number of tiles whose contents were not found in cloud storage cache and therefore resulted in a backend request to generate the tile content. */
totalCacheMisses: number;
/** The total number of tiles for which content requests were dispatched. */
totalDispatchedRequests: number;
/** The total number of tiles for which content requests were dispatched and then canceled on the backend before completion. */
totalAbortedRequests: number;
/** The number of in-flight IModelTileTreeProps requests. */
numActiveTileTreePropsRequests: number;
/** The number of pending IModelTileTreeProps requests. */
numPendingTileTreePropsRequests: number;
/** See [[TileContentDecodingStatistics]].
* @beta
*/
decoding: TileContentDecodingStatistics;
}
/** Describes the configuration of the [[TileAdmin]].
* @see [[TileAdmin.create]] to specify the configuration at [[IModelApp.startup]] time.
* @note Many of these settings serve as "feature gates" introduced alongside new, potentially experimental features.
* Over time, as a feature is tested and proven, their relevance wanes, and the feature becomes enabled by default.
* Such properties should be flagged as `beta` and removed or rendered non-operational once the feature itself is considered
* stable.