From 77571a29fd7e6d41a5c15da9f7fdf10e1e63e752 Mon Sep 17 00:00:00 2001 From: melt Date: Sun, 10 May 2026 11:02:29 +0100 Subject: [PATCH 1/9] feat: add collapsable output (wip) --- src/mdb/mdb_shell.py | 3 ++- src/mdb/utils.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/mdb/mdb_shell.py b/src/mdb/mdb_shell.py index 657c2b2..9713914 100644 --- a/src/mdb/mdb_shell.py +++ b/src/mdb/mdb_shell.py @@ -24,6 +24,7 @@ parse_ranks, pretty_print_response, sort_debug_response, + reduce_response, ) if TYPE_CHECKING: @@ -214,7 +215,7 @@ def ask_remain_calm(signame: str) -> None: if command_response.msg_type == "exchange_command_response": response = sort_debug_response(command_response.data["results"]) - pretty_print_response(response) + reduce_response(response) else: print("Received unexpected message type: %s", command_response.msg_type) return diff --git a/src/mdb/utils.py b/src/mdb/utils.py index 3ac7f88..6a45de1 100644 --- a/src/mdb/utils.py +++ b/src/mdb/utils.py @@ -4,6 +4,7 @@ import re from os.path import expanduser from typing import TYPE_CHECKING +from collections import defaultdict if TYPE_CHECKING: from .backend import DebugBackend @@ -21,6 +22,29 @@ def sort_debug_response(results: dict[int, str]) -> dict[int, str]: return dict(sorted(results.items())) +def collapse_ranges(values: list[int]) -> str: + """Collapse a list of integers into minimal range notation. + + e.g. [1, 2, 3, 6, 7, 10, 11, 12] -> '1-3,6-7,10-12' + """ + sorted_vals = sorted(set(values)) + if not sorted_vals: + return "" + + ranges = [] + start = end = sorted_vals[0] + + for n in sorted_vals[1:]: + if n == end + 1: + end = n + else: + ranges.append(f"{start}-{end}" if start != end else str(start)) + start = end = n + + ranges.append(f"{start}-{end}" if start != end else str(start)) + return ",".join(ranges) + + def pretty_print_response(response: dict[int, str]) -> None: lines = [] for rank, result in response.items(): @@ -30,6 +54,23 @@ def pretty_print_response(response: dict[int, str]) -> None: print(combined_output) +def reduce_response(response: dict[int, str]) -> None: + """ + """ + reduced = defaultdict(list) + for rank, result in response.items(): + if result: + for line in result.split("\r\n")[1:-1]: + reduced[line].append(rank) + + reduced = {k: collapse_ranges(v) for k, v in reduced.items()} + + max_len = max([len(v) for v in reduced.values()], default=0) + for line, ranks_str in reduced.items(): + padded = ranks_str.rjust(max_len) + print(f"{padded}: {line}") + + def extract_float(line: str, backend: "DebugBackend") -> float: float_regex = backend.float_regex line = strip_control_characters(line) From 963d1a9d594a128fb88ea2a61e9387102e1e9c5a Mon Sep 17 00:00:00 2001 From: melt Date: Sun, 10 May 2026 18:40:04 +0100 Subject: [PATCH 2/9] feat: allow user to switch between pretty and reduced output --- src/mdb/mdb_shell.py | 39 ++++++++++++++++++++++++++++++++++++++- src/mdb/utils.py | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/mdb/mdb_shell.py b/src/mdb/mdb_shell.py index 9713914..c7393c1 100644 --- a/src/mdb/mdb_shell.py +++ b/src/mdb/mdb_shell.py @@ -56,6 +56,7 @@ def __init__(self, shell_opts: ShellOpts, client: Client) -> None: self.exchange_select = parse_ranks(self.exchange_select_str) self.select_str = self.exchange_select_str self.select = self.exchange_select + self.output_mode = "reduce" # 'pretty' or 'reduce' backend_name = shell_opts["backend_name"].lower() if backend_name in backends: self.backend = backends[backend_name]() @@ -215,7 +216,10 @@ def ask_remain_calm(signame: str) -> None: if command_response.msg_type == "exchange_command_response": response = sort_debug_response(command_response.data["results"]) - reduce_response(response) + if self.output_mode == "reduce": + reduce_response(response) + else: + pretty_print_response(response) else: print("Received unexpected message type: %s", command_response.msg_type) return @@ -251,6 +255,39 @@ def do_shell(self, line: str) -> None: run(split(line)) return + def do_set(self, line: str) -> None: + """ + Description: + Set mdb options. + + Example: + Switch output format between pretty-printed and reduced (deduplicated) mode: + + (mdb) set output pretty + (mdb) set output reduce + + Show current settings: + + (mdb) set + """ + if not line: + print(f"output: {self.output_mode}") + return + + parts = line.split() + if len(parts) < 2: + print("Usage: set output [pretty|reduce]") + return + + if parts[0].lower() == "output": + mode = parts[1].lower() + if mode in ("pretty", "reduce"): + self.output_mode = mode + else: + print(f"Error: unknown output mode '{mode}'. Use 'pretty' or 'reduce'.") + else: + print(f"Error: unknown option '{parts[0]}'. Use 'output'.") + def do_select(self, line: str) -> None: """ Description: diff --git a/src/mdb/utils.py b/src/mdb/utils.py index 6a45de1..5dac63b 100644 --- a/src/mdb/utils.py +++ b/src/mdb/utils.py @@ -55,7 +55,37 @@ def pretty_print_response(response: dict[int, str]) -> None: def reduce_response(response: dict[int, str]) -> None: - """ + """Reduce debug output by deduplicating common lines across ranks. + + Parses each rank's output, groups identical lines together, and prints + each unique line prefixed by the collapsed set of ranks that produced it. + Lines from only one rank appear normally; shared lines are printed once + with all contributing ranks. + + Example output before and after: + + Before (raw output per rank): + + 0: process 45402 + 0: cmdline = '/mdb/examples/simple-mpi.exe' + 0: cwd = '/mdb' + 0: exe = '/mdb/examples/simple-mpi.exe' + ************************************************************************ + 1: process 45403 + 1: cmdline = '/mdb/examples/simple-mpi.exe' + 1: cwd = '/mdb' + 1: exe = '/mdb/examples/simple-mpi.exe' + + After (deduplicated, ranks collapsed): + + 0: process 45402 + 0-1: cmdline = '/mdb/examples/simple-mpi.exe' + 0-1: cwd = '/mdb' + 0-1: exe = '/mdb/examples/simple-mpi.exe' + 1: process 45403 + + Args: + response: dict mapping process rank (int) to its output string. """ reduced = defaultdict(list) for rank, result in response.items(): @@ -91,7 +121,7 @@ def extract_float(line: str, backend: "DebugBackend") -> float: def prepend_ranks(rank: int, result: str) -> str: return "".join( - [f"{rank}:\t" + line + "\r\n" for line in result.split("\r\n")[1:-1]] + [f"{rank}: " + line + "\r\n" for line in result.split("\r\n")[1:-1]] ) From bf3710a9349789d587143d203f0d4e3e4673f19e Mon Sep 17 00:00:00 2001 From: melt Date: Sun, 10 May 2026 18:40:39 +0100 Subject: [PATCH 3/9] test: add new test and update existing tests to reflect new reduce output --- tests/output/answer-gdb.stdout | 77 +++++++----------- tests/output/answer-lldb.stdout | 138 ++++++++++++-------------------- tests/test_utils.py | 79 +++++++++++++++++- 3 files changed, 159 insertions(+), 135 deletions(-) diff --git a/tests/output/answer-gdb.stdout b/tests/output/answer-gdb.stdout index ac72227..e6e9cb1 100644 --- a/tests/output/answer-gdb.stdout +++ b/tests/output/answer-gdb.stdout @@ -1,57 +1,36 @@ hello -0:cmdline = simple-mpi.exe -0:cwd = [mdb root] -0:exe = simple-mpi.exe -************************************************************************ -1:cmdline = simple-mpi.exe -1:cwd = [mdb root] -1:exe = simple-mpi.exe -0:Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. -************************************************************************ -1:Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. -0:Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. -************************************************************************ -1:Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. -0:Continuing. +0-1: cmdline = simple-mpi.exe +0-1: cwd = [mdb root] +0-1: exe = simple-mpi.exe +0-1: Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. +0-1: Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. +0-1: Continuing. +0-1: +0-1: Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 +0-1: 15 var = 10.*process_rank +0: Continuing. 0: -0:Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 -0:15 var = 10.*process_rank -************************************************************************ -1:Continuing. -1: -1:Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 -1:15 var = 10.*process_rank -0:Continuing. -0: -0:Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 -0:17 if (process_rank == 0) then -0:#0 simple () at simple-mpi.f90:17 -************************************************************************ -1:#0 simple () at simple-mpi.f90:15 +0: Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 +0: 17 if (process_rank == 0) then +0: #0 simple () at simple-mpi.f90:17 +1: #0 simple () at simple-mpi.f90:15 unrecognized command [made-up-command]. Type help to find out list of possible commands. Error: user specified option [select] must be subset of available ranks (check mdb launch command). select = [10] but available ranks are [0-1]. -1:Continuing. +1: Continuing. 1: -1:Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 -1:17 if (process_rank == 0) then +1: Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 +1: 17 if (process_rank == 0) then File [deliberately-missing-file.mdb] not found. Please check the file exists and try again. -0:25 -************************************************************************ -1:25 -0:Continuing. -0: 1 s... -0: 2 s... -0: 3 s... -0: in level 1 -0: in level 2 -0: internal process: 0 of 2 -0: var = 0.00000000 -************************************************************************ -1:Continuing. -1: in level 1 -1: in level 2 -1: internal process: 1 of 2 -1: var = 10.0000000 -************************************************************************ +0-1: 25 +0-1: Continuing. + 0: 1 s... + 0: 2 s... + 0: 3 s... +0-1: in level 1 +0-1: in level 2 + 0: internal process: 0 of 2 + 0: var = 0.00000000 + 1: internal process: 1 of 2 + 1: var = 10.0000000 exiting mdb... \ No newline at end of file diff --git a/tests/output/answer-lldb.stdout b/tests/output/answer-lldb.stdout index ca6d5fb..82b0433 100644 --- a/tests/output/answer-lldb.stdout +++ b/tests/output/answer-lldb.stdout @@ -1,89 +1,57 @@ hello -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 -0: 21 int process_rank, size_of_cluster; -0: 22 float var; -0: 23 -0:-> 24 var = 0.; -0: ^ -0: 25 -0: 26 MPI_Init(NULL, NULL); -0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -************************************************************************ -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 -1: 21 int process_rank, size_of_cluster; -1: 22 float var; -1: 23 -1:-> 24 var = 0.; -1: ^ -1: 25 -1: 26 MPI_Init(NULL, NULL); -1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -0:Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] -************************************************************************ -1:Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] -0:Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] -************************************************************************ -1:Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 -0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -0: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); -0: 29 -0:-> 30 var = 10.*process_rank; -0: ^ -0: 31 -0: 32 if (process_rank == 0){ -************************************************************************ -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 -1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -1: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); -1: 29 -1:-> 30 var = 10.*process_rank; -1: ^ -1: 31 -1: 32 if (process_rank == 0){ -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 -0: 29 -0: 30 var = 10.*process_rank; -0: 31 -0:-> 32 if (process_rank == 0){ -0: ^ -0: 34 -0: 35 for (int i = 0; i < 3; ++i) { -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 -************************************************************************ -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 +0-1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 +0-1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 +0-1: 21 int process_rank, size_of_cluster; +0-1: 22 float var; +0-1: 23 +0-1: -> 24 var = 0.; +0-1: ^ +0-1: 25 +0-1: 26 MPI_Init(NULL, NULL); +0-1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +0-1: Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] +0-1: Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] +0-1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 +0-1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 +0-1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +0-1: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); +0-1: 29 +0-1: -> 30 var = 10.*process_rank; +0-1: ^ +0-1: 31 +0-1: 32 if (process_rank == 0){ +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 +0: 29 +0: 30 var = 10.*process_rank; +0: 31 +0: -> 32 if (process_rank == 0){ +0: ^ +0: 34 +0: 35 for (int i = 0; i < 3; ++i) { +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 unrecognized command [made-up-command]. Type help to find out list of possible commands. -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 -1: 29 -1: 30 var = 10.*process_rank; -1: 31 -1:-> 32 if (process_rank == 0){ -1: ^ -1: 34 -1: 35 for (int i = 0; i < 3; ++i) { +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 +1: 29 +1: 30 var = 10.*process_rank; +1: 31 +1: -> 32 if (process_rank == 0){ +1: ^ +1: 34 +1: 35 for (int i = 0; i < 3; ++i) { File [deliberately-missing-file.mdb] not found. Please check the file exists and try again. -0:(int) 25 -************************************************************************ -1:(int) 25 -0:0 s... -0:1 s... -0:2 s... -0:in level 1 -0:in level 2 -0:internal process: 0 of 2 -0:var = 0 -************************************************************************ -1:in level 1 -1:in level 2 -1:internal process: 1 of 2 -1:var = 10 -************************************************************************ +0-1: (int) 25 + 0: 0 s... + 0: 1 s... + 0: 2 s... +0-1: in level 1 +0-1: in level 2 + 0: internal process: 0 of 2 + 0: var = 0 + 1: internal process: 1 of 2 + 1: var = 10 exiting mdb... \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 8dc9cac..a840e00 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,14 @@ # Copyright 2023-2026 Tom Meltzer. See the top-level COPYRIGHT file for # details. -from mdb.utils import parse_ranks, strip_bracketted_paste, strip_control_characters +from mdb.utils import ( + collapse_ranges, + parse_ranks, + pretty_print_response, + reduce_response, + strip_bracketted_paste, + strip_control_characters, +) def test_parse_ranks() -> None: @@ -18,6 +25,76 @@ def test_parse_ranks() -> None: assert ranks == [2, 3, 4] +def test_collapse_ranges() -> None: + # consecutive integers collapse to a range + assert collapse_ranges([1, 2, 3, 4]) == "1-4" + + # single integer + assert collapse_ranges([5]) == "5" + + # multiple disjoint ranges + assert collapse_ranges([1, 2, 3, 6, 7, 10, 11, 12]) == "1-3,6-7,10-12" + + # unsorted input is sorted internally + assert collapse_ranges([12, 10, 11, 7, 6, 3, 2, 1]) == "1-3,6-7,10-12" + + # duplicates are deduplicated + assert collapse_ranges([1, 1, 2, 2, 3]) == "1-3" + + # empty list returns empty string + assert collapse_ranges([]) == "" + + # gaps between single values + assert collapse_ranges([1, 3, 5, 7]) == "1,3,5,7" + + # mix of single values and ranges + assert collapse_ranges([1, 2, 5, 6, 7, 10]) == "1-2,5-7,10" + + +def test_pretty_print_response(capsys) -> None: + response = { + 0: "command ran\r\nline 1\r\nline 2\r\n", + 1: "command ran\r\nline 3\r\n", + } + pretty_print_response(response) + captured = capsys.readouterr() + + assert "0: line 1" in captured.out + assert "0: line 2" in captured.out + assert "1: line 3" in captured.out + assert "*" * 72 in captured.out # separator between ranks + # header lines are skipped + assert "command ran" not in captured.out + + +def test_reduce_response(capsys) -> None: + response = { + 0: "header\r\ncommon line\r\nunique to 0\r\n", + 1: "header\r\ncommon line\r\nunique to 1\r\n", + 2: "header\r\ncommon line\r\n", + } + reduce_response(response) + captured = capsys.readouterr() + + # shared line should be prefixed with collapsed rank range + assert "0-2: common line" in captured.out + # unique lines should show only their rank + assert " 0: unique to 0" in captured.out + assert " 1: unique to 1" in captured.out + # header line is skipped + assert "header" not in captured.out + + +def test_reduce_response_single_rank(capsys) -> None: + response = { + 3: "header\r\nonly line\r\n", + } + reduce_response(response) + captured = capsys.readouterr() + + assert "3: only line" in captured.out + + def test_strip_functions() -> None: text = "bt\r\n\x1b[?2004l\r#0 \x1b[33msimple\x1b[m () at \x1b[32msimple-mpi.f90\x1b[m:1\r\n\x1b[?2004h" text = strip_bracketted_paste(text) From 5bac4de3be62d5828180b225b206d63d9992f566 Mon Sep 17 00:00:00 2001 From: melt Date: Wed, 27 May 2026 15:16:55 +0100 Subject: [PATCH 4/9] feat: switch default back to pretty print and rename options --- src/mdb/mdb_shell.py | 19 ++++--- tests/output/answer-gdb.stdout | 61 ++++++++++++++------- tests/output/answer-lldb.stdout | 94 ++++++++++++++++++++++----------- 3 files changed, 115 insertions(+), 59 deletions(-) diff --git a/src/mdb/mdb_shell.py b/src/mdb/mdb_shell.py index c7393c1..94230d4 100644 --- a/src/mdb/mdb_shell.py +++ b/src/mdb/mdb_shell.py @@ -56,7 +56,7 @@ def __init__(self, shell_opts: ShellOpts, client: Client) -> None: self.exchange_select = parse_ranks(self.exchange_select_str) self.select_str = self.exchange_select_str self.select = self.exchange_select - self.output_mode = "reduce" # 'pretty' or 'reduce' + self.output_mode = "separate" # 'separate' or 'combined' backend_name = shell_opts["backend_name"].lower() if backend_name in backends: self.backend = backends[backend_name]() @@ -216,7 +216,7 @@ def ask_remain_calm(signame: str) -> None: if command_response.msg_type == "exchange_command_response": response = sort_debug_response(command_response.data["results"]) - if self.output_mode == "reduce": + if self.output_mode == "combined": reduce_response(response) else: pretty_print_response(response) @@ -261,10 +261,13 @@ def do_set(self, line: str) -> None: Set mdb options. Example: - Switch output format between pretty-printed and reduced (deduplicated) mode: + Switch output format between separate and combined mode: - (mdb) set output pretty - (mdb) set output reduce + (mdb) set output combined + (mdb) set output separate + + - separate shows all output for each rank separate by ***'s + - combined reduces common output across the ranks Show current settings: @@ -276,15 +279,15 @@ def do_set(self, line: str) -> None: parts = line.split() if len(parts) < 2: - print("Usage: set output [pretty|reduce]") + print("Usage: set output [separate|combined]") return if parts[0].lower() == "output": mode = parts[1].lower() - if mode in ("pretty", "reduce"): + if mode in ("separate", "combined"): self.output_mode = mode else: - print(f"Error: unknown output mode '{mode}'. Use 'pretty' or 'reduce'.") + print(f"Error: unknown output mode '{mode}'. Use 'separate' or 'combined'.") else: print(f"Error: unknown option '{parts[0]}'. Use 'output'.") diff --git a/tests/output/answer-gdb.stdout b/tests/output/answer-gdb.stdout index e6e9cb1..22a9a2f 100644 --- a/tests/output/answer-gdb.stdout +++ b/tests/output/answer-gdb.stdout @@ -1,18 +1,32 @@ hello -0-1: cmdline = simple-mpi.exe -0-1: cwd = [mdb root] -0-1: exe = simple-mpi.exe -0-1: Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. -0-1: Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. -0-1: Continuing. -0-1: -0-1: Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 -0-1: 15 var = 10.*process_rank +0: cmdline = simple-mpi.exe +0: cwd = [mdb root] +0: exe = simple-mpi.exe +************************************************************************ +1: cmdline = simple-mpi.exe +1: cwd = [mdb root] +1: exe = simple-mpi.exe +0: Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. +************************************************************************ +1: Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. +0: Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. +************************************************************************ +1: Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. +0: Continuing. +0: +0: Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 +0: 15 var = 10.*process_rank +************************************************************************ +1: Continuing. +1: +1: Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 +1: 15 var = 10.*process_rank 0: Continuing. 0: 0: Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 0: 17 if (process_rank == 0) then 0: #0 simple () at simple-mpi.f90:17 +************************************************************************ 1: #0 simple () at simple-mpi.f90:15 unrecognized command [made-up-command]. Type help to find out list of possible commands. Error: user specified option [select] must be subset of available ranks (check mdb launch command). @@ -22,15 +36,22 @@ select = [10] but available ranks are [0-1]. 1: Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 1: 17 if (process_rank == 0) then File [deliberately-missing-file.mdb] not found. Please check the file exists and try again. -0-1: 25 -0-1: Continuing. - 0: 1 s... - 0: 2 s... - 0: 3 s... -0-1: in level 1 -0-1: in level 2 - 0: internal process: 0 of 2 - 0: var = 0.00000000 - 1: internal process: 1 of 2 - 1: var = 10.0000000 +0: 25 +************************************************************************ +1: 25 +0: Continuing. +0: 1 s... +0: 2 s... +0: 3 s... +0: in level 1 +0: in level 2 +0: internal process: 0 of 2 +0: var = 0.00000000 +************************************************************************ +1: Continuing. +1: in level 1 +1: in level 2 +1: internal process: 1 of 2 +1: var = 10.0000000 +************************************************************************ exiting mdb... \ No newline at end of file diff --git a/tests/output/answer-lldb.stdout b/tests/output/answer-lldb.stdout index 82b0433..292e036 100644 --- a/tests/output/answer-lldb.stdout +++ b/tests/output/answer-lldb.stdout @@ -1,25 +1,50 @@ hello -0-1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 -0-1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 -0-1: 21 int process_rank, size_of_cluster; -0-1: 22 float var; -0-1: 23 -0-1: -> 24 var = 0.; -0-1: ^ -0-1: 25 -0-1: 26 MPI_Init(NULL, NULL); -0-1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -0-1: Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] -0-1: Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] -0-1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 -0-1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 -0-1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -0-1: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); -0-1: 29 -0-1: -> 30 var = 10.*process_rank; -0-1: ^ -0-1: 31 -0-1: 32 if (process_rank == 0){ +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 +0: 21 int process_rank, size_of_cluster; +0: 22 float var; +0: 23 +0: -> 24 var = 0.; +0: ^ +0: 25 +0: 26 MPI_Init(NULL, NULL); +0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +************************************************************************ +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 +1: 21 int process_rank, size_of_cluster; +1: 22 float var; +1: 23 +1: -> 24 var = 0.; +1: ^ +1: 25 +1: 26 MPI_Init(NULL, NULL); +1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +0: Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] +************************************************************************ +1: Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] +0: Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] +************************************************************************ +1: Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 +0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +0: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); +0: 29 +0: -> 30 var = 10.*process_rank; +0: ^ +0: 31 +0: 32 if (process_rank == 0){ +************************************************************************ +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 +1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +1: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); +1: 29 +1: -> 30 var = 10.*process_rank; +1: ^ +1: 31 +1: 32 if (process_rank == 0){ 0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 0: 29 @@ -31,6 +56,7 @@ hello 0: 35 for (int i = 0; i < 3; ++i) { 0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 +************************************************************************ 1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 unrecognized command [made-up-command]. Type help to find out list of possible commands. @@ -44,14 +70,20 @@ unrecognized command [made-up-command]. Type help to find out list of possible c 1: 34 1: 35 for (int i = 0; i < 3; ++i) { File [deliberately-missing-file.mdb] not found. Please check the file exists and try again. -0-1: (int) 25 - 0: 0 s... - 0: 1 s... - 0: 2 s... -0-1: in level 1 -0-1: in level 2 - 0: internal process: 0 of 2 - 0: var = 0 - 1: internal process: 1 of 2 - 1: var = 10 +0: (int) 25 +************************************************************************ +1: (int) 25 +0: 0 s... +0: 1 s... +0: 2 s... +0: in level 1 +0: in level 2 +0: internal process: 0 of 2 +0: var = 0 +************************************************************************ +1: in level 1 +1: in level 2 +1: internal process: 1 of 2 +1: var = 10 +************************************************************************ exiting mdb... \ No newline at end of file From cc8c5fd7ffdc9972864ae416d2bf9cb70c05d9ab Mon Sep 17 00:00:00 2001 From: melt Date: Wed, 27 May 2026 15:28:13 +0100 Subject: [PATCH 5/9] docs: add tutorial for `set output` command --- docs/source/changing-output.rst | 157 ++++++++++++++++++++++++++++++++ docs/source/index.rst | 1 + 2 files changed, 158 insertions(+) create mode 100644 docs/source/changing-output.rst diff --git a/docs/source/changing-output.rst b/docs/source/changing-output.rst new file mode 100644 index 0000000..dcbfab2 --- /dev/null +++ b/docs/source/changing-output.rst @@ -0,0 +1,157 @@ +.. Copyright 2023-2026 Tom Meltzer. See the top-level COPYRIGHT file for + details. + +.. _changing_output: + +Changing the Output Mode +======================== + +When debugging across multiple ranks, ``mdb`` can produce a large amount of output. By default, +each rank's output is displayed independently, separated by divider lines. This is called *separate* +mode and is the default behaviour. + +When the output from all ranks is identical, this can be redundant. The *combined* output mode +reduces this verbosity by merging common lines across ranks into a single block, prefixing them +with a combined rank range. + +This tutorial demonstrates the difference between the two modes using the ``set output`` command. + +The Example Program +------------------- + +We will use the ``simple-mpi.exe`` binary from the ``examples/`` directory (see :ref:`quick_start` +for details on compiling it). + +Launching the Debug Target +-------------------------- + +First, launch the program with two processes:: + + $ mdb launch -n 2 -t ./simple-mpi.exe + +This starts the debug server and waits for an ``mdb attach`` connection. + +Separate Output Mode (Default) +------------------------------ + +In separate mode, each rank's output is printed independently. The output from each rank is +prefixed with its rank ID (e.g., ``0:`` or ``1:``) and blocks for different ranks are separated by +a line of asterisks (``****``). + +Create a script file ``script-separate.mdb``:: + + set output separate + broadcast start + info proc + break 15 + continue + list + continue + quit + quit + +Run it with:: + + $ mdb attach -x script-separate.mdb > separate.out + +Here is a shortened excerpt of the output from the ``info proc`` and ``list`` commands: + +**``info proc`` in separate mode:** + +.. code-block:: console + + 0: process 44900 + 0: cmdline = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 0: cwd = '/home/melt/sync/cambridge/projects/side/mdb' + 0: exe = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + ************************************************************************ + 1: process 44903 + 1: cmdline = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 1: cwd = '/home/melt/sync/cambridge/projects/side/mdb' + 1: exe = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + +Each rank's output appears in its own block, prefixed with just the rank number, separated by +``****`` dividers. + +**``list`` in separate mode:** + +.. code-block:: console + + 0: 10 + 0: 11 call mpi_init(ierror) + 0: 12 call mpi_comm_size(mpi_comm_world, size_of_cluster, ierror) + 0: 13 call mpi_comm_rank(mpi_comm_world, process_rank, ierror) + 0: 14 + 0: 15 var = 10.*process_rank + 0: 16 + 0: 17 if (process_rank == 0) then + 0: 18 print *, 'process 0 sleeping for 3s...' + 0: 19 do i = 1, 3 + ************************************************************************ + 1: 10 + 1: 11 call mpi_init(ierror) + 1: 12 call mpi_comm_size(mpi_comm_world, size_of_cluster, ierror) + 1: 13 call mpi_comm_rank(mpi_comm_world, process_rank, ierror) + 1: 14 + 1: 15 var = 10.*process_rank + 1: 16 + 1: 17 if (process_rank == 0) then + 1: 18 print *, 'process 0 sleeping for 3s...' + 1: 19 do i = 1, 3 + +Since the source code is identical across ranks, the ``list`` output is duplicated for each rank. + +Combined Output Mode +-------------------- + +In combined mode, lines that are identical across all ranks are merged and prefixed with a combined +rank range (e.g., ``0-1:``). Lines that differ between ranks are still shown per-rank with a single +rank prefix (e.g., `` 0:``). No ``****`` dividers are used. + +Create a script file ``script-combined.mdb``:: + + set output combined + broadcast start + info proc + break 15 + continue + list + continue + quit + quit + +Run it with:: + + $ mdb attach -x script-combined.mdb > combined.out + +**``info proc`` in combined mode:** + +.. code-block:: console + + 0: process 45004 + 0-1: cmdline = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 0-1: cwd = '/home/melt/sync/cambridge/projects/side/mdb' + 0-1: exe = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 1: process 45005 + +Notice how the three identical lines (``cmdline``, ``cwd``, ``exe``) are merged under a single +``0-1:`` prefix. The ``process`` line differs between ranks (different PIDs) so it is shown +individually for each rank. + +**``list`` in combined mode:** + +.. code-block:: console + + 0-1: 10 + 0-1: 11 call mpi_init(ierror) + 0-1: 12 call mpi_comm_size(mpi_comm_world, size_of_cluster, ierror) + 0-1: 13 call mpi_comm_rank(mpi_comm_world, process_rank, ierror) + 0-1: 14 + 0-1: 15 var = 10.*process_rank + 0-1: 16 + 0-1: 17 if (process_rank == 0) then + 0-1: 18 print *, 'process 0 sleeping for 3s...' + 0-1: 19 do i = 1, 3 + +Since all ranks share the same source code, every line is identical and merged into a single +compact block. This avoids the duplication seen in separate mode. diff --git a/docs/source/index.rst b/docs/source/index.rst index eccdf0e..93db5f0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,6 +19,7 @@ serial debugger backends e.g., `gdb `_ and `lldb Installation Quick Start + Changing the Output Mode Debugging AMD GPU Kernels .. toctree:: From 9942ea59715de8d846c3c270b29b9db3c521a294 Mon Sep 17 00:00:00 2001 From: melt Date: Wed, 27 May 2026 15:31:26 +0100 Subject: [PATCH 6/9] chore: bump version for v1.0.7 release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index be47452..3dce575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mdb_debugger" -version = "1.0.6" +version = "1.0.7" dependencies = [ "click==8.1.7", "matplotlib==3.8.3", From 69a9bfeb40f5cabb8f09e2a13f83e1e1f6162f2d Mon Sep 17 00:00:00 2001 From: melt Date: Wed, 27 May 2026 16:05:43 +0100 Subject: [PATCH 7/9] chore: black format fix --- src/mdb/mdb_shell.py | 4 +++- src/mdb/utils.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mdb/mdb_shell.py b/src/mdb/mdb_shell.py index 94230d4..ff26571 100644 --- a/src/mdb/mdb_shell.py +++ b/src/mdb/mdb_shell.py @@ -287,7 +287,9 @@ def do_set(self, line: str) -> None: if mode in ("separate", "combined"): self.output_mode = mode else: - print(f"Error: unknown output mode '{mode}'. Use 'separate' or 'combined'.") + print( + f"Error: unknown output mode '{mode}'. Use 'separate' or 'combined'." + ) else: print(f"Error: unknown option '{parts[0]}'. Use 'output'.") diff --git a/src/mdb/utils.py b/src/mdb/utils.py index 5dac63b..de9f34f 100644 --- a/src/mdb/utils.py +++ b/src/mdb/utils.py @@ -120,9 +120,7 @@ def extract_float(line: str, backend: "DebugBackend") -> float: def prepend_ranks(rank: int, result: str) -> str: - return "".join( - [f"{rank}: " + line + "\r\n" for line in result.split("\r\n")[1:-1]] - ) + return "".join([f"{rank}: " + line + "\r\n" for line in result.split("\r\n")[1:-1]]) def strip_bracketted_paste(text: str) -> str: From ba13c4cf9ce5f6728bae7962c6b1cf2fbe749817 Mon Sep 17 00:00:00 2001 From: melt Date: Wed, 27 May 2026 16:07:31 +0100 Subject: [PATCH 8/9] chore: fix typo in docs --- docs/source/changing-output.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/changing-output.rst b/docs/source/changing-output.rst index dcbfab2..3a69b76 100644 --- a/docs/source/changing-output.rst +++ b/docs/source/changing-output.rst @@ -128,11 +128,11 @@ Run it with:: .. code-block:: console - 0: process 45004 + 0: process 45004 0-1: cmdline = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' 0-1: cwd = '/home/melt/sync/cambridge/projects/side/mdb' 0-1: exe = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' - 1: process 45005 + 1: process 45005 Notice how the three identical lines (``cmdline``, ``cwd``, ``exe``) are merged under a single ``0-1:`` prefix. The ``process`` line differs between ranks (different PIDs) so it is shown From 70f21feb6ffeb99d5a4f712f2ba5ca32c653b6b5 Mon Sep 17 00:00:00 2001 From: melt Date: Wed, 27 May 2026 16:11:56 +0100 Subject: [PATCH 9/9] chore: fix mypy errors --- tests/test_utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index a840e00..a9401f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,8 @@ # Copyright 2023-2026 Tom Meltzer. See the top-level COPYRIGHT file for # details. +import pytest + from mdb.utils import ( collapse_ranges, parse_ranks, @@ -51,7 +53,7 @@ def test_collapse_ranges() -> None: assert collapse_ranges([1, 2, 5, 6, 7, 10]) == "1-2,5-7,10" -def test_pretty_print_response(capsys) -> None: +def test_pretty_print_response(capsys: pytest.CaptureFixture[str]) -> None: response = { 0: "command ran\r\nline 1\r\nline 2\r\n", 1: "command ran\r\nline 3\r\n", @@ -67,7 +69,7 @@ def test_pretty_print_response(capsys) -> None: assert "command ran" not in captured.out -def test_reduce_response(capsys) -> None: +def test_reduce_response(capsys: pytest.CaptureFixture[str]) -> None: response = { 0: "header\r\ncommon line\r\nunique to 0\r\n", 1: "header\r\ncommon line\r\nunique to 1\r\n", @@ -85,7 +87,7 @@ def test_reduce_response(capsys) -> None: assert "header" not in captured.out -def test_reduce_response_single_rank(capsys) -> None: +def test_reduce_response_single_rank(capsys: pytest.CaptureFixture[str]) -> None: response = { 3: "header\r\nonly line\r\n", }