amd-debug-tools 0.2.2__py3-none-any.whl → 0.2.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.
amd_debug/sleep_report.py CHANGED
@@ -1,10 +1,10 @@
1
- #!/usr/bin/python3
2
1
  # SPDX-License-Identifier: MIT
3
2
 
4
3
  import os
5
4
  import re
6
5
  import math
7
6
  from datetime import datetime, timedelta
7
+ import numpy as np
8
8
  from tabulate import tabulate
9
9
  from jinja2 import Environment, FileSystemLoader
10
10
  import pandas as pd
@@ -73,6 +73,8 @@ def format_percent(val):
73
73
 
74
74
  def format_timedelta(val):
75
75
  """Format seconds as a nicer format"""
76
+ if math.isnan(val):
77
+ val = 0
76
78
  return str(timedelta(seconds=val))
77
79
 
78
80
 
@@ -97,8 +99,23 @@ class SleepReport(AmdTool):
97
99
  self.debug = report_debug
98
100
  self.format = fmt
99
101
  self.failures = []
100
- self.df = self.db.report_summary_dataframe(self.since, self.until)
101
- self.pre_process_dataframe()
102
+ if since and until:
103
+ self.df = self.db.report_summary_dataframe(self.since, self.until)
104
+ self.pre_process_dataframe()
105
+ else:
106
+ self.df = pd.DataFrame(
107
+ columns=[
108
+ "t0",
109
+ "t1",
110
+ "requested",
111
+ "hw",
112
+ "b0",
113
+ "b1",
114
+ "full",
115
+ "wake_irq",
116
+ "gpio",
117
+ ]
118
+ )
102
119
  self.battery_svg = None
103
120
  self.hwsleep_svg = None
104
121
 
@@ -136,6 +153,7 @@ class SleepReport(AmdTool):
136
153
  self.df["Duration"] = self.df["t1"].apply(format_as_seconds) - self.df[
137
154
  "t0"
138
155
  ].apply(format_as_seconds)
156
+ self.df["Duration"] = self.df["Duration"].replace(0, np.nan)
139
157
  self.df["Hardware Sleep"] = (self.df["hw"] / self.df["Duration"]).apply(
140
158
  parse_hw_sleep
141
159
  )
@@ -187,7 +205,8 @@ class SleepReport(AmdTool):
187
205
  format_watts
188
206
  )
189
207
 
190
- def convert_gpio_dataframe(self, content):
208
+ def convert_table_dataframe(self, content):
209
+ """Convert a table like dataframe to an HTML table"""
191
210
  header = False
192
211
  rows = []
193
212
  for line in content.split("\n"):
@@ -196,17 +215,25 @@ class SleepReport(AmdTool):
196
215
  if header:
197
216
  continue
198
217
  header = True
218
+ line = line.strip("│")
219
+ line = line.replace("├─", "└─")
199
220
  if "|" in line:
200
221
  # first column missing '|'
201
222
  rows.append(line.replace("\t", "|"))
202
223
  columns = [row.split("|") for row in rows]
203
224
  df = pd.DataFrame(columns[1:], columns=columns[0])
204
- return df.to_html(index=False, table_id="gpio")
225
+ return df.to_html(index=False, justify="center", col_space=30)
205
226
 
206
227
  def get_prereq_data(self):
207
228
  """Get the prereq data"""
208
229
  prereq = []
209
230
  prereq_debug = []
231
+ tables = [
232
+ "int|active",
233
+ "ACPI name",
234
+ "PCI Slot",
235
+ "DMI|value",
236
+ ]
210
237
  ts = self.db.get_last_prereq_ts()
211
238
  if not ts:
212
239
  return [], "", []
@@ -216,23 +243,24 @@ class SleepReport(AmdTool):
216
243
  if self.debug:
217
244
  for row in self.db.report_debug(t0):
218
245
  content = row[0]
219
- if self.format == "html" and "int|active" in content:
220
- content = self.convert_gpio_dataframe(content)
221
- prereq_debug.append(
222
- {"data": "{message}".format(message=content.strip())}
223
- )
246
+ if self.format == "html" and [
247
+ table for table in tables if table in content
248
+ ]:
249
+ content = self.convert_table_dataframe(content)
250
+ prereq_debug.append({"data": f"{content.strip()}"})
224
251
  return prereq, t0, prereq_debug
225
252
 
