Optimize statistics.

This commit is contained in:
Simon Lindh 2020-02-17 00:26:57 +07:00 committed by wiz
parent ac95c09ea6
commit acd658a0e7
No known key found for this signature in database
GPG Key ID: A394E332255A6173
11 changed files with 148 additions and 99 deletions

View File

@ -1,7 +1,7 @@
import memPool from './mempool'; import memPool from './mempool';
import { DB } from '../database'; import { DB } from '../database';
import { Statistic, SimpleTransaction } from '../interfaces'; import { Statistic, SimpleTransaction, OptimizedStatistic } from '../interfaces';
class Statistics { class Statistics {
protected intervalTimer: NodeJS.Timer | undefined; protected intervalTimer: NodeJS.Timer | undefined;
@ -278,95 +278,163 @@ class Statistics {
AVG(vsize_2000) AS vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${groupBy} ORDER BY id DESC LIMIT ${days}`; AVG(vsize_2000) AS vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${groupBy} ORDER BY id DESC LIMIT ${days}`;
} }
public async $get(id: number): Promise<Statistic | undefined> { public async $get(id: number): Promise<OptimizedStatistic | undefined> {
try { try {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = `SELECT * FROM statistics WHERE id = ?`; const query = `SELECT * FROM statistics WHERE id = ?`;
const [rows] = await connection.query<any>(query, [id]); const [rows] = await connection.query<any>(query, [id]);
connection.release(); connection.release();
return rows[0]; if (rows[0]) {
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
}
} catch (e) { } catch (e) {
console.log('$list2H() error', e); console.log('$list2H() error', e);
} }
} }
public async $list2H(): Promise<Statistic[]> { public async $list2H(): Promise<OptimizedStatistic[]> {
try { try {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = `SELECT * FROM statistics ORDER BY id DESC LIMIT 120`; const query = `SELECT * FROM statistics ORDER BY id DESC LIMIT 120`;
const [rows] = await connection.query<any>(query); const [rows] = await connection.query<any>(query);
connection.release(); connection.release();
return rows; return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) { } catch (e) {
console.log('$list2H() error', e); console.log('$list2H() error', e);
return []; return [];
} }
} }
public async $list24H(): Promise<Statistic[]> { public async $list24H(): Promise<OptimizedStatistic[]> {
try { try {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 720); const query = this.getQueryForDays(120, 720);
const [rows] = await connection.query<any>(query); const [rows] = await connection.query<any>(query);
connection.release(); connection.release();
return rows; return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) { } catch (e) {
return []; return [];
} }
} }
public async $list1W(): Promise<Statistic[]> { public async $list1W(): Promise<OptimizedStatistic[]> {
try { try {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 5040); const query = this.getQueryForDays(120, 5040);
const [rows] = await connection.query<any>(query); const [rows] = await connection.query<any>(query);
connection.release(); connection.release();
return rows; return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) { } catch (e) {
console.log('$list1W() error', e); console.log('$list1W() error', e);
return []; return [];
} }
} }
public async $list1M(): Promise<Statistic[]> { public async $list1M(): Promise<OptimizedStatistic[]> {
try { try {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 20160); const query = this.getQueryForDays(120, 20160);
const [rows] = await connection.query<any>(query); const [rows] = await connection.query<any>(query);
connection.release(); connection.release();
return rows; return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) { } catch (e) {
console.log('$list1M() error', e); console.log('$list1M() error', e);
return []; return [];
} }
} }
public async $list3M(): Promise<Statistic[]> { public async $list3M(): Promise<OptimizedStatistic[]> {
try { try {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 60480); const query = this.getQueryForDays(120, 60480);
const [rows] = await connection.query<any>(query); const [rows] = await connection.query<any>(query);
connection.release(); connection.release();
return rows; return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) { } catch (e) {
console.log('$list3M() error', e); console.log('$list3M() error', e);
return []; return [];
} }
} }
public async $list6M(): Promise<Statistic[]> { public async $list6M(): Promise<OptimizedStatistic[]> {
try { try {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 120960); const query = this.getQueryForDays(120, 120960);
const [rows] = await connection.query<any>(query); const [rows] = await connection.query<any>(query);
connection.release(); connection.release();
return rows; return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) { } catch (e) {
console.log('$list6M() error', e); console.log('$list6M() error', e);
return []; return [];
} }
} }
public async $list1Y(): Promise<OptimizedStatistic[]> {
try {
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(120, 241920);
const [rows] = await connection.query<any>(query);
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
console.log('$list6M() error', e);
return [];
}
}
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
return statistic.map((s) => {
return {
id: s.id || 0,
added: s.added,
unconfirmed_transactions: s.unconfirmed_transactions,
tx_per_second: s.tx_per_second,
vbytes_per_second: s.vbytes_per_second,
mempool_byte_weight: s.mempool_byte_weight,
total_fee: s.total_fee,
vsizes: [
s.vsize_1,
s.vsize_2,
s.vsize_3,
s.vsize_4,
s.vsize_5,
s.vsize_6,
s.vsize_8,
s.vsize_10,
s.vsize_12,
s.vsize_15,
s.vsize_20,
s.vsize_30,
s.vsize_40,
s.vsize_50,
s.vsize_60,
s.vsize_70,
s.vsize_80,
s.vsize_90,
s.vsize_100,
s.vsize_125,
s.vsize_150,
s.vsize_175,
s.vsize_200,
s.vsize_250,
s.vsize_300,
s.vsize_350,
s.vsize_400,
s.vsize_500,
s.vsize_600,
s.vsize_700,
s.vsize_800,
s.vsize_900,
s.vsize_1000,
s.vsize_1200,
s.vsize_1400,
s.vsize_1600,
s.vsize_1800,
s.vsize_2000,
]
};
});
}
} }
export default new Statistics(); export default new Statistics();

