Optimize block visualization rendering

This commit is contained in:
Mononaut 2022-06-14 23:08:18 +00:00
parent 7f4c6352ba
commit 300f5375c8
5 changed files with 108 additions and 54 deletions

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit } from '@angular/core'; import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy } from '@angular/core';
import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { WebsocketService } from 'src/app/services/websocket.service'; import { WebsocketService } from 'src/app/services/websocket.service';
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from './fast-vertex-array';
@ -11,7 +11,7 @@ import TxView from './tx-view';
templateUrl: './block-overview-graph.component.html', templateUrl: './block-overview-graph.component.html',
styleUrls: ['./block-overview-graph.component.scss'], styleUrls: ['./block-overview-graph.component.scss'],
}) })
export class BlockOverviewGraphComponent implements AfterViewInit { export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
@Input() isLoading: boolean; @Input() isLoading: boolean;
@Input() resolution: number; @Input() resolution: number;
@Input() blockLimit: number; @Input() blockLimit: number;
@ -24,6 +24,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
gl: WebGLRenderingContext; gl: WebGLRenderingContext;
animationFrameRequest: number; animationFrameRequest: number;
animationHeartBeat: number;
displayWidth: number; displayWidth: number;
displayHeight: number; displayHeight: number;
cssWidth: number; cssWidth: number;
@ -50,34 +51,46 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
this.resizeCanvas(); this.resizeCanvas();
} }
ngOnDestroy(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
clearTimeout(this.animationHeartBeat);
}
}
clear(direction): void { clear(direction): void {
this.exit(direction); this.exit(direction);
this.hoverTx = null; this.hoverTx = null;
this.selectedTx = null; this.selectedTx = null;
this.txPreviewEvent.emit(null); this.txPreviewEvent.emit(null);
this.start();
} }
enter(transactions: TransactionStripped[], direction: string): void { enter(transactions: TransactionStripped[], direction: string): void {
if (this.scene) { if (this.scene) {
this.scene.enter(transactions, direction); this.scene.enter(transactions, direction);
this.start();
} }
} }
exit(direction: string): void { exit(direction: string): void {
if (this.scene) { if (this.scene) {
this.scene.exit(direction); this.scene.exit(direction);
this.start();
} }
} }
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void { replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
if (this.scene) { if (this.scene) {
this.scene.replace(transactions || [], direction, sort); this.scene.replace(transactions || [], direction, sort);
this.start();
} }
} }
update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) { if (this.scene) {
this.scene.update(add, remove, direction, resetLayout); this.scene.update(add, remove, direction, resetLayout);
this.start();
} }
} }
@ -140,6 +153,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
} }
if (this.scene) { if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight }); this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
this.start();
} else { } else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray }); blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray });
@ -182,44 +196,64 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
start(): void { start(): void {
this.running = true; this.running = true;
this.ngZone.runOutsideAngular(() => this.run()); this.ngZone.runOutsideAngular(() => this.doRun());
}
doRun(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
}
this.animationFrameRequest = requestAnimationFrame(() => this.run());
} }
run(now?: DOMHighResTimeStamp): void { run(now?: DOMHighResTimeStamp): void {
if (!now) { if (!now) {
now = performance.now(); now = performance.now();
} }
// skip re-render if there's no change to the scene
if (this.scene) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
// frame timestamp
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
/* SET UP SHADER UNIFORMS */ if (this.vertexArray.dirty) {
// screen dimensions /* SET UP SHADER ATTRIBUTES */
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight); Object.keys(attribs).forEach((key, i) => {
// frame timestamp this.gl.vertexAttribPointer(attribs[key].pointer,
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now); attribs[key].count, // number of primitives in this attribute
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
false, // never normalised
stride, // distance between values of the same attribute
attribs[key].offset); // offset of the first value
});
/* SET UP SHADER ATTRIBUTES */ const pointArray = this.vertexArray.getVertexData();
Object.keys(attribs).forEach((key, i) => {
this.gl.vertexAttribPointer(attribs[key].pointer,
attribs[key].count, // number of primitives in this attribute
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
false, // never normalised
stride, // distance between values of the same attribute
attribs[key].offset); // offset of the first value
});
const pointArray = this.vertexArray.getVertexData(); if (pointArray.length) {
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
if (pointArray.length) { this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW); }
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize); this.vertexArray.dirty = false;
} else {
const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) {
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
}
}
} }
/* LOOP */ /* LOOP */
if (this.running) { if (this.running && this.scene && now <= (this.scene.animateUntil + 500)) {
if (this.animationFrameRequest) { this.doRun();
cancelAnimationFrame(this.animationFrameRequest); } else {
this.animationFrameRequest = null; if (this.animationHeartBeat) {
clearTimeout(this.animationHeartBeat);
} }
this.animationFrameRequest = requestAnimationFrame(() => this.run()); this.animationHeartBeat = window.setTimeout(() => {
this.start();
}, 1000);
} }
} }
@ -246,11 +280,15 @@ export class BlockOverviewGraphComponent implements AfterViewInit {
const currentPreview = this.selectedTx || this.hoverTx; const currentPreview = this.selectedTx || this.hoverTx;
if (selected !== currentPreview) { if (selected !== currentPreview) {
if (currentPreview) { if (currentPreview && this.scene) {
currentPreview.setHover(false); this.scene.setHover(currentPreview, false);
this.start();
} }
if (selected) { if (selected) {
selected.setHover(true); if (selected && this.scene) {
this.scene.setHover(selected, true);
this.start();
}
this.txPreviewEvent.emit({ this.txPreviewEvent.emit({
txid: selected.txid, txid: selected.txid,
fee: selected.fee, fee: selected.fee,

View File

@ -1,7 +1,7 @@
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from './fast-vertex-array';
import TxView from './tx-view'; import TxView from './tx-view';
import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Position, Square } from './sprite-types'; import { Position, Square, ViewUpdateParams } from './sprite-types';
export default class BlockScene { export default class BlockScene {
scene: { count: number, offset: { x: number, y: number}}; scene: { count: number, offset: { x: number, y: number}};
@ -19,6 +19,7 @@ export default class BlockScene {
unitWidth: number; unitWidth: number;
initialised: boolean; initialised: boolean;
layout: BlockLayout; layout: BlockLayout;
animateUntil = 0;
dirty: boolean; dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray }: constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
@ -41,7 +42,7 @@ export default class BlockScene {
this.dirty = true; this.dirty = true;
if (this.initialised && this.scene) { if (this.initialised && this.scene) {
this.updateAll(performance.now()); this.updateAll(performance.now(), 50);
} }
} }
@ -103,7 +104,7 @@ export default class BlockScene {
}); });
} }
this.updateAll(startTime, direction); this.updateAll(startTime, 200, direction);
} }
update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
@ -139,7 +140,7 @@ export default class BlockScene {
this.layout.applyGravity(); this.layout.applyGravity();
} }
this.updateAll(startTime, direction); this.updateAll(startTime, 100, direction);
} }
// return the tx at this screen position, if any // return the tx at this screen position, if any
@ -152,6 +153,10 @@ export default class BlockScene {
} }
} }
setHover(tx: TxView, value: boolean): void {
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
}
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }: private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
{ width: number, height: number, resolution: number, blockLimit: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray } orientation: string, flip: boolean, vertexArray: FastVertexArray }
@ -169,7 +174,7 @@ export default class BlockScene {
}; };
// Set the scale of the visualization (with a 5% margin) // Set the scale of the visualization (with a 5% margin)
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2); this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
this.gridWidth = resolution; this.gridWidth = resolution;
this.gridHeight = resolution; this.gridHeight = resolution;
this.resize({ width, height }); this.resize({ width, height });
@ -181,23 +186,21 @@ export default class BlockScene {
this.dirty = true; this.dirty = true;
} }
private insert(tx: TxView, startTime: number, direction: string = 'left'): void { private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void {
this.txs[tx.txid] = tx; this.animateUntil = Math.max(this.animateUntil, tx.update(update));
this.place(tx);
this.updateTx(tx, startTime, direction);
} }
private updateTx(tx: TxView, startTime: number, direction: string = 'left'): void { private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left'): void {
if (tx.dirty || this.dirty) { if (tx.dirty || this.dirty) {
this.saveGridToScreenPosition(tx); this.saveGridToScreenPosition(tx);
this.setTxOnScreen(tx, startTime, direction); this.setTxOnScreen(tx, startTime, delay, direction);
} }
} }
private setTxOnScreen(tx: TxView, startTime: number, direction: string = 'left'): void { private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left'): void {
if (!tx.initialised) { if (!tx.initialised) {
const txColor = tx.getColor(); const txColor = tx.getColor();
tx.update({ this.applyTxUpdate(tx, {
display: { display: {
position: { position: {
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4, x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
@ -209,35 +212,35 @@ export default class BlockScene {
start: startTime, start: startTime,
delay: 0, delay: 0,
}); });
tx.update({ this.applyTxUpdate(tx, {
display: { display: {
position: tx.screenPosition, position: tx.screenPosition,
color: txColor color: txColor
}, },
duration: 1000, duration: 1000,
start: startTime, start: startTime,
delay: 50, delay,
}); });
} else { } else {
tx.update({ this.applyTxUpdate(tx, {
display: { display: {
position: tx.screenPosition position: tx.screenPosition
}, },
duration: 1000, duration: 1000,
minDuration: 500, minDuration: 500,
start: startTime, start: startTime,
delay: 50, delay,
adjust: true adjust: true
}); });
} }
} }
private updateAll(startTime: number, direction: string = 'left'): void { private updateAll(startTime: number, delay: number = 50, direction: string = 'left'): void {
this.scene.count = 0; this.scene.count = 0;
const ids = this.getTxList(); const ids = this.getTxList();
startTime = startTime || performance.now(); startTime = startTime || performance.now();
for (const id of ids) { for (const id of ids) {
this.updateTx(this.txs[id], startTime, direction); this.updateTx(this.txs[id], startTime, delay, direction);
} }
this.dirty = false; this.dirty = false;
} }
@ -246,7 +249,7 @@ export default class BlockScene {
const tx = this.txs[id]; const tx = this.txs[id];
if (tx) { if (tx) {
this.layout.remove(tx); this.layout.remove(tx);
tx.update({ this.applyTxUpdate(tx, {
display: { display: {
position: { position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4, x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
@ -319,7 +322,7 @@ export default class BlockScene {
} }
} }
screenToGrid(position: Position): Position { private screenToGrid(position: Position): Position {
let x = position.x; let x = position.x;
let y = this.height - position.y; let y = this.height - position.y;
let t; let t;

View File

@ -18,6 +18,7 @@ export class FastVertexArray {
data: Float32Array; data: Float32Array;
freeSlots: number[]; freeSlots: number[];
lastSlot: number; lastSlot: number;
dirty = false;
constructor(length, stride) { constructor(length, stride) {
this.length = length; this.length = length;
@ -27,6 +28,7 @@ export class FastVertexArray {
this.data = new Float32Array(this.length * this.stride); this.data = new Float32Array(this.length * this.stride);
this.freeSlots = []; this.freeSlots = [];
this.lastSlot = 0; this.lastSlot = 0;
this.dirty = true;
} }
insert(sprite: TxSprite): number { insert(sprite: TxSprite): number {
@ -44,6 +46,7 @@ export class FastVertexArray {
} }
this.sprites[position] = sprite; this.sprites[position] = sprite;
return position; return position;
this.dirty = true;
} }
remove(index: number): void { remove(index: number): void {
@ -54,14 +57,17 @@ export class FastVertexArray {
if (this.length > 2048 && this.count < (this.length * 0.4)) { if (this.length > 2048 && this.count < (this.length * 0.4)) {
this.compact(); this.compact();
} }
this.dirty = true;
} }
setData(index: number, dataChunk: number[]): void { setData(index: number, dataChunk: number[]): void {
this.data.set(dataChunk, (index * this.stride)); this.data.set(dataChunk, (index * this.stride));
this.dirty = true;
} }
clearData(index: number): void { clearData(index: number): void {
this.data.fill(0, (index * this.stride), ((index + 1) * this.stride)); this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
this.dirty = true;
} }
getData(index: number): Float32Array { getData(index: number): Float32Array {
@ -73,6 +79,7 @@ export class FastVertexArray {
const newData = new Float32Array(this.length * this.stride); const newData = new Float32Array(this.length * this.stride);
newData.set(this.data); newData.set(this.data);
this.data = newData; this.data = newData;
this.dirty = true;
} }
compact(): void { compact(): void {
@ -97,6 +104,7 @@ export class FastVertexArray {
this.freeSlots = []; this.freeSlots = [];
this.lastSlot = i; this.lastSlot = i;
} }
this.dirty = true;
} }
getVertexData(): Float32Array { getVertexData(): Float32Array {

View File

@ -82,8 +82,10 @@ export default class TxView implements TransactionStripped {
delay: additional milliseconds to wait before starting delay: additional milliseconds to wait before starting
jitter: if set, adds a random amount to the delay, jitter: if set, adds a random amount to the delay,
adjust: if true, modify an in-progress transition instead of replacing it adjust: if true, modify an in-progress transition instead of replacing it
returns minimum transition end time
*/ */
update(params: ViewUpdateParams): void { update(params: ViewUpdateParams): number {
if (params.jitter) { if (params.jitter) {
params.delay += (Math.random() * params.jitter); params.delay += (Math.random() * params.jitter);
} }
@ -96,6 +98,7 @@ export default class TxView implements TransactionStripped {
); );
// apply any pending hover event // apply any pending hover event
if (this.hover) { if (this.hover) {
params.duration = Math.max(params.duration, hoverTransitionTime);
this.sprite.update({ this.sprite.update({
...this.hoverColor, ...this.hoverColor,
duration: hoverTransitionTime, duration: hoverTransitionTime,
@ -109,10 +112,12 @@ export default class TxView implements TransactionStripped {
); );
} }
this.dirty = false; this.dirty = false;
return (params.start || performance.now()) + (params.delay || 0) + (params.duration || 0);
} }
// Temporarily override the tx color // Temporarily override the tx color
setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): void { // returns minimum transition end time
setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): number {
if (hoverOn) { if (hoverOn) {
this.hover = true; this.hover = true;
this.hoverColor = color; this.hoverColor = color;
@ -131,6 +136,7 @@ export default class TxView implements TransactionStripped {
} }
} }
this.dirty = false; this.dirty = false;
return performance.now() + hoverTransitionTime;
} }
getColor(): Color { getColor(): Color {

View File

@ -216,7 +216,6 @@ export class BlockComponent implements OnInit, OnDestroy {
return of([]); return of([]);
}), }),
switchMap((transactions) => { switchMap((transactions) => {
console.log('overview loaded: ', prevBlock && prevBlock.height, block.height);
if (prevBlock) { if (prevBlock) {
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' }); return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
} else { } else {