macpm 0.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
macpm/__init__.py ADDED
File without changes
macpm/macpm.py ADDED
@@ -0,0 +1,529 @@
1
+ import time
2
+ import argparse
3
+ import humanize
4
+ from collections import deque
5
+ from dashing import VSplit, HSplit, HGauge, HChart, VGauge, HBrailleChart, HBrailleFilledChart
6
+ from .utils import *
7
+
8
+ parser = argparse.ArgumentParser(
9
+ description='macpm: Performance monitoring CLI tool for Apple Silicon')
10
+ parser.add_argument('--interval', type=int, default=1,
11
+ help='Display interval and sampling interval for powermetrics (seconds)')
12
+ parser.add_argument('--color', type=int, default=2,
13
+ help='Choose display color (0~8)')
14
+ parser.add_argument('--avg', type=int, default=30,
15
+ help='Interval for averaged values (seconds)')
16
+ parser.add_argument('--show_cores', type=bool, default=False,
17
+ help='Choose show cores mode')
18
+ parser.add_argument('--max_count', type=int, default=0,
19
+ help='Max show count to restart powermetrics')
20
+ args = parser.parse_args()
21
+
22
+
23
+ def main():
24
+ print("\nmacpm - Performance monitoring CLI tool for Apple Silicon")
25
+ print("You can update macpm by running `pip install macpm --upgrade`")
26
+ print("Get help at `https://github.com/visualcjy/macpm`")
27
+ print("P.S. You are recommended to run macpm with `sudo macpm`\n")
28
+ print("\n[1/3] Loading macpm\n")
29
+ print("\033[?25l")
30
+
31
+ cpu1_gauge = HGauge(title="E-CPU Usage", val=0, color=args.color)
32
+ cpu2_gauge = HGauge(title="P-CPU Usage", val=0, color=args.color)
33
+ gpu_gauge = HGauge(title="GPU Usage", val=0, color=args.color)
34
+ ane_gauge = HGauge(title="ANE", val=0, color=args.color)
35
+ gpu_ane_gauges = [gpu_gauge, ane_gauge]
36
+
37
+ soc_info_dict = get_soc_info()
38
+ e_core_count = soc_info_dict["e_core_count"]
39
+ e_core_gauges = [VGauge(val=0, color=args.color, border_color=args.color) for _ in range(e_core_count)]
40
+ p_core_count = soc_info_dict["p_core_count"]
41
+ p_core_gauges = [VGauge(val=0, color=args.color, border_color=args.color) for _ in range(min(p_core_count, 8))]
42
+ p_core_split = [HSplit(
43
+ *p_core_gauges,
44
+ )]
45
+ if p_core_count > 8:
46
+ p_core_gauges_ext = [VGauge(val=0, color=args.color, border_color=args.color) for _ in range(p_core_count - 8)]
47
+ p_core_split.append(HSplit(
48
+ *p_core_gauges_ext,
49
+ ))
50
+ processor_gauges = [cpu1_gauge,
51
+ HSplit(*e_core_gauges),
52
+ cpu2_gauge,
53
+ *p_core_split,
54
+ *gpu_ane_gauges
55
+ ] if args.show_cores else [
56
+ HSplit(cpu1_gauge, cpu2_gauge),
57
+ HSplit(*gpu_ane_gauges)
58
+ ]
59
+ processor_split = VSplit(
60
+ *processor_gauges,
61
+ title="Processor Utilization",
62
+ border_color=args.color,
63
+ )
64
+
65
+ ram_gauge = HGauge(title="RAM Usage", val=0, color=args.color)
66
+ """
67
+ ecpu_bw_gauge = HGauge(title="E-CPU B/W", val=50, color=args.color)
68
+ pcpu_bw_gauge = HGauge(title="P-CPU B/W", val=50, color=args.color)
69
+ gpu_bw_gauge = HGauge(title="GPU B/W", val=50, color=args.color)
70
+ media_bw_gauge = HGauge(title="Media B/W", val=50, color=args.color)
71
+ bw_gauges = [HSplit(
72
+ ecpu_bw_gauge,
73
+ pcpu_bw_gauge,
74
+ ),
75
+ HSplit(
76
+ gpu_bw_gauge,
77
+ media_bw_gauge,
78
+ )] if args.show_cores else [
79
+ HSplit(
80
+ ecpu_bw_gauge,
81
+ pcpu_bw_gauge,
82
+ gpu_bw_gauge,
83
+ media_bw_gauge,
84
+ )]
85
+ """
86
+ memory_gauges = VSplit(
87
+ ram_gauge,
88
+ #*bw_gauges,
89
+ border_color=args.color,
90
+ title="Memory"
91
+ )
92
+
93
+ cpu_power_chart = HChart(title="CPU Power", color=args.color)
94
+ gpu_power_chart = HChart(title="GPU Power", color=args.color)
95
+ power_charts = VSplit(
96
+ cpu_power_chart,
97
+ gpu_power_chart,
98
+ title="Power Chart",
99
+ border_color=args.color,
100
+ ) if args.show_cores else HSplit(
101
+ cpu_power_chart,
102
+ gpu_power_chart,
103
+ title="Power Chart",
104
+ border_color=args.color,
105
+ )
106
+
107
+ disk_read_iops_charts = HChart(title="read iops", color=args.color)
108
+ disk_write_iops_charts = HBrailleFilledChart(title="write iops", color=args.color)
109
+ disk_read_bps_charts = HChart(title="read Bps", color=args.color)
110
+ disk_write_bps_charts = HBrailleFilledChart(title="write Bps", color=args.color)
111
+ network_in_bps_charts = HChart(title="in Bps", color=args.color)
112
+ network_out_bps_charts = HBrailleFilledChart(title="out Bps", color=args.color)
113
+ disk_io_charts = HSplit(
114
+ disk_read_iops_charts,
115
+ disk_write_iops_charts,
116
+ disk_read_bps_charts,
117
+ disk_write_bps_charts,
118
+ title="Disk IO",
119
+ color=args.color,
120
+ border_color=args.color)
121
+
122
+ network_io_charts = HSplit(
123
+ network_in_bps_charts,
124
+ network_out_bps_charts,
125
+ title="Network IO",
126
+ color=args.color,
127
+ border_color=args.color)
128
+
129
+ ui = HSplit(
130
+ processor_split,
131
+ VSplit(
132
+ memory_gauges,
133
+ power_charts,
134
+ disk_io_charts,
135
+ network_io_charts,
136
+ )
137
+ ) if args.show_cores else VSplit(
138
+ processor_split,
139
+ memory_gauges,
140
+ power_charts,
141
+ disk_io_charts,
142
+ network_io_charts,
143
+ )
144
+
145
+ usage_gauges = ui.items[0]
146
+ #bw_gauges = memory_gauges.items[1]
147
+
148
+ cpu_title = "".join([
149
+ soc_info_dict["name"],
150
+ " (cores: ",
151
+ str(soc_info_dict["e_core_count"]),
152
+ "E+",
153
+ str(soc_info_dict["p_core_count"]),
154
+ "P+",
155
+ str(soc_info_dict["gpu_core_count"]),
156
+ "GPU)"
157
+ ])
158
+ usage_gauges.title = cpu_title
159
+ cpu_max_power = soc_info_dict["cpu_max_power"]
160
+ gpu_max_power = soc_info_dict["gpu_max_power"]
161
+ ane_max_power = 8.0
162
+ """max_cpu_bw = soc_info_dict["cpu_max_bw"]
163
+ max_gpu_bw = soc_info_dict["gpu_max_bw"]
164
+ max_media_bw = 7.0"""
165
+
166
+ cpu_peak_power = 0
167
+ gpu_peak_power = 0
168
+ package_peak_power = 0
169
+ disk_read_iops_peak = 0
170
+ disk_write_iops_peak = 0
171
+ disk_read_bps_peak = 0
172
+ disk_write_bps_peak = 0
173
+ network_in_bps_peak = 0
174
+ network_out_bps_peak = 0
175
+
176
+ print("\n[2/3] Starting powermetrics process\n")
177
+
178
+ timecode = str(int(time.time()))
179
+
180
+ powermetrics_process = run_powermetrics_process(timecode,
181
+ interval=args.interval * 1000)
182
+
183
+ print("\n[3/3] Waiting for first reading...\n")
184
+
185
+ def get_reading(wait=0.1):
186
+ ready = parse_powermetrics(timecode=timecode)
187
+ while not ready:
188
+ time.sleep(wait)
189
+ ready = parse_powermetrics(timecode=timecode)
190
+ return ready
191
+
192
+ ready = get_reading()
193
+ last_timestamp = ready[-1]
194
+
195
+ def get_avg(inlist):
196
+ avg = sum(inlist) / len(inlist)
197
+ return avg
198
+
199
+ avg_package_power_list = deque([], maxlen=int(args.avg / args.interval))
200
+ avg_cpu_power_list = deque([], maxlen=int(args.avg / args.interval))
201
+ avg_gpu_power_list = deque([], maxlen=int(args.avg / args.interval))
202
+
203
+ clear_console()
204
+
205
+ count=0
206
+ try:
207
+ while True:
208
+ if args.max_count > 0:
209
+ if count >= args.max_count:
210
+ count = 0
211
+ powermetrics_process.terminate()
212
+ timecode = str(int(time.time()))
213
+ powermetrics_process = run_powermetrics_process(
214
+ timecode, interval=args.interval * 1000)
215
+ count += 1
216
+ ready = parse_powermetrics(timecode=timecode)
217
+ if ready:
218
+ cpu_metrics_dict, gpu_metrics_dict, thermal_pressure, bandwidth_metrics, disk_metrics_dict, network_metrics_dict, timestamp = ready
219
+
220
+ if timestamp > last_timestamp:
221
+ last_timestamp = timestamp
222
+
223
+ if thermal_pressure == "Nominal":
224
+ thermal_throttle = "no"
225
+ else:
226
+ thermal_throttle = "yes"
227
+
228
+ """e_cpu_usage = 0
229
+ core_count = 0
230
+ for i in cpu_metrics_dict["e_core"]:
231
+ e_cpu_usage += cpu_metrics_dict["E-Cluster" + str(i) + "_active"]
232
+ core_count += 1
233
+ e_cpu_usage = (e_cpu_usage / core_count) if core_count > 0 else 0"""
234
+ cpu1_gauge.title = "".join([
235
+ "E-CPU Usage: ",
236
+ str(cpu_metrics_dict["E-Cluster_active"]),
237
+ "% @ ",
238
+ str(cpu_metrics_dict["E-Cluster_freq_Mhz"]),
239
+ " MHz"
240
+ ])
241
+ cpu1_gauge.value = cpu_metrics_dict["E-Cluster_active"]
242
+
243
+ """p_cpu_usage = 0
244
+ core_count = 0
245
+ for i in cpu_metrics_dict["p_core"]:
246
+ p_cpu_usage += cpu_metrics_dict["P-Cluster" + str(i) + "_active"]
247
+ core_count += 1
248
+ p_cpu_usage = (p_cpu_usage / core_count) if core_count > 0 else 0"""
249
+ cpu2_gauge.title = "".join([
250
+ "P-CPU Usage: ",
251
+ str(cpu_metrics_dict["P-Cluster_active"]),
252
+ "% @ ",
253
+ str(cpu_metrics_dict["P-Cluster_freq_Mhz"]),
254
+ " MHz"
255
+ ])
256
+ cpu2_gauge.value = cpu_metrics_dict["P-Cluster_active"]
257
+
258
+ if args.show_cores:
259
+ core_count = 0
260
+ for i in cpu_metrics_dict["e_core"]:
261
+ e_core_gauges[core_count % 4].title = "".join([
262
+ "Core-" + str(i + 1) + " ",
263
+ str(cpu_metrics_dict["E-Cluster" + str(i) + "_active"]),
264
+ "%",
265
+ ])
266
+ e_core_gauges[core_count % 4].value = cpu_metrics_dict["E-Cluster" + str(i) + "_active"]
267
+ core_count += 1
268
+ core_count = 0
269
+ for i in cpu_metrics_dict["p_core"]:
270
+ core_gauges = p_core_gauges if core_count < 8 else p_core_gauges_ext
271
+ core_gauges[core_count % 8].title = "".join([
272
+ ("Core-" if p_core_count < 6 else 'C-') + str(i + 1) + " ",
273
+ str(cpu_metrics_dict["P-Cluster" + str(i) + "_active"]),
274
+ "%",
275
+ ])
276
+ core_gauges[core_count % 8].value = cpu_metrics_dict["P-Cluster" + str(i) + "_active"]
277
+ core_count += 1
278
+
279
+ gpu_gauge.title = "".join([
280
+ "GPU Usage: ",
281
+ str(gpu_metrics_dict["active"]),
282
+ "% @ ",
283
+ str(gpu_metrics_dict["freq_MHz"]),
284
+ " MHz"
285
+ ])
286
+ gpu_gauge.value = gpu_metrics_dict["active"]
287
+
288
+ ane_util_percent = int(
289
+ cpu_metrics_dict["ane_W"] / args.interval / ane_max_power * 100)
290
+ ane_gauge.title = "".join([
291
+ "ANE Usage: ",
292
+ str(ane_util_percent),
293
+ "% @ ",
294
+ '{0:.1f}'.format(
295
+ cpu_metrics_dict["ane_W"] / args.interval),
296
+ " W"
297
+ ])
298
+ ane_gauge.value = ane_util_percent
299
+
300
+ ram_metrics_dict = get_ram_metrics_dict()
301
+
302
+ if ram_metrics_dict["swap_total_GB"] < 0.1:
303
+ ram_gauge.title = "".join([
304
+ "RAM Usage: ",
305
+ str(ram_metrics_dict["used_GB"]),
306
+ "/",
307
+ str(ram_metrics_dict["total_GB"]),
308
+ "GB - swap inactive"
309
+ ])
310
+ else:
311
+ ram_gauge.title = "".join([
312
+ "RAM Usage: ",
313
+ str(ram_metrics_dict["used_GB"]),
314
+ "/",
315
+ str(ram_metrics_dict["total_GB"]),
316
+ "GB",
317
+ " - swap:",
318
+ str(ram_metrics_dict["swap_used_GB"]),
319
+ "/",
320
+ str(ram_metrics_dict["swap_total_GB"]),
321
+ "GB"
322
+ ])
323
+ ram_gauge.value = ram_metrics_dict["free_percent"]
324
+
325
+ """
326
+
327
+ ecpu_bw_percent = int(
328
+ (bandwidth_metrics["ECPU DCS RD"] + bandwidth_metrics[
329
+ "ECPU DCS WR"]) / args.interval / max_cpu_bw * 100)
330
+ ecpu_read_GB = bandwidth_metrics["ECPU DCS RD"] / \
331
+ args.interval
332
+ ecpu_write_GB = bandwidth_metrics["ECPU DCS WR"] / \
333
+ args.interval
334
+ ecpu_bw_gauge.title = "".join([
335
+ "E-CPU: ",
336
+ '{0:.1f}'.format(ecpu_read_GB + ecpu_write_GB),
337
+ "GB/s"
338
+ ])
339
+ ecpu_bw_gauge.value = ecpu_bw_percent
340
+
341
+ pcpu_bw_percent = int(
342
+ (bandwidth_metrics["PCPU DCS RD"] + bandwidth_metrics[
343
+ "PCPU DCS WR"]) / args.interval / max_cpu_bw * 100)
344
+ pcpu_read_GB = bandwidth_metrics["PCPU DCS RD"] / \
345
+ args.interval
346
+ pcpu_write_GB = bandwidth_metrics["PCPU DCS WR"] / \
347
+ args.interval
348
+ pcpu_bw_gauge.title = "".join([
349
+ "P-CPU: ",
350
+ '{0:.1f}'.format(pcpu_read_GB + pcpu_write_GB),
351
+ "GB/s"
352
+ ])
353
+ pcpu_bw_gauge.value = pcpu_bw_percent
354
+
355
+ gpu_bw_percent = int(
356
+ (bandwidth_metrics["GFX DCS RD"] + bandwidth_metrics["GFX DCS WR"]) / max_gpu_bw * 100)
357
+ gpu_read_GB = bandwidth_metrics["GFX DCS RD"]
358
+ gpu_write_GB = bandwidth_metrics["GFX DCS WR"]
359
+ gpu_bw_gauge.title = "".join([
360
+ "GPU: ",
361
+ '{0:.1f}'.format(gpu_read_GB + gpu_write_GB),
362
+ "GB/s"
363
+ ])
364
+ gpu_bw_gauge.value = gpu_bw_percent
365
+
366
+ media_bw_percent = int(
367
+ bandwidth_metrics["MEDIA DCS"] / args.interval / max_media_bw * 100)
368
+ media_bw_gauge.title = "".join([
369
+ "Media: ",
370
+ '{0:.1f}'.format(
371
+ bandwidth_metrics["MEDIA DCS"] / args.interval),
372
+ "GB/s"
373
+ ])
374
+ media_bw_gauge.value = media_bw_percent
375
+
376
+ total_bw_GB = (
377
+ bandwidth_metrics["DCS RD"] + bandwidth_metrics["DCS WR"]) / args.interval
378
+ bw_gauges.title = "".join([
379
+ "Memory Bandwidth: ",
380
+ '{0:.2f}'.format(total_bw_GB),
381
+ " GB/s (R:",
382
+ '{0:.2f}'.format(
383
+ bandwidth_metrics["DCS RD"] / args.interval),
384
+ "/W:",
385
+ '{0:.2f}'.format(
386
+ bandwidth_metrics["DCS WR"] / args.interval),
387
+ " GB/s)"
388
+ ])
389
+ if args.show_cores:
390
+ bw_gauges_ext = memory_gauges.items[2]
391
+ bw_gauges_ext.title = "Memory Bandwidth:"
392
+ """
393
+
394
+ package_power_W = cpu_metrics_dict["package_W"] / \
395
+ args.interval
396
+ if package_power_W > package_peak_power:
397
+ package_peak_power = package_power_W
398
+ avg_package_power_list.append(package_power_W)
399
+ avg_package_power = get_avg(avg_package_power_list)
400
+ power_charts.title = "".join([
401
+ "CPU+GPU+ANE Power: ",
402
+ '{0:.2f}'.format(package_power_W),
403
+ "W (avg: ",
404
+ '{0:.2f}'.format(avg_package_power),
405
+ "W peak: ",
406
+ '{0:.2f}'.format(package_peak_power),
407
+ "W) throttle: ",
408
+ thermal_throttle,
409
+ ])
410
+
411
+ cpu_power_percent = int(
412
+ cpu_metrics_dict["cpu_W"] / args.interval / cpu_max_power * 100)
413
+ cpu_power_W = cpu_metrics_dict["cpu_W"] / args.interval
414
+ if cpu_power_W > cpu_peak_power:
415
+ cpu_peak_power = cpu_power_W
416
+ avg_cpu_power_list.append(cpu_power_W)
417
+ avg_cpu_power = get_avg(avg_cpu_power_list)
418
+ cpu_power_chart.title = "".join([
419
+ "CPU: ",
420
+ '{0:.2f}'.format(cpu_power_W),
421
+ "W (avg: ",
422
+ '{0:.2f}'.format(avg_cpu_power),
423
+ "W peak: ",
424
+ '{0:.2f}'.format(cpu_peak_power),
425
+ "W)"
426
+ ])
427
+ cpu_power_chart.append(cpu_power_percent)
428
+
429
+ gpu_power_percent = int(
430
+ cpu_metrics_dict["gpu_W"] / args.interval / gpu_max_power * 100)
431
+ gpu_power_W = cpu_metrics_dict["gpu_W"] / args.interval
432
+ if gpu_power_W > gpu_peak_power:
433
+ gpu_peak_power = gpu_power_W
434
+ avg_gpu_power_list.append(gpu_power_W)
435
+ avg_gpu_power = get_avg(avg_gpu_power_list)
436
+ gpu_power_chart.title = "".join([
437
+ "GPU: ",
438
+ '{0:.2f}'.format(gpu_power_W),
439
+ "W (avg: ",
440
+ '{0:.2f}'.format(avg_gpu_power),
441
+ "W peak: ",
442
+ '{0:.2f}'.format(gpu_peak_power),
443
+ "W)"
444
+ ])
445
+ gpu_power_chart.append(gpu_power_percent)
446
+
447
+ def format_number(number):
448
+ return humanize.naturalsize(number)
449
+
450
+ disk_read_iops = disk_metrics_dict["read_iops"]
451
+ if disk_read_iops > disk_read_iops_peak:
452
+ disk_read_iops_peak = disk_read_iops
453
+ disk_read_iops_charts.title = "Read iops: "+ f'{disk_read_iops} (peak: {disk_read_iops_peak})'
454
+ if disk_read_iops_charts.datapoints:
455
+ disk_read_iops_rate = int(disk_read_iops / disk_read_iops_peak * 100)
456
+ else:
457
+ disk_read_iops_rate = 100
458
+ disk_read_iops_charts.append(disk_read_iops_rate)
459
+
460
+ disk_write_iops = disk_metrics_dict["write_iops"]
461
+ if disk_write_iops > disk_write_iops_peak:
462
+ disk_write_iops_peak = disk_write_iops
463
+ disk_write_iops_charts.title = "Write iops: "+ f'{disk_write_iops} (peak: {disk_write_iops_peak})'
464
+ if disk_write_iops_charts.datapoints:
465
+ disk_write_iops_rate = int(disk_read_iops / disk_write_iops_peak * 100)
466
+ else:
467
+ disk_write_iops_rate = 100
468
+ disk_write_iops_charts.append(disk_write_iops_rate)
469
+
470
+ disk_read_bps = disk_metrics_dict["read_Bps"]
471
+ if disk_read_bps > disk_read_bps_peak:
472
+ disk_read_bps_peak = disk_read_bps
473
+ disk_read_bps_charts.title = "Read : "+ f'{format_number(disk_read_bps)}/s (peak: {format_number(disk_read_bps_peak)}/s)'
474
+ if disk_read_bps_charts.datapoints:
475
+ disk_read_bps_rate = int(disk_read_bps / disk_read_bps_peak * 100)
476
+ else:
477
+ disk_read_bps_rate = 100
478
+ disk_read_bps_charts.append(disk_read_bps_rate)
479
+
480
+ disk_write_bps = disk_metrics_dict["write_Bps"]
481
+ if disk_write_bps > disk_write_bps_peak:
482
+ disk_write_bps_peak = disk_write_bps
483
+ disk_write_bps_charts.title = "Write : "+ f'{format_number(disk_write_bps)}/s (peak: {format_number(disk_write_bps_peak)}/s)'
484
+ if disk_write_bps_charts.datapoints:
485
+ disk_write_bps_rate = int(disk_write_bps / disk_write_bps_peak * 100)
486
+ else:
487
+ disk_write_bps_rate = 100
488
+ disk_write_bps_charts.append(disk_write_bps_rate)
489
+
490
+ network_in_bps = network_metrics_dict["in_Bps"]
491
+ if network_in_bps > network_in_bps_peak:
492
+ network_in_bps_peak = network_in_bps
493
+ network_in_bps_charts.title = "in : "+ f'{format_number(network_in_bps)}/s (peak: {format_number(network_in_bps_peak)}/s)'
494
+ if network_in_bps_charts.datapoints:
495
+ network_in_bps_rate = int(network_in_bps / network_in_bps_peak * 100)
496
+ else:
497
+ network_in_bps_rate = 100
498
+ network_in_bps_charts.append(network_in_bps_rate)
499
+
500
+ network_out_bps = network_metrics_dict["out_Bps"]
501
+ if network_out_bps > network_out_bps_peak:
502
+ network_out_bps_peak = network_out_bps
503
+ network_out_bps_charts.title = "out : "+ f'{format_number(network_out_bps)}/s (peak: {format_number(network_out_bps_peak)}/s)'
504
+ if network_out_bps_charts.datapoints:
505
+ network_out_bps_rate = int(network_out_bps / network_out_bps_peak * 100)
506
+ else:
507
+ network_out_bps_rate = 100
508
+ network_out_bps_charts.append(network_out_bps_rate)
509
+
510
+ ui.display()
511
+
512
+ time.sleep(args.interval)
513
+
514
+ except KeyboardInterrupt:
515
+ print("Stopping...")
516
+ print("\033[?25h")
517
+
518
+ return powermetrics_process
519
+
520
+
521
+ if __name__ == "__main__":
522
+ powermetrics_process = main()
523
+ try:
524
+ powermetrics_process.terminate()
525
+ print("Successfully terminated powermetrics process")
526
+ except Exception as e:
527
+ print(e)
528
+ powermetrics_process.terminate()
529
+ print("Successfully terminated powermetrics process")
macpm/parsers.py ADDED
@@ -0,0 +1,171 @@
1
+ def parse_thermal_pressure(powermetrics_parse):
2
+ return powermetrics_parse["thermal_pressure"]
3
+
4
+
5
+ def parse_bandwidth_metrics(powermetrics_parse):
6
+ bandwidth_metrics = powermetrics_parse["bandwidth_counters"]
7
+ bandwidth_metrics_dict = {}
8
+ data_fields = ["PCPU0 DCS RD", "PCPU0 DCS WR",
9
+ "PCPU1 DCS RD", "PCPU1 DCS WR",
10
+ "PCPU2 DCS RD", "PCPU2 DCS WR",
11
+ "PCPU3 DCS RD", "PCPU3 DCS WR",
12
+ "PCPU DCS RD", "PCPU DCS WR",
13
+ "ECPU0 DCS RD", "ECPU0 DCS WR",
14
+ "ECPU1 DCS RD", "ECPU1 DCS WR",
15
+ "ECPU DCS RD", "ECPU DCS WR",
16
+ "GFX DCS RD", "GFX DCS WR",
17
+ "ISP DCS RD", "ISP DCS WR",
18
+ "STRM CODEC DCS RD", "STRM CODEC DCS WR",
19
+ "PRORES DCS RD", "PRORES DCS WR",
20
+ "VDEC DCS RD", "VDEC DCS WR",
21
+ "VENC0 DCS RD", "VENC0 DCS WR",
22
+ "VENC1 DCS RD", "VENC1 DCS WR",
23
+ "VENC2 DCS RD", "VENC2 DCS WR",
24
+ "VENC3 DCS RD", "VENC3 DCS WR",
25
+ "VENC DCS RD", "VENC DCS WR",
26
+ "JPG0 DCS RD", "JPG0 DCS WR",
27
+ "JPG1 DCS RD", "JPG1 DCS WR",
28
+ "JPG2 DCS RD", "JPG2 DCS WR",
29
+ "JPG3 DCS RD", "JPG3 DCS WR",
30
+ "JPG DCS RD", "JPG DCS WR",
31
+ "DCS RD", "DCS WR"]
32
+ for h in data_fields:
33
+ bandwidth_metrics_dict[h] = 0
34
+ for l in bandwidth_metrics:
35
+ if l["name"] in data_fields:
36
+ bandwidth_metrics_dict[l["name"]] = l["value"]/(1e9)
37
+ bandwidth_metrics_dict["PCPU DCS RD"] = bandwidth_metrics_dict["PCPU DCS RD"] + \
38
+ bandwidth_metrics_dict["PCPU0 DCS RD"] + \
39
+ bandwidth_metrics_dict["PCPU1 DCS RD"] + \
40
+ bandwidth_metrics_dict["PCPU2 DCS RD"] + \
41
+ bandwidth_metrics_dict["PCPU3 DCS RD"]
42
+ bandwidth_metrics_dict["PCPU DCS WR"] = bandwidth_metrics_dict["PCPU DCS WR"] + \
43
+ bandwidth_metrics_dict["PCPU0 DCS WR"] + \
44
+ bandwidth_metrics_dict["PCPU1 DCS WR"] + \
45
+ bandwidth_metrics_dict["PCPU2 DCS WR"] + \
46
+ bandwidth_metrics_dict["PCPU3 DCS WR"]
47
+ bandwidth_metrics_dict["JPG DCS RD"] = bandwidth_metrics_dict["JPG DCS RD"] + \
48
+ bandwidth_metrics_dict["JPG0 DCS RD"] + \
49
+ bandwidth_metrics_dict["JPG1 DCS RD"] + \
50
+ bandwidth_metrics_dict["JPG2 DCS RD"] + \
51
+ bandwidth_metrics_dict["JPG3 DCS RD"]
52
+ bandwidth_metrics_dict["JPG DCS WR"] = bandwidth_metrics_dict["JPG DCS WR"] + \
53
+ bandwidth_metrics_dict["JPG0 DCS WR"] + \
54
+ bandwidth_metrics_dict["JPG1 DCS WR"] + \
55
+ bandwidth_metrics_dict["JPG2 DCS WR"] + \
56
+ bandwidth_metrics_dict["JPG3 DCS WR"]
57
+ bandwidth_metrics_dict["VENC DCS RD"] = bandwidth_metrics_dict["VENC DCS RD"] + \
58
+ bandwidth_metrics_dict["VENC0 DCS RD"] + \
59
+ bandwidth_metrics_dict["VENC1 DCS RD"] + \
60
+ bandwidth_metrics_dict["VENC2 DCS RD"] + \
61
+ bandwidth_metrics_dict["VENC3 DCS RD"]
62
+ bandwidth_metrics_dict["VENC DCS WR"] = bandwidth_metrics_dict["VENC DCS WR"] + \
63
+ bandwidth_metrics_dict["VENC0 DCS WR"] + \
64
+ bandwidth_metrics_dict["VENC1 DCS WR"] + \
65
+ bandwidth_metrics_dict["VENC2 DCS WR"] + \
66
+ bandwidth_metrics_dict["VENC3 DCS WR"]
67
+ bandwidth_metrics_dict["MEDIA DCS"] = sum([
68
+ bandwidth_metrics_dict["ISP DCS RD"], bandwidth_metrics_dict["ISP DCS WR"],
69
+ bandwidth_metrics_dict["STRM CODEC DCS RD"], bandwidth_metrics_dict["STRM CODEC DCS WR"],
70
+ bandwidth_metrics_dict["PRORES DCS RD"], bandwidth_metrics_dict["PRORES DCS WR"],
71
+ bandwidth_metrics_dict["VDEC DCS RD"], bandwidth_metrics_dict["VDEC DCS WR"],
72
+ bandwidth_metrics_dict["VENC DCS RD"], bandwidth_metrics_dict["VENC DCS WR"],
73
+ bandwidth_metrics_dict["JPG DCS RD"], bandwidth_metrics_dict["JPG DCS WR"],
74
+ ])
75
+ return bandwidth_metrics_dict
76
+
77
+
78
+ def parse_cpu_metrics(powermetrics_parse):
79
+ e_core = []
80
+ p_core = []
81
+ cpu_metrics = powermetrics_parse["processor"]
82
+ cpu_metric_dict = {}
83
+ # cpu_clusters
84
+ cpu_clusters = cpu_metrics["clusters"]
85
+ e_total_idle_ratio = 0
86
+ e_core_count = 0
87
+ p_total_idle_ratio = 0
88
+ p_core_count = 0
89
+ for cluster in cpu_clusters:
90
+ name = cluster["name"]
91
+ cpu_metric_dict[name+"_freq_Mhz"] = int(cluster["freq_hz"]/(1e6))
92
+ cpu_metric_dict[name+"_active"] = int((1 - cluster["idle_ratio"])*100)
93
+ for cpu in cluster["cpus"]:
94
+ name = 'E-Cluster' if name[0] == 'E' else 'P-Cluster'
95
+ core = e_core if name[0] == 'E' else p_core
96
+ core.append(cpu["cpu"])
97
+ cpu_metric_dict[name + str(cpu["cpu"]) + "_freq_Mhz"] = int(cpu["freq_hz"] / (1e6))
98
+ cpu_metric_dict[name + str(cpu["cpu"]) + "_active"] = int((1 - cpu["idle_ratio"]) * 100)
99
+ if name[0] == 'E':
100
+ e_total_idle_ratio += cluster["down_ratio"] + (1 - cluster["down_ratio"]) * (cpu["idle_ratio"] + cpu["down_ratio"])
101
+ e_core_count += 1
102
+ else:
103
+ p_total_idle_ratio += cluster["down_ratio"] + (1 - cluster["down_ratio"]) * (cpu["idle_ratio"] + cpu["down_ratio"])
104
+ p_core_count += 1
105
+
106
+ cpu_metric_dict["E-Cluster_active"] = int((1 - e_total_idle_ratio/e_core_count)*100)
107
+ cpu_metric_dict["P-Cluster_active"] = int((1 - p_total_idle_ratio/p_core_count)*100)
108
+ cpu_metric_dict["e_core"] = e_core
109
+ cpu_metric_dict["p_core"] = p_core
110
+ if "E-Cluster_active" not in cpu_metric_dict:
111
+ # M1 Ultra
112
+ cpu_metric_dict["E-Cluster_active"] = int(
113
+ (cpu_metric_dict["E0-Cluster_active"] + cpu_metric_dict["E1-Cluster_active"])/2)
114
+ if "E-Cluster_freq_Mhz" not in cpu_metric_dict:
115
+ # M1 Ultra
116
+ cpu_metric_dict["E-Cluster_freq_Mhz"] = max(
117
+ cpu_metric_dict["E0-Cluster_freq_Mhz"], cpu_metric_dict["E1-Cluster_freq_Mhz"])
118
+ if "P-Cluster_active" not in cpu_metric_dict:
119
+ if "P2-Cluster_active" in cpu_metric_dict:
120
+ # M1 Ultra
121
+ cpu_metric_dict["P-Cluster_active"] = int((cpu_metric_dict["P0-Cluster_active"] + cpu_metric_dict["P1-Cluster_active"] +
122
+ cpu_metric_dict["P2-Cluster_active"] + cpu_metric_dict["P3-Cluster_active"]) / 4)
123
+ else:
124
+ cpu_metric_dict["P-Cluster_active"] = int(
125
+ (cpu_metric_dict["P0-Cluster_active"] + cpu_metric_dict["P1-Cluster_active"])/2)
126
+ if "P-Cluster_freq_Mhz" not in cpu_metric_dict:
127
+ if "P2-Cluster_freq_Mhz" in cpu_metric_dict:
128
+ # M1 Ultra
129
+ freqs = [
130
+ cpu_metric_dict["P0-Cluster_freq_Mhz"],
131
+ cpu_metric_dict["P1-Cluster_freq_Mhz"],
132
+ cpu_metric_dict["P2-Cluster_freq_Mhz"],
133
+ cpu_metric_dict["P3-Cluster_freq_Mhz"]]
134
+ cpu_metric_dict["P-Cluster_freq_Mhz"] = max(freqs)
135
+ else:
136
+ cpu_metric_dict["P-Cluster_freq_Mhz"] = max(
137
+ cpu_metric_dict["P0-Cluster_freq_Mhz"], cpu_metric_dict["P1-Cluster_freq_Mhz"])
138
+ # power
139
+ cpu_metric_dict["ane_W"] = cpu_metrics["ane_energy"]/1000
140
+ #cpu_metric_dict["dram_W"] = cpu_metrics["dram_energy"]/1000
141
+ cpu_metric_dict["cpu_W"] = cpu_metrics["cpu_energy"]/1000
142
+ cpu_metric_dict["gpu_W"] = cpu_metrics["gpu_energy"]/1000
143
+ cpu_metric_dict["package_W"] = cpu_metrics["combined_power"]/1000
144
+ return cpu_metric_dict
145
+
146
+
147
+ def parse_gpu_metrics(powermetrics_parse):
148
+ gpu_metrics = powermetrics_parse["gpu"]
149
+ gpu_metrics_dict = {
150
+ "freq_MHz": int(gpu_metrics["freq_hz"]),
151
+ "active": int((1 - gpu_metrics["idle_ratio"])*100),
152
+ }
153
+ return gpu_metrics_dict
154
+
155
+ def parse_disk_metrics(powermetrics_parse):
156
+ disk_metrics = powermetrics_parse["disk"]
157
+ disk_metrics_dict = {
158
+ "read_iops": int(disk_metrics["rops_per_s"]),
159
+ "write_iops": int(disk_metrics["wops_per_s"]),
160
+ "read_Bps": int(disk_metrics["rbytes_per_s"]),
161
+ "write_Bps": int(disk_metrics["wbytes_per_s"]),
162
+ }
163
+ return disk_metrics_dict
164
+
165
+ def parse_network_metrics(powermetrics_parse):
166
+ network_metrics = powermetrics_parse["network"]
167
+ network_metrics_dict = {
168
+ "out_Bps": int(network_metrics["obyte_rate"]),
169
+ "in_Bps": int(network_metrics["ibyte_rate"]),
170
+ }
171
+ return network_metrics_dict
macpm/utils.py ADDED
@@ -0,0 +1,192 @@
1
+ import os
2
+ import glob
3
+ import subprocess
4
+ from subprocess import PIPE
5
+ import psutil
6
+ from .parsers import *
7
+ import plistlib
8
+
9
+
10
+ def parse_powermetrics(path='/tmp/asitop_powermetrics', timecode="0"):
11
+ data = None
12
+ try:
13
+ with open(path+timecode, 'rb') as fp:
14
+ data = fp.read()
15
+ data = data.split(b'\x00')
16
+ powermetrics_parse = plistlib.loads(data[-1])
17
+ thermal_pressure = parse_thermal_pressure(powermetrics_parse)
18
+ cpu_metrics_dict = parse_cpu_metrics(powermetrics_parse)
19
+ gpu_metrics_dict = parse_gpu_metrics(powermetrics_parse)
20
+ disk_metrics_dict = parse_disk_metrics(powermetrics_parse)
21
+ network_metrics_dict = parse_network_metrics(powermetrics_parse)
22
+ #bandwidth_metrics = parse_bandwidth_metrics(powermetrics_parse)
23
+ bandwidth_metrics = None
24
+ timestamp = powermetrics_parse["timestamp"]
25
+ return cpu_metrics_dict, gpu_metrics_dict, thermal_pressure, bandwidth_metrics, disk_metrics_dict, network_metrics_dict, timestamp
26
+ except Exception as e:
27
+ if data:
28
+ if len(data) > 1:
29
+ powermetrics_parse = plistlib.loads(data[-2])
30
+ thermal_pressure = parse_thermal_pressure(powermetrics_parse)
31
+ cpu_metrics_dict = parse_cpu_metrics(powermetrics_parse)
32
+ gpu_metrics_dict = parse_gpu_metrics(powermetrics_parse)
33
+ disk_metrics_dict = parse_disk_metrics(powermetrics_parse)
34
+ network_metrics_dict = parse_network_metrics(powermetrics_parse)
35
+ #bandwidth_metrics = parse_bandwidth_metrics(powermetrics_parse)
36
+ bandwidth_metrics = None
37
+ timestamp = powermetrics_parse["timestamp"]
38
+ return cpu_metrics_dict, gpu_metrics_dict, thermal_pressure, bandwidth_metrics, disk_metrics_dict, network_metrics_dict, timestamp
39
+ return False
40
+
41
+
42
+ def clear_console():
43
+ command = 'clear'
44
+ os.system(command)
45
+
46
+
47
+ def convert_to_GB(value):
48
+ return round(value/1024/1024/1024, 1)
49
+
50
+
51
+ def run_powermetrics_process(timecode, nice=10, interval=1000):
52
+ #ver, *_ = platform.mac_ver()
53
+ #major_ver = int(ver.split(".")[0])
54
+ #for tmpf in glob.glob("/tmp/asitop_powermetrics*"):
55
+ # os.remove(tmpf)
56
+ output_file_flag = "-o"
57
+ command = " ".join([
58
+ "sudo nice -n",
59
+ str(nice),
60
+ "powermetrics",
61
+ "--samplers cpu_power,gpu_power,thermal,network,disk",
62
+ output_file_flag,
63
+ "/tmp/asitop_powermetrics"+timecode,
64
+ "-f plist",
65
+ "-i",
66
+ str(interval)
67
+ ])
68
+ process = subprocess.Popen(command.split(" "), stdin=PIPE, stdout=PIPE)
69
+ return process
70
+
71
+
72
+ def get_ram_metrics_dict():
73
+ ram_metrics = psutil.virtual_memory()
74
+ swap_metrics = psutil.swap_memory()
75
+ total_GB = convert_to_GB(ram_metrics.total)
76
+ free_GB = convert_to_GB(ram_metrics.available)
77
+ used_GB = convert_to_GB(ram_metrics.total-ram_metrics.available)
78
+ swap_total_GB = convert_to_GB(swap_metrics.total)
79
+ swap_used_GB = convert_to_GB(swap_metrics.used)
80
+ swap_free_GB = convert_to_GB(swap_metrics.total-swap_metrics.used)
81
+ if swap_total_GB > 0:
82
+ swap_free_percent = int(100-(swap_free_GB/swap_total_GB*100))
83
+ else:
84
+ swap_free_percent = None
85
+ ram_metrics_dict = {
86
+ "total_GB": round(total_GB, 1),
87
+ "free_GB": round(free_GB, 1),
88
+ "used_GB": round(used_GB, 1),
89
+ "free_percent": int(100-(ram_metrics.available/ram_metrics.total*100)),
90
+ "swap_total_GB": swap_total_GB,
91
+ "swap_used_GB": swap_used_GB,
92
+ "swap_free_GB": swap_free_GB,
93
+ "swap_free_percent": swap_free_percent,
94
+ }
95
+ return ram_metrics_dict
96
+
97
+
98
+ def get_cpu_info():
99
+ cpu_info = os.popen('sysctl -a | grep machdep.cpu').read()
100
+ cpu_info_lines = cpu_info.split("\n")
101
+ data_fields = ["machdep.cpu.brand_string", "machdep.cpu.core_count"]
102
+ cpu_info_dict = {}
103
+ for l in cpu_info_lines:
104
+ for h in data_fields:
105
+ if h in l:
106
+ value = l.split(":")[1].strip()
107
+ cpu_info_dict[h] = value
108
+ return cpu_info_dict
109
+
110
+
111
+ def get_core_counts():
112
+ cores_info = os.popen('sysctl -a | grep hw.perflevel').read()
113
+ cores_info_lines = cores_info.split("\n")
114
+ data_fields = ["hw.perflevel0.logicalcpu", "hw.perflevel1.logicalcpu"]
115
+ cores_info_dict = {}
116
+ for l in cores_info_lines:
117
+ for h in data_fields:
118
+ if h in l:
119
+ value = int(l.split(":")[1].strip())
120
+ cores_info_dict[h] = value
121
+ return cores_info_dict
122
+
123
+
124
+ def get_gpu_cores():
125
+ try:
126
+ cores = os.popen(
127
+ "system_profiler -detailLevel basic SPDisplaysDataType | grep 'Total Number of Cores'").read()
128
+ cores = int(cores.split(": ")[-1])
129
+ except:
130
+ cores = "?"
131
+ return cores
132
+
133
+
134
+ def get_soc_info():
135
+ cpu_info_dict = get_cpu_info()
136
+ core_counts_dict = get_core_counts()
137
+ try:
138
+ e_core_count = core_counts_dict["hw.perflevel1.logicalcpu"]
139
+ p_core_count = core_counts_dict["hw.perflevel0.logicalcpu"]
140
+ except:
141
+ e_core_count = "?"
142
+ p_core_count = "?"
143
+ soc_info = {
144
+ "name": cpu_info_dict["machdep.cpu.brand_string"],
145
+ "core_count": int(cpu_info_dict["machdep.cpu.core_count"]),
146
+ "cpu_max_power": None,
147
+ "gpu_max_power": None,
148
+ "cpu_max_bw": None,
149
+ "gpu_max_bw": None,
150
+ "e_core_count": e_core_count,
151
+ "p_core_count": p_core_count,
152
+ "gpu_core_count": get_gpu_cores()
153
+ }
154
+ # TDP (power)
155
+ if soc_info["name"] == "Apple M1 Max":
156
+ soc_info["cpu_max_power"] = 30
157
+ soc_info["gpu_max_power"] = 60
158
+ elif soc_info["name"] == "Apple M1 Pro":
159
+ soc_info["cpu_max_power"] = 30
160
+ soc_info["gpu_max_power"] = 30
161
+ elif soc_info["name"] == "Apple M1":
162
+ soc_info["cpu_max_power"] = 20
163
+ soc_info["gpu_max_power"] = 20
164
+ elif soc_info["name"] == "Apple M1 Ultra":
165
+ soc_info["cpu_max_power"] = 60
166
+ soc_info["gpu_max_power"] = 120
167
+ elif soc_info["name"] == "Apple M2":
168
+ soc_info["cpu_max_power"] = 25
169
+ soc_info["gpu_max_power"] = 15
170
+ else:
171
+ soc_info["cpu_max_power"] = 20
172
+ soc_info["gpu_max_power"] = 20
173
+ # bandwidth
174
+ if soc_info["name"] == "Apple M1 Max":
175
+ soc_info["cpu_max_bw"] = 250
176
+ soc_info["gpu_max_bw"] = 400
177
+ elif soc_info["name"] == "Apple M1 Pro":
178
+ soc_info["cpu_max_bw"] = 200
179
+ soc_info["gpu_max_bw"] = 200
180
+ elif soc_info["name"] == "Apple M1":
181
+ soc_info["cpu_max_bw"] = 70
182
+ soc_info["gpu_max_bw"] = 70
183
+ elif soc_info["name"] == "Apple M1 Ultra":
184
+ soc_info["cpu_max_bw"] = 500
185
+ soc_info["gpu_max_bw"] = 800
186
+ elif soc_info["name"] == "Apple M2":
187
+ soc_info["cpu_max_bw"] = 100
188
+ soc_info["gpu_max_bw"] = 100
189
+ else:
190
+ soc_info["cpu_max_bw"] = 70
191
+ soc_info["gpu_max_bw"] = 70
192
+ return soc_info
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Timothy Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: macpm
3
+ Version: 0.12
4
+ Summary: Performance monitoring CLI tool for Apple Silicon
5
+ Home-page: https://github.com/visualcjy/macpm
6
+ Author: Jinyu Chen
7
+ Author-email: visualcjy@mail.com
8
+ License: MIT
9
+ Keywords: macpm
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: MacOS
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: dashing
16
+ Requires-Dist: psutil
17
+ Requires-Dist: humanize
18
+
19
+ Performance monitoring CLI tool for Apple Silicon
@@ -0,0 +1,10 @@
1
+ macpm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ macpm/macpm.py,sha256=79tJ3aKn0MsllsqGBbJYuww6BlEu0TmFlvHPoeGT0SE,22932
3
+ macpm/parsers.py,sha256=7bQBu2aB3Xr15vlme7p3zBTXWCJPGn1t3VbB5DzLxns,8377
4
+ macpm/utils.py,sha256=6TxLpjiBL42oNn5C2hhLTpbPfy4581y_X-uhXD4AAt0,6905
5
+ macpm-0.12.dist-info/LICENSE,sha256=wodQcAwhBojn-AtjrZKnmoddrO3lZpb7_pNJ6vjn-jg,1068
6
+ macpm-0.12.dist-info/METADATA,sha256=6uQuGCR0zvWs2YvSK8mcrn4LTC926abD4tWB-kBIcf4,553
7
+ macpm-0.12.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
8
+ macpm-0.12.dist-info/entry_points.txt,sha256=nNKPrRSGAesaujANSKbu1cAnZx0DPtsenwIV1nxrhQo,43
9
+ macpm-0.12.dist-info/top_level.txt,sha256=Dmex3G-IgZRB5zrNjm5n2jXUNcWVOcmQsSTZB3ElyYY,6
10
+ macpm-0.12.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ macpm = macpm.macpm:main
@@ -0,0 +1 @@
1
+ macpm