Skip to content

Commit

Permalink
Bound the memory use of init_hydrate
Browse files Browse the repository at this point in the history
Allocate the memory for `init_hydrate` in a single buffer and reuse this buffer internally with typed arrays to make memory usage easy to predict and potentially help performance.

Add a test for a reordering corner case.
  • Loading branch information
hbirler committed Jun 12, 2021
1 parent 4f161fb commit d10b9ec
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 32 deletions.
139 changes: 107 additions & 32 deletions src/runtime/internal/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,35 @@ function init_hydrate(target: NodeEx) {
if (target.hydrate_init) return;
target.hydrate_init = true;

type NodeEx2 = NodeEx & {claim_order: number};

// We know that all children have claim_order values since the unclaimed have been detached
const children = target.childNodes as NodeListOf<NodeEx2>;
const children = Array.from(target.childNodes as NodeListOf<NodeEx & {claim_order: number}>);

const childCount = children.length;

/*
* The memory used by this algorithm (except for `children`) is entirely contained in this buffer
* This buffer is split into multiple chunks that are reused
*
*
* chunk0 chunk1 chunk2
* size=c size=c+1 size=c+1
* |-----------------|------------------|------------------|
* |---claimOrders---|--------m---------|---------p-------*|
* |---claimOrders---|-lis-*---toMove--*|--anchors--*--bc--|
*
* c: childCount
* bc: bucketCounts
*/
const buffer = new ArrayBuffer((childCount * 3 + 2) * 4);

const chunk0start = 0;
const chunk1start = childCount * 4;
const chunk2start = childCount * 8 + 4;

const claimOrders = new Int32Array(buffer, chunk0start, childCount);
for (let i = 0; i < childCount; i++) {
claimOrders[i] = children[i].claim_order;
}

/*
* Reorder claimed children optimally.
Expand All @@ -60,18 +85,18 @@ function init_hydrate(target: NodeEx) {

// Compute longest increasing subsequence
// m: subsequence length j => index k of smallest value that ends an increasing subsequence of length j
const m = new Int32Array(children.length + 1);
const m = new Int32Array(buffer, chunk1start, childCount + 1);
// Predecessor indices + 1
const p = new Int32Array(children.length);
const p = new Int32Array(buffer, chunk2start, childCount);

m[0] = -1;
let longest = 0;
for (let i = 0; i < children.length; i++) {
const current = children[i].claim_order;
for (let i = 0; i < claimOrders.length; i++) {
const current = claimOrders[i];
// Find the largest subsequence length such that it ends in a value less than our current value

// upper_bound returns first greater value, so we subtract one
const seqLen = upper_bound(1, longest + 1, idx => children[m[idx]].claim_order, current) - 1;
const seqLen = upper_bound(1, longest + 1, idx => claimOrders[m[idx]], current) - 1;

p[i] = m[seqLen] + 1;

Expand All @@ -83,36 +108,86 @@ function init_hydrate(target: NodeEx) {
longest = Math.max(newLen, longest);
}

// The longest increasing subsequence of nodes (initially reversed)
const lis: NodeEx2[] = [];
for (let cur = m[longest] + 1; cur != 0; cur = p[cur - 1]) {
const node = children[cur - 1];
lis.push(node);
node.is_in_lis = true;
}
lis.reverse();
const start = m[longest] + 1;
// The longest increasing subsequence of nodes (initially reversed).
const lis = new Int32Array(buffer, chunk1start, longest);
// The nodes that are not in lis. Reuse m since it won't be used again
const toMove = new Int32Array(buffer, chunk1start + lis.length * 4, childCount - lis.length);

for (let cur = start, lisInd = longest - 1; cur != 0; cur = p[cur - 1], --lisInd) {
lis[lisInd] = cur - 1;
}

let toMoveInd = 0;
let j = 0;
for (let i = 0; i < lis.length; j++, i++) {
const cur = lis[i];
for (; j < cur; j++, toMoveInd++) {
toMove[toMoveInd] = j;
}
}
for (; j < claimOrders.length; j++, toMoveInd++) {
toMove[toMoveInd] = j;
}

// The lis node that a node will be moved before
const anchors = new Int32Array(buffer, chunk2start, childCount - longest);
// The number of nodes per bucket. A bucket is the space between two lis node (or the space before the first or after the last lis node)
const bucketCounts = new Int32Array(buffer, chunk2start + anchors.length * 4, longest + 1);

for (let i = 0; i < toMove.length; i++) {
const idx = upper_bound(0, lis.length, idx => claimOrders[lis[idx]], claimOrders[toMove[i]]);
anchors[i] = idx;
bucketCounts[idx]++;
}

// Compute the prefix sum of bucketCounts to get the bucket offsets
for (let i = 0, sum = 0; i < bucketCounts.length; i++) {
const cur = sum;
sum += bucketCounts[i];
bucketCounts[i] = cur;
}

// Move all nodes that aren't in the longest increasing subsequence
const toMove = lis.map(() => [] as NodeEx2[]);
// For the nodes at the end
toMove.push([]);
for (let i = 0; i < children.length; i++) {
const node = children[i];
if (!node.is_in_lis) {
const idx = upper_bound(0, lis.length, idx => lis[idx].claim_order, node.claim_order);
toMove[idx].push(node);
// Organize the toMove nodes by buckets, in-place
// Takes O(toMove.length) time since:
// * Outer loop visits an index only once
// * A node is moved to the right position (inner kernel) at most once
// * Thus there is O(1) operations per node/index
for (let i = 0; i < toMove.length; i++) {
while (anchors[i] !== -1) {
const targetBucket = anchors[i];
const targetInd = bucketCounts[targetBucket];
bucketCounts[targetBucket] += 1;

// We move i to the right position
[toMove[targetInd], toMove[i]] = [toMove[i], toMove[targetInd]];
[anchors[targetInd], anchors[i]] = [anchors[i], anchors[targetInd]];
anchors[targetInd] = -1;
}
}

toMove.forEach((lst, idx) => {
// Make the insertBefore calls for the toMove nodes
let last = 0;
for (let i = 0; i < lis.length + 1; i++) {
const cur = bucketCounts[i];
if (last == cur) {
// The bucket is empty
continue;
}
const curToMove = toMove.subarray(last, cur);
const anchorNode = i < lis.length ? children[lis[i]] : null;
// We sort the nodes being moved to guarantee that their insertion order matches the claim order
lst.sort((a, b) => a.claim_order - b.claim_order);
// Only sorting within buckets is enough
curToMove.sort((a, b) => claimOrders[a] - claimOrders[b]);

const anchor = idx < lis.length ? lis[idx] : null;
lst.forEach(n => {
target.insertBefore(n, anchor);
});
});
// Move the nodes
for (const cInd of curToMove) {
const node = children[cInd];
target.insertBefore(node, anchorNode);
}

last = cur;
}
}

export function append(target: NodeEx, node: NodeEx) {
Expand Down
3 changes: 3 additions & 0 deletions test/hydration/samples/ordering/_after.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<h1></h1><h2></h2><h3></h3><h4></h4><h5></h5><h6></h6>
</div>
3 changes: 3 additions & 0 deletions test/hydration/samples/ordering/_before.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<h3></h3><h4></h4><h2></h2><h1></h1><h5></h5><h6></h6>
</div>
3 changes: 3 additions & 0 deletions test/hydration/samples/ordering/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<h1></h1><h2></h2><h3></h3><h4></h4><h5></h5><h6></h6>
</div>

0 comments on commit d10b9ec

Please sign in to comment.