feat(studyPrefetcher): Study Prefetcher (#4206)

This commit is contained in:
Leonardo Campos authored and GitHub committed 2024-06-28 08:58:39 -04:00
1 parent 257cc477cd
commit 2048b19484
24 files changed
+992 -98

No files matched your search

@@ -12,7 +12,14 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
experimentalStudyBrowserSort: false,
strictZSpacingForVolumeViewport: true,
studyPrefetcher: {
enabled: true,
displaySetsCount: 2,
maxNumPrefetchRequests: 10,
order: 'closest',
},
defaultDataSourceName: 'dicomweb',
dataSources: [
{
@@ -47,4 +54,7 @@ window.config = {
},
},
],
httpErrorHandler: error => {
console.warn(`HTTP Error Handler (status: ${error.status})`, error);
},
};
+2
View File
@@ -20,6 +20,7 @@ import {
CustomizationService,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
// utils,
} from '@ohif/core';
@@ -71,6 +72,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
PanelService.REGISTRATION,
WorkflowStepsService.REGISTRATION,
StateSyncService.REGISTRATION,
[StudyPrefetcherService.REGISTRATION, appConfig.studyPrefetcher],
]);
errorHandler.getHTTPErrorHandler = () => {
@@ -134,6 +134,12 @@ export default class ExtensionManager extends PubSubService {
return [...this.registeredExtensionIds];
}
private getUniqueServicesList(servicesManager: AppTypes.ServicesManager) {
// Make sure only one service instance is returned because almost all services are
// registered with different keys (eg: StudyPrefetcherService and studyPrefetcherService)
return Array.from(new Set(Object.values(servicesManager.services)));
}
/**
* Calls all the services and extension on mode enters.
* The service onModeEnter is called first
@@ -148,11 +154,12 @@ export default class ExtensionManager extends PubSubService {
_hotkeysManager,
_extensionLifeCycleHooks,
} = this;
const services = this.getUniqueServicesList(_servicesManager);
// The onModeEnter of the service must occur BEFORE the extension
// onModeEnter in order to reset the state to a standard state
// before the extension restores and cached data.
for (const service of Object.values(_servicesManager.services)) {
for (const service of services) {
service?.onModeEnter?.();
}
@@ -172,6 +179,7 @@ export default class ExtensionManager extends PubSubService {
public onModeExit(): void {
const { registeredExtensionIds, _servicesManager, _commandsManager, _extensionLifeCycleHooks } =
this;
const services = this.getUniqueServicesList(_servicesManager);
registeredExtensionIds.forEach(extensionId => {
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
@@ -186,7 +194,7 @@ export default class ExtensionManager extends PubSubService {
// The service onModeExit calls must occur after the extension ones
// so that extension ones can store/restore data.
for (const service of Object.values(_servicesManager.services)) {
for (const service of services) {
try {
service?.onModeExit?.();
} catch (e) {
+2 -1
View File
@@ -1,7 +1,7 @@
import * as OHIF from './index';
describe('Top level exports', () => {
test.only('have not changed', () => {
test('have not changed', () => {
const expectedExports = [
'MODULE_TYPES',
//
@@ -47,6 +47,7 @@ describe('Top level exports', () => {
'PubSubService',
'PanelService',
'WorkflowStepsService',
'StudyPrefetcherService',
'useToolbar',
].sort();
+3
View File
@@ -33,6 +33,7 @@ import {
StateSyncService,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
} from './services';
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
@@ -85,6 +86,7 @@ const OHIF = {
PanelService,
useToolbar,
WorkflowStepsService,
StudyPrefetcherService,
};
export {
@@ -130,6 +132,7 @@ export {
Types,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
useToolbar,
};
@@ -0,0 +1,686 @@
import { PubSubService } from '../_shared/pubSubServiceInterface';
import { ExtensionManager } from '../../extensions';
import ServicesManager from '../ServicesManager';
import ViewportGridService from '../ViewportGridService';
import { DisplaySet } from '../../types';
const IMAGE_REQUEST_TYPE = 'prefetch';
export const EVENTS = {
SERVICE_STARTED: 'event::studyPrefetcherService:started',
SERVICE_STOPPED: 'event::studyPrefetcherService:stopped',
DISPLAYSET_LOAD_PROGRESS: 'event::studyPrefetcherService:displaySetLoadProgress',
DISPLAYSET_LOAD_COMPLETE: 'event::studyPrefetcherService:displaySetLoadComplete',
};
/**
* Order used for prefetching display set
*/
enum StudyPrefetchOrder {
closest = 'closest',
downward = 'downward',
upward = 'upward',
}
/**
* Study Prefetcher configuration
*/
type StudyPrefetcherConfig = {
/* Enable/disable study prefetching service */
enabled: boolean;
/* Number of displaysets to be prefetched */
displaySetsCount: number;
/**
* Max number of concurrent prefetch requests
* High numbers may impact on the time to load a new dropped series because
* the browser will be busy with all prefetching requests. As soon as the
* prefetch requests get fulfilled the new ones from the new dropped series
* are sent to the server.
*
* TODO: abort all prefetch requests when a new series is loaded on a viewport.
* (need to add support for `AbortController` on Cornerstone)
* */
maxNumPrefetchRequests: number;
/* Display sets prefetching order (closest, downward and upward) */
order: StudyPrefetchOrder;
};
type DisplaySetLoadingState = {
displaySetInstanceUID: string;
numInstances: number;
pendingImageIds: Set<string>;
loadedImageIds: Set<string>;
failedImageIds: Set<string>;
loadingProgress: number;
};
type ImageRequest = {
displaySetInstanceUID: string;
imageId: string;
aborted: boolean;
};
type PubSubServiceSubscription = { unsubscribe: () => any };
interface ICache {
isImageCached(imageId: string): boolean;
}
interface IImageLoadPoolManager {
addRequest (
requestFn: () => Promise<any>,
type: string,
additionalDetails: Record<string, unknown>,
priority?: number
);
clearRequestStack(type: string): void;
}
interface IImageLoader {
loadAndCacheImage(imageId: string, options: any): Promise<any>;
}
type EventSubscription = {
unsubscribe: () => void
};
interface IImageLoadEventsManager {
addEventListeners(
onImageLoaded: (evt: any) => void,
onImageLoadFailed: (evt: any) => void
): EventSubscription[];
}
class StudyPrefetcherService extends PubSubService {
private _extensionManager: ExtensionManager;
private _servicesManager: ServicesManager;
private _subscriptions: PubSubServiceSubscription[];
private _activeDisplaySetsInstanceUIDs: string[] = [];
private _pendingRequests: ImageRequest[] = [];
private _inflightRequests = new Map<string, ImageRequest>();
private _isRunning = false;
private _displaySetLoadingStates = new Map<string, DisplaySetLoadingState>();
private _imageIdsToDisplaySetsMap = new Map<string, Set<string>>();
private config: StudyPrefetcherConfig = {
/* Enable/disable study prefetching service */
enabled: false,
/* Number of displaysets to be prefetched */
displaySetsCount: 1,
/**
* Max number of concurrent prefetch requests
* High numbers may impact on the time to load a new dropped series because
* the browser will be busy with all prefetching requests. As soon as the
* prefetch requests get fulfilled the new ones from the new dropped series
* are sent to the server.
*
* TODO: abort all prefetch requests when a new series is loaded on a viewport.
* (need to add support for `AbortController` on Cornerstone)
* */
maxNumPrefetchRequests: 10,
/* Display sets prefetching order (closest, downward and upward) */
order: StudyPrefetchOrder.downward,
};
// Properties set by Cornerstone extension (initStudyPrefetcherService)
public requestType: string = IMAGE_REQUEST_TYPE;
public cache: ICache;
public imageLoadPoolManager: IImageLoadPoolManager;
public imageLoader: IImageLoader;
public imageLoadEventsManager: IImageLoadEventsManager;
public static REGISTRATION = {
name: 'studyPrefetcherService',
altName: 'StudyPrefetcherService',
create: ({ configuration, servicesManager, extensionManager }): StudyPrefetcherService => {
return new StudyPrefetcherService({
servicesManager,
extensionManager,
configuration,
});
},
};
constructor({
servicesManager,
extensionManager,
configuration,
}: {
servicesManager: ServicesManager;
extensionManager: ExtensionManager;
configuration: StudyPrefetcherConfig;
}) {
super(EVENTS);
this._servicesManager = servicesManager;
this._extensionManager = extensionManager;
this._subscriptions = [];
Object.assign(this.config, configuration);
}
public onModeEnter(): void {
this._addEventListeners();
}
/**
* The onModeExit returns the service to the initial state.
*/
public onModeExit(): void {
this._removeEventListeners();
this._stopPrefetching();
}
private _addImageLoadingEventsListeners() {
const fnOnImageLoadCompleted = (imageId: string) => {
// `sendNextRequests` must be called after image loaded/failed events
// to make sure prefetch requests shall be sent as soon as the active
// displaySets (active viewport) are loaded.
//
// PS: active display sets are not loaded by this service and that is why
// the requests shall not be in the inflight queue.
if (!this._inflightRequests.get(imageId)) {
this._sendNextRequests();
}
}
const fnImageLoadedEventListener = (evt) => {
const { image } = evt.detail;
const { imageId } = image;
this._moveImageIdToLoadedSet(imageId);
fnOnImageLoadCompleted(imageId);
}
const fnImageLoadFailedEventListener = (evt) => {
const { imageId } = evt.detail;
this._moveImageIdToFailedSet(imageId);
fnOnImageLoadCompleted(imageId);
}
return this.imageLoadEventsManager.addEventListeners(
fnImageLoadedEventListener,
fnImageLoadFailedEventListener
);
}
private _addServicesListeners() {
const { displaySetService, viewportGridService } = this._servicesManager.services;
// Restart the prefetcher after any change to the displaySets
// (eg: sorting the displaySets on StudyBrowser)
const displaySetsChangedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_CHANGED,
() => this._syncWithActiveViewport({ forceRestart: true })
);
// Loads new datasets when making a new viewport active
const viewportGridActiveViewportIdSubscription = viewportGridService.subscribe(
ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
({ viewportId }) => this._syncWithActiveViewport({ activeViewportId: viewportId })
);
// Continue loading datasets after changing the layout (eg: from 1x1 to 2x1)
const viewportGridLayoutChangedSubscription = viewportGridService.subscribe(
ViewportGridService.EVENTS.LAYOUT_CHANGED,
() => this._syncWithActiveViewport()
);
// Loads new datasets after loading a new display set on a viewport
const viewportGridStateChangedSubscription = viewportGridService.subscribe(
ViewportGridService.EVENTS.GRID_STATE_CHANGED,
() => this._syncWithActiveViewport()
);
// Loads the first datasets right after opening the viewer
const viewportGridViewportreadySubscription = viewportGridService.subscribe(
ViewportGridService.EVENTS.VIEWPORTS_READY,
() => {
this._syncWithActiveViewport();
this._startPrefetching();
}
);
return [
displaySetsChangedSubscription,
viewportGridActiveViewportIdSubscription,
viewportGridLayoutChangedSubscription,
viewportGridStateChangedSubscription,
viewportGridViewportreadySubscription
];
}
private _addEventListeners() {
const imageLoadingEventsSubscriptions = this._addImageLoadingEventsListeners();
const servicesSubscriptions = this._addServicesListeners();
this._subscriptions.push(...imageLoadingEventsSubscriptions);
this._subscriptions.push(...servicesSubscriptions);
}
private _removeEventListeners() {
this._subscriptions.forEach(subscription => subscription.unsubscribe());
this._subscriptions = [];
}
private _syncWithActiveViewport(
{
activeViewportId,
forceRestart
}:
{
activeViewportId?: string,
forceRestart?: boolean
} = {}) {
const { viewportGridService } = this._servicesManager.services;
const viewportGridServiceState = viewportGridService.getState();
const { viewports } = viewportGridServiceState;
activeViewportId = activeViewportId ?? viewportGridServiceState.activeViewportId;
// If may be null when the viewer is loaded
if (!activeViewportId) {
return;
}
const activeViewport = viewports.get(activeViewportId);
const displaySetUpdated = this._setActiveDisplaySetsUIDs(activeViewport.displaySetInstanceUIDs);
if (forceRestart || displaySetUpdated) {
this._restartPrefetching();
}
}
private _setActiveDisplaySetsUIDs(newActiveDisplaySetInstanceUIDs: string[]): boolean {
const sameDisplaySets =
newActiveDisplaySetInstanceUIDs.length === this._activeDisplaySetsInstanceUIDs.length &&
newActiveDisplaySetInstanceUIDs.every(uid =>
this._activeDisplaySetsInstanceUIDs.includes(uid)
);
if (sameDisplaySets) {
return false;
}
this._activeDisplaySetsInstanceUIDs = [...newActiveDisplaySetInstanceUIDs];
this._restartPrefetching();
return true;
}
private _areActiveDisplaySetsLoaded() {
const { _activeDisplaySetsInstanceUIDs: displaySetsInstanceUIDs } = this;
return displaySetsInstanceUIDs.length && displaySetsInstanceUIDs.every(displaySetsInstanceUID =>
this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1);
}
private _getClosestDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
const sortedDisplaySets = [];
let previousIndex = activeDisplaySetIndex - 1;
let nextIndex = activeDisplaySetIndex + 1;
while (previousIndex >= 0 || nextIndex < displaySets.length) {
if (previousIndex >= 0) {
sortedDisplaySets.push(displaySets[previousIndex]);
previousIndex--;
}
if (nextIndex < displaySets.length) {
sortedDisplaySets.push(displaySets[nextIndex]);
nextIndex++;
}
}
return sortedDisplaySets;
}
private _getDownwardDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
const sortedDisplaySets = [];
for (let i = activeDisplaySetIndex + 1; i < displaySets.length; i++) {
sortedDisplaySets.push(displaySets[i]);
}
return sortedDisplaySets;
}
private _getUpwardDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
const sortedDisplaySets = [];
for (let i = activeDisplaySetIndex - 1; i >= 0 && i !== activeDisplaySetIndex; i--) {
sortedDisplaySets.push(displaySets[i]);
}
return sortedDisplaySets;
}
private _getSortedDisplaySetsToPrefetch(displaySets: DisplaySet[]): DisplaySet[] {
if (!this._activeDisplaySetsInstanceUIDs?.length) {
return [];
}
const { displaySetsCount } = this.config;
const activeDisplaySetsInstanceUIDs = this._activeDisplaySetsInstanceUIDs;
const [activeDisplaySetUID] = activeDisplaySetsInstanceUIDs;
const activeDisplaySetIndex = displaySets.findIndex(ds => ds.displaySetInstanceUID === activeDisplaySetUID);
const getDisplaySetsFunctionsMap = {
[StudyPrefetchOrder.closest]: this._getClosestDisplaySets,
[StudyPrefetchOrder.downward]: this._getDownwardDisplaySets,
[StudyPrefetchOrder.upward]: this._getUpwardDisplaySets,
};
const { order } = this.config;
const fnGetDisplaySets = getDisplaySetsFunctionsMap[order];
if (!fnGetDisplaySets) {
throw new Error(`Invalid order (${order})`);
}
// Creates a `Set` to look for UIDs in O(1) instead of O(n)
const uidsSet = new Set(activeDisplaySetsInstanceUIDs);
// Remove any active displaySet that may still be in the activeDisplaySetsInstanceUIDs.
// That may happen when activeDisplaySetsInstanceUIDs has more than one element.
return fnGetDisplaySets.call(this, displaySets, activeDisplaySetIndex).filter(
ds => !uidsSet.has(ds.displaySetInstanceUID)
).slice(0, displaySetsCount);
}
private _getDisplaySets() {
const { displaySetService } = this._servicesManager.services;
const displaySets = [...displaySetService.getActiveDisplaySets()];
let displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
return { displaySets, displaySetsToPrefetch };
}
private _updateImageIdsDisplaySetMap(
displaySetInstanceUID: string,
imageIds: string[]
): void {
for (const imageId of imageIds) {
let displaySetsInstanceUIDsMap = this._imageIdsToDisplaySetsMap.get(imageId);
if (!displaySetsInstanceUIDsMap) {
displaySetsInstanceUIDsMap = new Set();
this._imageIdsToDisplaySetsMap.set(imageId, displaySetsInstanceUIDsMap);
}
displaySetsInstanceUIDsMap.add(displaySetInstanceUID);
}
}
private _getImageIdsForDisplaySet(displaySet: DisplaySet): string[] {
const dataSource = this._extensionManager.getActiveDataSource()[0];
return dataSource.getImageIdsForDisplaySet(displaySet);
}
private _updateDisplaySetLoadingProgress(displaySetLoadingState: DisplaySetLoadingState) {
const { numInstances, loadedImageIds, failedImageIds } = displaySetLoadingState;
const loadingProgress = (loadedImageIds.size + failedImageIds.size) / numInstances;
displaySetLoadingState.loadingProgress = loadingProgress;
}
private _addDisplaySetLoadingState(displaySet: DisplaySet): void {
const { displaySetInstanceUID } = displaySet;
const imageIds = this._getImageIdsForDisplaySet(displaySet);
let displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
if (displaySetLoadingState) {
return;
}
const pendingImageIds = new Set<string>(imageIds);
const loadedImageIds = new Set<string>();
// Needs to check which image is already loaded to update the progress properly
// because some images may already be loaded (thumbnails and viewports).
for (const imageId of imageIds) {
if (this.cache.isImageCached(imageId)) {
loadedImageIds.add(imageId);
} else {
pendingImageIds.add(imageId);
}
}
displaySetLoadingState = {
displaySetInstanceUID,
numInstances: imageIds.length,
pendingImageIds,
loadedImageIds,
failedImageIds: new Set(),
loadingProgress: 0
};
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
this._displaySetLoadingStates.set(displaySetInstanceUID, displaySetLoadingState);
this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds)
// Notify the UI that something is already loaded (eg: update StudyBrowser)
if (loadedImageIds.size) {
this._triggerDisplaySetEvents(displaySetInstanceUID);
}
}
private _loadDisplaySets() {
const { displaySets, displaySetsToPrefetch } = this._getDisplaySets();
displaySets.forEach(displaySet => this._addDisplaySetLoadingState(displaySet));
displaySetsToPrefetch.forEach(displaySet => this._enqueueDisplaySetImagesRequests(displaySet));
}
private _moveImageIdToLoadedSet(imageId: string): boolean {
const displaySetsInstanceUIDs = this._imageIdsToDisplaySetsMap.get(imageId);
if (!displaySetsInstanceUIDs) {
return;
}
for (const displaySetInstanceUID of Array.from(displaySetsInstanceUIDs.values())) {
const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
const { pendingImageIds, loadedImageIds } = displaySetLoadingState;
pendingImageIds.delete(imageId);
loadedImageIds.add(imageId);
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
this._triggerDisplaySetEvents(displaySetInstanceUID);
}
return true;
}
private _moveImageIdToFailedSet(imageId: string): boolean {
const displaySetsInstanceUIDs = this._imageIdsToDisplaySetsMap.get(imageId);
if (!displaySetsInstanceUIDs) {
return;
}
for (const displaySetInstanceUID of Array.from(displaySetsInstanceUIDs.values())) {
const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
const { pendingImageIds, failedImageIds } = displaySetLoadingState;
pendingImageIds.delete(imageId);
failedImageIds.add(imageId);
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
this._triggerDisplaySetEvents(displaySetInstanceUID);
}
return true;
}
private _triggerDisplaySetEvents(displaySetInstanceUID: string) {
const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID);
const { loadingProgress, numInstances } = displaySetLoadingState;
this._broadcastEvent(this.EVENTS.DISPLAYSET_LOAD_PROGRESS, {
displaySetInstanceUID,
numInstances,
loadingProgress,
});
if (loadingProgress >= 1) {
this._broadcastEvent(this.EVENTS.DISPLAYSET_LOAD_COMPLETE, {
displaySetInstanceUID,
});
}
}
private _onImagePrefetchSuccess(imageRequest: ImageRequest) {
if (imageRequest.aborted) {
return;
}
const { imageId } = imageRequest;
this._inflightRequests.delete(imageId);
this._moveImageIdToLoadedSet(imageId);
// `sendNextRequests` must be called after removing the request from the inflight
// queue otherwise it shall not be able to send the request (maxNumPrefetchRequests)
this._sendNextRequests();
}
private _onImagePrefetchFailed(imageRequest, error) {
if (imageRequest.aborted) {
return;
}
console.warn(`An error ocurred when trying to load "${imageRequest.imageId}"`, error);
const { imageId } = imageRequest;
this._inflightRequests.delete(imageId);
this._moveImageIdToFailedSet(imageId);
// `sendNextRequests` must be called after removing the request from the inflight
// queue otherwise it shall not be able to send the request (maxNumPrefetchRequests)
this._sendNextRequests();
}
private async _sendNextRequests() {
// If the service has stopped with async requests in progress this method may
// get called again when each of those requests are fulfilled.
if (!this._isRunning) {
return;
}
// Does not send any prefetch request until the active display sets are loaded
if (!this._areActiveDisplaySetsLoaded()) {
return;
}
const { _pendingRequests: pendingRequests, _inflightRequests: inflightRequests } = this;
const { maxNumPrefetchRequests } = this.config;
if (!pendingRequests.length || inflightRequests.size >= maxNumPrefetchRequests) {
return;
}
const numImageRequests = Math.min(
pendingRequests.length,
maxNumPrefetchRequests - inflightRequests.size
);
const imageRequests = this._pendingRequests.splice(0, numImageRequests);
imageRequests.forEach(imageRequest => {
const { imageId } = imageRequest;
const options = {
priority: -5,
requestType: this.requestType,
additionalDetails: { imageId },
preScale: {
enabled: true,
},
};
this.imageLoadPoolManager.addRequest(
async () =>
this.imageLoader.loadAndCacheImage(imageId, options).then(
_image => this._onImagePrefetchSuccess(imageRequest),
error => this._onImagePrefetchFailed(imageRequest, error)
),
this.requestType,
{ imageId }
);
inflightRequests.set(imageId, imageRequest);
});
}
private _enqueueDisplaySetImagesRequests(displaySet: DisplaySet) {
const { displaySetInstanceUID } = displaySet;
const imageIds = this._getImageIdsForDisplaySet(displaySet);
imageIds.forEach(imageId => {
if (this.cache.isImageCached(imageId)) {
this._moveImageIdToLoadedSet(imageId);
return;
}
this._pendingRequests.push({
displaySetInstanceUID,
imageId,
aborted: false,
});
});
}
/**
* Start prefetching the display sets based on the active viewport and app configuration.
*/
private _startPrefetching(): void {
if (this._isRunning) {
return;
}
if (!this.config.enabled) {
console.log('StudyPrefetcher is not enabled');
return;
}
this._isRunning = true;
this._loadDisplaySets();
this._sendNextRequests();
this._broadcastEvent(this.EVENTS.SERVICE_STARTED, {});
}
/**
* Stop prefetching the display sets.
* All internal variables are cleared but activeDisplaySetsInstanceUIDs otherwise restart would not work.
*/
private _stopPrefetching(): void {
if (!this._isRunning) {
return;
}
this._isRunning = false;
// Mark all inflight requests as aborted before clearing the map.
this._inflightRequests.forEach(inflightRequest => (inflightRequest.aborted = true));
this._pendingRequests = [];
this._displaySetLoadingStates.clear();
this._imageIdsToDisplaySetsMap.clear();
this._inflightRequests.clear();
this.imageLoadPoolManager.clearRequestStack(IMAGE_REQUEST_TYPE);
this._broadcastEvent(this.EVENTS.SERVICE_STOPPED, {});
}
/**
* Restart prefetching in case it is already running.
*/
private _restartPrefetching(): void {
if (this._isRunning) {
this._stopPrefetching();
this._startPrefetching();
}
}
}
export { StudyPrefetcherService as default, StudyPrefetcherService };
@@ -0,0 +1,3 @@
import { StudyPrefetcherService } from './StudyPrefetcherService';
export { StudyPrefetcherService as default, StudyPrefetcherService };
@@ -105,8 +105,8 @@ class ViewportGridService extends PubSubService {
this.setDisplaySetsForViewports([props]);
}
public setDisplaySetsForViewports(props) {
this.serviceImplementation._setDisplaySetsForViewports(props);
public async setDisplaySetsForViewports(props) {
await this.serviceImplementation._setDisplaySetsForViewports(props);
const state = this.getState();
const viewports = [];
+2
View File
@@ -17,6 +17,7 @@ import CustomizationService from './CustomizationService';
import StateSyncService from './StateSyncService';
import PanelService from './PanelService';
import WorkflowStepsService from './WorkflowStepsService';
import StudyPrefetcherService from './StudyPrefetcherService';
import type Services from '../types/Services';
@@ -42,4 +43,5 @@ export {
UserAuthenticationService,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
};
+9
View File
@@ -14,6 +14,7 @@ import UserAuthenticationServiceType from '../services/UserAuthenticationService
import PanelServiceType from '../services/PanelService';
import UIDialogServiceType from '../services/UIDialogService';
import UIViewportDialogServiceType from '../services/UIViewportDialogService';
import StudyPrefetcherServiceType from '../services/StudyPrefetcherService';
import ServicesManagerType from '../services/ServicesManager';
import CommandsManagerType from '../classes/CommandsManager';
@@ -41,6 +42,7 @@ declare global {
export type UIDialogService = UIDialogServiceType;
export type UIViewportDialogService = UIViewportDialogServiceType;
export type PanelService = PanelServiceType;
export type StudyPrefetcherService = StudyPrefetcherServiceType;
export interface Managers {
servicesManager?: ServicesManager;
@@ -64,6 +66,7 @@ declare global {
uiDialogService?: UIDialogServiceType;
uiViewportDialogService?: UIViewportDialogServiceType;
panelService?: PanelServiceType;
studyPrefetcherService?: StudyPrefetcherServiceType;
}
export interface Config {
routerBasename?: string;
@@ -119,6 +122,12 @@ declare global {
onConfiguration?: (dicomWebConfig: any, options: any) => any;
dataSources?: any;
oidc?: any;
studyPrefetcher: {
enabled: boolean;
displaySetsCount: number;
maxNumPrefetchRequests: number;
order: 'closest' | 'downward' | 'upward';
}
}
export interface Test {
@@ -258,6 +258,35 @@ This will result in two panels, one with `dicomSeg.panel` and `tracked.measureme
:::
### Study Prefetcher
You can enable the study prefetcher so that OHIF loads the next/previous series/display sets
based on the proximity to the current series/display set. This can be useful to improve the user experience
```js
studyPrefetcher: {
/* Enable/disable study prefetching service (default: false) */
enabled: true,
/* Number of displaysets to be prefetched (default: 2)*/
displaySetCount: 2,
/**
* Max number of concurrent prefetch requests (default: 10)
* High numbers may impact on the time to load a new dropped series because
* the browser will be busy with all prefetching requests. As soon as the
* prefetch requests get fulfilled the new ones from the new dropped series
* are sent to the server.
*
* TODO: abort all prefetch requests when a new series is loaded on a viewport.
* (need to add support for `AbortController` on Cornerstone)
* */
maxNumPrefetchRequests: 10,
/* Display sets loading order (closest (deafult), downward or upward) */
order: 'closest',
},
```
### More on Accept Header Configuration
In the previous section we showed that you can modify the `acceptHeader`
configuration to request specific dicom transfer syntax. By default
@@ -102,7 +102,7 @@ There are various configurations available to customize the viewer. Each configu
OHIF v3 has a new configuration structure. The main difference is that the `servers` is renamed to `dataSources` and the configuration is now asynchronous. Datasources are more abstract and
far more capable than servers. Read more about dataSources [here](../platform/extensions/modules/data-source.md).
- `StudyPrefetcher` is not currently supported in OHIF v3.
- `StudyPrefetcher` is only available in OHIF v3.9 beta and will be available in the next stable 3.9 release.
- The `servers` object has been replaced with a `dataSources` array containing objects representing different data sources.
- The cornerstoneExtensionConfig property has been removed, you should use `customizationService` instead (you can read more [here](../platform/services/ui/customization-service.md))
- The maxConcurrentMetadataRequests property has been removed in favor of `maxNumRequests`
+2 -2
View File
@@ -33,7 +33,7 @@
"react-dom": "^18.3.1"
},
"dependencies": {
"@testing-library/react-hooks": "^3.2.1",
"@testing-library/react": "^13.1.0",
"browser-detect": "^0.2.28",
"classnames": "^2.3.2",
"d3-array": "3",
@@ -56,7 +56,7 @@
"react-modal": "3.11.2",
"react-outside-click-handler": "^1.3.0",
"react-select": "5.7.4",
"react-test-renderer": "^16.12.0",
"react-test-renderer": "^18.3.1",
"react-window": "^1.8.9",
"react-with-direction": "^1.3.1",
"swiper": "^8.4.2",
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="19" height="19" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="matrix(0.84873874,0,0,0.84873874,0.13586772,0.14249413)">
<ellipse cx="11" cy="4" rx="9" ry="3"/>
<path d="m 2,4 v 14 a 9,3 0 0 0 18,0 V 4" />
<path d="m 2,11 a 9,3 0 0 0 18,0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 474 B

@@ -9,6 +9,7 @@ import { ReactComponent as calendar } from './../../assets/icons/calendar.svg';
import { ReactComponent as cancel } from './../../assets/icons/cancel.svg';
import { ReactComponent as clipboard } from './../../assets/icons/clipboard.svg';
import { ReactComponent as close } from './../../assets/icons/closeIcon.svg';
import { ReactComponent as database } from './../../assets/icons/database.svg';
import { ReactComponent as dottedCircle } from './../../assets/icons/dotted-circle.svg';
import { ReactComponent as circledCheckmark } from './../../assets/icons/circled-checkmark.svg';
import { ReactComponent as chevronDown } from './../../assets/icons/chevron-down.svg';
@@ -214,6 +215,7 @@ const ICONS = {
cancel: cancel,
clipboard: clipboard,
close: close,
database: database,
'dotted-circle': dottedCircle,
'circled-checkmark': circledCheckmark,
'chevron-down': chevronDown,
@@ -17,6 +17,7 @@ const Thumbnail = ({
description,
seriesNumber,
numInstances,
loadingProgress,
countIcon,
messages,
dragData = {},
@@ -94,6 +95,17 @@ const Thumbnail = ({
/>
{` ${numInstances}`}
</div>
<div className="flex mr-2 last:mr-0">
{loadingProgress && loadingProgress < 1 && (
<>{Math.round(loadingProgress * 100)}%</>
)}
{loadingProgress && loadingProgress === 1 && (
<Icon
name={'database'}
className="w-3"
/>
)}
</div>
<DisplaySetMessageListTooltip
messages={messages}
id={`display-set-tooltip-${displaySetInstanceUID}`}
@@ -124,6 +136,7 @@ Thumbnail.propTypes = {
description: PropTypes.string.isRequired,
seriesNumber: StringNumber.isRequired,
numInstances: PropTypes.number.isRequired,
loadingProgress: PropTypes.number,
messages: PropTypes.object,
isActive: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
@@ -25,6 +25,7 @@ const ThumbnailList = ({
dragData,
seriesNumber,
numInstances,
loadingProgress,
modality,
componentType,
seriesDate,
@@ -66,6 +67,7 @@ const ThumbnailList = ({
description={description}
seriesNumber={seriesNumber}
numInstances={numInstances}
loadingProgress={loadingProgress}
countIcon={countIcon}
imageSrc={imageSrc}
imageAltText={imageAltText}
@@ -18,6 +18,7 @@ function ThumbnailTracked({
description,
seriesNumber,
numInstances,
loadingProgress,
countIcon,
messages,
dragData,
@@ -87,6 +88,7 @@ function ThumbnailTracked({
messages={messages}
numInstances={numInstances}
countIcon={countIcon}
loadingProgress={loadingProgress}
isActive={isActive}
onClick={onClick}
onDoubleClick={onDoubleClick}
@@ -114,6 +116,7 @@ ThumbnailTracked.propTypes = {
description: PropTypes.string.isRequired,
seriesNumber: StringNumber.isRequired,
numInstances: PropTypes.number.isRequired,
loadingProgress: PropTypes.number,
onClick: PropTypes.func.isRequired,
onDoubleClick: PropTypes.func.isRequired,
onClickUntrack: PropTypes.func.isRequired,
@@ -1,4 +1,5 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { act } from 'react';
import { renderHook } from '@testing-library/react';
import useSessionStorage from './useSessionStorage';
const SESSION_STORAGE_KEY = 'test';