mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-09 08:30:35 +02:00
Working on overlays and stats
This commit is contained in:
parent
4762e1a00c
commit
2bf53b625e
6 changed files with 306 additions and 251 deletions
|
@ -11,19 +11,22 @@ Hifi.Stats {
|
||||||
readonly property int sTATS_PING_MIN_WIDTH: 190
|
readonly property int sTATS_PING_MIN_WIDTH: 190
|
||||||
readonly property int sTATS_GEO_MIN_WIDTH: 240
|
readonly property int sTATS_GEO_MIN_WIDTH: 240
|
||||||
readonly property int sTATS_OCTREE_MIN_WIDTH: 410
|
readonly property int sTATS_OCTREE_MIN_WIDTH: 410
|
||||||
|
readonly property int fontSize: 12
|
||||||
|
readonly property string fontColor: "white"
|
||||||
|
readonly property string bgColor: "#99333333"
|
||||||
|
|
||||||
onParentChanged: {
|
onParentChanged: {
|
||||||
root.x = parent.width - root.width;
|
root.x = parent.width - root.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
z: 100
|
|
||||||
id: row
|
id: row
|
||||||
spacing: 8
|
spacing: 8
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: generalCol.width + 8;
|
width: generalCol.width + 8;
|
||||||
height: generalCol.height + 8;
|
height: generalCol.height + 8;
|
||||||
color: "#99333333";
|
color: root.bgColor;
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -34,18 +37,38 @@ Hifi.Stats {
|
||||||
id: generalCol
|
id: generalCol
|
||||||
spacing: 4; x: 4; y: 4;
|
spacing: 4; x: 4; y: 4;
|
||||||
width: sTATS_GENERAL_MIN_WIDTH
|
width: sTATS_GENERAL_MIN_WIDTH
|
||||||
Text { color: "white"; text: "Servers: " + root.serverCount }
|
Text {
|
||||||
Text { color: "white"; text: "Avatars: " + root.avatarCount }
|
color: root.fontColor;
|
||||||
Text { color: "white"; text: "Framerate: " + root.framerate }
|
font.pixelSize: root.fontSize
|
||||||
Text { color: "white"; text: "Packets In/Out: " + root.packetInCount + "/" + root.packetOutCount }
|
text: "Servers: " + root.serverCount
|
||||||
Text { color: "white"; text: "Mbps In/Out: " + root.mbpsIn.toFixed(2) + "/" + root.mbpsOut.toFixed(2) }
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
text: "Avatars: " + root.avatarCount
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
text: "Framerate: " + root.framerate
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
text: "Packets In/Out: " + root.packetInCount + "/" + root.packetOutCount
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
text: "Mbps In/Out: " + root.mbpsIn.toFixed(2) + "/" + root.mbpsOut.toFixed(2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: pingCol.width + 8
|
width: pingCol.width + 8
|
||||||
height: pingCol.height + 8
|
height: pingCol.height + 8
|
||||||
color: "#99333333"
|
color: root.bgColor;
|
||||||
visible: root.audioPing != -2
|
visible: root.audioPing != -2
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -55,45 +78,68 @@ Hifi.Stats {
|
||||||
id: pingCol
|
id: pingCol
|
||||||
spacing: 4; x: 4; y: 4;
|
spacing: 4; x: 4; y: 4;
|
||||||
width: sTATS_PING_MIN_WIDTH
|
width: sTATS_PING_MIN_WIDTH
|
||||||
Text { color: "white"; text: "Audio ping: " + root.audioPing }
|
Text {
|
||||||
Text { color: "white"; text: "Avatar ping: " + root.avatarPing }
|
color: root.fontColor
|
||||||
Text { color: "white"; text: "Entities avg ping: " + root.entitiesPing }
|
font.pixelSize: root.fontSize
|
||||||
Text { color: "white"; text: "Voxel max ping: " + 0; visible: root.expanded; }
|
text: "Audio ping: " + root.audioPing
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
text: "Avatar ping: " + root.avatarPing
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
text: "Entities avg ping: " + root.entitiesPing
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded;
|
||||||
|
text: "Voxel max ping: " + 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: geoCol.width
|
width: geoCol.width + 8
|
||||||
height: geoCol.height
|
height: geoCol.height + 8
|
||||||
color: "#99333333"
|
color: root.bgColor;
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: { root.expanded = !root.expanded; }
|
onClicked: { root.expanded = !root.expanded; }
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
spacing: 4
|
|
||||||
id: geoCol
|
id: geoCol
|
||||||
|
spacing: 4; x: 4; y: 4;
|
||||||
width: sTATS_GEO_MIN_WIDTH
|
width: sTATS_GEO_MIN_WIDTH
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
text: "Position: " + root.position.x.toFixed(1) + ", " +
|
text: "Position: " + root.position.x.toFixed(1) + ", " +
|
||||||
root.position.y.toFixed(1) + ", " + root.position.z.toFixed(1)
|
root.position.y.toFixed(1) + ", " + root.position.z.toFixed(1)
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
text: "Velocity: " + root.velocity.toFixed(1)
|
text: "Velocity: " + root.velocity.toFixed(1)
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
text: "Yaw: " + root.yaw.toFixed(1)
|
text: "Yaw: " + root.yaw.toFixed(1)
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
visible: root.expanded;
|
visible: root.expanded;
|
||||||
text: "Avatar Mixer: " + root.avatarMixerKbps + " kbps, " +
|
text: "Avatar Mixer: " + root.avatarMixerKbps + " kbps, " +
|
||||||
root.avatarMixerPps + "pps";
|
root.avatarMixerPps + "pps";
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
visible: root.expanded;
|
visible: root.expanded;
|
||||||
text: "Downloads: ";
|
text: "Downloads: ";
|
||||||
}
|
}
|
||||||
|
@ -102,7 +148,7 @@ Hifi.Stats {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: octreeCol.width + 8
|
width: octreeCol.width + 8
|
||||||
height: octreeCol.height + 8
|
height: octreeCol.height + 8
|
||||||
color: "#99333333"
|
color: root.bgColor;
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: { root.expanded = !root.expanded; }
|
onClicked: { root.expanded = !root.expanded; }
|
||||||
|
@ -112,31 +158,93 @@ Hifi.Stats {
|
||||||
spacing: 4; x: 4; y: 4;
|
spacing: 4; x: 4; y: 4;
|
||||||
width: sTATS_OCTREE_MIN_WIDTH
|
width: sTATS_OCTREE_MIN_WIDTH
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
text: "Triangles: " + root.triangles +
|
text: "Triangles: " + root.triangles +
|
||||||
" / Quads: " + root.quads + " / Material Switches: " + root.materialSwitches
|
" / Quads: " + root.quads + " / Material Switches: " + root.materialSwitches
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
visible: root.expanded;
|
visible: root.expanded;
|
||||||
text: "\tMesh Parts Rendered Opaque: " + root.meshOpaque +
|
text: "\tMesh Parts Rendered Opaque: " + root.meshOpaque +
|
||||||
" / Translucent: " + root.meshTranslucent;
|
" / Translucent: " + root.meshTranslucent;
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
visible: root.expanded;
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded;
|
||||||
text: "\tOpaque considered: " + root.opaqueConsidered +
|
text: "\tOpaque considered: " + root.opaqueConsidered +
|
||||||
" / Out of view: " + root.opaqueOutOfView + " / Too small: " + root.opaqueTooSmall;
|
" / Out of view: " + root.opaqueOutOfView + " / Too small: " + root.opaqueTooSmall;
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
color: "white";
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
visible: !root.expanded
|
visible: !root.expanded
|
||||||
text: "Octree Elements Server: ";
|
text: "Octree Elements Server: " + root.serverElements +
|
||||||
|
" Local: " + root.localElements;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded
|
||||||
|
text: "Octree Sending Mode: " + root.sendingMode;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded
|
||||||
|
text: "Octree Packets to Process: " + root.packetStats;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded
|
||||||
|
text: "Octree Elements - ";
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded
|
||||||
|
text: "\tServer: " + root.serverElements +
|
||||||
|
" Internal: " + root.serverInternal +
|
||||||
|
" Leaves: " + root.serverLeaves;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded
|
||||||
|
text: "\tLocal: " + root.localElements +
|
||||||
|
" Internal: " + root.localInternal +
|
||||||
|
" Leaves: " + root.localLeaves;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
color: root.fontColor;
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
visible: root.expanded
|
||||||
|
text: "LOD: " + root.lodStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
y: 250
|
||||||
|
width: perfText.width + 8
|
||||||
|
height: perfText.height + 8
|
||||||
|
color: root.bgColor;
|
||||||
|
anchors.top: row.bottom + 16
|
||||||
|
Text {
|
||||||
|
x: 4; y: 4
|
||||||
|
id: perfText
|
||||||
|
color: root.fontColor
|
||||||
|
font.family: "Lucida Console"
|
||||||
|
text: "-------------------------------------------------------- Function " +
|
||||||
|
"------------------------------------------------------- --msecs- -calls--\n" +
|
||||||
|
root.timingStats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.parent
|
target: root.parent
|
||||||
onWidthChanged: {
|
onWidthChanged: {
|
||||||
|
|
|
@ -1817,6 +1817,7 @@ void Application::idle() {
|
||||||
targetFramePeriod = 1000.0 / targetFramerate;
|
targetFramePeriod = 1000.0 / targetFramerate;
|
||||||
}
|
}
|
||||||
double timeSinceLastUpdate = (double)_lastTimeUpdated.nsecsElapsed() / 1000000.0;
|
double timeSinceLastUpdate = (double)_lastTimeUpdated.nsecsElapsed() / 1000000.0;
|
||||||
|
//if (true) {
|
||||||
if (timeSinceLastUpdate > targetFramePeriod) {
|
if (timeSinceLastUpdate > targetFramePeriod) {
|
||||||
_lastTimeUpdated.start();
|
_lastTimeUpdated.start();
|
||||||
{
|
{
|
||||||
|
@ -1843,7 +1844,7 @@ void Application::idle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// After finishing all of the above work, restart the idle timer, allowing 2ms to process events.
|
// After finishing all of the above work, restart the idle timer, allowing 2ms to process events.
|
||||||
idleTimer->start(2);
|
idleTimer->start(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,6 @@ ApplicationOverlay::~ApplicationOverlay() {
|
||||||
void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) {
|
void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) {
|
||||||
PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "ApplicationOverlay::displayOverlay()");
|
PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "ApplicationOverlay::displayOverlay()");
|
||||||
|
|
||||||
Stats::getInstance()->updateStats();
|
|
||||||
glm::vec2 size = qApp->getCanvasSize();
|
glm::vec2 size = qApp->getCanvasSize();
|
||||||
// TODO Handle fading and deactivation/activation of UI
|
// TODO Handle fading and deactivation/activation of UI
|
||||||
|
|
||||||
|
@ -104,8 +103,7 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) {
|
||||||
//renderOverlays(renderArgs);
|
//renderOverlays(renderArgs);
|
||||||
//renderAudioMeter(renderArgs);
|
//renderAudioMeter(renderArgs);
|
||||||
//renderCameraToggle(renderArgs);
|
//renderCameraToggle(renderArgs);
|
||||||
//renderStatsAndLogs(renderArgs);
|
renderStatsAndLogs(renderArgs);
|
||||||
|
|
||||||
|
|
||||||
renderDomainConnectionStatusBorder(renderArgs);
|
renderDomainConnectionStatusBorder(renderArgs);
|
||||||
renderQmlUi(renderArgs);
|
renderQmlUi(renderArgs);
|
||||||
|
@ -370,14 +368,9 @@ void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) {
|
||||||
// Display stats and log text onscreen
|
// Display stats and log text onscreen
|
||||||
|
|
||||||
// Determine whether to compute timing details
|
// Determine whether to compute timing details
|
||||||
bool shouldDisplayTimingDetail = Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) &&
|
|
||||||
Menu::getInstance()->isOptionChecked(MenuOption::Stats); //&&
|
|
||||||
// Stats::getInstance()->isExpanded();
|
|
||||||
if (shouldDisplayTimingDetail != PerformanceTimer::isActive()) {
|
|
||||||
PerformanceTimer::setActive(shouldDisplayTimingDetail);
|
|
||||||
}
|
|
||||||
Stats::getInstance()->updateStats();
|
Stats::getInstance()->updateStats();
|
||||||
|
|
||||||
|
/*
|
||||||
// Show on-screen msec timer
|
// Show on-screen msec timer
|
||||||
if (Menu::getInstance()->isOptionChecked(MenuOption::FrameTimer)) {
|
if (Menu::getInstance()->isOptionChecked(MenuOption::FrameTimer)) {
|
||||||
auto canvasSize = qApp->getCanvasSize();
|
auto canvasSize = qApp->getCanvasSize();
|
||||||
|
@ -401,6 +394,7 @@ void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) {
|
||||||
glEnable(GL_LIGHTING);
|
glEnable(GL_LIGHTING);
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE);
|
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApplicationOverlay::renderDomainConnectionStatusBorder(RenderArgs* renderArgs) {
|
void ApplicationOverlay::renderDomainConnectionStatusBorder(RenderArgs* renderArgs) {
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
|
|
||||||
#include "Stats.h"
|
#include "Stats.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtx/component_wise.hpp>
|
#include <glm/gtx/component_wise.hpp>
|
||||||
#include <glm/gtx/quaternion.hpp>
|
#include <glm/gtx/quaternion.hpp>
|
||||||
|
@ -41,7 +43,6 @@ Stats* Stats::getInstance() {
|
||||||
if (!INSTANCE) {
|
if (!INSTANCE) {
|
||||||
Stats::registerType();
|
Stats::registerType();
|
||||||
Stats::show();
|
Stats::show();
|
||||||
//Stats::toggle();
|
|
||||||
Q_ASSERT(INSTANCE);
|
Q_ASSERT(INSTANCE);
|
||||||
}
|
}
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
|
@ -104,6 +105,13 @@ void Stats::updateStats() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool shouldDisplayTimingDetail = Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) &&
|
||||||
|
Menu::getInstance()->isOptionChecked(MenuOption::Stats) && isExpanded();
|
||||||
|
if (shouldDisplayTimingDetail != PerformanceTimer::isActive()) {
|
||||||
|
PerformanceTimer::setActive(shouldDisplayTimingDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
auto nodeList = DependencyManager::get<NodeList>();
|
auto nodeList = DependencyManager::get<NodeList>();
|
||||||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||||
// we need to take one avatar out so we don't include ourselves
|
// we need to take one avatar out so we don't include ourselves
|
||||||
|
@ -144,7 +152,7 @@ void Stats::updateStats() {
|
||||||
pingVoxel = totalPingOctree / octreeServerCount;
|
pingVoxel = totalPingOctree / octreeServerCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
STAT_UPDATE(entitiesPing, pingVoxel);
|
//STAT_UPDATE(entitiesPing, pingVoxel);
|
||||||
//if (_expanded) {
|
//if (_expanded) {
|
||||||
// QString voxelMaxPing;
|
// QString voxelMaxPing;
|
||||||
// if (pingVoxel >= 0) { // Average is only meaningful if pingVoxel is valid.
|
// if (pingVoxel >= 0) { // Average is only meaningful if pingVoxel is valid.
|
||||||
|
@ -202,22 +210,137 @@ void Stats::updateStats() {
|
||||||
} // expanded avatar column
|
} // expanded avatar column
|
||||||
|
|
||||||
// Fourth column, octree stats
|
// Fourth column, octree stats
|
||||||
|
int serverCount = 0;
|
||||||
|
int movingServerCount = 0;
|
||||||
|
unsigned long totalNodes = 0;
|
||||||
|
unsigned long totalInternal = 0;
|
||||||
|
unsigned long totalLeaves = 0;
|
||||||
|
std::stringstream sendingModeStream("");
|
||||||
|
sendingModeStream << "[";
|
||||||
|
NodeToOctreeSceneStats* octreeServerSceneStats = Application::getInstance()->getOcteeSceneStats();
|
||||||
|
for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) {
|
||||||
|
//const QUuid& uuid = i->first;
|
||||||
|
OctreeSceneStats& stats = i->second;
|
||||||
|
serverCount++;
|
||||||
|
if (_expanded) {
|
||||||
|
if (serverCount > 1) {
|
||||||
|
sendingModeStream << ",";
|
||||||
|
}
|
||||||
|
if (stats.isMoving()) {
|
||||||
|
sendingModeStream << "M";
|
||||||
|
movingServerCount++;
|
||||||
|
} else {
|
||||||
|
sendingModeStream << "S";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate server node totals
|
||||||
|
totalNodes += stats.getTotalElements();
|
||||||
|
if (_expanded) {
|
||||||
|
totalInternal += stats.getTotalInternal();
|
||||||
|
totalLeaves += stats.getTotalLeaves();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_expanded) {
|
||||||
|
if (serverCount == 0) {
|
||||||
|
sendingModeStream << "---";
|
||||||
|
}
|
||||||
|
sendingModeStream << "] " << serverCount << " servers";
|
||||||
|
if (movingServerCount > 0) {
|
||||||
|
sendingModeStream << " <SCENE NOT STABLE>";
|
||||||
|
} else {
|
||||||
|
sendingModeStream << " <SCENE STABLE>";
|
||||||
|
}
|
||||||
|
QString sendingModeResult = sendingModeStream.str().c_str();
|
||||||
|
STAT_UPDATE(sendingMode, sendingModeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incoming packets
|
||||||
|
QLocale locale(QLocale::English);
|
||||||
|
auto voxelPacketsToProcess = qApp->getOctreePacketProcessor().packetsToProcessCount();
|
||||||
|
if (_expanded) {
|
||||||
|
std::stringstream octreeStats;
|
||||||
|
QString packetsString = locale.toString((int)voxelPacketsToProcess);
|
||||||
|
QString maxString = locale.toString((int)_recentMaxPackets);
|
||||||
|
octreeStats << "Octree Packets to Process: " << qPrintable(packetsString)
|
||||||
|
<< " [Recent Max: " << qPrintable(maxString) << "]";
|
||||||
|
QString str = octreeStats.str().c_str();
|
||||||
|
STAT_UPDATE(packetStats, str);
|
||||||
|
// drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_resetRecentMaxPacketsSoon && voxelPacketsToProcess > 0) {
|
||||||
|
_recentMaxPackets = 0;
|
||||||
|
_resetRecentMaxPacketsSoon = false;
|
||||||
|
}
|
||||||
|
if (voxelPacketsToProcess == 0) {
|
||||||
|
_resetRecentMaxPacketsSoon = true;
|
||||||
|
} else if (voxelPacketsToProcess > _recentMaxPackets) {
|
||||||
|
_recentMaxPackets = voxelPacketsToProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server Octree Elements
|
||||||
|
STAT_UPDATE(serverElements, totalNodes);
|
||||||
|
STAT_UPDATE(localElements, OctreeElement::getNodeCount());
|
||||||
|
|
||||||
|
if (_expanded) {
|
||||||
|
STAT_UPDATE(serverInternal, totalInternal);
|
||||||
|
STAT_UPDATE(serverLeaves, totalLeaves);
|
||||||
|
// Local Voxels
|
||||||
|
STAT_UPDATE(localInternal, OctreeElement::getInternalNodeCount());
|
||||||
|
STAT_UPDATE(localLeaves, OctreeElement::getLeafNodeCount());
|
||||||
|
// LOD Details
|
||||||
|
STAT_UPDATE(lodStatus, "You can see " + DependencyManager::get<LODManager>()->getLODFeedbackText());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool performanceTimerIsActive = PerformanceTimer::isActive();
|
||||||
|
bool displayPerf = _expanded && Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails);
|
||||||
|
if (displayPerf && performanceTimerIsActive) {
|
||||||
|
PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up
|
||||||
|
|
||||||
|
// we will also include room for 1 line per timing record and a header of 4 lines
|
||||||
|
// Timing details...
|
||||||
|
|
||||||
|
// First iterate all the records, and for the ones that should be included, insert them into
|
||||||
|
// a new Map sorted by average time...
|
||||||
|
bool onlyDisplayTopTen = Menu::getInstance()->isOptionChecked(MenuOption::OnlyDisplayTopTen);
|
||||||
|
QMap<float, QString> sortedRecords;
|
||||||
|
const QMap<QString, PerformanceTimerRecord>& allRecords = PerformanceTimer::getAllTimerRecords();
|
||||||
|
QMapIterator<QString, PerformanceTimerRecord> i(allRecords);
|
||||||
|
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
if (includeTimingRecord(i.key())) {
|
||||||
|
float averageTime = (float)i.value().getMovingAverage() / (float)USECS_PER_MSEC;
|
||||||
|
sortedRecords.insertMulti(averageTime, i.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int linesDisplayed = 0;
|
||||||
|
QMapIterator<float, QString> j(sortedRecords);
|
||||||
|
j.toBack();
|
||||||
|
QString perfLines;
|
||||||
|
while (j.hasPrevious()) {
|
||||||
|
j.previous();
|
||||||
|
static const QChar noBreakingSpace = QChar::Nbsp;
|
||||||
|
QString functionName = j.value();
|
||||||
|
const PerformanceTimerRecord& record = allRecords.value(functionName);
|
||||||
|
perfLines += QString("%1: %2 [%3]\n").
|
||||||
|
arg(QString(qPrintable(functionName)), 120, noBreakingSpace).
|
||||||
|
arg((float)record.getMovingAverage() / (float)USECS_PER_MSEC, 8, 'f', 3, noBreakingSpace).
|
||||||
|
arg((int)record.getCount(), 6, 10, noBreakingSpace);
|
||||||
|
linesDisplayed++;
|
||||||
|
if (onlyDisplayTopTen && linesDisplayed == 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_timingStats = perfLines;
|
||||||
|
emit timingStatsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Stats::setRenderDetails(const RenderDetails& details) {
|
void Stats::setRenderDetails(const RenderDetails& details) {
|
||||||
//STATS_PROPERTY(int, triangles, 0)
|
|
||||||
//STATS_PROPERTY(int, quads, 0)
|
|
||||||
//STATS_PROPERTY(int, materialSwitches, 0)
|
|
||||||
//STATS_PROPERTY(int, meshOpaque, 0)
|
|
||||||
//STATS_PROPERTY(int, meshTranslucent, 0)
|
|
||||||
//STATS_PROPERTY(int, opaqueConsidered, 0)
|
|
||||||
//STATS_PROPERTY(int, opaqueOutOfView, 0)
|
|
||||||
//STATS_PROPERTY(int, opaqueTooSmall, 0)
|
|
||||||
//STATS_PROPERTY(int, translucentConsidered, 0)
|
|
||||||
//STATS_PROPERTY(int, translucentOutOfView, 0)
|
|
||||||
//STATS_PROPERTY(int, translucentTooSmall, 0)
|
|
||||||
//STATS_PROPERTY(int, octreeElementsServer, 0)
|
|
||||||
//STATS_PROPERTY(int, octreeElementsLocal, 0)
|
|
||||||
STAT_UPDATE(triangles, details._trianglesRendered);
|
STAT_UPDATE(triangles, details._trianglesRendered);
|
||||||
STAT_UPDATE(quads, details._quadsRendered);
|
STAT_UPDATE(quads, details._quadsRendered);
|
||||||
STAT_UPDATE(materialSwitches, details._materialSwitches);
|
STAT_UPDATE(materialSwitches, details._materialSwitches);
|
||||||
|
@ -240,200 +363,8 @@ void Stats::display(
|
||||||
int voxelPacketsToProcess)
|
int voxelPacketsToProcess)
|
||||||
{
|
{
|
||||||
// iterate all the current voxel stats, and list their sending modes, and total voxel counts
|
// iterate all the current voxel stats, and list their sending modes, and total voxel counts
|
||||||
std::stringstream sendingMode("");
|
|
||||||
sendingMode << "Octree Sending Mode: [";
|
|
||||||
int serverCount = 0;
|
|
||||||
int movingServerCount = 0;
|
|
||||||
unsigned long totalNodes = 0;
|
|
||||||
unsigned long totalInternal = 0;
|
|
||||||
unsigned long totalLeaves = 0;
|
|
||||||
NodeToOctreeSceneStats* octreeServerSceneStats = Application::getInstance()->getOcteeSceneStats();
|
|
||||||
for(NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) {
|
|
||||||
//const QUuid& uuid = i->first;
|
|
||||||
OctreeSceneStats& stats = i->second;
|
|
||||||
serverCount++;
|
|
||||||
if (_expanded) {
|
|
||||||
if (serverCount > 1) {
|
|
||||||
sendingMode << ",";
|
|
||||||
}
|
|
||||||
if (stats.isMoving()) {
|
|
||||||
sendingMode << "M";
|
|
||||||
movingServerCount++;
|
|
||||||
} else {
|
|
||||||
sendingMode << "S";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate server node totals
|
|
||||||
totalNodes += stats.getTotalElements();
|
|
||||||
if (_expanded) {
|
|
||||||
totalInternal += stats.getTotalInternal();
|
|
||||||
totalLeaves += stats.getTotalLeaves();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_expanded) {
|
|
||||||
if (serverCount == 0) {
|
|
||||||
sendingMode << "---";
|
|
||||||
}
|
|
||||||
sendingMode << "] " << serverCount << " servers";
|
|
||||||
if (movingServerCount > 0) {
|
|
||||||
sendingMode << " <SCENE NOT STABLE>";
|
|
||||||
} else {
|
|
||||||
sendingMode << " <SCENE STABLE>";
|
|
||||||
}
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)sendingMode.str().c_str(), color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Incoming packets
|
|
||||||
if (_expanded) {
|
|
||||||
octreeStats.str("");
|
|
||||||
QString packetsString = locale.toString((int)voxelPacketsToProcess);
|
|
||||||
QString maxString = locale.toString((int)_recentMaxPackets);
|
|
||||||
octreeStats << "Octree Packets to Process: " << qPrintable(packetsString)
|
|
||||||
<< " [Recent Max: " << qPrintable(maxString) << "]";
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_resetRecentMaxPacketsSoon && voxelPacketsToProcess > 0) {
|
|
||||||
_recentMaxPackets = 0;
|
|
||||||
_resetRecentMaxPacketsSoon = false;
|
|
||||||
}
|
|
||||||
if (voxelPacketsToProcess == 0) {
|
|
||||||
_resetRecentMaxPacketsSoon = true;
|
|
||||||
} else {
|
|
||||||
if (voxelPacketsToProcess > _recentMaxPackets) {
|
|
||||||
_recentMaxPackets = voxelPacketsToProcess;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString serversTotalString = locale.toString((uint)totalNodes); // consider adding: .rightJustified(10, ' ');
|
|
||||||
unsigned long localTotal = OctreeElement::getNodeCount();
|
|
||||||
QString localTotalString = locale.toString((uint)localTotal); // consider adding: .rightJustified(10, ' ');
|
|
||||||
|
|
||||||
// Server Octree Elements
|
|
||||||
if (!_expanded) {
|
|
||||||
octreeStats.str("");
|
|
||||||
octreeStats << "Octree Elements Server: " << qPrintable(serversTotalString)
|
|
||||||
<< " Local:" << qPrintable(localTotalString);
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_expanded) {
|
|
||||||
octreeStats.str("");
|
|
||||||
octreeStats << "Octree Elements -";
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color);
|
|
||||||
|
|
||||||
QString serversInternalString = locale.toString((uint)totalInternal);
|
|
||||||
QString serversLeavesString = locale.toString((uint)totalLeaves);
|
|
||||||
|
|
||||||
octreeStats.str("");
|
|
||||||
octreeStats << " Server: " << qPrintable(serversTotalString) <<
|
|
||||||
" Internal: " << qPrintable(serversInternalString) <<
|
|
||||||
" Leaves: " << qPrintable(serversLeavesString);
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color);
|
|
||||||
|
|
||||||
// Local Voxels
|
|
||||||
unsigned long localInternal = OctreeElement::getInternalNodeCount();
|
|
||||||
unsigned long localLeaves = OctreeElement::getLeafNodeCount();
|
|
||||||
QString localInternalString = locale.toString((uint)localInternal);
|
|
||||||
QString localLeavesString = locale.toString((uint)localLeaves);
|
|
||||||
|
|
||||||
octreeStats.str("");
|
|
||||||
octreeStats << " Local: " << qPrintable(serversTotalString) <<
|
|
||||||
" Internal: " << qPrintable(localInternalString) <<
|
|
||||||
" Leaves: " << qPrintable(localLeavesString) << "";
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOD Details
|
|
||||||
octreeStats.str("");
|
|
||||||
QString displayLODDetails = DependencyManager::get<LODManager>()->getLODFeedbackText();
|
|
||||||
octreeStats << "LOD: You can see " << qPrintable(displayLODDetails.trimmed());
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(horizontalOffset, verticalOffset, scale, rotation, font, (char*)octreeStats.str().c_str(), color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//// Performance timer
|
|
||||||
|
|
||||||
|
|
||||||
bool performanceTimerIsActive = PerformanceTimer::isActive();
|
|
||||||
bool displayPerf = _expanded && Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails);
|
|
||||||
if (displayPerf && performanceTimerIsActive) {
|
|
||||||
PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up
|
|
||||||
columnOneWidth = _generalStatsWidth + _pingStatsWidth + _geoStatsWidth; // 3 columns wide...
|
|
||||||
// we will also include room for 1 line per timing record and a header of 4 lines
|
|
||||||
lines += 4;
|
|
||||||
|
|
||||||
const QMap<QString, PerformanceTimerRecord>& allRecords = PerformanceTimer::getAllTimerRecords();
|
|
||||||
QMapIterator<QString, PerformanceTimerRecord> i(allRecords);
|
|
||||||
bool onlyDisplayTopTen = Menu::getInstance()->isOptionChecked(MenuOption::OnlyDisplayTopTen);
|
|
||||||
int statsLines = 0;
|
|
||||||
while (i.hasNext()) {
|
|
||||||
i.next();
|
|
||||||
if (includeTimingRecord(i.key())) {
|
|
||||||
lines++;
|
|
||||||
statsLines++;
|
|
||||||
if (onlyDisplayTopTen && statsLines == 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: the display of these timing details should all be moved to JavaScript
|
|
||||||
if (displayPerf && performanceTimerIsActive) {
|
|
||||||
bool onlyDisplayTopTen = Menu::getInstance()->isOptionChecked(MenuOption::OnlyDisplayTopTen);
|
|
||||||
// Timing details...
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE * 4; // skip 3 lines to be under the other columns
|
|
||||||
drawText(columnOneHorizontalOffset, verticalOffset, scale, rotation, font,
|
|
||||||
"-------------------------------------------------------- Function "
|
|
||||||
"------------------------------------------------------- --msecs- -calls--", color);
|
|
||||||
|
|
||||||
// First iterate all the records, and for the ones that should be included, insert them into
|
|
||||||
// a new Map sorted by average time...
|
|
||||||
QMap<float, QString> sortedRecords;
|
|
||||||
const QMap<QString, PerformanceTimerRecord>& allRecords = PerformanceTimer::getAllTimerRecords();
|
|
||||||
QMapIterator<QString, PerformanceTimerRecord> i(allRecords);
|
|
||||||
|
|
||||||
while (i.hasNext()) {
|
|
||||||
i.next();
|
|
||||||
if (includeTimingRecord(i.key())) {
|
|
||||||
float averageTime = (float)i.value().getMovingAverage() / (float)USECS_PER_MSEC;
|
|
||||||
sortedRecords.insertMulti(averageTime, i.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int linesDisplayed = 0;
|
|
||||||
QMapIterator<float, QString> j(sortedRecords);
|
|
||||||
j.toBack();
|
|
||||||
while (j.hasPrevious()) {
|
|
||||||
j.previous();
|
|
||||||
QChar noBreakingSpace = QChar::Nbsp;
|
|
||||||
QString functionName = j.value();
|
|
||||||
const PerformanceTimerRecord& record = allRecords.value(functionName);
|
|
||||||
|
|
||||||
QString perfLine = QString("%1: %2 [%3]").
|
|
||||||
arg(QString(qPrintable(functionName)), 120, noBreakingSpace).
|
|
||||||
arg((float)record.getMovingAverage() / (float)USECS_PER_MSEC, 8, 'f', 3, noBreakingSpace).
|
|
||||||
arg((int)record.getCount(), 6, 10, noBreakingSpace);
|
|
||||||
|
|
||||||
verticalOffset += STATS_PELS_PER_LINE;
|
|
||||||
drawText(columnOneHorizontalOffset, verticalOffset, scale, rotation, font, perfLine.toUtf8().constData(), color);
|
|
||||||
linesDisplayed++;
|
|
||||||
if (onlyDisplayTopTen && linesDisplayed == 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
|
@ -30,7 +30,7 @@ private: \
|
||||||
class Stats : public QQuickItem {
|
class Stats : public QQuickItem {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
HIFI_QML_DECL
|
HIFI_QML_DECL
|
||||||
Q_PROPERTY(bool expanded READ isExpanded NOTIFY expandedChanged)
|
Q_PROPERTY(bool expanded READ isExpanded WRITE setExpanded NOTIFY expandedChanged)
|
||||||
STATS_PROPERTY(int, serverCount, 0)
|
STATS_PROPERTY(int, serverCount, 0)
|
||||||
STATS_PROPERTY(int, framerate, 0)
|
STATS_PROPERTY(int, framerate, 0)
|
||||||
STATS_PROPERTY(int, avatarCount, 0)
|
STATS_PROPERTY(int, avatarCount, 0)
|
||||||
|
@ -61,8 +61,16 @@ class Stats : public QQuickItem {
|
||||||
STATS_PROPERTY(int, translucentConsidered, 0)
|
STATS_PROPERTY(int, translucentConsidered, 0)
|
||||||
STATS_PROPERTY(int, translucentOutOfView, 0)
|
STATS_PROPERTY(int, translucentOutOfView, 0)
|
||||||
STATS_PROPERTY(int, translucentTooSmall, 0)
|
STATS_PROPERTY(int, translucentTooSmall, 0)
|
||||||
STATS_PROPERTY(int, octreeElementsServer, 0)
|
STATS_PROPERTY(QString, sendingMode, QString())
|
||||||
STATS_PROPERTY(int, octreeElementsLocal, 0)
|
STATS_PROPERTY(QString, packetStats, QString())
|
||||||
|
STATS_PROPERTY(QString, lodStatus, QString())
|
||||||
|
STATS_PROPERTY(QString, timingStats, QString())
|
||||||
|
STATS_PROPERTY(int, serverElements, 0)
|
||||||
|
STATS_PROPERTY(int, serverInternal, 0)
|
||||||
|
STATS_PROPERTY(int, serverLeaves, 0)
|
||||||
|
STATS_PROPERTY(int, localElements, 0)
|
||||||
|
STATS_PROPERTY(int, localInternal, 0)
|
||||||
|
STATS_PROPERTY(int, localLeaves, 0)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static Stats* getInstance();
|
static Stats* getInstance();
|
||||||
|
@ -76,8 +84,9 @@ public:
|
||||||
bool isExpanded() { return _expanded; }
|
bool isExpanded() { return _expanded; }
|
||||||
|
|
||||||
void setExpanded(bool expanded) {
|
void setExpanded(bool expanded) {
|
||||||
if (expanded != _expanded) {
|
if (_expanded != expanded) {
|
||||||
_expanded = expanded;
|
_expanded = expanded;
|
||||||
|
emit expandedChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,8 +122,16 @@ signals:
|
||||||
void translucentConsideredChanged();
|
void translucentConsideredChanged();
|
||||||
void translucentOutOfViewChanged();
|
void translucentOutOfViewChanged();
|
||||||
void translucentTooSmallChanged();
|
void translucentTooSmallChanged();
|
||||||
void octreeElementsServerChanged();
|
void sendingModeChanged();
|
||||||
void octreeElementsLocalChanged();
|
void packetStatsChanged();
|
||||||
|
void lodStatusChanged();
|
||||||
|
void serverElementsChanged();
|
||||||
|
void serverInternalChanged();
|
||||||
|
void serverLeavesChanged();
|
||||||
|
void localElementsChanged();
|
||||||
|
void localInternalChanged();
|
||||||
|
void localLeavesChanged();
|
||||||
|
void timingStatsChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int _recentMaxPackets{ 0 } ; // recent max incoming voxel packets to process
|
int _recentMaxPackets{ 0 } ; // recent max incoming voxel packets to process
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
#include <QOpenGLDebugLogger>
|
#include <QOpenGLDebugLogger>
|
||||||
#include <QGLWidget>
|
#include <QGLWidget>
|
||||||
#include <QtQml>
|
#include <QtQml>
|
||||||
|
|
||||||
|
#include <PerfStat.h>
|
||||||
|
|
||||||
#include "AbstractViewStateInterface.h"
|
#include "AbstractViewStateInterface.h"
|
||||||
|
|
||||||
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
|
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
|
||||||
|
@ -212,6 +215,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
|
||||||
|
|
||||||
|
|
||||||
void OffscreenQmlSurface::updateQuick() {
|
void OffscreenQmlSurface::updateQuick() {
|
||||||
|
PerformanceTimer perfTimer("qmlUpdate");
|
||||||
if (_paused) {
|
if (_paused) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue