├── .gitattributes ├── .gitignore ├── Configs ├── grafana-dashboard-config.yaml ├── grafana-datasource-config.yaml ├── loki-local-config.yaml ├── prometheus.yml └── promtail-local-config.yaml ├── Dashboards └── Unreal Engine Metrics Dashboard.json ├── Examples ├── Compose │ └── docker-compose.yml └── ThroughputTester │ └── tester.py ├── Images ├── Buccaneer.png ├── C++.png ├── Dashboard.png └── EventBP.png ├── LICENSE ├── Plugins ├── Buccaneer │ ├── Buccaneer.uplugin │ ├── README.md │ ├── Resources │ │ └── Icon128.png │ └── Source │ │ ├── BuccaneerCommon │ │ ├── BuccaneerCommon.build.cs │ │ ├── Private │ │ │ └── BuccaneerCommon.cpp │ │ └── Public │ │ │ └── BuccaneerCommon.h │ │ ├── SemanticEventEmitter │ │ ├── Private │ │ │ └── SemanticEventEmitter.cpp │ │ ├── Public │ │ │ ├── SemanticEventBlueprintFunctionLibrary.cpp │ │ │ ├── SemanticEventBlueprintFunctionLibrary.h │ │ │ └── SemanticEventEmitter.h │ │ └── SemanticEventEmitter.build.cs │ │ └── TimeSeriesDataEmitter │ │ ├── Private │ │ └── TimeSeriesDataEmitter.cpp │ │ ├── Public │ │ └── TimeSeriesDataEmitter.h │ │ └── TimeSeriesDataEmitter.build.cs └── Buccaneer4PixelStreaming │ ├── Buccaneer4PixelStreaming.uplugin │ ├── Resources │ └── Icon128.png │ └── Source │ └── Buccaneer4PixelStreaming │ ├── Buccaneer4PixelStreaming.Build.cs │ ├── Private │ └── Buccaneer4PixelStreaming.cpp │ └── Public │ └── Buccaneer4PixelStreaming.h ├── Server └── BuccaneerServer │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.work.sum └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.h linguist-language=C++ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/Binaries/ 2 | **/Intermediate/ 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | **/*.log -------------------------------------------------------------------------------- /Configs/grafana-dashboard-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: dashboards 5 | type: file 6 | updateIntervalSeconds: 30 7 | options: 8 | path: /etc/dashboards 9 | foldersFromFilesStructure: true 10 | -------------------------------------------------------------------------------- /Configs/grafana-datasource-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Loki 5 | type: loki 6 | access: proxy 7 | url: http://127.0.0.1:3100 8 | jsonData: 9 | maxLines: 1000 10 | - name: Prometheus 11 | type: prometheus 12 | access: proxy 13 | httpMethod: POST 14 | url: http://127.0.0.1:9090 15 | -------------------------------------------------------------------------------- /Configs/loki-local-config.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | grpc_listen_port: 9096 6 | 7 | common: 8 | path_prefix: /tmp/loki 9 | storage: 10 | filesystem: 11 | chunks_directory: /tmp/loki/chunks 12 | rules_directory: /tmp/loki/rules 13 | replication_factor: 1 14 | ring: 15 | instance_addr: 127.0.0.1 16 | kvstore: 17 | store: inmemory 18 | 19 | schema_config: 20 | configs: 21 | - from: 2020-10-24 22 | store: boltdb-shipper 23 | object_store: filesystem 24 | schema: v11 25 | index: 26 | prefix: index_ 27 | period: 24h 28 | 29 | ruler: 30 | alertmanager_url: http://127.0.0.1:9093 31 | -------------------------------------------------------------------------------- /Configs/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 2s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 2s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | 6 | scrape_configs: 7 | - job_name: "realtime_stats" 8 | static_configs: 9 | - targets: ["127.0.0.1:8000"] 10 | -------------------------------------------------------------------------------- /Configs/promtail-local-config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 9080 3 | grpc_listen_port: 0 4 | 5 | positions: 6 | filename: /tmp/positions.yaml 7 | 8 | clients: 9 | - url: http://127.0.0.1:3100/loki/api/v1/push 10 | 11 | scrape_configs: 12 | - job_name: system 13 | 14 | static_configs: 15 | - targets: 16 | - localhost 17 | labels: 18 | job: logs 19 | __path__: "/EventsServer/*.log" 20 | 21 | pipeline_stages: 22 | - json: 23 | expressions: 24 | output: log 25 | stream: stream 26 | timestamp: time 27 | instance: instance 28 | - labels: 29 | stream: 30 | instance: 31 | - timestamp: 32 | source: timestamp 33 | format: RFC3339Nano 34 | - output: 35 | source: output 36 | -------------------------------------------------------------------------------- /Dashboards/Unreal Engine Metrics Dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "datasource", 8 | "uid": "grafana" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | }, 22 | { 23 | "datasource": { 24 | "type": "loki", 25 | "uid": "P8E80F9AEF21F6940" 26 | }, 27 | "enable": true, 28 | "expr": "{stream=~\"Error|error\"}", 29 | "iconColor": "red", 30 | "instant": false, 31 | "name": "Error", 32 | "target": {}, 33 | "textFormat": "", 34 | "titleFormat": "" 35 | }, 36 | { 37 | "datasource": { 38 | "type": "loki", 39 | "uid": "P8E80F9AEF21F6940" 40 | }, 41 | "enable": true, 42 | "expr": "{stream=~\"Log|log\"}", 43 | "iconColor": "blue", 44 | "name": "Info", 45 | "target": {} 46 | }, 47 | { 48 | "datasource": { 49 | "type": "loki", 50 | "uid": "P8E80F9AEF21F6940" 51 | }, 52 | "enable": true, 53 | "expr": "{stream=~\"Warning|warning\"}", 54 | "iconColor": "orange", 55 | "name": "Warning", 56 | "target": {} 57 | } 58 | ] 59 | }, 60 | "editable": true, 61 | "fiscalYearStartMonth": 0, 62 | "graphTooltip": 0, 63 | "links": [], 64 | "liveNow": false, 65 | "panels": [ 66 | { 67 | "datasource": { 68 | "type": "prometheus", 69 | "uid": "PBFA97CFB590B2093" 70 | }, 71 | "description": "", 72 | "fieldConfig": { 73 | "defaults": { 74 | "color": { 75 | "mode": "palette-classic" 76 | }, 77 | "custom": { 78 | "axisLabel": "", 79 | "axisPlacement": "auto", 80 | "barAlignment": 0, 81 | "drawStyle": "line", 82 | "fillOpacity": 10, 83 | "gradientMode": "none", 84 | "hideFrom": { 85 | "legend": false, 86 | "tooltip": false, 87 | "viz": false 88 | }, 89 | "lineInterpolation": "smooth", 90 | "lineStyle": { 91 | "fill": "solid" 92 | }, 93 | "lineWidth": 2, 94 | "pointSize": 5, 95 | "scaleDistribution": { 96 | "type": "linear" 97 | }, 98 | "showPoints": "auto", 99 | "spanNulls": false, 100 | "stacking": { 101 | "group": "A", 102 | "mode": "none" 103 | }, 104 | "thresholdsStyle": { 105 | "mode": "off" 106 | } 107 | }, 108 | "mappings": [], 109 | "thresholds": { 110 | "mode": "absolute", 111 | "steps": [ 112 | { 113 | "color": "green", 114 | "value": null 115 | }, 116 | { 117 | "color": "red", 118 | "value": 80 119 | } 120 | ] 121 | } 122 | }, 123 | "overrides": [ 124 | { 125 | "__systemRef": "hideSeriesFrom", 126 | "matcher": { 127 | "id": "byNames", 128 | "options": { 129 | "mode": "exclude", 130 | "names": [ 131 | "Value" 132 | ], 133 | "prefix": "All except:", 134 | "readOnly": true 135 | } 136 | }, 137 | "properties": [ 138 | { 139 | "id": "custom.hideFrom", 140 | "value": { 141 | "legend": false, 142 | "tooltip": false, 143 | "viz": true 144 | } 145 | } 146 | ] 147 | } 148 | ] 149 | }, 150 | "gridPos": { 151 | "h": 9, 152 | "w": 12, 153 | "x": 0, 154 | "y": 0 155 | }, 156 | "id": 2, 157 | "options": { 158 | "legend": { 159 | "calcs": [], 160 | "displayMode": "list", 161 | "placement": "right" 162 | }, 163 | "tooltip": { 164 | "mode": "single", 165 | "sort": "none" 166 | } 167 | }, 168 | "targets": [ 169 | { 170 | "datasource": { 171 | "type": "prometheus", 172 | "uid": "PBFA97CFB590B2093" 173 | }, 174 | "exemplar": true, 175 | "expr": "sum(sum_over_time(mean_fps[2s])) / sum(count_over_time(mean_fps[2s]))", 176 | "interval": "", 177 | "legendFormat": "Average FPS", 178 | "refId": "A" 179 | } 180 | ], 181 | "title": "Network Average FPS", 182 | "type": "timeseries" 183 | }, 184 | { 185 | "datasource": { 186 | "type": "prometheus", 187 | "uid": "PBFA97CFB590B2093" 188 | }, 189 | "fieldConfig": { 190 | "defaults": { 191 | "color": { 192 | "mode": "palette-classic" 193 | }, 194 | "custom": { 195 | "axisLabel": "", 196 | "axisPlacement": "auto", 197 | "barAlignment": 0, 198 | "drawStyle": "line", 199 | "fillOpacity": 0, 200 | "gradientMode": "none", 201 | "hideFrom": { 202 | "legend": false, 203 | "tooltip": false, 204 | "viz": false 205 | }, 206 | "lineInterpolation": "linear", 207 | "lineWidth": 1, 208 | "pointSize": 5, 209 | "scaleDistribution": { 210 | "type": "linear" 211 | }, 212 | "showPoints": "auto", 213 | "spanNulls": false, 214 | "stacking": { 215 | "group": "A", 216 | "mode": "none" 217 | }, 218 | "thresholdsStyle": { 219 | "mode": "off" 220 | } 221 | }, 222 | "mappings": [], 223 | "thresholds": { 224 | "mode": "absolute", 225 | "steps": [ 226 | { 227 | "color": "green", 228 | "value": null 229 | }, 230 | { 231 | "color": "red", 232 | "value": 80 233 | } 234 | ] 235 | } 236 | }, 237 | "overrides": [] 238 | }, 239 | "gridPos": { 240 | "h": 9, 241 | "w": 12, 242 | "x": 12, 243 | "y": 0 244 | }, 245 | "id": 20, 246 | "interval": "1", 247 | "options": { 248 | "legend": { 249 | "calcs": [ 250 | "max" 251 | ], 252 | "displayMode": "list", 253 | "placement": "right" 254 | }, 255 | "tooltip": { 256 | "mode": "single", 257 | "sort": "none" 258 | } 259 | }, 260 | "targets": [ 261 | { 262 | "datasource": { 263 | "type": "prometheus", 264 | "uid": "PBFA97CFB590B2093" 265 | }, 266 | "exemplar": true, 267 | "expr": "count(count by (id) (mean_fps))", 268 | "interval": "", 269 | "legendFormat": "Players", 270 | "refId": "A" 271 | } 272 | ], 273 | "title": "Player Count", 274 | "type": "timeseries" 275 | }, 276 | { 277 | "datasource": { 278 | "type": "loki", 279 | "uid": "P8E80F9AEF21F6940" 280 | }, 281 | "fieldConfig": { 282 | "defaults": { 283 | "color": { 284 | "mode": "palette-classic" 285 | }, 286 | "custom": { 287 | "axisLabel": "", 288 | "axisPlacement": "auto", 289 | "barAlignment": 0, 290 | "drawStyle": "line", 291 | "fillOpacity": 100, 292 | "gradientMode": "none", 293 | "hideFrom": { 294 | "legend": false, 295 | "tooltip": false, 296 | "viz": false 297 | }, 298 | "lineInterpolation": "smooth", 299 | "lineWidth": 2, 300 | "pointSize": 5, 301 | "scaleDistribution": { 302 | "type": "linear" 303 | }, 304 | "showPoints": "auto", 305 | "spanNulls": false, 306 | "stacking": { 307 | "group": "A", 308 | "mode": "none" 309 | }, 310 | "thresholdsStyle": { 311 | "mode": "off" 312 | } 313 | }, 314 | "mappings": [], 315 | "thresholds": { 316 | "mode": "absolute", 317 | "steps": [ 318 | { 319 | "color": "green", 320 | "value": null 321 | }, 322 | { 323 | "color": "red", 324 | "value": 80 325 | } 326 | ] 327 | } 328 | }, 329 | "overrides": [] 330 | }, 331 | "gridPos": { 332 | "h": 9, 333 | "w": 12, 334 | "x": 0, 335 | "y": 9 336 | }, 337 | "id": 22, 338 | "options": { 339 | "legend": { 340 | "calcs": [], 341 | "displayMode": "hidden", 342 | "placement": "right" 343 | }, 344 | "tooltip": { 345 | "mode": "single", 346 | "sort": "none" 347 | } 348 | }, 349 | "targets": [ 350 | { 351 | "datasource": { 352 | "type": "loki", 353 | "uid": "P8E80F9AEF21F6940" 354 | }, 355 | "editorMode": "code", 356 | "expr": "rate(({stream=\"Log\"} |= \"Jumped\" )[1s])", 357 | "legendFormat": "Logs / s", 358 | "queryType": "range", 359 | "refId": "A" 360 | } 361 | ], 362 | "title": "\"Jumps' / Second", 363 | "type": "timeseries" 364 | }, 365 | { 366 | "datasource": { 367 | "type": "loki", 368 | "uid": "P8E80F9AEF21F6940" 369 | }, 370 | "fieldConfig": { 371 | "defaults": { 372 | "color": { 373 | "mode": "palette-classic" 374 | }, 375 | "custom": { 376 | "axisLabel": "", 377 | "axisPlacement": "auto", 378 | "barAlignment": 0, 379 | "drawStyle": "line", 380 | "fillOpacity": 100, 381 | "gradientMode": "none", 382 | "hideFrom": { 383 | "legend": false, 384 | "tooltip": false, 385 | "viz": false 386 | }, 387 | "lineInterpolation": "smooth", 388 | "lineWidth": 2, 389 | "pointSize": 5, 390 | "scaleDistribution": { 391 | "type": "linear" 392 | }, 393 | "showPoints": "auto", 394 | "spanNulls": false, 395 | "stacking": { 396 | "group": "A", 397 | "mode": "none" 398 | }, 399 | "thresholdsStyle": { 400 | "mode": "off" 401 | } 402 | }, 403 | "mappings": [], 404 | "thresholds": { 405 | "mode": "absolute", 406 | "steps": [ 407 | { 408 | "color": "green", 409 | "value": null 410 | } 411 | ] 412 | } 413 | }, 414 | "overrides": [] 415 | }, 416 | "gridPos": { 417 | "h": 9, 418 | "w": 12, 419 | "x": 12, 420 | "y": 9 421 | }, 422 | "id": 23, 423 | "options": { 424 | "legend": { 425 | "calcs": [], 426 | "displayMode": "hidden", 427 | "placement": "right" 428 | }, 429 | "tooltip": { 430 | "mode": "single", 431 | "sort": "none" 432 | } 433 | }, 434 | "targets": [ 435 | { 436 | "datasource": { 437 | "type": "loki", 438 | "uid": "P8E80F9AEF21F6940" 439 | }, 440 | "editorMode": "code", 441 | "expr": "count_over_time(({stream=\"Log\"} |= \"Jumped\" )[1d])", 442 | "legendFormat": "Logs / s", 443 | "queryType": "range", 444 | "refId": "A" 445 | } 446 | ], 447 | "title": "Total \"Jump\" Events", 448 | "type": "timeseries" 449 | }, 450 | { 451 | "datasource": { 452 | "type": "prometheus", 453 | "uid": "PBFA97CFB590B2093" 454 | }, 455 | "fieldConfig": { 456 | "defaults": { 457 | "color": { 458 | "mode": "palette-classic" 459 | }, 460 | "custom": { 461 | "axisLabel": "", 462 | "axisPlacement": "auto", 463 | "barAlignment": 0, 464 | "drawStyle": "line", 465 | "fillOpacity": 10, 466 | "gradientMode": "none", 467 | "hideFrom": { 468 | "legend": false, 469 | "tooltip": false, 470 | "viz": false 471 | }, 472 | "lineInterpolation": "smooth", 473 | "lineStyle": { 474 | "fill": "solid" 475 | }, 476 | "lineWidth": 2, 477 | "pointSize": 5, 478 | "scaleDistribution": { 479 | "type": "linear" 480 | }, 481 | "showPoints": "auto", 482 | "spanNulls": false, 483 | "stacking": { 484 | "group": "A", 485 | "mode": "none" 486 | }, 487 | "thresholdsStyle": { 488 | "mode": "off" 489 | } 490 | }, 491 | "mappings": [], 492 | "thresholds": { 493 | "mode": "absolute", 494 | "steps": [ 495 | { 496 | "color": "green", 497 | "value": null 498 | }, 499 | { 500 | "color": "red", 501 | "value": 80 502 | } 503 | ] 504 | } 505 | }, 506 | "overrides": [] 507 | }, 508 | "gridPos": { 509 | "h": 9, 510 | "w": 12, 511 | "x": 0, 512 | "y": 18 513 | }, 514 | "id": 18, 515 | "options": { 516 | "legend": { 517 | "calcs": [], 518 | "displayMode": "list", 519 | "placement": "right" 520 | }, 521 | "tooltip": { 522 | "mode": "single", 523 | "sort": "none" 524 | } 525 | }, 526 | "targets": [ 527 | { 528 | "datasource": { 529 | "type": "prometheus", 530 | "uid": "PBFA97CFB590B2093" 531 | }, 532 | "exemplar": true, 533 | "expr": "memory_gpu", 534 | "interval": "", 535 | "legendFormat": "ID: {{id}}", 536 | "refId": "A" 537 | } 538 | ], 539 | "title": "VRAM Usage (MB)", 540 | "transformations": [], 541 | "type": "timeseries" 542 | }, 543 | { 544 | "datasource": { 545 | "type": "prometheus", 546 | "uid": "PBFA97CFB590B2093" 547 | }, 548 | "fieldConfig": { 549 | "defaults": { 550 | "color": { 551 | "mode": "palette-classic" 552 | }, 553 | "custom": { 554 | "axisLabel": "", 555 | "axisPlacement": "auto", 556 | "barAlignment": 0, 557 | "drawStyle": "line", 558 | "fillOpacity": 10, 559 | "gradientMode": "none", 560 | "hideFrom": { 561 | "legend": false, 562 | "tooltip": false, 563 | "viz": false 564 | }, 565 | "lineInterpolation": "smooth", 566 | "lineStyle": { 567 | "fill": "solid" 568 | }, 569 | "lineWidth": 2, 570 | "pointSize": 5, 571 | "scaleDistribution": { 572 | "type": "linear" 573 | }, 574 | "showPoints": "auto", 575 | "spanNulls": false, 576 | "stacking": { 577 | "group": "A", 578 | "mode": "none" 579 | }, 580 | "thresholdsStyle": { 581 | "mode": "off" 582 | } 583 | }, 584 | "mappings": [], 585 | "thresholds": { 586 | "mode": "absolute", 587 | "steps": [ 588 | { 589 | "color": "green", 590 | "value": null 591 | }, 592 | { 593 | "color": "red", 594 | "value": 80 595 | } 596 | ] 597 | } 598 | }, 599 | "overrides": [] 600 | }, 601 | "gridPos": { 602 | "h": 9, 603 | "w": 12, 604 | "x": 12, 605 | "y": 18 606 | }, 607 | "id": 7, 608 | "options": { 609 | "legend": { 610 | "calcs": [ 611 | "mean" 612 | ], 613 | "displayMode": "list", 614 | "placement": "right" 615 | }, 616 | "tooltip": { 617 | "mode": "single", 618 | "sort": "none" 619 | } 620 | }, 621 | "targets": [ 622 | { 623 | "datasource": { 624 | "type": "prometheus", 625 | "uid": "PBFA97CFB590B2093" 626 | }, 627 | "exemplar": true, 628 | "expr": "mean_fps", 629 | "interval": "", 630 | "legendFormat": "ID: {{id}}", 631 | "refId": "A" 632 | } 633 | ], 634 | "title": "Individual FPS", 635 | "transformations": [], 636 | "type": "timeseries" 637 | }, 638 | { 639 | "datasource": { 640 | "type": "prometheus", 641 | "uid": "PBFA97CFB590B2093" 642 | }, 643 | "fieldConfig": { 644 | "defaults": { 645 | "color": { 646 | "mode": "palette-classic" 647 | }, 648 | "custom": { 649 | "axisLabel": "", 650 | "axisPlacement": "auto", 651 | "barAlignment": 0, 652 | "drawStyle": "line", 653 | "fillOpacity": 0, 654 | "gradientMode": "none", 655 | "hideFrom": { 656 | "legend": false, 657 | "tooltip": false, 658 | "viz": false 659 | }, 660 | "lineInterpolation": "linear", 661 | "lineWidth": 1, 662 | "pointSize": 5, 663 | "scaleDistribution": { 664 | "type": "linear" 665 | }, 666 | "showPoints": "auto", 667 | "spanNulls": false, 668 | "stacking": { 669 | "group": "A", 670 | "mode": "none" 671 | }, 672 | "thresholdsStyle": { 673 | "mode": "off" 674 | } 675 | }, 676 | "mappings": [], 677 | "thresholds": { 678 | "mode": "absolute", 679 | "steps": [ 680 | { 681 | "color": "green", 682 | "value": null 683 | }, 684 | { 685 | "color": "red", 686 | "value": 80 687 | } 688 | ] 689 | } 690 | }, 691 | "overrides": [ 692 | { 693 | "matcher": { 694 | "id": "byName", 695 | "options": "UNREAL_STATS_0{instance=\"localhost:8000\", job=\"realtime_stats\", v=\"GPUTime\"}" 696 | }, 697 | "properties": [ 698 | { 699 | "id": "color", 700 | "value": { 701 | "fixedColor": "yellow", 702 | "mode": "fixed" 703 | } 704 | } 705 | ] 706 | }, 707 | { 708 | "matcher": { 709 | "id": "byName", 710 | "options": "UNREAL_STATS_0{instance=\"localhost:8000\", job=\"realtime_stats\", v=\"GameThreadTime\"}" 711 | }, 712 | "properties": [ 713 | { 714 | "id": "color", 715 | "value": { 716 | "fixedColor": "red", 717 | "mode": "fixed" 718 | } 719 | } 720 | ] 721 | }, 722 | { 723 | "matcher": { 724 | "id": "byName", 725 | "options": "UNREAL_STATS_0{instance=\"localhost:8000\", job=\"realtime_stats\", v=\"PhysicalUsedMB\"}" 726 | }, 727 | "properties": [ 728 | { 729 | "id": "color", 730 | "value": { 731 | "fixedColor": "blue", 732 | "mode": "fixed" 733 | } 734 | } 735 | ] 736 | } 737 | ] 738 | }, 739 | "gridPos": { 740 | "h": 9, 741 | "w": 12, 742 | "x": 0, 743 | "y": 27 744 | }, 745 | "id": 5, 746 | "options": { 747 | "legend": { 748 | "calcs": [], 749 | "displayMode": "list", 750 | "placement": "right" 751 | }, 752 | "tooltip": { 753 | "mode": "single", 754 | "sort": "none" 755 | } 756 | }, 757 | "targets": [ 758 | { 759 | "datasource": { 760 | "type": "prometheus", 761 | "uid": "PBFA97CFB590B2093" 762 | }, 763 | "exemplar": true, 764 | "expr": "memory_physical", 765 | "interval": "", 766 | "legendFormat": "ID: {{id}}", 767 | "refId": "A" 768 | } 769 | ], 770 | "title": "Physical Memory Used (MB)", 771 | "type": "timeseries" 772 | }, 773 | { 774 | "datasource": { 775 | "type": "prometheus", 776 | "uid": "PBFA97CFB590B2093" 777 | }, 778 | "fieldConfig": { 779 | "defaults": { 780 | "color": { 781 | "mode": "palette-classic" 782 | }, 783 | "custom": { 784 | "axisLabel": "", 785 | "axisPlacement": "auto", 786 | "barAlignment": 0, 787 | "drawStyle": "line", 788 | "fillOpacity": 0, 789 | "gradientMode": "none", 790 | "hideFrom": { 791 | "legend": false, 792 | "tooltip": false, 793 | "viz": false 794 | }, 795 | "lineInterpolation": "linear", 796 | "lineWidth": 1, 797 | "pointSize": 5, 798 | "scaleDistribution": { 799 | "type": "linear" 800 | }, 801 | "showPoints": "auto", 802 | "spanNulls": false, 803 | "stacking": { 804 | "group": "A", 805 | "mode": "none" 806 | }, 807 | "thresholdsStyle": { 808 | "mode": "off" 809 | } 810 | }, 811 | "mappings": [], 812 | "thresholds": { 813 | "mode": "absolute", 814 | "steps": [ 815 | { 816 | "color": "green", 817 | "value": null 818 | }, 819 | { 820 | "color": "red", 821 | "value": 80 822 | } 823 | ] 824 | } 825 | }, 826 | "overrides": [ 827 | { 828 | "matcher": { 829 | "id": "byName", 830 | "options": "UNREAL_STATS_0{instance=\"localhost:8000\", job=\"realtime_stats\", v=\"GPUTime\"}" 831 | }, 832 | "properties": [ 833 | { 834 | "id": "color", 835 | "value": { 836 | "fixedColor": "yellow", 837 | "mode": "fixed" 838 | } 839 | } 840 | ] 841 | }, 842 | { 843 | "matcher": { 844 | "id": "byName", 845 | "options": "UNREAL_STATS_0{instance=\"localhost:8000\", job=\"realtime_stats\", v=\"GameThreadTime\"}" 846 | }, 847 | "properties": [ 848 | { 849 | "id": "color", 850 | "value": { 851 | "fixedColor": "red", 852 | "mode": "fixed" 853 | } 854 | } 855 | ] 856 | }, 857 | { 858 | "matcher": { 859 | "id": "byName", 860 | "options": "UNREAL_STATS_0{instance=\"localhost:8000\", job=\"realtime_stats\", v=\"PhysicalUsedMB\"}" 861 | }, 862 | "properties": [ 863 | { 864 | "id": "color", 865 | "value": { 866 | "fixedColor": "blue", 867 | "mode": "fixed" 868 | } 869 | } 870 | ] 871 | } 872 | ] 873 | }, 874 | "gridPos": { 875 | "h": 9, 876 | "w": 12, 877 | "x": 12, 878 | "y": 27 879 | }, 880 | "id": 8, 881 | "options": { 882 | "legend": { 883 | "calcs": [], 884 | "displayMode": "list", 885 | "placement": "right" 886 | }, 887 | "tooltip": { 888 | "mode": "single", 889 | "sort": "none" 890 | } 891 | }, 892 | "targets": [ 893 | { 894 | "datasource": { 895 | "type": "prometheus", 896 | "uid": "PBFA97CFB590B2093" 897 | }, 898 | "exemplar": true, 899 | "expr": "memory_virtual", 900 | "interval": "", 901 | "legendFormat": "ID: {{id}}", 902 | "refId": "A" 903 | } 904 | ], 905 | "title": "Virtual Memory Used (MB)", 906 | "type": "timeseries" 907 | }, 908 | { 909 | "datasource": { 910 | "type": "prometheus", 911 | "uid": "PBFA97CFB590B2093" 912 | }, 913 | "fieldConfig": { 914 | "defaults": { 915 | "color": { 916 | "mode": "palette-classic" 917 | }, 918 | "custom": { 919 | "axisLabel": "", 920 | "axisPlacement": "auto", 921 | "barAlignment": 0, 922 | "drawStyle": "line", 923 | "fillOpacity": 0, 924 | "gradientMode": "none", 925 | "hideFrom": { 926 | "legend": false, 927 | "tooltip": false, 928 | "viz": false 929 | }, 930 | "lineInterpolation": "linear", 931 | "lineWidth": 1, 932 | "pointSize": 5, 933 | "scaleDistribution": { 934 | "type": "linear" 935 | }, 936 | "showPoints": "auto", 937 | "spanNulls": false, 938 | "stacking": { 939 | "group": "A", 940 | "mode": "none" 941 | }, 942 | "thresholdsStyle": { 943 | "mode": "off" 944 | } 945 | }, 946 | "mappings": [], 947 | "thresholds": { 948 | "mode": "absolute", 949 | "steps": [ 950 | { 951 | "color": "green", 952 | "value": null 953 | }, 954 | { 955 | "color": "red", 956 | "value": 80 957 | } 958 | ] 959 | } 960 | }, 961 | "overrides": [] 962 | }, 963 | "gridPos": { 964 | "h": 8, 965 | "w": 12, 966 | "x": 0, 967 | "y": 36 968 | }, 969 | "id": 12, 970 | "options": { 971 | "legend": { 972 | "calcs": [], 973 | "displayMode": "list", 974 | "placement": "right" 975 | }, 976 | "tooltip": { 977 | "mode": "single", 978 | "sort": "none" 979 | } 980 | }, 981 | "targets": [ 982 | { 983 | "datasource": { 984 | "type": "prometheus", 985 | "uid": "PBFA97CFB590B2093" 986 | }, 987 | "exemplar": true, 988 | "expr": "mean_frametime", 989 | "interval": "", 990 | "legendFormat": "ID: {{id}}", 991 | "refId": "A" 992 | } 993 | ], 994 | "title": "Frame Time (MS)", 995 | "type": "timeseries" 996 | }, 997 | { 998 | "datasource": { 999 | "type": "prometheus", 1000 | "uid": "PBFA97CFB590B2093" 1001 | }, 1002 | "fieldConfig": { 1003 | "defaults": { 1004 | "color": { 1005 | "mode": "palette-classic" 1006 | }, 1007 | "custom": { 1008 | "axisLabel": "", 1009 | "axisPlacement": "auto", 1010 | "barAlignment": 0, 1011 | "drawStyle": "line", 1012 | "fillOpacity": 0, 1013 | "gradientMode": "none", 1014 | "hideFrom": { 1015 | "legend": false, 1016 | "tooltip": false, 1017 | "viz": false 1018 | }, 1019 | "lineInterpolation": "linear", 1020 | "lineWidth": 1, 1021 | "pointSize": 5, 1022 | "scaleDistribution": { 1023 | "type": "linear" 1024 | }, 1025 | "showPoints": "auto", 1026 | "spanNulls": false, 1027 | "stacking": { 1028 | "group": "A", 1029 | "mode": "none" 1030 | }, 1031 | "thresholdsStyle": { 1032 | "mode": "off" 1033 | } 1034 | }, 1035 | "mappings": [], 1036 | "thresholds": { 1037 | "mode": "absolute", 1038 | "steps": [ 1039 | { 1040 | "color": "green", 1041 | "value": null 1042 | }, 1043 | { 1044 | "color": "red", 1045 | "value": 80 1046 | } 1047 | ] 1048 | } 1049 | }, 1050 | "overrides": [] 1051 | }, 1052 | "gridPos": { 1053 | "h": 8, 1054 | "w": 12, 1055 | "x": 12, 1056 | "y": 36 1057 | }, 1058 | "id": 16, 1059 | "options": { 1060 | "legend": { 1061 | "calcs": [], 1062 | "displayMode": "list", 1063 | "placement": "right" 1064 | }, 1065 | "tooltip": { 1066 | "mode": "single", 1067 | "sort": "none" 1068 | } 1069 | }, 1070 | "targets": [ 1071 | { 1072 | "datasource": { 1073 | "type": "prometheus", 1074 | "uid": "PBFA97CFB590B2093" 1075 | }, 1076 | "exemplar": true, 1077 | "expr": "mean_gamethreadtime", 1078 | "interval": "", 1079 | "legendFormat": "ID: {{id}}", 1080 | "refId": "A" 1081 | } 1082 | ], 1083 | "title": "Game Thread Time (MS)", 1084 | "type": "timeseries" 1085 | }, 1086 | { 1087 | "datasource": { 1088 | "type": "prometheus", 1089 | "uid": "PBFA97CFB590B2093" 1090 | }, 1091 | "fieldConfig": { 1092 | "defaults": { 1093 | "color": { 1094 | "mode": "palette-classic" 1095 | }, 1096 | "custom": { 1097 | "axisLabel": "", 1098 | "axisPlacement": "auto", 1099 | "barAlignment": 0, 1100 | "drawStyle": "line", 1101 | "fillOpacity": 0, 1102 | "gradientMode": "none", 1103 | "hideFrom": { 1104 | "legend": false, 1105 | "tooltip": false, 1106 | "viz": false 1107 | }, 1108 | "lineInterpolation": "linear", 1109 | "lineWidth": 1, 1110 | "pointSize": 5, 1111 | "scaleDistribution": { 1112 | "type": "linear" 1113 | }, 1114 | "showPoints": "auto", 1115 | "spanNulls": false, 1116 | "stacking": { 1117 | "group": "A", 1118 | "mode": "none" 1119 | }, 1120 | "thresholdsStyle": { 1121 | "mode": "off" 1122 | } 1123 | }, 1124 | "mappings": [], 1125 | "thresholds": { 1126 | "mode": "absolute", 1127 | "steps": [ 1128 | { 1129 | "color": "green", 1130 | "value": null 1131 | }, 1132 | { 1133 | "color": "red", 1134 | "value": 80 1135 | } 1136 | ] 1137 | } 1138 | }, 1139 | "overrides": [] 1140 | }, 1141 | "gridPos": { 1142 | "h": 8, 1143 | "w": 12, 1144 | "x": 0, 1145 | "y": 44 1146 | }, 1147 | "id": 13, 1148 | "options": { 1149 | "legend": { 1150 | "calcs": [], 1151 | "displayMode": "list", 1152 | "placement": "right" 1153 | }, 1154 | "tooltip": { 1155 | "mode": "single", 1156 | "sort": "none" 1157 | } 1158 | }, 1159 | "targets": [ 1160 | { 1161 | "datasource": { 1162 | "type": "prometheus", 1163 | "uid": "PBFA97CFB590B2093" 1164 | }, 1165 | "exemplar": true, 1166 | "expr": "mean_gputime", 1167 | "interval": "", 1168 | "legendFormat": "ID: {{id}}", 1169 | "refId": "A" 1170 | } 1171 | ], 1172 | "title": "GPU Time (MS)", 1173 | "type": "timeseries" 1174 | }, 1175 | { 1176 | "datasource": { 1177 | "type": "prometheus", 1178 | "uid": "PBFA97CFB590B2093" 1179 | }, 1180 | "fieldConfig": { 1181 | "defaults": { 1182 | "color": { 1183 | "mode": "palette-classic" 1184 | }, 1185 | "custom": { 1186 | "axisLabel": "", 1187 | "axisPlacement": "auto", 1188 | "barAlignment": 0, 1189 | "drawStyle": "line", 1190 | "fillOpacity": 0, 1191 | "gradientMode": "none", 1192 | "hideFrom": { 1193 | "legend": false, 1194 | "tooltip": false, 1195 | "viz": false 1196 | }, 1197 | "lineInterpolation": "linear", 1198 | "lineWidth": 1, 1199 | "pointSize": 5, 1200 | "scaleDistribution": { 1201 | "type": "linear" 1202 | }, 1203 | "showPoints": "auto", 1204 | "spanNulls": false, 1205 | "stacking": { 1206 | "group": "A", 1207 | "mode": "none" 1208 | }, 1209 | "thresholdsStyle": { 1210 | "mode": "off" 1211 | } 1212 | }, 1213 | "mappings": [], 1214 | "thresholds": { 1215 | "mode": "absolute", 1216 | "steps": [ 1217 | { 1218 | "color": "green", 1219 | "value": null 1220 | }, 1221 | { 1222 | "color": "red", 1223 | "value": 80 1224 | } 1225 | ] 1226 | } 1227 | }, 1228 | "overrides": [] 1229 | }, 1230 | "gridPos": { 1231 | "h": 8, 1232 | "w": 12, 1233 | "x": 12, 1234 | "y": 44 1235 | }, 1236 | "id": 14, 1237 | "options": { 1238 | "legend": { 1239 | "calcs": [], 1240 | "displayMode": "list", 1241 | "placement": "right" 1242 | }, 1243 | "tooltip": { 1244 | "mode": "single", 1245 | "sort": "none" 1246 | } 1247 | }, 1248 | "targets": [ 1249 | { 1250 | "datasource": { 1251 | "type": "prometheus", 1252 | "uid": "PBFA97CFB590B2093" 1253 | }, 1254 | "exemplar": true, 1255 | "expr": "mean_rendertime", 1256 | "interval": "", 1257 | "legendFormat": "ID: {{id}}", 1258 | "refId": "A" 1259 | } 1260 | ], 1261 | "title": "Render Thread Time (MS)", 1262 | "type": "timeseries" 1263 | }, 1264 | { 1265 | "datasource": { 1266 | "type": "prometheus", 1267 | "uid": "PBFA97CFB590B2093" 1268 | }, 1269 | "fieldConfig": { 1270 | "defaults": { 1271 | "color": { 1272 | "mode": "palette-classic" 1273 | }, 1274 | "custom": { 1275 | "axisLabel": "", 1276 | "axisPlacement": "auto", 1277 | "barAlignment": 0, 1278 | "drawStyle": "line", 1279 | "fillOpacity": 0, 1280 | "gradientMode": "none", 1281 | "hideFrom": { 1282 | "legend": false, 1283 | "tooltip": false, 1284 | "viz": false 1285 | }, 1286 | "lineInterpolation": "linear", 1287 | "lineWidth": 1, 1288 | "pointSize": 5, 1289 | "scaleDistribution": { 1290 | "type": "linear" 1291 | }, 1292 | "showPoints": "auto", 1293 | "spanNulls": false, 1294 | "stacking": { 1295 | "group": "A", 1296 | "mode": "none" 1297 | }, 1298 | "thresholdsStyle": { 1299 | "mode": "off" 1300 | } 1301 | }, 1302 | "mappings": [], 1303 | "thresholds": { 1304 | "mode": "absolute", 1305 | "steps": [ 1306 | { 1307 | "color": "green", 1308 | "value": null 1309 | }, 1310 | { 1311 | "color": "red", 1312 | "value": 80 1313 | } 1314 | ] 1315 | } 1316 | }, 1317 | "overrides": [] 1318 | }, 1319 | "gridPos": { 1320 | "h": 8, 1321 | "w": 12, 1322 | "x": 0, 1323 | "y": 52 1324 | }, 1325 | "id": 15, 1326 | "options": { 1327 | "legend": { 1328 | "calcs": [], 1329 | "displayMode": "list", 1330 | "placement": "right" 1331 | }, 1332 | "tooltip": { 1333 | "mode": "single", 1334 | "sort": "none" 1335 | } 1336 | }, 1337 | "targets": [ 1338 | { 1339 | "datasource": { 1340 | "type": "prometheus", 1341 | "uid": "PBFA97CFB590B2093" 1342 | }, 1343 | "exemplar": true, 1344 | "expr": "mean_rhithreadtime", 1345 | "interval": "", 1346 | "legendFormat": "ID: {{id}}", 1347 | "refId": "A" 1348 | } 1349 | ], 1350 | "title": "RHI Thread Time (MS)", 1351 | "type": "timeseries" 1352 | }, 1353 | { 1354 | "datasource": { 1355 | "type": "prometheus", 1356 | "uid": "PBFA97CFB590B2093" 1357 | }, 1358 | "fieldConfig": { 1359 | "defaults": { 1360 | "color": { 1361 | "mode": "palette-classic" 1362 | }, 1363 | "custom": { 1364 | "axisLabel": "", 1365 | "axisPlacement": "auto", 1366 | "barAlignment": 0, 1367 | "drawStyle": "line", 1368 | "fillOpacity": 0, 1369 | "gradientMode": "none", 1370 | "hideFrom": { 1371 | "legend": false, 1372 | "tooltip": false, 1373 | "viz": false 1374 | }, 1375 | "lineInterpolation": "linear", 1376 | "lineWidth": 1, 1377 | "pointSize": 5, 1378 | "scaleDistribution": { 1379 | "type": "linear" 1380 | }, 1381 | "showPoints": "auto", 1382 | "spanNulls": false, 1383 | "stacking": { 1384 | "group": "A", 1385 | "mode": "none" 1386 | }, 1387 | "thresholdsStyle": { 1388 | "mode": "off" 1389 | } 1390 | }, 1391 | "mappings": [], 1392 | "thresholds": { 1393 | "mode": "absolute", 1394 | "steps": [ 1395 | { 1396 | "color": "green", 1397 | "value": null 1398 | }, 1399 | { 1400 | "color": "red", 1401 | "value": 80 1402 | } 1403 | ] 1404 | } 1405 | }, 1406 | "overrides": [] 1407 | }, 1408 | "gridPos": { 1409 | "h": 8, 1410 | "w": 12, 1411 | "x": 12, 1412 | "y": 52 1413 | }, 1414 | "id": 17, 1415 | "options": { 1416 | "legend": { 1417 | "calcs": [], 1418 | "displayMode": "list", 1419 | "placement": "right" 1420 | }, 1421 | "tooltip": { 1422 | "mode": "single", 1423 | "sort": "none" 1424 | } 1425 | }, 1426 | "targets": [ 1427 | { 1428 | "datasource": { 1429 | "type": "prometheus", 1430 | "uid": "PBFA97CFB590B2093" 1431 | }, 1432 | "exemplar": true, 1433 | "expr": "num_hangs", 1434 | "interval": "", 1435 | "legendFormat": "ID: {{id}}", 1436 | "refId": "A" 1437 | } 1438 | ], 1439 | "title": "Hung Frame Count", 1440 | "type": "timeseries" 1441 | } 1442 | ], 1443 | "refresh": "5s", 1444 | "schemaVersion": 36, 1445 | "style": "dark", 1446 | "tags": [], 1447 | "templating": { 1448 | "list": [] 1449 | }, 1450 | "time": { 1451 | "from": "now-5m", 1452 | "to": "now" 1453 | }, 1454 | "timepicker": {}, 1455 | "timezone": "", 1456 | "title": "Unreal Engine Metrics", 1457 | "uid": "LZLaM4tnk", 1458 | "version": 1, 1459 | "weekStart": "" 1460 | } -------------------------------------------------------------------------------- /Examples/Compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | unreal: 3 | image: "tensorworks/buccaneerdemo-application" 4 | command: [ "-PixelStreamingURL=ws://127.0.0.1:8888", "-BuccaneerURL=http://127.0.0.1:8000", "-RenderOffScreen", "-Res=1920x1080" ] 5 | container_name: unreal 6 | network_mode: "host" 7 | deploy: 8 | resources: 9 | reservations: 10 | devices: 11 | - driver: nvidia 12 | capabilities: [gpu] 13 | count: 1 14 | 15 | cirrus: 16 | image: "tensorworks/buccaneerdemo-cirrus" 17 | container_name: cirrus 18 | network_mode: "host" 19 | 20 | buccaneerserver: 21 | image: "tensorworks/buccaneerdemo-buccaneerserver" 22 | container_name: buccaneerserver 23 | network_mode: "host" 24 | 25 | prometheus: 26 | image: "prom/prometheus" 27 | container_name: prometheus 28 | network_mode: "host" 29 | volumes: 30 | - "../../Configs/prometheus.yml:/etc/prometheus/prometheus.yml" 31 | 32 | grafana: 33 | image: grafana/grafana 34 | container_name: grafana 35 | network_mode: "host" 36 | volumes: 37 | - "../../Configs/grafana-dashboard-config.yaml:/etc/grafana/provisioning/dashboards/grafana-dashboard-config.yaml" 38 | - "../../Configs/grafana-datasource-config.yaml:/etc/grafana/provisioning/datasources/grafana-datasource-config.yaml" 39 | - "../../Dashboards/:/etc/dashboards" 40 | 41 | loki: 42 | image: grafana/loki 43 | container_name: loki 44 | network_mode: "host" 45 | volumes: 46 | - "../../Configs/loki-local-config.yaml:/etc/loki/loki-local-config.yaml" 47 | 48 | promtail: 49 | image: grafana/promtail 50 | container_name: promtail 51 | command: -config.file=/etc/promtail/promtail-local-config.yaml 52 | network_mode: "host" 53 | volumes: 54 | - "../../Configs/promtail-local-config.yaml:/etc/promtail/promtail-local-config.yaml" 55 | - "eventslogs:/EventsServer" 56 | 57 | volumes: 58 | eventslogs: 59 | -------------------------------------------------------------------------------- /Examples/ThroughputTester/tester.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import uuid 4 | import random 5 | import threading 6 | import subprocess 7 | from datetime import datetime 8 | 9 | 10 | subprocess.check_call([sys.executable, '-m', 'pip', 'install', 11 | 'requests']) 12 | 13 | import requests 14 | 15 | MAX_THREADS = 100 16 | USLEEP = lambda x: time.sleep(x/1000000.0) 17 | BASE_URL = "http://localhost:8000" 18 | 19 | def thread_function(idx, result): 20 | # Sleep between 0 and 10 us. 21 | USLEEP(random.randint(0, 10)) 22 | 23 | test_duration = random.randint(300,600) 24 | start_time = datetime.now() 25 | delta = 0 26 | while(delta < test_duration): 27 | stats_json = { 28 | "id": str(uuid.uuid4()), 29 | "metadata": { 30 | }, 31 | "metrics": { 32 | "mean_fps": { 33 | "description": "The average fps", 34 | "value": random.randint(0, 60) 35 | }, 36 | "mean_frametime": { 37 | "description": "The average frame time", 38 | "value": random.randint(0, 60) 39 | }, 40 | "mean_gamethreadtime": { 41 | "description": "The average game thread time", 42 | "value": random.randint(0, 60) 43 | }, 44 | "mean_gputime": { 45 | "description": "The average gpu time", 46 | "value": random.randint(0, 60) 47 | }, 48 | "mean_rendertime": { 49 | "description": "The average render thread time", 50 | "value": random.randint(0, 60) 51 | }, 52 | "mean_rhithreadtime": { 53 | "description": "The average rhi thread time", 54 | "value": random.randint(0, 60) 55 | }, 56 | "num_hangs": { 57 | "description": "The average number of hangs over the recorded interval", 58 | "value": random.randint(0, 60) 59 | }, 60 | "memory_virtual": { 61 | "description": "The average virtual memory usage", 62 | "value": random.randint(0, 60) 63 | }, 64 | "memory_physical": { 65 | "description": "The average physical memory usage", 66 | "value": random.randint(0, 60) 67 | }, 68 | "memory_gpu": { 69 | "description": "The average gpu memory usage", 70 | "value": random.randint(0, 60) 71 | } 72 | } 73 | } 74 | 75 | res = requests.post(url=BASE_URL + "/stats", json=stats_json) 76 | if res.status_code != requests.codes.ok: 77 | result[idx] = False 78 | return 79 | 80 | USLEEP(1000000) 81 | delta = (datetime.now() - start_time).total_seconds() / 60.0 82 | 83 | result[idx] = True 84 | 85 | 86 | def main(): 87 | for thread_limit in range(10,MAX_THREADS,10): 88 | print("Testing {} concurrent instances".format(thread_limit)) 89 | threads = list() 90 | results = list() 91 | for idx in range(thread_limit): 92 | x = threading.Thread(target=thread_function, args=(idx, results)) 93 | threads.append(x) 94 | results.append(None) 95 | x.start() 96 | 97 | for _, thread in enumerate(threads): 98 | thread.join() 99 | 100 | if all(results): 101 | print("Success") 102 | else: 103 | print("Fail") 104 | 105 | main() -------------------------------------------------------------------------------- /Images/Buccaneer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/Buccaneer/8378e7ed619a639ec5e983c1fe2f515c1c8bf854/Images/Buccaneer.png -------------------------------------------------------------------------------- /Images/C++.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/Buccaneer/8378e7ed619a639ec5e983c1fe2f515c1c8bf854/Images/C++.png -------------------------------------------------------------------------------- /Images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/Buccaneer/8378e7ed619a639ec5e983c1fe2f515c1c8bf854/Images/Dashboard.png -------------------------------------------------------------------------------- /Images/EventBP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/Buccaneer/8378e7ed619a639ec5e983c1fe2f515c1c8bf854/Images/EventBP.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 TensorWorks Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Buccaneer.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "Buccaneer", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "William Belcher", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": true, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "BuccaneerCommon", 20 | "Type": "Runtime", 21 | "LoadingPhase": "Default" 22 | }, 23 | { 24 | "Name": "TimeSeriesDataEmitter", 25 | "Type": "Runtime", 26 | "LoadingPhase": "Default" 27 | }, 28 | { 29 | "Name": "SemanticEventEmitter", 30 | "Type": "Runtime", 31 | "LoadingPhase": "Default" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /Plugins/Buccaneer/README.md: -------------------------------------------------------------------------------- 1 | # Buccaneer 2 | 3 | 4 | 5 | ## Getting started 6 | 7 | To make it easy for you to get started with GitLab, here's a list of recommended next steps. 8 | 9 | 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)! 10 | 11 | ## Add your files 12 | 13 | - [ ] [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 14 | - [ ] [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: 15 | 16 | ``` 17 | cd existing_repo 18 | git remote add origin https://gitlab.com/TensorWorks/internal/buccaneer.git 19 | git branch -M main 20 | git push -uf origin main 21 | ``` 22 | 23 | ## Integrate with your tools 24 | 25 | - [ ] [Set up project integrations](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://gitlab.com/TensorWorks/internal/buccaneer/-/settings/integrations) 26 | 27 | ## Collaborate with your team 28 | 29 | - [ ] [Invite team members and collaborators](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/members/) 30 | - [ ] [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) 31 | - [ ] [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) 32 | - [ ] [Enable merge request approvals](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) 33 | - [ ] [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) 34 | 35 | ## Test and Deploy 36 | 37 | Use the built-in continuous integration in GitLab. 38 | 39 | - [ ] [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) 40 | - [ ] [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/) 41 | - [ ] [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) 42 | - [ ] [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/) 43 | - [ ] [Set up protected environments](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/ci/environments/protected_environments.html) 44 | 45 | *** 46 | 47 | # Editing this README 48 | 49 | 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. 50 | 51 | ## Suggestions for a good README 52 | 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. 53 | 54 | ## Name 55 | Choose a self-explaining name for your project. 56 | 57 | ## Description 58 | 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. 59 | 60 | ## Badges 61 | 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. 62 | 63 | ## Visuals 64 | 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. 65 | 66 | ## Installation 67 | 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. 68 | 69 | ## Usage 70 | 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. 71 | 72 | ## Support 73 | 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. 74 | 75 | ## Roadmap 76 | If you have ideas for releases in the future, it is a good idea to list them in the README. 77 | 78 | ## Contributing 79 | State if you are open to contributions and what your requirements are for accepting them. 80 | 81 | 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. 82 | 83 | 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. 84 | 85 | ## Authors and acknowledgment 86 | Show your appreciation to those who have contributed to the project. 87 | 88 | ## License 89 | For open source projects, say how it is licensed. 90 | 91 | ## Project status 92 | 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. 93 | 94 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/Buccaneer/8378e7ed619a639ec5e983c1fe2f515c1c8bf854/Plugins/Buccaneer/Resources/Icon128.png -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class BuccaneerCommon : ModuleRules 6 | { 7 | public BuccaneerCommon(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PrivateDependencyModuleNames.AddRange(new string[] 12 | { 13 | "Core", 14 | "Engine", 15 | "Json" 16 | }); 17 | 18 | PublicDependencyModuleNames.AddRange(new string[]{ 19 | "HTTP", 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "BuccaneerCommon.h" 4 | #include "Logging/LogMacros.h" 5 | 6 | DEFINE_LOG_CATEGORY(BuccaneerCommon); 7 | 8 | FBuccaneerCommonModule *FBuccaneerCommonModule::BuccaneerCommonModule = nullptr; 9 | 10 | void FBuccaneerCommonModule::StartupModule() 11 | { 12 | CVarBuccaneerEnableStats = IConsoleManager::Get().RegisterConsoleVariable( 13 | TEXT("Buccaneer.EnableStats"), 14 | true, 15 | TEXT("Disables the collection of and logging of performance metrics"), 16 | ECVF_Default); 17 | 18 | CVarBuccaneerEnableEvents = IConsoleManager::Get().RegisterConsoleVariable( 19 | TEXT("Buccaneer.EnableEvents"), 20 | true, 21 | TEXT("Disables the collection and logging of semantic events"), 22 | ECVF_Default); 23 | 24 | Setup(); 25 | } 26 | 27 | void FBuccaneerCommonModule::ShutdownModule() 28 | { 29 | } 30 | 31 | void FBuccaneerCommonModule::Setup() 32 | { 33 | ParseCommandLineOption(TEXT("BuccaneerEnableStats"), CVarBuccaneerEnableStats); 34 | ParseCommandLineOption(TEXT("BuccaneerEnableEvents"), CVarBuccaneerEnableEvents); 35 | 36 | if (!FParse::Value(FCommandLine::Get(), TEXT("BuccaneerURL="), BuccaneerURL)) 37 | { 38 | FString BuccaneerIP; 39 | uint16 BuccaneerPort; 40 | if (FParse::Value(FCommandLine::Get(), TEXT("BuccaneerIP="), BuccaneerIP) && FParse::Value(FCommandLine::Get(), TEXT("BuccaneerPort="), BuccaneerPort)) 41 | { 42 | // build the proper url. 43 | BuccaneerURL = FString::Printf(TEXT("http://%s:%d"), *BuccaneerIP, BuccaneerPort); 44 | } 45 | } 46 | 47 | if (BuccaneerURL.IsEmpty()) 48 | { 49 | UE_LOG(BuccaneerCommon, Warning, TEXT("Buccanner events and stats disabled, provide `BuccaneerURL` cmd-args to enable it")); 50 | CVarBuccaneerEnableEvents->Set(false, ECVF_SetByCommandline); 51 | CVarBuccaneerEnableStats->Set(false, ECVF_SetByCommandline); 52 | return; 53 | } 54 | 55 | // Try and parse an instance ID 56 | if (!FParse::Value(FCommandLine::Get(), TEXT("BuccaneerID="), InstanceID)) 57 | { 58 | // Try and parse a pixel streaming ID for users who don't want to pollute their command line by specifying two IDs 59 | if (!FParse::Value(FCommandLine::Get(), TEXT("PixelStreamingID="), InstanceID)) 60 | { 61 | // Generate an instance ID if one isn't provided 62 | InstanceID = FGuid::NewGuid().ToString(); 63 | } 64 | } 65 | 66 | // Additional Metadata 67 | MetadataJson = MakeShareable(new FJsonObject()); 68 | FString CmdLineMetadata; 69 | if (FParse::Value(FCommandLine::Get(), TEXT("BuccaneerMetadata="), CmdLineMetadata)) 70 | { 71 | UE_LOG(BuccaneerCommon, Warning, TEXT("%s"), *CmdLineMetadata); 72 | TArray ParsedMetadata; 73 | CmdLineMetadata.ParseIntoArray(ParsedMetadata, TEXT(";"), false); 74 | for (FString Element : ParsedMetadata) 75 | { 76 | if(Element.IsEmpty()) 77 | { 78 | continue; 79 | } 80 | 81 | FString Key, Value; 82 | Element.Split(TEXT(":"), &Key, &Value); 83 | if(Key.IsEmpty() || Value.IsEmpty()) 84 | { 85 | continue; 86 | } 87 | 88 | MetadataJson->SetField(*Key, MakeShared((TEXT("%s"), *Value))); 89 | } 90 | } 91 | 92 | SetupComplete.Broadcast(); 93 | } 94 | 95 | FBuccaneerCommonModule *FBuccaneerCommonModule::GetModule() 96 | { 97 | if (BuccaneerCommonModule) 98 | { 99 | return BuccaneerCommonModule; 100 | } 101 | FBuccaneerCommonModule *Module = FModuleManager::Get().LoadModulePtr("BuccaneerCommon"); 102 | if (Module) 103 | { 104 | BuccaneerCommonModule = Module; 105 | } 106 | return BuccaneerCommonModule; 107 | } 108 | 109 | void FBuccaneerCommonModule::SendStats(TSharedPtr JsonObject) 110 | { 111 | JsonObject->SetField("id", MakeShared((TEXT("%s"), *InstanceID))); 112 | JsonObject->SetField("metadata", MakeShared(MetadataJson)); 113 | SendHTTP(BuccaneerURL + FString("/stats"), JsonObject); 114 | } 115 | 116 | void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) 117 | { 118 | JsonObject->SetField("id", MakeShared((TEXT("%s"), *InstanceID))); 119 | SendHTTP(BuccaneerURL + FString("/event"), JsonObject); 120 | } 121 | 122 | void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonObject) 123 | { 124 | FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); 125 | 126 | FString body; 127 | TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&body); 128 | if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) 129 | { 130 | UE_LOG(BuccaneerCommon, Warning, TEXT("Cannot serialize json object")); 131 | } 132 | 133 | HttpRequest->SetURL(URL); 134 | HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); 135 | HttpRequest->SetVerb(TEXT("POST")); 136 | HttpRequest->SetContentAsString(body); 137 | bool bInFlight = true; 138 | HttpRequest->OnProcessRequestComplete().BindLambda( 139 | [&bInFlight](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) 140 | { 141 | FString ResponseStr, ErrorStr; 142 | 143 | if (bSucceeded && HttpResponse.IsValid()) 144 | { 145 | ResponseStr = HttpResponse->GetContentAsString(); 146 | if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) 147 | { 148 | ErrorStr = FString::Printf(TEXT("Invalid response. code=%d error=%s"), 149 | HttpResponse->GetResponseCode(), *ResponseStr); 150 | } 151 | } 152 | else 153 | { 154 | ErrorStr = TEXT("No response"); 155 | } 156 | 157 | if (!ErrorStr.IsEmpty()) 158 | { 159 | UE_LOG(BuccaneerCommon, Warning, TEXT("Push event response: %s"), *ErrorStr); 160 | } 161 | 162 | bInFlight = false; 163 | }); 164 | HttpRequest->ProcessRequest(); 165 | } 166 | 167 | void FBuccaneerCommonModule::ParseCommandLineOption(const TCHAR *Match, IConsoleVariable *CVar) 168 | { 169 | FString ValueMatch(Match); 170 | ValueMatch.Append(TEXT("=")); 171 | FString Value; 172 | if (FParse::Value(FCommandLine::Get(), *ValueMatch, Value)) 173 | { 174 | if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) 175 | { 176 | CVar->Set(true, ECVF_SetByCommandline); 177 | } 178 | else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) 179 | { 180 | CVar->Set(false, ECVF_SetByCommandline); 181 | } 182 | } 183 | else if (FParse::Param(FCommandLine::Get(), Match)) 184 | { 185 | CVar->Set(true, ECVF_SetByCommandline); 186 | } 187 | } 188 | 189 | IMPLEMENT_MODULE(FBuccaneerCommonModule, BuccaneerCommon) -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Modules/ModuleManager.h" 5 | #include "Interfaces/IHttpRequest.h" 6 | #include "Interfaces/IHttpResponse.h" 7 | #include "HttpModule.h" 8 | #include "Dom/JsonObject.h" 9 | #include "Serialization/JsonWriter.h" 10 | #include "HAL/IConsoleManager.h" 11 | #include "Misc/CommandLine.h" 12 | 13 | #include 14 | #include 15 | 16 | DECLARE_MULTICAST_DELEGATE(FOnSetupComplete); 17 | 18 | DECLARE_LOG_CATEGORY_EXTERN(BuccaneerCommon, Log, All); 19 | 20 | class BUCCANEERCOMMON_API FBuccaneerCommonModule : public IModuleInterface 21 | { 22 | public: 23 | /** IModuleInterface implementation */ 24 | virtual void StartupModule() override; 25 | virtual void ShutdownModule() override; 26 | 27 | static FBuccaneerCommonModule *GetModule(); 28 | static void ParseCommandLineOption(const TCHAR *Match, IConsoleVariable *CVar); 29 | 30 | void SendStats(TSharedPtr JsonObject); 31 | void SendEvent(TSharedPtr JsonObject); 32 | 33 | FOnSetupComplete SetupComplete; 34 | 35 | IConsoleVariable *CVarBuccaneerEnableStats; 36 | IConsoleVariable *CVarBuccaneerEnableEvents; 37 | 38 | private: 39 | void Setup(); 40 | 41 | void SendHTTP(FString URL, TSharedPtr JsonObject); 42 | 43 | FString BuccaneerURL; 44 | FString InstanceID; 45 | TSharedPtr MetadataJson; 46 | 47 | static FBuccaneerCommonModule *BuccaneerCommonModule; 48 | }; 49 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "SemanticEventEmitter.h" 4 | #include "CoreMinimal.h" 5 | #include "HAL/IConsoleManager.h" 6 | #include "Logging/LogMacros.h" 7 | #include "Dom/JsonObject.h" 8 | #include "BuccaneerCommon.h" 9 | 10 | #define LOCTEXT_NAMESPACE "SemanticEventEmitterModule" 11 | 12 | DEFINE_LOG_CATEGORY(SemanticEventEmitter); 13 | 14 | FSemanticEventEmitterModule *FSemanticEventEmitterModule::SemanticEmitterModule = nullptr; 15 | 16 | void FSemanticEventEmitterModule::StartupModule() 17 | { 18 | 19 | } 20 | 21 | void FSemanticEventEmitterModule::ShutdownModule() 22 | { 23 | } 24 | 25 | void FSemanticEventEmitterModule::EmitSemanticEvent(FString Level, FString Event) 26 | { 27 | if (!FBuccaneerCommonModule::GetModule()->CVarBuccaneerEnableEvents->GetBool()) 28 | { 29 | return; 30 | } 31 | 32 | UE_LOG(SemanticEventEmitter, Verbose, TEXT("%s: %s"), *Level, *Event); 33 | 34 | TSharedPtr JsonObject = MakeShareable(new FJsonObject()); 35 | JsonObject->SetField("level", MakeShared((TEXT("%s"), *Level))); 36 | JsonObject->SetField("message", MakeShared((TEXT("%s"), *Event))); 37 | 38 | FBuccaneerCommonModule::GetModule()->SendEvent(JsonObject); 39 | } 40 | 41 | FSemanticEventEmitterModule *FSemanticEventEmitterModule::GetModule() 42 | { 43 | if (SemanticEmitterModule) 44 | { 45 | return SemanticEmitterModule; 46 | } 47 | FSemanticEventEmitterModule *Module = FModuleManager::Get().GetModulePtr("SemanticEventEmitter"); 48 | if (Module) 49 | { 50 | SemanticEmitterModule = Module; 51 | } 52 | return SemanticEmitterModule; 53 | } 54 | 55 | #undef LOCTEXT_NAMESPACE 56 | 57 | IMPLEMENT_MODULE(FSemanticEventEmitterModule, SemanticEventEmitter) -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "SemanticEventBlueprintFunctionLibrary.h" 4 | 5 | 6 | void USemanticEventEmitterBlueprintLibrary::EmitSemanticEvent(FString Level, FString Event) 7 | { 8 | FSemanticEventEmitterModule* Module = FSemanticEventEmitterModule::GetModule(); 9 | if(Module) 10 | { 11 | Module->EmitSemanticEvent(Level, Event); 12 | } 13 | } -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "SemanticEventEmitter.h" 7 | #include "Kismet/BlueprintFunctionLibrary.h" 8 | #include "SemanticEventBlueprintFunctionLibrary.generated.h" 9 | 10 | UCLASS() 11 | class SEMANTICEVENTEMITTER_API USemanticEventEmitterBlueprintLibrary : public UBlueprintFunctionLibrary 12 | { 13 | GENERATED_BODY() 14 | public: 15 | UFUNCTION(BlueprintCallable, Category="Buccaneer") 16 | static void EmitSemanticEvent(FString Level, FString Event); 17 | }; 18 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | 9 | DECLARE_LOG_CATEGORY_EXTERN(SemanticEventEmitter, Log, All); 10 | 11 | class SEMANTICEVENTEMITTER_API FSemanticEventEmitterModule : public IModuleInterface 12 | { 13 | public: 14 | /** IModuleInterface implementation */ 15 | virtual void StartupModule() override; 16 | virtual void ShutdownModule() override; 17 | void EmitSemanticEvent(FString Level, FString Event); 18 | static FSemanticEventEmitterModule *GetModule(); 19 | 20 | private: 21 | static FSemanticEventEmitterModule *SemanticEmitterModule; 22 | }; 23 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/SemanticEventEmitter/SemanticEventEmitter.build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class SemanticEventEmitter : ModuleRules 6 | { 7 | public SemanticEventEmitter(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PrivateDependencyModuleNames.AddRange(new string[] 12 | { 13 | "Core", 14 | "RHI", 15 | "CoreUObject", 16 | "Engine", 17 | "Slate", 18 | "SlateCore", 19 | "RenderCore", 20 | "HTTP", 21 | "Json", 22 | "BuccaneerCommon" 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "TimeSeriesDataEmitter.h" 4 | #include "CoreMinimal.h" 5 | #include "Engine/Engine.h" 6 | #include "RHI.h" 7 | #include "SemanticEventEmitter.h" 8 | #include "BuccaneerCommon.h" 9 | #include "Stats/Stats.h" 10 | #include "Stats/StatsData.h" 11 | #define LOCTEXT_NAMESPACE "FTimeSeriesDataEmitterModule" 12 | 13 | #define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ 14 | ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; 15 | 16 | DEFINE_LOG_CATEGORY(TimeSeriesDataEmitter); 17 | 18 | void FTimeSeriesDataEmitterModule::StartupModule() 19 | { 20 | StatDescriptionMap = { 21 | { "mean_fps", "The average fps" }, 22 | { "mean_frametime", "The average frametime" }, 23 | { "mean_gamethreadtime", "The average game thread time" }, 24 | { "mean_gputime", "The average gpu time" }, 25 | { "mean_rendertime", "The average render thread time" }, 26 | { "mean_rhithreadtime", "The average rhi thread time" }, 27 | { "memory_virtual", "The virtual memory usage" }, 28 | { "memory_physical", "The physical memory usage" }, 29 | { "memory_gpu", "The gpu memory usage" }, 30 | { "num_hangs", "The number of frames hung in the recording interval" } 31 | }; 32 | 33 | MetricJson = MakeShareable(new FJsonObject()); 34 | JsonObject = MakeShareable(new FJsonObject()); 35 | JsonObject->SetField(TEXT("metrics"), MakeShared(MetricJson)); 36 | 37 | LastTickTime = InterimStart = FPlatformTime::Seconds(); 38 | } 39 | 40 | void FTimeSeriesDataEmitterModule::UpdateMetric(FString Name, double Value) 41 | { 42 | if(!StatDescriptionMap.Contains(Name)) 43 | { 44 | UE_LOG(TimeSeriesDataEmitter, Log, TEXT("No description for metric (%s)"), *Name); 45 | return; 46 | } 47 | 48 | TSharedPtr MetricInfoJson = MakeShareable(new FJsonObject()); 49 | MetricInfoJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[Name]))); 50 | MetricInfoJson->SetField("value", MakeShared(Value)); 51 | MetricJson->SetField(*Name, MakeShared(MetricInfoJson)); 52 | } 53 | 54 | void FTimeSeriesDataEmitterModule::ShutdownModule() 55 | { 56 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 57 | // we call this function before unloading the module. 58 | } 59 | 60 | bool FTimeSeriesDataEmitterModule::IsTickableWhenPaused() const 61 | { 62 | return true; 63 | } 64 | 65 | bool FTimeSeriesDataEmitterModule::IsTickableInEditor() const 66 | { 67 | return true; 68 | } 69 | 70 | void FTimeSeriesDataEmitterModule::Tick(float DeltaTime) 71 | { 72 | if (!FBuccaneerCommonModule::GetModule()->CVarBuccaneerEnableStats->GetBool()) 73 | { 74 | // Performance profiling hasn't been inititialized. Don't continue 75 | return; 76 | } 77 | 78 | double NowTime = FPlatformTime::Seconds(); 79 | 80 | InterimFrameCount++; 81 | double FrameTime = NowTime - LastTickTime; 82 | // Ignore frames that take longer than 250ms. Count these as a hang 83 | if (FrameTime > 0.25) 84 | { 85 | InterimHangCount++; 86 | FSemanticEventEmitterModule *Module = FSemanticEventEmitterModule::GetModule(); 87 | if (Module) 88 | { 89 | Module->EmitSemanticEvent(FString(TEXT("warning")), FString(TEXT("Frame hung"))); 90 | } 91 | } 92 | else 93 | { 94 | double GameThreadTime = FPlatformTime::ToMilliseconds(GGameThreadTime); 95 | double GPUFrameTime = FPlatformTime::ToMilliseconds(RHIGetGPUFrameCycles(0)); 96 | double RenderThreadTime = FPlatformTime::ToMilliseconds(GRenderThreadTime); 97 | double RHIThreadTime = FPlatformTime::ToMilliseconds(GRHIThreadTime); 98 | 99 | InterimMeanFrameTime = COMPUTE_MEAN(InterimMeanFrameTime, FrameTime * 1000, InterimFrameCount); 100 | InterimMeanGameThreadTime = COMPUTE_MEAN(InterimMeanGameThreadTime, GameThreadTime, InterimFrameCount); 101 | InterimMeanGPUTime = COMPUTE_MEAN(InterimMeanGPUTime, GPUFrameTime, InterimFrameCount); 102 | InterimMeanRenderThreadTime = COMPUTE_MEAN(InterimMeanRenderThreadTime, RenderThreadTime, InterimFrameCount); 103 | InterimMeanRHIThreadTime = COMPUTE_MEAN(InterimMeanRHIThreadTime, RHIThreadTime, InterimFrameCount); 104 | 105 | ComputeUsedMemory(); 106 | } 107 | if ((NowTime - InterimStart) >= InterimDuration) 108 | { 109 | PushStatsHTTP(); 110 | InterimStart = NowTime; 111 | InterimHangCount = 0; 112 | InterimFrameCount = 1; 113 | } 114 | LastTickTime = NowTime; 115 | } 116 | 117 | void FTimeSeriesDataEmitterModule::ComputeUsedMemory() 118 | { 119 | FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats(); 120 | 121 | const unsigned int BitsPerMB = (8u * 1024u * 1024u); 122 | UsedVirtualMemory = static_cast(MemoryStats.UsedVirtual) / BitsPerMB; 123 | UsedPhysicalMemory = static_cast(MemoryStats.UsedPhysical) / BitsPerMB; 124 | 125 | #if !UE_BUILD_SHIPPING 126 | TArray Stats; 127 | GetPermanentStats(Stats); 128 | 129 | FName NAME_STATGROUP_RHI(FStatGroup_STATGROUP_RHI::GetGroupName()); 130 | int64 TotalMemory = 0; 131 | for (int32 Index = 0; Index < Stats.Num(); Index++) 132 | { 133 | FStatMessage const &Meta = Stats[Index]; 134 | FName LastGroup = Meta.NameAndInfo.GetGroupName(); 135 | if (LastGroup == NAME_STATGROUP_RHI && Meta.NameAndInfo.GetFlag(EStatMetaFlags::IsMemory)) 136 | { 137 | TotalMemory += Meta.GetValue_int64(); 138 | } 139 | } 140 | UsedGPUMemory = (double)(TotalMemory / 1024.f / 1024.f); 141 | #endif 142 | } 143 | 144 | void FTimeSeriesDataEmitterModule::PushStatsHTTP() 145 | { 146 | // Collected Metrics 147 | // name value 148 | UpdateMetric("mean_fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f); 149 | UpdateMetric("mean_frametime", InterimMeanFrameTime); 150 | UpdateMetric("mean_gamethreadtime", InterimMeanGameThreadTime); 151 | UpdateMetric("mean_gputime", InterimMeanGPUTime); 152 | UpdateMetric("mean_rendertime", InterimMeanRenderThreadTime); 153 | UpdateMetric("mean_rhithreadtime", InterimMeanRHIThreadTime); 154 | UpdateMetric("memory_virtual", UsedVirtualMemory); 155 | UpdateMetric("memory_physical", UsedPhysicalMemory); 156 | UpdateMetric("memory_gpu", UsedGPUMemory); 157 | UpdateMetric("num_hangs", InterimHangCount); 158 | 159 | FBuccaneerCommonModule::GetModule()->SendStats(JsonObject); 160 | } 161 | 162 | TStatId FTimeSeriesDataEmitterModule::GetStatId() const 163 | { 164 | RETURN_QUICK_DECLARE_CYCLE_STAT(FTimeSeriesDataEmitterModule, STATGROUP_Tickables); 165 | } 166 | 167 | #undef LOCTEXT_NAMESPACE 168 | 169 | IMPLEMENT_MODULE(FTimeSeriesDataEmitterModule, TimeSeriesDataEmitter) -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | #include "BuccaneerCommon.h" 8 | #include "Tickable.h" 9 | #include "Dom/JsonObject.h" 10 | 11 | DECLARE_LOG_CATEGORY_EXTERN(TimeSeriesDataEmitter, Log, All); 12 | 13 | class TIMESERIESDATAEMITTER_API FTimeSeriesDataEmitterModule : public IModuleInterface, public FTickableGameObject 14 | { 15 | public: 16 | /** IModuleInterface implementation */ 17 | virtual void StartupModule() override; 18 | virtual void ShutdownModule() override; 19 | 20 | // FTickableGameObject 21 | bool IsTickableWhenPaused() const override; 22 | bool IsTickableInEditor() const override; 23 | void Tick(float DeltaTime) override; 24 | TStatId GetStatId() const override; 25 | 26 | private: 27 | void PushStatsHTTP(); 28 | void ComputeUsedMemory(); 29 | void UpdateMetric(FString Name, double Value); 30 | 31 | // Time keeping variables 32 | double LastTickTime = 0.0; 33 | double InterimStart = 0.0; 34 | double InterimDuration = 1.0; 35 | // Rolling average of times recorded during the defined period 36 | double InterimMeanFrameTime = 0.0; 37 | double InterimMeanGameThreadTime = 0.0; 38 | double InterimMeanRenderThreadTime = 0.0; 39 | double InterimMeanRHIThreadTime = 0.0; 40 | double InterimMeanGPUTime = 0.0; 41 | // Memory metrics 42 | double UsedVirtualMemory = 0.0; 43 | double UsedPhysicalMemory = 0.0; 44 | double UsedGPUMemory = 0.0; 45 | // Metrics to store information about the number of hangs and number of frames recorded during the interim 46 | // (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) 47 | double InterimHangCount = 0.0; 48 | uint32 InterimFrameCount = 1; 49 | 50 | // Variable for storing logging URL and logging object 51 | TSharedPtr JsonObject; 52 | TSharedPtr MetricJson; 53 | 54 | TMap StatDescriptionMap; 55 | }; 56 | -------------------------------------------------------------------------------- /Plugins/Buccaneer/Source/TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class TimeSeriesDataEmitter : ModuleRules 6 | { 7 | public TimeSeriesDataEmitter(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PrivateDependencyModuleNames.AddRange(new string[] 12 | { 13 | "Core", 14 | "RHI", 15 | "CoreUObject", 16 | "Engine", 17 | "Slate", 18 | "SlateCore", 19 | "RenderCore", 20 | "SemanticEventEmitter", 21 | "Json" 22 | }); 23 | 24 | PublicDependencyModuleNames.AddRange(new string[] 25 | { 26 | "BuccaneerCommon", 27 | }); 28 | 29 | PublicDefinitions.Add("_USE_32BIT_TIME_T"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Plugins/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "Buccaneer4PixelStreaming", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": true, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "Buccaneer4PixelStreaming", 20 | "Type": "Runtime", 21 | "LoadingPhase": "Default" 22 | } 23 | ], 24 | "Plugins": [ 25 | { 26 | "Name": "PixelStreaming", 27 | "Enabled": true 28 | }, 29 | { 30 | "Name": "Buccaneer", 31 | "Enabled": true 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/Buccaneer/8378e7ed619a639ec5e983c1fe2f515c1c8bf854/Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png -------------------------------------------------------------------------------- /Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class Buccaneer4PixelStreaming : ModuleRules 6 | { 7 | public Buccaneer4PixelStreaming(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange( 12 | new string[] 13 | { 14 | "Core", 15 | "PixelStreaming", 16 | "BuccaneerCommon", 17 | "Json" 18 | }); 19 | 20 | 21 | PrivateDependencyModuleNames.AddRange( 22 | new string[] 23 | { 24 | "CoreUObject", 25 | "Engine", 26 | "Slate", 27 | "SlateCore", 28 | "PixelStreaming", 29 | "BuccaneerCommon" 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "Buccaneer4PixelStreaming.h" 4 | #include "Logging/LogMacros.h" 5 | #include "PixelStreamingDelegates.h" 6 | 7 | #define LOCTEXT_NAMESPACE "FBuccaneer4PixelStreamingModule" 8 | 9 | 10 | DEFINE_LOG_CATEGORY(BuccaneerPixelStreaming); 11 | 12 | namespace Buccaneer4PixelStreaming 13 | { 14 | 15 | } 16 | 17 | void FBuccaneer4PixelStreamingModule::StartupModule() 18 | { 19 | StatDescriptionMap = { 20 | {"jitterBufferDelay", "jitterBufferDelay"}, 21 | {"framesSent", "framesSent"}, 22 | {"framesPerSecond", "framesPerSecond"}, 23 | {"framesReceived", "framesReceived"}, 24 | {"framesDropped", "framesDropped"}, 25 | {"framesDecoded", "framesDecoded"}, 26 | {"framesCorrupted", "framesCorrupted"}, 27 | {"partialFramesLost", "partialFramesLost"}, 28 | {"fullFramesLost", "fullFramesLost"}, 29 | {"hugeFramesSent", "hugeFramesSent"}, 30 | {"jitterBufferTargetDelay", "jitterBufferTargetDelay"}, 31 | {"interruptionCount", "interruptionCount"}, 32 | {"totalInterruptionDuration", "totalInterruptionDuration"}, 33 | {"freezeCount", "freezeCount"}, 34 | {"pauseCount", "pauseCount"}, 35 | {"totalFreezesDuration", "totalFreezesDuration"}, 36 | {"totalPausesDuration", "totalPausesDuration"}, 37 | {"firCount", "firCount"}, 38 | {"pliCount", "pliCount"}, 39 | {"nackCount", "nackCount"}, 40 | {"sliCount", "sliCount"}, 41 | {"retransmittedBytesSent", "retransmittedBytesSent"}, 42 | {"totalEncodedBytesTarget", "totalEncodedBytesTarget"}, 43 | {"keyFramesEncoded", "keyFramesEncoded"}, 44 | {"frameWidth", "frameWidth"}, 45 | {"frameHeight", "frameHeight"}, 46 | {"bytesSent", "bytesSent"}, 47 | {"qpSum", "qpSum"}, 48 | {"totalEncodeTime", "totalEncodeTime"}, 49 | {"totalPacketSendDelay", "totalPacketSendDelay"}, 50 | {"packetSendDelay", "packetSendDelay"}, 51 | {"framesEncoded", "framesEncoded"}, 52 | {"transmitFps", "transmit fps"}, 53 | {"bitrate", "bitrate (kb/s)"}, 54 | {"qp", "qp"}, 55 | {"encodeTime", "encode time (ms)"}, 56 | {"encodeFps", "encode fps"}, 57 | {"captureToSend", "capture to send (ms)"}, 58 | {"captureFps", "capture fps"} 59 | }; 60 | 61 | CVarBuccaneer4PixelStreamingEnableStats = IConsoleManager::Get().RegisterConsoleVariable( 62 | TEXT("Buccaneer4PixelStreaming.EnableStats"), 63 | true, 64 | TEXT("Disables the collection and logging of Pixel Streaming stats with Buccaneer"), 65 | ECVF_Default); 66 | 67 | Setup(); 68 | } 69 | 70 | void FBuccaneer4PixelStreamingModule::Setup() 71 | { 72 | FBuccaneerCommonModule::ParseCommandLineOption(TEXT("Buccaneer4PixelStreamingEnableStats"), CVarBuccaneer4PixelStreamingEnableStats); 73 | 74 | LoggingStart = FPlatformTime::Seconds(); 75 | ReportingInterval = 1; 76 | 77 | JsonObject = MakeShareable(new FJsonObject()); 78 | 79 | if (UPixelStreamingDelegates* Delegates = UPixelStreamingDelegates::GetPixelStreamingDelegates()) 80 | { 81 | Delegates->OnStatChangedNative.AddRaw(this, &FBuccaneer4PixelStreamingModule::ConsumeStat); 82 | } 83 | } 84 | 85 | void FBuccaneer4PixelStreamingModule::ShutdownModule() 86 | { 87 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 88 | // we call this function before unloading the module. 89 | } 90 | 91 | void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue) 92 | { 93 | if(!CVarBuccaneer4PixelStreamingEnableStats->GetBool() || PlayerId == TEXT("Application")) 94 | { 95 | return; 96 | } 97 | /** 98 | * "{StatName}": { 99 | * "description": "{StatDescription}", 100 | * "value": [ 101 | * "{PlayerId}": {StatValue} 102 | * ] 103 | * } 104 | */ 105 | 106 | const TSharedPtr* MetricJson = nullptr; 107 | if(JsonObject->TryGetObjectField((TEXT("%s"), *StatName.ToString()), MetricJson)) 108 | { 109 | TArray> ValueArray = (*MetricJson)->GetArrayField(TEXT("value")); 110 | 111 | bool bRequiresCreation = true; 112 | for (int i = 0; i < ValueArray.Num(); i++) 113 | { 114 | const TSharedPtr ValueJson = ValueArray[i]->AsObject(); 115 | double val; 116 | if(ValueJson->TryGetNumberField(*PlayerId, val)) 117 | { 118 | // This metric already has this player id, update the value accordingly 119 | ValueJson->SetField(*PlayerId, MakeShared(StatValue)); 120 | bRequiresCreation = false; 121 | break; 122 | } 123 | } 124 | 125 | if(bRequiresCreation) 126 | { 127 | TSharedPtr ValueJson = MakeShareable(new FJsonObject()); 128 | ValueJson->SetField(*PlayerId, MakeShared(StatValue)); 129 | 130 | ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); 131 | 132 | (*MetricJson)->SetArrayField((TEXT("value")), ValueArray); 133 | } 134 | } 135 | else 136 | { 137 | if(!StatDescriptionMap.Contains(*StatName.ToString())) 138 | { 139 | UE_LOG(BuccaneerPixelStreaming, Log, TEXT("%s"), *StatName.ToString()); 140 | return; 141 | } 142 | TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); 143 | NewMetricJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[*StatName.ToString()]))); 144 | 145 | 146 | TSharedPtr ValueJson = MakeShareable(new FJsonObject()); 147 | ValueJson->SetField(*PlayerId, MakeShared(StatValue)); 148 | 149 | TArray> ValueArray; 150 | ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); 151 | 152 | NewMetricJson->SetArrayField((TEXT("value")), ValueArray); 153 | 154 | JsonObject->SetObjectField((TEXT("%s"), *StatName.ToString()), NewMetricJson); 155 | } 156 | 157 | double NowTime = FPlatformTime::Seconds(); 158 | if ( (NowTime - LoggingStart) >= ReportingInterval ) 159 | { 160 | LoggingStart = NowTime; 161 | TSharedPtr PayloadJson = MakeShareable(new FJsonObject()); 162 | PayloadJson->SetObjectField(TEXT("metrics"), JsonObject); 163 | FBuccaneerCommonModule::GetModule()->SendStats(PayloadJson); 164 | 165 | JsonObject = MakeShareable(new FJsonObject()); 166 | } 167 | } 168 | 169 | #undef LOCTEXT_NAMESPACE 170 | 171 | IMPLEMENT_MODULE(FBuccaneer4PixelStreamingModule, Buccaneer4PixelStreaming) -------------------------------------------------------------------------------- /Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "BuccaneerCommon.h" 7 | #include "Modules/ModuleManager.h" 8 | #include "PixelStreamingDelegates.h" 9 | #include "IPixelStreamingModule.h" 10 | 11 | #include "Dom/JsonObject.h" 12 | #include "PixelStreamingPlayerId.h" 13 | 14 | DECLARE_LOG_CATEGORY_EXTERN(BuccaneerPixelStreaming, Log, All); 15 | 16 | class FBuccaneer4PixelStreamingModule : public IModuleInterface 17 | { 18 | public: 19 | 20 | /** IModuleInterface implementation */ 21 | virtual void StartupModule() override; 22 | virtual void ShutdownModule() override; 23 | void Setup(); 24 | UFUNCTION() 25 | void ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue); 26 | 27 | IConsoleVariable* CVarBuccaneer4PixelStreamingEnableStats; 28 | 29 | private: 30 | 31 | double LoggingStart; 32 | double ReportingInterval; 33 | 34 | TMap StatDescriptionMap; 35 | 36 | TSharedPtr JsonObject; 37 | }; 38 | -------------------------------------------------------------------------------- /Server/BuccaneerServer/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.20.6-alpine 4 | 5 | WORKDIR /app 6 | 7 | COPY go.mod ./ 8 | COPY go.sum ./ 9 | RUN go mod download 10 | 11 | COPY *.go ./ 12 | 13 | RUN go build -o /BuccaneerServer 14 | 15 | EXPOSE 8000 16 | 17 | CMD ["/BuccaneerServer"] -------------------------------------------------------------------------------- /Server/BuccaneerServer/go.mod: -------------------------------------------------------------------------------- 1 | module tensorworks.com.au/buccaneer-server 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/beorn7/perks v1.0.1 // indirect 7 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 8 | github.com/golang/protobuf v1.5.3 // indirect 9 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 10 | github.com/prometheus/client_golang v1.16.0 // indirect 11 | github.com/prometheus/client_model v0.3.0 // indirect 12 | github.com/prometheus/common v0.42.0 // indirect 13 | github.com/prometheus/procfs v0.10.1 // indirect 14 | golang.org/x/sys v0.8.0 // indirect 15 | google.golang.org/protobuf v1.30.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /Server/BuccaneerServer/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 6 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 7 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 8 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 9 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 10 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 11 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 12 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 13 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 14 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 15 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 16 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 17 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 18 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 19 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 20 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 21 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 22 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 23 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 26 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 27 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 28 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 29 | -------------------------------------------------------------------------------- /Server/BuccaneerServer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "time" 11 | 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | ) 15 | 16 | var ( 17 | // global map to store our collectors 18 | collectors sync.Map 19 | collectorsMutex = sync.RWMutex{} 20 | ) 21 | 22 | type collector struct { 23 | // metadata are labels that don't change during runtime. Eg you could tag instances with 24 | // their corresponding CL number to gain insight into performance regressions over time 25 | metadata map[string]string 26 | 27 | // metrics contain both a long description and a value 28 | metrics map[string]metric 29 | 30 | // unix timestamp to keep track of when this collector was last updated. 31 | // enables removal of stale collectors 32 | lastUpdateTime *int64 33 | } 34 | 35 | type metric struct { 36 | // long, human-readable, description 37 | description *prometheus.Desc 38 | // 39 | records map[string]record 40 | } 41 | 42 | type record struct { 43 | value float64 44 | time int64 45 | } 46 | 47 | type arbitraryJson map[string]interface{} 48 | 49 | func (collector *collector) Describe(ch chan<- *prometheus.Desc) { 50 | for _, metric := range collector.metrics { 51 | ch <- metric.description 52 | } 53 | } 54 | 55 | func (collector *collector) Collect(ch chan<- prometheus.Metric) { 56 | for _, metric := range collector.metrics { 57 | 58 | if record, ok := metric.records[""]; ok { 59 | // if we can get a value from no key, this indicates a system metric and isn't per player 60 | ch <- prometheus.MustNewConstMetric(metric.description, prometheus.GaugeValue, record.value) 61 | } else { 62 | // otherwise we must loop through all the player ids 63 | for key, record := range metric.records { 64 | ch <- prometheus.MustNewConstMetric(metric.description, prometheus.GaugeValue, record.value, key) 65 | } 66 | } 67 | } 68 | } 69 | 70 | func removeStaleCollectors() { 71 | // sub-routine to remove stale collectors 72 | for { 73 | collectorsMutex.Lock() 74 | collectors.Range(func(key, currCollector interface{}) bool { 75 | collector := currCollector.(collector) 76 | // TODO (belchy06): Perhaps this time should be configurable 77 | for name, metric := range collector.metrics { 78 | for id, record := range metric.records { 79 | if time.Now().Unix()-record.time > 60 { 80 | log.Printf("Deregistering \"%s\" records for player \"%s\"", name, id) 81 | delete(metric.records, id) 82 | } 83 | } 84 | } 85 | 86 | if time.Now().Unix()-*collector.lastUpdateTime > 60 { 87 | // Collector hasn't been update in X seconds, unregister and remove from our map 88 | log.Printf("Deregistering collector for instance \"%s\"", key) 89 | prometheus.Unregister(&collector) 90 | collectors.Delete(key) 91 | } 92 | return true 93 | }) 94 | collectorsMutex.Unlock() 95 | // sleep before running again 96 | time.Sleep(5 * time.Second) 97 | } 98 | } 99 | 100 | func main() { 101 | // start sub-routine 102 | go removeStaleCollectors() 103 | 104 | // either open or create the events log file scraped by promtail 105 | fs, err := os.OpenFile("./event.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 106 | if err != nil { 107 | log.Fatal(err.Error()) 108 | } 109 | defer fs.Close() 110 | 111 | // handler for when an instance posts an event 112 | http.HandleFunc("/event", func(res http.ResponseWriter, req *http.Request) { 113 | body, err := io.ReadAll(req.Body) 114 | if err != nil { 115 | http.Error(res, err.Error(), http.StatusBadRequest) 116 | return 117 | } 118 | 119 | var payload arbitraryJson 120 | json.Unmarshal([]byte(body), &payload) 121 | 122 | // check the event json contains a level, message and id 123 | if payload["level"] == nil || payload["message"] == nil || payload["id"] == nil { 124 | http.Error(res, "Malformed event json. Ensure your message contains all the required fields", http.StatusBadRequest) 125 | return 126 | } 127 | 128 | var ts = time.Now().Format(time.RFC3339Nano) 129 | _, _ = fs.WriteString("{" + "\"log\":\"level=" + payload["level"].(string) + " ts=" + ts + " msg=\\\"" + payload["message"].(string) + "\\\"\", \"stream\":\"" + payload["level"].(string) + "\", \"time\":\"" + ts + "\", \"instance\":\"" + payload["id"].(string) + "\"}\n") 130 | 131 | res.WriteHeader(http.StatusOK) 132 | }) 133 | 134 | // handler for when an instance posts its stats 135 | http.HandleFunc("/stats", func(res http.ResponseWriter, req *http.Request) { 136 | body, err := io.ReadAll(req.Body) 137 | if err != nil { 138 | http.Error(res, err.Error(), http.StatusBadRequest) 139 | return 140 | } 141 | 142 | var payload arbitraryJson 143 | json.Unmarshal([]byte(body), &payload) 144 | 145 | id := payload["id"] 146 | if id == nil { 147 | // error out if the instance doesn't provide us with its id 148 | http.Error(res, "ID not present in payload", http.StatusBadRequest) 149 | return 150 | } 151 | 152 | if _, exists := collectors.Load(id.(string)); !exists { 153 | // this is the first time we're seeing this ID, so configure accordingly 154 | ts := time.Now().Unix() 155 | collector := collector{ 156 | metadata: make(map[string]string), 157 | metrics: make(map[string]metric), 158 | lastUpdateTime: &ts, 159 | } 160 | 161 | // always add the id to this collectors metadata 162 | collector.metadata["id"] = id.(string) 163 | 164 | if data := payload["metadata"]; data != nil { 165 | // add user specifed metadata to the collector 166 | metadata := data.(map[string]interface{}) 167 | for key, value := range metadata { 168 | collector.metadata[key] = value.(string) 169 | } 170 | } 171 | 172 | // add all of the metrics in this payload to our collector 173 | if data := payload["metrics"]; data != nil { 174 | metrics := data.(map[string]interface{}) 175 | for key, value := range metrics { 176 | if _, exists := collector.metrics[key]; exists { 177 | continue 178 | } 179 | 180 | metricJson := value.(map[string]interface{}) 181 | 182 | if valueArray, ok := metricJson["value"].([]interface{}); ok { 183 | // if the value object is an array, then loop through this array 184 | recordMap := make(map[string]record) 185 | // if the value object is an array, then loop through this array 186 | for _, val := range valueArray { 187 | for k, v := range val.(map[string]interface{}) { 188 | recordMap[k] = record{ 189 | value: v.(float64), 190 | time: ts, 191 | } 192 | } 193 | } 194 | 195 | collector.metrics[key] = metric{ 196 | description: prometheus.NewDesc(key, metricJson["description"].(string), []string{"player"}, collector.metadata), 197 | records: recordMap, 198 | } 199 | } else { 200 | recordMap := make(map[string]record) 201 | recordMap[""] = record{ 202 | value: metricJson["value"].(float64), 203 | time: ts, 204 | } 205 | collector.metrics[key] = metric{ 206 | description: prometheus.NewDesc(key, metricJson["description"].(string), nil, collector.metadata), 207 | records: recordMap, 208 | } 209 | } 210 | } 211 | } 212 | 213 | // store collector in our internal map 214 | collectors.Store(id, collector) 215 | // register collector with Prometheus 216 | prometheus.Register(&collector) 217 | log.Printf("Registering collector for instance \"%s\"", id) 218 | 219 | // return OK 220 | res.WriteHeader(http.StatusOK) 221 | return 222 | } 223 | 224 | // collector already exists, just update metric values 225 | data := payload["metrics"] 226 | if data == nil { 227 | http.Error(res, "Malformed stats json. Ensure your message contains all the required fields", http.StatusBadRequest) 228 | return 229 | } 230 | 231 | // collector should exist at this point, but check just to be sure 232 | temp, exists := collectors.Load(id.(string)) 233 | if !exists { 234 | http.Error(res, "Unable to load collector", http.StatusBadRequest) 235 | return 236 | } 237 | 238 | collectorsMutex.Lock() 239 | 240 | collector := temp.(collector) 241 | metricsJson := data.(map[string]interface{}) 242 | // iterate over all the fields in the "metrics" section 243 | for key, value := range metricsJson { 244 | metricJson := value.(map[string]interface{}) 245 | 246 | if valueArray, ok := metricJson["value"].([]interface{}); ok { 247 | // if the value object is an array, then loop through this array 248 | recordMap := make(map[string]record) 249 | for _, val := range valueArray { 250 | for k, v := range val.(map[string]interface{}) { 251 | recordMap[k] = record{ 252 | value: v.(float64), 253 | time: time.Now().Unix(), 254 | } 255 | } 256 | } 257 | 258 | collector.metrics[key] = metric{ 259 | description: prometheus.NewDesc(key, metricJson["description"].(string), []string{"player"}, collector.metadata), 260 | records: recordMap, 261 | } 262 | } else { 263 | recordMap := make(map[string]record) 264 | recordMap[""] = record{ 265 | value: metricJson["value"].(float64), 266 | time: time.Now().Unix(), 267 | } 268 | collector.metrics[key] = metric{ 269 | description: prometheus.NewDesc(key, metricJson["description"].(string), nil, collector.metadata), 270 | records: recordMap, 271 | } 272 | } 273 | } 274 | 275 | collectors.Store(id, collector) 276 | 277 | // update lastUpdateTime 278 | *collector.lastUpdateTime = time.Now().Unix() 279 | collectorsMutex.Unlock() 280 | // return OK 281 | res.WriteHeader(http.StatusOK) 282 | }) 283 | 284 | // handler for when prometheus scrapes data 285 | http.Handle("/metrics", promhttp.Handler()) 286 | 287 | log.Fatal(http.ListenAndServe(":8000", nil)) 288 | } 289 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 3 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 6 | github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= 7 | github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= 8 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 9 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 10 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 11 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 12 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Buccaneer 2 | 3 |

