-
Notifications
You must be signed in to change notification settings - Fork 637
fix: Use map for efficient array reconiliation #2280
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -350,6 +350,30 @@ export function array<IT extends IAnyType>(subtype: IT): IArrayType<IT> { | |
| return new ArrayType<IT>(`${subtype.name}[]`, subtype) | ||
| } | ||
|
|
||
| function buildObjectByIdMap(nodes: AnyNode[]): [Set<string>, Map<string, Array<AnyNode>> | null] { | ||
| // Creates a map of node by identifier value. | ||
| // In theory, several nodes can have the same identifier, if the array contains different types, so every identifier is mapped to an array of nodes with the same values. | ||
| // In practice this in probably a rare case, so we can live with the performance hit. | ||
| // | ||
| // If not all nodes have identifier, we can't use the map for lookups, so we return null. | ||
|
Comment on lines
+354
to
+358
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Love that you've thought through this! I'd highly recommend capturing various edge cases in test (if we haven't already done so). Some cases that come to mind would be tests for arrays of:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, of course these should be tested. I'll try to add those soon |
||
| const identifierAttributes = new Set<string>() | ||
| const keyToObjectMap = new Map<string, Array<AnyObjectNode>>() | ||
|
|
||
| for (const node of nodes) { | ||
| if (node instanceof ObjectNode && node.identifierAttribute && node.identifier !== null) { | ||
| identifierAttributes.add(node.identifierAttribute) | ||
| const key = node.identifier | ||
| if (!keyToObjectMap.has(key)) { | ||
| keyToObjectMap.set(key, []) | ||
| } | ||
| keyToObjectMap.get(key)!.push(node) | ||
| } else { | ||
| return [identifierAttributes, null] // Not all nodes have identifier, so we can't use the map. | ||
| } | ||
| } | ||
| return [identifierAttributes, keyToObjectMap] | ||
| } | ||
|
|
||
| function reconcileArrayChildren<TT>( | ||
| parent: AnyObjectNode, | ||
| childType: IType<any, any, TT>, | ||
|
|
@@ -359,6 +383,8 @@ function reconcileArrayChildren<TT>( | |
| ): AnyNode[] | null { | ||
| let nothingChanged = true | ||
|
|
||
| const [identifierAttributes, oldNodeMap] = buildObjectByIdMap(oldNodes) | ||
|
|
||
| for (let i = 0; ; i++) { | ||
| const hasNewNode = i <= newValues.length - 1 | ||
| const oldNode = oldNodes[i] | ||
|
|
@@ -404,14 +430,28 @@ function reconcileArrayChildren<TT>( | |
| // nothing to do, try to reorder | ||
| let oldMatch = undefined | ||
|
|
||
| // find a possible candidate to reuse | ||
| for (let j = i; j < oldNodes.length; j++) { | ||
| if (areSame(oldNodes[j], newValue)) { | ||
| oldMatch = oldNodes.splice(j, 1)[0] | ||
| break | ||
| // Try to find match by identifier attributes | ||
| if (oldNodeMap && typeof newValue === "object" && newValue !== null) { | ||
| for (const identifierAttribute of identifierAttributes) { | ||
| if (!(identifierAttribute in newValue)) { | ||
| continue | ||
| } | ||
| const identifierValue = (newValue as any)[identifierAttribute] | ||
thegedge marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const matchingNodes = oldNodeMap.get(identifierValue) || [] | ||
|
|
||
| oldMatch = matchingNodes.find(node => areSame(node, newValue)) | ||
| if (oldMatch) { | ||
| break | ||
| } | ||
| } | ||
| } else { | ||
| for (let j = i; j < oldNodes.length; j++) { | ||
| if (areSame(oldNodes[j], newValue)) { | ||
| oldMatch = oldNodes.splice(j, 1)[0] | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| nothingChanged = false | ||
| const newNode = valueAsNode(childType, parent, newPath, newValue, oldMatch) | ||
| oldNodes.splice(i, 0, newNode) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line of your comment is an interesting one. Basically, it states that we still have the same worst case complexity.
That being said, in those scenarios we're likely to fall back to the old code path. In the case where we have an array with models of the same type containing identifiers, we get a nice perf boost 🚀
(nothing to action here, I just like talking things through)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I'm trying to understand when it's OK to use the map and when it isn't...