diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp
index 6508f09f72..f37e8cf3f7 100644
--- a/assignment-client/src/entities/EntityServer.cpp
+++ b/assignment-client/src/entities/EntityServer.cpp
@@ -266,3 +266,72 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
tree->setWantEditLogging(wantEditLogging);
tree->setWantTerseEditLogging(wantTerseEditLogging);
}
+
+
+// FIXME - this stats tracking is somewhat temporary to debug the Whiteboard issues. It's not a bad
+// set of stats to have, but we'd probably want a different data structure if we keep it very long.
+// Since this version uses a single shared QMap for all senders, there could be some lock contention
+// on this QWriteLocker
+void EntityServer::trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& viewerNode) {
+ QWriteLocker locker(&_viewerSendingStatsLock);
+ _viewerSendingStats[viewerNode][dataID] = { usecTimestampNow(), dataLastEdited };
+}
+
+void EntityServer::trackViewerGone(const QUuid& viewerNode) {
+ QWriteLocker locker(&_viewerSendingStatsLock);
+ _viewerSendingStats.remove(viewerNode);
+}
+
+QString EntityServer::serverSubclassStats() {
+ QLocale locale(QLocale::English);
+ QString statsString;
+
+ // display memory usage stats
+ statsString += "Entity Server Memory Statistics\r\n";
+ statsString += QString().sprintf("EntityTreeElement size... %ld bytes\r\n", sizeof(EntityTreeElement));
+ statsString += QString().sprintf(" EntityItem size... %ld bytes\r\n", sizeof(EntityItem));
+ statsString += "\r\n\r\n";
+
+ statsString += "Entity Server Sending to Viewer Statistics\r\n";
+ statsString += "----- Viewer Node ID ----------------- ----- Entity ID ---------------------- "
+ "---------- Last Sent To ---------- ---------- Last Edited -----------\r\n";
+
+ int viewers = 0;
+ const int COLUMN_WIDTH = 24;
+
+ {
+ QReadLocker locker(&_viewerSendingStatsLock);
+ quint64 now = usecTimestampNow();
+
+ for (auto viewerID : _viewerSendingStats.keys()) {
+ statsString += viewerID.toString() + "\r\n";
+
+ auto viewerData = _viewerSendingStats[viewerID];
+ for (auto entityID : viewerData.keys()) {
+ ViewerSendingStats stats = viewerData[entityID];
+
+ quint64 elapsedSinceSent = now - stats.lastSent;
+ double sentMsecsAgo = (double)(elapsedSinceSent / USECS_PER_MSEC);
+
+ quint64 elapsedSinceEdit = now - stats.lastEdited;
+ double editMsecsAgo = (double)(elapsedSinceEdit / USECS_PER_MSEC);
+
+ statsString += " "; // the viewerID spacing
+ statsString += entityID.toString();
+ statsString += " ";
+ statsString += QString("%1 msecs ago")
+ .arg(locale.toString((double)sentMsecsAgo).rightJustified(COLUMN_WIDTH, ' '));
+ statsString += QString("%1 msecs ago")
+ .arg(locale.toString((double)editMsecsAgo).rightJustified(COLUMN_WIDTH, ' '));
+ statsString += "\r\n";
+ }
+ viewers++;
+ }
+ }
+ if (viewers < 1) {
+ statsString += " no viewers... \r\n";
+ }
+ statsString += "\r\n\r\n";
+
+ return statsString;
+}
diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h
index d9795316c4..89b445c449 100644
--- a/assignment-client/src/entities/EntityServer.h
+++ b/assignment-client/src/entities/EntityServer.h
@@ -21,6 +21,12 @@
#include "EntityTree.h"
/// Handles assignments of type EntityServer - sending entities to various clients.
+
+struct ViewerSendingStats {
+ quint64 lastSent;
+ quint64 lastEdited;
+};
+
class EntityServer : public OctreeServer, public NewlyCreatedEntityHook {
Q_OBJECT
public:
@@ -44,6 +50,10 @@ public:
virtual void entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) override;
virtual void readAdditionalConfiguration(const QJsonObject& settingsSectionObject) override;
+ virtual QString serverSubclassStats();
+
+ virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& viewerNode);
+ virtual void trackViewerGone(const QUuid& viewerNode);
public slots:
void pruneDeletedEntities();
@@ -57,6 +67,9 @@ private slots:
private:
EntitySimulation* _entitySimulation;
QTimer* _pruneDeletedEntitiesTimer = nullptr;
+
+ QReadWriteLock _viewerSendingStatsLock;
+ QMap> _viewerSendingStats;
};
#endif // hifi_EntityServer_h
diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp
index 94d82b463e..0a32f574de 100644
--- a/assignment-client/src/octree/OctreeSendThread.cpp
+++ b/assignment-client/src/octree/OctreeSendThread.cpp
@@ -467,6 +467,12 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus
isFullScene, &nodeData->stats, _myServer->getJurisdiction(),
&nodeData->extraEncodeData);
+ // Our trackSend() function is implemented by the server subclass, and will be called back
+ // during the encodeTreeBitstream() as new entities/data elements are sent
+ params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) {
+ _myServer->trackSend(dataID, dataEdited, _nodeUUID);
+ };
+
// TODO: should this include the lock time or not? This stat is sent down to the client,
// it seems like it may be a good idea to include the lock time as part of the encode time
// are reported to client. Since you can encode without the lock
diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp
index 7cd3e59edf..dead61d65a 100644
--- a/assignment-client/src/octree/OctreeServer.cpp
+++ b/assignment-client/src/octree/OctreeServer.cpp
@@ -821,6 +821,11 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
.arg(locale.toString((uint)checkSum).rightJustified(16, ' '));
statsString += "\r\n\r\n";
+
+ statsString += serverSubclassStats();
+
+ statsString += "\r\n\r\n";
+
statsString += "\r\n";
statsString += "