Skip to content

Commit

Permalink
feat: ReuseListIterator for getAll() api (#390)
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths authored Jul 31, 2024
1 parent b6f18fb commit 04f8a16
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 14 deletions.
5 changes: 4 additions & 1 deletion packages/persistent-merkle-tree/src/hashComputation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export class HashComputationLevel {
* run before every run
*/
reset(): void {
// keep this.head
// keep this.head object, only release the data
this.head.src0 = null as unknown as Node;
this.head.src1 = null as unknown as Node;
this.head.dest = null as unknown as Node;
this.tail = null;
this._length = 0;
// totalLength is not reset
Expand Down
1 change: 1 addition & 0 deletions packages/ssz/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export {BitArray, getUint8ByteToBitBooleanArray} from "./value/bitArray";

// Utils
export {fromHexString, toHexString, byteArrayEquals} from "./util/byteArray";
export {ReusableListIterator} from "./util/reusableListIterator";

export {hash64, symbolCachedPermanentRoot} from "./util/merkleize";

Expand Down
6 changes: 6 additions & 0 deletions packages/ssz/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export interface List<T> extends ArrayLike<T> {
pop(): T | undefined;
}

export interface ListIterator<T> {
readonly length: number;
push(...values: T[]): void;
[Symbol.iterator](): Iterator<T>;
}

export type Container<T extends Record<string, unknown>> = T;

export type ByteVector = Vector<number>;
Expand Down
145 changes: 145 additions & 0 deletions packages/ssz/src/util/reusableListIterator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {ListIterator} from "../interface";

class LinkedNode<T> {
data: T;
next: LinkedNode<T> | null = null;

constructor(data: T) {
this.data = data;
}
}

/**
* A LinkedList that's designed to be reused overtime.
* Before every run, reset() should be called.
* After every run, clean() should be called.
*/
export class ReusableListIterator<T> implements ListIterator<T> {
private head: LinkedNode<T>;
private tail: LinkedNode<T> | null;
private _length = 0;
private _totalLength = 0;
private pointer: LinkedNode<T> | null;
// this avoids memory allocation
private iteratorResult: IteratorResult<T>;

constructor() {
this.head = {
data: null as unknown as T,
next: null,
};
this.tail = null;
this.pointer = null;
this.iteratorResult = {} as IteratorResult<T>;
}

get length(): number {
return this._length;
}

get totalLength(): number {
return this._totalLength;
}

/**
* run before every run
*/
reset(): void {
// keep this.head object, only release the data
this.head.data = null as unknown as T;
this.tail = null;
this._length = 0;
// totalLength is not reset
this.pointer = null;
// no need to reset iteratorResult
}

/**
* Append new data to the tail
* This will overwrite the existing data if it is not null, or grow the list if needed.
*/
push(value: T): void {
if (this.tail !== null) {
let newTail = this.tail.next;
if (newTail !== null) {
newTail.data = value;
} else {
// grow the list
newTail = {data: value, next: null};
this.tail.next = newTail;
this._totalLength++;
}
this.tail = newTail;
this._length++;
return;
}

// first item
this.head.data = value;
this.tail = this.head;
this._length = 1;
if (this._totalLength === 0) {
this._totalLength = 1;
}
// else _totalLength > 0, do not set
}

/**
* run after every run
* hashComps may still refer to the old Nodes, we should release them to avoid memory leak.
*/
clean(): void {
let node = this.tail?.next ?? null;
while (node !== null && node.data !== null) {
node.data = null as unknown as T;
node = node.next;
}
}

/**
* Implement Iterator for this class
*/
next(): IteratorResult<T> {
if (!this.pointer || this.tail === null) {
return {done: true, value: undefined};
}

// never yield value beyond the tail
const value = this.pointer.data;
this.pointer = this.pointer.next;
// should not allocate new object here
const isNull = value === null;
this.iteratorResult.done = isNull;
this.iteratorResult.value = isNull ? undefined : value;
return this.iteratorResult;
}

/**
* This is convenient method to consume HashComputationLevel with for-of loop
* See "next" method above for the actual implementation
*/
[Symbol.iterator](): IterableIterator<T> {
this.pointer = this.head;
return this;
}

toArray(): T[] {
const result: T[] = [];
for (const data of this) {
result.push(data);
}
return result;
}

/**
* For testing only
*/
dump(): T[] {
const result: T[] = [];
let node: LinkedNode<T> | null = this.head;
for (; node !== null; node = node.next) {
result.push(node.data);
}
return result;
}
}
32 changes: 32 additions & 0 deletions packages/ssz/src/view/arrayComposite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {getNodesAtDepth, HashComputationLevel, Node, toGindexBitstring, Tree} fr
import {ValueOf} from "../type/abstract";
import {CompositeType, CompositeView, CompositeViewDU} from "../type/composite";
import {TreeView} from "./abstract";
import {ListIterator} from "../interface";

/** Expected API of this View's type. This interface allows to break a recursive dependency between types and views */
export type ArrayCompositeType<
Expand Down Expand Up @@ -104,6 +105,22 @@ export class ArrayCompositeTreeView<
return views;
}

/**
* Similar to getAllReadonly but support ListIterator interface.
* Use ReusableListIterator to reuse over multiple calls.
*/
getAllReadonlyIter(views?: ListIterator<CompositeView<ElementType>>): ListIterator<CompositeView<ElementType>> {
const length = this.length;
const chunksNode = this.type.tree_getChunksNode(this.node);
const nodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, length);
views = views ?? new Array<CompositeView<ElementType>>();
for (let i = 0; i < length; i++) {
// TODO: Optimize
views.push(this.type.elementType.getView(new Tree(nodes[i])));
}
return views;
}

/**
* Returns an array of values of all elements in the array, from index zero to `this.length - 1`.
* The returned values are not Views so any changes won't be propagated upwards.
Expand All @@ -122,4 +139,19 @@ export class ArrayCompositeTreeView<
}
return values;
}

/**
* Similar to getAllReadonlyValues but support ListIterator interface.
* Use ReusableListIterator to reuse over multiple calls.
*/
getAllReadonlyValuesIter(values?: ListIterator<ValueOf<ElementType>>): ListIterator<ValueOf<ElementType>> {
const length = this.length;
const chunksNode = this.type.tree_getChunksNode(this.node);
const nodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, length);
values = values ?? new Array<ValueOf<ElementType>>();
for (let i = 0; i < length; i++) {
values.push(this.type.elementType.tree_toValue(nodes[i]));
}
return values;
}
}
31 changes: 31 additions & 0 deletions packages/ssz/src/viewDU/arrayComposite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {ValueOf} from "../type/abstract";
import {CompositeType, CompositeView, CompositeViewDU} from "../type/composite";
import {ArrayCompositeType} from "../view/arrayComposite";
import {TreeViewDU} from "./abstract";
import {ListIterator} from "../interface";