226
253
  def get_cycle_data(self):
227
254
  """Get the cycle data"""
228
255
  cycles = []
229
256
  debug = []
257
+ tables = ["Wakeup Source"]
230
258
  num = 0
231
259
  for cycle in self.df["Start Time"]:
232
260
  if self.format == "html":
233
261
  data = ""
234
262
  for line in self.db.report_cycle_data(cycle).split("\n"):
235
- data += "<p>{line}</p>".format(line=line)
263
+ data += f"<p>{line}</p>"
236
264
  cycles.append({"cycle_num": num, "data": data})
237
265
  else:
238
266
  cycles.append([num, self.db.report_cycle_data(cycle)])
@@ -240,7 +268,12 @@ class SleepReport(AmdTool):
240
268
  messages = []
241
269
  priorities = []
242
270
  for row in self.db.report_debug(cycle):
243
- messages.append(row[0])
271
+ content = row[0]
272
+ if self.format == "html" and [
273
+ table for table in tables if table in content
274
+ ]:
275
+ content = self.convert_table_dataframe(content)
276
+ messages.append(content)
244
277
  priorities.append(get_log_priority(row[1]))
245
278
  debug.append(
246
279
  {"cycle_num": num, "messages": messages, "priorities": priorities}
@@ -265,58 +298,69 @@ class SleepReport(AmdTool):
265
298
  prereq, prereq_date, prereq_debug = self.get_prereq_data()
266
299
 
267
300
  # Load the cycle and/or debug data
268
- cycles, debug = self.get_cycle_data()
269
-
270
- self.post_process_dataframe()
271
- failures = None
272
- if self.format == "md":
273
- summary = self.df.to_markdown(floatfmt=".02f")
274
- cycle_data = tabulate(cycles, headers=["Cycle", "data"], tablefmt="pipe")
275
- if self.failures:
276
- failures = tabulate(
277
- self.failures,
278
- headers=["Cycle", "Problem", "Explanation"],
279
- tablefmt="pipe",
301
+ if not self.df.empty:
302
+ cycles, debug = self.get_cycle_data()
303
+
304
+ self.post_process_dataframe()
305
+ failures = None
306
+ if self.format == "md":
307
+ summary = self.df.to_markdown(floatfmt=".02f")
308
+ cycle_data = tabulate(
309
+ cycles, headers=["Cycle", "data"], tablefmt="pipe"
280
310
  )
281
- elif self.format == "txt":
282
- summary = tabulate(self.df, headers=self.df.columns, tablefmt="fancy_grid")
283
- cycle_data = tabulate(
284
- cycles, headers=["Cycle", "data"], tablefmt="fancy_grid"
285
- )
286
- if self.failures:
287
- failures = tabulate(
288
- self.failures,
289
- headers=["Cycle", "Problem", "Explanation"],
290
- tablefmt="fancy_grid",
311
+ if self.failures:
312
+ failures = tabulate(
313
+ self.failures,
314
+ headers=["Cycle", "Problem", "Explanation"],
315
+ tablefmt="pipe",
316
+ )
317
+ elif self.format == "txt":
318
+ summary = tabulate(
319
+ self.df, headers=self.df.columns, tablefmt="fancy_grid"
320
+ )
321
+ cycle_data = tabulate(
322
+ cycles, headers=["Cycle", "data"], tablefmt="fancy_grid"
291
323
  )
292
- elif self.format == "html":
293
- summary = ""
294
- row = 0
295
- # we will use javascript to highlight the high values
296
- for line in self.df.to_html(table_id="summary", render_links=True).split(
297
- "\n"
298
- ):
299
- if "<tr>" in line:
300
- line = line.replace(
301
- "<tr>",
302
- '<tr class="row-low" onclick="pick_summary_cycle(%d)">' % row,
324
+ if self.failures:
325
+ failures = tabulate(
326
+ self.failures,
327
+ headers=["Cycle", "Problem", "Explanation"],
328
+ tablefmt="fancy_grid",
303
329
  )
304
- row = row + 1
305
- summary += line
306
- cycle_data = cycles
307
- failures = self.failures
308
- # only show one cycle in stdout output even if we found more
309
- else:
310
- df = self.df.tail(1)
311
- summary = tabulate(
312
- df, headers=self.df.columns, tablefmt="fancy_grid", showindex=False
313
- )
314
- if cycles[-1][0] == df.index.start:
315
- cycle_data = cycles[-1][-1]
330
+ elif self.format == "html":
331
+ summary = ""
332
+ row = 0
333
+ # we will use javascript to highlight the high values
334
+ for line in self.df.to_html(
335
+ table_id="summary", render_links=True
336
+ ).split("\n"):
337
+ if "<tr>" in line:
338
+ line = line.replace(
339
+ "<tr>",
340
+ f'<tr class="row-low" onclick="pick_summary_cycle({row})">',
341
+ )
342
+ row = row + 1
343
+ summary += line
344
+ cycle_data = cycles
345
+ failures = self.failures
346
+ # only show one cycle in stdout output even if we found more
316
347
  else:
317
- cycle_data = None
318
- if self.failures and self.failures[-1][0] == df.index.start:
319
- failures = self.failures[-1][-1]
348
+ df = self.df.tail(1)
349
+ summary = tabulate(
350
+ df, headers=self.df.columns, tablefmt="fancy_grid", showindex=False
351
+ )
352
+ if cycles[-1][0] == df.index.start:
353
+ cycle_data = cycles[-1][-1]
354
+ else:
355
+ cycle_data = None
356
+ if self.failures and self.failures[-1][0] == df.index.start:
357
+ failures = self.failures[-1][-1]
358
+ else:
359
+ cycles = []
360
+ debug = []
361
+ cycle_data = []
362
+ summary = "No sleep cycles found in the database."
363
+ failures = None
320
364
 
321
365
  # let it burn
322
366
  context = {
@@ -333,7 +377,7 @@ class SleepReport(AmdTool):
333
377
  "failures": failures,
334
378
  }
335
379
  if self.fname:
336
- with open(self.fname, "w") as f:
380
+ with open(self.fname, "w", encoding="utf-8") as f:
337
381
  f.write(template.render(context))
338
382
  if "SUDO_UID" in os.environ:
339
383
  os.chown(
@@ -345,30 +389,30 @@ class SleepReport(AmdTool):
345
389
 
346
390
  def build_battery_chart(self):
347
391
  """Build a battery chart using matplotlib and seaborn"""
348
- import matplotlib.pyplot as plt
349
- import seaborn as sns
350
- import io
392
+ import matplotlib.pyplot as plt # pylint: disable=import-outside-toplevel
393
+ import seaborn as sns # pylint: disable=import-outside-toplevel
394
+ import io # pylint: disable=import-outside-toplevel
351
395
 
352
396
  if "Battery Ave Rate" not in self.df.columns:
353
397
  return
354
398
 
355
399
  plt.set_loglevel("warning")
356
- fig, ax1 = plt.subplots()
357
- lns3 = ax1.plot(
400
+ _fig, ax1 = plt.subplots()
401
+ ax1.plot(
358
402
  self.df["Battery Ave Rate"], color="green", label="Charge/Discharge Rate"
359
403
  )
360
404
 
361
405
  ax2 = ax1.twinx()
362
- lns1 = sns.barplot(
406
+ sns.barplot(
363
407
  x=self.df.index,
364
408
  y=self.df["Battery Delta"],
365
409
  color="grey",
366
410
  label="Battery Change",
367
411
  alpha=0.3,
368
412
  )
369
- max = int(len(self.df.index) / 10)
370
- if max:
371
- ax1.set_xticks(range(0, len(self.df.index), max))
413
+ max_range = int(len(self.df.index) / 10)
414
+ if max_range:
415
+ ax1.set_xticks(range(0, len(self.df.index), max_range))
372
416
  ax1.set_xlabel("Cycle")
373
417
  ax1.set_ylabel("Rate (Watts)")
374
418
  ax2.set_ylabel("Battery Change (%)")
@@ -385,20 +429,20 @@ class SleepReport(AmdTool):
385
429
 
386
430
  def build_hw_sleep_chart(self):
387
431
  """Build the hardware sleep chart using matplotlib and seaborn"""
388
- import matplotlib.pyplot as plt
389
- import seaborn as sns
390
- import io
432
+ import matplotlib.pyplot as plt # pylint: disable=import-outside-toplevel
433
+ import seaborn as sns # pylint: disable=import-outside-toplevel
434
+ import io # pylint: disable=import-outside-toplevel
391
435
 
392
436
  plt.set_loglevel("warning")
393
- fig, ax1 = plt.subplots()
394
- lns3 = ax1.plot(
437
+ _fig, ax1 = plt.subplots()
438
+ ax1.plot(
395
439
  self.df["Hardware Sleep"],
396
440
  color="red",
397
441
  label="Hardware Sleep",
398
442
  )
399
443
 
400
444
  ax2 = ax1.twinx()
401
- lns1 = sns.barplot(
445
+ sns.barplot(
402
446
  x=self.df.index,
403
447
  y=self.df["Duration"] / 60,
404
448
  color="grey",
@@ -406,9 +450,9 @@ class SleepReport(AmdTool):
406
450
  alpha=0.3,
407
451
  )
408
452
 
409
- max = int(len(self.df.index) / 10)
410
- if max:
411
- ax1.set_xticks(range(0, len(self.df.index), max))
453
+ max_range = int(len(self.df.index) / 10)
454
+ if max_range:
455
+ ax1.set_xticks(range(0, len(self.df.index), max_range))
412
456
  ax1.set_xlabel("Cycle")
413
457
  ax1.set_ylabel("Percent")
414
458
  ax2.set_yscale("log")
@@ -427,15 +471,13 @@ class SleepReport(AmdTool):
427
471
  def run(self, inc_prereq=True):
428
472
  """Run the report"""
429
473
 
430
- if self.df.empty:
431
- raise ValueError(f"No data found between {self.since} and {self.until}")
432
-
433
474
  characters = print_temporary_message("Building report, please wait...")
434
475
 
435
- # Build charts in the page for html format
436
- if len(self.df.index) > 1 and self.format == "html":
437
- self.build_battery_chart()
438
- self.build_hw_sleep_chart()
476
+ if not self.df.empty:
477
+ # Build charts in the page for html format
478
+ if len(self.df.index) > 1 and self.format == "html":
479
+ self.build_battery_chart()
480
+ self.build_hw_sleep_chart()
439
481
 
440
482
  # Render the template using jinja
441
483
  msg = self.build_template(inc_prereq)
@@ -445,7 +487,7 @@ class SleepReport(AmdTool):
445
487
  text = line.strip()
446
488
  if not text:
447
489
  continue
448
- for group in ["🗣️", "❌", "🚦", "🦟", "💯", "○"]:
490
+ for group in ["🗣️", "❌", "🚦", "🦟", "🚫", "○"]:
449
491
  if line.startswith(group):
450
492
  text = line.split(group)[-1]
451
493
  color = get_group_color(group)
amd_debug/templates/html CHANGED
@@ -42,12 +42,14 @@
42
42
  table,
43
43
  th,
44
44
  td {
45
- border-width: 0;
45
+ border-width: 1;
46
+ border-collapse: collapse;
46
47
  table-layout: fixed;
47
48
  font-family: sans-serif;
48
49
  letter-spacing: 0.02em;
49
50
  color: #000000;
50
- margin-bottom: 10px;
51
+ text-align: left;
52
+ padding: 3px;
51
53
  }
52
54
 
53
55
  .○ {
amd_debug/ttm.py ADDED
@@ -0,0 +1,157 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """TTM configuration tool"""
3
+
4
+ import asyncio
5
+ import os
6
+ import argparse
7
+ from amd_debug.common import (
8
+ AmdTool,
9
+ bytes_to_gb,
10
+ gb_to_pages,
11
+ get_system_mem,
12
+ relaunch_sudo,
13
+ print_color,
14
+ reboot,
15
+ version,
16
+ )
17
+
18
+ TTM_PARAM_PATH = "/sys/module/ttm/parameters/pages_limit"
19
+ MODPROBE_CONF_PATH = "/etc/modprobe.d/ttm.conf"
20
+ # Maximum percentage of total system memory to allow for TTM
21
+ MAX_MEMORY_PERCENTAGE = 90
22
+
23
+
24
+ def maybe_reboot() -> bool:
25
+ """Prompt to reboot system"""
26
+ response = input("Would you like to reboot the system now? (y/n): ").strip().lower()
27
+ if response in ("y", "yes"):
28
+ return reboot()
29
+ return True
30
+
31
+
32
+ class AmdTtmTool(AmdTool):
33
+ """Class for handling TTM page configuration"""
34
+
35
+ def __init__(self, logging):
36
+ log_prefix = "ttm" if logging else None
37
+ super().__init__(log_prefix)
38
+
39
+ def get(self) -> bool:
40
+ """Read current page limit"""
41
+ try:
42
+ with open(TTM_PARAM_PATH, "r", encoding="utf-8") as f:
43
+ pages = int(f.read().strip())
44
+ gb_value = bytes_to_gb(pages)
45
+ print_color(
46
+ f"Current TTM pages limit: {pages} pages ({gb_value:.2f} GB)", "💻"
47
+ )
48
+ except FileNotFoundError:
49
+ print_color(f"Error: Could not find {TTM_PARAM_PATH}", "❌")
50
+ return False
51
+
52
+ total = get_system_mem()
53
+ if total > 0:
54
+ print_color(f"Total system memory: {total:.2f} GB", "💻")
55
+
56
+ return True
57
+
58
+ def set(self, gb_value) -> bool:
59
+ """Set a new page limit"""
60
+ relaunch_sudo()
61
+
62
+ # Check against system memory
63
+ total = get_system_mem()
64
+ if total > 0:
65
+ max_recommended_gb = total * MAX_MEMORY_PERCENTAGE / 100
66
+
67
+ if gb_value > total:
68
+ print_color(
69
+ f"{gb_value:.2f} GB is greater than total system memory ({total:.2f} GB)",
70
+ "❌",
71
+ )
72
+ return False
73
+
74
+ if gb_value > max_recommended_gb:
75
+ print_color(
76
+ f"Warning: The requested value ({gb_value:.2f} GB) exceeds {MAX_MEMORY_PERCENTAGE}% of your system memory ({max_recommended_gb:.2f} GB).",
77
+ "🚦",
78
+ )
79
+ response = (
80
+ input(
81
+ "This could cause system instability. Continue anyway? (y/n): "
82
+ )
83
+ .strip()
84
+ .lower()
85
+ )
86
+ if response not in ("y", "yes"):
87
+ print_color("Operation cancelled.", "🚦")
88
+ return False
89
+
90
+ pages = gb_to_pages(gb_value)
91
+
92
+ with open(MODPROBE_CONF_PATH, "w", encoding="utf-8") as f:
93
+ f.write(f"options ttm pages_limit={pages}\n")
94
+ print_color(
95
+ f"Successfully set TTM pages limit to {pages} pages ({gb_value:.2f} GB)",
96
+ "🐧",
97
+ )
98
+ print_color(f"Configuration written to {MODPROBE_CONF_PATH}", "🐧")
99
+ print_color("NOTE: You need to reboot for changes to take effect.", "○")
100
+
101
+ return maybe_reboot()
102
+
103
+ def clear(self) -> bool:
104
+ """Clears the page limit"""
105
+ if not os.path.exists(MODPROBE_CONF_PATH):
106
+ print_color(f"{MODPROBE_CONF_PATH} doesn't exist", "❌")
107
+ return False
108
+
109
+ relaunch_sudo()
110
+
111
+ os.remove(MODPROBE_CONF_PATH)
112
+ print_color(f"Configuration {MODPROBE_CONF_PATH} removed", "🐧")
113
+
114
+ return maybe_reboot()
115
+
116
+
117
+ def parse_args():
118
+ """Parse command line arguments."""
119
+ parser = argparse.ArgumentParser(description="Manage TTM pages limit")
120
+ parser.add_argument("--set", type=float, help="Set pages limit in GB")
121
+ parser.add_argument(
122
+ "--clear", action="store_true", help="Clear a previously set page limit"
123
+ )
124
+ parser.add_argument(
125
+ "--version", action="store_true", help="Show version information"
126
+ )
127
+ parser.add_argument(
128
+ "--tool-debug",
129
+ action="store_true",
130
+ help="Enable tool debug logging",
131
+ )
132
+
133
+ return parser.parse_args()
134
+
135
+
136
+ def main() -> None | int:
137
+ """Main function"""
138
+
139
+ args = parse_args()
140
+ tool = AmdTtmTool(args.tool_debug)
141
+ ret = False
142
+
143
+ if args.version:
144
+ print(version())
145
+ return
146
+ elif args.set is not None:
147
+ if args.set <= 0:
148
+ print("Error: GB value must be greater than 0")
149
+ return 1
150
+ ret = tool.set(args.set)
151
+ elif args.clear:
152
+ ret = tool.clear()
153
+ else:
154
+ ret = tool.get()
155
+ if ret is False:
156
+ return 1
157
+ return