Skip to content

Commit 21ec903

Browse files
committed
feat(osx): add Apple Silicon P-core/E-core support and monitoring
[AI generated]
1 parent 633b4ba commit 21ec903

File tree

11 files changed

+1337
-8
lines changed

11 files changed

+1337
-8
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ if(BTOP_LTO)
6767
endif()
6868

6969
if(APPLE)
70-
target_sources(libbtop PRIVATE src/osx/btop_collect.cpp src/osx/sensors.cpp src/osx/smc.cpp)
70+
target_sources(libbtop PRIVATE src/osx/btop_collect.cpp src/osx/sensors.cpp src/osx/smc.cpp src/osx/ioreport.cpp)
7171
elseif(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR CMAKE_SYSTEM_NAME STREQUAL "MidnightBSD")
7272
target_sources(libbtop PRIVATE src/freebsd/btop_collect.cpp)
7373
elseif(CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")

src/btop_draw.cpp

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,8 @@ namespace Cpu {
531531
vector<Draw::Graph> graphs_upper;
532532
vector<Draw::Graph> graphs_lower;
533533
Draw::Meter cpu_meter;
534+
#ifdef __APPLE__
535+
#endif
534536
vector<Draw::Meter> gpu_meters;
535537
vector<Draw::Graph> core_graphs;
536538
vector<Draw::Graph> temp_graphs;
@@ -698,7 +700,6 @@ namespace Cpu {
698700
}
699701

700702
cpu_meter = Draw::Meter{cpu_meter_width, "cpu"};
701-
702703
if (mid_line) {
703704
out += Mv::to(y + graph_up_height + 1, x) + Fx::ub + Theme::c("cpu_box") + Symbols::div_left + Theme::c("div_line")
704705
+ Symbols::h_line * (width - b_width - 2) + Symbols::div_right
@@ -834,6 +835,12 @@ namespace Cpu {
834835
+ Symbols::h_line * ((freq_range ? 17 : 7) - cpuHz.size())
835836
+ Symbols::title_left + Fx::b + Theme::c("title") + cpuHz + Fx::ub + Theme::c("div_line") + Symbols::title_right;
836837

838+
#ifdef __APPLE__
839+
//? Skip default CPU bar if we have P/E cores - we'll draw P-CPU and E-CPU bars in the core section
840+
const bool skip_cpu_bar = (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0);
841+
if (not skip_cpu_bar)
842+
#endif
843+
{
837844
out += Mv::to(b_y + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + "CPU " + cpu_meter(safeVal(cpu.cpu_percent, "total"s).back())
838845
+ Theme::g("cpu").at(clamp(safeVal(cpu.cpu_percent, "total"s).back(), 0ll, 100ll)) + rjust(to_string(safeVal(cpu.cpu_percent, "total"s).back()), 4) + Theme::c("main_fg") + '%';
839846
if (show_temps) {
@@ -850,10 +857,11 @@ namespace Cpu {
850857
string cwatts_post = "W";
851858

852859
max_observed_pwr = max(max_observed_pwr, cpu.usage_watts);
853-
out += Theme::g("cached").at(clamp(cpu.usage_watts / max_observed_pwr * 100.0f, 0.0f, 100.0f)) + cwatts + Theme::c("main_fg") + cwatts_post;
860+
out += Theme::g("cached").at(clamp(cpu.usage_watts / max_observed_pwr * 100.0f, 0.0f, 100.0f)) + cwatts + Theme::c("main_fg") + cwatts_post;
854861
}
855862

856863
out += Theme::c("div_line") + Symbols::v_line;
864+
}
857865
} catch (const std::exception& e) {
858866
throw std::runtime_error("graphs, clock, meter : " + string{e.what()});
859867
}
@@ -870,12 +878,41 @@ namespace Cpu {
870878
};
871879

872880
//? Core text and graphs
881+
#ifdef __APPLE__
882+
//? On Apple Silicon, start at row 0 since we skip the default CPU bar
883+
const bool pe_layout = (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0);
884+
int cx = 0, cy = (pe_layout ? 0 : 1), cc = 0, core_width = (b_column_size == 0 ? 2 : 3);
885+
#else
873886
int cx = 0, cy = 1, cc = 0, core_width = (b_column_size == 0 ? 2 : 3);
887+
#endif
874888
if (Shared::coreCount >= 100) core_width++;
875-
for (const auto& n : iota(0, Shared::coreCount)) {
889+
890+
//? Helper lambda to draw a single core line
891+
#ifdef __APPLE__
892+
const int p_cores_count = Cpu::cpu_core_info.p_cores;
893+
const int e_cores_count = Cpu::cpu_core_info.e_cores;
894+
const bool use_pe_labels = (p_cores_count > 0 and e_cores_count > 0);
895+
#endif
896+
897+
auto draw_core = [&](int n) {
876898
auto enabled = is_cpu_enabled(n);
899+
#ifdef __APPLE__
900+
string core_label;
901+
if (use_pe_labels) {
902+
// On Apple Silicon: E-cores are indices 0 to e_cores-1, P-cores are e_cores to end
903+
if (n < e_cores_count) {
904+
core_label = "E" + to_string(n);
905+
} else {
906+
core_label = "P" + to_string(n - e_cores_count);
907+
}
908+
} else {
909+
core_label = (Shared::coreCount < 100 ? "C" : "") + to_string(n);
910+
}
911+
out += Mv::to(b_y + cy + 1, b_x + cx + 1) + Theme::c(enabled ? "main_fg" : "inactive_fg") + Fx::b + ljust(core_label, core_width + 1) + Fx::ub;
912+
#else
877913
out += Mv::to(b_y + cy + 1, b_x + cx + 1) + Theme::c(enabled ? "main_fg" : "inactive_fg") + (Shared::coreCount < 100 ? Fx::b + 'C' + Fx::ub : "")
878914
+ ljust(to_string(n), core_width);
915+
#endif
879916
if ((b_column_size > 0 or extra_width > 0) and cmp_less(n, core_graphs.size()))
880917
out += Theme::c("inactive_fg") + graph_bg * (5 * b_column_size + extra_width) + Mv::l(5 * b_column_size + extra_width)
881918
+ core_graphs.at(n)(safeVal(cpu.core_percent, n), data_same or redraw);
@@ -893,10 +930,93 @@ namespace Cpu {
893930
}
894931

895932
out += Theme::c("div_line") + Symbols::v_line;
933+
};
896934

897-
if ((++cy > ceil((double)Shared::coreCount / b_columns) or cy == max_row) and n != Shared::coreCount - 1) {
898-
if (++cc >= b_columns) break;
899-
cy = 1; cx = (b_width / b_columns) * cc;
935+
#ifdef __APPLE__
936+
//? Apple Silicon: Show P-CPU section, then E-CPU section
937+
const int p_cores_total = Cpu::cpu_core_info.p_cores;
938+
const int e_cores_total = Cpu::cpu_core_info.e_cores;
939+
const bool has_pe_cores = (p_cores_total > 0 and e_cores_total > 0);
940+
941+
if (has_pe_cores) {
942+
const int col_width = b_width / b_columns;
943+
944+
//? On Apple Silicon: E-cores are indices 0 to e_cores-1, P-cores are e_cores to end
945+
//? Calculate P-core and E-core average percentages
946+
long long p_sum = 0, e_sum = 0;
947+
for (int i = 0; i < e_cores_total and i < (int)cpu.core_percent.size(); ++i) {
948+
e_sum += safeVal(cpu.core_percent, i).back();
949+
}
950+
for (int i = e_cores_total; i < e_cores_total + p_cores_total and i < (int)cpu.core_percent.size(); ++i) {
951+
p_sum += safeVal(cpu.core_percent, i).back();
952+
}
953+
const long long p_avg = p_cores_total > 0 ? p_sum / p_cores_total : 0;
954+
const long long e_avg = e_cores_total > 0 ? e_sum / e_cores_total : 0;
955+
956+
//? P-CPU bar (spans full width of all columns)
957+
string p_label = "P-CPU";
958+
const int p_freq = Cpu::cpu_core_info.p_freq_mhz;
959+
const string p_freq_str = p_freq > 0 ? " " + format_freq(p_freq) : "";
960+
//? Total row width = b_width - 1 (content area, box border is separate)
961+
//? Content = "P-CPU " (6) + meter + freq_str + border (1) = b_width - 1
962+
const int p_freq_width = static_cast<int>(p_freq_str.size());
963+
const int p_meter = max(1, b_width - 7 - p_freq_width - 1);
964+
out += Mv::to(b_y + cy + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + p_label + Fx::ub + " "
965+
+ Draw::Meter{p_meter, "cpu"}(p_avg)
966+
+ Theme::g("cpu").at(clamp(p_avg, 0ll, 100ll)) + p_freq_str
967+
+ Theme::c("div_line") + Symbols::v_line;
968+
cy++;
969+
970+
//? Draw P-cores (indices e_cores_total to e_cores_total+p_cores_total-1)
971+
int p_drawn = 0;
972+
const int p_rows = (p_cores_total + b_columns - 1) / b_columns;
973+
for (int row = 0; row < p_rows and p_drawn < p_cores_total and cy < max_row; ++row) {
974+
for (int col = 0; col < b_columns and p_drawn < p_cores_total; ++col) {
975+
cx = col * col_width;
976+
draw_core(e_cores_total + p_drawn);
977+
p_drawn++;
978+
}
979+
cy++;
980+
}
981+
982+
//? E-CPU section header bar (spans full width of all columns)
983+
if (cy < max_row) {
984+
cx = 0;
985+
string e_label = "E-CPU";
986+
const int e_freq = Cpu::cpu_core_info.e_freq_mhz;
987+
const string e_freq_str = e_freq > 0 ? " " + format_freq(e_freq) : "";
988+
const int e_freq_width = static_cast<int>(e_freq_str.size());
989+
const int e_meter = max(1, b_width - 7 - e_freq_width - 1);
990+
out += Mv::to(b_y + cy + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + e_label + Fx::ub + " "
991+
+ Draw::Meter{e_meter, "cpu"}(e_avg)
992+
+ Theme::g("cpu").at(clamp(e_avg, 0ll, 100ll)) + e_freq_str
993+
+ Theme::c("div_line") + Symbols::v_line;
994+
cy++;
995+
}
996+
997+
//? Draw E-cores (indices 0 to e_cores_total-1)
998+
const int e_rows = (e_cores_total + b_columns - 1) / b_columns;
999+
int e_drawn = 0;
1000+
for (int row = 0; row < e_rows and e_drawn < e_cores_total and cy < max_row; ++row) {
1001+
for (int col = 0; col < b_columns and e_drawn < e_cores_total; ++col) {
1002+
cx = col * col_width;
1003+
draw_core(e_drawn);
1004+
e_drawn++;
1005+
}
1006+
cy++;
1007+
}
1008+
cx = 0;
1009+
} else
1010+
#endif
1011+
{
1012+
//? Standard layout: all cores in columns
1013+
for (const auto& n : iota(0, Shared::coreCount)) {
1014+
draw_core(n);
1015+
1016+
if ((++cy > ceil((double)Shared::coreCount / b_columns) or cy == max_row) and n != Shared::coreCount - 1) {
1017+
if (++cc >= b_columns) break;
1018+
cy = 1; cx = (b_width / b_columns) * cc;
1019+
}
9001020
}
9011021
}
9021022

@@ -2277,6 +2397,23 @@ namespace Draw {
22772397
b_columns = max(2, (int)ceil((double)(Shared::coreCount + 1) / (height - gpus_extra_height - 5)));
22782398
#else
22792399
b_columns = max(1, (int)ceil((double)(Shared::coreCount + 1) / (height - 5)));
2400+
#endif
2401+
#ifdef __APPLE__
2402+
//? On Apple Silicon with P/E cores, we need more rows due to separate sections
2403+
//? Recalculate b_columns to ensure both P and E sections fit
2404+
if (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0) {
2405+
const int p_cores = Cpu::cpu_core_info.p_cores;
2406+
const int e_cores = Cpu::cpu_core_info.e_cores;
2407+
const int available_rows = height - 5 - 2; // -2 for P-CPU and E-CPU header bars
2408+
//? Find minimum columns needed so both P and E sections fit
2409+
//? Need: ceil(p/cols) + ceil(e/cols) <= available_rows
2410+
while (b_columns < Shared::coreCount) {
2411+
int p_rows = (p_cores + b_columns - 1) / b_columns;
2412+
int e_rows = (e_cores + b_columns - 1) / b_columns;
2413+
if (p_rows + e_rows <= available_rows) break;
2414+
b_columns++;
2415+
}
2416+
}
22802417
#endif
22812418
if (b_columns * (21 + 12 * show_temp) < width - (width / 3)) {
22822419
b_column_size = 2;
@@ -2301,6 +2438,22 @@ namespace Draw {
23012438
#else
23022439
b_height = min(height - 2, (int)ceil((double)Shared::coreCount / b_columns) + 4);
23032440
#endif
2441+
#ifdef __APPLE__
2442+
//? On Apple Silicon with P/E cores, recalculate b_height based on actual P/E row needs
2443+
//? P/E layout needs: P-CPU header + P rows + E-CPU header + E rows + base (4)
2444+
if (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0) {
2445+
const int p_cores = Cpu::cpu_core_info.p_cores;
2446+
const int e_cores = Cpu::cpu_core_info.e_cores;
2447+
int p_rows = (p_cores + b_columns - 1) / b_columns;
2448+
int e_rows = (e_cores + b_columns - 1) / b_columns;
2449+
//? +4 base, +2 for P-CPU and E-CPU headers (we skip default CPU bar, so net +1 from original)
2450+
int pe_height = p_rows + e_rows + 4 + 1;
2451+
#ifdef GPU_SUPPORT
2452+
pe_height += gpus_extra_height;
2453+
#endif
2454+
b_height = min(height - 2, pe_height);
2455+
}
2456+
#endif
23042457

23052458
b_x = x + width - b_width - 1;
23062459
b_y = y + ceil((double)(height - 2) / 2) - ceil((double)b_height / 2) + 1;

src/btop_shared.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,19 @@ namespace Cpu {
240240

241241
auto get_cpuHz() -> string;
242242

243+
#ifdef __APPLE__
244+
//* Apple Silicon P-core and E-core information
245+
struct core_info {
246+
int p_cores = 0; // Performance cores (perflevel0) - indices e_cores to e_cores+p_cores-1
247+
int e_cores = 0; // Efficiency cores (perflevel1) - indices 0 to e_cores-1
248+
int p_freq_mhz = 0; // Current P-cluster frequency in MHz
249+
int e_freq_mhz = 0; // Current E-cluster frequency in MHz
250+
};
251+
extern core_info cpu_core_info;
252+
auto get_core_info() -> core_info;
253+
void update_core_frequencies(); // Update current P/E frequencies via IOReport
254+
#endif
255+
243256
//* Get battery info from /sys
244257
auto get_battery() -> tuple<int, float, long, string>;
245258

src/btop_tools.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,21 @@ namespace Tools {
205205
return str;
206206
}
207207

208+
//* Format CPU frequency: 3 digits max, e.g., "1.9 GHz" or "600 MHz"
209+
inline string format_freq(int mhz) {
210+
if (mhz <= 0) return "";
211+
string str;
212+
if (mhz > 999) {
213+
str = fmt::format("{:.1f}", mhz / 1000.0);
214+
if (str.size() > 3) str.resize(3);
215+
if (str.back() == '.') str.pop_back();
216+
str += " GHz";
217+
} else {
218+
str = fmt::format("{} MHz", mhz);
219+
}
220+
return str;
221+
}
222+
208223
//* Check if vector <vec> contains value <find_val>
209224
template <typename T, typename T2>
210225
inline bool v_contains(const vector<T>& vec, const T2& find_val) {

src/osx/btop_collect.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ tab-size = 4
6767
#include "sensors.hpp"
6868
#endif
6969
#include "smc.hpp"
70+
#include "ioreport.hpp"
7071

7172
#if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
7273
#define kIOMainPortDefault kIOMasterPortDefault
@@ -109,6 +110,7 @@ namespace Cpu {
109110
string cpu_sensor;
110111
vector<string> core_sensors;
111112
std::unordered_map<int, int> core_mapping;
113+
core_info cpu_core_info;
112114
} // namespace Cpu
113115

114116
namespace Mem {
@@ -187,6 +189,7 @@ namespace Shared {
187189
Cpu::cpuName = Cpu::get_cpuName();
188190
Cpu::got_sensors = Cpu::get_sensors();
189191
Cpu::core_mapping = Cpu::get_core_mapping();
192+
Cpu::cpu_core_info = Cpu::get_core_info();
190193

191194
//? Init for namespace Mem
192195
Mem::old_uptime = system_uptime();
@@ -367,6 +370,42 @@ namespace Cpu {
367370
return core_map;
368371
}
369372

373+
auto get_core_info() -> core_info {
374+
core_info info;
375+
size_t size = sizeof(int);
376+
377+
// On Apple Silicon:
378+
// - perflevel0 = Performance cores (P-cores)
379+
// - perflevel1 = Efficiency cores (E-cores)
380+
// Note: E-cores are enumerated FIRST (indices 0 to e_cores-1), P-cores SECOND
381+
if (sysctlbyname("hw.perflevel0.physicalcpu", &info.p_cores, &size, nullptr, 0) != 0) {
382+
info.p_cores = 0;
383+
}
384+
385+
size = sizeof(int);
386+
if (sysctlbyname("hw.perflevel1.physicalcpu", &info.e_cores, &size, nullptr, 0) != 0) {
387+
info.e_cores = 0;
388+
}
389+
390+
// Fallback: if detection fails (Intel Mac or error), treat all as P-cores
391+
if (info.p_cores == 0 and info.e_cores == 0) {
392+
info.p_cores = Shared::coreCount;
393+
}
394+
395+
return info;
396+
}
397+
398+
void update_core_frequencies() {
399+
//? Initialize IOReport (handles double-init internally)
400+
IOReport::init();
401+
402+
if (IOReport::is_available()) {
403+
auto [e_freq, p_freq] = IOReport::get_cpu_frequencies();
404+
cpu_core_info.e_freq_mhz = static_cast<int>(e_freq);
405+
cpu_core_info.p_freq_mhz = static_cast<int>(p_freq);
406+
}
407+
}
408+
370409
class IOPSInfo_Wrap {
371410
CFTypeRef data;
372411
public:
@@ -431,6 +470,9 @@ namespace Cpu {
431470
return current_cpu;
432471
auto &cpu = current_cpu;
433472

473+
//? Update P/E core frequencies
474+
update_core_frequencies();
475+
434476
if (getloadavg(cpu.load_avg.data(), cpu.load_avg.size()) < 0) {
435477
Logger::error("failed to get load averages");
436478
}

0 commit comments

Comments
 (0)