RequestScheduler.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import Uri from "urijs";
  2. import Check from "./Check.js";
  3. import defaultValue from "./defaultValue.js";
  4. import defer from "./defer.js";
  5. import defined from "./defined.js";
  6. import Event from "./Event.js";
  7. import Heap from "./Heap.js";
  8. import isBlobUri from "./isBlobUri.js";
  9. import isDataUri from "./isDataUri.js";
  10. import RequestState from "./RequestState.js";
  11. function sortRequests(a, b) {
  12. return a.priority - b.priority;
  13. }
  14. const statistics = {
  15. numberOfAttemptedRequests: 0,
  16. numberOfActiveRequests: 0,
  17. numberOfCancelledRequests: 0,
  18. numberOfCancelledActiveRequests: 0,
  19. numberOfFailedRequests: 0,
  20. numberOfActiveRequestsEver: 0,
  21. lastNumberOfActiveRequests: 0,
  22. };
  23. let priorityHeapLength = 20;
  24. const requestHeap = new Heap({
  25. comparator: sortRequests,
  26. });
  27. requestHeap.maximumLength = priorityHeapLength;
  28. requestHeap.reserve(priorityHeapLength);
  29. const activeRequests = [];
  30. let numberOfActiveRequestsByServer = {};
  31. const pageUri =
  32. typeof document !== "undefined" ? new Uri(document.location.href) : new Uri();
  33. const requestCompletedEvent = new Event();
  34. /**
  35. * The request scheduler is used to track and constrain the number of active requests in order to prioritize incoming requests. The ability
  36. * to retain control over the number of requests in CesiumJS is important because due to events such as changes in the camera position,
  37. * a lot of new requests may be generated and a lot of in-flight requests may become redundant. The request scheduler manually constrains the
  38. * number of requests so that newer requests wait in a shorter queue and don't have to compete for bandwidth with requests that have expired.
  39. *
  40. * @namespace RequestScheduler
  41. *
  42. */
  43. function RequestScheduler() {}
  44. /**
  45. * The maximum number of simultaneous active requests. Un-throttled requests do not observe this limit.
  46. * @type {number}
  47. * @default 50
  48. */
  49. RequestScheduler.maximumRequests = 50;
  50. /**
  51. * The maximum number of simultaneous active requests per server. Un-throttled requests or servers specifically
  52. * listed in {@link requestsByServer} do not observe this limit.
  53. * @type {number}
  54. * @default 6
  55. */
  56. RequestScheduler.maximumRequestsPerServer = 6;
  57. /**
  58. * A per server key list of overrides to use for throttling instead of <code>maximumRequestsPerServer</code>.
  59. * Useful when streaming data from a known HTTP/2 or HTTP/3 server.
  60. * @type {object}
  61. *
  62. * @example
  63. * RequestScheduler.requestsByServer["myserver.com:443"] = 18;
  64. *
  65. * @example
  66. * RequestScheduler.requestsByServer = {
  67. * "api.cesium.com:443": 18,
  68. * "assets.cesium.com:443": 18,
  69. * };
  70. */
  71. RequestScheduler.requestsByServer = {
  72. "api.cesium.com:443": 18,
  73. "assets.ion.cesium.com:443": 18,
  74. "ibasemaps-api.arcgis.com:443": 18,
  75. };
  76. /**
  77. * Specifies if the request scheduler should throttle incoming requests, or let the browser queue requests under its control.
  78. * @type {boolean}
  79. * @default true
  80. */
  81. RequestScheduler.throttleRequests = true;
  82. /**
  83. * When true, log statistics to the console every frame
  84. * @type {boolean}
  85. * @default false
  86. * @private
  87. */
  88. RequestScheduler.debugShowStatistics = false;
  89. /**
  90. * An event that's raised when a request is completed. Event handlers are passed
  91. * the error object if the request fails.
  92. *
  93. * @type {Event}
  94. * @default Event()
  95. * @private
  96. */
  97. RequestScheduler.requestCompletedEvent = requestCompletedEvent;
  98. Object.defineProperties(RequestScheduler, {
  99. /**
  100. * Returns the statistics used by the request scheduler.
  101. *
  102. * @memberof RequestScheduler
  103. *
  104. * @type {object}
  105. * @readonly
  106. * @private
  107. */
  108. statistics: {
  109. get: function () {
  110. return statistics;
  111. },
  112. },
  113. /**
  114. * The maximum size of the priority heap. This limits the number of requests that are sorted by priority. Only applies to requests that are not yet active.
  115. *
  116. * @memberof RequestScheduler
  117. *
  118. * @type {number}
  119. * @default 20
  120. * @private
  121. */
  122. priorityHeapLength: {
  123. get: function () {
  124. return priorityHeapLength;
  125. },
  126. set: function (value) {
  127. // If the new length shrinks the heap, need to cancel some of the requests.
  128. // Since this value is not intended to be tweaked regularly it is fine to just cancel the high priority requests.
  129. if (value < priorityHeapLength) {
  130. while (requestHeap.length > value) {
  131. const request = requestHeap.pop();
  132. cancelRequest(request);
  133. }
  134. }
  135. priorityHeapLength = value;
  136. requestHeap.maximumLength = value;
  137. requestHeap.reserve(value);
  138. },
  139. },
  140. });
  141. function updatePriority(request) {
  142. if (defined(request.priorityFunction)) {
  143. request.priority = request.priorityFunction();
  144. }
  145. }
  146. /**
  147. * Check if there are open slots for a particular server key. If desiredRequests is greater than 1, this checks if the queue has room for scheduling multiple requests.
  148. * @param {string} serverKey The server key returned by {@link RequestScheduler.getServerKey}.
  149. * @param {number} [desiredRequests=1] How many requests the caller plans to request
  150. * @return {boolean} True if there are enough open slots for <code>desiredRequests</code> more requests.
  151. * @private
  152. */
  153. RequestScheduler.serverHasOpenSlots = function (serverKey, desiredRequests) {
  154. desiredRequests = defaultValue(desiredRequests, 1);
  155. const maxRequests = defaultValue(
  156. RequestScheduler.requestsByServer[serverKey],
  157. RequestScheduler.maximumRequestsPerServer
  158. );
  159. const hasOpenSlotsServer =
  160. numberOfActiveRequestsByServer[serverKey] + desiredRequests <= maxRequests;
  161. return hasOpenSlotsServer;
  162. };
  163. /**
  164. * Check if the priority heap has open slots, regardless of which server they
  165. * are from. This is used in {@link Multiple3DTileContent} for determining when
  166. * all requests can be scheduled
  167. * @param {number} desiredRequests The number of requests the caller intends to make
  168. * @return {boolean} <code>true</code> if the heap has enough available slots to meet the desiredRequests. <code>false</code> otherwise.
  169. *
  170. * @private
  171. */
  172. RequestScheduler.heapHasOpenSlots = function (desiredRequests) {
  173. const hasOpenSlotsHeap =
  174. requestHeap.length + desiredRequests <= priorityHeapLength;
  175. return hasOpenSlotsHeap;
  176. };
  177. function issueRequest(request) {
  178. if (request.state === RequestState.UNISSUED) {
  179. request.state = RequestState.ISSUED;
  180. request.deferred = defer();
  181. }
  182. return request.deferred.promise;
  183. }
  184. function getRequestReceivedFunction(request) {
  185. return function (results) {
  186. if (request.state === RequestState.CANCELLED) {
  187. // If the data request comes back but the request is cancelled, ignore it.
  188. return;
  189. }
  190. // explicitly set to undefined to ensure GC of request response data. See #8843
  191. const deferred = request.deferred;
  192. --statistics.numberOfActiveRequests;
  193. --numberOfActiveRequestsByServer[request.serverKey];
  194. requestCompletedEvent.raiseEvent();
  195. request.state = RequestState.RECEIVED;
  196. request.deferred = undefined;
  197. deferred.resolve(results);
  198. };
  199. }
  200. function getRequestFailedFunction(request) {
  201. return function (error) {
  202. if (request.state === RequestState.CANCELLED) {
  203. // If the data request comes back but the request is cancelled, ignore it.
  204. return;
  205. }
  206. ++statistics.numberOfFailedRequests;
  207. --statistics.numberOfActiveRequests;
  208. --numberOfActiveRequestsByServer[request.serverKey];
  209. requestCompletedEvent.raiseEvent(error);
  210. request.state = RequestState.FAILED;
  211. request.deferred.reject(error);
  212. };
  213. }
  214. function startRequest(request) {
  215. const promise = issueRequest(request);
  216. request.state = RequestState.ACTIVE;
  217. activeRequests.push(request);
  218. ++statistics.numberOfActiveRequests;
  219. ++statistics.numberOfActiveRequestsEver;
  220. ++numberOfActiveRequestsByServer[request.serverKey];
  221. request
  222. .requestFunction()
  223. .then(getRequestReceivedFunction(request))
  224. .catch(getRequestFailedFunction(request));
  225. return promise;
  226. }
  227. function cancelRequest(request) {
  228. const active = request.state === RequestState.ACTIVE;
  229. request.state = RequestState.CANCELLED;
  230. ++statistics.numberOfCancelledRequests;
  231. // check that deferred has not been cleared since cancelRequest can be called
  232. // on a finished request, e.g. by clearForSpecs during tests
  233. if (defined(request.deferred)) {
  234. const deferred = request.deferred;
  235. request.deferred = undefined;
  236. deferred.reject();
  237. }
  238. if (active) {
  239. --statistics.numberOfActiveRequests;
  240. --numberOfActiveRequestsByServer[request.serverKey];
  241. ++statistics.numberOfCancelledActiveRequests;
  242. }
  243. if (defined(request.cancelFunction)) {
  244. request.cancelFunction();
  245. }
  246. }
  247. /**
  248. * Sort requests by priority and start requests.
  249. * @private
  250. */
  251. RequestScheduler.update = function () {
  252. let i;
  253. let request;
  254. // Loop over all active requests. Cancelled, failed, or received requests are removed from the array to make room for new requests.
  255. let removeCount = 0;
  256. const activeLength = activeRequests.length;
  257. for (i = 0; i < activeLength; ++i) {
  258. request = activeRequests[i];
  259. if (request.cancelled) {
  260. // Request was explicitly cancelled
  261. cancelRequest(request);
  262. }
  263. if (request.state !== RequestState.ACTIVE) {
  264. // Request is no longer active, remove from array
  265. ++removeCount;
  266. continue;
  267. }
  268. if (removeCount > 0) {
  269. // Shift back to fill in vacated slots from completed requests
  270. activeRequests[i - removeCount] = request;
  271. }
  272. }
  273. activeRequests.length -= removeCount;
  274. // Update priority of issued requests and resort the heap
  275. const issuedRequests = requestHeap.internalArray;
  276. const issuedLength = requestHeap.length;
  277. for (i = 0; i < issuedLength; ++i) {
  278. updatePriority(issuedRequests[i]);
  279. }
  280. requestHeap.resort();
  281. // Get the number of open slots and fill with the highest priority requests.
  282. // Un-throttled requests are automatically added to activeRequests, so activeRequests.length may exceed maximumRequests
  283. const openSlots = Math.max(
  284. RequestScheduler.maximumRequests - activeRequests.length,
  285. 0
  286. );
  287. let filledSlots = 0;
  288. while (filledSlots < openSlots && requestHeap.length > 0) {
  289. // Loop until all open slots are filled or the heap becomes empty
  290. request = requestHeap.pop();
  291. if (request.cancelled) {
  292. // Request was explicitly cancelled
  293. cancelRequest(request);
  294. continue;
  295. }
  296. if (
  297. request.throttleByServer &&
  298. !RequestScheduler.serverHasOpenSlots(request.serverKey)
  299. ) {
  300. // Open slots are available, but the request is throttled by its server. Cancel and try again later.
  301. cancelRequest(request);
  302. continue;
  303. }
  304. startRequest(request);
  305. ++filledSlots;
  306. }
  307. updateStatistics();
  308. };
  309. /**
  310. * Get the server key from a given url.
  311. *
  312. * @param {string} url The url.
  313. * @returns {string} The server key.
  314. * @private
  315. */
  316. RequestScheduler.getServerKey = function (url) {
  317. //>>includeStart('debug', pragmas.debug);
  318. Check.typeOf.string("url", url);
  319. //>>includeEnd('debug');
  320. let uri = new Uri(url);
  321. if (uri.scheme() === "") {
  322. uri = uri.absoluteTo(pageUri);
  323. uri.normalize();
  324. }
  325. let serverKey = uri.authority();
  326. if (!/:/.test(serverKey)) {
  327. // If the authority does not contain a port number, add port 443 for https or port 80 for http
  328. serverKey = `${serverKey}:${uri.scheme() === "https" ? "443" : "80"}`;
  329. }
  330. const length = numberOfActiveRequestsByServer[serverKey];
  331. if (!defined(length)) {
  332. numberOfActiveRequestsByServer[serverKey] = 0;
  333. }
  334. return serverKey;
  335. };
  336. /**
  337. * Issue a request. If request.throttle is false, the request is sent immediately. Otherwise the request will be
  338. * queued and sorted by priority before being sent.
  339. *
  340. * @param {Request} request The request object.
  341. *
  342. * @returns {Promise|undefined} A Promise for the requested data, or undefined if this request does not have high enough priority to be issued.
  343. *
  344. * @private
  345. */
  346. RequestScheduler.request = function (request) {
  347. //>>includeStart('debug', pragmas.debug);
  348. Check.typeOf.object("request", request);
  349. Check.typeOf.string("request.url", request.url);
  350. Check.typeOf.func("request.requestFunction", request.requestFunction);
  351. //>>includeEnd('debug');
  352. if (isDataUri(request.url) || isBlobUri(request.url)) {
  353. requestCompletedEvent.raiseEvent();
  354. request.state = RequestState.RECEIVED;
  355. return request.requestFunction();
  356. }
  357. ++statistics.numberOfAttemptedRequests;
  358. if (!defined(request.serverKey)) {
  359. request.serverKey = RequestScheduler.getServerKey(request.url);
  360. }
  361. if (
  362. RequestScheduler.throttleRequests &&
  363. request.throttleByServer &&
  364. !RequestScheduler.serverHasOpenSlots(request.serverKey)
  365. ) {
  366. // Server is saturated. Try again later.
  367. return undefined;
  368. }
  369. if (!RequestScheduler.throttleRequests || !request.throttle) {
  370. return startRequest(request);
  371. }
  372. if (activeRequests.length >= RequestScheduler.maximumRequests) {
  373. // Active requests are saturated. Try again later.
  374. return undefined;
  375. }
  376. // Insert into the priority heap and see if a request was bumped off. If this request is the lowest
  377. // priority it will be returned.
  378. updatePriority(request);
  379. const removedRequest = requestHeap.insert(request);
  380. if (defined(removedRequest)) {
  381. if (removedRequest === request) {
  382. // Request does not have high enough priority to be issued
  383. return undefined;
  384. }
  385. // A previously issued request has been bumped off the priority heap, so cancel it
  386. cancelRequest(removedRequest);
  387. }
  388. return issueRequest(request);
  389. };
  390. function updateStatistics() {
  391. if (!RequestScheduler.debugShowStatistics) {
  392. return;
  393. }
  394. if (
  395. statistics.numberOfActiveRequests === 0 &&
  396. statistics.lastNumberOfActiveRequests > 0
  397. ) {
  398. if (statistics.numberOfAttemptedRequests > 0) {
  399. console.log(
  400. `Number of attempted requests: ${statistics.numberOfAttemptedRequests}`
  401. );
  402. statistics.numberOfAttemptedRequests = 0;
  403. }
  404. if (statistics.numberOfCancelledRequests > 0) {
  405. console.log(
  406. `Number of cancelled requests: ${statistics.numberOfCancelledRequests}`
  407. );
  408. statistics.numberOfCancelledRequests = 0;
  409. }
  410. if (statistics.numberOfCancelledActiveRequests > 0) {
  411. console.log(
  412. `Number of cancelled active requests: ${statistics.numberOfCancelledActiveRequests}`
  413. );
  414. statistics.numberOfCancelledActiveRequests = 0;
  415. }
  416. if (statistics.numberOfFailedRequests > 0) {
  417. console.log(
  418. `Number of failed requests: ${statistics.numberOfFailedRequests}`
  419. );
  420. statistics.numberOfFailedRequests = 0;
  421. }
  422. }
  423. statistics.lastNumberOfActiveRequests = statistics.numberOfActiveRequests;
  424. }
  425. /**
  426. * For testing only. Clears any requests that may not have completed from previous tests.
  427. *
  428. * @private
  429. */
  430. RequestScheduler.clearForSpecs = function () {
  431. while (requestHeap.length > 0) {
  432. const request = requestHeap.pop();
  433. cancelRequest(request);
  434. }
  435. const length = activeRequests.length;
  436. for (let i = 0; i < length; ++i) {
  437. cancelRequest(activeRequests[i]);
  438. }
  439. activeRequests.length = 0;
  440. numberOfActiveRequestsByServer = {};
  441. // Clear stats
  442. statistics.numberOfAttemptedRequests = 0;
  443. statistics.numberOfActiveRequests = 0;
  444. statistics.numberOfCancelledRequests = 0;
  445. statistics.numberOfCancelledActiveRequests = 0;
  446. statistics.numberOfFailedRequests = 0;
  447. statistics.numberOfActiveRequestsEver = 0;
  448. statistics.lastNumberOfActiveRequests = 0;
  449. };
  450. /**
  451. * For testing only.
  452. *
  453. * @private
  454. */
  455. RequestScheduler.numberOfActiveRequestsByServer = function (serverKey) {
  456. return numberOfActiveRequestsByServer[serverKey];
  457. };
  458. /**
  459. * For testing only.
  460. *
  461. * @private
  462. */
  463. RequestScheduler.requestHeap = requestHeap;
  464. export default RequestScheduler;