export type ArrayCompositeTreeViewDUCache = {
nodes: Node[];
Expand Down Expand Up @@ -160,6 +161,21 @@ export class ArrayCompositeTreeViewDU<
return views;
}

/**
* Similar to getAllReadonly but support ListIterator interface.
* Use ReusableListIterator to reuse over multiple calls.
*/
getAllReadonlyIter(views?: ListIterator<CompositeViewDU<ElementType>>): ListIterator<CompositeViewDU<ElementType>> {
this.populateAllNodes();

views = views ?? new Array<CompositeViewDU<ElementType>>();
for (let i = 0; i < this._length; i++) {
const view = this.type.elementType.getViewDU(this.nodes[i], this.caches[i]);
views.push(view);
}
return views;
}

/**
* WARNING: Returns all commited changes, if there are any pending changes commit them beforehand
*/
Expand All @@ -176,6 +192,21 @@ export class ArrayCompositeTreeViewDU<
return values;
}

/**
* Similar to getAllReadonlyValues but support ListIterator interface.
* Use ReusableListIterator to reuse over multiple calls.
*/
getAllReadonlyValuesIter(values?: ListIterator<ValueOf<ElementType>>): ListIterator<ValueOf<ElementType>> {
this.populateAllNodes();

values = values ?? new Array<ValueOf<ElementType>>();
for (let i = 0; i < this._length; i++) {
const value = this.type.elementType.tree_toValue(this.nodes[i]);
values.push(value);
}
return values;
}

