mirror of
https://github.com/overte-org/overte.git
synced 2025-07-22 21:06:24 +02:00
Only keep aggregated thread stats
This commit is contained in:
parent
b9217e2767
commit
cfbb4e29d1
6 changed files with 8 additions and 100 deletions
|
@ -746,65 +746,27 @@ void AvatarMixer::sendStatsPacket() {
|
||||||
|
|
||||||
|
|
||||||
AvatarMixerSlaveStats aggregateStats;
|
AvatarMixerSlaveStats aggregateStats;
|
||||||
QJsonObject slavesObject;
|
|
||||||
|
|
||||||
float secondsSinceLastStats = (float)(start - _lastStatsTime) / (float)USECS_PER_SECOND;
|
|
||||||
// gather stats
|
// gather stats
|
||||||
int slaveNumber = 1;
|
|
||||||
_slavePool.each([&](AvatarMixerSlave& slave) {
|
_slavePool.each([&](AvatarMixerSlave& slave) {
|
||||||
QJsonObject slaveObject;
|
|
||||||
AvatarMixerSlaveStats stats;
|
AvatarMixerSlaveStats stats;
|
||||||
slave.harvestStats(stats);
|
slave.harvestStats(stats);
|
||||||
slaveObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(stats.nodesProcessed);
|
|
||||||
slaveObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(stats.packetsProcessed);
|
|
||||||
|
|
||||||
slaveObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(stats.nodesBroadcastedTo);
|
|
||||||
slaveObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(stats.numBytesSent);
|
|
||||||
slaveObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(stats.numPacketsSent);
|
|
||||||
slaveObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(stats.numIdentityPackets);
|
|
||||||
|
|
||||||
float averageNodes = ((float)stats.nodesBroadcastedTo / (float)tightLoopFrames);
|
|
||||||
float averageOutboundAvatarKbps = averageNodes ? ((stats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
|
|
||||||
slaveObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
|
|
||||||
|
|
||||||
float averageOthersIncluded = averageNodes ? stats.numOthersIncluded / averageNodes : 0.0f;
|
|
||||||
slaveObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
|
|
||||||
|
|
||||||
float averageOverBudgetAvatars = averageNodes ? stats.overBudgetAvatars / averageNodes : 0.0f;
|
|
||||||
slaveObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
|
||||||
|
|
||||||
slaveObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(stats.processIncomingPacketsElapsedTime);
|
|
||||||
slaveObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(stats.ignoreCalculationElapsedTime);
|
|
||||||
slaveObject["timing_3_toByteArray"] = TIGHT_LOOP_STAT_UINT64(stats.toByteArrayElapsedTime);
|
|
||||||
slaveObject["timing_4_avatarDataPacking"] = TIGHT_LOOP_STAT_UINT64(stats.avatarDataPackingElapsedTime);
|
|
||||||
slaveObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(stats.packetSendingElapsedTime);
|
|
||||||
slaveObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(stats.jobElapsedTime);
|
|
||||||
|
|
||||||
slavesObject[QString::number(slaveNumber)] = slaveObject;
|
|
||||||
slaveNumber++;
|
|
||||||
|
|
||||||
aggregateStats += stats;
|
aggregateStats += stats;
|
||||||
});
|
});
|
||||||
|
|
||||||
QJsonObject slavesAggregatObject;
|
QJsonObject slavesAggregatObject;
|
||||||
|
|
||||||
slavesAggregatObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
|
slavesAggregatObject["received_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
|
||||||
slavesAggregatObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(aggregateStats.packetsProcessed);
|
|
||||||
|
|
||||||
slavesAggregatObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(aggregateStats.nodesBroadcastedTo);
|
slavesAggregatObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(aggregateStats.nodesBroadcastedTo);
|
||||||
slavesAggregatObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(aggregateStats.numBytesSent);
|
|
||||||
slavesAggregatObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(aggregateStats.numPacketsSent);
|
|
||||||
slavesAggregatObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityPackets);
|
|
||||||
|
|
||||||
float averageNodes = ((float)aggregateStats.nodesBroadcastedTo / (float)tightLoopFrames);
|
float averageNodes = ((float)aggregateStats.nodesBroadcastedTo / (float)tightLoopFrames);
|
||||||
float averageOutboundAvatarKbps = averageNodes ? ((aggregateStats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
|
|
||||||
slavesAggregatObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
|
|
||||||
|
|
||||||
float averageOthersIncluded = averageNodes ? aggregateStats.numOthersIncluded / averageNodes : 0.0f;
|
float averageOthersIncluded = averageNodes ? aggregateStats.numOthersIncluded / averageNodes : 0.0f;
|
||||||
slavesAggregatObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
|
slavesAggregatObject["sent_2_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
|
||||||
|
|
||||||
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
|
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
|
||||||
slavesAggregatObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
slavesAggregatObject["sent_3_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
||||||
|
|
||||||
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
|
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
|
||||||
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
|
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
|
||||||
|
@ -814,7 +776,6 @@ void AvatarMixer::sendStatsPacket() {
|
||||||
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
|
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
|
||||||
|
|
||||||
statsObject["slaves_aggregate"] = slavesAggregatObject;
|
statsObject["slaves_aggregate"] = slavesAggregatObject;
|
||||||
statsObject["slaves_individual"] = slavesObject;
|
|
||||||
|
|
||||||
_handleViewFrustumPacketElapsedTime = 0;
|
_handleViewFrustumPacketElapsedTime = 0;
|
||||||
_handleAvatarIdentityPacketElapsedTime = 0;
|
_handleAvatarIdentityPacketElapsedTime = 0;
|
||||||
|
|
|
@ -113,8 +113,6 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) :
|
||||||
// handle when a socket connection has its receiver side reset - might need to emit clientConnectionToNodeReset
|
// handle when a socket connection has its receiver side reset - might need to emit clientConnectionToNodeReset
|
||||||
connect(&_nodeSocket, &udt::Socket::clientHandshakeRequestComplete, this, &LimitedNodeList::clientConnectionToSockAddrReset);
|
connect(&_nodeSocket, &udt::Socket::clientHandshakeRequestComplete, this, &LimitedNodeList::clientConnectionToSockAddrReset);
|
||||||
|
|
||||||
_packetStatTimer.start();
|
|
||||||
|
|
||||||
if (_stunSockAddr.getAddress().isNull()) {
|
if (_stunSockAddr.getAddress().isNull()) {
|
||||||
// we don't know the stun server socket yet, add it to unfiltered once known
|
// we don't know the stun server socket yet, add it to unfiltered once known
|
||||||
connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::addSTUNHandlerToUnfiltered);
|
connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::addSTUNHandlerToUnfiltered);
|
||||||
|
@ -378,12 +376,6 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LimitedNodeList::collectPacketStats(const NLPacket& packet) {
|
|
||||||
// stat collection for packets
|
|
||||||
++_numCollectedPackets;
|
|
||||||
_numCollectedBytes += packet.getDataSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth) {
|
void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth) {
|
||||||
if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) {
|
if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) {
|
||||||
packet.writeSourceID(getSessionLocalID());
|
packet.writeSourceID(getSessionLocalID());
|
||||||
|
@ -414,7 +406,6 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiS
|
||||||
Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket",
|
Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket",
|
||||||
"Trying to send a reliable packet unreliably.");
|
"Trying to send a reliable packet unreliably.");
|
||||||
|
|
||||||
collectPacketStats(packet);
|
|
||||||
fillPacketHeader(packet, hmacAuth);
|
fillPacketHeader(packet, hmacAuth);
|
||||||
|
|
||||||
return _nodeSocket.writePacket(packet, sockAddr);
|
return _nodeSocket.writePacket(packet, sockAddr);
|
||||||
|
@ -436,7 +427,6 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr<NLPacket> packet, const HifiS
|
||||||
HMACAuth* hmacAuth) {
|
HMACAuth* hmacAuth) {
|
||||||
Q_ASSERT(!packet->isPartOfMessage());
|
Q_ASSERT(!packet->isPartOfMessage());
|
||||||
if (packet->isReliable()) {
|
if (packet->isReliable()) {
|
||||||
collectPacketStats(*packet);
|
|
||||||
fillPacketHeader(*packet, hmacAuth);
|
fillPacketHeader(*packet, hmacAuth);
|
||||||
|
|
||||||
auto size = packet->getDataSize();
|
auto size = packet->getDataSize();
|
||||||
|
@ -490,7 +480,6 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr<NLPacketList> packetList,
|
||||||
|
|
||||||
for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) {
|
for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) {
|
||||||
NLPacket* nlPacket = static_cast<NLPacket*>(packet.get());
|
NLPacket* nlPacket = static_cast<NLPacket*>(packet.get());
|
||||||
collectPacketStats(*nlPacket);
|
|
||||||
fillPacketHeader(*nlPacket);
|
fillPacketHeader(*nlPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,7 +494,6 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr<NLPacketList> packetList,
|
||||||
|
|
||||||
for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) {
|
for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) {
|
||||||
NLPacket* nlPacket = static_cast<NLPacket*>(packet.get());
|
NLPacket* nlPacket = static_cast<NLPacket*>(packet.get());
|
||||||
collectPacketStats(*nlPacket);
|
|
||||||
fillPacketHeader(*nlPacket, destinationNode.getAuthenticateHash());
|
fillPacketHeader(*nlPacket, destinationNode.getAuthenticateHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -832,23 +820,6 @@ SharedNodePointer LimitedNodeList::soloNodeOfType(NodeType_t nodeType) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void LimitedNodeList::getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond) {
|
|
||||||
packetsInPerSecond = (float) getPacketReceiver().getInPacketCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
|
|
||||||
bytesInPerSecond = (float) getPacketReceiver().getInByteCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
|
|
||||||
|
|
||||||
packetsOutPerSecond = (float) _numCollectedPackets / ((float) _packetStatTimer.elapsed() / 1000.0f);
|
|
||||||
bytesOutPerSecond = (float) _numCollectedBytes / ((float) _packetStatTimer.elapsed() / 1000.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LimitedNodeList::resetPacketStats() {
|
|
||||||
getPacketReceiver().resetCounters();
|
|
||||||
|
|
||||||
_numCollectedPackets = 0;
|
|
||||||
_numCollectedBytes = 0;
|
|
||||||
|
|
||||||
_packetStatTimer.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LimitedNodeList::removeSilentNodes() {
|
void LimitedNodeList::removeSilentNodes() {
|
||||||
|
|
||||||
QSet<SharedNodePointer> killedNodes;
|
QSet<SharedNodePointer> killedNodes;
|
||||||
|
|
|
@ -183,9 +183,6 @@ public:
|
||||||
unsigned int broadcastToNodes(std::unique_ptr<NLPacket> packet, const NodeSet& destinationNodeTypes);
|
unsigned int broadcastToNodes(std::unique_ptr<NLPacket> packet, const NodeSet& destinationNodeTypes);
|
||||||
SharedNodePointer soloNodeOfType(NodeType_t nodeType);
|
SharedNodePointer soloNodeOfType(NodeType_t nodeType);
|
||||||
|
|
||||||
void getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond);
|
|
||||||
void resetPacketStats();
|
|
||||||
|
|
||||||
std::unique_ptr<NLPacket> constructPingPacket(const QUuid& nodeId, PingType_t pingType = PingType::Agnostic);
|
std::unique_ptr<NLPacket> constructPingPacket(const QUuid& nodeId, PingType_t pingType = PingType::Agnostic);
|
||||||
std::unique_ptr<NLPacket> constructPingReplyPacket(ReceivedMessage& message);
|
std::unique_ptr<NLPacket> constructPingReplyPacket(ReceivedMessage& message);
|
||||||
|
|
||||||
|
@ -377,7 +374,6 @@ protected:
|
||||||
|
|
||||||
qint64 sendPacket(std::unique_ptr<NLPacket> packet, const Node& destinationNode,
|
qint64 sendPacket(std::unique_ptr<NLPacket> packet, const Node& destinationNode,
|
||||||
const HifiSockAddr& overridenSockAddr);
|
const HifiSockAddr& overridenSockAddr);
|
||||||
void collectPacketStats(const NLPacket& packet);
|
|
||||||
void fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth = nullptr);
|
void fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth = nullptr);
|
||||||
|
|
||||||
void setLocalSocket(const HifiSockAddr& sockAddr);
|
void setLocalSocket(const HifiSockAddr& sockAddr);
|
||||||
|
@ -406,10 +402,6 @@ protected:
|
||||||
|
|
||||||
PacketReceiver* _packetReceiver;
|
PacketReceiver* _packetReceiver;
|
||||||
|
|
||||||
std::atomic<int> _numCollectedPackets { 0 };
|
|
||||||
std::atomic<int> _numCollectedBytes { 0 };
|
|
||||||
|
|
||||||
QElapsedTimer _packetStatTimer;
|
|
||||||
NodePermissions _permissions;
|
NodePermissions _permissions;
|
||||||
|
|
||||||
QPointer<QTimer> _initialSTUNTimer;
|
QPointer<QTimer> _initialSTUNTimer;
|
||||||
|
|
|
@ -212,18 +212,12 @@ void PacketReceiver::handleVerifiedPacket(std::unique_ptr<udt::Packet> packet) {
|
||||||
auto nlPacket = NLPacket::fromBase(std::move(packet));
|
auto nlPacket = NLPacket::fromBase(std::move(packet));
|
||||||
auto receivedMessage = QSharedPointer<ReceivedMessage>::create(*nlPacket);
|
auto receivedMessage = QSharedPointer<ReceivedMessage>::create(*nlPacket);
|
||||||
|
|
||||||
_inPacketCount += 1;
|
|
||||||
_inByteCount += nlPacket->size();
|
|
||||||
|
|
||||||
handleVerifiedMessage(receivedMessage, true);
|
handleVerifiedMessage(receivedMessage, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PacketReceiver::handleVerifiedMessagePacket(std::unique_ptr<udt::Packet> packet) {
|
void PacketReceiver::handleVerifiedMessagePacket(std::unique_ptr<udt::Packet> packet) {
|
||||||
auto nlPacket = NLPacket::fromBase(std::move(packet));
|
auto nlPacket = NLPacket::fromBase(std::move(packet));
|
||||||
|
|
||||||
_inPacketCount += 1;
|
|
||||||
_inByteCount += nlPacket->size();
|
|
||||||
|
|
||||||
auto key = std::pair<HifiSockAddr, udt::Packet::MessageNumber>(nlPacket->getSenderSockAddr(), nlPacket->getMessageNumber());
|
auto key = std::pair<HifiSockAddr, udt::Packet::MessageNumber>(nlPacket->getSenderSockAddr(), nlPacket->getMessageNumber());
|
||||||
auto it = _pendingMessages.find(key);
|
auto it = _pendingMessages.find(key);
|
||||||
QSharedPointer<ReceivedMessage> message;
|
QSharedPointer<ReceivedMessage> message;
|
||||||
|
|
|
@ -49,13 +49,8 @@ public:
|
||||||
PacketReceiver(const PacketReceiver&) = delete;
|
PacketReceiver(const PacketReceiver&) = delete;
|
||||||
|
|
||||||
PacketReceiver& operator=(const PacketReceiver&) = delete;
|
PacketReceiver& operator=(const PacketReceiver&) = delete;
|
||||||
|
|
||||||
int getInPacketCount() const { return _inPacketCount; }
|
|
||||||
int getInByteCount() const { return _inByteCount; }
|
|
||||||
|
|
||||||
void setShouldDropPackets(bool shouldDropPackets) { _shouldDropPackets = shouldDropPackets; }
|
void setShouldDropPackets(bool shouldDropPackets) { _shouldDropPackets = shouldDropPackets; }
|
||||||
|
|
||||||
void resetCounters() { _inPacketCount = 0; _inByteCount = 0; }
|
|
||||||
|
|
||||||
// If deliverPending is false, ReceivedMessage will only be delivered once all packets for the message have
|
// If deliverPending is false, ReceivedMessage will only be delivered once all packets for the message have
|
||||||
// been received. If deliverPending is true, ReceivedMessage will be delivered as soon as the first packet
|
// been received. If deliverPending is true, ReceivedMessage will be delivered as soon as the first packet
|
||||||
|
@ -87,8 +82,7 @@ private:
|
||||||
|
|
||||||
QMutex _packetListenerLock;
|
QMutex _packetListenerLock;
|
||||||
QHash<PacketType, Listener> _messageListenerMap;
|
QHash<PacketType, Listener> _messageListenerMap;
|
||||||
int _inPacketCount = 0;
|
|
||||||
int _inByteCount = 0;
|
|
||||||
bool _shouldDropPackets = false;
|
bool _shouldDropPackets = false;
|
||||||
QMutex _directConnectSetMutex;
|
QMutex _directConnectSetMutex;
|
||||||
QSet<QObject*> _directlyConnectedObjects;
|
QSet<QObject*> _directlyConnectedObjects;
|
||||||
|
|
|
@ -94,15 +94,11 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy
|
||||||
void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) {
|
void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) {
|
||||||
auto nodeList = DependencyManager::get<NodeList>();
|
auto nodeList = DependencyManager::get<NodeList>();
|
||||||
|
|
||||||
float packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond;
|
|
||||||
nodeList->getPacketStats(packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond);
|
|
||||||
nodeList->resetPacketStats();
|
|
||||||
|
|
||||||
QJsonObject ioStats;
|
QJsonObject ioStats;
|
||||||
ioStats["inbound_bytes_per_s"] = bytesInPerSecond;
|
ioStats["inbound_kbps"] = nodeList->getInboundKbps();
|
||||||
ioStats["inbound_packets_per_s"] = packetsInPerSecond;
|
ioStats["inbound_pps"] = nodeList->getInboundPPS();
|
||||||
ioStats["outbound_bytes_per_s"] = bytesOutPerSecond;
|
ioStats["outbound_kbps"] = nodeList->getOutboundKbps();
|
||||||
ioStats["outbound_packets_per_s"] = packetsOutPerSecond;
|
ioStats["outbound_pps"] = nodeList->getOutboundPPS();
|
||||||
|
|
||||||
statsObject["io_stats"] = ioStats;
|
statsObject["io_stats"] = ioStats;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue