Refactored owners list to use cached metadata (and added more tests)

This commit is contained in:
Brian Vaughn
2019-04-25 15:06:24 -07:00
parent 15eacae02c
commit 09029acf00
3 changed files with 306 additions and 34 deletions
@@ -1,20 +1,121 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Store owners list should drill through interleaved intermediate components: 1: mount 1`] = `
[root]
▾ <Root>
▾ <Intermediate key="intermediate">
<Leaf key="leaf">
▾ <Wrapper key="wrapper">
<Leaf>
<Leaf key="leaf">
`;
exports[`Store owners list should drill through interleaved intermediate components: 2: components owned by <Root> 1`] = `
" ▾ <Root>
▾ <Intermediate key=\\"intermediate\\">
<Leaf>
<Leaf key=\\"leaf\\">"
`;
exports[`Store owners list should drill through interleaved intermediate components: 3: components owned by <Intermediate> 1`] = `
" ▾ <Intermediate key=\\"intermediate\\">
<Leaf key=\\"leaf\\">
▾ <Wrapper key=\\"wrapper\\">"
`;
exports[`Store owners list should drill through intermediate components: 1: mount 1`] = `
[root]
▾ <Root>
▾ <Intermediate>
▾ <Wrapper>
<Leaf key="children">
<Leaf>
`;
exports[`Store owners list should drill through intermediate components: 2: components owned by <Root> 1`] = `
" ▾ <Root>
▾ <Intermediate>
<Leaf key=\\"children\\">"
<Leaf>"
`;
exports[`Store owners list should drill through intermediate components: 3: components owned by <Intermediate> 1`] = `
" ▾ <Intermediate>
▾ <Wrapper>"
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 1: mount 1`] = `
[root]
▾ <Root>
▾ <Intermediate>
▾ <Wrapper>
<Leaf>
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 2: components owned by <Root> 1`] = `
" ▾ <Root>
▾ <Intermediate>
<Leaf>"
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 3: update to add direct 1`] = `
[root]
▾ <Root>
<Leaf>
▾ <Intermediate>
▾ <Wrapper>
<Leaf>
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 4: components owned by <Root> 1`] = `
" ▾ <Root>
<Leaf>
▾ <Intermediate>
<Leaf>"
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 5: update to remove indirect 1`] = `
[root]
▾ <Root>
<Leaf>
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 6: components owned by <Root> 1`] = `
" ▾ <Root>
<Leaf>"
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 7: update to remove both 1`] = `
[root]
<Root>
`;
exports[`Store owners list should show the proper owners list order and contents after insertions and deletions: 8: components owned by <Root> 1`] = `" <Root>"`;
exports[`Store owners list should show the proper owners list ordering after reordered children: 1: mount (ascending) 1`] = `
[root]
▾ <Root>
<Leaf key="A">
<Leaf key="B">
<Leaf key="C">
`;
exports[`Store owners list should show the proper owners list ordering after reordered children: 2: components owned by <Root> 1`] = `
" ▾ <Root>
<Leaf key=\\"A\\">
<Leaf key=\\"B\\">
<Leaf key=\\"C\\">"
`;
exports[`Store owners list should show the proper owners list ordering after reordered children: 3: update (descending) 1`] = `
[root]
▾ <Root>
<Leaf key="C">
<Leaf key="B">
<Leaf key="A">
`;
exports[`Store owners list should show the proper owners list ordering after reordered children: 4: components owned by <Root> 1`] = `
" ▾ <Root>
<Leaf key=\\"C\\">
<Leaf key=\\"B\\">
<Leaf key=\\"A\\">"
`;
+122 -1
View File
@@ -27,7 +27,9 @@ describe('Store owners list', () => {
it('should drill through intermediate components', () => {
const Root = () => (
<Intermediate>
<Leaf key="children" />
<div>
<Leaf />
</div>
</Intermediate>
);
const Wrapper = ({ children }) => children;
@@ -47,4 +49,123 @@ describe('Store owners list', () => {
printOwnersList(store.getOwnersListForElement(intermediateID))
).toMatchSnapshot('3: components owned by <Intermediate>');
});
it('should drill through interleaved intermediate components', () => {
const Root = () => [
<Intermediate key="intermediate">
<Leaf />
</Intermediate>,
<Leaf key="leaf" />,
];
const Wrapper = ({ children }) => children;
const Leaf = () => <div>Leaf</div>;
const Intermediate = ({ children }) => [
<Leaf key="leaf" />,
<Wrapper key="wrapper">{children}</Wrapper>,
];
act(() => ReactDOM.render(<Root />, document.createElement('div')));
expect(store).toMatchSnapshot('1: mount');
const rootID = store.getElementIDAtIndex(0);
expect(
printOwnersList(store.getOwnersListForElement(rootID))
).toMatchSnapshot('2: components owned by <Root>');
const intermediateID = store.getElementIDAtIndex(1);
expect(
printOwnersList(store.getOwnersListForElement(intermediateID))
).toMatchSnapshot('3: components owned by <Intermediate>');
});
it('should show the proper owners list order and contents after insertions and deletions', () => {
const Root = ({ includeDirect, includeIndirect }) => (
<div>
{includeDirect ? <Leaf /> : null}
{includeIndirect ? (
<Intermediate>
<Leaf />
</Intermediate>
) : null}
</div>
);
const Wrapper = ({ children }) => children;
const Leaf = () => <div>Leaf</div>;
const Intermediate = ({ children }) => <Wrapper>{children}</Wrapper>;
const container = document.createElement('div');
act(() =>
ReactDOM.render(
<Root includeDirect={false} includeIndirect={true} />,
container
)
);
expect(store).toMatchSnapshot('1: mount');
const rootID = store.getElementIDAtIndex(0);
expect(
printOwnersList(store.getOwnersListForElement(rootID))
).toMatchSnapshot('2: components owned by <Root>');
act(() =>
ReactDOM.render(
<Root includeDirect={true} includeIndirect={true} />,
container
)
);
expect(store).toMatchSnapshot('3: update to add direct');
expect(
printOwnersList(store.getOwnersListForElement(rootID))
).toMatchSnapshot('4: components owned by <Root>');
act(() =>
ReactDOM.render(
<Root includeDirect={true} includeIndirect={false} />,
container
)
);
expect(store).toMatchSnapshot('5: update to remove indirect');
expect(
printOwnersList(store.getOwnersListForElement(rootID))
).toMatchSnapshot('6: components owned by <Root>');
act(() =>
ReactDOM.render(
<Root includeDirect={false} includeIndirect={false} />,
container
)
);
expect(store).toMatchSnapshot('7: update to remove both');
expect(
printOwnersList(store.getOwnersListForElement(rootID))
).toMatchSnapshot('8: components owned by <Root>');
});
it('should show the proper owners list ordering after reordered children', () => {
const Root = ({ ascending }) =>
ascending
? [<Leaf key="A" />, <Leaf key="B" />, <Leaf key="C" />]
: [<Leaf key="C" />, <Leaf key="B" />, <Leaf key="A" />];
const Leaf = () => <div>Leaf</div>;
const container = document.createElement('div');
act(() => ReactDOM.render(<Root ascending={true} />, container));
expect(store).toMatchSnapshot('1: mount (ascending)');
const rootID = store.getElementIDAtIndex(0);
expect(
printOwnersList(store.getOwnersListForElement(rootID))
).toMatchSnapshot('2: components owned by <Root>');
act(() => ReactDOM.render(<Root ascending={false} />, container));
expect(store).toMatchSnapshot('3: update (descending)');
expect(
printOwnersList(store.getOwnersListForElement(rootID))
).toMatchSnapshot('4: components owned by <Root>');
});
});
+81 -31
View File
@@ -81,6 +81,10 @@ export default class Store extends EventEmitter {
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
_isProfiling: boolean = false;
// Map of element (id) to the set of elements (ids) it owns.
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
_ownersMap: Map<number, Set<number>> = new Map();
// Suspense cache for reading profiling data.
_profilingCache: ProfilingCache;
@@ -406,10 +410,65 @@ export default class Store extends EventEmitter {
return index;
}
getOwnersListForElement(id: number): Array<Element> {
getOwnersListForElement(ownerID: number): Array<Element> {
const list = [];
let element = this._idToElement.get(ownerID);
if (element != null) {
list.push({
...element,
depth: 0,
});
this._populateOwnersList(id, id, 0, list);
const unsortedIDs = this._ownersMap.get(ownerID);
if (unsortedIDs !== undefined) {
const depthMap: Map<number, number> = new Map([[ownerID, 0]]);
// Items in a set are ordered based on insertion.
// This does not correlate with their order in the tree.
// So first we need to order them.
// I wish we could avoid this sorting operation; we could sort at insertion time,
// but then we'd have to pay sorting costs even if the owners list was never used.
// Seems better to defer the cost, since the set of ids is probably pretty small.
const sortedIDs = Array.from(unsortedIDs).sort(
(idA, idB) =>
((this.getIndexOfElementID(idA): any): number) -
((this.getIndexOfElementID(idB): any): number)
);
// Next we need to determine the appropriate depth for each element in the list.
// The depth in the list may not correspond to the depth in the tree,
// because the list has been filtered to remove intermediate components.
// Perhaps the easiest way to do this is to walk up the tree until we reach either:
// (1) another node that's already in the tree, or (2) the root (owner)
// at which point, our depth is just the depth of that node plus one.
sortedIDs.forEach(id => {
const element = this._idToElement.get(id);
if (element != null) {
let parentID = element.parentID;
let depth = 0;
while (parentID > 0) {
if (parentID === ownerID || unsortedIDs.has(parentID)) {
depth = depthMap.get(parentID) + 1;
depthMap.set(id, depth);
break;
}
const parent = this._idToElement.get(parentID);
if (parent == null) {
break;
}
parentID = parent.parentID;
}
if (depth === 0) {
throw Error('Invalid owners list');
}
list.push({ ...element, depth });
}
});
}
}
return list;
}
@@ -561,32 +620,6 @@ export default class Store extends EventEmitter {
THROTTLE_CAPTURE_SCREENSHOT_DURATION
);
_populateOwnersList(
id: number,
ownerID: number,
depth: number,
list: Array<Element>
) {
const element = this._idToElement.get(id);
if (element != null) {
const isInList = id === ownerID || element.ownerID === ownerID;
if (isInList) {
list.push({
...element,
depth: depth,
});
}
element.children.forEach(childID =>
this._populateOwnersList(
childID,
ownerID,
isInList ? depth + 1 : depth,
list
)
);
}
}
_takeProfilingSnapshotRecursive = (id: number) => {
const element = this.getElementByID(id);
if (element !== null) {
@@ -779,6 +812,15 @@ export default class Store extends EventEmitter {
this._idToElement.set(id, element);
addedElementIDs.push(id);
this._adjustParentTreeWeight(parentElement, 1);
if (ownerID > 0) {
let set = this._ownersMap.get(ownerID);
if (set === undefined) {
set = new Set();
this._ownersMap.set(ownerID, set);
}
set.add(id);
}
}
break;
}
@@ -798,13 +840,13 @@ export default class Store extends EventEmitter {
i = i + 1;
const element = ((this._idToElement.get(id): any): Element);
if (element.children.length > 0) {
const { children, ownerID, parentID, weight } = element;
if (children.length > 0) {
throw new Error(`Node ${id} was removed before its children.`);
}
this._idToElement.delete(id);
const parentID = element.parentID;
let parentElement = null;
if (parentID === 0) {
if (__DEBUG__) {
@@ -830,8 +872,16 @@ export default class Store extends EventEmitter {
parentElement.children.splice(index, 1);
}
this._adjustParentTreeWeight(parentElement, -element.weight);
this._adjustParentTreeWeight(parentElement, -weight);
removedElementIDs.set(id, parentID);
this._ownersMap.delete(id);
if (ownerID > 0) {
const set = this._ownersMap.get(ownerID);
if (set !== undefined) {
set.delete(id);
}
}
}
break;
}