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