diff --git a/.github/macmon.png b/.github/macmon.png
new file mode 100644
index 0000000..ea22f92
Binary files /dev/null and b/.github/macmon.png differ
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..482707a
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,25 @@
+name: check
+
+on:
+ push:
+ branches:
+ - '**'
+
+jobs:
+ check:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/cache@v4
+ with:
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ target/
+
+ - run: rustup update --no-self-update stable && rustup default stable
+ - run: cargo fmt --check
+ - run: cargo build --release --locked
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..3c7b779
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,51 @@
+name: release
+
+on:
+ push:
+ tags: 'v*'
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/cache@v4
+ with:
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ target/
+
+ - run: rustup update --no-self-update stable && rustup default stable
+ - run: cargo fmt --check
+ - run: cargo build --release --locked
+ - name: archiving
+ id: archive
+ run: |
+ strip target/release/macmon
+ cp target/release/macmon macmon
+ tar czf macmon-${{ github.ref_name }}.tar.gz readme.md LICENSE macmon
+ ls -lah | grep macmon
+
+ - uses: softprops/action-gh-release@v2
+ with:
+ files: macmon-${{ github.ref_name }}.tar.gz
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: mislav/bump-homebrew-formula-action@v3
+ with:
+ homebrew-tap: vladkens/homebrew-apps
+ formula-name: macmon
+ formula-path: macmon.rb
+ commit-message: "{{formulaName}} {{version}}"
+ download-url: https://github.com/vladkens/macmon/releases/download/${{ github.ref_name }}/macmon-${{ github.ref_name }}.tar.gz
+ env:
+ COMMITTER_TOKEN: ${{ secrets.HOMEBREW_REPO_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d8f63cd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+/target
diff --git a/.rustfmt.toml b/.rustfmt.toml
new file mode 100644
index 0000000..45b28b5
--- /dev/null
+++ b/.rustfmt.toml
@@ -0,0 +1,5 @@
+# https://rust-lang.github.io/rustfmt/?version=v1.6.0&search=
+edition = "2021"
+max_width = 100
+use_small_heuristics = "Max"
+tab_spaces = 2
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3fbb61e
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "[rust]": {
+ "editor.defaultFormatter": "rust-lang.rust-analyzer",
+ "editor.formatOnSave": true,
+ },
+ "terminal.integrated.tabStopWidth": 2,
+ "editor.formatOnSave": true,
+ "code-runner.executorMap": {
+ "rust": "cargo run -r #$fileName",
+ },
+ "files.exclude": { "Cargo.lock": true },
+}
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..2b5748f
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,750 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[package]]
+name = "compact_str"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "crossterm"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "either"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "lru"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "macmon"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "core-foundation",
+ "crossterm",
+ "libc",
+ "ratatui",
+ "serde_json",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ratatui"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "compact_str",
+ "crossterm",
+ "itertools",
+ "lru",
+ "paste",
+ "stability",
+ "strum",
+ "unicode-segmentation",
+ "unicode-truncate",
+ "unicode-width",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "stability"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+
+[[package]]
+name = "unicode-truncate"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226"
+dependencies = [
+ "itertools",
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..6713af5
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "macmon"
+version = "0.1.0"
+edition = "2021"
+
+[lints.rust]
+non_camel_case_types = "allow"
+unused_assignments = "allow"
+
+[profile.release]
+panic = "abort"
+
+[dependencies]
+clap = {version = "4.5.7", features = ["derive"]}
+core-foundation = "0.9.4"
+crossterm = "0.27.0"
+libc = "0.2.155"
+ratatui = "0.26.3"
+serde_json = "1.0.117"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4659b63
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 vladkens
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..4cdabea
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,78 @@
+# macmon
+
+
+
+
+ Sudoless performance monitoring CLI tool for Apple Silicon processors.
+
+
+## Motivation
+
+Apple Silicon processors don't provide an easy way to see live power consumption. I was interested in this information while testing local LLM models. `asitop` is a nice and simple TUI to quickly see current metrics, but it reads data from `powermetrics` and requires root privileges. `macmon` uses a private macOS API to gather metrics (essentially the same as `powermetrics`) but runs without sudo. 🎉
+
+## 🌟 Features
+
+- 🚫 Works without sudo
+- ⚡ Real-time CPU / GPU / ANE power usage
+- 📊 CPU utilization per cluster
+- 💾 RAM / Swap usage
+- 📈 Historical charts + avg / max values
+- 🪟 Can be rendered in a small window
+- 🦀 Written in Rust
+
+## 🍺 Install via Homebrew
+
+```sh
+brew install vladkens/tap/macmon
+```
+
+## 📦 Install from source
+
+1. Install [Rust toolchain](https://www.rust-lang.org/tools/install)
+
+2. Clone the repo:
+
+```sh
+git clone https://github.com/vladkens/macmon.git && cd macmon
+```
+
+3. Build and run:
+
+```sh
+cargo run -r
+```
+
+4. (Optionally) Binary can be moved to bin folder:
+
+```sh
+sudo cp target/release/macmon /usr/local/bin
+```
+
+## 🚀 Usage
+
+```sh
+Usage: macmon [OPTIONS]
+
+Options:
+ -i, --interval Update interval in milliseconds [default: 1000]
+ --raw Print raw data instead of TUI
+ -h, --help Print help
+ -V, --version Print version
+```
+
+## 🤝 Contributing
+We love contributions! Whether you have ideas, suggestions, or bug reports, feel free to open an issue or submit a pull request. Your input is essential in helping us improve `macmon` 💪
+
+## 📝 License
+`macmon` is distributed under the MIT License. For more details, check out the LICENSE.
+
+## 🔍 See also
+- [tlkh/asitop](https://github.com/tlkh/asitop) – Original tool. Python, requires sudo.
+- [dehydratedpotato/socpowerbud](https://github.com/dehydratedpotato/socpowerbud) – ObjectiveC, sudoless, no TUI.
+- [op06072/NeoAsitop](https://github.com/op06072/NeoAsitop) – Swift, sudoless.
+- [graelo/pumas](https://github.com/graelo/pumas) – Rust, requires sudo.
+- [context-labs/mactop](https://github.com/context-labs/mactop) – Go, requires sudo.
+
+---
+
+*PS: One More Thing... Remember, monitoring your Mac's performance with `macmon` is like having a personal trainer for your processor — keeping those cores in shape! 💪*
diff --git a/src/app.rs b/src/app.rs
new file mode 100644
index 0000000..3122819
--- /dev/null
+++ b/src/app.rs
@@ -0,0 +1,332 @@
+use crate::metrics::{self, MemoryUsage, PerfSample, SocInfo};
+use crossterm::{
+ event::{self, KeyCode, KeyModifiers},
+ terminal, ExecutableCommand,
+};
+use ratatui::{prelude::*, widgets::*};
+use std::{error::Error, io::stdout, time::Instant};
+use std::{sync::mpsc, time::Duration};
+
+const GB: u64 = 1024 * 1024 * 1024;
+const MAX_SPARKLINE: usize = 128;
+const BASE_COLOR: Color = Color::LightGreen;
+
+fn enter_term() -> Terminal {
+ std::panic::set_hook(Box::new(|info| {
+ leave_term();
+ eprintln!("{}", info);
+ }));
+
+ terminal::enable_raw_mode().unwrap();
+ stdout().execute(terminal::EnterAlternateScreen).unwrap();
+
+ let term = CrosstermBackend::new(std::io::stdout());
+ let term = Terminal::new(term).unwrap();
+ term
+}
+
+fn leave_term() {
+ terminal::disable_raw_mode().unwrap();
+ stdout().execute(terminal::LeaveAlternateScreen).unwrap();
+}
+
+// Metrics
+
+fn items_add(vec: &mut Vec, val: u64) -> &Vec {
+ vec.insert(0, val);
+ if vec.len() > MAX_SPARKLINE {
+ vec.pop();
+ }
+ vec
+}
+
+#[derive(Debug, Default)]
+struct FreqStore {
+ items: Vec,
+ top_value: u64,
+ usage: u8,
+}
+
+impl FreqStore {
+ fn push(&mut self, value: u64, usage: u8) {
+ // items_add(&mut self.items, value);
+ items_add(&mut self.items, usage as u64);
+ self.top_value = value;
+ self.usage = usage;
+ }
+}
+
+#[derive(Debug, Default)]
+struct PowerStore {
+ items: Vec,
+ top_value: f64,
+ max_value: f64,
+ avg_value: f64,
+}
+
+impl PowerStore {
+ fn push(&mut self, value: f64) {
+ items_add(&mut self.items, (value * 1000.0) as u64);
+ self.top_value = value;
+ self.avg_value = self.items.iter().sum::() as f64 / self.items.len() as f64 / 1000.0;
+ self.max_value = self.items.iter().max().map_or(0, |v| *v) as f64 / 1000.0;
+ }
+}
+
+#[derive(Debug, Default)]
+struct MemoryStore {
+ items: Vec,
+ ram_usage: u64,
+ ram_total: u64,
+ swap_usage: u64,
+ swap_total: u64,
+ max_ram: u64,
+}
+
+impl MemoryStore {
+ fn push(&mut self, value: MemoryUsage) {
+ items_add(&mut self.items, value.ram_usage);
+ self.ram_usage = value.ram_usage;
+ self.ram_total = value.ram_total;
+ self.swap_usage = value.swap_usage;
+ self.swap_total = value.swap_total;
+ self.max_ram = self.items.iter().max().map_or(0, |v| *v);
+ }
+}
+
+// Rendering
+
+fn h_stack(area: Rect) -> (Rect, Rect) {
+ let ha = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([Constraint::Fill(1), Constraint::Fill(1)].as_ref())
+ .split(area);
+
+ (ha[0], ha[1])
+}
+
+fn title_block<'a>(label_l: &str, label_r: &str) -> Block<'a> {
+ let mut block = Block::new()
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded)
+ .border_style(BASE_COLOR)
+ // .title_style(Style::default().gray())
+ .padding(Padding::horizontal(0));
+
+ if label_l.len() > 0 {
+ block = block.title(block::Title::from(format!(" {label_l} ")).alignment(Alignment::Left));
+ }
+
+ if label_r.len() > 0 {
+ block = block.title(block::Title::from(format!(" {label_r} ")).alignment(Alignment::Right));
+ }
+
+ block
+}
+
+fn get_freq_block<'a>(label: &str, val: &'a FreqStore) -> Sparkline<'a> {
+ let label = format!("{} {:3}% @ {:4.0} MHz", label, val.usage, val.top_value);
+ Sparkline::default()
+ .block(title_block(label.as_str(), ""))
+ .direction(RenderDirection::RightToLeft)
+ .data(&val.items)
+ .max(100)
+ .style(BASE_COLOR)
+}
+
+fn get_power_block<'a>(label: &str, val: &'a PowerStore) -> Sparkline<'a> {
+ let label = format!(
+ // "{} {:.2}W (avg: {:.2}W, max: {:.2}W)",
+ "{} {:.2}W (~{:.2}W ^{:.2}W)",
+ // "{} {:.2}W (~{:.2}W ↑{:.2}W)",
+ label,
+ val.top_value,
+ val.avg_value,
+ val.max_value
+ );
+
+ Sparkline::default()
+ .block(title_block(label.as_str(), ""))
+ .direction(RenderDirection::RightToLeft)
+ .data(&val.items)
+ .style(BASE_COLOR)
+}
+
+fn get_ram_block<'a>(val: &'a MemoryStore) -> Sparkline<'a> {
+ let ram_usage_gb = val.ram_usage as f64 / GB as f64;
+ let ram_total_gb = val.ram_total as f64 / GB as f64;
+
+ let swap_usage_gb = val.swap_usage as f64 / GB as f64;
+ let swap_total_gb = val.swap_total as f64 / GB as f64;
+
+ let label_l = format!("RAM {:4.2} / {:4.1} GB", ram_usage_gb, ram_total_gb);
+ let label_r = format!("SWAP {:.2} / {:.1} GB", swap_usage_gb, swap_total_gb);
+
+ Sparkline::default()
+ .block(title_block(label_l.as_str(), label_r.as_str()))
+ .direction(RenderDirection::RightToLeft)
+ .data(&val.items)
+ .max(val.ram_total)
+ .style(BASE_COLOR)
+}
+
+// App
+
+enum Event {
+ Update(PerfSample),
+ Quit,
+ Tick,
+}
+
+fn run_input_loop(tx: mpsc::Sender, tick: u64) {
+ let tick_rate = Duration::from_millis(tick);
+
+ std::thread::spawn(move || {
+ let mut last_tick = Instant::now();
+
+ loop {
+ if event::poll(Duration::from_millis(100)).unwrap() {
+ match event::read().unwrap() {
+ event::Event::Key(key) => {
+ if key.code == KeyCode::Char('q')
+ || (key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL)
+ {
+ tx.send(Event::Quit).unwrap();
+ }
+ }
+ _ => {}
+ };
+ }
+ if last_tick.elapsed() >= tick_rate {
+ tx.send(Event::Tick).unwrap();
+ last_tick = Instant::now();
+ }
+ }
+ });
+}
+
+fn run_perfs_loop(tx: mpsc::Sender, info: SocInfo, cycle_time: u64) {
+ let interval = cycle_time.max(100);
+ let check_ts = 100;
+
+ std::thread::spawn(move || {
+ let mut sub = metrics::SubsChan::new(info).unwrap();
+
+ loop {
+ let data = sub.sample(check_ts).unwrap();
+ tx.send(Event::Update(data)).unwrap();
+ std::thread::sleep(Duration::from_millis(interval - check_ts));
+ }
+ });
+}
+
+#[derive(Debug, Default)]
+pub struct App {
+ info: metrics::SocInfo,
+ ecpu_freq: FreqStore,
+ pcpu_freq: FreqStore,
+ gpu_freq: FreqStore,
+ cpu_power: PowerStore,
+ gpu_power: PowerStore,
+ ane_power: PowerStore,
+ all_power: PowerStore,
+ memory: MemoryStore,
+}
+
+impl App {
+ pub fn new(info: metrics::SocInfo) -> Self {
+ let mut app = App::default();
+ app.info = info;
+ app
+ }
+
+ fn update_metrics(&mut self, data: PerfSample) {
+ self.cpu_power.push(data.cpu_power as f64);
+ self.gpu_power.push(data.gpu_power as f64);
+ self.ane_power.push(data.ane_power as f64);
+ self.all_power.push(data.all_watts as f64);
+ self.ecpu_freq.push(data.ecpu_usage.0 as u64, (data.ecpu_usage.1 * 100.0) as u8);
+ self.pcpu_freq.push(data.pcpu_usage.0 as u64, (data.pcpu_usage.1 * 100.0) as u8);
+ self.gpu_freq.push(data.gpu_usage.0 as u64, (data.gpu_usage.1 * 100.0) as u8);
+ self.memory.push(data.memory);
+ }
+
+ fn render(&self, f: &mut Frame) {
+ let label_l = format!(
+ "{} ({}E+{}P+{}GPU {}GB)",
+ self.info.chip_name,
+ self.info.ecpu_cores,
+ self.info.pcpu_cores,
+ self.info.gpu_cores,
+ self.info.memory_gb,
+ );
+
+ let label_r = format!(
+ "Power: {:.2}W (avg: {:.2}W, max: {:.2}W)",
+ self.all_power.top_value, self.all_power.avg_value, self.all_power.max_value
+ );
+
+ // let p = LineGauge::default().label(format!("─ {label_l}"));
+ // f.render_widget(p, f.size());
+ // return;
+
+ let rows = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([Constraint::Fill(2), Constraint::Fill(1)].as_ref())
+ .split(f.size());
+
+ let block = title_block(&label_l, "");
+ let iarea = block.inner(rows[0]);
+ f.render_widget(block, rows[0]);
+
+ let iarea = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([Constraint::Fill(1), Constraint::Fill(1)].as_ref())
+ .split(iarea);
+
+ // 1st row
+ let (c1, c2) = h_stack(iarea[0]);
+ f.render_widget(get_freq_block("E-CPU", &self.ecpu_freq), c1);
+ f.render_widget(get_freq_block("P-CPU", &self.pcpu_freq), c2);
+
+ // 2nd row
+ let (c1, c2) = h_stack(iarea[1]);
+ f.render_widget(get_ram_block(&self.memory), c1);
+ f.render_widget(get_freq_block("GPU", &self.gpu_freq), c2);
+
+ // 3rd row
+ let block = title_block(&label_r, "");
+ let iarea = block.inner(rows[1]);
+ f.render_widget(block, rows[1]);
+
+ let ha = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([Constraint::Fill(1), Constraint::Fill(1), Constraint::Fill(1)].as_ref())
+ .split(iarea);
+
+ f.render_widget(get_power_block("CPU", &self.cpu_power), ha[0]);
+ f.render_widget(get_power_block("GPU", &self.gpu_power), ha[1]);
+ f.render_widget(get_power_block("ANE", &self.ane_power), ha[2]);
+ }
+
+ pub fn run_loop(&mut self, interval: u64) -> Result<(), Box> {
+ let (tx, rx) = mpsc::channel::();
+ run_input_loop(tx.clone(), 200);
+ run_perfs_loop(tx.clone(), self.info.clone(), interval);
+
+ let mut term = enter_term();
+
+ loop {
+ term.draw(|f| self.render(f)).unwrap();
+
+ match rx.recv()? {
+ Event::Update(data) => self.update_metrics(data),
+ Event::Quit => break,
+ _ => {}
+ }
+ }
+
+ leave_term();
+ Ok(())
+ }
+}
diff --git a/src/cfutil.rs b/src/cfutil.rs
new file mode 100644
index 0000000..2453793
--- /dev/null
+++ b/src/cfutil.rs
@@ -0,0 +1,124 @@
+use core_foundation::{
+ base::{kCFAllocatorDefault, kCFAllocatorNull, CFRelease, CFTypeRef},
+ dictionary::{
+ CFDictionaryGetCount, CFDictionaryGetKeysAndValues, CFDictionaryGetValue, CFDictionaryRef,
+ },
+ string::{kCFStringEncodingUTF8, CFStringCreateWithBytesNoCopy, CFStringGetCString, CFStringRef},
+};
+
+pub type CVoidRef = *const std::ffi::c_void;
+
+// pub fn json_save(filepath: &str, val: &Value) {
+// use std::fs::File;
+// use std::io::prelude::*;
+// let val = serde_json::to_string_pretty(&val).unwrap();
+// let mut file = File::create(filepath).unwrap();
+// file.write_all(val.as_bytes()).unwrap();
+// }
+
+// pub fn cf_to_json(val: CFTypeRef) -> Value {
+// unsafe {
+// let tid = CFGetTypeID(val);
+// match tid {
+// _ if tid == CFStringGetTypeID() => {
+// json!(CFString::wrap_under_get_rule(val as CFStringRef).to_string())
+// }
+// _ if tid == CFNumberGetTypeID() => {
+// json!(CFNumber::wrap_under_get_rule(val as CFNumberRef).to_i64())
+// }
+// _ if tid == CFBooleanGetTypeID() => {
+// json!(CFBooleanGetValue(val as CFBooleanRef))
+// }
+// _ if tid == CFDictionaryGetTypeID() => {
+// let val = CFDictionary::::wrap_under_get_rule(val as CFDictionaryRef);
+// let (keys, vals) = val.get_keys_and_values();
+
+// let mut map: HashMap = HashMap::new();
+// for (key, value) in keys.iter().zip(vals.iter()) {
+// map.insert(
+// CFString::wrap_under_get_rule(*key as CFStringRef).to_string(),
+// cf_to_json(*value),
+// );
+// }
+// json!(map)
+// }
+// _ if tid == CFArrayGetTypeID() => {
+// let val = CFArray::::wrap_under_get_rule(val as CFArrayRef);
+// let mut arr: Vec = Vec::new();
+// for x in val.iter() {
+// arr.push(cf_to_json(*x));
+// }
+// json!(arr)
+// }
+// _ if tid == CFDataGetTypeID() => {
+// let val = CFData::wrap_under_get_rule(val as CFDataRef);
+// json!(val.bytes().to_vec())
+// }
+// _ => {
+// eprintln!("UNKNOWN_TYPE: {:?}", tid);
+// CFShow(val);
+// json!(null)
+// }
+// }
+// }
+// }
+
+pub fn cfstr(val: &str) -> CFStringRef {
+ // this creates broken objects if string len > 9
+ // CFString::from_static_string(val).as_concrete_TypeRef()
+ // CFString::new(val).as_concrete_TypeRef()
+
+ unsafe {
+ CFStringCreateWithBytesNoCopy(
+ kCFAllocatorDefault,
+ val.as_ptr(),
+ val.len() as isize,
+ kCFStringEncodingUTF8,
+ 0,
+ kCFAllocatorNull,
+ )
+ }
+}
+
+pub fn from_cfstr(val: CFStringRef) -> String {
+ unsafe {
+ let mut buf = Vec::with_capacity(128);
+ if CFStringGetCString(val, buf.as_mut_ptr(), 128, kCFStringEncodingUTF8) == 0 {
+ panic!("Failed to convert CFString to CString");
+ }
+ std::ffi::CStr::from_ptr(buf.as_ptr()).to_string_lossy().to_string()
+ }
+}
+
+pub fn cfdict_keys(dict: CFDictionaryRef) -> Vec {
+ unsafe {
+ let count = CFDictionaryGetCount(dict) as usize;
+ let mut keys: Vec = Vec::with_capacity(count);
+ let mut vals: Vec = Vec::with_capacity(count);
+ CFDictionaryGetKeysAndValues(dict, keys.as_mut_ptr() as _, vals.as_mut_ptr());
+ keys.set_len(count);
+ vals.set_len(count);
+
+ keys.iter().map(|k| from_cfstr(*k as _)).collect()
+ }
+}
+
+pub fn cfdict_get_val(dict: CFDictionaryRef, key: &str) -> Option {
+ unsafe {
+ let key = cfstr(key);
+ let val = CFDictionaryGetValue(dict, key as _);
+ CFRelease(key as _);
+
+ match val {
+ _ if val.is_null() => None,
+ _ => Some(val),
+ }
+ }
+}
+
+pub fn cfdict_get_str(dict: CFDictionaryRef, key: &str) -> Option {
+ match cfdict_get_val(dict, key) {
+ Some(val) => Some(from_cfstr(val as _)),
+ None => None,
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..c2ef258
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,38 @@
+pub mod app;
+pub mod cfutil;
+pub mod metrics;
+
+use clap::Parser;
+use std::error::Error;
+
+/// Sudoless performance monitoring CLI tool for Apple Silicon processors
+/// https://github.com/vladkens/macmon
+#[derive(Debug, Parser)]
+#[command(version, verbatim_doc_comment)]
+struct Cli {
+ /// Update interval in milliseconds
+ #[arg(short, long, default_value_t = 1000)]
+ interval: u64,
+
+ /// Print raw data instead of TUI
+ #[arg(long, default_value_t = false)]
+ raw: bool,
+}
+
+fn main() -> Result<(), Box> {
+ let args = Cli::parse();
+ let info = metrics::initialize().unwrap();
+
+ if args.raw {
+ let mut subs = metrics::SubsChan::new(info)?;
+ loop {
+ let data = subs.sample(args.interval)?;
+ println!("{:?}", data);
+ }
+ } else {
+ let mut app = app::App::new(info);
+ app.run_loop(args.interval).unwrap();
+ }
+
+ Ok(())
+}
diff --git a/src/metrics.rs b/src/metrics.rs
new file mode 100644
index 0000000..3cf6f4d
--- /dev/null
+++ b/src/metrics.rs
@@ -0,0 +1,492 @@
+use crate::cfutil::*;
+use core_foundation::{
+ array::{CFArrayGetCount, CFArrayGetValueAtIndex, CFArrayRef},
+ base::{kCFAllocatorDefault, CFAllocatorRef, CFRange, CFRelease, CFTypeRef},
+ data::{CFDataGetBytes, CFDataGetLength, CFDataRef},
+ dictionary::{
+ CFDictionaryCreateMutableCopy, CFDictionaryGetCount, CFDictionaryRef, CFMutableDictionaryRef,
+ },
+ string::CFStringRef,
+};
+use std::marker::{PhantomData, PhantomPinned};
+use std::mem::MaybeUninit;
+use std::ptr::null;
+
+type WithError = Result>;
+
+const CPU_POWER_SUBG: &str = "CPU Complex Performance States";
+const GPU_POWER_SUBG: &str = "GPU Performance States";
+
+#[rustfmt::skip]
+#[link(name = "IOKit", kind = "framework")]
+extern "C" {
+ fn IOServiceMatching(name: *const i8) -> CFMutableDictionaryRef;
+ fn IOServiceGetMatchingServices(mainPort: u32, matching: CFDictionaryRef, existing: *mut u32) -> i32;
+ fn IOIteratorNext(iterator: u32) -> u32;
+ fn IORegistryEntryGetName(entry: u32, name: *mut i8) -> i32;
+ fn IORegistryEntryCreateCFProperties(entry: u32, properties: *mut CFMutableDictionaryRef, allocator: CFAllocatorRef, options: u32) -> i32;
+ fn IOObjectRelease(obj: u32) -> u32;
+}
+
+#[repr(C)]
+struct IOReportSubscription {
+ _data: [u8; 0],
+ _phantom: PhantomData<(*mut u8, PhantomPinned)>,
+}
+
+type IOReportSubscriptionRef = *const IOReportSubscription;
+
+#[rustfmt::skip]
+#[link(name = "IOReport", kind = "dylib")]
+extern "C" {
+ fn IOReportCopyAllChannels(a: u64, b: u64) -> CFDictionaryRef;
+ fn IOReportCopyChannelsInGroup(a: CFStringRef, b: CFStringRef, c: u64, d: u64, e: u64) -> CFDictionaryRef;
+ fn IOReportMergeChannels(a: CFDictionaryRef, b: CFDictionaryRef, nil: CFTypeRef);
+ fn IOReportCreateSubscription(a: CVoidRef, b: CFMutableDictionaryRef, c: *mut CFMutableDictionaryRef, d: u64, b: CFTypeRef) -> IOReportSubscriptionRef;
+ fn IOReportCreateSamples(a: IOReportSubscriptionRef, b: CFMutableDictionaryRef, c: CFTypeRef) -> CFDictionaryRef;
+ fn IOReportCreateSamplesDelta(a: CFDictionaryRef, b: CFDictionaryRef, c: CFTypeRef) -> CFDictionaryRef;
+ fn IOReportChannelGetChannelName(a: CFDictionaryRef) -> CFStringRef;
+ fn IOReportSimpleGetIntegerValue(a: CFDictionaryRef, b: i32) -> i64;
+ fn IOReportChannelGetUnitLabel(a: CFDictionaryRef) -> CFStringRef;
+ fn IOReportStateGetCount(a: CFDictionaryRef) -> i32;
+ fn IOReportStateGetNameForIndex(a: CFDictionaryRef, b: i32) -> CFStringRef;
+ fn IOReportStateGetResidency(a: CFDictionaryRef, b: i32) -> i64;
+}
+
+fn cfio_iter_next(existing: u32) -> Option<(u32, String)> {
+ unsafe {
+ let el = IOIteratorNext(existing);
+ if el == 0 {
+ return None;
+ }
+
+ let mut name = [0; 128]; // 128 defined in apple docs
+ if IORegistryEntryGetName(el, name.as_mut_ptr()) != 0 {
+ return None;
+ }
+
+ let name = std::ffi::CStr::from_ptr(name.as_ptr()).to_string_lossy().to_string();
+ Some((el, name))
+ }
+}
+
+fn cfio_get_props(entry: u32, name: String) -> WithError {
+ unsafe {
+ let mut props: MaybeUninit = MaybeUninit::uninit();
+ if IORegistryEntryCreateCFProperties(entry, props.as_mut_ptr(), kCFAllocatorDefault, 0) != 0 {
+ return Err(format!("Failed to get properties for {}", name).into());
+ }
+
+ Ok(props.assume_init())
+ }
+}
+
+// dynamic voltage and frequency scaling
+fn get_dvfs_mhz(dict: CFDictionaryRef, key: &str) -> (Vec, Vec) {
+ unsafe {
+ let obj = cfdict_get_val(dict, key).unwrap() as CFDataRef;
+ let obj_len = CFDataGetLength(obj);
+ let obj_val = vec![0u8; obj_len as usize];
+ CFDataGetBytes(obj, CFRange::init(0, obj_len), obj_val.as_ptr() as *mut u8);
+
+ // obj_val is pairs of (freq, voltage) 4 bytes each
+ let items_count = (obj_len / 8) as usize;
+ let [mut freqs, mut volts] = [vec![0u32; items_count], vec![0u32; items_count]];
+ for (i, x) in obj_val.chunks_exact(8).enumerate() {
+ volts[i] = u32::from_le_bytes([x[4], x[5], x[6], x[7]]);
+ freqs[i] = u32::from_le_bytes([x[0], x[1], x[2], x[3]]);
+ freqs[i] = freqs[i] / 1000 / 1000; // as MHz
+ }
+
+ (volts, freqs)
+ }
+}
+
+// General info
+
+#[derive(Debug, Default, Clone)]
+pub struct SocInfo {
+ pub chip_name: String,
+ pub memory_gb: u8,
+ pub ecpu_cores: u8,
+ pub pcpu_cores: u8,
+ pub gpu_cores: u8,
+ pub ecpu_freqs: Vec,
+ pub pcpu_freqs: Vec,
+ pub gpu_freqs: Vec,
+}
+
+fn fill_basic_info(info: &mut SocInfo) -> WithError<()> {
+ // system_profiler -listDataTypes
+ let out = std::process::Command::new("system_profiler")
+ .args(&["SPHardwareDataType", "SPDisplaysDataType", "-json"])
+ .output()
+ .unwrap();
+
+ let out = std::str::from_utf8(&out.stdout).unwrap();
+ let out = serde_json::from_str::(out).unwrap();
+
+ // SPHardwareDataType.0.chip_type
+ let chip_name = out["SPHardwareDataType"][0]["chip_type"].as_str().unwrap().to_string();
+
+ // SPHardwareDataType.0.physical_memory -> "x GB"
+ let mem_gb = out["SPHardwareDataType"][0]["physical_memory"].as_str();
+ let mem_gb = mem_gb.expect("No memory found").strip_suffix(" GB").unwrap();
+ let mem_gb = mem_gb.parse::().unwrap();
+
+ // SPHardwareDataType.0.number_processors -> "proc x:y:z"
+ let cpu_cores = out["SPHardwareDataType"][0]["number_processors"].as_str();
+ let cpu_cores = cpu_cores.expect("No CPU cores found").strip_prefix("proc ").unwrap();
+ let cpu_cores = cpu_cores.split(':').map(|x| x.parse::().unwrap()).collect::>();
+ assert_eq!(cpu_cores.len(), 3, "Invalid number of CPU cores");
+ let (ecpu_cores, pcpu_cores, _) = (cpu_cores[2], cpu_cores[1], cpu_cores[0]);
+
+ let gpu_cores = match out["SPDisplaysDataType"][0]["sppci_cores"].as_str() {
+ Some(x) => x.parse::().unwrap(),
+ None => 0,
+ };
+
+ info.chip_name = chip_name;
+ info.memory_gb = mem_gb as u8;
+ info.gpu_cores = gpu_cores as u8;
+ info.ecpu_cores = ecpu_cores as u8;
+ info.pcpu_cores = pcpu_cores as u8;
+
+ Ok(())
+}
+
+fn fill_cores_info(info: &mut SocInfo) -> WithError<()> {
+ unsafe {
+ let service_name = std::ffi::CString::new("AppleARMIODevice").unwrap();
+ let service = IOServiceMatching(service_name.as_ptr() as *const i8);
+
+ let mut existing = 0;
+ if IOServiceGetMatchingServices(0, service, &mut existing) != 0 {
+ return Err("AppleARMIODevice not found".into());
+ }
+
+ // println!("Found {} services", existing);
+ while let Some(obj) = cfio_iter_next(existing) {
+ let (entry, name) = obj;
+ // println!("Found service ({:?}): {}", entry, name);
+
+ if name == "pmgr" {
+ let item = cfio_get_props(entry, name)?;
+ // let keys = cfdict_keys(item);
+ // println!("Keys: {:?}", keys);
+ // CFShow(item as _);
+
+ info.ecpu_freqs = get_dvfs_mhz(item, "voltage-states1-sram").1;
+ info.pcpu_freqs = get_dvfs_mhz(item, "voltage-states5-sram").1;
+ info.gpu_freqs = get_dvfs_mhz(item, "voltage-states9").1;
+
+ CFRelease(item as _);
+ }
+ }
+
+ IOObjectRelease(existing);
+ }
+
+ if info.ecpu_freqs.len() == 0 || info.pcpu_freqs.len() == 0 {
+ return Err("No CPU cores found".into());
+ }
+
+ Ok(())
+}
+
+pub fn initialize() -> WithError {
+ let mut info = SocInfo::default();
+ fill_basic_info(&mut info)?;
+ fill_cores_info(&mut info)?;
+ Ok(info)
+}
+
+// Memory
+
+fn libc_ram_info() -> WithError<(u64, u64)> {
+ let (mut usage, mut total) = (0u64, 0u64);
+
+ unsafe {
+ let mut name = [libc::CTL_HW, libc::HW_MEMSIZE];
+ let mut size = std::mem::size_of::();
+ let ret_code = libc::sysctl(
+ name.as_mut_ptr(),
+ name.len() as _,
+ &mut total as *mut _ as *mut _,
+ &mut size,
+ std::ptr::null_mut(),
+ 0,
+ );
+
+ if ret_code != 0 {
+ return Err("Failed to get total memory".into());
+ }
+ }
+
+ unsafe {
+ let mut count: u32 = libc::HOST_VM_INFO64_COUNT as _;
+ let mut stats = std::mem::zeroed::();
+
+ let ret_code = libc::host_statistics64(
+ libc::mach_host_self(),
+ libc::HOST_VM_INFO64,
+ &mut stats as *mut _ as *mut _,
+ &mut count,
+ );
+
+ if ret_code != 0 {
+ return Err("Failed to get memory stats".into());
+ }
+
+ let page_size_kb = libc::sysconf(libc::_SC_PAGESIZE) as u64;
+
+ usage = (0
+ + stats.active_count as u64
+ + stats.inactive_count as u64
+ + stats.wire_count as u64
+ + stats.speculative_count as u64
+ + stats.compressor_page_count as u64
+ - stats.purgeable_count as u64
+ - stats.external_page_count as u64
+ + 0)
+ * page_size_kb;
+ }
+
+ Ok((usage, total))
+}
+
+fn libc_swap_info() -> WithError<(u64, u64)> {
+ let (mut usage, mut total) = (0u64, 0u64);
+
+ unsafe {
+ let mut name = [libc::CTL_VM, libc::VM_SWAPUSAGE];
+ let mut size = std::mem::size_of::();
+ let mut xsw: libc::xsw_usage = std::mem::zeroed::();
+
+ let ret_code = libc::sysctl(
+ name.as_mut_ptr(),
+ name.len() as _,
+ &mut xsw as *mut _ as *mut _,
+ &mut size,
+ std::ptr::null_mut(),
+ 0,
+ );
+
+ if ret_code != 0 {
+ return Err("Failed to get swap usage".into());
+ }
+
+ usage = xsw.xsu_used;
+ total = xsw.xsu_total;
+ }
+
+ Ok((usage, total))
+}
+
+// Metrics collector
+
+unsafe fn cfio_get_chan(items: Vec<(&str, Option<&str>)>) -> WithError {
+ // if no items are provided, return all channels
+ if items.len() == 0 {
+ let c = IOReportCopyAllChannels(0, 0);
+ let r = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, CFDictionaryGetCount(c), c);
+ CFRelease(c as _);
+ return Ok(r);
+ }
+
+ let mut channels = vec![];
+ for (group, subgroup) in items {
+ let gname = cfstr(group);
+ let sname = subgroup.map_or(null(), |x| cfstr(x));
+ let chan = IOReportCopyChannelsInGroup(gname, sname, 0, 0, 0);
+ channels.push(chan);
+
+ CFRelease(gname as _);
+ if subgroup.is_some() {
+ CFRelease(sname as _);
+ }
+ }
+
+ let chan = channels[0];
+ for i in 1..channels.len() {
+ IOReportMergeChannels(chan, channels[i], null());
+ }
+
+ let size = CFDictionaryGetCount(chan);
+ let chan = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, chan);
+
+ for i in 0..channels.len() {
+ CFRelease(channels[i] as _);
+ }
+
+ if cfdict_get_val(chan, "IOReportChannels").is_none() {
+ return Err("Failed to get channels".into());
+ }
+
+ Ok(chan)
+}
+
+unsafe fn cfio_get_subs(chan: CFMutableDictionaryRef) -> WithError {
+ let mut s: MaybeUninit = MaybeUninit::uninit();
+ let rs = IOReportCreateSubscription(std::ptr::null(), chan, s.as_mut_ptr(), 0, std::ptr::null());
+ if rs == std::ptr::null() {
+ return Err("Failed to create subscription".into());
+ }
+
+ s.assume_init();
+ Ok(rs)
+}
+
+#[derive(Debug, Default)]
+pub struct MemoryUsage {
+ pub ram_total: u64, // bytes
+ pub ram_usage: u64, // bytes
+ pub swap_total: u64, // bytes
+ pub swap_usage: u64, // bytes
+}
+
+#[derive(Debug, Default)]
+pub struct PerfSample {
+ pub ecpu_usage: (u32, f32), // freq, percent_from_max
+ pub pcpu_usage: (u32, f32), // freq, percent_from_max
+ pub gpu_usage: (u32, f32), // freq, percent_from_max
+ pub all_watts: f32, // W
+ pub cpu_power: f32, // W
+ pub gpu_power: f32, // W
+ pub ane_power: f32, // W
+ pub memory: MemoryUsage,
+}
+
+unsafe fn calc_freq(item: CFDictionaryRef, freqs: &Vec) -> (u32, f32) {
+ let count = IOReportStateGetCount(item) as usize;
+ assert!(count > freqs.len(), "Invalid freqs count"); // todo?
+
+ let mut residencies = vec![0; count];
+ for i in 0..count as i32 {
+ let val = IOReportStateGetResidency(item, i);
+ residencies[i as usize] = val as u64;
+
+ let _key = from_cfstr(IOReportStateGetNameForIndex(item, i));
+ // println!("{} {}: {}", i, _key, val)
+ }
+
+ let count = freqs.len();
+ let total = residencies.iter().sum::();
+ let usage = residencies.iter().skip(1).sum::(); // first is IDLE for CPU and OFF for GPU
+
+ let mut freq = 0f64;
+ for i in 0..count {
+ let percent = match usage {
+ 0 => 0.0,
+ _ => residencies[i + 1] as f64 / usage as f64,
+ };
+
+ freq += percent * freqs[i] as f64;
+ }
+
+ let percent = usage as f64 / total as f64;
+ let max_freq = freqs.last().unwrap().clone() as f64;
+ let from_max = (freq * percent) / max_freq;
+
+ (freq as u32, from_max as f32)
+}
+
+fn get_watts(item: CFDictionaryRef, unit: &String, duration: u64) -> WithError {
+ let val = unsafe { IOReportSimpleGetIntegerValue(item, 0) } as f32;
+ let val = val / (duration as f32 / 1000.0);
+ match unit.as_str() {
+ "mJ" => Ok(val / 1e3f32),
+ "nJ" => Ok(val / 1e9f32),
+ _ => Err(format!("Invalid energy unit: {}", unit).into()),
+ }
+}
+
+unsafe fn cfio_parse_sample(
+ subs: IOReportSubscriptionRef,
+ chan: CFMutableDictionaryRef,
+ info: &SocInfo,
+ duration: u64,
+) -> WithError {
+ let sample1 = IOReportCreateSamples(subs, chan, null());
+ std::thread::sleep(std::time::Duration::from_millis(duration));
+ let sample2 = IOReportCreateSamples(subs, chan, null());
+
+ let sample = IOReportCreateSamplesDelta(sample1, sample2, null());
+ CFRelease(sample1 as _);
+ CFRelease(sample2 as _);
+
+ let mut rs = PerfSample::default();
+ let na = "na".to_string();
+
+ let items = cfdict_get_val(sample, "IOReportChannels").unwrap() as CFArrayRef;
+ for i in 0..CFArrayGetCount(items) {
+ let item = CFArrayGetValueAtIndex(items, i) as CFDictionaryRef;
+
+ let gname = cfdict_get_str(item, "IOReportGroupName").unwrap_or(na.clone());
+ let sname = cfdict_get_str(item, "IOReportSubGroupName").unwrap_or(na.clone());
+ let cname = from_cfstr(IOReportChannelGetChannelName(item));
+ let _unit = from_cfstr(IOReportChannelGetUnitLabel(item));
+
+ if gname == "CPU Stats" || gname == "GPU Stats" {
+ match (sname.as_str(), cname.as_str()) {
+ (CPU_POWER_SUBG, "ECPU") => rs.ecpu_usage = calc_freq(item, &info.ecpu_freqs),
+ (CPU_POWER_SUBG, "PCPU") => rs.pcpu_usage = calc_freq(item, &info.pcpu_freqs),
+ (GPU_POWER_SUBG, "GPUPH") => rs.gpu_usage = calc_freq(item, &info.gpu_freqs[1..].to_vec()),
+ _ => {}
+ }
+ }
+
+ if gname == "Energy Model" {
+ // ultra chip is two joined max chips
+ match cname.as_str() {
+ "CPU Energy" => rs.cpu_power += get_watts(item, &_unit, duration)?,
+ "GPU Energy" => rs.gpu_power += get_watts(item, &_unit, duration)?,
+ x if x.starts_with("ANE") => rs.ane_power += get_watts(item, &_unit, duration)?,
+ _ => (),
+ }
+ }
+ }
+
+ CFRelease(sample as _);
+ rs.all_watts = rs.cpu_power + rs.gpu_power;
+ Ok(rs)
+}
+
+pub struct SubsChan {
+ subs: IOReportSubscriptionRef,
+ chan: CFMutableDictionaryRef,
+ info: SocInfo,
+}
+
+impl Drop for SubsChan {
+ fn drop(&mut self) {
+ unsafe {
+ CFRelease(self.subs as _);
+ CFRelease(self.chan as _);
+ }
+ }
+}
+
+impl SubsChan {
+ pub fn new(info: SocInfo) -> WithError {
+ let channels = vec![
+ ("Energy Model", None), // cpu+gpu+ane power
+ ("CPU Stats", Some(CPU_POWER_SUBG)), // cpu freq by cluster
+ ("GPU Stats", Some(GPU_POWER_SUBG)), // gpu freq
+ ];
+
+ let chan = unsafe { cfio_get_chan(channels)? };
+ let subs = unsafe { cfio_get_subs(chan)? };
+ Ok(Self { subs, chan, info })
+ }
+
+ pub fn sample(&mut self, duration: u64) -> WithError {
+ let (ram_usage, ram_total) = libc_ram_info()?;
+ let (swap_usage, swap_total) = libc_swap_info()?;
+
+ let mut res = unsafe { cfio_parse_sample(self.subs, self.chan, &self.info, duration) }?;
+ res.memory = MemoryUsage { ram_total, ram_usage, swap_total, swap_usage };
+
+ Ok(res)
+ }
+}