4 | 5 |

6 | 7 | Buccaneer is a solution for the Unreal Engine that logs a variety of performance metrics and semantic events by pushing the collected data to an accompanying part of Go servers. From here, the [Prometheus](https://prometheus.io/) software scrapes the endpoints exposed by the Go server, storing the scraped metrics as it goes. Through the use of a [Grafana](https://grafana.com/) dashboard, the scraped metrics can be seen in an easily interpretable manner, with the semantic events being overlayed as annotations. 8 | 9 | NOTE: The Buccaneer4PixelStreaming plugin only works in Unreal Engine 5 and is dependent on both the Buccaneer base plugin and the [Pixel Streaming](https://docs.unrealengine.com/4.27/en-US/SharingAndReleasing/PixelStreaming/) plugin. 10 |

11 | 12 | 13 | ## Requirements 14 | 15 | - [Prometheus](https://prometheus.io/download/) 16 | - [Grafana](https://grafana.com/grafana/download?platform=windows) 17 | - [Loki and Promtail](https://github.com/grafana/loki/releases) 18 |

19 | 20 | 21 | ## Architecture 22 | 23 | The Buccaneer plugin consists of two primary components: 24 | 25 | - **Time-Series Data Emitter** 26 | 27 | The time-series data emitter collects real-time performance data and exports it for consumption by Prometheus where the per-instance data can then be aggregated in order to examine the performance of an entire multi-instance simulation. 28 | 29 | - **Semantic Event Emitter** 30 | 31 | The semantic event emitter facilitates emitting arbitrary and user-defined discrete event information for consumption by [Promtail](https://grafana.com/docs/loki/latest/clients/promtail/). The consumed logs are then displayed as annotations on the Grafana dashboard through the use of [Loki](https://grafana.com/oss/loki/). 32 | 33 |

34 | 35 |

36 | 37 |

38 | 39 | 40 | ## Using Buccaneer 41 | 42 | ### How do I use these custom plugins in my Unreal Project? 43 | 44 | - Adding to your project 45 | 46 | - Navigate to your project folder which contains [ProjectName].uproject 47 | 48 | - Copy the `Plugins` folder to this directory 49 | 50 |

51 | 52 | ### Using the Semantic Event Emitter 53 | 54 | Interacting with the semantic event emitter can be done with the `Emit Semantic Event` blueprint node 55 | 56 |

57 | 58 |

59 | 60 | or with the `EmitSemanticEvent(FString Level, FString Event)` function. In order to use this function, you must add the `SemanticEventEmitter` to your module dependencies and include `SemanticEventEmitter.h` 61 | 62 |

63 | 64 |

65 | 66 | ### Configuration 67 | 68 | The majority of the configurations provided can be left as-is. However, [Line 19 of `promtail-local-config.yaml`](https://github.com/Belchy06/Buccaneer/blob/48aff076edbfad76fe349c0de4d85e52f7b3d0c2/Configs/promtail-local-config.yaml#L19) requires you to configure the path to the log file of the events server. 69 | 70 | #### Starting the Buccaneer Server 71 | 72 | The Buccaneer server can be started by running its executable located under `/Build` or by running `go run ./main.go` from the `Server/BuccaneerServer` directory. 73 | 74 | By default, the server listens for metric and event pushes on `127.0.0.1:8000` 75 |

76 | 77 | #### Required Launch Arguments 78 | 79 | The following launch arguments must be specified when launching an Unreal Engine application that uses Buccaneer: 80 | | Command Line Argument | Description | 81 | | --------------------- | ------------ | 82 | | -BuccaneerURL=\ | Specifies the URL used to push performance metrics and semantic events to | 83 | 84 |

85 | 86 | #### Optional Launch Arguments 87 | 88 | The following launch arguments aren't required. However, many applications using Buccaneer may find them useful: 89 | | Command Line Argument | Description | 90 | | --------------------- | ------------ | 91 | | -BuccaneerID=\ | Specifies the ID of the instance. This is used in distinguishing which metrics came from which instance. If the argument is left empty, the ID is generated by the stats server | 92 | | -BuccaneerMetadata=\ | Specifies additional metadata for use in distinguishing instances in multi-instance deployments. This is a semi-colon separated list of key:value pairs. eg "-BuccaneerMetadata=KeyX:ValueX;KeyY:ValueY" | 93 | 94 |

95 | 96 | #### Buccaneer Plugin Configuration 97 | 98 | The following launch arguments must be specified when launching an Unreal Engine application that uses Buccaneer: 99 | | Command Line Argument | Default Value| Description | 100 | | --------------------- | ------------ |------------ | 101 | | -BuccaneerEnableStats=\ | true | Whether the pushing of performance stats is enabled | 102 | | -BuccaneerEnableEvents=\ | true | Whether the pushing of semantic events is enabled | 103 | | -Buccaneer4PixelStreamingEnableStats=\ | true | Whether the pushing of stats collected from pixel streaming is enabled | 104 | 105 |

106 | 107 | #### Buccaneer Runtime Configuration 108 | 109 | The following CVars can be used to toggle on and off aspects of Buccaneer at runtime: 110 | | Command Line Argument | Default Value| Description | 111 | | --------------------- | ------------ |------------ | 112 | | Buccaneer.EnableStats=\<1/0> | 1 | Whether the pushing of performance stats is enabled | 113 | | Buccaneer.EnableEvents=\<1/0> | 1 | Whether the pushing of semantic events is enabled | 114 | 115 |

116 | 117 | A typical application launch is as follows: 118 | ``` 119 | MyBuccaneerApplication.exe -BuccaneerURL="http://127.0.0.1:8000" 120 | ``` 121 | 122 | 123 | ## Running the Docker Compose demo 124 | 125 | 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: 126 | 127 | - 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) 128 | 129 | - The proprietary NVIDIA GPU drivers 130 | 131 | - [Docker](https://www.docker.com/) 132 | 133 | - [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/overview.html) 134 | 135 | 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: 136 | 137 | - - This is a demo Unreal Engine application that uses Pixel Streaming to allow streaming via a browser. 138 | 139 | - - 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. 140 | 141 | 142 | ## Legal 143 | 144 | Copyright © 2021-2022, TensorWorks Pty Ltd. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details. 145 | --------------------------------------------------------------------------------