1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-01-16 18:59:57 +00:00
openmw/components/resource/stats.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

616 lines
20 KiB
C++
Raw Normal View History

#include "stats.hpp"
2019-03-17 17:18:53 +00:00
#include <algorithm>
#include <iomanip>
2024-03-17 18:01:11 +00:00
#include <span>
#include <sstream>
2024-03-17 18:01:11 +00:00
#include <string>
#include <string_view>
#include <vector>
#include <osg/PolygonMode>
2022-07-17 14:16:18 +00:00
#include <osgText/Font>
#include <osgText/Text>
#include <osgDB/Registry>
#include <osgViewer/Renderer>
#include <osgViewer/Viewer>
2022-07-07 15:34:18 +00:00
#include <components/vfs/manager.hpp>
2023-12-21 23:23:49 +00:00
#include "cachestats.hpp"
namespace Resource
{
2024-03-17 18:01:11 +00:00
namespace
{
2024-03-17 18:01:11 +00:00
constexpr float statsWidth = 1280.0f;
constexpr float statsHeight = 1024.0f;
constexpr float characterSize = 17.0f;
constexpr float backgroundMargin = 5;
constexpr float backgroundSpacing = 3;
constexpr float maxStatsHeight = 420.0f;
constexpr std::size_t pageSize
= static_cast<std::size_t>((maxStatsHeight - 2 * backgroundMargin) / characterSize);
constexpr int statsHandlerKey = osgGA::GUIEventAdapter::KEY_F4;
const VFS::Path::Normalized fontName("Fonts/DejaVuLGCSansMono.ttf");
bool collectStatRendering = false;
bool collectStatCameraObjects = false;
bool collectStatViewerObjects = false;
bool collectStatResource = false;
bool collectStatGPU = false;
bool collectStatEvent = false;
bool collectStatFrameRate = false;
bool collectStatUpdate = false;
bool collectStatEngine = false;
2023-12-21 23:23:49 +00:00
std::vector<std::string> generateAllStatNames()
{
constexpr std::size_t itemsPerPage = 24;
2023-12-21 23:23:49 +00:00
constexpr std::string_view firstPage[] = {
"FrameNumber",
"",
2024-04-25 06:18:13 +00:00
"Loading",
2023-12-21 23:23:49 +00:00
"Compiling",
"WorkQueue",
"WorkThread",
"UnrefQueue",
"",
"Texture",
"StateSet",
"Composite",
"",
"Mechanics Actors",
"Mechanics Objects",
"",
"Physics Actors",
"Physics Objects",
"Physics Projectiles",
"Physics HeightFields",
"",
"Lua UsedMemory",
"",
"",
"",
};
static_assert(std::size(firstPage) == itemsPerPage);
2023-12-21 23:23:49 +00:00
constexpr std::string_view caches[] = {
"Node",
"Shape",
"Shape Instance",
"Image",
"Nif",
"Keyframe",
2024-04-18 01:00:20 +00:00
"BSShader Material",
2023-12-21 23:23:49 +00:00
"Groundcover Chunk",
"Object Chunk",
"Terrain Chunk",
"Terrain Texture",
"Land",
};
constexpr std::string_view cellPreloader[] = {
"CellPreloader Count",
"CellPreloader Added",
"CellPreloader Evicted",
"CellPreloader Loaded",
"CellPreloader Expired",
};
constexpr std::string_view navMesh[] = {
"NavMesh Jobs",
"NavMesh Removing",
"NavMesh Updating",
"NavMesh Delayed",
2023-12-21 23:23:49 +00:00
"NavMesh Pushed",
"NavMesh Processing",
"NavMesh DbJobs Write",
"NavMesh DbJobs Read",
"NavMesh DbCache Get",
"NavMesh DbCache Hit",
"NavMesh CacheSize",
"NavMesh UsedTiles",
"NavMesh CachedTiles",
"NavMesh Cache Get",
"NavMesh Cache Hit",
};
std::vector<std::string> statNames;
for (std::string_view name : firstPage)
statNames.emplace_back(name);
for (std::size_t i = 0; i < std::size(caches); ++i)
{
Resource::addCacheStatsAttibutes(caches[i], statNames);
if ((i + 1) % 5 != 0)
statNames.emplace_back();
}
for (std::string_view name : cellPreloader)
statNames.emplace_back(name);
while (statNames.size() % itemsPerPage != 0)
statNames.emplace_back();
2023-12-21 23:23:49 +00:00
for (std::string_view name : navMesh)
statNames.emplace_back(name);
return statNames;
}
2024-03-17 18:01:11 +00:00
void setupStatCollection()
{
const char* envList = getenv("OPENMW_OSG_STATS_LIST");
if (envList == nullptr)
return;
2024-03-17 18:01:11 +00:00
std::string_view kwList(envList);
2024-03-17 18:01:11 +00:00
auto kwBegin = kwList.begin();
2024-03-17 18:01:11 +00:00
while (kwBegin != kwList.end())
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
auto kwEnd = std::find(kwBegin, kwList.end(), ';');
const auto kw = kwList.substr(std::distance(kwList.begin(), kwBegin), std::distance(kwBegin, kwEnd));
if (kw == "gpu")
collectStatGPU = true;
else if (kw == "event")
collectStatEvent = true;
else if (kw == "frame_rate")
collectStatFrameRate = true;
else if (kw == "update")
collectStatUpdate = true;
else if (kw == "engine")
collectStatEngine = true;
else if (kw == "rendering")
collectStatRendering = true;
else if (kw == "cameraobjects")
collectStatCameraObjects = true;
else if (kw == "viewerobjects")
collectStatViewerObjects = true;
else if (kw == "resource")
collectStatResource = true;
else if (kw == "times")
{
collectStatGPU = true;
collectStatEvent = true;
collectStatFrameRate = true;
collectStatUpdate = true;
collectStatEngine = true;
collectStatRendering = true;
}
2024-03-17 18:01:11 +00:00
if (kwEnd == kwList.end())
break;
2024-03-17 18:01:11 +00:00
kwBegin = std::next(kwEnd);
}
2022-09-22 18:26:05 +00:00
}
2022-07-17 16:53:38 +00:00
2024-03-17 18:01:11 +00:00
osg::ref_ptr<osg::Geometry> createBackgroundRectangle(
const osg::Vec3& pos, const float width, const float height, const osg::Vec4& color)
2022-07-17 16:53:38 +00:00
{
2024-03-17 18:01:11 +00:00
osg::ref_ptr<osg::Geometry> geometry = new osg::Geometry;
geometry->setUseDisplayList(false);
osg::ref_ptr<osg::StateSet> stateSet = new osg::StateSet;
geometry->setStateSet(stateSet);
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(pos.x(), pos.y(), 0));
vertices->push_back(osg::Vec3(pos.x(), pos.y() - height, 0));
vertices->push_back(osg::Vec3(pos.x() + width, pos.y() - height, 0));
vertices->push_back(osg::Vec3(pos.x() + width, pos.y(), 0));
geometry->setVertexArray(vertices);
osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array;
colors->push_back(color);
geometry->setColorArray(colors, osg::Array::BIND_OVERALL);
osg::ref_ptr<osg::DrawElementsUShort> base
= new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_FAN, 0);
base->push_back(0);
base->push_back(1);
base->push_back(2);
base->push_back(3);
geometry->addPrimitiveSet(base);
return geometry;
2022-07-17 16:53:38 +00:00
}
2024-03-17 18:01:11 +00:00
osg::ref_ptr<osgText::Font> getMonoFont(const VFS::Manager& vfs)
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
if (osgDB::Registry::instance()->getReaderWriterForExtension("ttf") && vfs.exists(fontName))
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
const Files::IStreamPtr streamPtr = vfs.get(fontName);
return osgText::readRefFontStream(*streamPtr);
2022-09-22 18:26:05 +00:00
}
2024-03-17 18:01:11 +00:00
return nullptr;
2022-07-17 19:59:35 +00:00
}
2024-03-17 18:01:11 +00:00
class SetFontVisitor : public osg::NodeVisitor
{
public:
SetFontVisitor(osgText::Font* font)
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
, mFont(font)
{
}
2024-03-17 18:01:11 +00:00
void apply(osg::Drawable& node) override
{
if (osgText::Text* text = dynamic_cast<osgText::Text*>(&node))
{
text->setFont(mFont);
}
}
2022-07-17 16:53:38 +00:00
2024-03-17 18:01:11 +00:00
private:
osgText::Font* mFont;
};
}
2024-03-17 18:01:11 +00:00
Profiler::Profiler(bool offlineCollect, const VFS::Manager& vfs)
: mOfflineCollect(offlineCollect)
, mTextFont(getMonoFont(vfs))
{
2024-03-17 18:01:11 +00:00
_characterSize = characterSize;
2022-07-17 16:53:38 +00:00
_font.clear();
setKeyEventTogglesOnScreenStats(osgGA::GUIEventAdapter::KEY_F3);
setupStatCollection();
}
2022-07-17 16:53:38 +00:00
void Profiler::setUpFonts()
{
2024-03-17 18:01:11 +00:00
if (mTextFont != nullptr)
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
SetFontVisitor visitor(mTextFont);
2022-07-17 16:53:38 +00:00
_switch->accept(visitor);
}
2024-03-17 18:01:11 +00:00
mInitFonts = true;
2022-07-17 16:53:38 +00:00
}
bool Profiler::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{
osgViewer::ViewerBase* viewer = nullptr;
bool handled = StatsHandler::handle(ea, aa);
2024-03-17 18:01:11 +00:00
if (_initialized && !mInitFonts)
2022-07-17 16:53:38 +00:00
setUpFonts();
auto* view = dynamic_cast<osgViewer::View*>(&aa);
if (view)
viewer = view->getViewerBase();
2024-03-17 18:01:11 +00:00
if (viewer != nullptr)
{
// Add/remove openmw stats to the osd as necessary
viewer->getViewerStats()->collectStats("engine", _statsType >= StatsHandler::StatsType::VIEWER_STATS);
2024-03-17 18:01:11 +00:00
if (mOfflineCollect)
collectStatistics(*viewer);
2022-09-22 18:26:05 +00:00
}
return handled;
}
2024-03-17 18:01:11 +00:00
StatsHandler::StatsHandler(bool offlineCollect, const VFS::Manager& vfs)
: mOfflineCollect(offlineCollect)
, mSwitch(new osg::Switch)
, mCamera(new osg::Camera)
, mTextFont(getMonoFont(vfs))
2023-12-21 23:23:49 +00:00
, mStatNames(generateAllStatNames())
2024-03-17 18:01:11 +00:00
{
osg::ref_ptr<osg::StateSet> stateset = mSwitch->getOrCreateStateSet();
stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
#ifdef OSG_GL1_AVAILABLE
stateset->setAttribute(new osg::PolygonMode(), osg::StateAttribute::PROTECTED);
#endif
mCamera->getOrCreateStateSet()->setGlobalDefaults();
mCamera->setRenderer(new osgViewer::Renderer(mCamera.get()));
mCamera->setProjectionResizePolicy(osg::Camera::FIXED);
mCamera->addChild(mSwitch);
}
bool StatsHandler::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{
if (ea.getHandled())
return false;
2022-09-22 18:26:05 +00:00
switch (ea.getEventType())
{
case (osgGA::GUIEventAdapter::KEYDOWN):
{
2024-03-17 18:01:11 +00:00
if (ea.getKey() == statsHandlerKey)
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
osgViewer::View* const view = dynamic_cast<osgViewer::View*>(&aa);
if (view == nullptr)
return false;
2024-03-17 18:01:11 +00:00
osgViewer::ViewerBase* const viewer = view->getViewerBase();
2024-03-17 18:01:11 +00:00
if (viewer == nullptr)
return false;
2024-03-17 18:01:11 +00:00
toggle(*viewer);
if (mOfflineCollect)
collectStatistics(*viewer);
aa.requestRedraw();
return true;
2022-09-22 18:26:05 +00:00
}
break;
}
case osgGA::GUIEventAdapter::RESIZE:
{
setWindowSize(ea.getWindowWidth(), ea.getWindowHeight());
2022-09-22 18:26:05 +00:00
break;
}
default:
break;
}
return false;
}
void StatsHandler::setWindowSize(int width, int height)
{
if (width <= 0 || height <= 0)
2022-09-22 18:26:05 +00:00
return;
2024-03-17 18:01:11 +00:00
mCamera->setViewport(0, 0, width, height);
if (std::abs(height * statsWidth) <= std::abs(width * statsHeight))
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
mCamera->setProjectionMatrix(
osg::Matrix::ortho2D(statsWidth - width * statsHeight / height, statsWidth, 0.0, statsHeight));
}
else
{
2024-03-17 18:01:11 +00:00
mCamera->setProjectionMatrix(
osg::Matrix::ortho2D(0.0, statsWidth, statsHeight - height * statsWidth / width, statsHeight));
2022-09-22 18:26:05 +00:00
}
}
2024-03-17 18:01:11 +00:00
void StatsHandler::toggle(osgViewer::ViewerBase& viewer)
{
2024-03-17 18:01:11 +00:00
if (!mInitialized)
2022-09-22 18:26:05 +00:00
{
setUpHUDCamera(viewer);
setUpScene(viewer);
2024-03-17 18:01:11 +00:00
mInitialized = true;
}
2024-03-17 18:01:11 +00:00
if (mPage == mSwitch->getNumChildren())
{
2024-03-17 18:01:11 +00:00
mPage = 0;
mCamera->setNodeMask(0);
mSwitch->setAllChildrenOff();
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("resource", false);
}
else
{
2024-03-17 18:01:11 +00:00
mCamera->setNodeMask(0xffffffff);
mSwitch->setSingleChildOn(mPage);
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("resource", true);
++mPage;
2022-09-22 18:26:05 +00:00
}
}
2024-03-17 18:01:11 +00:00
void StatsHandler::setUpHUDCamera(osgViewer::ViewerBase& viewer)
{
// Try GraphicsWindow first so we're likely to get the main viewer window
2024-03-17 18:01:11 +00:00
osg::GraphicsContext* context = dynamic_cast<osgViewer::GraphicsWindow*>(mCamera->getGraphicsContext());
if (!context)
{
osgViewer::Viewer::Windows windows;
2024-03-17 18:01:11 +00:00
viewer.getWindows(windows);
if (!windows.empty())
context = windows.front();
2022-09-22 18:26:05 +00:00
else
{
// No GraphicsWindows were found, so let's try to find a GraphicsContext
2024-03-17 18:01:11 +00:00
context = mCamera->getGraphicsContext();
2022-09-22 18:26:05 +00:00
if (!context)
2022-09-22 18:26:05 +00:00
{
osgViewer::Viewer::Contexts contexts;
2024-03-17 18:01:11 +00:00
viewer.getContexts(contexts);
if (contexts.empty())
return;
context = contexts.front();
2022-09-22 18:26:05 +00:00
}
}
}
2024-03-17 18:01:11 +00:00
mCamera->setGraphicsContext(context);
2024-03-17 18:01:11 +00:00
mCamera->setRenderOrder(osg::Camera::POST_RENDER, 11);
2024-03-17 18:01:11 +00:00
mCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
mCamera->setViewMatrix(osg::Matrix::identity());
setWindowSize(context->getTraits()->width, context->getTraits()->height);
// only clear the depth buffer
2024-03-17 18:01:11 +00:00
mCamera->setClearMask(0);
mCamera->setAllowEventFocus(false);
2024-03-17 18:01:11 +00:00
mCamera->setRenderer(new osgViewer::Renderer(mCamera.get()));
2022-09-22 18:26:05 +00:00
}
2024-03-17 18:01:11 +00:00
namespace
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
class ResourceStatsTextDrawCallback : public osg::Drawable::DrawCallback
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
public:
explicit ResourceStatsTextDrawCallback(osg::Stats* stats, std::span<const std::string> statNames)
: mStats(stats)
, mStatNames(statNames)
{
}
2024-03-17 18:01:11 +00:00
void drawImplementation(osg::RenderInfo& renderInfo, const osg::Drawable* drawable) const override
{
if (mStats == nullptr)
return;
2024-03-17 18:01:11 +00:00
osgText::Text* text = (osgText::Text*)(drawable);
2024-03-17 18:01:11 +00:00
std::ostringstream viewStr;
viewStr.setf(std::ios::left, std::ios::adjustfield);
viewStr.width(14);
// Used fixed formatting, as scientific will switch to "...e+.." notation for
// large numbers of vertices/drawables/etc.
viewStr.setf(std::ios::fixed);
viewStr.precision(0);
2024-03-17 18:01:11 +00:00
const unsigned int frameNumber = renderInfo.getState()->getFrameStamp()->getFrameNumber() - 1;
2022-09-22 18:26:05 +00:00
2024-03-17 18:01:11 +00:00
for (const std::string& statName : mStatNames)
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
if (statName.empty())
viewStr << std::endl;
2022-09-22 18:26:05 +00:00
else
2024-03-17 18:01:11 +00:00
{
double value = 0.0;
if (mStats->getAttribute(frameNumber, statName, value))
viewStr << std::setw(8) << value << std::endl;
else
viewStr << std::setw(8) << "." << std::endl;
}
2022-09-22 18:26:05 +00:00
}
2024-03-17 18:01:11 +00:00
text->setText(viewStr.str());
2024-03-17 18:01:11 +00:00
text->drawImplementation(renderInfo);
}
2024-03-17 18:01:11 +00:00
private:
osg::ref_ptr<osg::Stats> mStats;
std::span<const std::string> mStatNames;
};
}
2024-03-17 18:01:11 +00:00
void StatsHandler::setUpScene(osgViewer::ViewerBase& viewer)
{
2024-03-17 18:01:11 +00:00
const osg::Vec4 backgroundColor(0.0, 0.0, 0.0f, 0.3);
const osg::Vec4 staticTextColor(1.0, 1.0, 0.0f, 1.0);
const osg::Vec4 dynamicTextColor(1.0, 1.0, 1.0f, 1.0);
2023-12-21 23:23:49 +00:00
const auto longest = std::max_element(mStatNames.begin(), mStatNames.end(),
2024-03-17 18:01:11 +00:00
[](const std::string& lhs, const std::string& rhs) { return lhs.size() < rhs.size(); });
const std::size_t longestSize = longest->size();
const float statNamesWidth = longestSize * characterSize * 0.6 + 2 * backgroundMargin;
const float statTextWidth = 7 * characterSize + 2 * backgroundMargin;
const float statHeight = pageSize * characterSize + 2 * backgroundMargin;
const float width = statNamesWidth + backgroundSpacing + statTextWidth;
2023-12-21 23:23:49 +00:00
for (std::size_t offset = 0; offset < mStatNames.size(); offset += pageSize)
{
2024-03-17 18:01:11 +00:00
osg::ref_ptr<osg::Group> group = new osg::Group;
group->setCullingActive(false);
2024-03-17 18:01:11 +00:00
2023-12-21 23:23:49 +00:00
const std::size_t count = std::min(mStatNames.size() - offset, pageSize);
std::span<const std::string> currentStatNames(mStatNames.data() + offset, count);
2024-03-17 18:01:11 +00:00
osg::Vec3 pos(statsWidth - width, statHeight - characterSize, 0.0f);
2022-09-22 18:26:05 +00:00
group->addChild(
2024-03-17 18:01:11 +00:00
createBackgroundRectangle(pos + osg::Vec3(-backgroundMargin, backgroundMargin + characterSize, 0),
statNamesWidth, statHeight, backgroundColor));
2022-09-22 18:26:05 +00:00
osg::ref_ptr<osgText::Text> staticText = new osgText::Text;
group->addChild(staticText.get());
staticText->setColor(staticTextColor);
2024-03-17 18:01:11 +00:00
staticText->setCharacterSize(characterSize);
staticText->setPosition(pos);
2022-09-22 18:26:05 +00:00
std::ostringstream viewStr;
viewStr.clear();
viewStr.setf(std::ios::left, std::ios::adjustfield);
2024-03-17 18:01:11 +00:00
viewStr.width(longestSize);
for (const std::string& statName : currentStatNames)
2019-03-17 16:45:54 +00:00
viewStr << statName << std::endl;
staticText->setText(viewStr.str());
2019-03-17 17:18:53 +00:00
pos.x() += statNamesWidth + backgroundSpacing;
group->addChild(
2024-03-17 18:01:11 +00:00
createBackgroundRectangle(pos + osg::Vec3(-backgroundMargin, backgroundMargin + characterSize, 0),
statTextWidth, statHeight, backgroundColor));
osg::ref_ptr<osgText::Text> statsText = new osgText::Text;
group->addChild(statsText.get());
statsText->setColor(dynamicTextColor);
2024-03-17 18:01:11 +00:00
statsText->setCharacterSize(characterSize);
statsText->setPosition(pos);
statsText->setText("");
2024-03-17 18:01:11 +00:00
statsText->setDrawCallback(new ResourceStatsTextDrawCallback(viewer.getViewerStats(), currentStatNames));
2022-07-17 14:16:18 +00:00
2024-03-17 18:01:11 +00:00
if (mTextFont != nullptr)
2022-07-17 14:16:18 +00:00
{
2024-03-17 18:01:11 +00:00
staticText->setFont(mTextFont);
statsText->setFont(mTextFont);
2022-09-22 18:26:05 +00:00
}
2024-03-17 18:01:11 +00:00
mSwitch->addChild(group, false);
2022-07-17 14:16:18 +00:00
}
}
void StatsHandler::getUsage(osg::ApplicationUsage& usage) const
2022-09-22 18:26:05 +00:00
{
2024-03-17 18:01:11 +00:00
usage.addKeyboardMouseBinding(statsHandlerKey, "On screen resource usage stats.");
2022-09-22 18:26:05 +00:00
}
2024-03-17 18:01:11 +00:00
void collectStatistics(osgViewer::ViewerBase& viewer)
{
osgViewer::Viewer::Cameras cameras;
2024-03-17 18:01:11 +00:00
viewer.getCameras(cameras);
for (auto* camera : cameras)
2022-09-22 18:26:05 +00:00
{
if (collectStatGPU)
camera->getStats()->collectStats("gpu", true);
if (collectStatRendering)
camera->getStats()->collectStats("rendering", true);
if (collectStatCameraObjects)
camera->getStats()->collectStats("scene", true);
2022-09-22 18:26:05 +00:00
}
if (collectStatEvent)
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("event", true);
if (collectStatFrameRate)
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("frame_rate", true);
if (collectStatUpdate)
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("update", true);
if (collectStatResource)
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("resource", true);
if (collectStatViewerObjects)
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("scene", true);
if (collectStatEngine)
2024-03-17 18:01:11 +00:00
viewer.getViewerStats()->collectStats("engine", true);
}
}