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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ReuseListIterator for getAll() api #390

Merged
merged 6 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading