Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[draft] Benchmark linked list v11 impl #4256

Draft
wants to merge 72 commits into
base: v11-benchmark-base
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
9e341b6
WIP: Convert internals to use linked list for children
andrewiggins Dec 21, 2022
dda7076
Implement basic patchChildren using linked list and skewed old head.
andrewiggins Jan 2, 2023
5e6cb22
WIP: Switch insertion loop to loop backwards through internal children
andrewiggins Jan 2, 2023
27d57d0
WIP: Unmount before inserting and fix up insertion loop
andrewiggins Jan 3, 2023
31a9967
Add two more keys test
andrewiggins Jan 19, 2023
8d138c9
Simplify keys test and focus on only test we currently care about
andrewiggins Jan 19, 2023
02e9046
Fix null ref bug when mounting children at start
andrewiggins Jan 19, 2023
07b9f1b
Fix setting parent._child
andrewiggins Jan 19, 2023
aa884e2
Remove some commented code
andrewiggins Jan 19, 2023
e80c37a
Use a modified LIS algorithm to determine which internals to move
andrewiggins Jan 19, 2023
c707247
Simplify insertion loop a bit
andrewiggins Jan 19, 2023
e04e7e7
WIP child diffing explanation
andrewiggins Jan 20, 2023
ffb7d52
WIP child diffing explanation progress
andrewiggins Jan 21, 2023
91c8247
Use INSERT_INTERNAL flag to mark nodes for insertion
andrewiggins Jan 22, 2023
133d09a
Eagerly clear _prev to prevent code in mount, patch, or insert from t…
andrewiggins Jan 22, 2023
b0484d0
WIP: progress on children diffing doc
andrewiggins Jan 22, 2023
14a23f6
Fix unmounting
andrewiggins Jan 22, 2023
1337b1d
Add "summary" and some more notes to children algorithm
andrewiggins Jan 23, 2023
fce7ce4
WIP: progress on doc
andrewiggins Jan 26, 2023
d6bc741
Update getParentDom tests to use linked list structure
andrewiggins Jan 28, 2023
bc4bc4c
Improve getDomSibling tests
andrewiggins Jan 28, 2023
4bd061b
Fix up getDomSibling for linked list
andrewiggins Jan 29, 2023
7fd9f59
Rename getChildDom to getFirstDom
andrewiggins Jan 29, 2023
d759b08
Skip over non-renderable children in insertion loop
andrewiggins Jan 29, 2023
682399b
Apply refs in insertion loop
andrewiggins Jan 29, 2023
0576c45
Check if any children exist before running LDS
andrewiggins Jan 29, 2023
a43e559
Fix updating child pointer on parent internal
andrewiggins Jan 30, 2023
4ee0483
Fix forwardRef
andrewiggins Jan 30, 2023
f9aaf2c
Convert nested arrays into Fragments
andrewiggins Jan 30, 2023
6ccd7e9
Properly cleanup DOM in portal tests
andrewiggins Jan 30, 2023
1e3f694
Fix handling errors while unmounting
andrewiggins Jan 30, 2023
560d403
Correctly mount new DOM nodes while patching
andrewiggins Jan 31, 2023
9fe14d7
Skip over root nodes with different parentDOMs when inserting
andrewiggins Jan 31, 2023
1e22b22
Properly account and track null placeholders
andrewiggins Jan 31, 2023
5db19bd
Fix forward ref, pt. 2
andrewiggins Jan 31, 2023
6e6a5d6
Properly skip over non-renderable children in insertion loop
andrewiggins Jan 31, 2023
e3a5320
Convert nested arrays to Fragments, pt. 2
andrewiggins Jan 31, 2023
6db4fb6
Fix suspended hydration
andrewiggins Feb 1, 2023
0a0fa83
Move oldHead pointer on null placeholders
andrewiggins Feb 3, 2023
0d60437
Remove internal check around LDS loop
andrewiggins Feb 3, 2023
13ade35
Remove old commented out patchChildren code
andrewiggins Feb 6, 2023
1970840
Break up patchChildren into functions
andrewiggins Feb 6, 2023
ef7a9b6
Use microtick outside of events when rerendering
andrewiggins Feb 6, 2023
a5c25c3
Add fast path for diffing no moved Internals
andrewiggins Feb 6, 2023
b3d8fe7
=== BEGIN v11-linked-list-prev-index-nextDom === Use flags to track m…
andrewiggins Feb 3, 2023
9f7db6d
Remove older commented code
andrewiggins Feb 3, 2023
0354b3d
WIP: Walk loop forwards and pass along the boundary dom node in patch…
andrewiggins Feb 5, 2023
6be6e89
Fix null placeholder tracking
andrewiggins Feb 5, 2023
1b940e6
Fix Portal placement
andrewiggins Feb 5, 2023
cf64778
Fix up keyed test assertions
andrewiggins Feb 5, 2023
c251d82
Clean up patchChildren implementation
andrewiggins Feb 5, 2023
0e3c34c
Remove _prev and rename _prevLIS to _tempNext...
andrewiggins Feb 5, 2023
994b6e3
Start search from prevMatchedInternal._next
andrewiggins Feb 5, 2023
b65e1b6
Skip LIS when nothing moves
andrewiggins Feb 7, 2023
fba2129
Remove usage of boundary node parameter
andrewiggins Feb 7, 2023
5c955a1
Use Internal class
andrewiggins Feb 12, 2023
61985ec
Use key and type map instead of loop
andrewiggins Feb 12, 2023
72550c4
Use map instead of object for keys
andrewiggins Feb 12, 2023
869a20b
Use a single map for types and keys
andrewiggins Feb 12, 2023
5de56b0
Remove LIS algo
andrewiggins Feb 13, 2023
4e9b5e5
Removed unused code, namely LIS algo and keyed loop search
andrewiggins Feb 17, 2023
a9f0d59
Inline insertionLoop into patchChildren
andrewiggins Feb 17, 2023
51053fb
Remove _tempNext pointer
andrewiggins Feb 17, 2023
38bc3f6
Combine oldHead and internal variables in findMatches
andrewiggins Feb 17, 2023
a5310ad
Reuse vnode variable and remove normalizedVNode variable
andrewiggins Feb 17, 2023
d00f85c
Move building the key map into its own function to save some bytes
andrewiggins Feb 18, 2023
9e7cac2
Fix dom operation tests
andrewiggins Feb 18, 2023
2b042c0
Skip over internals that haven't been mounted yet when searching for …
andrewiggins Feb 18, 2023
ef08d9b
[debug] Remove WeakMap support check and improve options typing
andrewiggins Mar 6, 2023
77cb172
Improve typing inference on debug option hooks
andrewiggins Mar 7, 2023
1e505c5
Move markup validations to their own file
andrewiggins Mar 7, 2023
ccbaf42
Merge branch 'v11-benchmark-base' into v11-linked-list-prev-index-nex…
andrewiggins Jan 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions compat/src/forwardRef.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import { options } from 'preact';

