mirror of
https://github.com/overte-org/overte.git
synced 2025-04-25 22:56:29 +02:00
adding CounterStats class and support for showing running average stats on voxels recieved, created, and colored in client
This commit is contained in:
parent
23429441ac
commit
53cfd0ecdd
7 changed files with 275 additions and 12 deletions
|
@ -82,6 +82,30 @@ void VoxelSystem::createSphere(float r,float xc, float yc, float zc, float s, bo
|
||||||
setupNewVoxelsForDrawing();
|
setupNewVoxelsForDrawing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long int VoxelSystem::getVoxelsCreated() {
|
||||||
|
return tree->voxelsCreated;
|
||||||
|
}
|
||||||
|
|
||||||
|
long int VoxelSystem::getVoxelsCreatedRunningAverage() {
|
||||||
|
return tree->voxelsCreatedStats.getRunningAverage();
|
||||||
|
}
|
||||||
|
|
||||||
|
long int VoxelSystem::getVoxelsColored() {
|
||||||
|
return tree->voxelsColored;
|
||||||
|
}
|
||||||
|
|
||||||
|
long int VoxelSystem::getVoxelsColoredRunningAverage() {
|
||||||
|
return tree->voxelsColoredStats.getRunningAverage();
|
||||||
|
}
|
||||||
|
|
||||||
|
long int VoxelSystem::getVoxelsBytesRead() {
|
||||||
|
return tree->voxelsBytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
long int VoxelSystem::getVoxelsBytesReadRunningAverage() {
|
||||||
|
return tree->voxelsBytesReadStats.getRunningAverage();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void VoxelSystem::parseData(void *data, int size) {
|
void VoxelSystem::parseData(void *data, int size) {
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,14 @@ public:
|
||||||
void setViewerHead(Head *newViewerHead);
|
void setViewerHead(Head *newViewerHead);
|
||||||
void loadVoxelsFile(const char* fileName,bool wantColorRandomizer);
|
void loadVoxelsFile(const char* fileName,bool wantColorRandomizer);
|
||||||
void createSphere(float r,float xc, float yc, float zc, float s, bool solid, bool wantColorRandomizer);
|
void createSphere(float r,float xc, float yc, float zc, float s, bool solid, bool wantColorRandomizer);
|
||||||
|
|
||||||
|
long int getVoxelsCreated();
|
||||||
|
long int getVoxelsColored();
|
||||||
|
long int getVoxelsBytesRead();
|
||||||
|
long int getVoxelsCreatedRunningAverage();
|
||||||
|
long int getVoxelsColoredRunningAverage();
|
||||||
|
long int getVoxelsBytesReadRunningAverage();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int voxelsRendered;
|
int voxelsRendered;
|
||||||
Head *viewerHead;
|
Head *viewerHead;
|
||||||
|
|
|
@ -147,6 +147,7 @@ bool starsOn = false; // Whether to display the stars
|
||||||
bool paintOn = false; // Whether to paint voxels as you fly around
|
bool paintOn = false; // Whether to paint voxels as you fly around
|
||||||
VoxelDetail paintingVoxel; // The voxel we're painting if we're painting
|
VoxelDetail paintingVoxel; // The voxel we're painting if we're painting
|
||||||
unsigned char dominantColor = 0; // The dominant color of the voxel we're painting
|
unsigned char dominantColor = 0; // The dominant color of the voxel we're painting
|
||||||
|
bool perfStatsOn = false; // Do we want to display perfStats?
|
||||||
int noise_on = 0; // Whether to add random noise
|
int noise_on = 0; // Whether to add random noise
|
||||||
float noise = 1.0; // Overall magnitude scaling for random noise levels
|
float noise = 1.0; // Overall magnitude scaling for random noise levels
|
||||||
|
|
||||||
|
@ -282,18 +283,45 @@ void display_stats(void)
|
||||||
voxelStats << "Voxels Rendered: " << voxels.getVoxelsRendered();
|
voxelStats << "Voxels Rendered: " << voxels.getVoxelsRendered();
|
||||||
drawtext(10,70,0.10f, 0, 1.0, 0, (char *)voxelStats.str().c_str());
|
drawtext(10,70,0.10f, 0, 1.0, 0, (char *)voxelStats.str().c_str());
|
||||||
|
|
||||||
// Get the PerfStats group details. We need to allocate and array of char* long enough to hold 1+groups
|
voxelStats.str("");
|
||||||
char** perfStatLinesArray = new char*[PerfStat::getGroupCount()+1];
|
voxelStats << "Voxels Created: " << voxels.getVoxelsCreated() << " (" << voxels.getVoxelsCreatedRunningAverage()
|
||||||
int lines = PerfStat::DumpStats(perfStatLinesArray);
|
<< "/sec in last "<< COUNTETSTATS_TIME_FRAME << " seconds) ";
|
||||||
int atZ = 150; // arbitrary place on screen that looks good
|
drawtext(10,250,0.10f, 0, 1.0, 0, (char *)voxelStats.str().c_str());
|
||||||
for (int line=0; line < lines; line++) {
|
|
||||||
drawtext(10,atZ,0.10f, 0, 1.0, 0, perfStatLinesArray[line]);
|
voxelStats.str("");
|
||||||
delete perfStatLinesArray[line]; // we're responsible for cleanup
|
voxelStats << "Voxels Colored: " << voxels.getVoxelsColored() << " (" << voxels.getVoxelsColoredRunningAverage()
|
||||||
perfStatLinesArray[line]=NULL;
|
<< "/sec in last "<< COUNTETSTATS_TIME_FRAME << " seconds) ";
|
||||||
atZ+=20; // height of a line
|
drawtext(10,270,0.10f, 0, 1.0, 0, (char *)voxelStats.str().c_str());
|
||||||
}
|
|
||||||
delete []perfStatLinesArray; // we're responsible for cleanup
|
voxelStats.str("");
|
||||||
|
voxelStats << "Voxels Bytes Read: " << voxels.getVoxelsBytesRead()
|
||||||
|
<< " (" << voxels.getVoxelsBytesReadRunningAverage() << "/sec in last "<< COUNTETSTATS_TIME_FRAME << " seconds) ";
|
||||||
|
drawtext(10,290,0.10f, 0, 1.0, 0, (char *)voxelStats.str().c_str());
|
||||||
|
|
||||||
|
voxelStats.str("");
|
||||||
|
long int voxelsBytesPerColored = voxels.getVoxelsColored() ? voxels.getVoxelsBytesRead()/voxels.getVoxelsColored() : 0;
|
||||||
|
long int voxelsBytesPerColoredAvg = voxels.getVoxelsColoredRunningAverage() ?
|
||||||
|
voxels.getVoxelsBytesReadRunningAverage()/voxels.getVoxelsColoredRunningAverage() : 0;
|
||||||
|
|
||||||
|
voxelStats << "Voxels Bytes per Colored: " << voxelsBytesPerColored
|
||||||
|
<< " (" << voxelsBytesPerColoredAvg << "/sec in last "<< COUNTETSTATS_TIME_FRAME << " seconds) ";
|
||||||
|
drawtext(10,310,0.10f, 0, 1.0, 0, (char *)voxelStats.str().c_str());
|
||||||
|
|
||||||
|
|
||||||
|
if (::perfStatsOn) {
|
||||||
|
// Get the PerfStats group details. We need to allocate and array of char* long enough to hold 1+groups
|
||||||
|
char** perfStatLinesArray = new char*[PerfStat::getGroupCount()+1];
|
||||||
|
int lines = PerfStat::DumpStats(perfStatLinesArray);
|
||||||
|
int atZ = 150; // arbitrary place on screen that looks good
|
||||||
|
for (int line=0; line < lines; line++) {
|
||||||
|
drawtext(10,atZ,0.10f, 0, 1.0, 0, perfStatLinesArray[line]);
|
||||||
|
delete perfStatLinesArray[line]; // we're responsible for cleanup
|
||||||
|
perfStatLinesArray[line]=NULL;
|
||||||
|
atZ+=20; // height of a line
|
||||||
|
}
|
||||||
|
delete []perfStatLinesArray; // we're responsible for cleanup
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
std::stringstream angles;
|
std::stringstream angles;
|
||||||
angles << "render_yaw: " << myHead.getRenderYaw() << ", Yaw: " << myHead.getYaw();
|
angles << "render_yaw: " << myHead.getRenderYaw() << ", Yaw: " << myHead.getYaw();
|
||||||
|
|
90
shared/src/CounterStats.cpp
Normal file
90
shared/src/CounterStats.cpp
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
//
|
||||||
|
// CounterStats.cpp
|
||||||
|
// hifi
|
||||||
|
//
|
||||||
|
// Created by Brad Hefta-Gaub on 2013/04/08.
|
||||||
|
//
|
||||||
|
// Poor-man's counter stats collector class. Useful for collecting running averages
|
||||||
|
// and other stats for countable things.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "CounterStats.h"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
|
||||||
|
//private:
|
||||||
|
// long int currentCount;
|
||||||
|
// long int currentDelta;
|
||||||
|
// double currentTime;
|
||||||
|
// double totalTime;
|
||||||
|
//
|
||||||
|
// long int countSamples[COUNTETSTATS_SAMPLES_TO_KEEP] = {};
|
||||||
|
// long int deltaSamples[COUNTETSTATS_SAMPLES_TO_KEEP] = {};
|
||||||
|
// double timeSamples[COUNTETSTATS_SAMPLES_TO_KEEP] = {};
|
||||||
|
// int sampleAt;
|
||||||
|
|
||||||
|
|
||||||
|
void CounterStatHistory::recordSample(long int thisCount) {
|
||||||
|
timeval now;
|
||||||
|
gettimeofday(&now,NULL);
|
||||||
|
double nowSeconds = (now.tv_usec/1000000.0)+(now.tv_sec);
|
||||||
|
this->recordSample(nowSeconds,thisCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CounterStatHistory::recordSample(double thisTime, long int thisCount) {
|
||||||
|
|
||||||
|
// how much did we change since last sample?
|
||||||
|
long int thisDelta = thisCount - this->lastCount;
|
||||||
|
double elapsed = thisTime - this->lastTime;
|
||||||
|
|
||||||
|
// record the latest values
|
||||||
|
this->currentCount = thisCount;
|
||||||
|
this->currentTime = thisTime;
|
||||||
|
this->currentDelta = thisDelta;
|
||||||
|
|
||||||
|
//printf("CounterStatHistory[%s]::recordSample(thisTime %lf, thisCount= %ld)\n",this->name.c_str(),thisTime,thisCount);
|
||||||
|
|
||||||
|
// if more than 1/10th of a second has passed, then record
|
||||||
|
// things in our rolling history
|
||||||
|
if (elapsed > 0.1) {
|
||||||
|
this->lastTime = thisTime;
|
||||||
|
this->lastCount = thisCount;
|
||||||
|
|
||||||
|
// record it in our history...
|
||||||
|
this->sampleAt = (this->sampleAt+1)%COUNTETSTATS_SAMPLES_TO_KEEP;
|
||||||
|
if (this->sampleCount<COUNTETSTATS_SAMPLES_TO_KEEP) {
|
||||||
|
this->sampleCount++;
|
||||||
|
}
|
||||||
|
this->countSamples[this->sampleAt]=thisCount;
|
||||||
|
this->timeSamples[this->sampleAt]=thisTime;
|
||||||
|
this->deltaSamples[this->sampleAt]=thisDelta;
|
||||||
|
|
||||||
|
//printf("CounterStatHistory[%s]::recordSample() ACTUALLY RECORDING IT sampleAt=%d thisTime %lf, thisCount= %ld)\n",this->name.c_str(),this->sampleAt,thisTime,thisCount);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
long int CounterStatHistory::getRunningAverage() {
|
||||||
|
// before we calculate our running average, always "reset" the current count, with the current time
|
||||||
|
// this will flush out old data, if we haven't been adding any new data.
|
||||||
|
this->recordSample(this->currentCount);
|
||||||
|
|
||||||
|
long int runningTotal = 0;
|
||||||
|
double minTime = this->timeSamples[0];
|
||||||
|
double maxTime = this->timeSamples[0];
|
||||||
|
|
||||||
|
for (int i =0; i < this->sampleCount; i++) {
|
||||||
|
minTime = std::min(minTime,this->timeSamples[i]);
|
||||||
|
maxTime = std::max(maxTime,this->timeSamples[i]);
|
||||||
|
runningTotal += this->deltaSamples[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
double elapsedTime = maxTime-minTime;
|
||||||
|
long int runningAverage = runningTotal/elapsedTime;
|
||||||
|
return runningAverage;
|
||||||
|
}
|
79
shared/src/CounterStats.h
Normal file
79
shared/src/CounterStats.h
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// CounterStats.h
|
||||||
|
// hifi
|
||||||
|
//
|
||||||
|
// Created by Brad Hefta-Gaub on 3/29/13.
|
||||||
|
//
|
||||||
|
// Poor-man's counter stats collector class. Useful for collecting running averages
|
||||||
|
// and other stats for countable things.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef __hifi__CounterStats__
|
||||||
|
#define __hifi__CounterStats__
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
// TIME_FRAME should be SAMPLES_TO_KEEP * TIME_BETWEEN_SAMPLES
|
||||||
|
#define COUNTETSTATS_SAMPLES_TO_KEEP 50
|
||||||
|
#define COUNTETSTATS_TIME_BETWEEN_SAMPLES 0.1
|
||||||
|
#define COUNTETSTATS_TIME_FRAME (COUNTETSTATS_SAMPLES_TO_KEEP*COUNTETSTATS_TIME_BETWEEN_SAMPLES)
|
||||||
|
|
||||||
|
class CounterStatHistory {
|
||||||
|
|
||||||
|
private:
|
||||||
|
long int currentCount;
|
||||||
|
long int currentDelta;
|
||||||
|
double currentTime;
|
||||||
|
|
||||||
|
long int lastCount;
|
||||||
|
double lastTime;
|
||||||
|
|
||||||
|
double totalTime;
|
||||||
|
|
||||||
|
long int countSamples[COUNTETSTATS_SAMPLES_TO_KEEP] = {};
|
||||||
|
long int deltaSamples[COUNTETSTATS_SAMPLES_TO_KEEP] = {};
|
||||||
|
double timeSamples[COUNTETSTATS_SAMPLES_TO_KEEP] = {};
|
||||||
|
int sampleAt;
|
||||||
|
int sampleCount;
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string name;
|
||||||
|
|
||||||
|
CounterStatHistory(std::string myName):
|
||||||
|
currentCount(0), currentDelta(0),currentTime(0.0),
|
||||||
|
lastCount(0),lastTime(0.0),
|
||||||
|
totalTime(0.0),
|
||||||
|
sampleAt(-1),sampleCount(0), name(myName) {};
|
||||||
|
|
||||||
|
CounterStatHistory():
|
||||||
|
currentCount(0), currentDelta(0),currentTime(0.0),
|
||||||
|
lastCount(0),lastTime(0.0),
|
||||||
|
totalTime(0.0),
|
||||||
|
sampleAt(-1),sampleCount(0) {};
|
||||||
|
|
||||||
|
CounterStatHistory(std::string myName, double initialTime, long int initialCount) :
|
||||||
|
currentCount(initialCount), currentDelta(0), currentTime(initialTime),
|
||||||
|
lastCount(initialCount),lastTime(initialTime),
|
||||||
|
totalTime(initialTime),
|
||||||
|
sampleAt(-1), sampleCount(0), name(myName) {};
|
||||||
|
|
||||||
|
void recordSample(long int thisCount);
|
||||||
|
void recordSample(double thisTime, long int thisCount);
|
||||||
|
long int getRunningAverage();
|
||||||
|
|
||||||
|
long int getAverage() {
|
||||||
|
return currentCount/totalTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
double getTotalTime() {
|
||||||
|
return totalTime;
|
||||||
|
};
|
||||||
|
long int getCount() {
|
||||||
|
return currentCount;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(__hifi__CounterStat__) */
|
|
@ -10,6 +10,7 @@
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include "SharedUtil.h"
|
#include "SharedUtil.h"
|
||||||
|
#include "CounterStats.h"
|
||||||
#include "OctalCode.h"
|
#include "OctalCode.h"
|
||||||
#include "VoxelTree.h"
|
#include "VoxelTree.h"
|
||||||
#include <iostream> // to load voxels from file
|
#include <iostream> // to load voxels from file
|
||||||
|
@ -44,6 +45,15 @@ VoxelTree::VoxelTree() {
|
||||||
rootNode = new VoxelNode();
|
rootNode = new VoxelNode();
|
||||||
rootNode->octalCode = new unsigned char[1];
|
rootNode->octalCode = new unsigned char[1];
|
||||||
*rootNode->octalCode = 0;
|
*rootNode->octalCode = 0;
|
||||||
|
|
||||||
|
// Some stats tracking
|
||||||
|
this->voxelsCreated = 0; // when a voxel is created in the tree (object new'd)
|
||||||
|
this->voxelsColored = 0; // when a voxel is colored/set in the tree (object may have already existed)
|
||||||
|
this->voxelsBytesRead = 0;
|
||||||
|
voxelsCreatedStats.name = "voxelsCreated";
|
||||||
|
voxelsColoredStats.name = "voxelsColored";
|
||||||
|
voxelsBytesReadStats.name = "voxelsBytesRead";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VoxelTree::~VoxelTree() {
|
VoxelTree::~VoxelTree() {
|
||||||
|
@ -94,6 +104,9 @@ VoxelNode * VoxelTree::createMissingNode(VoxelNode *lastParentNode, unsigned cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BHG Notes: We appear to call this function for every Voxel Node getting created.
|
||||||
|
// This is recursive in nature. So, for example, if we are given an octal code for
|
||||||
|
// a 1/256th size voxel, we appear to call this function 8 times. Maybe??
|
||||||
int VoxelTree::readNodeData(VoxelNode *destinationNode,
|
int VoxelTree::readNodeData(VoxelNode *destinationNode,
|
||||||
unsigned char * nodeData,
|
unsigned char * nodeData,
|
||||||
int bytesLeftToRead) {
|
int bytesLeftToRead) {
|
||||||
|
@ -107,11 +120,15 @@ int VoxelTree::readNodeData(VoxelNode *destinationNode,
|
||||||
// create the child if it doesn't exist
|
// create the child if it doesn't exist
|
||||||
if (destinationNode->children[i] == NULL) {
|
if (destinationNode->children[i] == NULL) {
|
||||||
destinationNode->addChildAtIndex(i);
|
destinationNode->addChildAtIndex(i);
|
||||||
|
this->voxelsCreated++;
|
||||||
|
this->voxelsCreatedStats.recordSample(this->voxelsCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pull the color for this child
|
// pull the color for this child
|
||||||
memcpy(destinationNode->children[i]->color, nodeData + bytesRead, 3);
|
memcpy(destinationNode->children[i]->color, nodeData + bytesRead, 3);
|
||||||
destinationNode->children[i]->color[3] = 1;
|
destinationNode->children[i]->color[3] = 1;
|
||||||
|
this->voxelsColored++;
|
||||||
|
this->voxelsColoredStats.recordSample(this->voxelsColored);
|
||||||
|
|
||||||
bytesRead += 3;
|
bytesRead += 3;
|
||||||
}
|
}
|
||||||
|
@ -133,6 +150,8 @@ int VoxelTree::readNodeData(VoxelNode *destinationNode,
|
||||||
if (destinationNode->children[childIndex] == NULL) {
|
if (destinationNode->children[childIndex] == NULL) {
|
||||||
// add a child at that index, if it doesn't exist
|
// add a child at that index, if it doesn't exist
|
||||||
destinationNode->addChildAtIndex(childIndex);
|
destinationNode->addChildAtIndex(childIndex);
|
||||||
|
this->voxelsCreated++;
|
||||||
|
this->voxelsCreatedStats.recordSample(this->voxelsCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tell the child to read the subsequent data
|
// tell the child to read the subsequent data
|
||||||
|
@ -158,6 +177,9 @@ void VoxelTree::readBitstreamToTree(unsigned char * bitstream, int bufferSizeByt
|
||||||
|
|
||||||
int octalCodeBytes = bytesRequiredForCodeLength(*bitstream);
|
int octalCodeBytes = bytesRequiredForCodeLength(*bitstream);
|
||||||
readNodeData(bitstreamRootNode, bitstream + octalCodeBytes, bufferSizeBytes - octalCodeBytes);
|
readNodeData(bitstreamRootNode, bitstream + octalCodeBytes, bufferSizeBytes - octalCodeBytes);
|
||||||
|
|
||||||
|
this->voxelsBytesRead += bufferSizeBytes;
|
||||||
|
this->voxelsBytesReadStats.recordSample(this->voxelsBytesRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: uses the codeColorBuffer format, but the color's are ignored, because
|
// Note: uses the codeColorBuffer format, but the color's are ignored, because
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
#ifndef __hifi__VoxelTree__
|
#ifndef __hifi__VoxelTree__
|
||||||
#define __hifi__VoxelTree__
|
#define __hifi__VoxelTree__
|
||||||
|
|
||||||
|
#include "CounterStats.h"
|
||||||
|
|
||||||
#include "VoxelNode.h"
|
#include "VoxelNode.h"
|
||||||
#include "MarkerNode.h"
|
#include "MarkerNode.h"
|
||||||
|
|
||||||
|
@ -20,7 +22,17 @@ class VoxelTree {
|
||||||
VoxelNode * nodeForOctalCode(VoxelNode *ancestorNode, unsigned char * needleCode, VoxelNode** parentOfFoundNode);
|
VoxelNode * nodeForOctalCode(VoxelNode *ancestorNode, unsigned char * needleCode, VoxelNode** parentOfFoundNode);
|
||||||
VoxelNode * createMissingNode(VoxelNode *lastParentNode, unsigned char *deepestCodeToCreate);
|
VoxelNode * createMissingNode(VoxelNode *lastParentNode, unsigned char *deepestCodeToCreate);
|
||||||
int readNodeData(VoxelNode *destinationNode, unsigned char * nodeData, int bufferSizeBytes);
|
int readNodeData(VoxelNode *destinationNode, unsigned char * nodeData, int bufferSizeBytes);
|
||||||
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
long int voxelsCreated;
|
||||||
|
long int voxelsColored;
|
||||||
|
long int voxelsBytesRead;
|
||||||
|
|
||||||
|
CounterStatHistory voxelsCreatedStats;
|
||||||
|
CounterStatHistory voxelsColoredStats;
|
||||||
|
CounterStatHistory voxelsBytesReadStats;
|
||||||
|
|
||||||
VoxelTree();
|
VoxelTree();
|
||||||
~VoxelTree();
|
~VoxelTree();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue