Skip to content

Commit

Permalink
Tree-shake SparseSnapshotTree (#4523)
Browse files Browse the repository at this point in the history
  • Loading branch information
hsubox76 committed Feb 25, 2021
1 parent 89a4f64 commit 261617b
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 196 deletions.
65 changes: 41 additions & 24 deletions packages/database/src/core/Repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import {
pathGetFront,
pathPopFront
} from './util/Path';
import { SparseSnapshotTree } from './SparseSnapshotTree';
import {
newSparseSnapshotTree,
sparseSnapshotTreeForEachTree,
sparseSnapshotTreeForget,
sparseSnapshotTreeRemember
} from './SparseSnapshotTree';
import { SyncTree } from './SyncTree';
import { SnapshotHolder } from './SnapshotHolder';
import {
Expand Down Expand Up @@ -147,7 +152,7 @@ export class Repo {
__database: Database;

/** A list of data pieces and paths to be set when this client disconnects. */
onDisconnect_ = new SparseSnapshotTree();
onDisconnect_ = newSparseSnapshotTree();

/** Stores queues of outstanding transactions for Firebase locations. */
transactionQueueTree_ = new Tree<Transaction[]>();
Expand Down Expand Up @@ -569,27 +574,35 @@ function repoRunOnDisconnectEvents(repo: Repo): void {
repoLog(repo, 'onDisconnectEvents');

const serverValues = repoGenerateServerValues(repo);
const resolvedOnDisconnectTree = new SparseSnapshotTree();
repo.onDisconnect_.forEachTree(newEmptyPath(), (path, node) => {
const resolved = resolveDeferredValueTree(
path,
node,
repo.serverSyncTree_,
serverValues
);
resolvedOnDisconnectTree.remember(path, resolved);
});
const resolvedOnDisconnectTree = newSparseSnapshotTree();
sparseSnapshotTreeForEachTree(
repo.onDisconnect_,
newEmptyPath(),
(path, node) => {
const resolved = resolveDeferredValueTree(
path,
node,
repo.serverSyncTree_,
serverValues
);
sparseSnapshotTreeRemember(resolvedOnDisconnectTree, path, resolved);
}
);
let events: Event[] = [];

resolvedOnDisconnectTree.forEachTree(newEmptyPath(), (path, snap) => {
events = events.concat(
repo.serverSyncTree_.applyServerOverwrite(path, snap)
);
const affectedPath = repoAbortTransactions(repo, path);
repoRerunTransactions(repo, affectedPath);
});
sparseSnapshotTreeForEachTree(
resolvedOnDisconnectTree,
newEmptyPath(),
(path, snap) => {
events = events.concat(
repo.serverSyncTree_.applyServerOverwrite(path, snap)
);
const affectedPath = repoAbortTransactions(repo, path);
repoRerunTransactions(repo, affectedPath);
}
);

repo.onDisconnect_ = new SparseSnapshotTree();
repo.onDisconnect_ = newSparseSnapshotTree();
eventQueueRaiseEventsForChangedPath(repo.eventQueue_, newEmptyPath(), events);
}

Expand All @@ -600,7 +613,7 @@ export function repoOnDisconnectCancel(
): void {
repo.server_.onDisconnectCancel(path.toString(), (status, errorReason) => {
if (status === 'ok') {
repo.onDisconnect_.forget(path);
sparseSnapshotTreeForget(repo.onDisconnect_, path);
}
repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
});
Expand All @@ -618,7 +631,7 @@ export function repoOnDisconnectSet(
newNode.val(/*export=*/ true),
(status, errorReason) => {
if (status === 'ok') {
repo.onDisconnect_.remember(path, newNode);
sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
}
repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
}
Expand All @@ -638,7 +651,7 @@ export function repoOnDisconnectSetWithPriority(
newNode.val(/*export=*/ true),
(status, errorReason) => {
if (status === 'ok') {
repo.onDisconnect_.remember(path, newNode);
sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
}
repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
}
Expand All @@ -664,7 +677,11 @@ export function repoOnDisconnectUpdate(
if (status === 'ok') {
each(childrenToMerge, (childName: string, childNode: unknown) => {
const newChildNode = nodeFromJSON(childNode);
repo.onDisconnect_.remember(pathChild(path, childName), newChildNode);
sparseSnapshotTreeRemember(
repo.onDisconnect_,
pathChild(path, childName),
newChildNode
);
});
}
repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
Expand Down
235 changes: 131 additions & 104 deletions packages/database/src/core/SparseSnapshotTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,129 +22,156 @@ import { Node } from './snap/Node';
/**
* Helper class to store a sparse set of snapshots.
*/
export class SparseSnapshotTree {
private value: Node | null = null;
export interface SparseSnapshotTree {
value: Node | null;
readonly children: Map<string, SparseSnapshotTree>;
}

private readonly children: Map<string, SparseSnapshotTree> = new Map();
export function newSparseSnapshotTree(): SparseSnapshotTree {
return {
value: null,
children: new Map()
};
}

/**
* Gets the node stored at the given path if one exists.
*
* @param path Path to look up snapshot for.
* @return The retrieved node, or null.
*/
find(path: Path): Node | null {
if (this.value != null) {
return this.value.getChild(path);
} else if (!pathIsEmpty(path) && this.children.size > 0) {
const childKey = pathGetFront(path);
path = pathPopFront(path);
if (this.children.has(childKey)) {
const childTree = this.children.get(childKey);
return childTree.find(path);
} else {
return null;
}
/**
* Gets the node stored at the given path if one exists.
* Only seems to be used in tests.
*
* @param path Path to look up snapshot for.
* @return The retrieved node, or null.
*/
export function sparseSnapshotTreeFind(
sparseSnapshotTree: SparseSnapshotTree,
path: Path
): Node | null {
if (sparseSnapshotTree.value != null) {
return sparseSnapshotTree.value.getChild(path);
} else if (!pathIsEmpty(path) && sparseSnapshotTree.children.size > 0) {
const childKey = pathGetFront(path);
path = pathPopFront(path);
if (sparseSnapshotTree.children.has(childKey)) {
const childTree = sparseSnapshotTree.children.get(childKey);
return sparseSnapshotTreeFind(childTree, path);
} else {
return null;
}
} else {
return null;
}
}

/**
* Stores the given node at the specified path. If there is already a node
* at a shallower path, it merges the new data into that snapshot node.
*
* @param path Path to look up snapshot for.
* @param data The new data, or null.
*/
remember(path: Path, data: Node) {
if (pathIsEmpty(path)) {
this.value = data;
this.children.clear();
} else if (this.value !== null) {
this.value = this.value.updateChild(path, data);
} else {
const childKey = pathGetFront(path);
if (!this.children.has(childKey)) {
this.children.set(childKey, new SparseSnapshotTree());
}

const child = this.children.get(childKey);
path = pathPopFront(path);
child.remember(path, data);
/**
* Stores the given node at the specified path. If there is already a node
* at a shallower path, it merges the new data into that snapshot node.
*
* @param path Path to look up snapshot for.
* @param data The new data, or null.
*/
export function sparseSnapshotTreeRemember(
sparseSnapshotTree: SparseSnapshotTree,
path: Path,
data: Node
): void {
if (pathIsEmpty(path)) {
sparseSnapshotTree.value = data;
sparseSnapshotTree.children.clear();
} else if (sparseSnapshotTree.value !== null) {
sparseSnapshotTree.value = sparseSnapshotTree.value.updateChild(path, data);
} else {
const childKey = pathGetFront(path);
if (!sparseSnapshotTree.children.has(childKey)) {
sparseSnapshotTree.children.set(childKey, newSparseSnapshotTree());
}

const child = sparseSnapshotTree.children.get(childKey);
path = pathPopFront(path);
sparseSnapshotTreeRemember(child, path, data);
}
}

/**
* Purge the data at path from the cache.
*
* @param path Path to look up snapshot for.
* @return True if this node should now be removed.
*/
forget(path: Path): boolean {
if (pathIsEmpty(path)) {
this.value = null;
this.children.clear();
return true;
} else {
if (this.value !== null) {
if (this.value.isLeafNode()) {
// We're trying to forget a node that doesn't exist
return false;
} else {
const value = this.value;
this.value = null;
/**
* Purge the data at path from the cache.
*
* @param path Path to look up snapshot for.
* @return True if this node should now be removed.
*/
export function sparseSnapshotTreeForget(
sparseSnapshotTree: SparseSnapshotTree,
path: Path
): boolean {
if (pathIsEmpty(path)) {
sparseSnapshotTree.value = null;
sparseSnapshotTree.children.clear();
return true;
} else {
if (sparseSnapshotTree.value !== null) {
if (sparseSnapshotTree.value.isLeafNode()) {
// We're trying to forget a node that doesn't exist
return false;
} else {
const value = sparseSnapshotTree.value;
sparseSnapshotTree.value = null;

const self = this;
value.forEachChild(PRIORITY_INDEX, (key, tree) => {
self.remember(new Path(key), tree);
});
value.forEachChild(PRIORITY_INDEX, (key, tree) => {
sparseSnapshotTreeRemember(sparseSnapshotTree, new Path(key), tree);
});

return this.forget(path);
}
} else if (this.children.size > 0) {
const childKey = pathGetFront(path);
path = pathPopFront(path);
if (this.children.has(childKey)) {
const safeToRemove = this.children.get(childKey).forget(path);
if (safeToRemove) {
this.children.delete(childKey);
}
return sparseSnapshotTreeForget(sparseSnapshotTree, path);
}
} else if (sparseSnapshotTree.children.size > 0) {
const childKey = pathGetFront(path);
path = pathPopFront(path);
if (sparseSnapshotTree.children.has(childKey)) {
const safeToRemove = sparseSnapshotTreeForget(
sparseSnapshotTree.children.get(childKey),
path
);
if (safeToRemove) {
sparseSnapshotTree.children.delete(childKey);
}

return this.children.size === 0;
} else {
return true;
}
}
}

/**
* Recursively iterates through all of the stored tree and calls the
* callback on each one.
*
* @param prefixPath Path to look up node for.
* @param func The function to invoke for each tree.
*/
forEachTree(prefixPath: Path, func: (a: Path, b: Node) => unknown) {
if (this.value !== null) {
func(prefixPath, this.value);
return sparseSnapshotTree.children.size === 0;
} else {
this.forEachChild((key, tree) => {
const path = new Path(prefixPath.toString() + '/' + key);
tree.forEachTree(path, func);
});
return true;
}
}
}

/**
* Iterates through each immediate child and triggers the callback.
*
* @param func The function to invoke for each child.
*/
forEachChild(func: (a: string, b: SparseSnapshotTree) => void) {
this.children.forEach((tree, key) => {
func(key, tree);
/**
* Recursively iterates through all of the stored tree and calls the
* callback on each one.
*
* @param prefixPath Path to look up node for.
* @param func The function to invoke for each tree.
*/
export function sparseSnapshotTreeForEachTree(
sparseSnapshotTree: SparseSnapshotTree,
prefixPath: Path,
func: (a: Path, b: Node) => unknown
): void {
if (sparseSnapshotTree.value !== null) {
func(prefixPath, sparseSnapshotTree.value);
} else {
sparseSnapshotTreeForEachChild(sparseSnapshotTree, (key, tree) => {
const path = new Path(prefixPath.toString() + '/' + key);
sparseSnapshotTreeForEachTree(tree, path, func);
});
}
}

/**
* Iterates through each immediate child and triggers the callback.
* Only seems to be used in tests.
*
* @param func The function to invoke for each child.
*/
export function sparseSnapshotTreeForEachChild(
sparseSnapshotTree: SparseSnapshotTree,
func: (a: string, b: SparseSnapshotTree) => void
): void {
sparseSnapshotTree.children.forEach((tree, key) => {
func(key, tree);
});
}
Loading

0 comments on commit 261617b

Please sign in to comment.