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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control flow list diffing faster update #52227

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 29 additions & 17 deletions packages/core/src/render3/instructions/control_flow.ts
Expand Up @@ -101,7 +101,9 @@ export function 傻傻repeaterTrackByIdentity<T>(_: number, value: T) {
}

class RepeaterMetadata {
constructor(public hasEmptyBlock: boolean, public trackByFn: TrackByFunction<unknown>) {}
constructor(
public hasEmptyBlock: boolean, public trackByFn: TrackByFunction<unknown>,
public liveCollection?: LiveCollectionLContainerImpl) {}
}

/**
Expand Down Expand Up @@ -153,27 +155,23 @@ export function 傻傻repeaterCreate(
}

class LiveCollectionLContainerImpl extends
LiveCollection<LView<RepeaterContext<unknown>>, RepeaterContext<unknown>> {
LiveCollection<LView<RepeaterContext<unknown>>, unknown> {
/**
Property indicating if indexes in the repeater context need to be updated following the live
collection changes. Index updates are necessary if and only if views are inserted / removed in
the middle of LContainer. Adds and removals at the end don't require index updates.
*/
private needsIndexUpdate = false;
constructor(
private lContainer: LContainer, private hostLView: LView, private templateTNode: TNode,
private trackByFn: TrackByFunction<unknown>) {
private lContainer: LContainer, private hostLView: LView, private templateTNode: TNode) {
super();
}

override get length(): number {
return this.lContainer.length - CONTAINER_HEADER_OFFSET;
}
override at(index: number): LView<RepeaterContext<unknown>> {
return getExistingLViewFromLContainer(this.lContainer, index);
}
override key(index: number): unknown {
return this.trackByFn(index, this.at(index)[CONTEXT].$implicit);
override at(index: number): unknown {
return this.getLView(index)[CONTEXT].$implicit;
}
override attach(index: number, lView: LView<RepeaterContext<unknown>>): void {
const dehydratedView = lView[HYDRATION] as DehydratedContainerView;
Expand All @@ -198,16 +196,24 @@ class LiveCollectionLContainerImpl extends
destroyLView(lView[TVIEW], lView);
}
override updateValue(index: number, value: unknown): void {
this.at(index)[CONTEXT].$implicit = value;
this.getLView(index)[CONTEXT].$implicit = value;
}

reset() {
this.needsIndexUpdate = false;
}

updateIndexes() {
if (this.needsIndexUpdate) {
for (let i = 0; i < this.length; i++) {
this.at(i)[CONTEXT].$index = i;
this.getLView(i)[CONTEXT].$index = i;
}
}
}

private getLView(index: number): LView<RepeaterContext<unknown>> {
return getExistingLViewFromLContainer(this.lContainer, index);
}
}

/**
Expand All @@ -225,12 +231,18 @@ export function 傻傻repeater(
const hostLView = getLView();
const hostTView = hostLView[TVIEW];
const metadata = hostLView[HEADER_OFFSET + metadataSlotIdx] as RepeaterMetadata;
const containerIndex = metadataSlotIdx + 1;
const lContainer = getLContainer(hostLView, HEADER_OFFSET + containerIndex);
const itemTemplateTNode = getExistingTNode(hostTView, containerIndex);

const liveCollection = new LiveCollectionLContainerImpl(
lContainer, hostLView, itemTemplateTNode, metadata.trackByFn);
if (metadata.liveCollection === undefined) {
const containerIndex = metadataSlotIdx + 1;
const lContainer = getLContainer(hostLView, HEADER_OFFSET + containerIndex);
const itemTemplateTNode = getExistingTNode(hostTView, containerIndex);
metadata.liveCollection =
new LiveCollectionLContainerImpl(lContainer, hostLView, itemTemplateTNode);
} else {
metadata.liveCollection.reset();
}

const liveCollection = metadata.liveCollection;
reconcile(liveCollection, collection, metadata.trackByFn);

// moves in the container might caused context's index to get out of order, re-adjust if needed
Expand All @@ -239,7 +251,7 @@ export function 傻傻repeater(
// handle empty blocks
if (metadata.hasEmptyBlock) {
const bindingIndex = nextBindingIndex();
const isCollectionEmpty = lContainer.length - CONTAINER_HEADER_OFFSET === 0;
const isCollectionEmpty = liveCollection.length === 0;
if (bindingUpdated(hostLView, bindingIndex, isCollectionEmpty)) {
const emptyTemplateIndex = metadataSlotIdx + 2;
const lContainerForEmpty = getLContainer(hostLView, HEADER_OFFSET + emptyTemplateIndex);
Expand Down
98 changes: 64 additions & 34 deletions packages/core/src/render3/list_reconciliation.ts
Expand Up @@ -15,8 +15,7 @@ import {TrackByFunction} from '../change_detection';
*/
export abstract class LiveCollection<T, V> {
abstract get length(): number;
abstract at(index: number): T;
abstract key(index: number): unknown;
abstract at(index: number): V;
abstract attach(index: number, item: T): void;
abstract detach(index: number): T;
abstract create(index: number, value: V): T;
Expand Down Expand Up @@ -47,6 +46,20 @@ export abstract class LiveCollection<T, V> {
}
}

