Skip to content

Commit

Permalink
Perf: Rely on more efficient Map for Priority Queue internal state (#…
Browse files Browse the repository at this point in the history
…47156)

The priority queue has been maintaining an array of enqueued contexts
and a `WeakMap` of enqueued callbacks for dispatching idle updates.

Priority ordering of context is maintained through the inherent order of
the array, but this comes at the cost of calling `.shift()` on every
dispatch, which rewrites the ordering of all the remaining keys or
indices of the array. When the priority queue gets large (such as in
store updates) this can incur a measurable cost in time. Additionally,
the use of the array for ordering relies on `WeakMap` semantics to
loosely hold references to the contexts, which might be removed.

In this patch we're relying solely on a single `Map`. By spec, `Map`
iteration follows the order of first insertion of its keys, which exactly
matches the needs of the priority queue iteration. We're able to avoid
using the `Array` altogether, and while not totally free, `Map` deletion
is measurably faster than `Array.prototype.shift()` and will involve at
least one fewer references to the context memory than having it in the
array _and_ the `WeakMap` did.

The goal of this patch is small and likely hard to measure. It aims
to lower memory allocation and pressure on the garbage collector during
the critical hot-patch of code which runs on every keypress or change
in the editor, so being a little better here will hopefully add up to
macro-level improvements in editor responsiveness.
  • Loading branch information
dmsnell committed Jan 24, 2023
1 parent a065bb5 commit 14e8390
Showing 1 changed file with 37 additions and 49 deletions.
86 changes: 37 additions & 49 deletions packages/priority-queue/src/index.js
Expand Up @@ -67,61 +67,62 @@ import requestIdleCallback from './request-idle-callback';
* @return {WPPriorityQueue} Queue object with `add`, `flush` and `reset` methods.
*/
export const createQueue = () => {
/** @type {WPPriorityQueueContext[]} */
let waitingList = [];

/** @type {WeakMap<WPPriorityQueueContext,WPPriorityQueueCallback>} */
let elementsMap = new WeakMap();

/** @type {Map<WPPriorityQueueContext, WPPriorityQueueCallback>} */
const waitingList = new Map();
let isRunning = false;

/**
* Callback to process as much queue as time permits.
*
* Map Iteration follows the original insertion order. This means that here
* we can iterate the queue and know that the first contexts which were
* added will be run first. On the other hand, if anyone adds a new callback
* for an existing context it will supplant the previously-set callback for
* that context because we reassigned that map key's value.
*
* In the case that a callback adds a new callback to its own context then
* the callback it adds will appear at the end of the iteration and will be
* run only after all other existing contexts have finished executing.
*
* @param {IdleDeadline|number} deadline Idle callback deadline object, or
* animation frame timestamp.
*/
const runWaitingList = ( deadline ) => {
const hasTimeRemaining =
typeof deadline === 'number'
? () => false
: () => deadline.timeRemaining() > 0;

do {
if ( waitingList.length === 0 ) {
isRunning = false;
return;
for ( const [ nextElement, callback ] of waitingList ) {
waitingList.delete( nextElement );
callback();

if (
'number' === typeof deadline ||
deadline.timeRemaining() <= 0
) {
break;
}
}

const nextElement = /** @type {WPPriorityQueueContext} */ (
waitingList.shift()
);
const callback = /** @type {WPPriorityQueueCallback} */ (
elementsMap.get( nextElement )
);
// If errors with undefined callbacks are encountered double check that all of your useSelect calls
// have all dependecies set correctly in second parameter. Missing dependencies can cause unexpected
// loops and race conditions in the queue.
callback();
elementsMap.delete( nextElement );
} while ( hasTimeRemaining() );
if ( waitingList.size === 0 ) {
isRunning = false;
return;
}

requestIdleCallback( runWaitingList );
};

/**
* Add a callback to the queue for a given context.
*
* If errors with undefined callbacks are encountered double check that
* all of your useSelect calls have the right dependencies set correctly
* in their second parameter. Missing dependencies can cause unexpected
* loops and race conditions in the queue.
*
* @type {WPPriorityQueueAdd}
*
* @param {WPPriorityQueueContext} element Context object.
* @param {WPPriorityQueueCallback} item Callback function.
*/
const add = ( element, item ) => {
if ( ! elementsMap.has( element ) ) {
waitingList.push( element );
}
elementsMap.set( element, item );
waitingList.set( element, item );
if ( ! isRunning ) {
isRunning = true;
requestIdleCallback( runWaitingList );
Expand All @@ -139,16 +140,12 @@ export const createQueue = () => {
* @return {boolean} Whether flush was performed.
*/
const flush = ( element ) => {
if ( ! elementsMap.has( element ) ) {
const callback = waitingList.get( element );
if ( undefined === callback ) {
return false;
}

const index = waitingList.indexOf( element );
waitingList.splice( index, 1 );
const callback = /** @type {WPPriorityQueueCallback} */ (
elementsMap.get( element )
);
elementsMap.delete( element );
waitingList.delete( element );
callback();

return true;
Expand All @@ -166,15 +163,7 @@ export const createQueue = () => {
* @return {boolean} Whether any callbacks got cancelled.
*/
const cancel = ( element ) => {
if ( ! elementsMap.has( element ) ) {
return false;
}

const index = waitingList.indexOf( element );
waitingList.splice( index, 1 );
elementsMap.delete( element );

return true;
return waitingList.delete( element );
};

/**
Expand All @@ -183,8 +172,7 @@ export const createQueue = () => {
* @type {WPPriorityQueueReset}
*/
const reset = () => {
waitingList = [];
elementsMap = new WeakMap();
waitingList.clear();
isRunning = false;
};

Expand Down

1 comment on commit 14e8390

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in 14e8390.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4002887620
📝 Reported issues:

Please sign in to comment.