All files / packages/core/src/requestPool requestPoolManager.ts

70% Statements 49/70
45.16% Branches 14/31
65% Functions 13/20
69.56% Lines 48/69

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                                                                                                                                        3x                                             3x   3x             3x 3x   3x             3x                                                 6x                                                                                 52x             52x 3x       52x   52x                       5x 20x 20x 30x                                               208x 208x   208x 260x 260x 208x 52x 52x 52x     52x 52x         52x 52x 52x 52x                                 260x 260x 52x 52x       208x       52x     52x     52x     52x   52x           52x         52x 52x                                     260x   401x   260x                                
import RequestType from '../enums/RequestType';
import { IImage } from '../types';
import { uuidv4 } from '../utilities';
 
type AdditionalDetails = {
  imageId?: string;
  volumeId?: string;
};
 
type RequestDetailsInterface = {
  requestFn: () => Promise<IImage | void>;
  type: RequestType;
  additionalDetails: AdditionalDetails;
};
 
type RequestPool = {
  [name in RequestType]: { [key: number]: RequestDetailsInterface[] };
};
 
/**
 * RequestPool manager class is a base class that manages the request pools.
 * It is used imageRetrievalPoolManager, and imageLoadPoolManager to retrieve and load images.
 * Previously requestPoolManager was used to manage the retrieval and loading and decoding
 * of the images in a way that new requests were sent after the image was both loaded and decoded
 * which was not performant since it was waiting for the image to be loaded and decoded before
 * sending the next request which is a network request and can be done in parallel.
 * Now, we use separate imageRetrievalPoolManager and imageLoadPoolManager
 * to improve performance and both are extending the RequestPoolManager class which
 * is a basic queueing pool.
 *
 * A new requestPool can be created by instantiating a new RequestPoolManager class.
 *
 * ```javascript
 * const requestPoolManager = new RequestPoolManager()
 * ```
 *
 * ## ImageLoadPoolManager
 *
 * You can use the imageLoadPoolManager to load images, by providing a `requestFn`
 * that returns a promise for the image. You can provide a `type` to specify the type of
 * request (interaction, thumbnail, prefetch), and you can provide additional details
 * that will be passed to the requestFn. Below is an example of a requestFn that loads
 * an image from an imageId:
 *
 * ```javascript
 *
 * const priority = -5
 * const requestType = RequestType.Interaction
 * const additionalDetails = { imageId }
 * const options = {
 *   targetBuffer: {
 *     type: 'Float32Array',
 *     offset: null,
 *     length: null,
 *   },
 *   preScale: {
 *      enabled: true,
 *    },
 * }
 *
 * imageLoadPoolManager.addRequest(
 *   loadAndCacheImage(imageId, options).then(() => { // set on viewport}),
 *   requestType,
 *   additionalDetails,
 *   priority
 * )
 * ```
 * ### ImageRetrievalPoolManager
 * You don't need to directly use the imageRetrievalPoolManager to load images
 * since the imageLoadPoolManager will automatically use it for retrieval. However,
 * maximum number of concurrent requests can be set by calling `setMaxConcurrentRequests`.
 */
class RequestPoolManager {
  private id: string;
  private awake: boolean;
  private requestPool: RequestPool;
  private numRequests = {
    interaction: 0,
    thumbnail: 0,
    prefetch: 0,
    compute: 0,
  };
  /* maximum number of requests of each type. */
  public maxNumRequests: {
    interaction: number;
    thumbnail: number;
    prefetch: number;
    compute: number;
  };
  /* A public property that is used to set the delay between requests. */
  public grabDelay: number;
  private timeoutHandle: number;
 
  /**
   * By default a request pool containing three priority groups, one for each
   * of the request types, is created. Maximum number of requests of each type
   * is set to 6.
   */
  constructor(id?: string) {
    this.id = id ? id : uuidv4();
 
    this.requestPool = {
      interaction: { 0: [] },
      thumbnail: { 0: [] },
      prefetch: { 0: [] },
      compute: { 0: [] },
    };
 
    this.grabDelay = 5;
    this.awake = false;
 
    this.numRequests = {
      interaction: 0,
      thumbnail: 0,
      prefetch: 0,
      compute: 0,
    };
 
    this.maxNumRequests = {
      interaction: 6,
      thumbnail: 6,
      prefetch: 5,
      // I believe there is a bug right now, where if there are two workers
      // and one wants to run a compute job 6 times and the limit is just 5, then
      // the other worker will never get a chance to run its compute job.
      // we should probably have a separate limit for compute jobs per worker
      // context as there is another layer of parallelism there. For this reason
      // I'm setting the limit to 1000 for now.
      compute: 1000,
    };
  }
 
  /**
   * This function sets the maximum number of requests for a given request type.
   * @param type - The type of request you want to set the max number
   * of requests for it can be either of interaction, prefetch, or thumbnail.
   * @param maxNumRequests - The maximum number of requests that can be
   * made at a time.
   */
  public setMaxSimultaneousRequests(
    type: RequestType,
    maxNumRequests: number
  ): void {
    this.maxNumRequests[type] = maxNumRequests;
  }
 
  /**
   * It returns the maximum number of requests of a given type that can be made
   * @param type - The type of request.
   * @returns The maximum number of requests of a given type.
   */
  public getMaxSimultaneousRequests(type: RequestType): number {
    return this.maxNumRequests[type];
  }
 