function valuesMatching<V>(
liveIdx: number, liveValue: V, newIdx: number, newValue: V,
trackBy: TrackByFunction<V>): number {
if (liveIdx === newIdx && Object.is(liveValue, newValue)) {
// matching and no value identity to update
return 1;
Copy link
Member

Choose a reason for hiding this comment

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

We can make constants for these (or a const enum) to give semantic meaning to the return values.

} else if (Object.is(trackBy(liveIdx, liveValue), trackBy(newIdx, newValue))) {
// matching but requires value identity update
return -1;
}

return 0;
}

/**
* The live collection reconciliation algorithm that perform various in-place operations, so it
* reflects the content of the new (incoming) collection.
Expand Down Expand Up @@ -84,41 +97,50 @@ export function reconcile<T, V>(

while (liveStartIdx <= liveEndIdx && liveStartIdx <= newEndIdx) {
// compare from the beginning
const liveStartKey = liveCollection.key(liveStartIdx);
const liveStartValue = liveCollection.at(liveStartIdx);
const newStartValue = newCollection[liveStartIdx];
const newStartKey = trackByFn(liveStartIdx, newStartValue);
if (Object.is(liveStartKey, newStartKey)) {
liveCollection.updateValue(liveStartIdx, newStartValue);
const isStartMatching =
valuesMatching(liveStartIdx, liveStartValue, liveStartIdx, newStartValue, trackByFn);
if (isStartMatching !== 0) {
if (isStartMatching < 0) {
liveCollection.updateValue(liveStartIdx, newStartValue);
}
liveStartIdx++;
continue;
}

// compare from the end
// TODO(perf): do _all_ the matching from the end
const liveEndKey = liveCollection.key(liveEndIdx);
const newEndItem = newCollection[newEndIdx];
const newEndKey = trackByFn(newEndIdx, newEndItem);
if (Object.is(liveEndKey, newEndKey)) {
liveCollection.updateValue(liveEndIdx, newEndItem);
const liveEndValue = liveCollection.at(liveEndIdx);
const newEndValue = newCollection[newEndIdx];
const isEndMatching =
valuesMatching(liveEndIdx, liveEndValue, newEndIdx, newEndValue, trackByFn);
if (isEndMatching !== 0) {
if (isEndMatching < 0) {
liveCollection.updateValue(liveEndIdx, newEndValue);
}
liveEndIdx--;
newEndIdx--;
continue;
}

// Detect swap / moves:
if (Object.is(newStartKey, liveEndKey) && Object.is(newEndKey, liveStartKey)) {
// swap on both ends;
liveCollection.swap(liveStartIdx, liveEndIdx);
liveCollection.updateValue(liveStartIdx, newStartValue);
liveCollection.updateValue(liveEndIdx, newEndItem);
newEndIdx--;
liveStartIdx++;
liveEndIdx--;
continue;
} else if (Object.is(newStartKey, liveEndKey)) {
// the new item is the same as the live item with the end pointer - this is a move forward
// to an earlier index;
liveCollection.move(liveEndIdx, liveStartIdx);
// Detect swap and moves:
const liveStartKey = trackByFn(liveStartIdx, liveStartValue);
const liveEndKey = trackByFn(liveEndIdx, liveEndValue);
const newStartKey = trackByFn(liveStartIdx, newStartValue);
if (Object.is(newStartKey, liveEndKey)) {
const newEndKey = trackByFn(newEndIdx, newEndValue);
// detect swap on both ends;
if (Object.is(newEndKey, liveStartKey)) {
liveCollection.swap(liveStartIdx, liveEndIdx);
liveCollection.updateValue(liveEndIdx, newEndValue);
newEndIdx--;
liveEndIdx--;
} else {
// the new item is the same as the live item with the end pointer - this is a move forward
// to an earlier index;
liveCollection.move(liveEndIdx, liveStartIdx);
}
liveCollection.updateValue(liveStartIdx, newStartValue);
liveStartIdx++;
continue;
Expand All @@ -127,7 +149,8 @@ export function reconcile<T, V>(
// Fallback to the slow path: we need to learn more about the content of the live and new
// collections.
detachedItems ??= new MultiMap();
liveKeysInTheFuture ??= initLiveItemsInTheFuture(liveCollection, liveStartIdx, liveEndIdx);
liveKeysInTheFuture ??=
initLiveItemsInTheFuture(liveCollection, liveStartIdx, liveEndIdx, trackByFn);

// Check if I'm inserting a previously detached item: if so, attach it here
if (attachPreviouslyDetached(liveCollection, detachedItems, liveStartIdx, newStartKey)) {
Expand Down Expand Up @@ -162,19 +185,24 @@ export function reconcile<T, V>(
const newCollectionIterator = newCollection[Symbol.iterator]();
let newIterationResult = newCollectionIterator.next();
while (!newIterationResult.done && liveStartIdx <= liveEndIdx) {
const liveValue = liveCollection.at(liveStartIdx);
const newValue = newIterationResult.value;
const newKey = trackByFn(liveStartIdx, newValue);
const liveKey = liveCollection.key(liveStartIdx);
if (Object.is(liveKey, newKey)) {
// found a match - move on
liveCollection.updateValue(liveStartIdx, newValue);
const isStartMatching =
valuesMatching(liveStartIdx, liveValue, liveStartIdx, newValue, trackByFn);
if (isStartMatching !== 0) {
// found a match - move on, but update value
if (isStartMatching < 0) {
liveCollection.updateValue(liveStartIdx, newValue);
}
liveStartIdx++;
newIterationResult = newCollectionIterator.next();
} else {
detachedItems ??= new MultiMap();
liveKeysInTheFuture ??= initLiveItemsInTheFuture(liveCollection, liveStartIdx, liveEndIdx);
liveKeysInTheFuture ??=
initLiveItemsInTheFuture(liveCollection, liveStartIdx, liveEndIdx, trackByFn);

// Check if I'm inserting a previously detached item: if so, attach it here
const newKey = trackByFn(liveStartIdx, newValue);
if (attachPreviouslyDetached(liveCollection, detachedItems, liveStartIdx, newKey)) {
liveCollection.updateValue(liveStartIdx, newValue);
liveStartIdx++;
Expand All @@ -187,6 +215,7 @@ export function reconcile<T, V>(
newIterationResult = newCollectionIterator.next();
} else {
// it is a move forward - detach the current item without advancing in collections
const liveKey = trackByFn(liveStartIdx, liveValue);
detachedItems.set(liveKey, liveCollection.detach(liveStartIdx));
liveEndIdx--;
}
Expand Down Expand Up @@ -236,10 +265,11 @@ function createOrAttach<T, V>(
}

function initLiveItemsInTheFuture<T>(
liveCollection: LiveCollection<unknown, unknown>, start: number, end: number): Set<unknown> {
liveCollection: LiveCollection<unknown, unknown>, start: number, end: number,
trackByFn: TrackByFunction<unknown>): Set<unknown> {
const keys = new Set();
for (let i = start; i <= end; i++) {
keys.add(liveCollection.key(i));
keys.add(trackByFn(i, liveCollection.at(i)));
}
return keys;
}
Expand Down