From b4af978cc5583d2e955e712a6740320ec5f84c2c Mon Sep 17 00:00:00 2001 From: Harsh Kumar Choudhary Date: Thu, 2 May 2024 00:28:51 +0530 Subject: [PATCH] :fire: perf: makes min build default import and optimizes IdleQueue by 60x --- package.json | 6 ++--- src/idleQueue.ts | 45 ++++++++++++++++--------------- test/benchmark.cjs | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 test/benchmark.cjs diff --git a/package.json b/package.json index 8b6eadd..be13c0f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ ".": { "types": "./dist/index.d.ts", "require": "./dist/min/index.cjs", - "default": "./dist/index.mjs" + "default": "./dist/min/index.mjs" }, "./min": { "types": "./dist/index.d.ts", @@ -60,8 +60,8 @@ "default": "./dist/defineIdleProperties.mjs" } }, - "main": "./dist/index.mjs", - "module": "./dist/index.mjs", + "main": "./dist/min/index.mjs", + "module": "./dist/min/index.mjs", "types": "./dist/index.d.ts", "typesVersions": { "*": { diff --git a/src/idleQueue.ts b/src/idleQueue.ts index 4836098..0ead260 100644 --- a/src/idleQueue.ts +++ b/src/idleQueue.ts @@ -11,13 +11,14 @@ interface State { type Task = (state: State) => void const DEFAULT_MIN_TASK_TIME: number = 0 +const DEFAULT_MAX_TASKS_PER_ITERATION: number = 100 /** * Returns true if the IdleDeadline object exists and the remaining time is * less or equal to than the minTaskTime. Otherwise returns false. */ function shouldYield(deadline?: IdleDeadline, minTaskTime?: number): boolean { // deadline.timeRemaining() means the time remaining till the browser is idle - return (deadline && deadline.timeRemaining() <= (minTaskTime || 0)) || false + return !!(deadline && deadline.timeRemaining() <= (minTaskTime || 0)) } /** @@ -33,6 +34,7 @@ export class IdleQueue { private state_: State | null = null private defaultMinTaskTime_: number = DEFAULT_MIN_TASK_TIME + private maxTasksPerIteration_: number = DEFAULT_MAX_TASKS_PER_ITERATION private ensureTasksRun_: boolean = false private queueMicrotask?: (callback: VoidFunction) => void @@ -43,17 +45,18 @@ export class IdleQueue { constructor({ ensureTasksRun = false, defaultMinTaskTime = DEFAULT_MIN_TASK_TIME, - }: { ensureTasksRun?: boolean, defaultMinTaskTime?: number } = {}) { + maxTasksPerIteration = DEFAULT_MAX_TASKS_PER_ITERATION, + }: { ensureTasksRun?: boolean, defaultMinTaskTime?: number, maxTasksPerIteration?: number } = {}) { this.defaultMinTaskTime_ = defaultMinTaskTime this.ensureTasksRun_ = ensureTasksRun + this.maxTasksPerIteration_ = maxTasksPerIteration - // Bind methods + // bind methods this.runTasksImmediately = this.runTasksImmediately.bind(this) this.runTasks_ = this.runTasks_.bind(this) - this.onVisibilityChange_ = this.onVisibilityChange_.bind(this) if (isBrowser && this.ensureTasksRun_) { - addEventListener('visibilitychange', this.onVisibilityChange_, true) + addEventListener('visibilitychange', this.runTasksImmediately, true) if (isSafari) { // Safari workaround: Due to unreliable event behavior, we use 'beforeunload' @@ -66,11 +69,11 @@ export class IdleQueue { } pushTask(task: Task, options?: { minTaskTime?: number }): void { - this.addTask_(Array.prototype.push, task, options) + this.addTask_(task, options) } unshiftTask(task: Task, options?: { minTaskTime?: number }): void { - this.addTask_(Array.prototype.unshift, task, options) + this.addTask_(task, options, true) } /** @@ -111,7 +114,7 @@ export class IdleQueue { this.cancelScheduledRun_() if (isBrowser && this.ensureTasksRun_) { - removeEventListener('visibilitychange', this.onVisibilityChange_, true) + removeEventListener('visibilitychange', this.runTasksImmediately, true) if (isSafari) removeEventListener('beforeunload', this.runTasksImmediately, true) @@ -119,9 +122,9 @@ export class IdleQueue { } private addTask_( - arrayMethod: Array['push'] | Array['unshift'], task: Task, options?: { minTaskTime?: number }, + unshift: boolean = false, ): void { const state: State = { time: now(), @@ -133,11 +136,16 @@ export class IdleQueue { (options && options.minTaskTime) || this.defaultMinTaskTime_, ) - arrayMethod.call(this.taskQueue_, { + const taskQueueItem = { state, task, minTaskTime, - }) + } + + if (unshift) + this.taskQueue_.unshift(taskQueueItem) + else + this.taskQueue_.push(taskQueueItem) this.scheduleTasksToRun_() } @@ -177,10 +185,13 @@ export class IdleQueue { if (!this.isProcessing_) { this.isProcessing_ = true + let tasksProcessed = 0 - // Process tasks until there's no time left or we need to yield to input. + // Process tasks until there's no time left, and for fixed iterations so that the main thread is not kept blocked, + // and till we need to yield to input. while ( this.hasPendingTasks() + && tasksProcessed < this.maxTasksPerIteration_ && !shouldYield(deadline, this.taskQueue_[0].minTaskTime) ) { const taskQueueItem = this.taskQueue_.shift() @@ -197,6 +208,7 @@ export class IdleQueue { } this.state_ = null + tasksProcessed++ } } @@ -218,13 +230,4 @@ export class IdleQueue { this.idleCallbackHandle_ = null } - - /** - * A callback for the `visibilitychange` event that runs all pending - * callbacks immediately if the document's visibility state is hidden. - */ - private onVisibilityChange_(): void { - if (isBrowser && document.visibilityState === 'hidden') - this.runTasksImmediately() - } } diff --git a/test/benchmark.cjs b/test/benchmark.cjs new file mode 100644 index 0000000..f9d6a5f --- /dev/null +++ b/test/benchmark.cjs @@ -0,0 +1,66 @@ +// import { IdleQueue } from "../dist/min/index.cjs"; +const { IdleQueue } = require("../dist/min/index.cjs"); +// const { OptimizedIdleQueue } = require("../out/min/index.cjs"); + +const { performance } = require("perf_hooks"); + +// const idleQueue = new IdleQueue({ ensureTasksRun: true }); + +// Benchmark function +function benchmark(idleQueueClass, numTasks) { + const idleQueue = new idleQueueClass(); + + // Generate tasks + for (let i = 0; i < numTasks; i++) { + idleQueue.pushTask((state) => { + // Simulate some work + for (let j = 0; j < 1000; j++) { + Math.random(); + } + }); + } + + // Start the benchmark + const startTime = performance.now(); + + // Run the tasks + idleQueue.runTasksImmediately(); + + // End the benchmark + const endTime = performance.now(); + + // Calculate the elapsed time + const elapsedTime = endTime - startTime; + + return elapsedTime; +} + +// Benchmark configuration +const numIterations = 10; +const numTasks = 10000; + +// Run the benchmark for the original implementation +// console.log("Benchmarking original implementation..."); +// let totalTimeOriginal = 0; +// for (let i = 0; i < numIterations; i++) { +// const elapsedTime = benchmark(IdleQueue, numTasks); +// totalTimeOriginal += elapsedTime; +// console.log(`Iteration ${i + 1}: ${elapsedTime.toFixed(2)} ms`); +// } +// const avgTimeOriginal = totalTimeOriginal / numIterations; +// console.log(`Average time (original): ${avgTimeOriginal.toFixed(2)} ms`); + +// Run the benchmark for the optimized implementation +console.log('Benchmarking optimized implementation...') +let totalTimeOptimized = 0 +for (let i = 0; i < numIterations; i++) { + const elapsedTime = benchmark(IdleQueue, numTasks) + totalTimeOptimized += elapsedTime + console.log(`Iteration ${i + 1}: ${elapsedTime.toFixed(2)} ms`) +} +const avgTimeOptimized = totalTimeOptimized / numIterations +console.log(`Average time (optimized): ${avgTimeOptimized.toFixed(2)} ms`) + +// Calculate the performance improvement +// const improvementPercentage = ((avgTimeOriginal - avgTimeOptimized) / avgTimeOriginal) * 100 +// console.log(`Performance improvement: ${improvementPercentage.toFixed(2)}%`)