diff --git a/meshroom/core/node.py b/meshroom/core/node.py
index 60c7573e1c..f2e98a2de3 100644
--- a/meshroom/core/node.py
+++ b/meshroom/core/node.py
@@ -285,6 +285,7 @@ def process(self, forceCompute=False):
# ask and wait for the stats thread to stop
self.statThread.stopRequest()
self.statThread.join()
+ self.statistics = stats.Statistics()
del runningProcesses[self.name]
self.upgradeStatusTo(Status.SUCCESS)
diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py
index adc226253b..ec07dbef31 100644
--- a/meshroom/core/stats.py
+++ b/meshroom/core/stats.py
@@ -1,8 +1,18 @@
from collections import defaultdict
+import subprocess
import logging
import psutil
import time
import threading
+import platform
+import os
+import sys
+
+if sys.version_info[0] == 2:
+ # On Python 2 use C implementation for performance and to avoid lots of warnings
+ from xml.etree import cElementTree as ET
+else:
+ import xml.etree.ElementTree as ET
def bytes2human(n):
@@ -25,15 +35,58 @@ def bytes2human(n):
class ComputerStatistics:
def __init__(self):
- # TODO: init
self.nbCores = 0
self.cpuFreq = 0
+ self.ramTotal = 0
self.ramAvailable = 0 # GB
self.vramAvailable = 0 # GB
self.swapAvailable = 0
-
+ self.gpuMemoryTotal = 0
+ self.gpuName = ''
self.curves = defaultdict(list)
+ self._isInit = False
+
+ def initOnFirstTime(self):
+ if self._isInit:
+ return
+ self._isInit = True
+
+ self.cpuFreq = psutil.cpu_freq().max
+ self.ramTotal = psutil.virtual_memory().total / 1024/1024/1024
+
+ if platform.system() == "Windows":
+ from distutils import spawn
+ # If the platform is Windows and nvidia-smi
+ # could not be found from the environment path,
+ # try to find it from system drive with default installation path
+ self.nvidia_smi = spawn.find_executable('nvidia-smi')
+ if self.nvidia_smi is None:
+ self.nvidia_smi = "%s\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe" % os.environ['systemdrive']
+ else:
+ self.nvidia_smi = "nvidia-smi"
+
+ try:
+ p = subprocess.Popen([self.nvidia_smi, "-q", "-x"], stdout=subprocess.PIPE)
+ xmlGpu, stdError = p.communicate()
+
+ smiTree = ET.fromstring(xmlGpu)
+ gpuTree = smiTree.find('gpu')
+
+ try:
+ self.gpuMemoryTotal = gpuTree.find('fb_memory_usage').find('total').text.split(" ")[0]
+ except Exception as e:
+ logging.debug('Failed to get gpuMemoryTotal: "{}".'.format(str(e)))
+ pass
+ try:
+ self.gpuName = gpuTree.find('product_name').text
+ except Exception as e:
+ logging.debug('Failed to get gpuName: "{}".'.format(str(e)))
+ pass
+
+ except Exception as e:
+ logging.debug('Failed to get information from nvidia_smi at init: "{}".'.format(str(e)))
+
def _addKV(self, k, v):
if isinstance(v, tuple):
for ki, vi in v._asdict().items():
@@ -45,11 +98,41 @@ def _addKV(self, k, v):
self.curves[k].append(v)
def update(self):
+ self.initOnFirstTime()
self._addKV('cpuUsage', psutil.cpu_percent(percpu=True)) # interval=None => non-blocking (percentage since last call)
self._addKV('ramUsage', psutil.virtual_memory().percent)
self._addKV('swapUsage', psutil.swap_memory().percent)
self._addKV('vramUsage', 0)
self._addKV('ioCounters', psutil.disk_io_counters())
+ self.updateGpu()
+
+ def updateGpu(self):
+ try:
+ p = subprocess.Popen([self.nvidia_smi, "-q", "-x"], stdout=subprocess.PIPE)
+ xmlGpu, stdError = p.communicate()
+
+ smiTree = ET.fromstring(xmlGpu)
+ gpuTree = smiTree.find('gpu')
+
+ try:
+ self._addKV('gpuMemoryUsed', gpuTree.find('fb_memory_usage').find('used').text.split(" ")[0])
+ except Exception as e:
+ logging.debug('Failed to get gpuMemoryUsed: "{}".'.format(str(e)))
+ pass
+ try:
+ self._addKV('gpuUsed', gpuTree.find('utilization').find('gpu_util').text.split(" ")[0])
+ except Exception as e:
+ logging.debug('Failed to get gpuUsed: "{}".'.format(str(e)))
+ pass
+ try:
+ self._addKV('gpuTemperature', gpuTree.find('temperature').find('gpu_temp').text.split(" ")[0])
+ except Exception as e:
+ logging.debug('Failed to get gpuTemperature: "{}".'.format(str(e)))
+ pass
+
+ except Exception as e:
+ logging.debug('Failed to get information from nvidia_smi: "{}".'.format(str(e)))
+ return
def toDict(self):
return self.__dict__
@@ -145,12 +228,13 @@ def fromDict(self, d):
class Statistics:
"""
"""
- fileVersion = 1.0
+ fileVersion = 2.0
def __init__(self):
self.computer = ComputerStatistics()
self.process = ProcStatistics()
self.times = []
+ self.interval = 5
def update(self, proc):
'''
@@ -169,19 +253,28 @@ def toDict(self):
'computer': self.computer.toDict(),
'process': self.process.toDict(),
'times': self.times,
+ 'interval': self.interval
}
def fromDict(self, d):
- version = d.get('fileVersion', 1.0)
+ version = d.get('fileVersion', 0.0)
if version != self.fileVersion:
- logging.info('Cannot load statistics, version was {} and we only support {}.'.format(version, self.fileVersion))
- self.computer = {}
- self.process = {}
- self.times = []
- return
- self.computer.fromDict(d.get('computer', {}))
- self.process.fromDict(d.get('process', {}))
- self.times = d.get('times', [])
+ logging.debug('Statistics: file version was {} and the current version is {}.'.format(version, self.fileVersion))
+ self.computer = {}
+ self.process = {}
+ self.times = []
+ try:
+ self.computer.fromDict(d.get('computer', {}))
+ except Exception as e:
+ logging.debug('Failed while loading statistics: computer: "{}".'.format(str(e)))
+ try:
+ self.process.fromDict(d.get('process', {}))
+ except Exception as e:
+ logging.debug('Failed while loading statistics: process: "{}".'.format(str(e)))
+ try:
+ self.times = d.get('times', [])
+ except Exception as e:
+ logging.debug('Failed while loading statistics: times: "{}".'.format(str(e)))
bytesPerGiga = 1024. * 1024. * 1024.
@@ -204,7 +297,7 @@ def run(self):
try:
while True:
self.updateStats()
- if self._stopFlag.wait(60):
+ if self._stopFlag.wait(self.statistics.interval):
# stopFlag has been set
# update stats one last time and exit main loop
if self.proc.is_running():
diff --git a/meshroom/ui/qml/Charts/ChartViewCheckBox.qml b/meshroom/ui/qml/Charts/ChartViewCheckBox.qml
new file mode 100644
index 0000000000..0b395b72e7
--- /dev/null
+++ b/meshroom/ui/qml/Charts/ChartViewCheckBox.qml
@@ -0,0 +1,34 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+
+
+/**
+ * A custom CheckBox designed to be used in ChartView's legend.
+ */
+CheckBox {
+ id: root
+
+ property color color
+
+ leftPadding: 0
+ font.pointSize: 8
+
+ indicator: Rectangle {
+ width: 11
+ height: width
+ border.width: 1
+ border.color: root.color
+ color: "transparent"
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: parent.border.width + 1
+ visible: parent.parent.checkState != Qt.Unchecked
+ anchors.topMargin: parent.parent.checkState === Qt.PartiallyChecked ? 5 : 2
+ anchors.bottomMargin: anchors.topMargin
+ color: parent.border.color
+ anchors.centerIn: parent
+ }
+ }
+}
diff --git a/meshroom/ui/qml/Charts/ChartViewLegend.qml b/meshroom/ui/qml/Charts/ChartViewLegend.qml
new file mode 100644
index 0000000000..1244871559
--- /dev/null
+++ b/meshroom/ui/qml/Charts/ChartViewLegend.qml
@@ -0,0 +1,105 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.9
+import QtCharts 2.3
+
+
+/**
+ * ChartViewLegend is an interactive legend component for ChartViews.
+ * It provides a CheckBox for each series that can control its visibility,
+ * and highlight on hovering.
+ */
+Flow {
+ id: root
+
+ // The ChartView to create the legend for
+ property ChartView chartView
+ // Currently hovered series
+ property var hoveredSeries: null
+
+ readonly property ButtonGroup buttonGroup: ButtonGroup {
+ id: legendGroup
+ exclusive: false
+ }
+
+ /// Shortcut function to clear legend
+ function clear() {
+ seriesModel.clear();
+ }
+
+ // Update internal ListModel when ChartView's series change
+ Connections {
+ target: chartView
+ onSeriesAdded: seriesModel.append({"series": series})
+ onSeriesRemoved: {
+ for(var i = 0; i < seriesModel.count; ++i)
+ {
+ if(seriesModel.get(i)["series"] === series)
+ {
+ seriesModel.remove(i);
+ return;
+ }
+ }
+ }
+ }
+
+ onChartViewChanged: {
+ clear();
+ for(var i = 0; i < chartView.count; ++i)
+ seriesModel.append({"series": chartView.series(i)});
+ }
+
+ Repeater {
+
+ // ChartView series can't be accessed directly as a model.
+ // Use an intermediate ListModel populated with those series.
+ model: ListModel {
+ id: seriesModel
+ }
+
+ ChartViewCheckBox {
+ ButtonGroup.group: legendGroup
+
+ checked: series.visible
+ text: series.name
+ color: series.color
+
+ onHoveredChanged: {
+ if(hovered && series.visible)
+ root.hoveredSeries = series;
+ else
+ root.hoveredSeries = null;
+ }
+
+ // hovered serie properties override
+ states: [
+ State {
+ when: series && root.hoveredSeries === series
+ PropertyChanges { target: series; width: 5.0 }
+ },
+ State {
+ when: series && root.hoveredSeries && root.hoveredSeries !== series
+ PropertyChanges { target: series; width: 0.2 }
+ }
+ ]
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if(mouse.modifiers & Qt.ControlModifier)
+ root.soloSeries(index);
+ else
+ series.visible = !series.visible;
+ }
+ }
+ }
+ }
+
+ /// Hide all series but the one at index 'idx'
+ function soloSeries(idx) {
+ for(var i = 0; i < seriesModel.count; i++) {
+ chartView.series(i).visible = false;
+ }
+ chartView.series(idx).visible = true;
+ }
+
+}
diff --git a/meshroom/ui/qml/Charts/qmldir b/meshroom/ui/qml/Charts/qmldir
new file mode 100644
index 0000000000..32ea2d3271
--- /dev/null
+++ b/meshroom/ui/qml/Charts/qmldir
@@ -0,0 +1,4 @@
+module Charts
+
+ChartViewLegend 1.0 ChartViewLegend.qml
+ChartViewCheckBox 1.0 ChartViewCheckBox.qml
diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml
index 872a1ef13a..3d5923e66f 100644
--- a/meshroom/ui/qml/GraphEditor/NodeLog.qml
+++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml
@@ -89,8 +89,10 @@ FocusScope {
// only set text file viewer source when ListView is fully ready
// (either empty or fully populated with a valid currentChunk)
// to avoid going through an empty url when switching between two nodes
+
if(!chunksLV.count || chunksLV.currentChunk)
- textFileViewer.source = Filepath.stringToUrl(currentFile);
+ logComponentLoader.source = Filepath.stringToUrl(currentFile);
+
}
TabButton {
@@ -111,12 +113,35 @@ FocusScope {
}
}
- TextFileViewer {
- id: textFileViewer
+ Loader {
+ id: logComponentLoader
+ clip: true
Layout.fillWidth: true
Layout.fillHeight: true
- autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING"
- // source is set in fileSelector
+ property url source
+ sourceComponent: fileSelector.currentItem.fileProperty === "statisticsFile" ? statViewerComponent : textFileViewerComponent
+ }
+
+ Component {
+ id: textFileViewerComponent
+ TextFileViewer {
+ id: textFileViewer
+ source: logComponentLoader.source
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING"
+ // source is set in fileSelector
+ }
+ }
+
+ Component {
+ id: statViewerComponent
+ StatViewer {
+ id: statViewer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ source: logComponentLoader.source
+ }
}
}
}
diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml
new file mode 100644
index 0000000000..6ebf29c92b
--- /dev/null
+++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml
@@ -0,0 +1,539 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.3
+import QtCharts 2.2
+import QtQuick.Layouts 1.11
+import Utils 1.0
+import Charts 1.0
+import MaterialIcons 2.2
+
+Item {
+ id: root
+
+ implicitWidth: 500
+ implicitHeight: 500
+
+ /// Statistics source file
+ property url source
+
+ property var sourceModified: undefined
+ property var jsonObject
+ property real fileVersion: 0.0
+
+ property int nbReads: 1
+ property real deltaTime: 1
+
+ property int nbCores: 0
+ property int cpuFrequency: 0
+
+ property int ramTotal
+ property string ramLabel: "RAM: "
+
+ property int gpuTotalMemory
+ property int gpuMaxAxis: 100
+ property string gpuName
+
+ property color textColor: Colors.sysPalette.text
+
+
+ readonly property var colors: [
+ "#f44336",
+ "#e91e63",
+ "#9c27b0",
+ "#673ab7",
+ "#3f51b5",
+ "#2196f3",
+ "#03a9f4",
+ "#00bcd4",
+ "#009688",
+ "#4caf50",
+ "#8bc34a",
+ "#cddc39",
+ "#ffeb3b",
+ "#ffc107",
+ "#ff9800",
+ "#ff5722",
+ "#b71c1c",
+ "#880E4F",
+ "#4A148C",
+ "#311B92",
+ "#1A237E",
+ "#0D47A1",
+ "#01579B",
+ "#006064",
+ "#004D40",
+ "#1B5E20",
+ "#33691E",
+ "#827717",
+ "#F57F17",
+ "#FF6F00",
+ "#E65100",
+ "#BF360C"
+ ]
+
+ onSourceChanged: {
+ sourceModified = undefined;
+ resetCharts()
+ readSourceFile()
+ }
+
+ function getPropertyWithDefault(prop, name, defaultValue) {
+ if(prop.hasOwnProperty(name)) {
+ return prop[name];
+ }
+ return defaultValue;
+ }
+
+ Timer {
+ id: reloadTimer
+ interval: root.deltaTime * 60000; running: true; repeat: false
+ onTriggered: readSourceFile()
+
+ }
+
+ function readSourceFile() {
+ // make sure we are trying to load a statistics file
+ if(!Filepath.urlToString(source).endsWith("statistics"))
+ return;
+
+ var xhr = new XMLHttpRequest;
+ xhr.open("GET", source);
+
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === XMLHttpRequest.DONE && xhr.status == 200) {
+
+ if(sourceModified === undefined || sourceModified < xhr.getResponseHeader('Last-Modified')) {
+ try {
+ root.jsonObject = JSON.parse(xhr.responseText);
+ }
+ catch(exc)
+ {
+ console.warning("Failed to parse statistics file: " + source)
+ root.jsonObject = {};
+ return;
+ }
+ resetCharts();
+ sourceModified = xhr.getResponseHeader('Last-Modified')
+ root.createCharts();
+ reloadTimer.restart();
+ }
+ }
+ };
+ xhr.send();
+ }
+
+ function resetCharts() {
+ root.fileVersion = 0.0
+ cpuLegend.clear()
+ cpuChart.removeAllSeries()
+ ramChart.removeAllSeries()
+ gpuChart.removeAllSeries()
+ }
+
+ function createCharts() {
+ root.deltaTime = getPropertyWithDefault(jsonObject, 'interval', 30) / 60.0;
+ root.fileVersion = getPropertyWithDefault(jsonObject, 'fileVersion', 0.0)
+ initCpuChart()
+ initRamChart()
+ initGpuChart()
+ }
+
+
+/**************************
+*** CPU ***
+**************************/
+
+ function initCpuChart() {
+
+ var categories = []
+ var categoryCount = 0
+ var category
+ do {
+ category = jsonObject.computer.curves["cpuUsage." + categoryCount]
+ if(category !== undefined) {
+ categories.push(category)
+ categoryCount++
+ }
+ } while(category !== undefined)
+
+ var nbCores = categories.length
+ root.nbCores = nbCores
+
+ root.cpuFrequency = getPropertyWithDefault(jsonObject.computer, 'cpuFreq', -1)
+
+ root.nbReads = categories[0].length-1
+
+ for(var j = 0; j < nbCores; j++) {
+ var lineSerie = cpuChart.createSeries(ChartView.SeriesTypeLine, "CPU" + j, valueAxisX, valueAxisY)
+
+ if(categories[j].length === 1) {
+ lineSerie.append(0, categories[j][0])
+ lineSerie.append(root.deltaTime, categories[j][0])
+ } else {
+ for(var k = 0; k < categories[j].length; k++) {
+ lineSerie.append(k * root.deltaTime, categories[j][k])
+ }
+ }
+ lineSerie.color = colors[j % colors.length]
+ }
+
+ var averageLine = cpuChart.createSeries(ChartView.SeriesTypeLine, "AVERAGE", valueAxisX, valueAxisY)
+ var average = []
+
+ for(var l = 0; l < categories[0].length; l++) {
+ average.push(0)
+ }
+
+ for(var m = 0; m < categories.length; m++) {
+ for(var n = 0; n < categories[m].length; n++) {
+ average[n] += categories[m][n]
+ }
+ }
+
+ for(var q = 0; q < average.length; q++) {
+ average[q] = average[q] / (categories.length)
+
+ averageLine.append(q * root.deltaTime, average[q])
+ }
+
+ averageLine.color = colors[colors.length-1]
+ }
+
+ function hideOtherCpu(index) {
+ for(var i = 0; i < cpuChart.count; i++) {
+ cpuChart.series(i).visible = false;
+ }
+ cpuChart.series(index).visible = true;
+ }
+
+
+/**************************
+*** RAM ***
+**************************/
+
+ function initRamChart() {
+
+ var ram = getPropertyWithDefault(jsonObject.computer.curves, 'ramUsage', -1)
+
+ root.ramTotal = getPropertyWithDefault(jsonObject.computer, 'ramTotal', -1)
+ root.ramLabel = "RAM: "
+ if(root.ramTotal <= 0)
+ {
+ var maxRamPeak = 0
+ for(var i = 0; i < ram.length; i++) {
+ maxRamPeak = Math.max(maxRamPeak, ram[i])
+ }
+ root.ramTotal = maxRamPeak
+ root.ramLabel = "RAM Max Peak: "
+ }
+
+ var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, root.ramLabel + root.ramTotal + "GB", valueAxisX2, valueAxisRam)
+
+ if(ram.length === 1) {
+ // Create 2 entries if we have only one input value to create a segment that can be display
+ ramSerie.append(0, ram[0])
+ ramSerie.append(root.deltaTime, ram[0])
+ } else {
+ for(var i = 0; i < ram.length; i++) {
+ ramSerie.append(i * root.deltaTime, ram[i])
+ }
+ }
+ ramSerie.color = colors[10]
+ }
+
+/**************************
+*** GPU ***
+**************************/
+
+ function initGpuChart() {
+ root.gpuTotalMemory = getPropertyWithDefault(jsonObject.computer, 'gpuMemoryTotal', 0)
+ root.gpuName = getPropertyWithDefault(jsonObject.computer, 'gpuName', '')
+
+ var gpuUsedMemory = getPropertyWithDefault(jsonObject.computer.curves, 'gpuMemoryUsed', 0)
+ var gpuUsed = getPropertyWithDefault(jsonObject.computer.curves, 'gpuUsed', 0)
+ var gpuTemperature = getPropertyWithDefault(jsonObject.computer.curves, 'gpuTemperature', 0)
+
+ var gpuUsedSerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "GPU", valueAxisX3, valueAxisY3)
+ var gpuUsedMemorySerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "Memory", valueAxisX3, valueAxisY3)
+ var gpuTemperatureSerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "Temperature", valueAxisX3, valueAxisY3)
+
+ if(gpuUsedMemory.length === 1) {
+ gpuUsedSerie.append(0, gpuUsed[0])
+ gpuUsedSerie.append(1 * root.deltaTime, gpuUsed[0])
+
+ gpuUsedMemorySerie.append(0, gpuUsedMemory[0] / root.gpuTotalMemory * 100)
+ gpuUsedMemorySerie.append(1 * root.deltaTime, gpuUsedMemory[0] / root.gpuTotalMemory * 100)
+
+ gpuTemperatureSerie.append(0, gpuTemperature[0])
+ gpuTemperatureSerie.append(1 * root.deltaTime, gpuTemperature[0])
+ root.gpuMaxAxis = Math.max(gpuMaxAxis, gpuTemperature[0])
+ } else {
+ for(var i = 0; i < gpuUsedMemory.length; i++) {
+ gpuUsedSerie.append(i * root.deltaTime, gpuUsed[i])
+
+ gpuUsedMemorySerie.append(i * root.deltaTime, gpuUsedMemory[i] / root.gpuTotalMemory * 100)
+
+ gpuTemperatureSerie.append(i * root.deltaTime, gpuTemperature[i])
+ root.gpuMaxAxis = Math.max(gpuMaxAxis, gpuTemperature[i])
+ }
+ }
+ }
+
+
+/**************************
+*** UI ***
+**************************/
+
+ ScrollView {
+ height: root.height
+ width: root.width
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOn
+
+ ColumnLayout {
+ width: root.width
+
+
+/**************************
+*** CPU UI ***
+**************************/
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ Button {
+ id: toggleCpuBtn
+ Layout.fillWidth: true
+ text: "Toggle CPU's"
+ state: "closed"
+
+ onClicked: state === "opened" ? state = "closed" : state = "opened"
+
+ MaterialLabel {
+ text: MaterialIcons.arrow_drop_down
+ font.pointSize: 14
+ anchors.right: parent.right
+ }
+
+ states: [
+ State {
+ name: "opened"
+ PropertyChanges { target: cpuBtnContainer; visible: true }
+ PropertyChanges { target: toggleCpuBtn; down: true }
+ },
+ State {
+ name: "closed"
+ PropertyChanges { target: cpuBtnContainer; visible: false }
+ PropertyChanges { target: toggleCpuBtn; down: false }
+ }
+ ]
+ }
+
+ Item {
+ id: cpuBtnContainer
+
+ Layout.fillWidth: true
+ implicitHeight: childrenRect.height
+ Layout.leftMargin: 25
+
+ RowLayout {
+ width: parent.width
+ anchors.horizontalCenter: parent.horizontalCenter
+
+
+ ChartViewCheckBox {
+ id: allCPU
+ text: "ALL"
+ color: textColor
+ checkState: cpuLegend.buttonGroup.checkState
+ leftPadding: 0
+ onClicked: {
+ var _checked = checked;
+ for(var i = 0; i < cpuChart.count; ++i)
+ {
+ cpuChart.series(i).visible = _checked;
+ }
+ }
+ }
+
+ ChartViewLegend {
+ id: cpuLegend
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ chartView: cpuChart
+ }
+
+ }
+ }
+
+ ChartView {
+ id: cpuChart
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: width/2
+ margins.top: 0
+ margins.bottom: 0
+ antialiasing: true
+
+ legend.visible: false
+ theme: ChartView.ChartThemeLight
+ backgroundColor: "transparent"
+ plotAreaColor: "transparent"
+ titleColor: textColor
+
+ visible: (root.fileVersion > 0.0) // only visible if we have valid information
+ title: "CPU: " + root.nbCores + " cores, " + root.cpuFrequency + "Hz"
+
+ ValueAxis {
+ id: valueAxisY
+ min: 0
+ max: 100
+ titleText: "%"
+ color: textColor
+ gridLineColor: textColor
+ minorGridLineColor: textColor
+ shadesColor: textColor
+ shadesBorderColor: textColor
+ labelsColor: textColor
+ }
+
+ ValueAxis {
+ id: valueAxisX
+ min: 0
+ max: root.deltaTime * Math.max(1, root.nbReads)
+ titleText: "Minutes"
+ color: textColor
+ gridLineColor: textColor
+ minorGridLineColor: textColor
+ shadesColor: textColor
+ shadesBorderColor: textColor
+ labelsColor: textColor
+ }
+
+ }
+ }
+
+
+
+/**************************
+*** RAM UI ***
+**************************/
+
+ ColumnLayout {
+
+ ChartView {
+ id: ramChart
+ margins.top: 0
+ margins.bottom: 0
+ Layout.fillWidth: true
+ Layout.preferredHeight: width/2
+ antialiasing: true
+ legend.color: textColor
+ legend.labelColor: textColor
+ legend.visible: false
+ theme: ChartView.ChartThemeLight
+ backgroundColor: "transparent"
+ plotAreaColor: "transparent"
+ titleColor: textColor
+
+ visible: (root.fileVersion > 0.0) // only visible if we have valid information
+ title: root.ramLabel + root.ramTotal + "GB"
+
+ ValueAxis {
+ id: valueAxisY2
+ min: 0
+ max: 100
+ titleText: "%"
+ color: textColor
+ gridLineColor: textColor
+ minorGridLineColor: textColor
+ shadesColor: textColor
+ shadesBorderColor: textColor
+ labelsColor: textColor
+ }
+
+ ValueAxis {
+ id: valueAxisRam
+ min: 0
+ max: root.ramTotal
+ titleText: "GB"
+ color: textColor
+ gridLineColor: textColor
+ minorGridLineColor: textColor
+ shadesColor: textColor
+ shadesBorderColor: textColor
+ labelsColor: textColor
+ }
+
+ ValueAxis {
+ id: valueAxisX2
+ min: 0
+ max: root.deltaTime * Math.max(1, root.nbReads)
+ titleText: "Minutes"
+ color: textColor
+ gridLineColor: textColor
+ minorGridLineColor: textColor
+ shadesColor: textColor
+ shadesBorderColor: textColor
+ labelsColor: textColor
+ }
+ }
+ }
+
+
+
+/**************************
+*** GPU UI ***
+**************************/
+
+ ColumnLayout {
+
+
+ ChartView {
+ id: gpuChart
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: width/2
+ margins.top: 0
+ margins.bottom: 0
+ antialiasing: true
+ legend.color: textColor
+ legend.labelColor: textColor
+ theme: ChartView.ChartThemeLight
+ backgroundColor: "transparent"
+ plotAreaColor: "transparent"
+ titleColor: textColor
+
+ visible: (root.fileVersion >= 2.0) // No GPU information was collected before stats 2.0 fileVersion
+ title: (root.gpuName || root.gpuTotalMemory) ? ("GPU: " + root.gpuName + ", " + root.gpuTotalMemory + "MB") : "No GPU"
+
+ ValueAxis {
+ id: valueAxisY3
+ min: 0
+ max: root.gpuMaxAxis
+ titleText: "%, °C"
+ color: textColor
+ gridLineColor: textColor
+ minorGridLineColor: textColor
+ shadesColor: textColor
+ shadesBorderColor: textColor
+ labelsColor: textColor
+ }
+
+ ValueAxis {
+ id: valueAxisX3
+ min: 0
+ max: root.deltaTime * Math.max(1, root.nbReads)
+ titleText: "Minutes"
+ color: textColor
+ gridLineColor: textColor
+ minorGridLineColor: textColor
+ shadesColor: textColor
+ shadesBorderColor: textColor
+ labelsColor: textColor
+ }
+ }
+ }
+
+ }
+ }
+
+}
diff --git a/requirements.txt b/requirements.txt
index a0994cf01c..7daba766c1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
# runtime
-psutil
+psutil>=5.6.3
enum34;python_version<"3.4"
PySide2==5.13.0
markdown==2.6.11