bitfeed/client/src/controllers/TxController.js
2022-02-08 13:50:58 -06:00

318 lines
10 KiB
JavaScript

import TxPoolScene from '../models/TxPoolScene.js'
import TxMondrianPoolScene from '../models/TxMondrianPoolScene.js'
import TxBlockScene from '../models/TxBlockScene.js'
import BitcoinTx from '../models/BitcoinTx.js'
import BitcoinBlock from '../models/BitcoinBlock.js'
import TxSprite from '../models/TxSprite.js'
import { FastVertexArray } from '../utils/memory.js'
import { txQueueLength, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, blockAreaSize, highlight } from '../stores.js'
import config from "../config.js"
export default class TxController {
constructor ({ width, height }) {
this.vertexArray = new FastVertexArray(2048, TxSprite.dataSize, txCount)
this.debugVertexArray = new FastVertexArray(1024, TxSprite.dataSize)
this.txs = {}
this.expiredTxs = {}
this.poolScene = new TxMondrianPoolScene({ width, height, controller: this, heightStore: mempoolScreenHeight })
this.blockAreaSize = Math.min(window.innerWidth * 0.75, window.innerHeight / 2.5)
blockAreaSize.set(this.blockAreaSize)
this.blockScene = null
this.clearBlockTimeout = null
this.txDelay = 0 //config.txDelay
this.maxTxDelay = config.txDelay
this.knownBlocks = {}
this.selectedTx = null
this.selectionLocked = false
this.pendingTxs = []
this.pendingMap = {}
this.queueTimeout = null
this.queueLength = 0
highlight.subscribe(criteria => {
this.highlightCriteria = criteria
this.applyHighlighting()
})
this.scheduleQueue(1000)
}
getVertexData () {
return this.vertexArray.getVertexData()
}
getDebugVertexData () {
return this.debugVertexArray.getVertexData()
}
getScenes () {
if (this.blockScene) return [this.poolScene, this.blockScene]
else return [this.poolScene]
}
redoLayout ({ width, height }) {
this.poolScene.layoutAll({ width, height })
if (this.blockScene) {
this.blockScene.layoutAll({ width: this.blockAreaSize, height: this.blockAreaSize })
}
}
resize ({ width, height }) {
this.blockAreaSize = Math.min(window.innerWidth * 0.75, window.innerHeight / 2.5)
blockAreaSize.set(this.blockAreaSize)
this.redoLayout({ width, height })
}
applyHighlighting () {
this.poolScene.applyHighlighting(this.highlightCriteria)
if (this.blockScene) {
this.blockScene.applyHighlighting(this.highlightCriteria)
}
}
addTx (txData) {
const tx = new BitcoinTx(txData, this.vertexArray)
tx.applyHighlighting(this.highlightCriteria)
if (!this.txs[tx.id] && !this.expiredTxs[tx.id]) {
this.pendingTxs.push([tx, performance.now()])
this.pendingTxs[tx.id] = tx
txQueueLength.increment()
}
}
// Dual-strategy queue processing:
// - ensure transactions are queued for at least txDelay
// - when queue length exceeds 500, process iteratively to avoid unbounded growth
// - while queue is small, use jittered timeouts to evenly distribute arrivals
//
// transactions tend to arrive in groups, so for smoothest
// animation the queue should stay short but never empty.
processQueue () {
let done
let delay
while (!done) {
if (this.pendingTxs && this.pendingTxs.length) {
if (this.txs[this.pendingTxs[0][0].id] || this.expiredTxs[this.pendingTxs[0][0].id]) {
// duplicate transaction, skip without delay
const tx = this.pendingTxs.shift()[0]
delete this.pendingMap[tx.id]
txQueueLength.decrement()
} else {
const timeSince = performance.now() - this.pendingTxs[0][1]
if (timeSince > this.txDelay) {
//process the next tx in the queue, if it arrived longer ago than txDelay
if (this.txDelay < this.maxTxDelay) {
// slowly ramp up from 0 to maxTxDelay on start, so there's no wait for the first txs on page load
this.txDelay += 50
}
const tx = this.pendingTxs.shift()[0]
delete this.pendingMap[tx.id]
txQueueLength.decrement()
this.txs[tx.id] = tx
this.poolScene.insert(this.txs[tx.id])
mempoolCount.increment()
} else {
// end the loop when the head of the queue arrived more recently than txDelay
done = true
// schedule to continue processing when head of queue matures
delay = this.txDelay - timeSince
}
if (this.pendingTxs.length < 500) {
// or the queue is under 500
done = true
}
// otherwise keep processing the queue
}
} else done = true
}
// randomly jitter arrival times so that txs enter more naturally
// with jittered delay inversely proportional to size of queue
let jitter = Math.random() * Math.max(1, (500 - this.pendingTxs.length))
this.scheduleQueue(delay || jitter)
}
scheduleQueue (delay) {
if (this.queueTimeout) clearTimeout(this.queueTimeout)
this.queueTimeout = setTimeout(() => {
this.processQueue()
}, delay)
}
addBlock (blockData) {
// discard duplicate blocks
if (!blockData || !blockData.id || this.knownBlocks[blockData.id]) {
return
}
this.poolScene.scrollLock = true
const block = (blockData && blockData.isBlock) ? blockData : new BitcoinBlock(blockData)
this.knownBlocks[block.id] = true
if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout)
this.expiredTxs = {}
this.clearBlock()
this.blockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this })
let poolCount = 0
let knownCount = 0
let unknownCount = 0
for (let i = 0; i < block.txns.length; i++) {
if (this.poolScene.remove(block.txns[i].id)) {
poolCount++
knownCount++
this.txs[block.txns[i].id].setBlock(block.id)
this.blockScene.insert(this.txs[block.txns[i].id], false)
} else if (this.pendingMap[block.txns[i].id]) {
knownCount++
const tx = this.pendingMap[tx.id]
const pendingIndex = this.pendingTxs.indexOf(tx)
if (pendingIndex >= 0) this.pendingTxs.splice(pendingIndex, 1)
delete this.pendingMap[tx.id]
tx.setBlock(block.id)
this.txs[tx.id] = tx
this.blockScene.insert(tx, false)
} else {
unknownCount++
const tx = new BitcoinTx({
...block.txns[i],
block: block.id
}, this.vertexArray)
this.txs[tx.id] = tx
this.txs[tx.id].applyHighlighting(this.highlightCriteria)
this.blockScene.insert(tx, false)
}
this.expiredTxs[block.txns[i].id] = true
}
console.log(`New block with ${knownCount} known transactions and ${unknownCount} unknown transactions`)
mempoolCount.subtract(poolCount)
this.blockScene.initialLayout()
setTimeout(() => { this.poolScene.scrollLock = false; this.poolScene.layoutAll() }, 4000)
currentBlock.set(block)
blockVisible.set(true)
return block
}
simulateBlock () {
for (var i = 0; i < 1000; i++) {
this.addTx(new BitcoinTx({
version: 'simulated',
time: Date.now(),
id: `simulated_${i}_${Math.random()}`,
value: Math.floor(Math.pow(2,(0.1-(Math.log(1 - Math.random()))) * 8))
}))
}
const simulatedTxns = this.pendingTxs.map(pending => {
return {
version: pending[0].version,
id: pending[0].id,
time: pending[0].time,
value: pending[0].value
}
})
// const simulatedTxns = []
Object.values(this.poolScene.txs).forEach(tx => {
if (Math.random() < 0.5) {
simulatedTxns.push({
version: tx.version,
id: tx.id,
time: tx.time,
value: tx.value
})
}
})
const block = new BitcoinBlock({
version: 'fake',
id: 'fake_block' + Math.random(),
value: 12345678900,
prev_block: 'also_fake',
merkle_root: 'merkle',
timestamp: Date.now() / 1000,
bits: 'none',
bytes: 1379334,
txn_count: simulatedTxns.length,
txns: simulatedTxns
})
setTimeout(() => {
this.addBlock(block)
}, 2500)
return block
}
simulateDumpTx (n, value) {
for (var i = 0; i < n; i++) {
this.addTx(new BitcoinTx({
version: 'simulated',
time: Date.now(),
id: `simulated_${i}_${Math.random()}`,
value: value || Math.min(10000, Math.floor(Math.pow(2,(0.1-(Math.log(1 - Math.random()))) * 8))) // horrible but plausible distribution of tx values
// value: value || Math.pow(10,Math.floor(Math.random() * 5))
}, this.vertexArray))
}
}
hideBlock () {
if (this.blockScene) {
this.blockScene.hide()
}
}
showBlock () {
if (this.blockScene) {
this.blockScene.show()
}
}
clearBlock () {
if (this.blockScene) {
this.blockScene.expire()
}
currentBlock.set(null)
if (this.blockVisibleUnsub) this.blockVisibleUnsub()
}
destroyTx (id) {
this.getScenes().forEach(scene => {
scene.remove(id)
})
if (this.txs[id]) this.txs[id].destroy()
delete this.txs[id]
}
mouseMove (position) {
if (this.poolScene && !this.selectionLocked) {
let selected = this.poolScene.selectAt(position)
if (!selected && this.blockScene && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
if (selected !== this.selectedTx) {
if (this.selectedTx) this.selectedTx.hoverOff()
if (selected) selected.hoverOn()
}
this.selectedTx = selected
selectedTx.set(this.selectedTx)
}
}
mouseClick (position) {
if (this.poolScene) {
let selected = this.poolScene.selectAt(position)
if (!selected && this.blockScene && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
let sameTx = true
if (selected !== this.selectedTx) {
sameTx = false
if (this.selectedTx) this.selectedTx.hoverOff()
if (selected) selected.hoverOn()
}
this.selectedTx = selected
selectedTx.set(this.selectedTx)
this.selectionLocked = !!this.selectedTx && !(this.selectionLocked && sameTx)
}
}
}