Skip to content

Commit

Permalink
🔥 perf: makes min build default import and optimizes IdleQueue by 60x
Browse files Browse the repository at this point in the history
  • Loading branch information
Harsh Kumar Choudhary authored and Harsh Kumar Choudhary committed May 1, 2024
1 parent d65b3af commit b4af978
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 24 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
"*": {
Expand Down
45 changes: 24 additions & 21 deletions src/idleQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

/**
Expand All @@ -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

Expand All @@ -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'
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -111,17 +114,17 @@ 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)
}
}

private addTask_(
arrayMethod: Array<any>['push'] | Array<any>['unshift'],
task: Task,
options?: { minTaskTime?: number },
unshift: boolean = false,
): void {
const state: State = {
time: now(),
Expand All @@ -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_()
}
Expand Down Expand Up @@ -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()
Expand All @@ -197,6 +208,7 @@ export class IdleQueue {
}

this.state_ = null
tasksProcessed++
}
}

Expand All @@ -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()
}
}
66 changes: 66 additions & 0 deletions test/benchmark.cjs
Original file line number Diff line number Diff line change
@@ -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)}%`)

0 comments on commit b4af978

Please sign in to comment.