godot-runtime-telemetry-lab 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- godot_runtime_telemetry_lab-0.1.0/LICENSE +21 -0
- godot_runtime_telemetry_lab-0.1.0/PKG-INFO +74 -0
- godot_runtime_telemetry_lab-0.1.0/README.md +53 -0
- godot_runtime_telemetry_lab-0.1.0/pyproject.toml +37 -0
- godot_runtime_telemetry_lab-0.1.0/setup.cfg +4 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab/__init__.py +3 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab/__main__.py +5 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab/cli.py +70 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab/telemetry.py +185 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab.egg-info/PKG-INFO +74 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab.egg-info/SOURCES.txt +13 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab.egg-info/dependency_links.txt +1 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab.egg-info/entry_points.txt +2 -0
- godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab.egg-info/top_level.txt +1 -0
- godot_runtime_telemetry_lab-0.1.0/tests/test_runtime_telemetry_lab.py +54 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Godot Runtime Telemetry Lab contributors
|
|
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.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: godot-runtime-telemetry-lab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Summarize and compare Godot runtime telemetry, frame budgets, and scenario performance evidence.
|
|
5
|
+
Author: Godot Runtime Telemetry Lab contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-runtime-telemetry-lab
|
|
8
|
+
Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
|
|
9
|
+
Keywords: godot,telemetry,performance,profiler,gamedev,ci
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Godot Runtime Telemetry Lab
|
|
23
|
+
|
|
24
|
+
`godot-runtime-telemetry-lab` summarizes and compares lightweight runtime
|
|
25
|
+
telemetry from Godot scenario runs, smoke tests, soak tests, or project-owned
|
|
26
|
+
debug exporters. It is designed for CI artifacts and release reviews, not as a
|
|
27
|
+
replacement for Godot's built-in profiler.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
python -m pip install godot-runtime-telemetry-lab
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
From a source checkout:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
python -m pip install -e .\godot-runtime-telemetry-lab
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
godot-telemetry-lab summarize reports\runtime --format markdown --output reports\runtime.md
|
|
45
|
+
godot-telemetry-lab compare reports\baseline reports\current --format json --output reports\runtime-compare.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Input Shape
|
|
49
|
+
|
|
50
|
+
The tool accepts `.json` or `.csv` files. JSON files can contain a list of
|
|
51
|
+
samples, or an object with a `samples`, `frames`, or `events` list.
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"samples": [
|
|
56
|
+
{"scenario": "menu", "frame_ms": 12.4, "physics_ms": 2.1, "memory_mb": 180},
|
|
57
|
+
{"scenario": "menu", "frame_ms": 18.8, "physics_ms": 2.5, "memory_mb": 181}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Recognized numeric fields are `frame_ms`, `physics_ms`, `memory_mb`, `nodes`,
|
|
63
|
+
and `draw_calls`. Unknown fields are ignored by the first release.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
- `summarize`: reports sample counts, frame percentiles, and budget findings.
|
|
68
|
+
- `compare`: compares current telemetry with a baseline and reports regressions.
|
|
69
|
+
|
|
70
|
+
## Outputs
|
|
71
|
+
|
|
72
|
+
- `text`: local terminal report.
|
|
73
|
+
- `json`: CI and scripts.
|
|
74
|
+
- `markdown`: PR comments and release notes.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Godot Runtime Telemetry Lab
|
|
2
|
+
|
|
3
|
+
`godot-runtime-telemetry-lab` summarizes and compares lightweight runtime
|
|
4
|
+
telemetry from Godot scenario runs, smoke tests, soak tests, or project-owned
|
|
5
|
+
debug exporters. It is designed for CI artifacts and release reviews, not as a
|
|
6
|
+
replacement for Godot's built-in profiler.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
python -m pip install godot-runtime-telemetry-lab
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
From a source checkout:
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
python -m pip install -e .\godot-runtime-telemetry-lab
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
godot-telemetry-lab summarize reports\runtime --format markdown --output reports\runtime.md
|
|
24
|
+
godot-telemetry-lab compare reports\baseline reports\current --format json --output reports\runtime-compare.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Input Shape
|
|
28
|
+
|
|
29
|
+
The tool accepts `.json` or `.csv` files. JSON files can contain a list of
|
|
30
|
+
samples, or an object with a `samples`, `frames`, or `events` list.
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"samples": [
|
|
35
|
+
{"scenario": "menu", "frame_ms": 12.4, "physics_ms": 2.1, "memory_mb": 180},
|
|
36
|
+
{"scenario": "menu", "frame_ms": 18.8, "physics_ms": 2.5, "memory_mb": 181}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Recognized numeric fields are `frame_ms`, `physics_ms`, `memory_mb`, `nodes`,
|
|
42
|
+
and `draw_calls`. Unknown fields are ignored by the first release.
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
- `summarize`: reports sample counts, frame percentiles, and budget findings.
|
|
47
|
+
- `compare`: compares current telemetry with a baseline and reports regressions.
|
|
48
|
+
|
|
49
|
+
## Outputs
|
|
50
|
+
|
|
51
|
+
- `text`: local terminal report.
|
|
52
|
+
- `json`: CI and scripts.
|
|
53
|
+
- `markdown`: PR comments and release notes.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "godot-runtime-telemetry-lab"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Summarize and compare Godot runtime telemetry, frame budgets, and scenario performance evidence."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Godot Runtime Telemetry Lab contributors" }]
|
|
13
|
+
keywords = ["godot", "telemetry", "performance", "profiler", "gamedev", "ci"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
21
|
+
"Topic :: Utilities"
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-runtime-telemetry-lab"
|
|
27
|
+
Issues = "https://github.com/NonniGB/godot-production-toolkit/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
godot-telemetry-lab = "godot_runtime_telemetry_lab.cli:entrypoint"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 100
|
|
37
|
+
target-version = "py311"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .telemetry import compare, render, summarize
|
|
8
|
+
|
|
9
|
+
VERSION_LABEL = "godot-telemetry-lab 0.1.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(description="Summarize and compare Godot runtime telemetry.")
|
|
14
|
+
parser.add_argument("--version", action="version", version=VERSION_LABEL)
|
|
15
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
16
|
+
|
|
17
|
+
summarize_parser = subparsers.add_parser("summarize", help="Summarize JSON or CSV telemetry samples.")
|
|
18
|
+
summarize_parser.add_argument("path")
|
|
19
|
+
_add_common_args(summarize_parser)
|
|
20
|
+
|
|
21
|
+
compare_parser = subparsers.add_parser("compare", help="Compare current telemetry with a baseline.")
|
|
22
|
+
compare_parser.add_argument("baseline")
|
|
23
|
+
compare_parser.add_argument("current")
|
|
24
|
+
compare_parser.add_argument("--regression-ratio", type=float, default=1.25)
|
|
25
|
+
_add_common_args(compare_parser)
|
|
26
|
+
|
|
27
|
+
args = parser.parse_args(argv)
|
|
28
|
+
if args.command == "summarize":
|
|
29
|
+
report = summarize(Path(args.path), args.frame_budget_ms)
|
|
30
|
+
elif args.command == "compare":
|
|
31
|
+
report = compare(Path(args.baseline), Path(args.current), args.frame_budget_ms, args.regression_ratio)
|
|
32
|
+
else:
|
|
33
|
+
parser.print_help()
|
|
34
|
+
return 2
|
|
35
|
+
|
|
36
|
+
_emit(render(report, args.format), args.output)
|
|
37
|
+
return _exit_code(report, args.fail_on)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def entrypoint() -> None:
|
|
41
|
+
raise SystemExit(main())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
45
|
+
parser.add_argument("--frame-budget-ms", type=float, default=16.67)
|
|
46
|
+
parser.add_argument("--format", choices=["text", "json", "markdown"], default="text")
|
|
47
|
+
parser.add_argument("--output")
|
|
48
|
+
parser.add_argument("--fail-on", choices=["none", "warning", "error"], default="error")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _emit(rendered: str, output: str | None) -> None:
|
|
52
|
+
if output:
|
|
53
|
+
output_path = Path(output)
|
|
54
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
output_path.write_text(rendered + "\n", encoding="utf-8")
|
|
56
|
+
else:
|
|
57
|
+
print(rendered)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _exit_code(report: dict[str, object], fail_on: str) -> int:
|
|
61
|
+
summary = report["summary"]
|
|
62
|
+
if fail_on == "none":
|
|
63
|
+
return 0
|
|
64
|
+
if fail_on == "warning":
|
|
65
|
+
return 1 if int(summary["errors"]) + int(summary["warnings"]) > 0 else 0
|
|
66
|
+
return 1 if int(summary["errors"]) > 0 else 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from statistics import mean
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
NUMERIC_FIELDS = ("frame_ms", "physics_ms", "memory_mb", "nodes", "draw_calls")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def summarize(path: Path, frame_budget_ms: float = 16.67) -> dict[str, Any]:
|
|
16
|
+
samples = load_samples(path)
|
|
17
|
+
summary = _summary(samples)
|
|
18
|
+
findings = _findings(summary, frame_budget_ms)
|
|
19
|
+
return {
|
|
20
|
+
"tool": "godot-runtime-telemetry-lab",
|
|
21
|
+
"tool_version": __version__,
|
|
22
|
+
"schema_version": "1.0",
|
|
23
|
+
"kind": "runtime_telemetry_summary",
|
|
24
|
+
"summary": summary,
|
|
25
|
+
"findings": findings,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compare(baseline: Path, current: Path, frame_budget_ms: float = 16.67, regression_ratio: float = 1.25) -> dict[str, Any]:
|
|
30
|
+
baseline_report = summarize(baseline, frame_budget_ms)
|
|
31
|
+
current_report = summarize(current, frame_budget_ms)
|
|
32
|
+
findings = list(current_report["findings"])
|
|
33
|
+
baseline_p95 = float(baseline_report["summary"]["frame_ms"]["p95"])
|
|
34
|
+
current_p95 = float(current_report["summary"]["frame_ms"]["p95"])
|
|
35
|
+
if baseline_p95 > 0 and current_p95 >= baseline_p95 * regression_ratio:
|
|
36
|
+
findings.append(
|
|
37
|
+
{
|
|
38
|
+
"rule_id": "frame_p95_regression",
|
|
39
|
+
"severity": "warning",
|
|
40
|
+
"message": (
|
|
41
|
+
f"Frame p95 rose from {baseline_p95:.2f} ms to {current_p95:.2f} ms "
|
|
42
|
+
f"at a {regression_ratio:g}x regression threshold."
|
|
43
|
+
),
|
|
44
|
+
"rule_help": "Compare recent runtime, rendering, loading, and scenario changes before updating the baseline.",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
return {
|
|
48
|
+
"tool": "godot-runtime-telemetry-lab",
|
|
49
|
+
"tool_version": __version__,
|
|
50
|
+
"schema_version": "1.0",
|
|
51
|
+
"kind": "runtime_telemetry_compare",
|
|
52
|
+
"baseline": baseline_report["summary"],
|
|
53
|
+
"current": current_report["summary"],
|
|
54
|
+
"summary": {
|
|
55
|
+
"errors": sum(1 for finding in findings if finding["severity"] == "error"),
|
|
56
|
+
"warnings": sum(1 for finding in findings if finding["severity"] == "warning"),
|
|
57
|
+
"samples": current_report["summary"]["samples"],
|
|
58
|
+
},
|
|
59
|
+
"findings": findings,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_samples(path: Path) -> list[dict[str, Any]]:
|
|
64
|
+
files = [path] if path.is_file() else sorted(path.glob("*.json")) + sorted(path.glob("*.csv"))
|
|
65
|
+
samples: list[dict[str, Any]] = []
|
|
66
|
+
for file_path in files:
|
|
67
|
+
if file_path.suffix.lower() == ".csv":
|
|
68
|
+
samples.extend(_load_csv(file_path))
|
|
69
|
+
elif file_path.suffix.lower() == ".json":
|
|
70
|
+
samples.extend(_load_json(file_path))
|
|
71
|
+
return samples
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def render(report: dict[str, Any], output_format: str) -> str:
|
|
75
|
+
if output_format == "json":
|
|
76
|
+
return json.dumps(report, indent=2, sort_keys=True)
|
|
77
|
+
if output_format == "markdown":
|
|
78
|
+
return _markdown(report)
|
|
79
|
+
return _text(report)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _load_json(path: Path) -> list[dict[str, Any]]:
|
|
83
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
84
|
+
if isinstance(data, list):
|
|
85
|
+
return [item for item in data if isinstance(item, dict)]
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
for key in ("samples", "frames", "events"):
|
|
88
|
+
value = data.get(key)
|
|
89
|
+
if isinstance(value, list):
|
|
90
|
+
return [item for item in value if isinstance(item, dict)]
|
|
91
|
+
return [data]
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _load_csv(path: Path) -> list[dict[str, Any]]:
|
|
96
|
+
with path.open("r", encoding="utf-8", newline="") as handle:
|
|
97
|
+
return [dict(row) for row in csv.DictReader(handle)]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _summary(samples: list[dict[str, Any]]) -> dict[str, Any]:
|
|
101
|
+
scenarios = sorted({str(sample.get("scenario", "default")) for sample in samples})
|
|
102
|
+
return {
|
|
103
|
+
"samples": len(samples),
|
|
104
|
+
"scenarios": scenarios,
|
|
105
|
+
"errors": 0,
|
|
106
|
+
"warnings": 0,
|
|
107
|
+
**{field: _metric(samples, field) for field in NUMERIC_FIELDS},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _metric(samples: list[dict[str, Any]], field: str) -> dict[str, float]:
|
|
112
|
+
values = sorted(_number(sample.get(field)) for sample in samples if _number(sample.get(field)) is not None)
|
|
113
|
+
if not values:
|
|
114
|
+
return {"min": 0.0, "avg": 0.0, "p95": 0.0, "max": 0.0}
|
|
115
|
+
return {
|
|
116
|
+
"min": values[0],
|
|
117
|
+
"avg": mean(values),
|
|
118
|
+
"p95": _percentile(values, 0.95),
|
|
119
|
+
"max": values[-1],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _findings(summary: dict[str, Any], frame_budget_ms: float) -> list[dict[str, str]]:
|
|
124
|
+
findings: list[dict[str, str]] = []
|
|
125
|
+
p95 = float(summary["frame_ms"]["p95"])
|
|
126
|
+
if p95 > frame_budget_ms:
|
|
127
|
+
findings.append(
|
|
128
|
+
{
|
|
129
|
+
"rule_id": "frame_p95_over_budget",
|
|
130
|
+
"severity": "warning",
|
|
131
|
+
"message": f"Frame p95 is {p95:.2f} ms, above the {frame_budget_ms:g} ms budget.",
|
|
132
|
+
"rule_help": "Inspect scenario phases and recent rendering or script changes around the slow frames.",
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
return findings
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _number(value: object) -> float | None:
|
|
139
|
+
try:
|
|
140
|
+
return float(value)
|
|
141
|
+
except (TypeError, ValueError):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _percentile(values: list[float], fraction: float) -> float:
|
|
146
|
+
if len(values) == 1:
|
|
147
|
+
return values[0]
|
|
148
|
+
index = round((len(values) - 1) * fraction)
|
|
149
|
+
return values[index]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _text(report: dict[str, Any]) -> str:
|
|
153
|
+
summary = report["summary"]
|
|
154
|
+
lines = [
|
|
155
|
+
"Godot Runtime Telemetry Lab",
|
|
156
|
+
f"Samples: {summary['samples']} | Errors: {summary['errors']} | Warnings: {summary['warnings']}",
|
|
157
|
+
f"Frame p95: {summary['frame_ms']['p95']:.2f} ms | max: {summary['frame_ms']['max']:.2f} ms",
|
|
158
|
+
]
|
|
159
|
+
for finding in report["findings"]:
|
|
160
|
+
lines.append(f"- {finding['severity'].upper()} {finding['rule_id']}: {finding['message']}")
|
|
161
|
+
return "\n".join(lines)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _markdown(report: dict[str, Any]) -> str:
|
|
165
|
+
summary = report["summary"]
|
|
166
|
+
lines = [
|
|
167
|
+
"# Godot Runtime Telemetry",
|
|
168
|
+
"",
|
|
169
|
+
"| Metric | Value |",
|
|
170
|
+
"|---|---:|",
|
|
171
|
+
f"| Samples | {summary['samples']} |",
|
|
172
|
+
f"| Frame p95 ms | {summary['frame_ms']['p95']:.2f} |",
|
|
173
|
+
f"| Frame max ms | {summary['frame_ms']['max']:.2f} |",
|
|
174
|
+
f"| Warnings | {summary['warnings']} |",
|
|
175
|
+
"",
|
|
176
|
+
"## Findings",
|
|
177
|
+
"",
|
|
178
|
+
]
|
|
179
|
+
if not report["findings"]:
|
|
180
|
+
lines.append("No telemetry findings.")
|
|
181
|
+
else:
|
|
182
|
+
lines.extend(["| Severity | Rule | Message |", "|---|---|---|"])
|
|
183
|
+
for finding in report["findings"]:
|
|
184
|
+
lines.append(f"| {finding['severity']} | `{finding['rule_id']}` | {finding['message']} |")
|
|
185
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: godot-runtime-telemetry-lab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Summarize and compare Godot runtime telemetry, frame budgets, and scenario performance evidence.
|
|
5
|
+
Author: Godot Runtime Telemetry Lab contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-runtime-telemetry-lab
|
|
8
|
+
Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
|
|
9
|
+
Keywords: godot,telemetry,performance,profiler,gamedev,ci
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Godot Runtime Telemetry Lab
|
|
23
|
+
|
|
24
|
+
`godot-runtime-telemetry-lab` summarizes and compares lightweight runtime
|
|
25
|
+
telemetry from Godot scenario runs, smoke tests, soak tests, or project-owned
|
|
26
|
+
debug exporters. It is designed for CI artifacts and release reviews, not as a
|
|
27
|
+
replacement for Godot's built-in profiler.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
python -m pip install godot-runtime-telemetry-lab
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
From a source checkout:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
python -m pip install -e .\godot-runtime-telemetry-lab
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
godot-telemetry-lab summarize reports\runtime --format markdown --output reports\runtime.md
|
|
45
|
+
godot-telemetry-lab compare reports\baseline reports\current --format json --output reports\runtime-compare.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Input Shape
|
|
49
|
+
|
|
50
|
+
The tool accepts `.json` or `.csv` files. JSON files can contain a list of
|
|
51
|
+
samples, or an object with a `samples`, `frames`, or `events` list.
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"samples": [
|
|
56
|
+
{"scenario": "menu", "frame_ms": 12.4, "physics_ms": 2.1, "memory_mb": 180},
|
|
57
|
+
{"scenario": "menu", "frame_ms": 18.8, "physics_ms": 2.5, "memory_mb": 181}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Recognized numeric fields are `frame_ms`, `physics_ms`, `memory_mb`, `nodes`,
|
|
63
|
+
and `draw_calls`. Unknown fields are ignored by the first release.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
- `summarize`: reports sample counts, frame percentiles, and budget findings.
|
|
68
|
+
- `compare`: compares current telemetry with a baseline and reports regressions.
|
|
69
|
+
|
|
70
|
+
## Outputs
|
|
71
|
+
|
|
72
|
+
- `text`: local terminal report.
|
|
73
|
+
- `json`: CI and scripts.
|
|
74
|
+
- `markdown`: PR comments and release notes.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/godot_runtime_telemetry_lab/__init__.py
|
|
5
|
+
src/godot_runtime_telemetry_lab/__main__.py
|
|
6
|
+
src/godot_runtime_telemetry_lab/cli.py
|
|
7
|
+
src/godot_runtime_telemetry_lab/telemetry.py
|
|
8
|
+
src/godot_runtime_telemetry_lab.egg-info/PKG-INFO
|
|
9
|
+
src/godot_runtime_telemetry_lab.egg-info/SOURCES.txt
|
|
10
|
+
src/godot_runtime_telemetry_lab.egg-info/dependency_links.txt
|
|
11
|
+
src/godot_runtime_telemetry_lab.egg-info/entry_points.txt
|
|
12
|
+
src/godot_runtime_telemetry_lab.egg-info/top_level.txt
|
|
13
|
+
tests/test_runtime_telemetry_lab.py
|
godot_runtime_telemetry_lab-0.1.0/src/godot_runtime_telemetry_lab.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
godot_runtime_telemetry_lab
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import redirect_stdout
|
|
4
|
+
from io import StringIO
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
12
|
+
|
|
13
|
+
from godot_runtime_telemetry_lab.cli import main
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RuntimeTelemetryLabTests(unittest.TestCase):
|
|
17
|
+
def test_summarize_reports_frame_budget_warning(self) -> None:
|
|
18
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
19
|
+
path = Path(tmp) / "telemetry.json"
|
|
20
|
+
path.write_text(
|
|
21
|
+
json.dumps({"samples": [{"scenario": "menu", "frame_ms": 12}, {"scenario": "menu", "frame_ms": 24}]}),
|
|
22
|
+
encoding="utf-8",
|
|
23
|
+
)
|
|
24
|
+
stdout = StringIO()
|
|
25
|
+
|
|
26
|
+
with redirect_stdout(stdout):
|
|
27
|
+
exit_code = main(["summarize", str(path), "--frame-budget-ms", "16", "--format", "json", "--fail-on", "none"])
|
|
28
|
+
|
|
29
|
+
report = json.loads(stdout.getvalue())
|
|
30
|
+
self.assertEqual(exit_code, 0)
|
|
31
|
+
self.assertEqual(report["tool_version"], "0.1.0")
|
|
32
|
+
self.assertEqual(report["summary"]["samples"], 2)
|
|
33
|
+
self.assertEqual(report["findings"][0]["rule_id"], "frame_p95_over_budget")
|
|
34
|
+
|
|
35
|
+
def test_compare_reports_regression(self) -> None:
|
|
36
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
37
|
+
root = Path(tmp)
|
|
38
|
+
baseline = root / "baseline.json"
|
|
39
|
+
current = root / "current.json"
|
|
40
|
+
baseline.write_text(json.dumps([{"frame_ms": 10}, {"frame_ms": 12}]), encoding="utf-8")
|
|
41
|
+
current.write_text(json.dumps([{"frame_ms": 20}, {"frame_ms": 24}]), encoding="utf-8")
|
|
42
|
+
stdout = StringIO()
|
|
43
|
+
|
|
44
|
+
with redirect_stdout(stdout):
|
|
45
|
+
exit_code = main(["compare", str(baseline), str(current), "--format", "json", "--fail-on", "none"])
|
|
46
|
+
|
|
47
|
+
report = json.loads(stdout.getvalue())
|
|
48
|
+
self.assertEqual(exit_code, 0)
|
|
49
|
+
rules = {finding["rule_id"] for finding in report["findings"]}
|
|
50
|
+
self.assertIn("frame_p95_regression", rules)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
unittest.main()
|