  /**
   * Stops further fetching of the requests, all the ongoing requests will still
   * be retrieved
   */
  public destroy(): void {
    if (this.timeoutHandle) {
      window.clearTimeout(this.timeoutHandle);
    }
  }
 
  /**
   * Adds the requests to the pool of requests.
   *
   * @param requestFn - A function that returns a promise which resolves in the image
   * @param type - Priority category, it can be either of interaction, prefetch,
   * or thumbnail.
   * @param additionalDetails - Additional details that requests can contain.
   * For instance the volumeId for the volume requests
   * @param priority - Priority number for each category of requests. Its default
   * value is priority 0. The lower the priority number, the higher the priority number
   *
   */
  public addRequest(
    requestFn: () => Promise<IImage | void>,
    type: RequestType,
    additionalDetails: Record<string, unknown>,
    priority = 0
  ): void {
    // Describe the request
    const requestDetails: RequestDetailsInterface = {
      requestFn,
      type,
      additionalDetails,
    };
 
    // Check if the priority group exists on the request type
    if (this.requestPool[type][priority] === undefined) {
      this.requestPool[type][priority] = [];
    }
 
    // Adding the request to the correct priority group of the request type
    this.requestPool[type][priority].push(requestDetails);
 
    this.startGrabbing();
  }
 
  /**
   * Filter the requestPoolManager's pool of request based on the result of
   * provided filter function. The provided filter function needs to return false or true
   *
   * @param filterFunction - The filter function for filtering of the requests to keep
   */
  public filterRequests(
    filterFunction: (requestDetails: RequestDetailsInterface) => boolean
  ): void {
    Object.keys(this.requestPool).forEach((type: string) => {
      const requestType = this.requestPool[type];
      Object.keys(requestType).forEach((priority) => {
        requestType[priority] = requestType[priority].filter(
          (requestDetails: RequestDetailsInterface) => {
            return filterFunction(requestDetails);
          }
        );
      });
    });
  }
 
  /**
   * Clears the requests specific to the provided type. For instance, the
   * pool of requests of type 'interaction' can be cleared via this function.
   *
   *
   * @param type - category of the request (either interaction, prefetch or thumbnail)
   */
  public clearRequestStack(type: string): void {
    if (!this.requestPool[type]) {
      throw new Error(`No category for the type ${type} found`);
    }
    this.requestPool[type] = { 0: [] };
  }
 
  private sendRequests(type) {
    const requestsToSend = this.maxNumRequests[type] - this.numRequests[type];
    let syncImageCount = 0;
 
    for (let i = 0; i < requestsToSend; i++) {
      const requestDetails = this.getNextRequest(type);
      if (requestDetails === null) {
        return false;
      } else Eif (requestDetails) {
        this.numRequests[type]++;
        this.awake = true;
 
        let requestResult;
        try {
          requestResult = requestDetails.requestFn();
        } catch (e) {
          // This is the only warning one will get, so need a warn message
          console.warn('sendRequest failed', e);
        }
        if (requestResult?.finally) {
          requestResult.finally(() => {
            this.numRequests[type]--;
            this.startAgain();
          });
        } else E{
          // Handle non-async request functions too - typically just short circuit ones
          this.numRequests[type]--;
          syncImageCount++;
        }
      }
    }
    if (syncImageCount) {
      this.startAgain();
    }
 
    return true;
  }
 
  private getNextRequest(type): RequestDetailsInterface | null {
    const interactionPriorities = this.getSortedPriorityGroups(type);
    for (const priority of interactionPriorities) {
      Eif (this.requestPool[type][priority].length) {
        return this.requestPool[type][priority].shift();
      }
    }
 
    return null;
  }
 
  protected startGrabbing(): void {
    const hasRemainingInteractionRequests = this.sendRequests(
      RequestType.Interaction
    );
    const hasRemainingThumbnailRequests = this.sendRequests(
      RequestType.Thumbnail
    );
    const hasRemainingPrefetchRequests = this.sendRequests(
      RequestType.Prefetch
    );
    const hasRemainingComputeRequests = this.sendRequests(RequestType.Compute);
 
    Eif (
      !hasRemainingInteractionRequests &&
      !hasRemainingThumbnailRequests &&
      !hasRemainingPrefetchRequests &&
      !hasRemainingComputeRequests
    ) {
      this.awake = false;
    }
  }
 
  protected startAgain(): void {
    Eif (!this.awake) {
      return;
    }
 
    if (this.grabDelay !== undefined) {
      // Prevents calling setTimeout hundreds of times when hundreds of requests
      // are added which make it slower and works in an unexpected way when
      // destroy/clearTimeout is called because only the last handle is stored.
      if (!this.timeoutHandle) {
        this.timeoutHandle = window.setTimeout(() => {
          this.timeoutHandle = null;
          this.startGrabbing();
        }, this.grabDelay);
      }
    } else {
      this.startGrabbing();
    }
  }
 
  protected getSortedPriorityGroups(type: string): Array<number> {
    const priorities = Object.keys(this.requestPool[type])
      .map(Number)
      .filter((priority) => this.requestPool[type][priority].length)
      .sort((a, b) => a - b);
    return priorities;
  }
 
  /**
   * Returns the request pool containing different categories, their priority and
   * the added request details.
   *
   * @returns the request pool which contains different categories, their priority and
   * the added request details
   */
  getRequestPool(): RequestPool {
    return this.requestPool;
  }
}
 
export { RequestPoolManager };