/**
* When we need to compute HashComputations (hcByLevel != null):
* - if old _rootNode is hashed, then only need to put pending changes to hcByLevel
Expand Down
48 changes: 38 additions & 10 deletions packages/ssz/test/perf/byType/listComposite.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import {itBench} from "@dapplion/benchmark";
import {ContainerNodeStructType, ContainerType, ListCompositeType, UintNumberType} from "../../../src";
import {
CompositeViewDU,
ContainerNodeStructType,
ContainerType,
ListCompositeType,
ReusableListIterator,
UintNumberType,
ValueOf,
} from "../../../src";

const byteType = new UintNumberType(1);

Expand All @@ -20,33 +28,53 @@ describe("ListCompositeType types", () => {
});
}

for (const type of [
new ListCompositeType(containerType, 2 ** 40, {typeName: "List(Container)"}),
new ListCompositeType(containerNodeStructType, 2 ** 40, {typeName: "List(ContainerNodeStruct)"}),
]) {
const viewDU = type.toViewDU(newFilledArray(len, {a: 1, b: 2}));
for (const [i, type] of [containerType, containerNodeStructType].entries()) {
const listType = new ListCompositeType(type, 2 ** 40, {
typeName: `List(${i === 0 ? "Container" : "ContainerNodeStruct"})`,
});
const viewDU = listType.toViewDU(newFilledArray(len, {a: 1, b: 2}));

itBench(`${type.typeName} len ${len} ViewDU.getAllReadonly() + iterate`, () => {
itBench(`${listType.typeName} len ${len} ViewDU.getAllReadonly() + iterate`, () => {
const values = viewDU.getAllReadonly();
for (let i = 0; i < len; i++) {
values[i];
}
});

itBench(`${type.typeName} len ${len} ViewDU.getAllReadonlyValues() + iterate`, () => {
const viewDUs = new ReusableListIterator<CompositeViewDU<typeof type>>();
itBench(`${listType.typeName} len ${len} ViewDU.getAllReadonlyIter() + iterate`, () => {
viewDUs.reset();
viewDU.getAllReadonlyIter(viewDUs);
viewDUs.clean();
for (const viewDU of viewDUs) {
viewDU;
}
});

itBench(`${listType.typeName} len ${len} ViewDU.getAllReadonlyValues() + iterate`, () => {
const values = viewDU.getAllReadonlyValues();
for (let i = 0; i < len; i++) {
values[i];
}
});

itBench(`${type.typeName} len ${len} ViewDU.get(i)`, () => {
const values = new ReusableListIterator<ValueOf<typeof type>>();
itBench(`${listType.typeName} len ${len} ViewDU.getAllReadonlyValuesIter() + iterate`, () => {
values.clean();
viewDU.getAllReadonlyValuesIter(values);
values.reset();
for (const value of values) {
value;
}
});

itBench(`${listType.typeName} len ${len} ViewDU.get(i)`, () => {
for (let i = 0; i < len; i++) {
viewDU.get(i);
}
});

itBench(`${type.typeName} len ${len} ViewDU.getReadonly(i)`, () => {
itBench(`${listType.typeName} len ${len} ViewDU.getReadonly(i)`, () => {
for (let i = 0; i < len; i++) {
viewDU.getReadonly(i);
}
Expand Down
20 changes: 18 additions & 2 deletions packages/ssz/test/perf/iterate.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {itBench, setBenchOpts} from "@dapplion/benchmark";
import {ListBasicType, UintNumberType} from "../../src";
import {Validators} from "../lodestarTypes/phase0/sszTypes";
import {CompositeViewDU, ListBasicType, ReusableListIterator, UintNumberType} from "../../src";
import {Validators, Validator} from "../lodestarTypes/phase0/sszTypes";

describe("iterate", () => {
setBenchOpts({noThreshold: true});
Expand Down Expand Up @@ -53,6 +53,22 @@ describe("readonly values - iterator vs array", () => {
validatorsArray[i];
}
});

const viewDUs = new ReusableListIterator<CompositeViewDU<typeof Validator>>();
itBench("compositeListValue.getAllReadonlyIter()", () => {
viewDUs.reset();
validators.getAllReadonlyIter(viewDUs);
viewDUs.clean();
});

itBench("compositeListValue.getAllReadonlyIter() + loop all", () => {
viewDUs.reset();
validators.getAllReadonlyIter(viewDUs);
viewDUs.clean();
for (const viewDU of viewDUs) {
viewDU;
}
});
});

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
Expand Down
Loading

0 comments on commit 04f8a16

Please sign in to comment.