// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {ImageRequestTask} from './image_request_task.js';
// `deviceMemory` might be 0.5 or 0.25, so we normalize to minimum of 2.
const memory = Math.max(2, navigator.deviceMemory);
// For low end devices `hardwareCount` can be low like 4 some other devies are
// low in memory, it will have the value 4 as in 4GB.
const resourceCount = Math.min(navigator.hardwareConcurrency, memory, 10);
/**
* Maximum download tasks to be run in parallel, for low end devices we expect
* the result to be 2, higher end devices we expect to be at least 4, but no
* more than 5.
*/
export const MAXIMUM_IN_PARALLEL = resourceCount / 2;
console.warn(`Image Loader maximum parallel tasks: ${MAXIMUM_IN_PARALLEL}`);
/**
* Scheduler for ImageRequestTask objects. Fetches tasks from a queue and
* processes them synchronously, taking into account priorities. The highest
* priority is 0.
*/
export class Scheduler {
/**
* List of tasks waiting to be checked. If these items are available in
* cache, then they are processed immediately after starting the scheduler.
* However, if they have to be downloaded, then these tasks are moved to
* pendingTasks_.
*/
private newTasks_: ImageRequestTask[] = [];
/** List of pending tasks for images to be downloaded. */
private pendingTasks_: ImageRequestTask[] = [];
/** List of tasks being processed. */
private activeTasks_: ImageRequestTask[] = [];
/**
* Map of tasks being added to the queue, but not finalized yet. Keyed by
* the ImageRequestTask id.
*/
private tasks_: Record<string, ImageRequestTask> = {};
/** If the scheduler has been started. */
private started_: boolean = false;
/**
* Adds a task to the internal priority queue and executes it when tasks
* with higher priorities are finished. If the result is cached, then it is
* processed immediately once the scheduler is started.
*/
add(task: ImageRequestTask) {
if (!this.started_) {
this.newTasks_.push(task);
this.tasks_[task.getId()] = task;
return;
}
// Enqueue the tasks, since already started.
this.pendingTasks_.push(task);
this.sortPendingTasks_();
this.continue_();
}
/** Removes a task from the scheduler (if exists). */
remove(taskId: string) {
const task = this.tasks_[taskId];
if (!task) {
return;
}
// Remove from the internal queues with pending tasks.
const newIndex = this.newTasks_.indexOf(task);
if (newIndex !== -1) {
this.newTasks_.splice(newIndex, 1);
}
const pendingIndex = this.pendingTasks_.indexOf(task);
if (pendingIndex !== -1) {
this.pendingTasks_.splice(pendingIndex, 1);
}
// Cancel the task.
task.cancel();
delete this.tasks_[taskId];
}
/** Starts handling tasks. */
start() {
this.started_ = true;
// Process tasks added before scheduler has been started.
this.pendingTasks_ = this.newTasks_;
this.sortPendingTasks_();
this.newTasks_ = [];
// Start serving enqueued tasks.
this.continue_();
}
/** Sorts pending tasks by priorities. */
private sortPendingTasks_() {
this.pendingTasks_.sort((a, b) => {
return a.getPriority() - b.getPriority();
});
}
/**
* Processes pending tasks from the queue. There is no guarantee that
* all of the tasks will be processed at once.
*/
private continue_() {
// Run only up to MAXIMUM_IN_PARALLEL in the same time.
while (this.pendingTasks_.length > 0 &&
this.activeTasks_.length < MAXIMUM_IN_PARALLEL) {
const task = this.pendingTasks_.shift()!;
this.activeTasks_.push(task);
// Try to load from cache. If doesn't exist, then download.
task.loadFromCacheAndProcess(
() => this.finish_(task),
() => task.downloadAndProcess(() => this.finish_(task)));
}
}
/** Handles a finished task. */
private finish_(task: ImageRequestTask) {
const index = this.activeTasks_.indexOf(task);
if (index < 0) {
console.warn('ImageRequestTask not found.');
}
this.activeTasks_.splice(index, 1);
delete this.tasks_[task.getId()];
// Continue handling the most important tasks (if started).
if (this.started_) {
this.continue_();
}
}
}