View File

@ -167,6 +167,7 @@ class Server {
.get(config.API_ENDPOINT + 'statistics/1m', routes.get1MStatistics.bind(routes)) .get(config.API_ENDPOINT + 'statistics/1m', routes.get1MStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes)) .get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.bind(routes)) .get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/1y', routes.get1YStatistics.bind(routes))
; ;
} }
} }

View File

@ -160,6 +160,17 @@ export interface Statistic {
vsize_2000: number; vsize_2000: number;
} }
export interface OptimizedStatistic {
id: number;
added: string;
unconfirmed_transactions: number;
tx_per_second: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
vsizes: number[];
}
export interface Outspend { export interface Outspend {
spent: boolean; spent: boolean;
txid: string; txid: string;

View File

@ -16,6 +16,7 @@ class Routes {
this.cache['1m'] = await statistics.$list1M(); this.cache['1m'] = await statistics.$list1M();
this.cache['3m'] = await statistics.$list3M(); this.cache['3m'] = await statistics.$list3M();
this.cache['6m'] = await statistics.$list6M(); this.cache['6m'] = await statistics.$list6M();
this.cache['1y'] = await statistics.$list1Y();
console.log('Statistics cache created'); console.log('Statistics cache created');
} }
@ -44,6 +45,10 @@ class Routes {
res.send(this.cache['6m']); res.send(this.cache['6m']);
} }
public get1YStatistics(req, res) {
res.send(this.cache['1y']);
}
public async getRecommendedFees(req, res) { public async getRecommendedFees(req, res) {
const result = feeApi.getRecommendedFee(); const result = feeApi.getRecommendedFee();
res.send(result); res.send(result);

View File

@ -49,7 +49,7 @@
<input ngbButton type="radio" [value]="'6m'" [routerLink]="['/graphs']" fragment="6m"> 6M <input ngbButton type="radio" [value]="'6m'" [routerLink]="['/graphs']" fragment="6m"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'1y'"> 1Y <input ngbButton type="radio" [value]="'1y'" [routerLink]="['/graphs']" fragment="1y"> 1Y
</label> </label>
</div> </div>
</form> </form>

View File

@ -6,7 +6,7 @@ import { of, merge} from 'rxjs';
import { switchMap, tap } from 'rxjs/operators'; import { switchMap, tap } from 'rxjs/operators';
import { VbytesPipe } from '../../pipes/bytes-pipe/vbytes.pipe'; import { VbytesPipe } from '../../pipes/bytes-pipe/vbytes.pipe';
import { MempoolStats } from '../../interfaces/node-api.interface'; import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
@ -22,7 +22,7 @@ export class StatisticsComponent implements OnInit {
loading = true; loading = true;
spinnerLoading = false; spinnerLoading = false;
mempoolStats: MempoolStats[] = []; mempoolStats: OptimizedMempoolStats[] = [];
mempoolVsizeFeesData: any; mempoolVsizeFeesData: any;
mempoolUnconfirmedTransactionsData: any; mempoolUnconfirmedTransactionsData: any;
@ -62,6 +62,7 @@ export class StatisticsComponent implements OnInit {
case '1m': case '1m':
case '3m': case '3m':
case '6m': case '6m':
case '1y':
value = formatDate(value, 'dd/MM', this.locale); value = formatDate(value, 'dd/MM', this.locale);
} }
@ -124,7 +125,7 @@ export class StatisticsComponent implements OnInit {
this.route this.route
.fragment .fragment
.subscribe((fragment) => { .subscribe((fragment) => {
if (['2h', '24h', '1w', '1m', '3m', '6m'].indexOf(fragment) > -1) { if (['2h', '24h', '1w', '1m', '3m', '6m', '1y'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
} }
}); });
@ -158,7 +159,10 @@ export class StatisticsComponent implements OnInit {
if (this.radioGroupForm.controls.dateSpan.value === '3m') { if (this.radioGroupForm.controls.dateSpan.value === '3m') {
return this.apiService.list3MStatistics$(); return this.apiService.list3MStatistics$();
} }
return this.apiService.list6MStatistics$(); if (this.radioGroupForm.controls.dateSpan.value === '6m') {
return this.apiService.list6MStatistics$();
}
return this.apiService.list1YStatistics$();
}) })
) )
.subscribe((mempoolStats: any) => { .subscribe((mempoolStats: any) => {
@ -176,7 +180,7 @@ export class StatisticsComponent implements OnInit {
}); });
} }
handleNewMempoolData(mempoolStats: MempoolStats[]) { handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
mempoolStats.reverse(); mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added); const labels = mempoolStats.map(stats => stats.added);
@ -196,20 +200,14 @@ export class StatisticsComponent implements OnInit {
}; };
} }
generateArray(mempoolStats: MempoolStats[]) { generateArray(mempoolStats: OptimizedMempoolStats[]) {
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
logFees.reverse();
const finalArray: number[][] = []; const finalArray: number[][] = [];
let feesArray: number[] = []; let feesArray: number[] = [];
logFees.forEach((fee) => { for (let index = 37; index > -1; index--) {
feesArray = []; feesArray = [];
mempoolStats.forEach((stats) => { mempoolStats.forEach((stats) => {
// @ts-ignore const theFee = stats.vsizes[index].toString();
const theFee = stats['vsize_' + fee];
if (theFee) { if (theFee) {
feesArray.push(parseInt(theFee, 10)); feesArray.push(parseInt(theFee, 10));
} else { } else {
@ -220,7 +218,7 @@ export class StatisticsComponent implements OnInit {
feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]); feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
} }
finalArray.push(feesArray); finalArray.push(feesArray);
}); }
finalArray.reverse(); finalArray.reverse();
return finalArray; return finalArray;
} }

View File

@ -4,7 +4,7 @@ import { VbytesPipe } from '../../pipes/bytes-pipe/vbytes.pipe';
import * as Chartist from 'chartist'; import * as Chartist from 'chartist';
import { WebsocketService } from 'src/app/services/websocket.service'; import { WebsocketService } from 'src/app/services/websocket.service';
import { MempoolStats } from '../../interfaces/node-api.interface'; import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
import { StateService } from 'src/app/services/state.service'; import { StateService } from 'src/app/services/state.service';
import { ApiService } from 'src/app/services/api.service'; import { ApiService } from 'src/app/services/api.service';
@ -16,7 +16,7 @@ import { ApiService } from 'src/app/services/api.service';
export class TelevisionComponent implements OnInit { export class TelevisionComponent implements OnInit {
loading = true; loading = true;
mempoolStats: MempoolStats[] = []; mempoolStats: OptimizedMempoolStats[] = [];
mempoolVsizeFeesData: any; mempoolVsizeFeesData: any;
mempoolVsizeFeesOptions: any; mempoolVsizeFeesOptions: any;
@ -88,7 +88,7 @@ export class TelevisionComponent implements OnInit {
}); });
} }
handleNewMempoolData(mempoolStats: MempoolStats[]) { handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
mempoolStats.reverse(); mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added); const labels = mempoolStats.map(stats => stats.added);
@ -103,20 +103,14 @@ export class TelevisionComponent implements OnInit {
}; };
} }
generateArray(mempoolStats: MempoolStats[]) { generateArray(mempoolStats: OptimizedMempoolStats[]) {
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
logFees.reverse();
const finalArray: number[][] = []; const finalArray: number[][] = [];
let feesArray: number[] = []; let feesArray: number[] = [];
logFees.forEach((fee) => { for (let index = 37; index > -1; index--) {
feesArray = []; feesArray = [];
mempoolStats.forEach((stats) => { mempoolStats.forEach((stats) => {
// @ts-ignore const theFee = stats.vsizes[index].toString();
const theFee = stats['vsize_' + fee];
if (theFee) { if (theFee) {
feesArray.push(parseInt(theFee, 10)); feesArray.push(parseInt(theFee, 10));
} else { } else {
@ -127,7 +121,7 @@ export class TelevisionComponent implements OnInit {
feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]); feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
} }
finalArray.push(feesArray); finalArray.push(feesArray);
}); }
finalArray.reverse(); finalArray.reverse();
return finalArray; return finalArray;
} }

View File

@ -3,51 +3,15 @@ export interface BlockTransaction {
f: number; f: number;
} }
export interface MempoolStats { export interface OptimizedMempoolStats {
id: number; id: number;
added: string; added: string;
unconfirmed_transactions: number; unconfirmed_transactions: number;
tx_per_second: number;
vbytes_per_second: number; vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number; mempool_byte_weight: number;
fee_data: FeeData; vsizes: number[] | string[];
vsize_1: number;
vsize_2: number;
vsize_3: number;
vsize_4: number;
vsize_5: number;
vsize_6: number;
vsize_8: number;
vsize_10: number;
vsize_12: number;
vsize_15: number;
vsize_20: number;
vsize_30: number;
vsize_40: number;
vsize_50: number;
vsize_60: number;
vsize_70: number;
vsize_80: number;
vsize_90: number;
vsize_100: number;
vsize_125: number;
vsize_150: number;
vsize_175: number;
vsize_200: number;
vsize_250: number;
vsize_300: number;
vsize_350: number;
vsize_400: number;
vsize_500: number;
vsize_600: number;
vsize_700: number;
vsize_800: number;
vsize_900: number;
vsize_1000: number;
vsize_1200: number;
vsize_1400: number;
vsize_1600: number;
vsize_1800: number;
vsize_2000: number;
} }
interface FeeData { interface FeeData {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { MempoolStats, BlockTransaction } from '../interfaces/node-api.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
const API_BASE_URL = '/api/v1'; const API_BASE_URL = '/api/v1';
@ -13,27 +13,31 @@ export class ApiService {
private httpClient: HttpClient, private httpClient: HttpClient,
) { } ) { }
list2HStatistics$(): Observable<MempoolStats[]> { list2HStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<MempoolStats[]>(API_BASE_URL + '/statistics/2h'); return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/2h');
} }
list24HStatistics$(): Observable<MempoolStats[]> { list24HStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<MempoolStats[]>(API_BASE_URL + '/statistics/24h'); return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/24h');
} }
list1WStatistics$(): Observable<MempoolStats[]> { list1WStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<MempoolStats[]>(API_BASE_URL + '/statistics/1w'); return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/1w');
} }
list1MStatistics$(): Observable<MempoolStats[]> { list1MStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<MempoolStats[]>(API_BASE_URL + '/statistics/1m'); return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/1m');
} }
list3MStatistics$(): Observable<MempoolStats[]> { list3MStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<MempoolStats[]>(API_BASE_URL + '/statistics/3m'); return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/3m');
} }
list6MStatistics$(): Observable<MempoolStats[]> { list6MStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<MempoolStats[]>(API_BASE_URL + '/statistics/6m'); return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/6m');
}
list1YStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/1y');
} }
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject } from 'rxjs';
import { Block } from '../interfaces/electrs.interface'; import { Block } from '../interfaces/electrs.interface';
import { MempoolBlock } from '../interfaces/websocket.interface'; import { MempoolBlock } from '../interfaces/websocket.interface';
import { MempoolStats } from '../interfaces/node-api.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -13,7 +13,7 @@ export class StateService {
conversions$ = new ReplaySubject<any>(1); conversions$ = new ReplaySubject<any>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
txConfirmed = new Subject<Block>(); txConfirmed = new Subject<Block>();
live2Chart$ = new Subject<MempoolStats>(); live2Chart$ = new Subject<OptimizedMempoolStats>();
viewFiat$ = new BehaviorSubject<boolean>(false); viewFiat$ = new BehaviorSubject<boolean>(false);
isOffline$ = new BehaviorSubject<boolean>(false); isOffline$ = new BehaviorSubject<boolean>(false);

View File

@ -18,6 +18,10 @@ $link-hover-decoration: underline !default;
@import "~bootstrap/scss/bootstrap"; @import "~bootstrap/scss/bootstrap";
@import '~tlite/tlite.css'; @import '~tlite/tlite.css';
html, body {
height: 100%;
}
body { body {
background-color: #11131f; background-color: #11131f;
} }