diff --git a/README.md b/README.md
index 17ad984..c784ed4 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,20 @@
-# hog
+# The Power Hog
-The hog is a tool that periodically collects energy statistics of your mac and makes them available to you.
+The power hog is a tool that periodically collects energy statistics of your mac and makes them available to you.
There are two main aims:
1) Identify which apps are using a lot of energy on your machine.
-2) Collecting the data from as many machines as possible to identify wasteful apps.
+2) Collecting the data from as many machines as possible to identify wasteful apps globally.
-The hog consists of 2 apps.
+We provide a website for detailed analytics of your data. The hog by default uploads your measurement data to our
+[Green Metrics Tool](https://github.com/green-coding-berlin/green-metrics-tool) backend. We put in a lot of effort
+to make sure that no confidential information is exposed but please refer to the [settings](#settings) section if you
+want to disable the upload or submit the data to your own [backend](https://docs.green-coding.berlin/docs/installation/installation-linux/).
+
+The hog consists of 2 apps that run on your local system. You need to power logger but not the app!
## Power logger
@@ -20,7 +25,20 @@ give some statistics. You can either call it by hand and send it to the backgrou
For development purposes we recommend to always first run the program in the foreground and see if everything works fine
and then use the launch agent.
-### Launch agent
+If you want to avoid running the desktop app you can call the `power_logger.py` script with `-w` which will give
+you the details url.
+
+You can also run the `powermetrics` process yourself and then use `power_logger.py` to process the data and upload it.
+You can use the `-f` parameter with a filename. Please submit the data in the plist format. You can use the following call string:
+`powermetrics --show-all -i 5000 -f plist -o FILENAME` and to run the powermetrics process yourself.
+
+### Parameter list
+
+- `-d`: Set's debug/ development mode to true. The Settings are set to local environments and we output statistics when running.
+- `-w`: Gives you the url of the analysis website and exits. This is especially useful when not using the desktop app
+- `-f filename`: Use the file as powermetrics input and don't start the process internally.
+
+### Setup of the power collection script
This is a description on how to set everything up if you did a git clone. You can also just do
@@ -29,6 +47,8 @@ curl -fsSL https://raw.githubusercontent.com/green-coding-berlin/hog/main/instal
```
which will do the whole install for you.
+#### Do it manually
+
Make the `power_logger.py` script executable with `chmod a+x power_logger.py`
Please modify the `berlin.green-coding.hog.plist` file to reference the right path. There is a script below that does
@@ -69,16 +89,30 @@ sudo launchctl unload /Library/LaunchDaemons/berlin.green-coding.hog.plist
### Settings
It is possible to configure your own settings by using a `settings.ini` file in the same directory as the `power_logger.py`
-script. Following keys are currently used:
+script or adding a `.hog_settings.ini` to your home folder. The home folder settings will be prioritized.
+
+Following keys are currently used:
- `powermetrics`: This is the delta in ms that power metrics should take samples. So if you set this to 5000 powermetrics will return the aggregated values every 5 seconds
- `upload_delta`: This is the time delta data should be uploaded in seconds.
- `api_url`: The url endpoint the data should be uploaded to. You can use the https://github.com/green-coding-berlin/green-metrics-tool if you want but also write/ use your own backend.
+- `web_url`: The url where the analytics can be found. We will append the machine ID to this so make sure the end of the string is a `=`
## The desktop App
The hog desktop app gives you analytics of the data that was recorded. Please move this into your app folder.
+### Description of the headings
+
+- `Name`: This is the name of the process coalition. A coalition can be multiple processes. For example a program might fork
+ new process which will all show up in the coalition. Sometimes a shell might turn up here. Please tell us so we can
+ add this as an exception
+- `Energy Impact`: This is the value mac gives it's processes. The exact formula is not known but we know that quite some
+ factors are considered. For now this is the best value we've got 🫣
+- `AVG Cpu Time %`: This is how long this coalition has spent on the CPUs. We take a percentage which can be over 100% as
+ the coalition could run on multiple cpus at the same time. So if a process takes up 100% of cpu time and runs on 4 cpus
+ the time will be 400%.
+
## Database
All data is saved in an sqlite database that is located under:
diff --git a/app/hog/hog.xcodeproj/project.pbxproj b/app/hog/hog.xcodeproj/project.pbxproj
index 5851cfd..45a5127 100644
--- a/app/hog/hog.xcodeproj/project.pbxproj
+++ b/app/hog/hog.xcodeproj/project.pbxproj
@@ -198,7 +198,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1430;
- LastUpgradeCheck = 1430;
+ LastUpgradeCheck = 1500;
TargetAttributes = {
0AEC07722A40D4C2003C82E7 = {
CreatedOnToolsVersion = 14.3.1;
@@ -307,6 +307,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -336,9 +337,11 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -368,6 +371,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -397,9 +401,11 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -429,13 +435,14 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"hog/Preview Content\"";
DEVELOPMENT_TEAM = SBWA476E6F;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = hog/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = Hog;
+ INFOPLIST_KEY_CFBundleDisplayName = "Power Hog";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
@@ -443,7 +450,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MARKETING_VERSION = 1.1;
+ MARKETING_VERSION = 0.2;
PRODUCT_BUNDLE_IDENTIFIER = "berlin.green-coding.hog";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -462,13 +469,14 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"hog/Preview Content\"";
DEVELOPMENT_TEAM = SBWA476E6F;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = hog/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = Hog;
+ INFOPLIST_KEY_CFBundleDisplayName = "Power Hog";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
@@ -476,7 +484,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MARKETING_VERSION = 1.1;
+ MARKETING_VERSION = 0.2;
PRODUCT_BUNDLE_IDENTIFIER = "berlin.green-coding.hog";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -491,6 +499,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = SBWA476E6F;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.3;
@@ -510,6 +519,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = SBWA476E6F;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.3;
@@ -528,6 +538,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = SBWA476E6F;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
@@ -545,6 +556,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = SBWA476E6F;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
diff --git a/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate b/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate
index 8214af3..68be723 100644
Binary files a/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate and b/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/app/hog/hog.xcodeproj/xcshareddata/xcschemes/hog.xcscheme b/app/hog/hog.xcodeproj/xcshareddata/xcschemes/hog.xcscheme
index 1228815..978ba18 100644
--- a/app/hog/hog.xcodeproj/xcshareddata/xcschemes/hog.xcscheme
+++ b/app/hog/hog.xcodeproj/xcshareddata/xcschemes/hog.xcscheme
@@ -1,6 +1,6 @@
Bool {
- // Because of the app sandbox this doesn't work anymore. We should discuss if we drop the sandboxing
-// let process = Process()
-// let outputPipe = Pipe()
-//
-// process.launchPath = "/usr/bin/env"
-// process.arguments = ["pgrep", "-f", scriptName]
-// process.standardOutput = outputPipe
-//
-// do {
-// try process.run()
-// process.waitUntilExit()
-//
-// let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
-// if let output = String(data: outputData, encoding: .utf8), !output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
-// return true
-// }
-// } catch {
-// print("An error occurred: \(error)")
-// }
-//
-// return false
-
+ if isAppSandboxed() {
+ return isScriptRunningUsingDBCheck()
+ } else {
+ return isScriptRunningUsingPGrep(scriptName: scriptName)
+ }
+}
+
+func isAppSandboxed() -> Bool {
+ let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
+ let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: bundleIdentifier)
+ return containerURL != nil
+}
+
+
+private func isScriptRunningUsingPGrep(scriptName: String) -> Bool {
+ let process = Process()
+ let outputPipe = Pipe()
+
+ process.launchPath = "/usr/bin/env"
+ process.arguments = ["pgrep", "-f", scriptName]
+ process.standardOutput = outputPipe
+
+ do {
+ try process.run()
+ process.waitUntilExit()
+
+ let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
+ if let output = String(data: outputData, encoding: .utf8), !output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+ return true
+ }
+ } catch {
+ print("Error checking script using pgrep: \(error)")
+ }
+
+ return false
+}
+
+private func isScriptRunningUsingDBCheck() -> Bool {
var db: OpaquePointer?
var running = false
- if sqlite3_open(db_path, &db) != SQLITE_OK { // Open database
+ if sqlite3_open(db_path, &db) != SQLITE_OK {
print("error opening database")
return false
}
@@ -67,7 +83,6 @@ public func isScriptRunning(scriptName: String) -> Bool {
sqlite3_close(db)
return running
-
}
public func getNameByAppName(appName: String) -> String {
@@ -130,84 +145,84 @@ func checkDB() -> Bool {
return fileManager.fileExists(atPath: db_path)
}
-//class SettingsManager: ObservableObject {
-// var lookBackTime:Int = 0
-//
-// @Published var uploading: Bool = true
-// @Published var upload_url: String = "Loading ..."
-// @Published var isLoading: Bool = false
-//
-// public func refreshData() -> Void{
-// self.isLoading = true
-//
-// DispatchQueue.global(qos: .userInitiated).async {
-// self.loadDataFrom()
-// DispatchQueue.main.async {
-// self.isLoading = false
-// }
-// }
-// }
-//
-// func loadDataFrom() {
-// var db: OpaquePointer?
-//
-//
-// if sqlite3_open(db_path, &db) != SQLITE_OK { // Open database
-// print("error opening database")
-// return
-// }
-//
-// var newEnergy: CGFloat = 0
-// var energyQuery:String
-// if self.lookBackTime == 0 {
-// energyQuery = "SELECT COALESCE(sum(combined_energy), 0) FROM power_measurements;"
-// }else{
-// energyQuery = "SELECT COALESCE(sum(combined_energy), 0) FROM power_measurements WHERE time >= ((CAST(strftime('%s', 'now') AS INTEGER) * 1000) - \(self.lookBackTime));"
-// }
-// if let result: CGFloat = queryDatabase(db: db, query:energyQuery, type: .float) {
-// newEnergy = result
-// }
-//
-//
-// var newTopApp: String = "Loading"
-// var topQuery:String
-//
-// if self.lookBackTime == 0 {
-// topQuery = """
-// SELECT name
-// FROM top_processes
-// GROUP BY name
-// ORDER BY SUM(energy_impact) DESC
-// LIMIT 1; -- to get only the top name
-// """
-// }else{
-// topQuery = """
-// SELECT name
-// FROM top_processes
-// WHERE time >= ((CAST(strftime('%s', 'now') AS INTEGER) * 1000) - \(self.lookBackTime))
-// GROUP BY name
-// ORDER BY SUM(energy_impact) DESC
-// LIMIT 1; -- to get only the top name
-// """
-// }
-//
-// if let result: String = queryDatabase(db: db, query:topQuery, type: .string) {
-// newTopApp = String(result)
-// } else {
-// newTopApp = "No data"
-// }
-//
-// DispatchQueue.main.async {
-// self.energy = newEnergy
-// self.providerRunning = isScriptRunning(scriptName: "power_logger.py")
-// self.topApp = newTopApp
-// }
-//
-// sqlite3_close(db)
-//
-// }
-//
-//}
+class SettingsManager: ObservableObject {
+ var lookBackTime:Int = 0
+
+ @Published var machine_id: String = "Loading ..."
+ @Published var powermetrics: Int = 0
+ @Published var api_url: String = "Loading ..."
+ @Published var web_url: String = "Loading ..."
+ @Published var upload_data: Bool = true
+
+ @Published var upload_backlog: Int = 0
+
+ @Published var isLoading: Bool = false
+
+ init(){
+ self.isLoading = true
+
+ DispatchQueue.global(qos: .userInitiated).async {
+ self.loadDataFrom()
+ DispatchQueue.main.async {
+ self.isLoading = false
+ }
+ }
+ }
+
+ func loadDataFrom() {
+ var db: OpaquePointer?
+
+ if sqlite3_open(db_path, &db) != SQLITE_OK { // Open database
+ print("error opening database")
+ return
+ }
+
+ let lastMeasurementQuery = "SELECT machine_id, powermetrics, api_url, web_url, upload_data FROM settings ORDER BY time DESC LIMIT 1;"
+ var queryStatement: OpaquePointer?
+
+ var new_machine_id = "Loading ..."
+ var new_powermetrics: Int = 0
+ var new_api_url = "Loading ..."
+ var new_web_url = "Loading ..."
+ var upload_data = true
+
+ if sqlite3_prepare_v2(db, lastMeasurementQuery, -1, &queryStatement, nil) == SQLITE_OK {
+ if sqlite3_step(queryStatement) == SQLITE_ROW {
+ new_machine_id = String(cString: sqlite3_column_text(queryStatement, 0))
+ new_powermetrics = Int(sqlite3_column_int(queryStatement, 1))
+ new_api_url = String(cString: sqlite3_column_text(queryStatement, 2))
+ new_web_url = String(cString: sqlite3_column_text(queryStatement, 3))
+ upload_data = sqlite3_column_int(queryStatement, 4) != 0 // assuming it's stored as 0 for false, non-0 for true
+ }
+ sqlite3_finalize(queryStatement)
+ }
+
+ let uploadCountQuery = "SELECT COUNT(*) FROM measurements WHERE uploaded = 0;"
+ var new_upload_backlog: Int = 0
+
+ if sqlite3_prepare_v2(db, uploadCountQuery, -1, &queryStatement, nil) == SQLITE_OK {
+ if sqlite3_step(queryStatement) == SQLITE_ROW {
+ new_upload_backlog = Int(sqlite3_column_int(queryStatement, 0))
+ }
+ sqlite3_finalize(queryStatement) // Always finalize your statement when done
+ } else {
+ print("SELECT statement could not be prepared")
+ }
+
+ sqlite3_close(db)
+
+ DispatchQueue.main.async {
+ self.machine_id = new_machine_id
+ self.powermetrics = new_powermetrics
+ self.api_url = new_api_url
+ self.web_url = new_web_url
+ self.upload_data = upload_data
+ self.upload_backlog = new_upload_backlog
+ }
+
+ }
+
+}
class ValueManager: ObservableObject {
@@ -216,22 +231,21 @@ class ValueManager: ObservableObject {
@Published var energy: CGFloat = 0
@Published var providerRunning: Bool = false
@Published var topApp: String = "Loading..."
- @Published var isLoading: Bool = false
+ @Published var isLoading: Bool = true
+
enum ValueType {
case float
case string
}
public func refreshData(lookBackTime: Int = 0) -> Void{
+
self.isLoading = true
self.lookBackTime = lookBackTime
DispatchQueue.global(qos: .userInitiated).async {
self.loadDataFrom()
- DispatchQueue.main.async {
- self.isLoading = false
- }
}
}
@@ -284,10 +298,13 @@ class ValueManager: ObservableObject {
newTopApp = "No data"
}
+ let newScriptRunning = isScriptRunning(scriptName: "power_logger.py")
+
DispatchQueue.main.async {
self.energy = newEnergy
- self.providerRunning = isScriptRunning(scriptName: "power_logger.py")
+ self.providerRunning = newScriptRunning
self.topApp = newTopApp
+ self.isLoading = false
}
sqlite3_close(db)
@@ -322,14 +339,14 @@ struct TopProcess: Codable, Identifiable {
let id: UUID = UUID() // Add this line if you want a unique identifier
let name: String
let energy_impact: Double
- let cputime_ns: Int64
+ let cputime_per: Int32
enum CodingKeys: String, CodingKey {
- case name, energy_impact, cputime_ns
+ case name, energy_impact, cputime_per
}
}
-class TopProcessData: ObservableObject, RandomAccessCollection {
+class TopProcessData: Identifiable, ObservableObject, RandomAccessCollection {
var lookBackTime:Int = 0
typealias Element = TopProcess
typealias Index = Array.Index
@@ -339,20 +356,34 @@ class TopProcessData: ObservableObject, RandomAccessCollection {
var startIndex: Index { lines.startIndex }
var endIndex: Index { lines.endIndex }
- @Published var isLoading: Bool = false
+ @Published var isLoading: Bool = true
subscript(position: Index) -> Element {
lines[position]
}
+ func sort(using sortOrder: [KeyPathComparator]) {
+ // Implement sorting logic
+ lines.sort { a, b in
+ for comparator in sortOrder {
+ switch comparator.compare(a, b) {
+ case .orderedAscending:
+ return true
+ case .orderedDescending:
+ return false
+ case .orderedSame:
+ continue
+ }
+ }
+ return false
+ }
+ }
+
public func refreshData(lookBackTime: Int = 0) -> Void{
self.isLoading = true
self.lookBackTime = lookBackTime
DispatchQueue.global(qos: .userInitiated).async {
self.loadDataFrom()
- DispatchQueue.main.async {
- self.isLoading = false
- }
}
}
@@ -371,7 +402,7 @@ class TopProcessData: ObservableObject, RandomAccessCollection {
let queryString: String
if self.lookBackTime == 0 {
queryString = """
- SELECT name, SUM(energy_impact), SUM(cputime_ns)
+ SELECT name, SUM(energy_impact), AVG(cputime_per)
FROM top_processes
GROUP BY name
ORDER BY SUM(energy_impact) DESC
@@ -380,7 +411,7 @@ class TopProcessData: ObservableObject, RandomAccessCollection {
"""
} else {
queryString = """
- SELECT name, SUM(energy_impact), SUM(cputime_ns)
+ SELECT name, SUM(energy_impact), AVG(cputime_per)
FROM top_processes
WHERE time >= ((CAST(strftime('%s', 'now') AS INTEGER) * 1000) - \(self.lookBackTime))
GROUP BY name
@@ -396,12 +427,14 @@ class TopProcessData: ObservableObject, RandomAccessCollection {
name = String(cString: namePointer)
}
let energy_impact = sqlite3_column_double(queryStatement, 1)
- let cputime_ns = sqlite3_column_int64(queryStatement, 2)
+ let cputime_per = sqlite3_column_int(queryStatement, 2)
- newLines.append(TopProcess(name: name, energy_impact: energy_impact, cputime_ns: cputime_ns))
+ newLines.append(TopProcess(name: name, energy_impact: energy_impact, cputime_per: cputime_per))
}
DispatchQueue.main.async {
self.lines = newLines
+ self.isLoading = false
+
}
}
@@ -530,6 +563,16 @@ struct PointsGraph: View {
struct TopProcessTable: View {
@ObservedObject var tpData: TopProcessData
+ @State private var sortOrder = [
+ //KeyPathComparator(\TopProcess.name, order: .forward),
+ KeyPathComparator(\TopProcess.energy_impact, order: .forward),
+ KeyPathComparator(\TopProcess.cputime_per, order: .forward),
+ ]
+ @Environment(\.colorScheme) var colorScheme
+
+ var tableColour: Color {
+ return colorScheme == .dark ? Color.white : Color.primary
+ }
init(tpData: TopProcessData) {
self.tpData = tpData
@@ -544,28 +587,37 @@ struct TopProcessTable: View {
} else {
if tpData.isEmpty {
} else {
- Table(tpData) {
- TableColumn(""){ line in
- Image(nsImage: getIconByAppName(appName: line.name) ?? NSImage())
- .resizable()
- .frame(width: 15, height: 15)
- .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ VStack{
+ Table(tpData, sortOrder: $sortOrder) {
+ TableColumn(""){ line in
+ Image(nsImage: getIconByAppName(appName: line.name) ?? NSImage())
+ .resizable()
+ .frame(width: 15, height: 15)
+ .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+
+ }.width(20)
- }.width(20)
-
- TableColumn("Name", value: \.name)
- TableColumn("Energy Impact"){ line in
- Text(String(format: "%.2f", line.energy_impact))
- }
- TableColumn("CPU time"){ line in
- Text(String(line.cputime_ns))
+ TableColumn("Name", value: \TopProcess.name)
+ TableColumn("Energy Impact", value: \TopProcess.energy_impact){ line in
+ Text(String(format: "%.0f", line.energy_impact))
+ }
+ TableColumn("AVG CPU time %", value: \TopProcess.cputime_per){ line in
+ Text(String(line.cputime_per))
+ }
}
- }
- .padding()
- .tableStyle(.bordered(alternatesRowBackgrounds: true))
+ .onChange(of: sortOrder) { newOrder in
+ tpData.sort(using: newOrder)
+ }.foregroundColor(tableColour)
- }
+ .tableStyle(.bordered(alternatesRowBackgrounds: true))
+ HStack {
+ Spacer() // Pushes the Link to the right side.
+ Link("Description", destination: URL(string: "https://github.com/green-coding-berlin/hog#the-desktop-app")!)
+ .font(.footnote) // This makes the font size smaller.
+ }
+ }.padding()
+ }
}
}
}
@@ -577,12 +629,18 @@ struct DataView: View {
@State var lineData = TopProcessData()
@ObservedObject var valueManager = ValueManager()
@State private var isHovering = false
+ @State private var refreshFlag = false
+
+ var settingsManager = SettingsManager()
var lookBackTime: Int
init(lookBackTime: Int = 0) {
self.lookBackTime = lookBackTime
+ self.chartData.refreshData(lookBackTime: self.lookBackTime)
+ self.lineData.refreshData(lookBackTime: self.lookBackTime)
+ self.valueManager.refreshData(lookBackTime: self.lookBackTime)
}
var body: some View {
@@ -590,15 +648,11 @@ struct DataView: View {
HStack {
VStack(alignment: .leading, spacing: 8) {
- Text("This is a minimalistic overview of your energy usage and the apps that are using the most resources.")
+ Text("This is a very minimalistic overview of your energy usage.")
}
Spacer(minLength: 10)
- Button("Detailed analytics") {
- if let url = URL(string: "https://metrics.green-coding.berlin/hog.html?machine_id=\(getMachineId())") {
- NSWorkspace.shared.open(url)
- }
- }
+
Button(action: {
self.chartData.refreshData(lookBackTime: self.lookBackTime)
self.lineData.refreshData(lookBackTime: self.lookBackTime)
@@ -616,19 +670,32 @@ struct DataView: View {
VStack{
VStack(spacing: 0) {
- ProcessBadge(title: "App with the highest energy usage", color: Color("chartColor2"), process: valueManager.topApp)
- EnergyBadge(title: "System energy usage", color: Color("chartColor2"), image: "clock.badge.checkmark", value: valueManager.energy, unit: "mJ")
- if valueManager.providerRunning {
- TextBadge(title: "", color: Color("chartColor2"), image: "checkmark.seal", value: "Provider App running")
+ if valueManager.isLoading {
+ Text("Loading")
} else {
- HStack{
- TextBadge(title: "", color: Color("red"), image: "exclamationmark.octagon", value: "Provider App is not running")
- Link(destination: URL(string: "https://github.com/green-coding-berlin/hog#power-logger")!) {
- Image(systemName: "questionmark.circle.fill")
- .font(.system(size: 24))
+ ProcessBadge(title: "App with the highest energy usage", color: Color("chartColor2"), process: valueManager.topApp)
+ EnergyBadge(title: "System energy usage", color: Color("chartColor2"), image: "clock.badge.checkmark", value: valueManager.energy)
+ if valueManager.providerRunning {
+ TextBadge(title: "", color: Color("chartColor2"), image: "checkmark.seal", value: "All measurement systems are functional")
+ } else {
+ HStack{
+ TextBadge(title: "", color: Color("red"), image: "exclamationmark.octagon", value: "Measurement systems not running!")
+ Link(destination: URL(string: "https://github.com/green-coding-berlin/hog#power-logger")!) {
+ Image(systemName: "questionmark.circle.fill")
+ .font(.system(size: 24))
+ }
}
}
}
+ Button(action: {
+ if let url = URL(string: "\(settingsManager.web_url)\(settingsManager.machine_id)") {
+ NSWorkspace.shared.open(url)
+ }
+ }) {
+ Text("View Detailed Analytics")
+ .padding(20)
+ }
+
}
@@ -636,11 +703,6 @@ struct DataView: View {
TopProcessTable(tpData: lineData)
}
- .onAppear {
- self.chartData.refreshData(lookBackTime: self.lookBackTime)
- self.lineData.refreshData(lookBackTime: self.lookBackTime)
- self.valueManager.refreshData(lookBackTime: self.lookBackTime)
- }
}.padding()
}
@@ -665,15 +727,28 @@ func ProcessBadge(title: String, color: Color, process: String)->some View {
.frame(maxWidth: .infinity, alignment: .leading)
}
+func formatEnergy(_ mJ: Double) -> String {
+ let joules = mJ / 1000.0
+ let wattHours = joules / 3600.0
+ let wattMinutes = joules / 60.0
+
+ if wattHours >= 1 {
+ return String(format: "%.2f Watt Hours", wattHours)
+ } else {
+ return String(format: "%.2f Watt Min", wattMinutes)
+ }
+}
+
+
@ViewBuilder
-func EnergyBadge(title: String, color: Color, image: String, value: CGFloat, unit: String)->some View {
+func EnergyBadge(title: String, color: Color, image: String, value: CGFloat)->some View {
HStack {
Image(systemName: image)
.font(.title2)
.foregroundColor(color)
.padding(10)
- Text(String(format: "%.1f %@", value / 1000, unit))
+ Text(String(format: "%@", formatEnergy(value)))
.font(.title2.bold())
Text(title)
@@ -760,7 +835,7 @@ struct ThreeView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
- Text("3) You will need to enter your password").font(.headline)
+ Text("3) You will need to enter your password and install xcode tools.").font(.headline)
Button(action: {
showInfo.toggle()
}) {
@@ -846,42 +921,68 @@ struct InstallView: View {
}
+private struct SettingDetailView: View {
+ let title: String
+ let value: String
+
+ var body: some View {
+ Group {
+ Text(title)
+ .bold()
+ Text(value)
+ .padding(.bottom, 10)
+ }
+ }
+}
struct SettingsView: View {
+
+ @ObservedObject var settingsManager = SettingsManager()
+
var body: some View {
- VStack{
- Text("Settings").font(.headline)
- Text("Not much to see till now as everything works out of the box :)")
- Text("- Upload data")
- Text("- Upload URL")
+ VStack(alignment: .leading) {
+ Text("Settings")
+ .font(.headline)
+ .bold()
+ Text("These are the settings that are set by the power logger.\nPlease refer to https://github.com/green-coding-berlin/hog#settings")
+ Divider().padding()
+ SettingDetailView(title: "Machine ID:", value: settingsManager.machine_id)
+ SettingDetailView(title: "Powermetrics Intervall:", value: "\(settingsManager.powermetrics)")
+ SettingDetailView(title: "Upload to URL:", value: settingsManager.api_url)
+ SettingDetailView(title: "Web View URL:", value: settingsManager.web_url)
+ SettingDetailView(title: "Upload data:", value: settingsManager.upload_data ? "Yes" : "No")
+ SettingDetailView(title: "Upload Backlog Count:", value: "\(settingsManager.upload_backlog)")
+
}
+ .padding()
}
}
+
struct DetailView: View {
@ObservedObject var viewModel = InstallViewModel()
@Environment(\.colorScheme) var colorScheme
var body: some View {
- if checkDB() || viewModel.renderToggle {
+ if checkDB() {
TabView(selection: $viewModel.selectedTab) {
DataView(lookBackTime: 300000)
.tabItem {
- Label("last 5 minutes", systemImage: "list.dash")
+ Label("Last 5 Minutes", systemImage: "list.dash")
}
.tag(TabSelection.last5Minutes)
DataView(lookBackTime: 86400000)
.tabItem {
- Label("last 24 hours", systemImage: "square.and.pencil")
+ Label("Last 24 Hours", systemImage: "square.and.pencil")
}
.tag(TabSelection.last24Hours)
DataView()
.tabItem {
- Label("all time", systemImage: "square.and.pencil")
+ Label("All Time", systemImage: "square.and.pencil")
}
.tag(TabSelection.allTime)
diff --git a/app/hog/hog/hogApp.swift b/app/hog/hog/hogApp.swift
index bb2b1e0..a77be75 100644
--- a/app/hog/hog/hogApp.swift
+++ b/app/hog/hog/hogApp.swift
@@ -13,9 +13,7 @@ struct hogApp: App {
MenuBarExtra("QuickView", image: "logo") {
DetailView().frame(
- minWidth: 600, maxWidth: 800,
- minHeight: 850, maxHeight: 1000)
-
+ minWidth: 600, minHeight: 850)
}.menuBarExtraStyle(WindowMenuBarExtraStyle())
}
diff --git a/hog.app/Contents/Info.plist b/hog.app/Contents/Info.plist
index e134ea2..d441945 100644
--- a/hog.app/Contents/Info.plist
+++ b/hog.app/Contents/Info.plist
@@ -3,11 +3,11 @@
BuildMachineOSBuild
- 22G90
+ 22G91
CFBundleDevelopmentRegion
en
CFBundleDisplayName
- Hog
+ Power Hog
CFBundleExecutable
hog
CFBundleIconFile
@@ -23,7 +23,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.1
+ 0.2
CFBundleSupportedPlatforms
MacOSX
@@ -37,20 +37,22 @@
DTPlatformName
macosx
DTPlatformVersion
- 13.3
+ 14.0
DTSDKBuild
- 22E245
+ 23A334
DTSDKName
- macosx13.3
+ macosx14.0
DTXcode
- 1431
+ 1500
DTXcodeBuild
- 14E300c
+ 15A240d
LSApplicationCategoryType
public.app-category.developer-tools
LSMinimumSystemVersion
13.3
LSUIElement
+ com.apple.security.app-sandbox
+
diff --git a/hog.app/Contents/MacOS/hog b/hog.app/Contents/MacOS/hog
index 1d08db5..da3deb1 100755
Binary files a/hog.app/Contents/MacOS/hog and b/hog.app/Contents/MacOS/hog differ
diff --git a/hog.app/Contents/Resources/Assets.car b/hog.app/Contents/Resources/Assets.car
index 598be79..b5ba2f8 100644
Binary files a/hog.app/Contents/Resources/Assets.car and b/hog.app/Contents/Resources/Assets.car differ
diff --git a/hog.app/Contents/Resources/demo_db.db b/hog.app/Contents/Resources/demo_db.db
new file mode 100644
index 0000000..f5b8310
Binary files /dev/null and b/hog.app/Contents/Resources/demo_db.db differ
diff --git a/hog.app/Contents/_CodeSignature/CodeResources b/hog.app/Contents/_CodeSignature/CodeResources
index 8584f68..8cc7ec0 100644
--- a/hog.app/Contents/_CodeSignature/CodeResources
+++ b/hog.app/Contents/_CodeSignature/CodeResources
@@ -10,7 +10,11 @@
Resources/Assets.car
- zIUlYVS50122TcRTB4jHUSLwH5w=
+ KVS4Oun43bgfxB3YkNfdhw1dl3g=
+
+ Resources/demo_db.db
+
+ NmamsuHtbF3MJSq/eA2hH4xS5xc=
files2
@@ -26,7 +30,14 @@
hash2
- 7BIv92peWXXNaN0yViQ/4WWo114V2m2LKRsG0E72IMg=
+ Tyc2sRL+1ilGRhjOAlNflHIsFpR7wKLVWqMqs6PBik8=
+
+
+ Resources/demo_db.db
+
+ hash2
+
+ 4GA8WkYJ3DAZ/cliiqHc7iXRieF/BtZ+YXzOW1z3MsI=
diff --git a/install.sh b/install.sh
index 86470b7..e75d544 100644
--- a/install.sh
+++ b/install.sh
@@ -47,7 +47,7 @@ chmod +x /usr/local/bin/hog/power_logger.py
mv /usr/local/bin/hog/berlin.green-coding.hog.plist /Library/LaunchDaemons/berlin.green-coding.hog.plist
-sed -i.bak "s|PATH_PLASE_CHANGE|/usr/local/bin/hog/|g" /Library/LaunchDaemons/berlin.green-coding.hog.plist
+sed -i "s|PATH_PLASE_CHANGE|/usr/local/bin/hog/|g" /Library/LaunchDaemons/berlin.green-coding.hog.plist
chown root:wheel /Library/LaunchDaemons/berlin.green-coding.hog.plist
chmod 644 /Library/LaunchDaemons/berlin.green-coding.hog.plist
diff --git a/migrations/20230909161250_first_db.py b/migrations/20230909161250_first_db.py
index c78a03e..f41c4eb 100644
--- a/migrations/20230909161250_first_db.py
+++ b/migrations/20230909161250_first_db.py
@@ -10,7 +10,6 @@ def upgrade(connection):
(id INTEGER PRIMARY KEY,
time INT,
data STRING,
- settings STRING,
uploaded INT)'''
connection.execute(tbl_measurements)
@@ -24,11 +23,17 @@ def upgrade(connection):
connection.execute(tbl_power_measurements)
tbl_top_processes = '''CREATE TABLE IF NOT EXISTS top_processes
- (time INT, name STRING, energy_impact REAL, cputime_ns INT)'''
+ (time INT, name STRING, energy_impact REAL, cputime_per INT)'''
connection.execute(tbl_top_processes)
tbl_settings = '''CREATE TABLE IF NOT EXISTS settings
- (machine_id TEXT)'''
+ (time INT,
+ machine_id TEXT,
+ powermetrics INT,
+ api_url STRING,
+ web_url STRING,
+ upload_delta INT,
+ upload_data NUMERIC)'''
connection.execute(tbl_settings)
connection.commit()
diff --git a/power_logger.py b/power_logger.py
index 9e23e37..96190ff 100755
--- a/power_logger.py
+++ b/power_logger.py
@@ -28,7 +28,11 @@
stop_signal = False
stats = {
- 'combined_power':0
+ 'combined_power': 0,
+ 'cpu_energy': 0,
+ 'gpu_energy': 0,
+ 'ane_energy': 0,
+ 'energy_impact': 0,
}
def sigint_handler(_, __):
@@ -38,7 +42,7 @@ def sigint_handler(_, __):
sys.exit()
stop_signal = True
- print("Received stop signal. Terminating all processes.")
+ print('Received stop signal. Terminating all processes.')
def siginfo_handler(_, __):
print(stats)
@@ -49,31 +53,48 @@ def siginfo_handler(_, __):
signal.signal(signal.SIGINFO, siginfo_handler)
-APP_NAME = "berlin.green-coding.hog"
+APP_NAME = 'berlin.green-coding.hog'
app_support_path = Path(f"/Library/Application Support/{APP_NAME}")
app_support_path.mkdir(parents=True, exist_ok=True)
DATABASE_FILE = app_support_path / 'db.db'
-MIGRATIONS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "migrations")
-
-config = configparser.ConfigParser()
-config.read('settings.ini')
+MIGRATIONS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'migrations')
default_settings = {
'powermetrics': 5000,
'upload_delta': 300,
'api_url': 'https://api.green-coding.berlin/v1/hog/add',
+ 'web_url': 'http://metrics.green-coding.berlin/hog-details.html?machine_id=',
'upload_data': True,
}
+home_dir = os.path.expanduser('~')
+script_dir = os.path.dirname(os.path.realpath(__file__))
+
+if os.path.exists(os.path.join(home_dir, '.hog_settings.ini')):
+ config_path = os.path.join(home_dir, '.hog_settings.ini')
+elif os.path.exists(os.path.join(script_dir, 'settings.ini')):
+ config_path = os.path.join(script_dir, 'settings.ini')
+else:
+ config_path = None
+
+config = configparser.ConfigParser()
+
+SETTINGS = {}
+if config_path:
+ config.read(config_path)
+ SETTINGS = {
+ 'powermetrics': config['DEFAULT'].get('powermetrics', default_settings['powermetrics']),
+ 'upload_delta': config['DEFAULT'].get('upload_delta', default_settings['upload_delta']),
+ 'api_url': config['DEFAULT'].get('api_url', default_settings['api_url']),
+ 'web_url': config['DEFAULT'].get('web_url', default_settings['web_url']),
+ 'upload_data': config['DEFAULT'].getboolean('upload_data', default_settings['upload_data']),
+ }
+else:
+ SETTINGS = default_settings
+
-SETTINGS = {
- 'powermetrics': config['DEFAULT'].get('powermetrics', default_settings['powermetrics']),
- 'upload_delta': config['DEFAULT'].get('upload_delta', default_settings['upload_delta']),
- 'api_url': config['DEFAULT'].get('api_url', default_settings['api_url']),
- 'upload_data': config['DEFAULT'].getboolean('upload_data', default_settings['upload_data']),
-}
machine_id = None
@@ -81,50 +102,56 @@ def siginfo_handler(_, __):
c = conn.cursor()
-def run_powermetrics(debug: bool):
-
- # We ignore stderr here as powermetrics is quite verbose on stderr and the buffer fills up quite fast
- cmd = ['powermetrics',
- '--show-all',
- '-i', str(SETTINGS['powermetrics']),
- '-f', 'plist']
+def run_powermetrics(debug: bool, filename: str = None):
- process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
+ def process_lines(lines, debug):
+ buffer = []
+ last_upload_time = time.time()
+ for line in lines:
+ line = line.strip().replace('&', '&')
+ buffer.append(line)
+ if line == '':
+ parse_powermetrics_output(''.join(buffer))
- buffer = []
- last_upload_time = time.time()
+ if debug:
+ print(stats)
+ sys.stdout.flush()
- for line in process.stdout:
- line = line.strip().replace("&", "&")
+ buffer = []
- buffer.append(line)
- if line == '':
- # We only add the data to the queue once it is complete to avoid race conditions
- parse_powermetrics_output(''.join(buffer))
+ if SETTINGS['upload_data']:
+ current_time = time.time()
+ if current_time - last_upload_time >= SETTINGS['upload_delta']:
+ upload_data_to_endpoint()
+ last_upload_time = current_time
- if debug:
- print(stats)
- sys.stdout.flush()
+ if stop_signal:
+ break
- buffer = []
-
- if SETTINGS['upload_data']:
- current_time = time.time()
- if current_time - last_upload_time >= SETTINGS['upload_delta']:
- upload_data_to_endpoint()
- last_upload_time = current_time
+ if filename:
+ with open(filename, 'r') as file:
+ lines = file.readlines()
+ process_lines(lines, debug)
+ else:
+ cmd = ['powermetrics',
+ '--show-all',
+ '-i', str(SETTINGS['powermetrics']),
+ '-f', 'plist']
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
+ process_lines(process.stdout, debug)
if stop_signal:
process.terminate()
- break
+ # Make sure that all data has been uploaded when exiting
+ upload_data_to_endpoint()
def upload_data_to_endpoint():
while True:
# We need to limit the amount of data here as otherwise the payload becomes to big
- c.execute("SELECT id, time, data, settings FROM measurements WHERE uploaded = 0 LIMIT 10;")
+ c.execute('SELECT id, time, data FROM measurements WHERE uploaded = 0 LIMIT 10;')
rows = c.fetchall()
if not rows:
@@ -132,10 +159,12 @@ def upload_data_to_endpoint():
payload = []
for row in rows:
- row_id, time_val, data_val, settings_val = row
+ row_id, time_val, data_val = row
- settings_upload = json.loads(settings_val)
- del settings_upload['api_url'] # We don't need this in the DB on the server
+ settings_upload = SETTINGS.copy()
+ # We don't need this in the DB on the server
+ del settings_upload['api_url']
+ del settings_upload['web_url']
payload.append({
'time': time_val,
@@ -154,7 +183,7 @@ def upload_data_to_endpoint():
with urllib.request.urlopen(req) as response:
if response.status == 200:
for p in payload:
- c.execute("UPDATE measurements SET uploaded = ?, data = NULL WHERE id = ?;", (int(time.time()), p['row_id']))
+ c.execute('UPDATE measurements SET uploaded = ?, data = NULL WHERE id = ?;', (int(time.time()), p['row_id']))
conn.commit()
else:
print(f"Failed to upload data: {payload}\n HTTP status: {response.status}")
@@ -170,16 +199,12 @@ def upload_data_to_endpoint():
pass
-
-###### END IMPORT BLOCK #######
-
-
def find_top_processes(data: list):
# As iterm2 will probably show up as it spawns the processes called from the shell we look at the tasks
new_data = []
for coalition in data:
if coalition['name'] == 'com.googlecode.iterm2' or coalition['name'].strip() == '':
- new_data.extend(coalition["tasks"])
+ new_data.extend(coalition['tasks'])
else:
new_data.append(coalition)
@@ -211,47 +236,104 @@ def parse_powermetrics_output(output: str):
compressed_data = zlib.compress(str(json.dumps(data)).encode())
compressed_data_str = base64.b64encode(compressed_data).decode()
- c.execute("INSERT INTO measurements (time, data, settings, uploaded) VALUES (?, ?, ?, 0)",
- (data['timestamp'], compressed_data_str, json.dumps(SETTINGS)))
-
- c.execute("""INSERT INTO power_measurements
+ c.execute('INSERT INTO measurements (time, data, uploaded) VALUES (?, ?, 0)',
+ (data['timestamp'], compressed_data_str))
+
+ cpu_energy_data = {}
+ if 'ane_energy' in data['processor']:
+ cpu_energy_data = {
+ 'combined_power': int(data['processor'].get('combined_power', 0) * data['elapsed_ns'] / 1_000_000_000.0),
+ 'cpu_energy': int(data['processor'].get('cpu_energy', 0)),
+ 'gpu_energy': int(data['processor'].get('gpu_energy', 0)),
+ 'ane_energy': int(data['processor'].get('ane_energy', 0)),
+ 'energy_impact': data['all_tasks'].get('energy_impact'),
+ }
+ elif 'package_joules' in data['processor']:
+ # Intel processors report in joules/ watts and not mJ
+ cpu_energy_data = {
+ 'combined_power': int(data['processor'].get('package_joules', 0) * 1_000),
+ 'cpu_energy': int(data['processor'].get('cpu_joules', 0) * 1_000),
+ 'gpu_energy': int(data['processor'].get('igpu_watts', 0) * data['elapsed_ns'] / 1_000_000_000.0 * 1_000),
+ 'ane_energy': 0,
+ 'energy_impact': data['all_tasks'].get('energy_impact'),
+ }
+
+ c.execute('''INSERT INTO power_measurements
(time, combined_energy, cpu_energy, gpu_energy, ane_energy, energy_impact) VALUES
- (?, ?, ?, ?, ?, ?)""",
+ (?, ?, ?, ?, ?, ?)''',
(data['timestamp'],
- int(data['processor'].get('combined_power', 0) * data['elapsed_ns'] / 1_000_000_000.0),
- int(data['processor'].get('cpu_energy', 0)),
- int(data['processor'].get('gpu_energy', 0)),
- int(data['processor'].get('ane_energy', 0)),
- data['all_tasks'].get('energy_impact'),
- ))
+ cpu_energy_data['combined_power'],
+ cpu_energy_data['cpu_energy'],
+ cpu_energy_data['gpu_energy'],
+ cpu_energy_data['ane_energy'],
+ cpu_energy_data['energy_impact']))
+
+ for key in stats.keys():
+ stats[key] += cpu_energy_data[key]
- stats['combined_power'] += data['processor'].get('combined_power', 0) * data['elapsed_ns'] / 1_000_000_000.0
for process in find_top_processes(data['coalitions']):
- c.execute("INSERT INTO top_processes (time, name, energy_impact, cputime_ns) VALUES (?, ?, ?, ?)",
- (data['timestamp'], process['name'], process['energy_impact'], process['cputime_ns']))
+ cpu_per = int(process['cputime_ns'] / data['elapsed_ns'] * 100)
+ c.execute('INSERT INTO top_processes (time, name, energy_impact, cputime_per) VALUES (?, ?, ?, ?)',
+ (data['timestamp'], process['name'], process['energy_impact'], cpu_per))
conn.commit()
+def save_settings():
+ global machine_id
+
+ c.execute('SELECT machine_id, powermetrics, api_url, web_url, upload_delta, upload_data FROM settings ORDER BY time DESC LIMIT 1;')
+ result = c.fetchone()
+
+ if result:
+ machine_id, last_powermetrics, last_api_url, last_web_url, last_upload_delta, last_upload_data = result
+
+ if (last_powermetrics == SETTINGS['powermetrics'] and
+ last_api_url.strip() == SETTINGS['api_url'].strip() and
+ last_web_url.strip() == SETTINGS['web_url'].strip() and
+ last_upload_delta == SETTINGS['upload_delta'] and
+ last_upload_data == SETTINGS['upload_data']):
+ return
+ else:
+ machine_id = str(uuid.uuid1())
+
+ c.execute('''INSERT INTO settings
+ (time, machine_id, powermetrics, api_url, web_url, upload_delta, upload_data) VALUES
+ (?, ?, ?, ?, ?, ?, ?)''', (
+ int(time.time()),
+ machine_id,
+ SETTINGS['powermetrics'],
+ SETTINGS['api_url'].strip(),
+ SETTINGS['web_url'].strip(),
+ SETTINGS['upload_delta'],
+ SETTINGS['upload_data'],
+ ))
+
+ conn.commit()
+
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=
- """A powermetrics wrapper that does simple parsing and writes to a file.""")
- parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode')
+ '''A powermetrics wrapper that does simple parsing and writes to a file.''')
+ parser.add_argument('-d', '--debug', action='store_true', help='Enable debug/ development mode')
+ parser.add_argument('-w', '--website', action='store_true', help='Shows the website URL')
+ parser.add_argument('-f', '--file', type=str, help='Path to the input file')
+
args = parser.parse_args()
if args.debug:
SETTINGS = {
'powermetrics' : 1000,
'upload_delta': 5,
- 'api_url': "http://api.green-coding.internal:9142/v1/hog/add",
+ 'api_url': 'http://api.green-coding.internal:9142/v1/hog/add',
+ 'web_url': 'http://metrics.green-coding.internal:9142/hog-details.html?machine_id=',
'upload_data': True,
}
if os.geteuid() != 0:
- print("The script needs to be run as root!")
- sys.exit()
+ print('The script needs to be run as root!')
+ sys.exit(1)
# Make sure that everyone can write to the DB
os.chmod(DATABASE_FILE, stat.S_IRUSR | stat.S_IWUSR |
@@ -262,17 +344,14 @@ def parse_powermetrics_output(output: str):
# Make sure the DB is migrated
caribou.upgrade(DATABASE_FILE, MIGRATIONS_PATH)
- c.execute("SELECT machine_id FROM settings LIMIT 1")
- result = c.fetchone()
+ save_settings()
- if result:
- machine_id = result[0]
- else:
- machine_id = str(uuid.uuid1())
- c.execute("INSERT INTO settings (machine_id) VALUES (?)", (machine_id,))
- conn.commit()
+ if args.website:
+ print('Please visit this url for detailed analytics:')
+ print(f"{SETTINGS['web_url']}{machine_id}")
+ sys.exit()
- run_powermetrics(args.debug)
+ run_powermetrics(args.debug, args.file)
c.close()
diff --git a/settings.ini b/settings.ini
index 0bdde86..6829ba0 100644
--- a/settings.ini
+++ b/settings.ini
@@ -1,5 +1,6 @@
[DEFAULT]
api_url = https://api.green-coding.berlin/v1/hog/add
+web_url = http://metrics.green-coding.berlin/hog-details.html?machine_id=
upload_delta = 300
powermetrics = 5000
upload_data = true
\ No newline at end of file