let oldDiffHook = options._diff;
options._diff = (internal, vnode) => {
if (internal.type && internal.type._forwarded && vnode.ref) {
vnode.props.ref = vnode.ref;
vnode.ref = null;
internal.ref = null;
if (
internal.type &&
internal.type._forwarded &&
(internal.ref || (vnode && vnode.ref))
) {
if (vnode) {
vnode.props.ref = vnode.ref;
vnode.ref = null;
} else {
internal.props.ref = internal.ref;
internal.ref = null;
}
}
if (oldDiffHook) oldDiffHook(internal, vnode);
};
Expand Down
12 changes: 7 additions & 5 deletions compat/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
REACT_ELEMENT_TYPE,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
} from './render';
import { getChildDom } from '../../src/tree';
import { getFirstDom } from '../../src/tree';
export * from './scheduler';

const version = '17.0.2'; // trick libraries to think we are react
Expand Down Expand Up @@ -72,7 +72,7 @@ function cloneElement(element) {
* @returns {boolean}
*/
function unmountComponentAtNode(container) {
if (container._children) {
if (container._child) {
preactRender(null, container);
return true;
}
Expand All @@ -89,9 +89,11 @@ function findDOMNode(component) {
return null;
} else if (component.nodeType == 1) {
return component;
} else if (component._internal._child == null) {
return null;
}

return getChildDom(component._internal, 0);
return getFirstDom(component._internal._child, 0);
}

/**
Expand All @@ -112,11 +114,11 @@ const StrictMode = Fragment;

/**
* In React, `flushSync` flushes the entire tree and forces a rerender. It's
* implmented here as a no-op.
* implemented here as a no-op.
* @template Arg
* @template Result
* @param {(arg: Arg) => Result} callback function that runs before the flush
* @param {Arg} [arg] Optional arugment that can be passed to the callback
* @param {Arg} [arg] Optional argument that can be passed to the callback
* @returns
*/
const flushSync = (callback, arg) => callback(arg);
Expand Down
4 changes: 2 additions & 2 deletions compat/src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ Component.prototype.isReactComponent = {};
export function render(vnode, parent, callback) {
// React destroys any existing DOM nodes, see #1727
// ...but only on the first render, see #1828
if (parent._children == null) {
if (parent._child == null) {
parent.textContent = '';
}

preactRender(vnode, parent);
if (typeof callback == 'function') callback();

const internal = parent._children._children[0];
const internal = parent._child._child;
return internal ? internal._component : null;
}

Expand Down
21 changes: 12 additions & 9 deletions debug/src/component-stack.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { options, Fragment } from 'preact';
import { options as rawOptions, Fragment } from 'preact';

const options = /** @type {import('../../src/internal').Options} */ (rawOptions);

/**
* Get human readable name of the component/dom node
* @param {import('./internal').VNode} vnode
* @param {import('./internal').VNode} vnode
* @param {import('../../src/internal').ComponentChild} vnode
* @returns {string}
*/
export function getDisplayName(vnode) {
if (vnode.type === Fragment) {
return 'Fragment';
} else if (typeof vnode.type == 'function') {
return vnode.type.displayName || vnode.type.name;
} else if (typeof vnode.type == 'string') {
return vnode.type;
if (vnode != null && typeof vnode === 'object') {
if (vnode.type === Fragment) {
return 'Fragment';
} else if (typeof vnode.type == 'function') {
return vnode.type.displayName || vnode.type.name;
} else if (typeof vnode.type == 'string') {
return vnode.type;
}
}

return '#text';
Expand Down
128 changes: 52 additions & 76 deletions debug/src/debug.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { checkPropTypes } from './check-props';
import { options, Component } from 'preact';
import { options as rawOptions, Component } from 'preact';
import {
ELEMENT_NODE,
DOCUMENT_NODE,
Expand All @@ -12,16 +12,9 @@
getDisplayName
} from './component-stack';
import { IS_NON_DIMENSIONAL } from '../../compat/src/util';
import { validateTableMarkup } from './validateMarkup';

const isWeakMapSupported = typeof WeakMap == 'function';

function getClosestDomNodeParent(parent) {
if (!parent) return {};
if (typeof parent.type == 'function') {
return getClosestDomNodeParent(parent._parent);
}
return parent;
}
const options = /** @type {import('../../src/internal').Options} */ (rawOptions);

export function initDebug() {
setupComponentStack();
Expand All @@ -35,24 +28,32 @@
let oldCatchError = options._catchError;
let oldRoot = options._root;
let oldHook = options._hook;
const warnedComponents = !isWeakMapSupported
? null
: {
useEffect: new WeakMap(),
useLayoutEffect: new WeakMap(),
lazyPropTypes: new WeakMap()
};
const warnedComponents = {
useEffect: new WeakMap(),
useLayoutEffect: new WeakMap(),
lazyPropTypes: new WeakMap()
};
const deprecations = [];

options._catchError = (error, vnode, oldVNode) => {
let component = vnode && vnode._component;
options._catchError = catchErrorHook;
options._root = rootHook;
options._diff = diffHook;
options._hook = hookHook;
options.vnode = vnodeHook;
options.diffed = diffedHook;

/** @type {typeof options["_catchError"]} */
function catchErrorHook(error, internal) {
let component = internal && internal._component;
if (component && typeof error.then == 'function') {
const promise = error;
error = new Error(
`Missing Suspense. The throwing component was: ${getDisplayName(vnode)}`
`Missing Suspense. The throwing component was: ${getDisplayName(
internal
)}`
);

let parent = vnode;
let parent = internal;
for (; parent; parent = parent._parent) {
if (parent._component && parent._component._childDidSuspend) {
error = promise;
Expand All @@ -68,7 +69,7 @@
}

try {
oldCatchError(error, vnode, oldVNode);
oldCatchError(error, internal);

// when an error was handled by an ErrorBoundary we will nontheless emit an error
// event on the window object. This is to make up for react compatibility in dev mode
Expand All @@ -81,9 +82,10 @@
} catch (e) {
throw e;
}
};
}

options._root = (vnode, parentNode) => {
/** @type {typeof options["_root"]} */
function rootHook(vnode, parentNode) {
if (!parentNode) {
throw new Error(
'Undefined parent passed to render(), this is the second argument.\n' +
Expand All @@ -110,10 +112,16 @@
}

if (oldRoot) oldRoot(vnode, parentNode);
};
}

/** @type {typeof options["_diff"]} */
function diffHook(internal, vnode) {
if (vnode === null || typeof vnode !== 'object') {
// TODO: This isn't correct. We need these checks to run on mount
oldBeforeDiff(internal, vnode);
return;
}

options._diff = (internal, vnode) => {
if (vnode === null || typeof vnode !== 'object') return;
// Check if the user passed plain objects as children. Note that we cannot
// move this check into `options.vnode` because components can receive
// children in any shape they want (e.g.
Expand All @@ -126,7 +134,7 @@
);
}

let { type, _parent: parent } = internal;

Check failure on line 137 in debug/src/debug.js

View workflow job for this annotation

GitHub Actions / Build & Test

'parent' is assigned a value but never used. Allowed unused vars must match /^h|React|_[0-9]?$/

if (type === undefined) {
throw new Error(
Expand All @@ -153,44 +161,10 @@
);
}

let parentVNode = getClosestDomNodeParent(parent);
validateTableMarkup(internal);

hooksAllowed = true;

if (
(type === 'thead' || type === 'tfoot' || type === 'tbody') &&
parentVNode.type !== 'table'
) {
console.error(
'Improper nesting of table. Your <thead/tbody/tfoot> should have a <table> parent.' +
serializeVNode(internal) +
`\n\n${getOwnerStack(internal)}`
);
} else if (
type === 'tr' &&
parentVNode.type !== 'thead' &&
parentVNode.type !== 'tfoot' &&
parentVNode.type !== 'tbody' &&
parentVNode.type !== 'table'
) {
console.error(
'Improper nesting of table. Your <tr> should have a <thead/tbody/tfoot/table> parent.' +
serializeVNode(internal) +
`\n\n${getOwnerStack(internal)}`
);
} else if (type === 'td' && parentVNode.type !== 'tr') {
console.error(
'Improper nesting of table. Your <td> should have a <tr> parent.' +
serializeVNode(internal) +
`\n\n${getOwnerStack(internal)}`
);
} else if (type === 'th' && parentVNode.type !== 'tr') {
console.error(
'Improper nesting of table. Your <th> should have a <tr>.' +
serializeVNode(internal) +
`\n\n${getOwnerStack(internal)}`
);
}
let isCompatNode = '$$typeof' in vnode;
if (
internal.ref !== undefined &&
Expand Down Expand Up @@ -244,7 +218,6 @@
if (typeof internal.type == 'function' && internal.type.propTypes) {
if (
internal.type.displayName === 'Lazy' &&
warnedComponents &&
!warnedComponents.lazyPropTypes.has(internal.type)
) {
const m =
Expand Down Expand Up @@ -279,15 +252,16 @@
}

if (oldBeforeDiff) oldBeforeDiff(internal, vnode);
};
}

options._hook = (internal, index, type) => {
/** @type {typeof options["_hook"]} */
function hookHook(internal, index, type) {
if (!internal || !hooksAllowed) {
throw new Error('Hook can only be invoked from render methods.');
}

if (oldHook) oldHook(internal, index, type);
};
}

// Ideally we'd want to print a warning once per component, but we
// don't have access to the vnode that triggered it here. As a
Expand Down Expand Up @@ -327,7 +301,8 @@
// https://esbench.com/bench/6021ebd7d9c27600a7bfdba3
const deprecatedProto = Object.create({}, deprecatedAttributes);

options.vnode = vnode => {
/** @type {typeof options["vnode"]} */
function vnodeHook(vnode) {
const props = vnode.props;
if (props != null && ('__source' in props || '__self' in props)) {
Object.defineProperties(props, debugProps);
Expand All @@ -338,17 +313,18 @@
// eslint-disable-next-line
vnode.__proto__ = deprecatedProto;
if (oldVnode) oldVnode(vnode);
};
}

options.diffed = vnode => {
/** @type {typeof options["diffed"]} */
function diffedHook(internal) {
hooksAllowed = false;

if (oldDiffed) oldDiffed(vnode);
if (oldDiffed) oldDiffed(internal);

if (vnode._children != null) {
if (internal._children != null) {
const keys = [];
for (let i = 0; i < vnode._children.length; i++) {
const child = vnode._children[i];
for (let i = 0; i < internal._children.length; i++) {
const child = internal._children[i];
if (!child || child.key == null) continue;

const key = child.key;
Expand All @@ -357,8 +333,8 @@
'Following component has two or more children with the ' +
`same key attribute: "${key}". This may cause glitches and misbehavior ` +
'in rendering process. Component: \n\n' +
serializeVNode(vnode) +
`\n\n${getOwnerStack(vnode)}`
serializeVNode(internal) +
`\n\n${getOwnerStack(internal)}`
);

// Break early to not spam the console
Expand All @@ -368,7 +344,7 @@
keys.push(key);
}
}
};
}
}

const setState = Component.prototype.setState;
Expand Down
Loading
Loading