diff --git a/Dashboards/PS1 Barnacle Dashboard.json b/Dashboards/PS1 Barnacle Dashboard.json new file mode 100644 index 0000000..3095e2b --- /dev/null +++ b/Dashboards/PS1 Barnacle Dashboard.json @@ -0,0 +1,1355 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "res_y", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameHeight", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "res_x", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameWidth", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "targetBitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "targetBitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "bitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Bitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeFps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeFPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 15 + }, + { + "color": "#EAB839", + "value": 50 + }, + { + "color": "#6ED0E0", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_WebRTC_Fps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "WebRTC FPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "jitter", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Jitter", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "qp", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Quantization Parameter (QP)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MinQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMinQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MaxQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMaxQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeTime", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeTime", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "packetsLost", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Packets Lost", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_physical", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Physical Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_gpu", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "GPU Memory Usage", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "PS1 Barnacle Dashboard", + "uid": "c9df3f76-9c17-4a1e-89a3-bd6851876b30", + "version": 34 +} \ No newline at end of file diff --git a/Dashboards/PS2 Barnacle Dashboard.json b/Dashboards/PS2 Barnacle Dashboard.json new file mode 100644 index 0000000..6564a64 --- /dev/null +++ b/Dashboards/PS2 Barnacle Dashboard.json @@ -0,0 +1,1355 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "res_y", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameHeight", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "res_x", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameWidth", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "targetBitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "targetBitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "bitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Bitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeFps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeFPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 15 + }, + { + "color": "#EAB839", + "value": 50 + }, + { + "color": "#6ED0E0", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_WebRTC_Fps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "WebRTC FPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "jitter", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Jitter", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "qp", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Quantization Parameter (QP)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MinQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMinQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MaxQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMaxQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeTime", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeTime", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "packetsLost", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Packets Lost", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_physical", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Physical Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_gpu", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "GPU Memory Usage", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "PS2 Barnacle Dashboard", + "uid": "c9df3f76-9c17-4a1e-89a3-bd6851876b30", + "version": 34 +} \ No newline at end of file diff --git a/Examples/Compose/docker-compose-all.yml b/Examples/Compose/docker-compose-all.yml new file mode 100644 index 0000000..89ffd29 --- /dev/null +++ b/Examples/Compose/docker-compose-all.yml @@ -0,0 +1,58 @@ +services: + unreal: + image: "tensorworks/buccaneerdemo-application" + command: [ "-PixelStreamingURL=ws://127.0.0.1:8888", "-BuccaneerURL=http://127.0.0.1:8000", "-RenderOffScreen", "-Res=1920x1080" ] + container_name: unreal + network_mode: "host" + deploy: + resources: + reservations: + devices: + - driver: nvidia + capabilities: [gpu] + count: 1 + + cirrus: + image: "tensorworks/buccaneerdemo-cirrus" + container_name: cirrus + network_mode: "host" + + buccaneerserver: + image: "tensorworks/buccaneerdemo-buccaneerserver" + container_name: buccaneerserver + network_mode: "host" + + prometheus: + image: "prom/prometheus" + container_name: prometheus + network_mode: "host" + volumes: + - "../../Configs/prometheus.yml:/etc/prometheus/prometheus.yml" + + grafana: + image: grafana/grafana + container_name: grafana + network_mode: "host" + volumes: + - "../../Configs/grafana-dashboard-config.yaml:/etc/grafana/provisioning/dashboards/grafana-dashboard-config.yaml" + - "../../Configs/grafana-datasource-config.yaml:/etc/grafana/provisioning/datasources/grafana-datasource-config.yaml" + - "../../Dashboards/:/etc/dashboards" + + loki: + image: grafana/loki + container_name: loki + network_mode: "host" + volumes: + - "../../Configs/loki-local-config.yaml:/etc/loki/loki-local-config.yaml" + + promtail: + image: grafana/promtail + container_name: promtail + command: -config.file=/etc/promtail/promtail-local-config.yaml + network_mode: "host" + volumes: + - "../../Configs/promtail-local-config.yaml:/etc/promtail/promtail-local-config.yaml" + - "eventslogs:/EventsServer" + +volumes: + eventslogs: diff --git a/Examples/Compose/docker-compose.yml b/Examples/Compose/docker-compose.yml index 89ffd29..3bf7d58 100644 --- a/Examples/Compose/docker-compose.yml +++ b/Examples/Compose/docker-compose.yml @@ -1,26 +1,11 @@ services: - unreal: - image: "tensorworks/buccaneerdemo-application" - command: [ "-PixelStreamingURL=ws://127.0.0.1:8888", "-BuccaneerURL=http://127.0.0.1:8000", "-RenderOffScreen", "-Res=1920x1080" ] - container_name: unreal - network_mode: "host" - deploy: - resources: - reservations: - devices: - - driver: nvidia - capabilities: [gpu] - count: 1 - - cirrus: - image: "tensorworks/buccaneerdemo-cirrus" - container_name: cirrus - network_mode: "host" - - buccaneerserver: - image: "tensorworks/buccaneerdemo-buccaneerserver" + buccaneer-server: + build: + context: ../../Server/BuccaneerServer + dockerfile: Dockerfile container_name: buccaneerserver - network_mode: "host" + ports: + - "8000:8000" prometheus: image: "prom/prometheus" @@ -44,6 +29,7 @@ services: network_mode: "host" volumes: - "../../Configs/loki-local-config.yaml:/etc/loki/loki-local-config.yaml" + command: -config.file=/etc/loki/loki-local-config.yaml promtail: image: grafana/promtail diff --git a/Plugins/Buccaneer/Buccaneer.uplugin b/Plugins/Buccaneer/Buccaneer.uplugin index 3766b88..4b7fbd5 100644 --- a/Plugins/Buccaneer/Buccaneer.uplugin +++ b/Plugins/Buccaneer/Buccaneer.uplugin @@ -21,12 +21,12 @@ "LoadingPhase": "Default" }, { - "Name": "TimeSeriesDataEmitter", + "Name": "BuccaneerStats", "Type": "Runtime", "LoadingPhase": "Default" }, { - "Name": "SemanticEventEmitter", + "Name": "BuccaneerEvents", "Type": "Runtime", "LoadingPhase": "Default" } diff --git a/Plugins/Buccaneer/README.md b/Plugins/Buccaneer/README.md deleted file mode 100644 index 08c3d3d..0000000 --- a/Plugins/Buccaneer/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Buccaneer - - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin https://gitlab.com/TensorWorks/internal/buccaneer.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://gitlab.com/TensorWorks/internal/buccaneer/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. - diff --git a/Plugins/Buccaneer/Resources/Icon128.png b/Plugins/Buccaneer/Resources/Icon128.png index 1231d4a..c175b12 100644 Binary files a/Plugins/Buccaneer/Resources/Icon128.png and b/Plugins/Buccaneer/Resources/Icon128.png differ diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs b/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs index 9dfc4f1..be796ee 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. using UnrealBuildTool; @@ -17,6 +17,9 @@ public BuccaneerCommon(ReadOnlyTargetRules Target) : base(Target) PublicDependencyModuleNames.AddRange(new string[]{ "HTTP", + "CoreUObject", + "DeveloperSettings", + "EngineSettings" }); } } diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp deleted file mode 100644 index 9c613c6..0000000 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "BuccaneerCommon.h" -#include "Logging/LogMacros.h" - -DEFINE_LOG_CATEGORY(BuccaneerCommon); - -FBuccaneerCommonModule *FBuccaneerCommonModule::BuccaneerCommonModule = nullptr; - -void FBuccaneerCommonModule::StartupModule() -{ - CVarBuccaneerEnableStats = IConsoleManager::Get().RegisterConsoleVariable( - TEXT("Buccaneer.EnableStats"), - true, - TEXT("Disables the collection of and logging of performance metrics"), - ECVF_Default); - - CVarBuccaneerEnableEvents = IConsoleManager::Get().RegisterConsoleVariable( - TEXT("Buccaneer.EnableEvents"), - true, - TEXT("Disables the collection and logging of semantic events"), - ECVF_Default); - - Setup(); -} - -void FBuccaneerCommonModule::ShutdownModule() -{ -} - -void FBuccaneerCommonModule::Setup() -{ - ParseCommandLineOption(TEXT("BuccaneerEnableStats"), CVarBuccaneerEnableStats); - ParseCommandLineOption(TEXT("BuccaneerEnableEvents"), CVarBuccaneerEnableEvents); - - if (!FParse::Value(FCommandLine::Get(), TEXT("BuccaneerURL="), BuccaneerURL)) - { - FString BuccaneerIP; - uint16 BuccaneerPort; - if (FParse::Value(FCommandLine::Get(), TEXT("BuccaneerIP="), BuccaneerIP) && FParse::Value(FCommandLine::Get(), TEXT("BuccaneerPort="), BuccaneerPort)) - { - // build the proper url. - BuccaneerURL = FString::Printf(TEXT("http://%s:%d"), *BuccaneerIP, BuccaneerPort); - } - } - - if (BuccaneerURL.IsEmpty()) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("Buccanner events and stats disabled, provide `BuccaneerURL` cmd-args to enable it")); - CVarBuccaneerEnableEvents->Set(false, ECVF_SetByCommandline); - CVarBuccaneerEnableStats->Set(false, ECVF_SetByCommandline); - return; - } - - // Try and parse an instance ID - if (!FParse::Value(FCommandLine::Get(), TEXT("BuccaneerID="), InstanceID)) - { - // Try and parse a pixel streaming ID for users who don't want to pollute their command line by specifying two IDs - if (!FParse::Value(FCommandLine::Get(), TEXT("PixelStreamingID="), InstanceID)) - { - // Generate an instance ID if one isn't provided - InstanceID = FGuid::NewGuid().ToString(); - } - } - - // Additional Metadata - MetadataJson = MakeShareable(new FJsonObject()); - FString CmdLineMetadata; - if (FParse::Value(FCommandLine::Get(), TEXT("BuccaneerMetadata="), CmdLineMetadata)) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("%s"), *CmdLineMetadata); - TArray ParsedMetadata; - CmdLineMetadata.ParseIntoArray(ParsedMetadata, TEXT(";"), false); - for (FString Element : ParsedMetadata) - { - if(Element.IsEmpty()) - { - continue; - } - - FString Key, Value; - Element.Split(TEXT(":"), &Key, &Value); - if(Key.IsEmpty() || Value.IsEmpty()) - { - continue; - } - - MetadataJson->SetField(*Key, MakeShared((TEXT("%s"), *Value))); - } - } - - SetupComplete.Broadcast(); -} - -FBuccaneerCommonModule *FBuccaneerCommonModule::GetModule() -{ - if (BuccaneerCommonModule) - { - return BuccaneerCommonModule; - } - FBuccaneerCommonModule *Module = FModuleManager::Get().LoadModulePtr("BuccaneerCommon"); - if (Module) - { - BuccaneerCommonModule = Module; - } - return BuccaneerCommonModule; -} - -void FBuccaneerCommonModule::SendStats(TSharedPtr JsonObject) -{ - JsonObject->SetField("id", MakeShared((TEXT("%s"), *InstanceID))); - JsonObject->SetField("metadata", MakeShared(MetadataJson)); - SendHTTP(BuccaneerURL + FString("/stats"), JsonObject); -} - -void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) -{ - JsonObject->SetField("id", MakeShared((TEXT("%s"), *InstanceID))); - SendHTTP(BuccaneerURL + FString("/event"), JsonObject); -} - -void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonObject) -{ - FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - - FString body; - TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&body); - if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("Cannot serialize json object")); - } - - HttpRequest->SetURL(URL); - HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - HttpRequest->SetVerb(TEXT("POST")); - HttpRequest->SetContentAsString(body); - bool bInFlight = true; - HttpRequest->OnProcessRequestComplete().BindLambda( - [&bInFlight](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) - { - FString ResponseStr, ErrorStr; - - if (bSucceeded && HttpResponse.IsValid()) - { - ResponseStr = HttpResponse->GetContentAsString(); - if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) - { - ErrorStr = FString::Printf(TEXT("Invalid response. code=%d error=%s"), - HttpResponse->GetResponseCode(), *ResponseStr); - } - } - else - { - ErrorStr = TEXT("No response"); - } - - if (!ErrorStr.IsEmpty()) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("Push event response: %s"), *ErrorStr); - } - - bInFlight = false; - }); - HttpRequest->ProcessRequest(); -} - -void FBuccaneerCommonModule::ParseCommandLineOption(const TCHAR *Match, IConsoleVariable *CVar) -{ - FString ValueMatch(Match); - ValueMatch.Append(TEXT("=")); - FString Value; - if (FParse::Value(FCommandLine::Get(), *ValueMatch, Value)) - { - if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) - { - CVar->Set(true, ECVF_SetByCommandline); - } - else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) - { - CVar->Set(false, ECVF_SetByCommandline); - } - } - else if (FParse::Param(FCommandLine::Get(), Match)) - { - CVar->Set(true, ECVF_SetByCommandline); - } -} - -IMPLEMENT_MODULE(FBuccaneerCommonModule, BuccaneerCommon) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp new file mode 100644 index 0000000..b91cafd --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -0,0 +1,231 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerCommonModule.h" +#include "BuccaneerMetrics.h" +#include "BuccaneerSettings.h" +#include "HttpModule.h" +#include "Interfaces/IHttpRequest.h" +#include "Interfaces/IHttpResponse.h" +#include "Logging.h" +#include "HAL/FileManager.h" + +void FBuccaneerCommonModule::StartupModule() +{ + if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread().IsEmpty() && !UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Buccanner events and stats disabled, provide `BuccaneerURL` or `BuccaneerEnableJSONOutput` cmd-args to enable it"); + UBuccaneerSettings::CVarEnableStats->Set(false, ECVF_SetByCommandline); + UBuccaneerSettings::CVarEnableEvents->Set(false, ECVF_SetByCommandline); + return; + } + + // Auto-generate BuccaneerID if not provided + FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); + if (BuccaneerID.IsEmpty()) + { + // Generate unique ID using short GUID + BuccaneerID = FGuid::NewGuid().ToString(EGuidFormats::Short); + + UBuccaneerSettings::CVarID->Set(*BuccaneerID, ECVF_SetByCommandline); + UE_LOGFMT(LogBuccaneerCommon, Warning, + "No BuccaneerID provided. Auto-generated: {0}", BuccaneerID); + } + + if (UBuccaneerSettings::FDelegates *Delegates = UBuccaneerSettings::Delegates()) + { + Delegates->OnMetadataChanged.AddRaw(this, &FBuccaneerCommonModule::FormatMetadata); + } + + FormatMetadata(nullptr); + + // Generate the JSON output filename once at startup if using JSON output + if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) + { + FString OutputFile = UBuccaneerSettings::CVarJSONOutputFile.GetValueOnAnyThread(); + if (OutputFile.IsEmpty()) + { + // Generate default filename: _Stats.json + FString SanitizedID = BuccaneerID.Replace(TEXT(":"), TEXT("-")).Replace(TEXT("/"), TEXT("-")); + OutputFile = FString::Printf(TEXT("%s_Stats.json"), *SanitizedID); + } + else + { + // Ensure .json extension is present (add if missing, don't duplicate if already there) + if (!OutputFile.EndsWith(TEXT(".json"), ESearchCase::IgnoreCase)) + { + OutputFile += TEXT(".json"); + } + } + CachedJSONOutputFileName = OutputFile; + } + + bModuleReady = true; + ReadyEvent.Broadcast(*this); +} + +void FBuccaneerCommonModule::ShutdownModule() +{ +} + +FBuccaneerCommonModule::FReadyEvent &FBuccaneerCommonModule::OnReady() +{ + return ReadyEvent; +} + +bool FBuccaneerCommonModule::IsReady() +{ + return bModuleReady; +} + +void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection& StatsCollection) +{ + const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); + + TSharedPtr JsonObject = StatsCollection.ToJson(); + TSharedPtr JsonBuccaneerID = MakeShared(BuccaneerID); + JsonObject->SetField("id", JsonBuccaneerID); + + // Check if there is any metadata to send + if(UBuccaneerSettings::CVarMetadata.GetValueOnAnyThread() != "") + { + JsonObject->SetField("metadata", MakeShared(MetadataJson)); + } + + // Write metrics JSON to either HTTP Buccaneer server or to disk depending on the CVar settings + + // Case: Sending stats to disk + if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) + { + WriteJSON(CachedJSONOutputFileName, JsonObject); + } + // Case: Sending stats to Buccaneer server + else if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread() != "") + { + SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/stats"), JsonObject); + } +} + +void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) +{ + const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); + TSharedPtr JsonBuccaneerID = MakeShared(BuccaneerID); + JsonObject->SetField("id", JsonBuccaneerID); + + // Only send events to server if we are not in JSON writing mode + if (!UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) + { + SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/event"), JsonObject); + } +} + +void FBuccaneerCommonModule::WriteJSON(FString FileName, TSharedPtr JsonObject) +{ + static FCriticalSection JsonFileMutex; + FScopeLock Lock(&JsonFileMutex); + + FString FilePath = FPaths::Combine(UBuccaneerSettings::CVarJSONOutputDirectory.GetValueOnAnyThread(), FileName); + + // This is how we turn the JSON object into a string + + FString JsonString; + TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&JsonString); + if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Cannot serialize json object"); + return; + } + + IFileManager &FileManager = IFileManager::Get(); + + // Check if file exists + bool bFileExists = FileManager.FileExists(*FilePath); + + // Open for read/write (no "truncate") + TUniquePtr FileAr(FileManager.CreateFileWriter(*FilePath, FILEWRITE_Append)); + + if (!FileAr) + { + UE_LOG(LogTemp, Error, TEXT("Failed to open file for append: %s"), *FilePath); + return; + } + + // If file is empty or just an empty array like "[]", start a new array. + if (FileAr->TotalSize() <= 2) + { + // First time writing OR writing to a corrupted file. + FileAr->Seek(0); + FString Start = TEXT("[\n") + JsonString + TEXT("\n]"); + FTCHARToUTF8 Converter(*Start); + FileAr->Serialize((UTF8CHAR *)Converter.Get(), Converter.Length()); + } + else + { + // Not first time writing: Insert new JSON at correct position. + // Seek before the last two characters, assuming they are '\n]'. + FileAr->Seek(FileAr->TotalSize() - 2); + FString Content = TEXT(",\n") + JsonString + TEXT("\n]"); + FTCHARToUTF8 Converter(*Content); + FileAr->Serialize((UTF8CHAR *)Converter.Get(), Converter.Length()); + } + + FileAr->Close(); +} + +void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonObject) +{ + FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + + FString Body; + TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&Body); + if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Cannot serialize json object"); + } + + HttpRequest->SetURL(URL); + HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + HttpRequest->SetVerb(TEXT("POST")); + HttpRequest->SetContentAsString(Body); + HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) + { + FString ResponseStr, ErrorStr; + + if (bSucceeded && HttpResponse.IsValid()) + { + ResponseStr = HttpResponse->GetContentAsString(); + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorStr = FString::Printf(TEXT("Invalid response. code=%d error=%s"), HttpResponse->GetResponseCode(), *ResponseStr); + } + } + else + { + ErrorStr = TEXT("No response"); + } + + if (!ErrorStr.IsEmpty()) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Push event response: {0}", *ErrorStr); + } }); + + HttpRequest->ProcessRequest(); +} + +void FBuccaneerCommonModule::FormatMetadata(IConsoleVariable *Var) +{ + // Additional Metadata + MetadataJson = MakeShared(); + + TMap MetadataMap = UBuccaneerSettings::GetMetadata(); + for (const TPair &Pair : MetadataMap) + { + if (Pair.Key.IsEmpty() || Pair.Value.IsEmpty()) + { + continue; + } + + MetadataJson->SetField(*Pair.Key, MakeShared(Pair.Value)); + } +} + +IMPLEMENT_MODULE(FBuccaneerCommonModule, BuccaneerCommon) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h new file mode 100644 index 0000000..c046a4a --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h @@ -0,0 +1,32 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "IBuccaneerCommonModule.h" + +struct FMetricsCollection; + +class FBuccaneerCommonModule : public IBuccaneerCommonModule +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + virtual FReadyEvent& OnReady() override; + virtual bool IsReady() override; + virtual void SendMetrics(const FMetricsCollection& StatsCollection) override; + virtual void SendEvent(TSharedPtr JsonObject) override; + +private: + bool bModuleReady = false; + FReadyEvent ReadyEvent; + +private: + void SendHTTP(FString URL, TSharedPtr JsonObject); + void WriteJSON(FString FileName, TSharedPtr JsonObject); + void FormatMetadata(IConsoleVariable* Var); + + TSharedPtr MetadataJson = MakeShareable(new FJsonObject()); + FString CachedJSONOutputFileName; +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp new file mode 100644 index 0000000..e6acff7 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -0,0 +1,167 @@ +#include "BuccaneerMetrics.h" + +namespace +{ + // Helper function to format metric names to be Prometheus-compatible + // Valid Prometheus names must be alphanumeric or underscores + FString FormatMetricName(const FString& Name) + { + FString FormattedName = Name; + FormattedName.ReplaceInline(TEXT("-"), TEXT("_")); + FormattedName.ReplaceInline(TEXT("."), TEXT("_")); + FormattedName.ReplaceInline(TEXT(" "), TEXT("_")); + FormattedName.ReplaceInline(TEXT("/"), TEXT("_")); + FormattedName.ReplaceInline(TEXT("\\"), TEXT("_")); + FormattedName.ReplaceInline(TEXT("("), TEXT("")); + FormattedName.ReplaceInline(TEXT(")"), TEXT("")); + FormattedName.ReplaceInline(TEXT("\""), TEXT("")); + FormattedName.ReplaceInline(TEXT("'"), TEXT("")); + FormattedName.ReplaceInline(TEXT(">"), TEXT("")); + FormattedName.ReplaceInline(TEXT("<"), TEXT("")); + return FormattedName; + } +} + +TSharedPtr FMetricsCollection::ToJson() const +{ + + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + TSharedPtr MetricsJson = MakeShareable(new FJsonObject()); + + /** + * This is the format that the Buccaneer server wants from "single value" metrics. + * + * Single value metrics could be: + * - Global application stats (like framerate, memory usage, etc) + * - Gameplay stats that are not per-player (like play time, session unix timestamp, session id etc) + * - Any other type of stats where there is only one value for the stat + * + * "{MetricName}": { + * "description": "{StatDescription}", + * "value": {StatValue} + * } + * Example: + * "memory_used": { + * "description": "The memory used by the game", + * "value": 1024 + * } + */ + + // Build JSON object using the "single value metrics" we have stored in the FMetricsCollection + for (const FBuccaneerMetric &Stat : SingleValueMetrics) + { + FString MetricName = FormatMetricName(Stat.Name); + + TSharedPtr MetricJson = MakeShareable(new FJsonObject()); + MetricJson->SetStringField(TEXT("description"), Stat.Description); + MetricJson->SetNumberField(TEXT("value"), Stat.Value); + MetricsJson->SetObjectField(MetricName, MetricJson); + } + + /** + * This is the format that the Buccaneer server wants for grouped metrics. + * + * Grouped metrics could be: + * - Pixel Streaming peer stats (if we are using one of the Buccaneer Pixel Streaming plugins, each player/peer will have their own stats) + * - Per-player gameplay stats (if the game is local multiplayer for example and you extended Buccaneer to support that) + * - Any other type of stats where there are multiple entities each with their own value for the same stat + * + * "{MetricName}": { + * "description": "{StatDescription}", + * "value": [ + * { + * "{GroupId}": {StatValue} + * }, + * ] + * } + * Example: + * "bitrate": { + * "description": "The bitrate of the stream", + * "value": [ + * { + * "player0": 60 + * }, + * { + * "player1": 57 + * }, + * ] + * } + */ + + // We need to perform a transformation on our grouped metrics because it does not match the JSON format that the server expects. + // Specifically, the server is grouped by metric name, whereas our data structure is grouped by group id (e.g. player id). + + // We use this map to build up a JSON object for each stat name (key=stat name, value=JSON object) + TMap> GroupMetricsToJsonMap = TMap>(); + + // Build JSON using each of the multi-entry metrics (e.g. per player metrics) that we have stored in the FMetricsCollection + for (auto const& MetricGroup : GroupedMetrics) + { + FString GroupId = MetricGroup.Key; + const TArray& GroupMetricsArr = MetricGroup.Value; + + // Iterate each stored value within the current multi value metric (e.g. for FPS: player0's fps, player1's fps, etc) + // and store it the JSON we are building up + for (const FBuccaneerMetric& Metric : GroupMetricsArr) + { + FString MetricName = FormatMetricName(Metric.Name); + + if(!GroupMetricsToJsonMap.Contains(MetricName)) + { + // Make JSON for the `description` field + TSharedPtr MetricJson = MakeShareable(new FJsonObject()); + MetricJson->SetStringField(TEXT("description"), Metric.Description); + // Make JSON for the `value` field (which is a JSON array) + TArray> ValueArray; + + // Note: For these grouped metrics each value is stored in the JSON object like so { "GroupId": value } + TSharedPtr MetricsValueJson = MakeShareable(new FJsonObject()); + MetricsValueJson->SetNumberField(GroupId, Metric.Value); + ValueArray.Add(MakeShareable(new FJsonValueObject(MetricsValueJson))); + + // Set the `value` array on the MetricJson + MetricJson->SetArrayField(TEXT("value"), ValueArray); + + // Put the whole MetricJson object we just made into the map + // so we can add to it as we iterate through the other players + GroupMetricsToJsonMap.Add(MetricName, MetricJson); + } + else + { + // We have already created the MetricJson for this MetricName + // so just need to add to the `value` array + TSharedPtr MetricJson = GroupMetricsToJsonMap[MetricName]; + const TArray>* ValueArrayPtr; + if(MetricJson->TryGetArrayField(TEXT("value"), ValueArrayPtr)) + { + // Copy the existing array so we can modify it + TArray> ValueArray = *ValueArrayPtr; + + // Create a new JSON object for this player's value + TSharedPtr ValueJson = MakeShareable(new FJsonObject()); + ValueJson->SetNumberField(GroupId, Metric.Value); + + // Add it to the array + ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); + + // Set the modified array back + MetricJson->SetArrayField(TEXT("value"), ValueArray); + } + } + } + + } + + // Iterate the `GroupMetricsToJsonMap` and add each stat to the main MetricsJson + for (const auto& MetricEntry : GroupMetricsToJsonMap) + { + const FString& MetricName = MetricEntry.Key; + const TSharedPtr& MetricJson = MetricEntry.Value; + MetricsJson->SetObjectField(MetricName, MetricJson); + } + + JsonObject->SetObjectField(TEXT("metrics"), MetricsJson); + JsonObject->SetNumberField(TEXT("timestamp"), Timestamp); + + return JsonObject; +} diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp new file mode 100644 index 0000000..a137063 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp @@ -0,0 +1,583 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerSettings.h" + +#include "Logging.h" +#include "Misc/CommandLine.h" +#include "UObject/ReflectedTypeAccessors.h" + +namespace Util +{ + FString ConsoleVariableToCommandArgValue(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg for parsing + // we need to remove the . and add a "=" + return InCVarName.Replace(TEXT("."), TEXT("")).Append(TEXT("=")); + } + + FString ConsoleVariableToCommandArgParam(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg parameter, we need to to remove the . + return InCVarName.Replace(TEXT("."), TEXT("")); + } + + FString FindCVarFromProperty(const TSet> Set, const FString& Value) + { + for (const TPair& Pair : Set) + { + if (Pair.Value == Value) + { + return Pair.Key; + } + } + + return ""; + } + + void SetMetadataCVarFromProperty(UObject* This, FProperty* Property) + { + if (FMapProperty* MapProperty = CastField(Property)) + { + FString CVarString = ""; + + TMap& Map = *MapProperty->ContainerPtrToValuePtr>(This); + for (const TPair& Pair : Map) + { + if (Pair.Key.IsEmpty() || Pair.Value.IsEmpty()) + { + continue; + } + + CVarString += FString::Printf(TEXT("%s:%s;"), *Pair.Key, *Pair.Value); + } + + UBuccaneerSettings::CVarMetadata->Set(*CVarString, ECVF_SetByProjectSetting); + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [Buccaneer.Metadata] to [\"{1}\"] from Property [Metadata]", CVarString); + } + } + + void SetMetadataCVarAndPropertyFromValue(UObject* This, FProperty* Property, const FString& CmdValue) + { + UBuccaneerSettings::CVarMetadata->Set(*CmdValue, ECVF_SetByCommandline); + + if (FMapProperty* MapProperty = CastField(Property)) + { + TMap& Map = *MapProperty->ContainerPtrToValuePtr>(This); + + TArray Pairs; + CmdValue.ParseIntoArray(Pairs, TEXT(";"), true); + for (FString Pair : Pairs) + { + if (Pair.IsEmpty()) + { + continue; + } + + FString Key, Value; + Pair.Split(TEXT(":"), &Key, &Value); + if(!Key.IsEmpty() && !Value.IsEmpty()) + { + Map.Add(Key, Value); + } + } + } + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [Buccaneer.Metadata] and Property [Metadata] to [{0}] from command line", CmdValue); + } +} + +static const TSet> GetCmdArg = { + { "Buccaneer.URL", "URL" }, + { "Buccaneer.ID", "ID" }, + { "Buccaneer.EnableStats", "EnableStats" }, + { "Buccaneer.EnableEvents", "EnableEvents" }, + { "Buccaneer.Metadata", "Metadata" }, + { "Buccaneer.ReportingInterval", "ReportingInterval" }, + { "Buccaneer.EnableJSONOutput", "EnableJSONOutput" }, + { "Buccaneer.JSONOutputDirectory", "JSONOutputDirectory" }, + { "Buccaneer.JSONOutputFile", "JSONOutputFile" } +}; + +// Map a legacy cvar to its new property +static const TSet> GetLegacyCmdArg = { + { "Buccaneer.IP", "URL" }, // Moved to URL + { "Buccaneer.Port", "URL" } // Moved to URL +}; + +TAutoConsoleVariable UBuccaneerSettings::CVarURL( + TEXT("Buccaneer.URL"), + TEXT(""), + TEXT("URL to send stats and events to. This should be a URL to a Buccaneer Server"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarID( + TEXT("Buccaneer.ID"), + TEXT(""), + TEXT("ID to identify this instance. Defaults to a new GUID"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarEnableStats( + TEXT("Buccaneer.EnableStats"), + true, + TEXT("Enables the collection of performance metrics (default: true)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarEnableEvents( + TEXT("Buccaneer.EnableEvents"), + true, + TEXT("Enables the collection of semantic events (default: true)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarMetadata( + TEXT("Buccaneer.Metadata"), + TEXT(""), + TEXT(""), + FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* Var) { Delegates()->OnMetadataChanged.Broadcast(Var); }), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarReportingInterval( + TEXT("Buccaneer.ReportingInterval"), + 1.0f, + TEXT("The interval at which to report performance metrics (default: 1.0 seconds)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarEnableJSONOutput( + TEXT("Buccaneer.EnableJSONOutput"), + false, + TEXT("Enables writing stats and events to a JSON file (default: false)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarJSONOutputDirectory( + TEXT("Buccaneer.JSONOutputDirectory"), + TEXT(""), + TEXT("The directory to write JSON files to"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarJSONOutputFile( + TEXT("Buccaneer.JSONOutputFile"), + TEXT(""), + TEXT("The filename for JSON output (default: __Stats.json)"), + ECVF_Default); + +UBuccaneerSettings::FDelegates* UBuccaneerSettings::DelegateSingleton = nullptr; + +UBuccaneerSettings::FDelegates* UBuccaneerSettings::Delegates() +{ + if (DelegateSingleton == nullptr && !IsEngineExitRequested()) + { + DelegateSingleton = new UBuccaneerSettings::FDelegates(); + return DelegateSingleton; + } + return DelegateSingleton; +} + +UBuccaneerSettings::~UBuccaneerSettings() +{ + DelegateSingleton = nullptr; +} + +TMap UBuccaneerSettings::GetMetadata() +{ + TMap MetadataMap; + + FString MetadataString = CVarMetadata.GetValueOnAnyThread(); + if (!MetadataString.IsEmpty()) + { + TArray Pairs; + MetadataString.ParseIntoArray(Pairs, TEXT(";"), true); + for (FString Pair : Pairs) + { + if (Pair.IsEmpty()) + { + continue; + } + + FString Key, Value; + Pair.Split(TEXT(":"), &Key, &Value); + if(!Key.IsEmpty() && !Value.IsEmpty()) + { + MetadataMap.Add(Key, Value); + } + } + } + + return MetadataMap; +} + +FName UBuccaneerSettings::GetCategoryName() const +{ + return TEXT("Plugins"); +} + +#if WITH_EDITOR +FText UBuccaneerSettings::GetSectionText() const +{ + return NSLOCTEXT("BuccaneerPlugin", "BuccaneerSettingsSection", "Buccaneer"); +} + +void UBuccaneerSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FString PropertyName = PropertyChangedEvent.Property->GetNameCPP(); + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CVarName.IsEmpty()) + { + if (PropertyName == "Metadata") + { + Util::SetMetadataCVarFromProperty(this, PropertyChangedEvent.Property); + } + else + { + SetCVarFromProperty(CVarName, PropertyChangedEvent.Property); + } + } +} +#endif + +void UBuccaneerSettings::SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + ByteProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + int64 EnumIndex = EnumProperty->GetEnum()->GetIndexByNameString(Value.Replace(TEXT("_"), TEXT(""))); + if (EnumIndex != INDEX_NONE) + { + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex), ECVF_SetByCommandline); + + FNumericProperty* UnderlyingProp = EnumProperty->GetUnderlyingProperty(); + int64* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + *PropertyAddress = EnumProperty->GetEnum()->GetValueByIndex(EnumIndex); + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex)); + } + else + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "{0} is not a valid enum value for {1}", Value, EnumProperty->GetEnum()->CppType); + } + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + bool bValue = false; + if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) + { + bValue = true; + } + else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) + { + bValue = false; + } + CVar->Set(bValue, ECVF_SetByCommandline); + BoolProperty->SetPropertyValue_InContainer(this, bValue); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), bValue); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + IntProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FCString::Atof(*Value), ECVF_SetByCommandline); + FloatProperty->SetPropertyValue_InContainer(this, FCString::Atof(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atof(*Value)); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + StringProperty->SetPropertyValue_InContainer(this, *Value); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + NameProperty->SetPropertyValue_InContainer(this, FName(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + CVar->Set(*Value, ECVF_SetByCommandline); + + TArray StringArray; + Value.ParseIntoArray(StringArray, TEXT(","), true); + + TArray& Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + Array = StringArray; + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } +} + +void UBuccaneerSettings::SetCVarFromProperty(const FString& CVarName, FProperty* Property) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(ByteProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, ByteProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + void* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + int64 CurrentValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(PropertyAddress); + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), Property->GetNameCPP()); + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + CVar->Set(BoolProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, BoolProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(IntProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, IntProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FloatProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, FloatProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*StringProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, StringProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*NameProperty->GetPropertyValue_InContainer(this).ToString(), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, NameProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + TArray Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + CVar->Set(*FString::Join(Array, TEXT(",")), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, FString::Join(Array, TEXT(",")), Property->GetNameCPP()); + } +} + +void UBuccaneerSettings::InitializeCVarsFromProperties() +{ + UE_LOGFMT(LogBuccaneerCommon, Log, "Initializing CVars from ini"); + for (FProperty* Property = GetClass()->PropertyLink; Property; Property = Property->PropertyLinkNext) + { + if (!Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + // Handle the majority of commandline argument + if (Property->GetNameCPP() == "Metadata") + { + Util::SetMetadataCVarFromProperty(this, Property); + continue; + } + + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, Property->GetNameCPP()); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, Property); + continue; + } + } +} + +void UBuccaneerSettings::ValidateCommandLineArgs() +{ + FString CommandLine = FCommandLine::Get(); + + TArray CommandArray; + CommandLine.ParseIntoArray(CommandArray, TEXT(" "), true); + + for (FString Command : CommandArray) + { + Command.RemoveFromStart(TEXT("-")); + if (!Command.StartsWith("Buccaneer")) + { + continue; + } + + // Get the pure command line arg from an arg that contains an '=', eg BuccaneerURL= + FString CurrentCommandLineArg = Command; + if (Command.Contains("=")) + { + Command.Split(TEXT("="), &CurrentCommandLineArg, nullptr); + } + + bool bValidArg = false; + for (const TPair& Pair : GetCmdArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + + if (!bValidArg) + { + for (const TPair& Pair : GetLegacyCmdArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + } + + if (!bValidArg) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Unknown Buccaneer command line arg: {0}", CurrentCommandLineArg); + } + } +} + +void UBuccaneerSettings::ParseCommandlineArgs() +{ + UE_LOGFMT(LogBuccaneerCommon, Verbose, "Updating CVars and properties with command line args"); + for (const TPair& Pair : GetCmdArg) + { + FString CVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + if (PropertyName == "Metadata") + { + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + Util::SetMetadataCVarAndPropertyFromValue(this, Property, ConsoleString); + } + continue; + } + + // Handle a directly parsable commandline + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(CVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(CVarString))) + { + SetCVarAndPropertyFromValue(CVarString, Property, TEXT("true")); + } + } +} + +void UBuccaneerSettings::ParseLegacyCommandlineArgs() +{ + FString BuccaneerIP; + FString BuccaneerPort; + + for (const TPair& Pair : GetLegacyCmdArg) + { + FString LegacyCVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + FString NewCVarString; + if (FString CmdArgCVar = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CmdArgCVar.IsEmpty()) + { + NewCVarString = CmdArgCVar; + } + else + { + continue; + } + + if (LegacyCVarString == "Buccaneer.IP" || LegacyCVarString == "Buccaneer.Port") + { + if (LegacyCVarString == "Buccaneer.IP") + { + FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(LegacyCVarString), BuccaneerIP); + } + else if (LegacyCVarString == "Buccaneer.Port") + { + FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(LegacyCVarString), BuccaneerPort); + } + + if (!BuccaneerIP.IsEmpty() && !BuccaneerPort.IsEmpty()) + { + FString LegacyUrl = TEXT("http://") + BuccaneerIP + TEXT(":") + BuccaneerPort; + SetCVarAndPropertyFromValue(NewCVarString, Property, LegacyUrl); + UE_LOGFMT(LogBuccaneerCommon, Warning, "BuccaneerIP and BuccaneerPort are legacy settings converted to -BuccaneerURL={0}", CVarURL.GetValueOnAnyThread()); + } + + continue; + } + + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(LegacyCVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(NewCVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(LegacyCVarString))) + { + SetCVarAndPropertyFromValue(NewCVarString, Property, TEXT("true")); + } + else + { + continue; + } + + UE_LOGFMT(LogBuccaneerCommon, Warning, "{0} is a legacy setting and has been converted to {1}", Util::ConsoleVariableToCommandArgParam(LegacyCVarString), Util::ConsoleVariableToCommandArgParam(NewCVarString)); + } + + // End legacy buccaneer command line args +} + +void UBuccaneerSettings::PostInitProperties() +{ + Super::PostInitProperties(); + + UE_LOGFMT(LogBuccaneerCommon, Log, "Initialising Buccaneer settings."); + + // Set all the CVars to reflect the state of the ini + InitializeCVarsFromProperties(); + + // Validate command line args to log if they're invalid + ValidateCommandLineArgs(); + + // Update CVars and properties based on command line args + ParseCommandlineArgs(); + + // Handle parsing of legacy command line args (such as -PixelStreamingIP) after .ini and new commandline args. + ParseLegacyCommandlineArgs(); +} \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.cpp new file mode 100644 index 0000000..fac5a79 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneerCommon); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.h new file mode 100644 index 0000000..ed6022d --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.h @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneerCommon, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h deleted file mode 100644 index 2c01570..0000000 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "Modules/ModuleManager.h" -#include "Interfaces/IHttpRequest.h" -#include "Interfaces/IHttpResponse.h" -#include "HttpModule.h" -#include "Dom/JsonObject.h" -#include "Serialization/JsonWriter.h" -#include "HAL/IConsoleManager.h" -#include "Misc/CommandLine.h" - -#include -#include - -DECLARE_MULTICAST_DELEGATE(FOnSetupComplete); - -DECLARE_LOG_CATEGORY_EXTERN(BuccaneerCommon, Log, All); - -class BUCCANEERCOMMON_API FBuccaneerCommonModule : public IModuleInterface -{ -public: - /** IModuleInterface implementation */ - virtual void StartupModule() override; - virtual void ShutdownModule() override; - - static FBuccaneerCommonModule *GetModule(); - static void ParseCommandLineOption(const TCHAR *Match, IConsoleVariable *CVar); - - void SendStats(TSharedPtr JsonObject); - void SendEvent(TSharedPtr JsonObject); - - FOnSetupComplete SetupComplete; - - IConsoleVariable *CVarBuccaneerEnableStats; - IConsoleVariable *CVarBuccaneerEnableEvents; - -private: - void Setup(); - - void SendHTTP(FString URL, TSharedPtr JsonObject); - - FString BuccaneerURL; - FString InstanceID; - TSharedPtr MetadataJson; - - static FBuccaneerCommonModule *BuccaneerCommonModule; -}; diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h new file mode 100644 index 0000000..b041447 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h @@ -0,0 +1,42 @@ +/** + * @file BuccaneerStats.h + * @brief Defines the data structures for collecting and storing statistics. + */ + +#pragma once + +#include "Dom/JsonObject.h" + +/** + * @struct FBuccaneerMetric + * @brief Represents a single metric, including its name, description, and value. + */ +struct FBuccaneerMetric +{ + FString Name; + FString Description; + double Value; +}; + +/** + * @struct FMetricsCollection + * @brief Represents a collection of metrics captured at a specific moment in time. + */ +struct FMetricsCollection +{ + double Timestamp; + + // These metrics have a single value each. + // They can be used to record things such as global metrics, application-wide stats, session stats, etc. + TArray SingleValueMetrics; + + // These metrics are stored in logical groups by unique ids (key: player id, value: array of metrics). + // For example, they could be used for per-peer metrics for Pixel Streaming or per-player metrics in a local multiplayer game. + TMap> GroupedMetrics; + + /** + * @brief Converts the FMetricsCollection to a nested FJsonObject. + * @return A TSharedPtr to the created FJsonObject. + */ + TSharedPtr ToJson() const; +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h new file mode 100644 index 0000000..b077786 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h @@ -0,0 +1,128 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Containers/UnrealString.h" +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" + +#include "BuccaneerSettings.generated.h" + +namespace Util +{ + BUCCANEERCOMMON_API FString ConsoleVariableToCommandArgValue(const FString InCVarName); + + BUCCANEERCOMMON_API FString ConsoleVariableToCommandArgParam(const FString InCVarName); + + BUCCANEERCOMMON_API FString FindCVarFromProperty(const TSet> Set, const FString& Value); +} + +// Config loaded/saved to an .ini file. +// It is also exposed through the plugin settings page in editor. +UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Buccaneer")) +class BUCCANEERCOMMON_API UBuccaneerSettings : public UDeveloperSettings +{ + GENERATED_BODY() + + virtual ~UBuccaneerSettings(); + +public: + static TAutoConsoleVariable CVarURL; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "URL", + ToolTip = "URL to send stats and events to. This should be a URL to a Buccaneer Server" + )) + FString URL; + + static TAutoConsoleVariable CVarID; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "ID", + ToolTip = "ID to identify this instance. Auto-generates a short GUID if not provided" + )) + FString ID; + + static TAutoConsoleVariable CVarEnableStats; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Enable Stats", + ToolTip = "Enables the collection of performance metrics" + )) + bool EnableStats = true; + + static TAutoConsoleVariable CVarEnableEvents; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Enable Events", + ToolTip = "Enables the collection of semantic events" + )) + bool EnableEvents = true; + + static TAutoConsoleVariable CVarMetadata; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Metadata", + ToolTip = "Key:Value pairs of metadata to send with this instance", + ForceInlineRow + )) + TMap Metadata; + + static TMap GetMetadata(); + + static TAutoConsoleVariable CVarReportingInterval; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Reporting Interval (seconds)", + ToolTip = "The interval at which to report performance metrics. <= 0 disables reporting" + )) + float ReportingInterval = 1.0f; + + static TAutoConsoleVariable CVarEnableJSONOutput; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Enable JSON Output", + ToolTip = "Enables writing stats and events to a JSON file" + )) + bool EnableJSONOutput = false; + + static TAutoConsoleVariable CVarJSONOutputDirectory; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "JSON Output Directory", + ToolTip = "The directory to write JSON files to" + )) + FString JSONOutputDirectory = FPaths::ProjectLogDir(); + + static TAutoConsoleVariable CVarJSONOutputFile; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "JSON Output File", + ToolTip = "The filename for JSON output (default: _Stats.json)" + )) + FString JSONOutputFile; + + // Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + +#if WITH_EDITOR + virtual FText GetSectionText() const override; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + // End UDeveloperSettings Interface + + // Begin UObject Interface + virtual void PostInitProperties() override; + // End UObject Interface + + struct FDelegates + { + DECLARE_TS_MULTICAST_DELEGATE_OneParam(FOnMetadataChanged, IConsoleVariable*); + FOnMetadataChanged OnMetadataChanged; + }; + + static FDelegates* Delegates(); + +private: + void SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value); + void SetCVarFromProperty(const FString& CVarName, FProperty* Property); + + void InitializeCVarsFromProperties(); + void ValidateCommandLineArgs(); + void ParseCommandlineArgs(); + void ParseLegacyCommandlineArgs(); + + static FDelegates* DelegateSingleton; + +}; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h new file mode 100644 index 0000000..ea46d0c --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h @@ -0,0 +1,61 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" +#include "BuccaneerMetrics.h" + +class BUCCANEERCOMMON_API IBuccaneerCommonModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneerCommonModule& Get() + { + return FModuleManager::LoadModuleChecked("BuccaneerCommon"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("BuccaneerCommon"); + } + + /** + * Event fired when internal streamer is initialized and the methods on this module are ready for use. + */ + DECLARE_EVENT_OneParam(IBuccaneerCommonModule, FReadyEvent, IBuccaneerCommonModule&); + + /** + * A getter for the OnReady event. Intent is for users to call IBuccaneerCommonModule::Get().OnReady().AddXXX. + * @return The bindable OnReady event. + */ + virtual FReadyEvent& OnReady() = 0; + + /** + * Is the BuccaneerCommon module actually ready to use? Is the streamer created. + * @return True if BuccaneerCommon module methods are ready for use. + */ + virtual bool IsReady() = 0; + + /** + * + */ + virtual void SendMetrics(const FMetricsCollection& StatsCollection) = 0; + + /** + * + */ + virtual void SendEvent(TSharedPtr JsonObject) = 0; +}; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/SemanticEventEmitter.build.cs b/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs similarity index 64% rename from Plugins/Buccaneer/Source/SemanticEventEmitter/SemanticEventEmitter.build.cs rename to Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs index 93fd52a..ef9a82f 100644 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/SemanticEventEmitter.build.cs +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs @@ -1,10 +1,10 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. using UnrealBuildTool; -public class SemanticEventEmitter : ModuleRules +public class BuccaneerEvents : ModuleRules { - public SemanticEventEmitter(ReadOnlyTargetRules Target) : base(Target) + public BuccaneerEvents(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.cpp b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.cpp new file mode 100644 index 0000000..85d87e6 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.cpp @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerEventsBlueprintFunctionLibrary.h" + +void UBuccaneerEventsBlueprintFunctionLibrary::EmitEvent(FString Level, FString Event) +{ + IBuccaneerEventsModule::Get().EmitEvent(Level, Event); +} \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.h new file mode 100644 index 0000000..7eca779 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.h @@ -0,0 +1,18 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "IBuccaneerEventsModule.h" + +#include "BuccaneerEventsBlueprintFunctionLibrary.generated.h" + +UCLASS() +class BUCCANEEREVENTS_API UBuccaneerEventsBlueprintFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() +public: + UFUNCTION(BlueprintCallable, Category="Buccaneer") + static void EmitEvent(FString Level, FString Event); +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp new file mode 100644 index 0000000..d4fd99f --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp @@ -0,0 +1,35 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerEventsModule.h" + +#include "CoreMinimal.h" +#include "Logging.h" +#include "Dom/JsonObject.h" +#include "IBuccaneerCommonModule.h" +#include "BuccaneerSettings.h" + +void FBuccaneerEventsModule::StartupModule() +{ +} + +void FBuccaneerEventsModule::ShutdownModule() +{ +} + +void FBuccaneerEventsModule::EmitEvent(FString Level, FString Event) +{ + if (!UBuccaneerSettings::CVarEnableEvents.GetValueOnAnyThread()) + { + return; + } + + UE_LOGFMT(LogBuccaneerEvents, Verbose, "{0}: {1}", Level, Event); + + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + JsonObject->SetField("level", MakeShared(Level)); + JsonObject->SetField("message", MakeShared(Event)); + + IBuccaneerCommonModule::Get().SendEvent(JsonObject); +} + +IMPLEMENT_MODULE(FBuccaneerEventsModule, BuccaneerEvents) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.h new file mode 100644 index 0000000..0c93c41 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.h @@ -0,0 +1,14 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "IBuccaneerEventsModule.h" + +class FBuccaneerEventsModule : public IBuccaneerEventsModule +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + virtual void EmitEvent(FString Level, FString Event) override; +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.cpp b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.cpp new file mode 100644 index 0000000..fc06cc9 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneerEvents); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.h new file mode 100644 index 0000000..4212d77 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.h @@ -0,0 +1,10 @@ + + +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneerEvents, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Public/IBuccaneerEventsModule.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Public/IBuccaneerEventsModule.h new file mode 100644 index 0000000..bc34f0f --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Public/IBuccaneerEventsModule.h @@ -0,0 +1,36 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class BUCCANEEREVENTS_API IBuccaneerEventsModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneerEventsModule& Get() + { + return FModuleManager::LoadModuleChecked("BuccaneerEvents"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("BuccaneerEvents"); + } + + /** + * + */ + virtual void EmitEvent(FString Level, FString Event) = 0; +}; diff --git a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs b/Plugins/Buccaneer/Source/BuccaneerStats/BuccaneerStats.build.cs similarity index 66% rename from Plugins/Buccaneer/Source/TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs rename to Plugins/Buccaneer/Source/BuccaneerStats/BuccaneerStats.build.cs index 1a30dc7..3752d28 100644 --- a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs +++ b/Plugins/Buccaneer/Source/BuccaneerStats/BuccaneerStats.build.cs @@ -1,10 +1,10 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. using UnrealBuildTool; -public class TimeSeriesDataEmitter : ModuleRules +public class BuccaneerStats : ModuleRules { - public TimeSeriesDataEmitter(ReadOnlyTargetRules Target) : base(Target) + public BuccaneerStats(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; @@ -17,7 +17,7 @@ public TimeSeriesDataEmitter(ReadOnlyTargetRules Target) : base(Target) "Slate", "SlateCore", "RenderCore", - "SemanticEventEmitter", + "BuccaneerEvents", "Json" }); diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp new file mode 100644 index 0000000..9c8c522 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -0,0 +1,144 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerStatsModule.h" + +#include "BuccaneerSettings.h" +#include "CoreMinimal.h" +#include "Engine/Engine.h" +#include "IBuccaneerEventsModule.h" +#include "Logging.h" +#include "RHI.h" +#include "Stats/Stats.h" +#include "Stats/StatsData.h" +#include "Math/UnrealMathUtility.h" +#include "BuccaneerMetrics.h" + +#define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ + ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; + +void FBuccaneerStatsModule::StartupModule() +{ + LastTickTime = InterimStart = FPlatformTime::Seconds(); +} + +void FBuccaneerStatsModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +bool FBuccaneerStatsModule::IsTickableWhenPaused() const +{ + return true; +} + +bool FBuccaneerStatsModule::IsTickableInEditor() const +{ + return true; +} + +void FBuccaneerStatsModule::Tick(float DeltaTime) +{ + if (!UBuccaneerSettings::CVarEnableStats.GetValueOnAnyThread() || UBuccaneerSettings::CVarReportingInterval.GetValueOnAnyThread() <= 0) + { + // Performance profiling hasn't been inititialized. Don't continue + return; + } + + double NowTime = FPlatformTime::Seconds(); + + InterimFrameCount++; + double FrameTime = NowTime - LastTickTime; + // Ignore frames that take longer than 250ms. Count these as a hang + if (FrameTime > 0.25) + { + InterimHangCount++; + IBuccaneerEventsModule::Get().EmitEvent(TEXT("warning"), TEXT("Frame hung")); + } + else + { + // Get application resolution + if(GEngine && GEngine->GameViewport && GEngine->GameViewport->Viewport) + { + const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY()); + ResolutionX = ViewportSize.X; + ResolutionY = ViewportSize.Y; + } + + double GameThreadTime = FPlatformTime::ToMilliseconds(GGameThreadTime); + double GPUFrameTime = FPlatformTime::ToMilliseconds(RHIGetGPUFrameCycles(0)); + double RenderThreadTime = FPlatformTime::ToMilliseconds(GRenderThreadTime); + double RHIThreadTime = FPlatformTime::ToMilliseconds(GRHIThreadTime); + + InterimMeanFrameTime = COMPUTE_MEAN(InterimMeanFrameTime, FrameTime * 1000, InterimFrameCount); + InterimMeanGameThreadTime = COMPUTE_MEAN(InterimMeanGameThreadTime, GameThreadTime, InterimFrameCount); + InterimMeanGPUTime = COMPUTE_MEAN(InterimMeanGPUTime, GPUFrameTime, InterimFrameCount); + InterimMeanRenderThreadTime = COMPUTE_MEAN(InterimMeanRenderThreadTime, RenderThreadTime, InterimFrameCount); + InterimMeanRHIThreadTime = COMPUTE_MEAN(InterimMeanRHIThreadTime, RHIThreadTime, InterimFrameCount); + + ComputeUsedMemory(); + } + if ((NowTime - InterimStart) >= UBuccaneerSettings::CVarReportingInterval.GetValueOnAnyThread()) + { + PushStats(); + InterimStart = NowTime; + InterimHangCount = 0; + InterimFrameCount = 1; + } + LastTickTime = NowTime; +} + +void FBuccaneerStatsModule::ComputeUsedMemory() +{ + FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats(); + + const unsigned int BytesPerMB = (1024u * 1024u); + UsedVirtualMemory = static_cast(MemoryStats.UsedVirtual) / BytesPerMB; + UsedPhysicalMemory = static_cast(MemoryStats.UsedPhysical) / BytesPerMB; + +#if !UE_BUILD_SHIPPING + TArray Metrics; + GetPermanentStats(Metrics); + + FName NAME_STATGROUP_RHI(FStatGroup_STATGROUP_RHI::GetGroupName()); + int64 TotalMemory = 0; + for (int32 Index = 0; Index < Metrics.Num(); Index++) + { + FStatMessage const &Meta = Metrics[Index]; + FName LastGroup = Meta.NameAndInfo.GetGroupName(); + if (LastGroup == NAME_STATGROUP_RHI && Meta.NameAndInfo.GetFlag(EStatMetaFlags::IsMemory)) + { + TotalMemory += Meta.GetValue_int64(); + } + } + UsedGPUMemory = (double)(TotalMemory / 1024.f / 1024.f); +#endif +} + +void FBuccaneerStatsModule::PushStats() +{ + FMetricsCollection MetricsCollection; + MetricsCollection.Timestamp = IBuccaneerStatsModule::GetStatsTimestamp(); + MetricsCollection.SingleValueMetrics.Add({"mean_fps", "The average fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f}); + MetricsCollection.SingleValueMetrics.Add({"mean_frametime", "The average frametime", InterimMeanFrameTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_gamethreadtime", "The average game thread time", InterimMeanGameThreadTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_gputime", "The average gpu time", InterimMeanGPUTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_rendertime", "The average render thread time", InterimMeanRenderThreadTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_rhithreadtime", "The average rhi thread time", InterimMeanRHIThreadTime}); + MetricsCollection.SingleValueMetrics.Add({"memory_virtual", "The virtual memory usage", UsedVirtualMemory}); + MetricsCollection.SingleValueMetrics.Add({"memory_physical", "The physical memory usage", UsedPhysicalMemory}); + MetricsCollection.SingleValueMetrics.Add({"memory_gpu", "The gpu memory usage", UsedGPUMemory}); + MetricsCollection.SingleValueMetrics.Add({"num_hangs", "The number of frames hung in the recording interval", (double)InterimHangCount}); + MetricsCollection.SingleValueMetrics.Add({"res_x", "The width of the application", ResolutionX}); + MetricsCollection.SingleValueMetrics.Add({"res_y", "The height of the application", ResolutionY}); + IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); +} + +TStatId FBuccaneerStatsModule::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(FBuccaneerStatsModule, STATGROUP_Tickables); +} + +#undef COMPUTE_MEAN + +IMPLEMENT_MODULE(FBuccaneerStatsModule, BuccaneerStats) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h similarity index 70% rename from Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h rename to Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index f2b9bc4..014486a 100644 --- a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -1,16 +1,15 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. #pragma once #include "CoreMinimal.h" +#include "Dom/JsonObject.h" +#include "IBuccaneerCommonModule.h" +#include "IBuccaneerStatsModule.h" #include "Modules/ModuleManager.h" -#include "BuccaneerCommon.h" #include "Tickable.h" -#include "Dom/JsonObject.h" - -DECLARE_LOG_CATEGORY_EXTERN(TimeSeriesDataEmitter, Log, All); -class TIMESERIESDATAEMITTER_API FTimeSeriesDataEmitterModule : public IModuleInterface, public FTickableGameObject +class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGameObject { public: /** IModuleInterface implementation */ @@ -24,14 +23,12 @@ class TIMESERIESDATAEMITTER_API FTimeSeriesDataEmitterModule : public IModuleInt TStatId GetStatId() const override; private: - void PushStatsHTTP(); + void PushStats(); void ComputeUsedMemory(); - void UpdateMetric(FString Name, double Value); // Time keeping variables double LastTickTime = 0.0; double InterimStart = 0.0; - double InterimDuration = 1.0; // Rolling average of times recorded during the defined period double InterimMeanFrameTime = 0.0; double InterimMeanGameThreadTime = 0.0; @@ -46,10 +43,7 @@ class TIMESERIESDATAEMITTER_API FTimeSeriesDataEmitterModule : public IModuleInt // (using an unsigned int as there shouldn't be more than 4.2 million hangs during a time period, and if there is you have bigger problems) double InterimHangCount = 0.0; uint32 InterimFrameCount = 1; - - // Variable for storing logging URL and logging object - TSharedPtr JsonObject; - TSharedPtr MetricJson; - - TMap StatDescriptionMap; -}; + // Resolution of the application + double ResolutionX = 0.0; + double ResolutionY = 0.0; +}; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.cpp new file mode 100644 index 0000000..b2374da --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneerStats); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.h new file mode 100644 index 0000000..eef7f4e --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.h @@ -0,0 +1,10 @@ + + +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneerStats, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h new file mode 100644 index 0000000..9fad7da --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h @@ -0,0 +1,42 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class BUCCANEERSTATS_API IBuccaneerStatsModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneerStatsModule& Get() + { + return FModuleManager::LoadModuleChecked("BuccaneerStats"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("BuccaneerStats"); + } + + /* + * Get the timestamp for the purposes of stats reporting (so we all use the same clock/timekeeping system) + */ + static int64 GetStatsTimestamp() + { + // Return platform timestamp in milliseconds + return FMath::RoundToInt64((FPlatformTime::Seconds()) * 1000); + } +}; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp b/Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp deleted file mode 100644 index 005f0bd..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "SemanticEventEmitter.h" -#include "CoreMinimal.h" -#include "HAL/IConsoleManager.h" -#include "Logging/LogMacros.h" -#include "Dom/JsonObject.h" -#include "BuccaneerCommon.h" - -#define LOCTEXT_NAMESPACE "SemanticEventEmitterModule" - -DEFINE_LOG_CATEGORY(SemanticEventEmitter); - -FSemanticEventEmitterModule *FSemanticEventEmitterModule::SemanticEmitterModule = nullptr; - -void FSemanticEventEmitterModule::StartupModule() -{ - -} - -void FSemanticEventEmitterModule::ShutdownModule() -{ -} - -void FSemanticEventEmitterModule::EmitSemanticEvent(FString Level, FString Event) -{ - if (!FBuccaneerCommonModule::GetModule()->CVarBuccaneerEnableEvents->GetBool()) - { - return; - } - - UE_LOG(SemanticEventEmitter, Verbose, TEXT("%s: %s"), *Level, *Event); - - TSharedPtr JsonObject = MakeShareable(new FJsonObject()); - JsonObject->SetField("level", MakeShared((TEXT("%s"), *Level))); - JsonObject->SetField("message", MakeShared((TEXT("%s"), *Event))); - - FBuccaneerCommonModule::GetModule()->SendEvent(JsonObject); -} - -FSemanticEventEmitterModule *FSemanticEventEmitterModule::GetModule() -{ - if (SemanticEmitterModule) - { - return SemanticEmitterModule; - } - FSemanticEventEmitterModule *Module = FModuleManager::Get().GetModulePtr("SemanticEventEmitter"); - if (Module) - { - SemanticEmitterModule = Module; - } - return SemanticEmitterModule; -} - -#undef LOCTEXT_NAMESPACE - -IMPLEMENT_MODULE(FSemanticEventEmitterModule, SemanticEventEmitter) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp b/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp deleted file mode 100644 index 7e8e81d..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "SemanticEventBlueprintFunctionLibrary.h" - - -void USemanticEventEmitterBlueprintLibrary::EmitSemanticEvent(FString Level, FString Event) -{ - FSemanticEventEmitterModule* Module = FSemanticEventEmitterModule::GetModule(); - if(Module) - { - Module->EmitSemanticEvent(Level, Event); - } -} \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h b/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h deleted file mode 100644 index e2f78c4..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "SemanticEventEmitter.h" -#include "Kismet/BlueprintFunctionLibrary.h" -#include "SemanticEventBlueprintFunctionLibrary.generated.h" - -UCLASS() -class SEMANTICEVENTEMITTER_API USemanticEventEmitterBlueprintLibrary : public UBlueprintFunctionLibrary -{ - GENERATED_BODY() -public: - UFUNCTION(BlueprintCallable, Category="Buccaneer") - static void EmitSemanticEvent(FString Level, FString Event); -}; diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h b/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h deleted file mode 100644 index 7a6ec38..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Modules/ModuleManager.h" - - -DECLARE_LOG_CATEGORY_EXTERN(SemanticEventEmitter, Log, All); - -class SEMANTICEVENTEMITTER_API FSemanticEventEmitterModule : public IModuleInterface -{ -public: - /** IModuleInterface implementation */ - virtual void StartupModule() override; - virtual void ShutdownModule() override; - void EmitSemanticEvent(FString Level, FString Event); - static FSemanticEventEmitterModule *GetModule(); - -private: - static FSemanticEventEmitterModule *SemanticEmitterModule; -}; diff --git a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp b/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp deleted file mode 100644 index f2a0bb9..0000000 --- a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "TimeSeriesDataEmitter.h" -#include "CoreMinimal.h" -#include "Engine/Engine.h" -#include "RHI.h" -#include "SemanticEventEmitter.h" -#include "BuccaneerCommon.h" -#include "Stats/Stats.h" -#include "Stats/StatsData.h" -#define LOCTEXT_NAMESPACE "FTimeSeriesDataEmitterModule" - -#define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ - ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; - -DEFINE_LOG_CATEGORY(TimeSeriesDataEmitter); - -void FTimeSeriesDataEmitterModule::StartupModule() -{ - StatDescriptionMap = { - { "mean_fps", "The average fps" }, - { "mean_frametime", "The average frametime" }, - { "mean_gamethreadtime", "The average game thread time" }, - { "mean_gputime", "The average gpu time" }, - { "mean_rendertime", "The average render thread time" }, - { "mean_rhithreadtime", "The average rhi thread time" }, - { "memory_virtual", "The virtual memory usage" }, - { "memory_physical", "The physical memory usage" }, - { "memory_gpu", "The gpu memory usage" }, - { "num_hangs", "The number of frames hung in the recording interval" } - }; - - MetricJson = MakeShareable(new FJsonObject()); - JsonObject = MakeShareable(new FJsonObject()); - JsonObject->SetField(TEXT("metrics"), MakeShared(MetricJson)); - - LastTickTime = InterimStart = FPlatformTime::Seconds(); -} - -void FTimeSeriesDataEmitterModule::UpdateMetric(FString Name, double Value) -{ - if(!StatDescriptionMap.Contains(Name)) - { - UE_LOG(TimeSeriesDataEmitter, Log, TEXT("No description for metric (%s)"), *Name); - return; - } - - TSharedPtr MetricInfoJson = MakeShareable(new FJsonObject()); - MetricInfoJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[Name]))); - MetricInfoJson->SetField("value", MakeShared(Value)); - MetricJson->SetField(*Name, MakeShared(MetricInfoJson)); -} - -void FTimeSeriesDataEmitterModule::ShutdownModule() -{ - // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, - // we call this function before unloading the module. -} - -bool FTimeSeriesDataEmitterModule::IsTickableWhenPaused() const -{ - return true; -} - -bool FTimeSeriesDataEmitterModule::IsTickableInEditor() const -{ - return true; -} - -void FTimeSeriesDataEmitterModule::Tick(float DeltaTime) -{ - if (!FBuccaneerCommonModule::GetModule()->CVarBuccaneerEnableStats->GetBool()) - { - // Performance profiling hasn't been inititialized. Don't continue - return; - } - - double NowTime = FPlatformTime::Seconds(); - - InterimFrameCount++; - double FrameTime = NowTime - LastTickTime; - // Ignore frames that take longer than 250ms. Count these as a hang - if (FrameTime > 0.25) - { - InterimHangCount++; - FSemanticEventEmitterModule *Module = FSemanticEventEmitterModule::GetModule(); - if (Module) - { - Module->EmitSemanticEvent(FString(TEXT("warning")), FString(TEXT("Frame hung"))); - } - } - else - { - double GameThreadTime = FPlatformTime::ToMilliseconds(GGameThreadTime); - double GPUFrameTime = FPlatformTime::ToMilliseconds(RHIGetGPUFrameCycles(0)); - double RenderThreadTime = FPlatformTime::ToMilliseconds(GRenderThreadTime); - double RHIThreadTime = FPlatformTime::ToMilliseconds(GRHIThreadTime); - - InterimMeanFrameTime = COMPUTE_MEAN(InterimMeanFrameTime, FrameTime * 1000, InterimFrameCount); - InterimMeanGameThreadTime = COMPUTE_MEAN(InterimMeanGameThreadTime, GameThreadTime, InterimFrameCount); - InterimMeanGPUTime = COMPUTE_MEAN(InterimMeanGPUTime, GPUFrameTime, InterimFrameCount); - InterimMeanRenderThreadTime = COMPUTE_MEAN(InterimMeanRenderThreadTime, RenderThreadTime, InterimFrameCount); - InterimMeanRHIThreadTime = COMPUTE_MEAN(InterimMeanRHIThreadTime, RHIThreadTime, InterimFrameCount); - - ComputeUsedMemory(); - } - if ((NowTime - InterimStart) >= InterimDuration) - { - PushStatsHTTP(); - InterimStart = NowTime; - InterimHangCount = 0; - InterimFrameCount = 1; - } - LastTickTime = NowTime; -} - -void FTimeSeriesDataEmitterModule::ComputeUsedMemory() -{ - FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats(); - - const unsigned int BitsPerMB = (8u * 1024u * 1024u); - UsedVirtualMemory = static_cast(MemoryStats.UsedVirtual) / BitsPerMB; - UsedPhysicalMemory = static_cast(MemoryStats.UsedPhysical) / BitsPerMB; - -#if !UE_BUILD_SHIPPING - TArray Stats; - GetPermanentStats(Stats); - - FName NAME_STATGROUP_RHI(FStatGroup_STATGROUP_RHI::GetGroupName()); - int64 TotalMemory = 0; - for (int32 Index = 0; Index < Stats.Num(); Index++) - { - FStatMessage const &Meta = Stats[Index]; - FName LastGroup = Meta.NameAndInfo.GetGroupName(); - if (LastGroup == NAME_STATGROUP_RHI && Meta.NameAndInfo.GetFlag(EStatMetaFlags::IsMemory)) - { - TotalMemory += Meta.GetValue_int64(); - } - } - UsedGPUMemory = (double)(TotalMemory / 1024.f / 1024.f); -#endif -} - -void FTimeSeriesDataEmitterModule::PushStatsHTTP() -{ - // Collected Metrics - // name value - UpdateMetric("mean_fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f); - UpdateMetric("mean_frametime", InterimMeanFrameTime); - UpdateMetric("mean_gamethreadtime", InterimMeanGameThreadTime); - UpdateMetric("mean_gputime", InterimMeanGPUTime); - UpdateMetric("mean_rendertime", InterimMeanRenderThreadTime); - UpdateMetric("mean_rhithreadtime", InterimMeanRHIThreadTime); - UpdateMetric("memory_virtual", UsedVirtualMemory); - UpdateMetric("memory_physical", UsedPhysicalMemory); - UpdateMetric("memory_gpu", UsedGPUMemory); - UpdateMetric("num_hangs", InterimHangCount); - - FBuccaneerCommonModule::GetModule()->SendStats(JsonObject); -} - -TStatId FTimeSeriesDataEmitterModule::GetStatId() const -{ - RETURN_QUICK_DECLARE_CYCLE_STAT(FTimeSeriesDataEmitterModule, STATGROUP_Tickables); -} - -#undef LOCTEXT_NAMESPACE - -IMPLEMENT_MODULE(FTimeSeriesDataEmitterModule, TimeSeriesDataEmitter) \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png b/Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png index 1231d4a..202edfc 100644 Binary files a/Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png and b/Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png differ diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs index b302020..50869be 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs @@ -1,4 +1,5 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + using UnrealBuildTool; @@ -13,8 +14,11 @@ public Buccaneer4PixelStreaming(ReadOnlyTargetRules Target) : base(Target) { "Core", "PixelStreaming", - "BuccaneerCommon", - "Json" + "Json", + "CoreUObject", + "DeveloperSettings", + "EngineSettings", + "BuccaneerCommon" }); @@ -25,8 +29,8 @@ public Buccaneer4PixelStreaming(ReadOnlyTargetRules Target) : base(Target) "Engine", "Slate", "SlateCore", - "PixelStreaming", - "BuccaneerCommon" + "BuccaneerCommon", + "BuccaneerStats" }); } } diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 678494e..4cffcc8 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -1,82 +1,57 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. #include "Buccaneer4PixelStreaming.h" -#include "Logging/LogMacros.h" +#include "IBuccaneerStatsModule.h" +#include "Logging.h" #include "PixelStreamingDelegates.h" - -#define LOCTEXT_NAMESPACE "FBuccaneer4PixelStreamingModule" - - -DEFINE_LOG_CATEGORY(BuccaneerPixelStreaming); - -namespace Buccaneer4PixelStreaming -{ +#include "Buccaneer4PixelStreamingSettings.h" + +TMap StatDescriptionMap = { + {"jitterBufferDelay", "Current playout delay introduced by the jitter buffer"}, + {"framesSent", "Total number of video frames sent"}, + {"framesPerSecond", "Current streaming rate in frames (fps)"}, + {"framesReceived", "Total number of video frames received"}, + {"framesDropped", "Number of frames dropped to preserve bitrate"}, + {"framesDecoded", "Total number of video frames decoded"}, + {"framesCorrupted", "Total number of corrupted video frames"}, + {"partialFramesLost", "Number of partially lost frames"}, + {"fullFramesLost", "Number of fully lost frames"}, + {"hugeFramesSent", "Number of huge frames sent"}, + {"jitterBufferTargetDelay", "Target delay the jitter buffer aims to maintain"}, + {"interruptionCount", "Number of playback interruptions"}, + {"totalInterruptionDuration", "Total duration of playback interruptions"}, + {"freezeCount", "Number of playback freezes (lags of 300ms+)"}, + {"pauseCount", "Number of playback pauses"}, + {"totalFreezesDuration", "Total duration of playback freezes"}, + {"totalPausesDuration", "Total duration of playback pauses"}, + {"firCount", "Number of Full Intra Request (FIR) packets sent"}, + {"pliCount", "Number of Picture Loss Indication (PLI) packets sent"}, + {"nackCount", "Number of Negative Acknowledgement (NACK) packets sent"}, + {"sliCount", "Number of Slice Loss Indication (SLI) packets sent"}, + {"retransmittedBytesSent", "Number of retransmitted bytes sent"}, + {"totalEncodedBytesTarget", "Target total encoded bytes over time"}, + {"keyFramesEncoded", "Total number of key frames encoded"}, + {"frameWidth", "Width of the video frame"}, + {"frameHeight", "Height of the video frame"}, + {"bytesSent", "Total number of bytes sent"}, + {"qpSum", "Sum of quantization parameters for encoded frames, a good measure video quality"}, + {"totalEncodeTime", "Total encoding time (ms)"}, + {"totalPacketSendDelay", "Total packet send delay (ms)"}, + {"packetSendDelay", "Packet send delay (ms)"}, + {"framesEncoded", "Total number of frames encoded"}, + {"transmitFps", "Transmit frames per second (fps)"}, + {"bitrate", "Bitrate (kb/s)"}, + {"qp", "Quantization parameter used for video encoding, a good measure of video quality"}, + {"encodeTime", "Encode time (ms)"}, + {"encodeFps", "Encode frames per second (fps)"}, + {"captureToSend", "Capture to send time (ms)"}, + {"captureFps", "Capture frames per second (fps)"}}; -} - void FBuccaneer4PixelStreamingModule::StartupModule() { - StatDescriptionMap = { - {"jitterBufferDelay", "jitterBufferDelay"}, - {"framesSent", "framesSent"}, - {"framesPerSecond", "framesPerSecond"}, - {"framesReceived", "framesReceived"}, - {"framesDropped", "framesDropped"}, - {"framesDecoded", "framesDecoded"}, - {"framesCorrupted", "framesCorrupted"}, - {"partialFramesLost", "partialFramesLost"}, - {"fullFramesLost", "fullFramesLost"}, - {"hugeFramesSent", "hugeFramesSent"}, - {"jitterBufferTargetDelay", "jitterBufferTargetDelay"}, - {"interruptionCount", "interruptionCount"}, - {"totalInterruptionDuration", "totalInterruptionDuration"}, - {"freezeCount", "freezeCount"}, - {"pauseCount", "pauseCount"}, - {"totalFreezesDuration", "totalFreezesDuration"}, - {"totalPausesDuration", "totalPausesDuration"}, - {"firCount", "firCount"}, - {"pliCount", "pliCount"}, - {"nackCount", "nackCount"}, - {"sliCount", "sliCount"}, - {"retransmittedBytesSent", "retransmittedBytesSent"}, - {"totalEncodedBytesTarget", "totalEncodedBytesTarget"}, - {"keyFramesEncoded", "keyFramesEncoded"}, - {"frameWidth", "frameWidth"}, - {"frameHeight", "frameHeight"}, - {"bytesSent", "bytesSent"}, - {"qpSum", "qpSum"}, - {"totalEncodeTime", "totalEncodeTime"}, - {"totalPacketSendDelay", "totalPacketSendDelay"}, - {"packetSendDelay", "packetSendDelay"}, - {"framesEncoded", "framesEncoded"}, - {"transmitFps", "transmit fps"}, - {"bitrate", "bitrate (kb/s)"}, - {"qp", "qp"}, - {"encodeTime", "encode time (ms)"}, - {"encodeFps", "encode fps"}, - {"captureToSend", "capture to send (ms)"}, - {"captureFps", "capture fps"} - }; - - CVarBuccaneer4PixelStreamingEnableStats = IConsoleManager::Get().RegisterConsoleVariable( - TEXT("Buccaneer4PixelStreaming.EnableStats"), - true, - TEXT("Disables the collection and logging of Pixel Streaming stats with Buccaneer"), - ECVF_Default); - - Setup(); -} - -void FBuccaneer4PixelStreamingModule::Setup() -{ - FBuccaneerCommonModule::ParseCommandLineOption(TEXT("Buccaneer4PixelStreamingEnableStats"), CVarBuccaneer4PixelStreamingEnableStats); - LoggingStart = FPlatformTime::Seconds(); - ReportingInterval = 1; - - JsonObject = MakeShareable(new FJsonObject()); - if (UPixelStreamingDelegates* Delegates = UPixelStreamingDelegates::GetPixelStreamingDelegates()) + if (UPixelStreamingDelegates *Delegates = UPixelStreamingDelegates::GetPixelStreamingDelegates()) { Delegates->OnStatChangedNative.AddRaw(this, &FBuccaneer4PixelStreamingModule::ConsumeStat); } @@ -90,82 +65,74 @@ void FBuccaneer4PixelStreamingModule::ShutdownModule() void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue) { - if(!CVarBuccaneer4PixelStreamingEnableStats->GetBool() || PlayerId == TEXT("Application")) + if (!UBuccaneer4PixelStreamingSettings::CVarEnabled.GetValueOnAnyThread()) { return; } - /** - * "{StatName}": { - * "description": "{StatDescription}", - * "value": [ - * "{PlayerId}": {StatValue} - * ] - * } - */ - const TSharedPtr* MetricJson = nullptr; - if(JsonObject->TryGetObjectField((TEXT("%s"), *StatName.ToString()), MetricJson)) + FBuccaneerMetric NewMetric; + NewMetric.Name = StatName.ToString(); + if (const FString* Description = StatDescriptionMap.Find(StatName.ToString())) { - TArray> ValueArray = (*MetricJson)->GetArrayField(TEXT("value")); + NewMetric.Description = *Description; + } + else + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Unknown stat {0}", StatName.ToString()); + NewMetric.Description = StatName.ToString(); // Default description + } + NewMetric.Value = StatValue; - bool bRequiresCreation = true; - for (int i = 0; i < ValueArray.Num(); i++) - { - const TSharedPtr ValueJson = ValueArray[i]->AsObject(); - double val; - if(ValueJson->TryGetNumberField(*PlayerId, val)) + // Application level stats go into SingleValueMetrics + if(PlayerId == TEXT("Application")) + { + bool bFound = false; + for (FBuccaneerMetric& Metric : MetricsCollection.SingleValueMetrics) + { + if (Metric.Name == NewMetric.Name) { - // This metric already has this player id, update the value accordingly - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - bRequiresCreation = false; + Metric.Value = NewMetric.Value; + bFound = true; break; } - } - - if(bRequiresCreation) + } + if (!bFound) { - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - - ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); - - (*MetricJson)->SetArrayField((TEXT("value")), ValueArray); + MetricsCollection.SingleValueMetrics.Add(NewMetric); } } else { - if(!StatDescriptionMap.Contains(*StatName.ToString())) - { - UE_LOG(BuccaneerPixelStreaming, Log, TEXT("%s"), *StatName.ToString()); - return; - } - TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); - NewMetricJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[*StatName.ToString()]))); - - - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - - TArray> ValueArray; - ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); - - NewMetricJson->SetArrayField((TEXT("value")), ValueArray); - - JsonObject->SetObjectField((TEXT("%s"), *StatName.ToString()), NewMetricJson); + // All other metrics are player-specific + TArray& PlayerStats = MetricsCollection.GroupedMetrics.FindOrAdd(PlayerId); + bool bFound = false; + for (FBuccaneerMetric& Metric : PlayerStats) + { + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + PlayerStats.Add(NewMetric); + } } - double NowTime = FPlatformTime::Seconds(); - if ( (NowTime - LoggingStart) >= ReportingInterval ) + double NowTime = IBuccaneerStatsModule::GetStatsTimestamp(); + const double ReportingIntervalSeconds = UBuccaneer4PixelStreamingSettings::CVarReportingInterval.GetValueOnAnyThread(); + if (ReportingIntervalSeconds > 0.0 && (NowTime - LoggingStart) >= ReportingIntervalSeconds) { LoggingStart = NowTime; - TSharedPtr PayloadJson = MakeShareable(new FJsonObject()); - PayloadJson->SetObjectField(TEXT("metrics"), JsonObject); - FBuccaneerCommonModule::GetModule()->SendStats(PayloadJson); + MetricsCollection.Timestamp = LoggingStart; - JsonObject = MakeShareable(new FJsonObject()); + IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); + + MetricsCollection.SingleValueMetrics.Empty(); + MetricsCollection.GroupedMetrics.Empty(); } } -#undef LOCTEXT_NAMESPACE - IMPLEMENT_MODULE(FBuccaneer4PixelStreamingModule, Buccaneer4PixelStreaming) \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h new file mode 100644 index 0000000..73dde74 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h @@ -0,0 +1,27 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + + +#pragma once + +#include "CoreMinimal.h" +#include "BuccaneerMetrics.h" +#include "IBuccaneerCommonModule.h" +#include "IBuccaneer4PixelStreamingModule.h" +#include "IPixelStreamingModule.h" +#include "Modules/ModuleManager.h" +#include "PixelStreamingDelegates.h" +#include "PixelStreamingPlayerId.h" + +class FBuccaneer4PixelStreamingModule : public IBuccaneer4PixelStreamingModule +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + void ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue); + +private: + double LoggingStart; + FMetricsCollection MetricsCollection; +}; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp new file mode 100644 index 0000000..4aae0b4 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp @@ -0,0 +1,294 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Buccaneer4PixelStreamingSettings.h" + +#include "BuccaneerSettings.h" +#include "Logging.h" +#include "Misc/CommandLine.h" +#include "UObject/ReflectedTypeAccessors.h" + +static const TSet> GetCmdArg = { + { "Buccaneer4PixelStreaming.EnableStats", "Enabled" }, + { "Buccaneer4PixelStreaming.ReportingInterval", "ReportingInterval" } +}; + +TAutoConsoleVariable UBuccaneer4PixelStreamingSettings::CVarEnabled( + TEXT("Buccaneer4PixelStreaming.EnableStats"), + true, + TEXT("Enables the collection and logging of Pixel Streaming stats with Buccaneer (default: true)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneer4PixelStreamingSettings::CVarReportingInterval( + TEXT("Buccaneer4PixelStreaming.ReportingInterval"), + 1.0f, + TEXT("The interval at which to report Pixel Streaming performance metrics (default: 1.0 seconds)"), + ECVF_Default); + +FName UBuccaneer4PixelStreamingSettings::GetCategoryName() const +{ + return TEXT("Plugins"); +} + +#if WITH_EDITOR +FText UBuccaneer4PixelStreamingSettings::GetSectionText() const +{ + return NSLOCTEXT("Buccaneer4PixelStreamingPlugin", "Buccaneer4PixelStreamingSettingsSection", "Buccaneer4PixelStreaming"); +} + +void UBuccaneer4PixelStreamingSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FString PropertyName = PropertyChangedEvent.Property->GetNameCPP(); + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, PropertyChangedEvent.Property); + } +} +#endif + +void UBuccaneer4PixelStreamingSettings::SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + ByteProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + int64 EnumIndex = EnumProperty->GetEnum()->GetIndexByNameString(Value.Replace(TEXT("_"), TEXT(""))); + if (EnumIndex != INDEX_NONE) + { + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex), ECVF_SetByCommandline); + + FNumericProperty* UnderlyingProp = EnumProperty->GetUnderlyingProperty(); + int64* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + *PropertyAddress = EnumProperty->GetEnum()->GetValueByIndex(EnumIndex); + + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex)); + } + else + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "{0} is not a valid enum value for {1}", Value, EnumProperty->GetEnum()->CppType); + } + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + bool bValue = false; + if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) + { + bValue = true; + } + else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) + { + bValue = false; + } + CVar->Set(bValue, ECVF_SetByCommandline); + BoolProperty->SetPropertyValue_InContainer(this, bValue); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), bValue); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + IntProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FCString::Atof(*Value), ECVF_SetByCommandline); + FloatProperty->SetPropertyValue_InContainer(this, FCString::Atof(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atof(*Value)); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + StringProperty->SetPropertyValue_InContainer(this, *Value); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + NameProperty->SetPropertyValue_InContainer(this, FName(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + CVar->Set(*Value, ECVF_SetByCommandline); + + TArray StringArray; + Value.ParseIntoArray(StringArray, TEXT(","), true); + + TArray& Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + Array = StringArray; + + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } +} + +void UBuccaneer4PixelStreamingSettings::SetCVarFromProperty(const FString& CVarName, FProperty* Property) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(ByteProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, ByteProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + void* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + int64 CurrentValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(PropertyAddress); + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), Property->GetNameCPP()); + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + CVar->Set(BoolProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, BoolProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(IntProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, IntProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FloatProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, FloatProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*StringProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, StringProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*NameProperty->GetPropertyValue_InContainer(this).ToString(), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, NameProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + TArray Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + CVar->Set(*FString::Join(Array, TEXT(",")), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, FString::Join(Array, TEXT(",")), Property->GetNameCPP()); + } +} + +void UBuccaneer4PixelStreamingSettings::InitializeCVarsFromProperties() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Initializing CVars from ini"); + for (FProperty* Property = GetClass()->PropertyLink; Property; Property = Property->PropertyLinkNext) + { + if (!Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, Property->GetNameCPP()); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, Property); + continue; + } + } +} + +void UBuccaneer4PixelStreamingSettings::ValidateCommandLineArgs() +{ + FString CommandLine = FCommandLine::Get(); + + TArray CommandArray; + CommandLine.ParseIntoArray(CommandArray, TEXT(" "), true); + + for (FString Command : CommandArray) + { + Command.RemoveFromStart(TEXT("-")); + if (!Command.StartsWith("Buccaneer4PixelStreaming")) + { + continue; + } + + // Get the pure command line arg from an arg that contains an '=', eg BuccaneerURL= + FString CurrentCommandLineArg = Command; + if (Command.Contains("=")) + { + Command.Split(TEXT("="), &CurrentCommandLineArg, nullptr); + } + + bool bValidArg = false; + for (const TPair& Pair : GetCmdArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + + if (!bValidArg) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "Unknown Buccaneer4PixelStreaming command line arg: {0}", CurrentCommandLineArg); + } + } +} + +void UBuccaneer4PixelStreamingSettings::ParseCommandlineArgs() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming, Verbose, "Updating CVars and properties with command line args"); + for (const TPair& Pair : GetCmdArg) + { + FString CVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + // Handle a directly parsable commandline + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(CVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(CVarString))) + { + SetCVarAndPropertyFromValue(CVarString, Property, TEXT("true")); + } + } +} + +void UBuccaneer4PixelStreamingSettings::PostInitProperties() +{ + Super::PostInitProperties(); + + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Initialising Buccaneer4PixelStreaming settings."); + + // Set all the CVars to reflect the state of the ini + InitializeCVarsFromProperties(); + + // Validate command line args to log if they're invalid + ValidateCommandLineArgs(); + + // Update CVars and properties based on command line args + ParseCommandlineArgs(); +} \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.cpp new file mode 100644 index 0000000..5db9e43 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneer4PixelStreaming); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.h new file mode 100644 index 0000000..89ea0f2 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.h @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneer4PixelStreaming, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming.h deleted file mode 100644 index 30c2a0a..0000000 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "BuccaneerCommon.h" -#include "Modules/ModuleManager.h" -#include "PixelStreamingDelegates.h" -#include "IPixelStreamingModule.h" - -#include "Dom/JsonObject.h" -#include "PixelStreamingPlayerId.h" - -DECLARE_LOG_CATEGORY_EXTERN(BuccaneerPixelStreaming, Log, All); - -class FBuccaneer4PixelStreamingModule : public IModuleInterface -{ -public: - - /** IModuleInterface implementation */ - virtual void StartupModule() override; - virtual void ShutdownModule() override; - void Setup(); - UFUNCTION() - void ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue); - - IConsoleVariable* CVarBuccaneer4PixelStreamingEnableStats; - -private: - - double LoggingStart; - double ReportingInterval; - - TMap StatDescriptionMap; - - TSharedPtr JsonObject; -}; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h new file mode 100644 index 0000000..aaab581 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h @@ -0,0 +1,50 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Containers/UnrealString.h" +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" + +#include "Buccaneer4PixelStreamingSettings.generated.h" + +// Config loaded/saved to an .ini file. +// It is also exposed through the plugin settings page in editor. +UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Buccaneer4PixelStreaming")) +class BUCCANEER4PIXELSTREAMING_API UBuccaneer4PixelStreamingSettings : public UDeveloperSettings +{ + GENERATED_BODY() + + virtual ~UBuccaneer4PixelStreamingSettings() = default; + +public: + static TAutoConsoleVariable CVarEnabled; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer4PixelStreaming", meta = ( + DisplayName = "Enabled", + ToolTip = "Enables the collection of Pixel Streaming performance metrics" + )) + bool Enabled = true; + + static TAutoConsoleVariable CVarReportingInterval; + + // Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + +#if WITH_EDITOR + virtual FText GetSectionText() const override; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + // End UDeveloperSettings Interface + + // Begin UObject Interface + virtual void PostInitProperties() override; + // End UObject Interface + +private: + void SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value); + void SetCVarFromProperty(const FString& CVarName, FProperty* Property); + + void InitializeCVarsFromProperties(); + void ValidateCommandLineArgs(); + void ParseCommandlineArgs(); +}; \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreamingModule.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreamingModule.h new file mode 100644 index 0000000..b65d713 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreamingModule.h @@ -0,0 +1,33 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class BUCCANEER4PIXELSTREAMING_API IBuccaneer4PixelStreamingModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneer4PixelStreamingModule& Get() + { + return FModuleManager::LoadModuleChecked("Buccaneer4PixelStreaming"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("Buccaneer4PixelStreaming"); + } +}; \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Buccaneer4PixelStreaming2.uplugin b/Plugins/Buccaneer4PixelStreaming2/Buccaneer4PixelStreaming2.uplugin new file mode 100644 index 0000000..f2f1486 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Buccaneer4PixelStreaming2.uplugin @@ -0,0 +1,34 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Buccaneer4PixelStreaming2", + "Description": "", + "Category": "Other", + "CreatedBy": "", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": false, + "IsExperimentalVersion": true, + "Installed": false, + "Modules": [ + { + "Name": "Buccaneer4PixelStreaming2", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "PixelStreaming2", + "Enabled": true + }, + { + "Name": "Buccaneer", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png b/Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png new file mode 100644 index 0000000..63db5e4 Binary files /dev/null and b/Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png differ diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs new file mode 100644 index 0000000..e818715 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs @@ -0,0 +1,34 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +using UnrealBuildTool; + +public class Buccaneer4PixelStreaming2 : ModuleRules +{ + public Buccaneer4PixelStreaming2(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "PixelStreaming2", + "Json", + "CoreUObject", + "DeveloperSettings", + "EngineSettings" + }); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "BuccaneerCommon", + "BuccaneerStats" + }); + } +} diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp new file mode 100644 index 0000000..0457258 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -0,0 +1,136 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Buccaneer4PixelStreaming2.h" +#include "IBuccaneerStatsModule.h" +#include "Logging.h" +#include "Buccaneer4PixelStreaming2Settings.h" + +TMap PSStatDescriptionMap = { + {"jitterBufferDelay", "Current playout delay introduced by the jitter buffer"}, + {"framesSent", "Total number of video frames sent"}, + {"framesPerSecond", "Current streaming rate in frames (fps)"}, + {"framesReceived", "Total number of video frames received"}, + {"framesDropped", "Number of frames dropped to preserve bitrate"}, + {"framesDecoded", "Total number of video frames decoded"}, + {"framesCorrupted", "Total number of corrupted video frames"}, + {"partialFramesLost", "Number of partially lost frames"}, + {"fullFramesLost", "Number of fully lost frames"}, + {"hugeFramesSent", "Number of huge frames sent"}, + {"jitterBufferTargetDelay", "Target delay the jitter buffer aims to maintain"}, + {"interruptionCount", "Number of playback interruptions"}, + {"totalInterruptionDuration", "Total duration of playback interruptions"}, + {"freezeCount", "Number of playback freezes (lags of 300ms+)"}, + {"pauseCount", "Number of playback pauses"}, + {"totalFreezesDuration", "Total duration of playback freezes"}, + {"totalPausesDuration", "Total duration of playback pauses"}, + {"firCount", "Number of Full Intra Request (FIR) packets sent"}, + {"pliCount", "Number of Picture Loss Indication (PLI) packets sent"}, + {"nackCount", "Number of Negative Acknowledgement (NACK) packets sent"}, + {"sliCount", "Number of Slice Loss Indication (SLI) packets sent"}, + {"retransmittedBytesSent", "Number of retransmitted bytes sent"}, + {"totalEncodedBytesTarget", "Target total encoded bytes over time"}, + {"keyFramesEncoded", "Total number of key frames encoded"}, + {"frameWidth", "Width of the video frame"}, + {"frameHeight", "Height of the video frame"}, + {"bytesSent", "Total number of bytes sent"}, + {"qpSum", "Sum of quantization parameters for encoded frames, a good measure video quality"}, + {"totalEncodeTime", "Total encoding time (ms)"}, + {"totalPacketSendDelay", "Total packet send delay (ms)"}, + {"packetSendDelay", "Packet send delay (ms)"}, + {"framesEncoded", "Total number of frames encoded"}, + {"transmitFps", "Transmit frames per second (fps)"}, + {"bitrate", "Bitrate (kb/s)"}, + {"qp", "Quantization parameter used for video encoding, a good measure of video quality"}, + {"encodeTime", "Encode time (ms)"}, + {"encodeFps", "Encode frames per second (fps)"}, + {"captureToSend", "Capture to send time (ms)"}, + {"captureFps", "Capture frames per second (fps)"}}; + +void FBuccaneer4PixelStreaming2Module::StartupModule() +{ + LoggingStart = FPlatformTime::Seconds(); + + if (UPixelStreaming2Delegates *Delegates = UPixelStreaming2Delegates::Get()) + { + Delegates->OnStatChangedNative.AddRaw(this, &FBuccaneer4PixelStreaming2Module::ConsumeStat); + } +} + +void FBuccaneer4PixelStreaming2Module::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatName, float StatValue) +{ + if (!UBuccaneer4PixelStreaming2Settings::CVarEnabled.GetValueOnAnyThread() || UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread() <= 0) + { + return; + } + + FBuccaneerMetric NewMetric; + NewMetric.Name = StatName.ToString(); + if (const FString* Description = PSStatDescriptionMap.Find(StatName.ToString())) + { + NewMetric.Description = *Description; + } + else + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Verbose, "Unknown stat {0}", StatName.ToString()); + NewMetric.Description = StatName.ToString(); // Default description + } + NewMetric.Value = StatValue; + + // Application level stats go into SingleValueMetrics + if(PlayerId == TEXT("Application")) + { + bool bFound = false; + for (FBuccaneerMetric& Metric : MetricsCollection.SingleValueMetrics) + { + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + MetricsCollection.SingleValueMetrics.Add(NewMetric); + } + } + else + { + // All other metrics are player-specific + TArray& PlayerStats = MetricsCollection.GroupedMetrics.FindOrAdd(PlayerId); + bool bFound = false; + for (FBuccaneerMetric& Metric : PlayerStats) + { + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + PlayerStats.Add(NewMetric); + } + } + + double NowTime = IBuccaneerStatsModule::GetStatsTimestamp(); + if ((NowTime - LoggingStart) >= UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread()) + { + LoggingStart = NowTime; + MetricsCollection.Timestamp = LoggingStart; + + IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); + + MetricsCollection.SingleValueMetrics.Empty(); + MetricsCollection.GroupedMetrics.Empty(); + } +} + +IMPLEMENT_MODULE(FBuccaneer4PixelStreaming2Module, Buccaneer4PixelStreaming2) \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h new file mode 100644 index 0000000..8038066 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h @@ -0,0 +1,25 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "BuccaneerMetrics.h" +#include "IBuccaneerCommonModule.h" +#include "IBuccaneer4PixelStreaming2Module.h" +#include "Modules/ModuleManager.h" +#include "PixelStreaming2Delegates.h" + +class FBuccaneer4PixelStreaming2Module : public IBuccaneer4PixelStreaming2Module +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + void ConsumeStat(FString PlayerId, FName StatName, float StatValue); + +private: + double LoggingStart; + + FMetricsCollection MetricsCollection; +}; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp new file mode 100644 index 0000000..a2f2932 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp @@ -0,0 +1,294 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Buccaneer4PixelStreaming2Settings.h" + +#include "BuccaneerSettings.h" +#include "Logging.h" +#include "Misc/CommandLine.h" +#include "UObject/ReflectedTypeAccessors.h" + +static const TSet> GetCmdLineArg = { + { "Buccaneer4PixelStreaming2.EnableStats", "Enabled" }, + { "Buccaneer4PixelStreaming2.ReportingInterval", "ReportingInterval" } +}; + +TAutoConsoleVariable UBuccaneer4PixelStreaming2Settings::CVarEnabled( + TEXT("Buccaneer4PixelStreaming2.EnableStats"), + true, + TEXT("Enables the collection and logging of Pixel Streaming stats with Buccaneer (default: true)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneer4PixelStreaming2Settings::CVarReportingInterval( + TEXT("Buccaneer4PixelStreaming2.ReportingInterval"), + 1.0f, + TEXT("The interval at which to report Pixel Streaming 2 performance metrics (default: 1.0 seconds)"), + ECVF_Default); + +FName UBuccaneer4PixelStreaming2Settings::GetCategoryName() const +{ + return TEXT("Plugins"); +} + +#if WITH_EDITOR +FText UBuccaneer4PixelStreaming2Settings::GetSectionText() const +{ + return NSLOCTEXT("Buccaneer4PixelStreaming2Plugin", "Buccaneer4PixelStreaming2SettingsSection", "Buccaneer4PixelStreaming2"); +} + +void UBuccaneer4PixelStreaming2Settings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FString PropertyName = PropertyChangedEvent.Property->GetNameCPP(); + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdLineArg, PropertyName); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, PropertyChangedEvent.Property); + } +} +#endif + +void UBuccaneer4PixelStreaming2Settings::SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + ByteProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + int64 EnumIndex = EnumProperty->GetEnum()->GetIndexByNameString(Value.Replace(TEXT("_"), TEXT(""))); + if (EnumIndex != INDEX_NONE) + { + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex), ECVF_SetByCommandline); + + FNumericProperty* UnderlyingProp = EnumProperty->GetUnderlyingProperty(); + int64* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + *PropertyAddress = EnumProperty->GetEnum()->GetValueByIndex(EnumIndex); + + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex)); + } + else + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "{0} is not a valid enum value for {1}", Value, EnumProperty->GetEnum()->CppType); + } + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + bool bValue = false; + if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) + { + bValue = true; + } + else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) + { + bValue = false; + } + CVar->Set(bValue, ECVF_SetByCommandline); + BoolProperty->SetPropertyValue_InContainer(this, bValue); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), bValue); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + IntProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FCString::Atof(*Value), ECVF_SetByCommandline); + FloatProperty->SetPropertyValue_InContainer(this, FCString::Atof(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atof(*Value)); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + StringProperty->SetPropertyValue_InContainer(this, *Value); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + NameProperty->SetPropertyValue_InContainer(this, FName(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + CVar->Set(*Value, ECVF_SetByCommandline); + + TArray StringArray; + Value.ParseIntoArray(StringArray, TEXT(","), true); + + TArray& Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + Array = StringArray; + + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } +} + +void UBuccaneer4PixelStreaming2Settings::SetCVarFromProperty(const FString& CVarName, FProperty* Property) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(ByteProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, ByteProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + void* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + int64 CurrentValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(PropertyAddress); + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), Property->GetNameCPP()); + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + CVar->Set(BoolProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, BoolProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(IntProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, IntProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FloatProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, FloatProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*StringProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, StringProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*NameProperty->GetPropertyValue_InContainer(this).ToString(), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, NameProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + TArray Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + CVar->Set(*FString::Join(Array, TEXT(",")), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, FString::Join(Array, TEXT(",")), Property->GetNameCPP()); + } +} + +void UBuccaneer4PixelStreaming2Settings::InitializeCVarsFromProperties() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Initializing CVars from ini"); + for (FProperty* Property = GetClass()->PropertyLink; Property; Property = Property->PropertyLinkNext) + { + if (!Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdLineArg, Property->GetNameCPP()); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, Property); + continue; + } + } +} + +void UBuccaneer4PixelStreaming2Settings::ValidateCommandLineArgs() +{ + FString CommandLine = FCommandLine::Get(); + + TArray CommandArray; + CommandLine.ParseIntoArray(CommandArray, TEXT(" "), true); + + for (FString Command : CommandArray) + { + Command.RemoveFromStart(TEXT("-")); + if (!Command.StartsWith("Buccaneer4PixelStreaming2")) + { + continue; + } + + // Get the pure command line arg from an arg that contains an '=', eg BuccaneerURL= + FString CurrentCommandLineArg = Command; + if (Command.Contains("=")) + { + Command.Split(TEXT("="), &CurrentCommandLineArg, nullptr); + } + + bool bValidArg = false; + for (const TPair& Pair : GetCmdLineArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + + if (!bValidArg) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "Unknown Buccaneer4PixelStreaming2 command line arg: {0}", CurrentCommandLineArg); + } + } +} + +void UBuccaneer4PixelStreaming2Settings::ParseCommandlineArgs() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Verbose, "Updating CVars and properties with command line args"); + for (const TPair& Pair : GetCmdLineArg) + { + FString CVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + // Handle a directly parsable commandline + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(CVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(CVarString))) + { + SetCVarAndPropertyFromValue(CVarString, Property, TEXT("true")); + } + } +} + +void UBuccaneer4PixelStreaming2Settings::PostInitProperties() +{ + Super::PostInitProperties(); + + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Initialising Buccaneer4PixelStreaming2 settings."); + + // Set all the CVars to reflect the state of the ini + InitializeCVarsFromProperties(); + + // Validate command line args to log if they're invalid + ValidateCommandLineArgs(); + + // Update CVars and properties based on command line args + ParseCommandlineArgs(); +} \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.cpp new file mode 100644 index 0000000..c30147d --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneer4PixelStreaming2); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.h new file mode 100644 index 0000000..a43062e --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.h @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneer4PixelStreaming2, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h new file mode 100644 index 0000000..047986d --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h @@ -0,0 +1,55 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Containers/UnrealString.h" +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" + +#include "Buccaneer4PixelStreaming2Settings.generated.h" + +// Config loaded/saved to an .ini file. +// It is also exposed through the plugin settings page in editor. +UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Buccaneer4PixelStreaming2")) +class BUCCANEER4PIXELSTREAMING2_API UBuccaneer4PixelStreaming2Settings : public UDeveloperSettings +{ + GENERATED_BODY() + + virtual ~UBuccaneer4PixelStreaming2Settings() = default; + +public: + static TAutoConsoleVariable CVarEnabled; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer4PixelStreaming2", meta = ( + DisplayName = "Enabled", + ToolTip = "Enables the collection of Pixel Streaming 2 performance metrics" + )) + bool Enabled = true; + + static TAutoConsoleVariable CVarReportingInterval; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer4PixelStreaming2", meta = ( + DisplayName = "Reporting Interval (seconds)", + ToolTip = "The interval at which to report Pixel Streaming 2 performance metrics. <= 0 disables reporting" + )) + float ReportingInterval = 1.0f; + + // Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + +#if WITH_EDITOR + virtual FText GetSectionText() const override; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + // End UDeveloperSettings Interface + + // Begin UObject Interface + virtual void PostInitProperties() override; + // End UObject Interface + +private: + void SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value); + void SetCVarFromProperty(const FString& CVarName, FProperty* Property); + + void InitializeCVarsFromProperties(); + void ValidateCommandLineArgs(); + void ParseCommandlineArgs(); +}; \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreaming2Module.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreaming2Module.h new file mode 100644 index 0000000..7e01d4e --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreaming2Module.h @@ -0,0 +1,33 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class BUCCANEER4PIXELSTREAMING2_API IBuccaneer4PixelStreaming2Module : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneer4PixelStreaming2Module& Get() + { + return FModuleManager::LoadModuleChecked("Buccaneer4PixelStreaming2"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("Buccaneer4PixelStreaming2"); + } +}; \ No newline at end of file diff --git a/Server/BuccaneerServer/Dockerfile b/Server/BuccaneerServer/Dockerfile index 5197ba8..cecaef8 100644 --- a/Server/BuccaneerServer/Dockerfile +++ b/Server/BuccaneerServer/Dockerfile @@ -4,8 +4,7 @@ FROM golang:1.20.6-alpine WORKDIR /app -COPY go.mod ./ -COPY go.sum ./ +COPY go.mod go.sum ./ RUN go mod download COPY *.go ./ diff --git a/Server/BuccaneerServer/main.go b/Server/BuccaneerServer/main.go index 0320e1a..fdbeeba 100644 --- a/Server/BuccaneerServer/main.go +++ b/Server/BuccaneerServer/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "flag" "io" "log" "net/http" @@ -47,12 +48,18 @@ type record struct { type arbitraryJson map[string]interface{} func (collector *collector) Describe(ch chan<- *prometheus.Desc) { + collectorsMutex.Lock() + defer collectorsMutex.Unlock() + for _, metric := range collector.metrics { ch <- metric.description } } func (collector *collector) Collect(ch chan<- prometheus.Metric) { + collectorsMutex.Lock() + defer collectorsMutex.Unlock() + for _, metric := range collector.metrics { if record, ok := metric.records[""]; ok { @@ -98,6 +105,15 @@ func removeStaleCollectors() { } func main() { + // Determine the port to use: command line arg > environment variable > default + port := flag.String("port", os.Getenv("PORT"), "Port to listen on (can also be set via PORT environment variable)") + flag.Parse() + + // Use default if neither flag nor env var is set + if *port == "" { + *port = "8000" + } + // start sub-routine go removeStaleCollectors() @@ -151,6 +167,8 @@ func main() { if _, exists := collectors.Load(id.(string)); !exists { // this is the first time we're seeing this ID, so configure accordingly + collectorsMutex.Lock() + ts := time.Now().Unix() collector := collector{ metadata: make(map[string]string), @@ -179,6 +197,12 @@ func main() { metricJson := value.(map[string]interface{}) + // Get description, defaulting to the metric name if not provided + description := key + if desc, ok := metricJson["description"].(string); ok { + description = desc + } + if valueArray, ok := metricJson["value"].([]interface{}); ok { // if the value object is an array, then loop through this array recordMap := make(map[string]record) @@ -193,7 +217,7 @@ func main() { } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), []string{"player"}, collector.metadata), + description: prometheus.NewDesc(key, description, []string{"player"}, collector.metadata), records: recordMap, } } else { @@ -203,7 +227,7 @@ func main() { time: ts, } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), nil, collector.metadata), + description: prometheus.NewDesc(key, description, nil, collector.metadata), records: recordMap, } } @@ -212,7 +236,11 @@ func main() { // store collector in our internal map collectors.Store(id, collector) - // register collector with Prometheus + + collectorsMutex.Unlock() + + // register collector with Prometheus (must be done outside the mutex lock + // because Prometheus will call Describe which also needs the mutex) prometheus.Register(&collector) log.Printf("Registering collector for instance \"%s\"", id) @@ -243,6 +271,12 @@ func main() { for key, value := range metricsJson { metricJson := value.(map[string]interface{}) + // Get description, defaulting to the metric name if not provided + description := key + if desc, ok := metricJson["description"].(string); ok { + description = desc + } + if valueArray, ok := metricJson["value"].([]interface{}); ok { // if the value object is an array, then loop through this array recordMap := make(map[string]record) @@ -256,7 +290,7 @@ func main() { } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), []string{"player"}, collector.metadata), + description: prometheus.NewDesc(key, description, []string{"player"}, collector.metadata), records: recordMap, } } else { @@ -266,7 +300,7 @@ func main() { time: time.Now().Unix(), } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), nil, collector.metadata), + description: prometheus.NewDesc(key, description, nil, collector.metadata), records: recordMap, } } @@ -281,8 +315,26 @@ func main() { res.WriteHeader(http.StatusOK) }) + // handler for root endpoint - health check + http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + res.Write([]byte("Buccaneer Server is Running")) + }) + // handler for when prometheus scrapes data http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(":8000", nil)) + // Log server startup information + addr := "127.0.0.1:" + *port + log.Println("===========================================") + log.Printf("Buccaneer Server starting on http://%s", addr) + log.Println("===========================================") + log.Println("Available endpoints:") + log.Printf(" GET http://%s/ - Server health check", addr) + log.Printf(" POST http://%s/event - Receive semantic events from Buccaneer clients such as UE Buccaneer plugins", addr) + log.Printf(" POST http://%s/stats - Receive performance metrics from Buccaneer clients such as UE Buccaneer plugins", addr) + log.Printf(" GET http://%s/metrics - Prometheus scrape endpoint", addr) + log.Println("===========================================") + + log.Fatal(http.ListenAndServe(":"+*port, nil)) } diff --git a/json_to_csv.py b/json_to_csv.py new file mode 100644 index 0000000..545de47 --- /dev/null +++ b/json_to_csv.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +JSON to CSV Converter for BuccLog Stats +Converts stats.json performance metrics to CSV format for time-series analysis. +""" + +import json +import csv +from datetime import datetime +from pathlib import Path + + +def convert_json_to_csv(json_file_path, csv_file_path=None): + """ + Convert stats.json to CSV format. + + Args: + json_file_path: Path to the input JSON file + csv_file_path: Path to the output CSV file (optional, defaults to same name with .csv extension) + """ + # Set default output path if not provided + if csv_file_path is None: + json_path = Path(json_file_path) + csv_file_path = json_path.parent / f"{json_path.stem}.csv" + + # Read the JSON file + print(f"Reading JSON file: {json_file_path}") + with open(json_file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + print(f"Found {len(data)} records") + + # Collect all unique metric names across all records + all_metrics = set() + for record in data: + if 'metrics' in record: + all_metrics.update(record['metrics'].keys()) + + # Sort metrics for consistent column order + metric_names = sorted(all_metrics) + + # Create CSV header + header = ['timestamp', 'timestamp_readable', 'id'] + metric_names + + # Prepare rows + rows = [] + for record in data: + # Base fields + timestamp = record.get('timestamp', '') + record_id = record.get('id', '') + + # Convert timestamp to readable format (assuming milliseconds since epoch) + timestamp_readable = '' + if timestamp: + try: + # Adjust divisor based on timestamp magnitude + # If timestamp is very large, it might be in microseconds or nanoseconds + if timestamp > 10**12: # Likely microseconds or nanoseconds + timestamp_seconds = timestamp / 10**6 # Try microseconds first + else: + timestamp_seconds = timestamp / 1000 # Milliseconds + timestamp_readable = datetime.fromtimestamp(timestamp_seconds).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + except (ValueError, OSError): + timestamp_readable = 'N/A' + + # Create row with base fields + row = { + 'timestamp': timestamp, + 'timestamp_readable': timestamp_readable, + 'id': record_id + } + + # Add metric values + metrics = record.get('metrics', {}) + for metric_name in metric_names: + if metric_name in metrics: + row[metric_name] = metrics[metric_name].get('value', '') + else: + row[metric_name] = '' # Empty value if metric not present in this record + + rows.append(row) + + # Write to CSV + print(f"Writing CSV file: {csv_file_path}") + with open(csv_file_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=header) + writer.writeheader() + writer.writerows(rows) + + print(f"Successfully converted {len(rows)} records") + print(f"CSV file created: {csv_file_path}") + print(f"Columns: {len(header)} ({len(metric_names)} metrics)") + + return csv_file_path + + +def main(): + """Main entry point for the script.""" + import sys + + # Get input file from command line or use default + if len(sys.argv) > 1: + input_file = sys.argv[1] + else: + # Default to stats.json in the same directory as the script + script_dir = Path(__file__).parent + input_file = script_dir / "stats.json" + + # Get output file from command line (optional) + output_file = sys.argv[2] if len(sys.argv) > 2 else None + + # Check if input file exists + if not Path(input_file).exists(): + print(f"Error: Input file not found: {input_file}") + print("\nUsage: python json_to_csv.py [input_json_file] [output_csv_file]") + sys.exit(1) + + # Convert the file + try: + convert_json_to_csv(input_file, output_file) + except Exception as e: + print(f"Error during conversion: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/readme.md b/readme.md index 6db7e78..49a6013 100644 --- a/readme.md +++ b/readme.md @@ -120,23 +120,82 @@ MyBuccaneerApplication.exe -BuccaneerURL="http://127.0.0.1:8000" ``` -## Running the Docker Compose demo +## Running with Docker Compose (Development Setup) -To try Buccaneer with a demo project, you can use the Docker Compose demo located in the [Examples](./Examples) subdirectory. The demo has the following requirements: +For development and testing with your own Unreal Engine application, Buccaneer provides a lightweight Docker Compose configuration that starts only the Buccaneer server and monitoring components (Prometheus, Grafana, Loki, and Promtail). This allows you to quickly set up the monitoring stack while you develop and run your own Unreal Engine application locally. -- One of the Linux distributions that is [supported by the NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#supported-platforms) +### Requirements -- The proprietary NVIDIA GPU drivers +- [Docker](https://www.docker.com/) with Docker Compose support + +### Starting the Monitoring Stack + +To start the Buccaneer server and monitoring components, run the following command from the repository root: + +```bash +docker compose -f Examples/Compose/docker-compose.yml up +``` + +Or navigate to the [Examples/Compose](./Examples/Compose) subdirectory and run: + +```bash +docker compose up +``` + +Docker Compose will build the Buccaneer server from the local source code and start all required containers. Once everything is running, you can access: + +- - Grafana dashboard for viewing metrics. Log in using the username `admin` and the password `admin`, and select "Unreal Engine Metrics" from the list of available dashboards. + +### Running Your Unreal Engine Application + +After starting the monitoring stack, launch your Unreal Engine application with the Buccaneer plugin enabled. Make sure to specify the Buccaneer server URL in the launch arguments: + +```bash +MyUnrealApp.exe -BuccaneerURL="http://127.0.0.1:8000" +``` -- [Docker](https://www.docker.com/) +For Pixel Streaming applications, you may also want to include: +```bash +MyUnrealApp.exe -BuccaneerURL="http://127.0.0.1:8000" -PixelStreamingUrl=ws://127.0.0.1:8888 +``` + +The application will now send performance metrics and semantic events to the Buccaneer server, which will be scraped by Prometheus and displayed in the Grafana dashboard. + + +## Running the Full Demo with Docker Compose + +For a complete batteries-included demonstration, Buccaneer provides a full Docker Compose setup that includes: +- The Buccaneer server +- All monitoring components (Prometheus, Grafana, Loki, and Promtail) +- A demo Unreal Engine application with Pixel Streaming enabled +- A Pixel Streaming signaling server + +### Requirements + +- One of the Linux distributions that is [supported by the NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#supported-platforms) +- The proprietary NVIDIA GPU drivers +- [Docker](https://www.docker.com/) with Docker Compose support - [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/overview.html) -To start the demo, simply run the command `docker compose up` in the [Examples/Compose](./Examples/Compose) subdirectory. Docker Compose will automatically download all of the required container images and start the containers for each component of the stack. Once everything is running, open two web browser tabs: +### Running the Demo + +To start the full demo, run the following command from the repository root: + +```bash +docker compose -f Examples/Compose/docker-compose-all.yml up +``` + +Or navigate to the [Examples/Compose](./Examples/Compose) subdirectory and run: + +```bash +docker compose -f docker-compose-all.yml up +``` -- - This is a demo Unreal Engine application that uses Pixel Streaming to allow streaming via a browser. +Docker Compose will automatically download all required container images and start all components. Once everything is running, open two web browser tabs: -- - This is the Grafana dashboard that displays metrics collected from the Unreal Engine application. Log in using the username `admin` and the password `admin`, and select "Unreal Engine Metrics" from the list of available dashboards. +- - The demo Unreal Engine application streaming via Pixel Streaming +- - Grafana dashboard displaying metrics from the application. Log in using the username `admin` and the password `admin`, and select "Unreal Engine Metrics" from the list of available dashboards. ## Legal