| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Tasks and Idle state related utilities. |
| */ |
| |
| // Interface for all real time operations. Can be mocked easily. |
| export interface TaskTimer { |
| clear(id: number): void; |
| reset(then: Function, ms: number): number; |
| now(): number; |
| } |
| |
| // Real time TaskTimer. |
| export class LiveTaskTimer implements TaskTimer { |
| clear(id: number): void { |
| clearTimeout(id); |
| } |
| reset(then: Function, ms: number): number { |
| return setTimeout(then, ms); |
| } |
| now(): number { |
| return Date.now(); |
| } |
| } |
| |
| // Minimum delay between two tasks. |
| export const TASK_SEPARATOR_MS = 50; |
| |
| // Minimum delay after user activity. |
| export const TASK_ACTIVITY_DELAY_MS = 300; |
| |
| // Task manager wrapper `TaskTimer`. It also monitors user's activity and pushes |
| // back the timer if needed. |
| // TODO(crbug.com/40936184): replace by requestIdleCallback when available or |
| // move to general ts utilities. |
| export class IdleTaskTracker { |
| // The events to monitor for user activity. |
| private eventNames = [ |
| 'gesturechange', |
| 'gesturestart', |
| 'mousemove', |
| 'mousedown', |
| 'touchmove', |
| 'touchstart', |
| 'click', |
| 'keydown', |
| 'scroll', |
| ]; |
| |
| // Id for the current timer for the next task. |
| private timerId = 0; |
| |
| // Keeps track of calls to `startActivityListeners` and |
| // `stopActivityListeners` to properly install and remove event listeners. |
| private activityListenersCount = 0; |
| |
| // The last time a user activity was recorded. |
| lastActivityMs = 0; |
| |
| // Map of all tasks, keyed by the task callback `Function`. `value` is the |
| // current wake up time requested. |
| private tasks = new Map<Function, number>(); |
| |
| // Requires a `taskTimer` so it can be easily testable. `activityDelayMs` is |
| // the delay between a user activity and when the next task should be |
| // triggered (if any). `minimumTaskDelayMs` is an extra delay between two |
| // tasks to avoid blocking the main thread for too long. |
| constructor( |
| private taskTimer: TaskTimer = new LiveTaskTimer(), |
| private activityDelayMs = TASK_ACTIVITY_DELAY_MS, |
| private minimumTaskDelayMs = TASK_SEPARATOR_MS) { |
| this.lastActivityMs = taskTimer.now() - this.activityDelayMs; |
| } |
| |
| // Queues the given `task` for execution when the page is idled, starting |
| // `wakeUpDelayMs` from now. If the task exists, its new execution time is |
| // updated. |
| schedule(task: Function, wakeUpDelayMs = this.minimumTaskDelayMs): void { |
| const taskStartMs = this.taskTimer.now() + wakeUpDelayMs; |
| if (!this.tasks.has(task)) { |
| // For each task, start activity listeners only once. |
| this.startActivityListeners(); |
| } |
| // Add to tasks and reschedule next wakeup time from new set of tasks. |
| this.tasks.set(task, taskStartMs); |
| this.setNextWakeup(this.nextWakeUpMs()); |
| } |
| |
| // Stops and clears this scheduler. |
| shutdown(): void { |
| this.setNextWakeup(0); |
| this.activityListenersCount = 1; |
| this.stopActivityListeners(); |
| this.tasks.clear(); |
| } |
| |
| // Starts all activity listeners. Will do it only once when |
| // `activityListenersCount` goes from 0 to 1. |
| startActivityListeners(): void { |
| if (++this.activityListenersCount !== 1) { |
| return; |
| } |
| this.eventNames.forEach((eventName) => { |
| window.addEventListener(eventName, this.recordLastActivity); |
| }); |
| } |
| |
| // Stops all activity listeners. Will do it only once when |
| // `activityListenersCount` goes from 1 to 0. |
| stopActivityListeners(): void { |
| if (--this.activityListenersCount !== 0) { |
| return; |
| } |
| this.eventNames.forEach((eventName) => { |
| window.removeEventListener(eventName, this.recordLastActivity); |
| }); |
| } |
| |
| // Returns the next task and its wakeup time. |
| private nextTask(): [Function|null, number] { |
| let earlierTaskMs = 0; |
| let earlierTask: Function|null = null; |
| this.tasks.forEach((taskMs: number, task: Function) => { |
| if (!earlierTask || taskMs < earlierTaskMs) { |
| earlierTaskMs = taskMs; |
| earlierTask = task; |
| } |
| }); |
| return [earlierTask, earlierTaskMs]; |
| } |
| |
| // Returns next wakeup time based on current set of tasks. |
| private nextWakeUpMs(): number { |
| const [earlierTask, earlierTaskMs] = this.nextTask(); |
| if (!earlierTask) { |
| return 0; |
| } |
| return earlierTaskMs; |
| } |
| |
| // Resets timer for the new `wakeUpMs` wakeup time. A `wakeUp` time of 0 stops |
| // the timer. Minimum delay will be `minimumTaskDelayMs` from now, to let user |
| // some time between tasks. |
| private setNextWakeup(wakeUpMs: number): void { |
| if (this.timerId) { |
| this.taskTimer.clear(this.timerId); |
| this.timerId = 0; |
| } |
| if (wakeUpMs > 0) { |
| const now = this.taskTimer.now(); |
| wakeUpMs = Math.max(wakeUpMs, now + this.minimumTaskDelayMs); |
| this.timerId = this.taskTimer.reset(this.onWakeUp, wakeUpMs - now); |
| } |
| } |
| |
| // Called when the timer triggers. It will check that enough idle time |
| // has passed, if not schedule more time, then execute one task. Finally |
| // will schedule next wakeup time based on tasks left. |
| private onWakeUp = () => { |
| const now = this.taskTimer.now(); |
| if (this.lastActivityMs + this.activityDelayMs > now) { |
| this.setNextWakeup(this.lastActivityMs + this.activityDelayMs); |
| } else { |
| const [earlierTask] = this.nextTask(); |
| if (earlierTask) { |
| this.tasks.delete(earlierTask); |
| earlierTask(); |
| this.stopActivityListeners(); |
| } |
| this.setNextWakeup(this.nextWakeUpMs()); |
| } |
| }; |
| |
| // Records last activity time event listener. |
| private recordLastActivity = () => { |
| this.lastActivityMs = this.taskTimer.now(); |
| }; |
| } |