greenmining 1.0.5__py3-none-any.whl → 1.0.7__py3-none-any.whl
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.
- greenmining/__init__.py +54 -2
- greenmining/analyzers/__init__.py +9 -0
- greenmining/analyzers/metrics_power_correlator.py +165 -0
- greenmining/analyzers/power_regression.py +212 -0
- greenmining/analyzers/version_power_analyzer.py +246 -0
- greenmining/config.py +46 -34
- greenmining/dashboard/__init__.py +5 -0
- greenmining/dashboard/app.py +200 -0
- greenmining/energy/__init__.py +8 -1
- greenmining/energy/base.py +45 -35
- greenmining/energy/carbon_reporter.py +242 -0
- greenmining/energy/codecarbon_meter.py +25 -24
- greenmining/energy/cpu_meter.py +144 -0
- greenmining/energy/rapl.py +30 -36
- greenmining/services/__init__.py +13 -3
- greenmining/services/commit_extractor.py +9 -5
- greenmining/services/local_repo_analyzer.py +325 -63
- greenmining/services/reports.py +5 -8
- {greenmining-1.0.5.dist-info → greenmining-1.0.7.dist-info}/METADATA +212 -43
- {greenmining-1.0.5.dist-info → greenmining-1.0.7.dist-info}/RECORD +23 -16
- {greenmining-1.0.5.dist-info → greenmining-1.0.7.dist-info}/WHEEL +0 -0
- {greenmining-1.0.5.dist-info → greenmining-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {greenmining-1.0.5.dist-info → greenmining-1.0.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Version-by-version power analysis.
|
|
2
|
+
# Measure power consumption across multiple software versions/tags.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from greenmining.utils import colored_print
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class VersionPowerProfile:
|
|
15
|
+
# Power profile for a single version.
|
|
16
|
+
|
|
17
|
+
version: str
|
|
18
|
+
commit_sha: str
|
|
19
|
+
energy_joules: float = 0.0
|
|
20
|
+
power_watts_avg: float = 0.0
|
|
21
|
+
duration_seconds: float = 0.0
|
|
22
|
+
iterations: int = 0
|
|
23
|
+
energy_std: float = 0.0 # Standard deviation across iterations
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"version": self.version,
|
|
28
|
+
"commit_sha": self.commit_sha,
|
|
29
|
+
"energy_joules": round(self.energy_joules, 4),
|
|
30
|
+
"power_watts_avg": round(self.power_watts_avg, 4),
|
|
31
|
+
"duration_seconds": round(self.duration_seconds, 4),
|
|
32
|
+
"iterations": self.iterations,
|
|
33
|
+
"energy_std": round(self.energy_std, 4),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class VersionPowerReport:
|
|
39
|
+
# Complete power analysis report across versions.
|
|
40
|
+
|
|
41
|
+
versions: List[VersionPowerProfile] = field(default_factory=list)
|
|
42
|
+
trend: str = "stable" # increasing, decreasing, stable
|
|
43
|
+
total_change_percent: float = 0.0
|
|
44
|
+
most_efficient: str = ""
|
|
45
|
+
least_efficient: str = ""
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"versions": [v.to_dict() for v in self.versions],
|
|
50
|
+
"trend": self.trend,
|
|
51
|
+
"total_change_percent": round(self.total_change_percent, 2),
|
|
52
|
+
"most_efficient": self.most_efficient,
|
|
53
|
+
"least_efficient": self.least_efficient,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def summary(self) -> str:
|
|
57
|
+
# Generate human-readable summary.
|
|
58
|
+
lines = [
|
|
59
|
+
"Version Power Analysis Report",
|
|
60
|
+
"-" * 40,
|
|
61
|
+
f"Versions analyzed: {len(self.versions)}",
|
|
62
|
+
f"Trend: {self.trend}",
|
|
63
|
+
f"Total change: {self.total_change_percent:+.2f}%",
|
|
64
|
+
f"Most efficient: {self.most_efficient}",
|
|
65
|
+
f"Least efficient: {self.least_efficient}",
|
|
66
|
+
"",
|
|
67
|
+
"Per-version breakdown:",
|
|
68
|
+
]
|
|
69
|
+
for v in self.versions:
|
|
70
|
+
lines.append(
|
|
71
|
+
f" {v.version}: {v.energy_joules:.4f}J "
|
|
72
|
+
f"({v.power_watts_avg:.2f}W avg, {v.duration_seconds:.2f}s)"
|
|
73
|
+
)
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class VersionPowerAnalyzer:
|
|
78
|
+
# Measure and compare power consumption across software versions.
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
test_command: str = "pytest tests/",
|
|
83
|
+
energy_backend: str = "rapl",
|
|
84
|
+
iterations: int = 10,
|
|
85
|
+
warmup_iterations: int = 2,
|
|
86
|
+
):
|
|
87
|
+
# Initialize version power analyzer.
|
|
88
|
+
# Args:
|
|
89
|
+
# test_command: Shell command to run for measurement
|
|
90
|
+
# energy_backend: Energy measurement backend
|
|
91
|
+
# iterations: Number of measurement iterations per version
|
|
92
|
+
# warmup_iterations: Warmup runs before measuring
|
|
93
|
+
self.test_command = test_command
|
|
94
|
+
self.energy_backend = energy_backend
|
|
95
|
+
self.iterations = iterations
|
|
96
|
+
self.warmup_iterations = warmup_iterations
|
|
97
|
+
self._meter = None
|
|
98
|
+
|
|
99
|
+
def _get_energy_meter(self):
|
|
100
|
+
# Get energy meter instance.
|
|
101
|
+
if self._meter is None:
|
|
102
|
+
from greenmining.energy.base import get_energy_meter
|
|
103
|
+
|
|
104
|
+
self._meter = get_energy_meter(self.energy_backend)
|
|
105
|
+
return self._meter
|
|
106
|
+
|
|
107
|
+
def _measure_version(self, repo_path: str, version: str) -> VersionPowerProfile:
|
|
108
|
+
# Measure power consumption for a specific version.
|
|
109
|
+
meter = self._get_energy_meter()
|
|
110
|
+
|
|
111
|
+
# Checkout version
|
|
112
|
+
sha = self._checkout_version(repo_path, version)
|
|
113
|
+
colored_print(f" Measuring {version} ({sha[:8]})...", "cyan")
|
|
114
|
+
|
|
115
|
+
# Warmup
|
|
116
|
+
for i in range(self.warmup_iterations):
|
|
117
|
+
subprocess.run(
|
|
118
|
+
self.test_command,
|
|
119
|
+
shell=True,
|
|
120
|
+
cwd=repo_path,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=600,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Measure iterations
|
|
127
|
+
measurements = []
|
|
128
|
+
for i in range(self.iterations):
|
|
129
|
+
meter.start()
|
|
130
|
+
subprocess.run(
|
|
131
|
+
self.test_command,
|
|
132
|
+
shell=True,
|
|
133
|
+
cwd=repo_path,
|
|
134
|
+
capture_output=True,
|
|
135
|
+
text=True,
|
|
136
|
+
timeout=600,
|
|
137
|
+
)
|
|
138
|
+
metrics = meter.stop()
|
|
139
|
+
measurements.append(metrics)
|
|
140
|
+
|
|
141
|
+
# Aggregate
|
|
142
|
+
import numpy as np
|
|
143
|
+
|
|
144
|
+
energies = [m.joules for m in measurements]
|
|
145
|
+
avg_energy = float(np.mean(energies))
|
|
146
|
+
std_energy = float(np.std(energies))
|
|
147
|
+
avg_power = float(np.mean([m.watts_avg for m in measurements]))
|
|
148
|
+
avg_duration = float(np.mean([m.duration_seconds for m in measurements]))
|
|
149
|
+
|
|
150
|
+
profile = VersionPowerProfile(
|
|
151
|
+
version=version,
|
|
152
|
+
commit_sha=sha,
|
|
153
|
+
energy_joules=avg_energy,
|
|
154
|
+
power_watts_avg=avg_power,
|
|
155
|
+
duration_seconds=avg_duration,
|
|
156
|
+
iterations=self.iterations,
|
|
157
|
+
energy_std=std_energy,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
colored_print(
|
|
161
|
+
f" {version}: {avg_energy:.4f}J +/-{std_energy:.4f} "
|
|
162
|
+
f"({avg_power:.2f}W, {avg_duration:.2f}s)",
|
|
163
|
+
"green",
|
|
164
|
+
)
|
|
165
|
+
return profile
|
|
166
|
+
|
|
167
|
+
def analyze_versions(
|
|
168
|
+
self,
|
|
169
|
+
repo_path: str,
|
|
170
|
+
versions: List[str],
|
|
171
|
+
) -> VersionPowerReport:
|
|
172
|
+
# Analyze power consumption across multiple versions.
|
|
173
|
+
# Args:
|
|
174
|
+
# repo_path: Path to local git repository
|
|
175
|
+
# versions: List of version tags or commit references
|
|
176
|
+
colored_print(f"\nAnalyzing {len(versions)} versions for power consumption", "cyan")
|
|
177
|
+
colored_print(f" Test: {self.test_command}", "cyan")
|
|
178
|
+
colored_print(f" Iterations: {self.iterations} (+{self.warmup_iterations} warmup)", "cyan")
|
|
179
|
+
|
|
180
|
+
profiles = []
|
|
181
|
+
for version in versions:
|
|
182
|
+
try:
|
|
183
|
+
profile = self._measure_version(repo_path, version)
|
|
184
|
+
profiles.append(profile)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
colored_print(f" Error measuring {version}: {e}", "red")
|
|
187
|
+
|
|
188
|
+
if not profiles:
|
|
189
|
+
return VersionPowerReport()
|
|
190
|
+
|
|
191
|
+
# Determine trend
|
|
192
|
+
first_energy = profiles[0].energy_joules
|
|
193
|
+
last_energy = profiles[-1].energy_joules
|
|
194
|
+
|
|
195
|
+
if first_energy > 0:
|
|
196
|
+
total_change = ((last_energy - first_energy) / first_energy) * 100
|
|
197
|
+
else:
|
|
198
|
+
total_change = 0.0
|
|
199
|
+
|
|
200
|
+
if total_change > 5:
|
|
201
|
+
trend = "increasing"
|
|
202
|
+
elif total_change < -5:
|
|
203
|
+
trend = "decreasing"
|
|
204
|
+
else:
|
|
205
|
+
trend = "stable"
|
|
206
|
+
|
|
207
|
+
# Find most/least efficient
|
|
208
|
+
most_efficient = min(profiles, key=lambda p: p.energy_joules)
|
|
209
|
+
least_efficient = max(profiles, key=lambda p: p.energy_joules)
|
|
210
|
+
|
|
211
|
+
report = VersionPowerReport(
|
|
212
|
+
versions=profiles,
|
|
213
|
+
trend=trend,
|
|
214
|
+
total_change_percent=total_change,
|
|
215
|
+
most_efficient=most_efficient.version,
|
|
216
|
+
least_efficient=least_efficient.version,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
colored_print(f"\nTrend: {trend} ({total_change:+.2f}%)", "cyan")
|
|
220
|
+
colored_print(f"Most efficient: {most_efficient.version}", "green")
|
|
221
|
+
colored_print(f"Least efficient: {least_efficient.version}", "red")
|
|
222
|
+
|
|
223
|
+
# Restore to latest version
|
|
224
|
+
try:
|
|
225
|
+
self._checkout_version(repo_path, versions[-1])
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
return report
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _checkout_version(repo_path: str, version: str) -> str:
|
|
233
|
+
# Checkout a version and return its SHA.
|
|
234
|
+
subprocess.run(
|
|
235
|
+
["git", "checkout", version, "--quiet"],
|
|
236
|
+
cwd=repo_path,
|
|
237
|
+
capture_output=True,
|
|
238
|
+
text=True,
|
|
239
|
+
)
|
|
240
|
+
result = subprocess.run(
|
|
241
|
+
["git", "rev-parse", "HEAD"],
|
|
242
|
+
cwd=repo_path,
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
)
|
|
246
|
+
return result.stdout.strip()
|
greenmining/config.py
CHANGED
|
@@ -11,7 +11,8 @@ def _load_yaml_config(yaml_path: Path) -> Dict[str, Any]:
|
|
|
11
11
|
return {}
|
|
12
12
|
try:
|
|
13
13
|
import yaml
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
with open(yaml_path, "r") as f:
|
|
15
16
|
return yaml.safe_load(f) or {}
|
|
16
17
|
except ImportError:
|
|
17
18
|
return {}
|
|
@@ -30,7 +31,7 @@ class Config:
|
|
|
30
31
|
load_dotenv(env_path)
|
|
31
32
|
else:
|
|
32
33
|
load_dotenv() # Load from system environment
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
# Load YAML config (takes precedence for certain options)
|
|
35
36
|
yaml_path = Path(yaml_file)
|
|
36
37
|
self._yaml_config = _load_yaml_config(yaml_path)
|
|
@@ -45,22 +46,32 @@ class Config:
|
|
|
45
46
|
|
|
46
47
|
# Search and Processing Configuration (YAML: sources.search.keywords)
|
|
47
48
|
yaml_search = self._yaml_config.get("sources", {}).get("search", {})
|
|
48
|
-
self.GITHUB_SEARCH_KEYWORDS = yaml_search.get(
|
|
49
|
-
["microservices", "microservice-architecture", "cloud-native"]
|
|
49
|
+
self.GITHUB_SEARCH_KEYWORDS = yaml_search.get(
|
|
50
|
+
"keywords", ["microservices", "microservice-architecture", "cloud-native"]
|
|
51
|
+
)
|
|
50
52
|
|
|
51
53
|
# Supported Languages (YAML: sources.search.languages)
|
|
52
|
-
self.SUPPORTED_LANGUAGES = yaml_search.get(
|
|
53
|
-
"
|
|
54
|
-
|
|
54
|
+
self.SUPPORTED_LANGUAGES = yaml_search.get(
|
|
55
|
+
"languages",
|
|
56
|
+
[
|
|
57
|
+
"Java",
|
|
58
|
+
"Python",
|
|
59
|
+
"Go",
|
|
60
|
+
"JavaScript",
|
|
61
|
+
"TypeScript",
|
|
62
|
+
"C#",
|
|
63
|
+
"Rust",
|
|
64
|
+
],
|
|
65
|
+
)
|
|
55
66
|
|
|
56
67
|
# Repository and Commit Limits (YAML: extraction.*)
|
|
57
68
|
yaml_extraction = self._yaml_config.get("extraction", {})
|
|
58
69
|
self.MIN_STARS = yaml_search.get("min_stars", int(os.getenv("MIN_STARS", "100")))
|
|
59
70
|
self.MAX_REPOS = int(os.getenv("MAX_REPOS", "100"))
|
|
60
|
-
self.COMMITS_PER_REPO = yaml_extraction.get(
|
|
61
|
-
int(os.getenv("COMMITS_PER_REPO", "50"))
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
self.COMMITS_PER_REPO = yaml_extraction.get(
|
|
72
|
+
"max_commits", int(os.getenv("COMMITS_PER_REPO", "50"))
|
|
73
|
+
)
|
|
74
|
+
self.DAYS_BACK = yaml_extraction.get("days_back", int(os.getenv("DAYS_BACK", "730")))
|
|
64
75
|
self.SKIP_MERGES = yaml_extraction.get("skip_merges", True)
|
|
65
76
|
|
|
66
77
|
# Analysis Configuration (YAML: analysis.*)
|
|
@@ -72,14 +83,17 @@ class Config:
|
|
|
72
83
|
self.TEMPORAL_GRANULARITY = os.getenv("TEMPORAL_GRANULARITY", "quarter")
|
|
73
84
|
self.ENABLE_ML_FEATURES = os.getenv("ENABLE_ML_FEATURES", "false").lower() == "true"
|
|
74
85
|
self.VALIDATION_SAMPLE_SIZE = int(os.getenv("VALIDATION_SAMPLE_SIZE", "30"))
|
|
75
|
-
|
|
86
|
+
|
|
76
87
|
# PyDriller options (YAML: analysis.process_metrics, etc.)
|
|
77
|
-
self.PROCESS_METRICS_ENABLED = yaml_analysis.get(
|
|
78
|
-
os.getenv("PROCESS_METRICS_ENABLED", "true").lower() == "true"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
self.PROCESS_METRICS_ENABLED = yaml_analysis.get(
|
|
89
|
+
"process_metrics", os.getenv("PROCESS_METRICS_ENABLED", "true").lower() == "true"
|
|
90
|
+
)
|
|
91
|
+
self.STRUCTURAL_METRICS_ENABLED = yaml_analysis.get(
|
|
92
|
+
"structural_metrics", os.getenv("STRUCTURAL_METRICS_ENABLED", "true").lower() == "true"
|
|
93
|
+
)
|
|
94
|
+
self.DMM_ENABLED = yaml_analysis.get(
|
|
95
|
+
"delta_maintainability", os.getenv("DMM_ENABLED", "true").lower() == "true"
|
|
96
|
+
)
|
|
83
97
|
|
|
84
98
|
# Temporal Filtering
|
|
85
99
|
self.CREATED_AFTER = os.getenv("CREATED_AFTER")
|
|
@@ -102,8 +116,7 @@ class Config:
|
|
|
102
116
|
|
|
103
117
|
# Output Configuration (YAML: output.directory)
|
|
104
118
|
yaml_output = self._yaml_config.get("output", {})
|
|
105
|
-
self.OUTPUT_DIR = Path(yaml_output.get("directory",
|
|
106
|
-
os.getenv("OUTPUT_DIR", "./data")))
|
|
119
|
+
self.OUTPUT_DIR = Path(yaml_output.get("directory", os.getenv("OUTPUT_DIR", "./data")))
|
|
107
120
|
self.OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
108
121
|
|
|
109
122
|
# File Paths
|
|
@@ -119,25 +132,24 @@ class Config:
|
|
|
119
132
|
yaml_urls = self._yaml_config.get("sources", {}).get("urls", [])
|
|
120
133
|
env_urls = self._parse_repository_urls(os.getenv("REPOSITORY_URLS", ""))
|
|
121
134
|
self.REPOSITORY_URLS: List[str] = yaml_urls if yaml_urls else env_urls
|
|
122
|
-
|
|
135
|
+
|
|
123
136
|
# Clone path (YAML: extraction.clone_path)
|
|
124
|
-
self.CLONE_PATH = Path(
|
|
125
|
-
os.getenv("CLONE_PATH", "/tmp/greenmining_repos"))
|
|
126
|
-
self.CLEANUP_AFTER_ANALYSIS = (
|
|
127
|
-
os.getenv("CLEANUP_AFTER_ANALYSIS", "true").lower() == "true"
|
|
137
|
+
self.CLONE_PATH = Path(
|
|
138
|
+
yaml_extraction.get("clone_path", os.getenv("CLONE_PATH", "/tmp/greenmining_repos"))
|
|
128
139
|
)
|
|
140
|
+
self.CLEANUP_AFTER_ANALYSIS = os.getenv("CLEANUP_AFTER_ANALYSIS", "true").lower() == "true"
|
|
129
141
|
|
|
130
142
|
# Energy Measurement (YAML: energy.*)
|
|
131
143
|
yaml_energy = self._yaml_config.get("energy", {})
|
|
132
|
-
self.ENERGY_ENABLED = yaml_energy.get(
|
|
133
|
-
os.getenv("ENERGY_ENABLED", "false").lower() == "true"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
self.CARBON_TRACKING = yaml_energy.get(
|
|
137
|
-
os.getenv("CARBON_TRACKING", "false").lower() == "true"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
self.ENERGY_ENABLED = yaml_energy.get(
|
|
145
|
+
"enabled", os.getenv("ENERGY_ENABLED", "false").lower() == "true"
|
|
146
|
+
)
|
|
147
|
+
self.ENERGY_BACKEND = yaml_energy.get("backend", os.getenv("ENERGY_BACKEND", "rapl"))
|
|
148
|
+
self.CARBON_TRACKING = yaml_energy.get(
|
|
149
|
+
"carbon_tracking", os.getenv("CARBON_TRACKING", "false").lower() == "true"
|
|
150
|
+
)
|
|
151
|
+
self.COUNTRY_ISO = yaml_energy.get("country_iso", os.getenv("COUNTRY_ISO", "USA"))
|
|
152
|
+
|
|
141
153
|
# Power profiling (YAML: energy.power_profiling.*)
|
|
142
154
|
yaml_power = yaml_energy.get("power_profiling", {})
|
|
143
155
|
self.POWER_PROFILING_ENABLED = yaml_power.get("enabled", False)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Flask-based web dashboard for GreenMining analysis visualization.
|
|
2
|
+
# Provides interactive charts for repository analysis, pattern distribution,
|
|
3
|
+
# temporal trends, and energy consumption.
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_app(data_dir: str = "./data"):
|
|
13
|
+
# Create Flask application for the dashboard.
|
|
14
|
+
# Args:
|
|
15
|
+
# data_dir: Path to directory containing analysis JSON files
|
|
16
|
+
# Returns:
|
|
17
|
+
# Flask application instance
|
|
18
|
+
try:
|
|
19
|
+
from flask import Flask, render_template_string, jsonify, request
|
|
20
|
+
except ImportError:
|
|
21
|
+
raise ImportError("Flask is required for the dashboard. Install it with: pip install flask")
|
|
22
|
+
|
|
23
|
+
app = Flask(__name__)
|
|
24
|
+
data_path = Path(data_dir)
|
|
25
|
+
|
|
26
|
+
def _load_data(filename: str) -> Dict[str, Any]:
|
|
27
|
+
filepath = data_path / filename
|
|
28
|
+
if filepath.exists():
|
|
29
|
+
with open(filepath, encoding="utf-8") as f:
|
|
30
|
+
return json.load(f)
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
@app.route("/")
|
|
34
|
+
def index():
|
|
35
|
+
return render_template_string(DASHBOARD_HTML)
|
|
36
|
+
|
|
37
|
+
@app.route("/api/repositories")
|
|
38
|
+
def api_repositories():
|
|
39
|
+
data = _load_data("repositories.json")
|
|
40
|
+
return jsonify(data)
|
|
41
|
+
|
|
42
|
+
@app.route("/api/analysis")
|
|
43
|
+
def api_analysis():
|
|
44
|
+
data = _load_data("analysis_results.json")
|
|
45
|
+
return jsonify(data)
|
|
46
|
+
|
|
47
|
+
@app.route("/api/statistics")
|
|
48
|
+
def api_statistics():
|
|
49
|
+
data = _load_data("aggregated_statistics.json")
|
|
50
|
+
return jsonify(data)
|
|
51
|
+
|
|
52
|
+
@app.route("/api/energy")
|
|
53
|
+
def api_energy():
|
|
54
|
+
data = _load_data("energy_report.json")
|
|
55
|
+
return jsonify(data)
|
|
56
|
+
|
|
57
|
+
@app.route("/api/summary")
|
|
58
|
+
def api_summary():
|
|
59
|
+
# Build summary from available data
|
|
60
|
+
repos = _load_data("repositories.json")
|
|
61
|
+
analysis = _load_data("analysis_results.json")
|
|
62
|
+
|
|
63
|
+
repo_count = 0
|
|
64
|
+
if isinstance(repos, list):
|
|
65
|
+
repo_count = len(repos)
|
|
66
|
+
elif isinstance(repos, dict):
|
|
67
|
+
repo_count = repos.get("total_repositories", len(repos.get("repositories", [])))
|
|
68
|
+
|
|
69
|
+
commit_count = 0
|
|
70
|
+
green_count = 0
|
|
71
|
+
if isinstance(analysis, list):
|
|
72
|
+
commit_count = len(analysis)
|
|
73
|
+
green_count = sum(1 for a in analysis if a.get("green_aware"))
|
|
74
|
+
elif isinstance(analysis, dict):
|
|
75
|
+
results = analysis.get("results", [])
|
|
76
|
+
for r in results:
|
|
77
|
+
commits = r.get("commits", [])
|
|
78
|
+
commit_count += len(commits)
|
|
79
|
+
green_count += sum(1 for c in commits if c.get("green_aware"))
|
|
80
|
+
|
|
81
|
+
green_rate = (green_count / commit_count * 100) if commit_count > 0 else 0
|
|
82
|
+
|
|
83
|
+
return jsonify(
|
|
84
|
+
{
|
|
85
|
+
"repositories": repo_count,
|
|
86
|
+
"commits_analyzed": commit_count,
|
|
87
|
+
"green_commits": green_count,
|
|
88
|
+
"green_rate": round(green_rate, 1),
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return app
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_dashboard(data_dir: str = "./data", host: str = "127.0.0.1", port: int = 5000):
|
|
96
|
+
# Run the dashboard server.
|
|
97
|
+
# Args:
|
|
98
|
+
# data_dir: Path to analysis data directory
|
|
99
|
+
# host: Host to bind to
|
|
100
|
+
# port: Port to bind to
|
|
101
|
+
app = create_app(data_dir)
|
|
102
|
+
print(f"GreenMining Dashboard running at http://{host}:{port}")
|
|
103
|
+
print(f"Data directory: {data_dir}")
|
|
104
|
+
app.run(host=host, port=port, debug=False)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Dashboard HTML template with embedded JS (no external dependencies)
|
|
108
|
+
DASHBOARD_HTML = """
|
|
109
|
+
<!DOCTYPE html>
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="UTF-8">
|
|
113
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
114
|
+
<title>GreenMining Dashboard</title>
|
|
115
|
+
<style>
|
|
116
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
117
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
118
|
+
background: #f5f7fa; color: #333; }
|
|
119
|
+
.header { background: #1a472a; color: white; padding: 20px 40px; }
|
|
120
|
+
.header h1 { font-size: 24px; font-weight: 600; }
|
|
121
|
+
.header p { font-size: 14px; opacity: 0.8; margin-top: 4px; }
|
|
122
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
123
|
+
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
|
|
124
|
+
.card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
125
|
+
.card h3 { font-size: 13px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
126
|
+
.card .value { font-size: 32px; font-weight: 700; color: #1a472a; margin-top: 8px; }
|
|
127
|
+
.card .subtitle { font-size: 12px; color: #999; margin-top: 4px; }
|
|
128
|
+
.section { margin-bottom: 24px; }
|
|
129
|
+
.section h2 { font-size: 18px; margin-bottom: 12px; color: #1a472a; }
|
|
130
|
+
table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px;
|
|
131
|
+
overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
132
|
+
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; }
|
|
133
|
+
th { background: #f8f9fa; font-size: 12px; text-transform: uppercase; color: #666; }
|
|
134
|
+
td { font-size: 14px; }
|
|
135
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; }
|
|
136
|
+
.badge-green { background: #d4edda; color: #155724; }
|
|
137
|
+
.badge-gray { background: #e9ecef; color: #495057; }
|
|
138
|
+
.bar { height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden; }
|
|
139
|
+
.bar-fill { height: 100%; background: #1a472a; border-radius: 4px;
|
|
140
|
+
transition: width 0.5s ease; }
|
|
141
|
+
.loading { text-align: center; padding: 40px; color: #999; }
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<div class="header">
|
|
146
|
+
<h1>GreenMining Dashboard</h1>
|
|
147
|
+
<p>Mining Software Repositories for Green IT Research</p>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="container">
|
|
150
|
+
<div class="grid" id="summary-cards">
|
|
151
|
+
<div class="card"><h3>Repositories</h3><div class="value" id="repo-count">-</div></div>
|
|
152
|
+
<div class="card"><h3>Commits Analyzed</h3><div class="value" id="commit-count">-</div></div>
|
|
153
|
+
<div class="card"><h3>Green Commits</h3><div class="value" id="green-count">-</div></div>
|
|
154
|
+
<div class="card"><h3>Green Rate</h3><div class="value" id="green-rate">-</div></div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="section">
|
|
157
|
+
<h2>Repositories</h2>
|
|
158
|
+
<div id="repo-table"><div class="loading">Loading data...</div></div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<script>
|
|
162
|
+
async function loadDashboard() {
|
|
163
|
+
try {
|
|
164
|
+
const summary = await fetch('/api/summary').then(r => r.json());
|
|
165
|
+
document.getElementById('repo-count').textContent = summary.repositories;
|
|
166
|
+
document.getElementById('commit-count').textContent = summary.commits_analyzed.toLocaleString();
|
|
167
|
+
document.getElementById('green-count').textContent = summary.green_commits.toLocaleString();
|
|
168
|
+
document.getElementById('green-rate').textContent = summary.green_rate + '%';
|
|
169
|
+
} catch(e) {
|
|
170
|
+
console.log('Summary not available:', e);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const repos = await fetch('/api/repositories').then(r => r.json());
|
|
175
|
+
const list = repos.repositories || (Array.isArray(repos) ? repos : []);
|
|
176
|
+
if (list.length > 0) {
|
|
177
|
+
let html = '<table><thead><tr><th>Repository</th><th>Language</th>' +
|
|
178
|
+
'<th>Stars</th><th>Description</th></tr></thead><tbody>';
|
|
179
|
+
list.slice(0, 50).forEach(r => {
|
|
180
|
+
html += '<tr><td><strong>' + (r.full_name || r.name) + '</strong></td>' +
|
|
181
|
+
'<td><span class="badge badge-green">' + (r.language || '-') + '</span></td>' +
|
|
182
|
+
'<td>' + (r.stars || 0).toLocaleString() + '</td>' +
|
|
183
|
+
'<td>' + (r.description || '-').substring(0, 80) + '</td></tr>';
|
|
184
|
+
});
|
|
185
|
+
html += '</tbody></table>';
|
|
186
|
+
document.getElementById('repo-table').innerHTML = html;
|
|
187
|
+
} else {
|
|
188
|
+
document.getElementById('repo-table').innerHTML =
|
|
189
|
+
'<div class="card">No repository data found. Run an analysis first.</div>';
|
|
190
|
+
}
|
|
191
|
+
} catch(e) {
|
|
192
|
+
document.getElementById('repo-table').innerHTML =
|
|
193
|
+
'<div class="card">Run an analysis to populate the dashboard.</div>';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
loadDashboard();
|
|
197
|
+
</script>
|
|
198
|
+
</body>
|
|
199
|
+
</html>
|
|
200
|
+
"""
|
greenmining/energy/__init__.py
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# Energy measurement module for GreenMining.
|
|
2
2
|
|
|
3
|
-
from .base import EnergyMeter, EnergyMetrics, EnergyBackend
|
|
3
|
+
from .base import EnergyMeter, EnergyMetrics, EnergyBackend, CommitEnergyProfile, get_energy_meter
|
|
4
4
|
from .rapl import RAPLEnergyMeter
|
|
5
5
|
from .codecarbon_meter import CodeCarbonMeter
|
|
6
|
+
from .cpu_meter import CPUEnergyMeter
|
|
7
|
+
from .carbon_reporter import CarbonReporter, CarbonReport
|
|
6
8
|
|
|
7
9
|
__all__ = [
|
|
8
10
|
"EnergyMeter",
|
|
9
11
|
"EnergyMetrics",
|
|
10
12
|
"EnergyBackend",
|
|
13
|
+
"CommitEnergyProfile",
|
|
14
|
+
"get_energy_meter",
|
|
11
15
|
"RAPLEnergyMeter",
|
|
12
16
|
"CodeCarbonMeter",
|
|
17
|
+
"CPUEnergyMeter",
|
|
18
|
+
"CarbonReporter",
|
|
19
|
+
"CarbonReport",
|
|
13
20
|
]
|