Skip to content

Commit 5e7d7f5

Browse files
caisqtensorflower-gardener
authored andcommitted
tfdbg CLI: add menus and mouse-triggered commands
This is the first pass at adding mouse support to tfdbg CLI. It focuses on the analyzer part of the CLI. All these changes are backward compatible, in the sense that all keyboard commands continue to work as before. * The list_tensors (lt) command now has hyperlink-like clickable text in its output, which users can use to navigate to the tensor value display with only one click. * The outputs from all analyzer commands (lt, pt, ni, li, lo) have a main menu displayed near the top, right below the title bar. The menu contain items such as list_tensors, node_info, print_tensor, which get enabled and disabled properly depending on the context. Also in this CL: * Let users print tensors using the node name (instead of the tensor name), if the node has generated only one dump from one output slot. Change: 142605017
1 parent 02b4d7a commit 5e7d7f5

12 files changed

Lines changed: 1310 additions & 239 deletions

tensorflow/g3doc/how_tos/debugger/index.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ stuck. Success!
327327

328328
## Other Features of the tfdbg Diagnostics CLI:
329329

330+
* Some commands provide clickable links and menu items in their output. These
331+
are underlined. They can help you navigate to the desired information more
332+
quickly.
330333
* Navigation through command history using the Up and Down arrow keys.
331334
Prefix-based navigation is also supported.
332335
* Tab completion of commands and some command arguments.
@@ -443,5 +446,13 @@ bazel build -c opt tensorflow/python/debug:debug_errors && \
443446
# Debugging uninitialized variable.
444447
bazel build -c opt tensorflow/python/debug:debug_errors && \
445448
bazel-bin/tensorflow/python/debug/debug_errors \
446-
-error uninitialized_variable --debug
449+
-error uninitialized_variable --debug
447450
```
451+
452+
**Q**: _Why can't I select text in the tfdbg CLI?_
453+
454+
**A**: This is because the tfdbg CLI enables mouse events in the terminal by
455+
default. This [mouse-mask](https://linux.die.net/man/3/mousemask) mode
456+
overrides default terminal interactions, including text selection. You
457+
can re-enable text selection by using the command `mouse off` or
458+
`m off`.

tensorflow/python/debug/cli/analyzer_cli.py

Lines changed: 161 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,75 @@
5151
ELLIPSIS = "..."
5252

5353

54+
def _add_main_menu(output,
55+
node_name=None,
56+
enable_list_tensors=True,
57+
enable_node_info=True,
58+
enable_print_tensor=True,
59+
enable_list_inputs=True,
60+
enable_list_outputs=True):
61+
"""Generate main menu for the screen output from a command.
62+
63+
Args:
64+
output: (debugger_cli_common.RichTextLines) the output object to modify.
65+
node_name: (str or None) name of the node involved (if any). If None,
66+
the menu items node_info, list_inputs and list_outputs will be
67+
automatically disabled, overriding the values of arguments
68+
enable_node_info, enable_list_inputs and enable_list_outputs.
69+
enable_list_tensors: (bool) whether the list_tensor menu item will be
70+
enabled.
71+
enable_node_info: (bool) whether the node_info item will be enabled.
72+
enable_print_tensor: (bool) whether the print_tensor item will be enabled.
73+
enable_list_inputs: (bool) whether the item list_inputs will be enabled.
74+
enable_list_outputs: (bool) whether the item list_outputs will be enabled.
75+
"""
76+
77+
menu = debugger_cli_common.Menu()
78+
79+
menu.append(
80+
debugger_cli_common.MenuItem(
81+
"list_tensors", "list_tensors", enabled=enable_list_tensors))
82+
83+
if node_name:
84+
menu.append(
85+
debugger_cli_common.MenuItem(
86+
"node_info",
87+
"node_info -a -d %s" % node_name,
88+
enabled=enable_node_info))
89+
menu.append(
90+
debugger_cli_common.MenuItem(
91+
"print_tensor",
92+
"print_tensor %s" % node_name,
93+
enabled=enable_print_tensor))
94+
menu.append(
95+
debugger_cli_common.MenuItem(
96+
"list_inputs",
97+
"list_inputs -c -r %s" % node_name,
98+
enabled=enable_list_inputs))
99+
menu.append(
100+
debugger_cli_common.MenuItem(
101+
"list_outputs",
102+
"list_outputs -c -r %s" % node_name,
103+
enabled=enable_list_outputs))
104+
else:
105+
menu.append(
106+
debugger_cli_common.MenuItem(
107+
"node_info", None, enabled=False))
108+
menu.append(
109+
debugger_cli_common.MenuItem("print_tensor", None, enabled=False))
110+
menu.append(
111+
debugger_cli_common.MenuItem("list_inputs", None, enabled=False))
112+
menu.append(
113+
debugger_cli_common.MenuItem("list_outputs", None, enabled=False))
114+
115+
menu.append(
116+
debugger_cli_common.MenuItem("run_info", "run_info"))
117+
menu.append(
118+
debugger_cli_common.MenuItem("help", "help"))
119+
120+
output.annotations[debugger_cli_common.MAIN_MENU_KEY] = menu
121+
122+
54123
class DebugAnalyzer(object):
55124
"""Analyzer for debug data from dump directories."""
56125

@@ -301,6 +370,7 @@ def list_tensors(self, args, screen_info=None):
301370
parsed = self._arg_parsers["list_tensors"].parse_args(args)
302371

303372
output = []
373+
font_attr_segs = {}
304374

305375
filter_strs = []
306376
if parsed.op_type_filter:
@@ -316,12 +386,16 @@ def list_tensors(self, args, screen_info=None):
316386
else:
317387
node_name_regex = None
318388

389+
filter_output = debugger_cli_common.RichTextLines(filter_strs)
390+
319391
if parsed.tensor_filter:
320392
try:
321393
filter_callable = self.get_tensor_filter(parsed.tensor_filter)
322394
except ValueError:
323-
return cli_shared.error(
324-
"There is no tensor filter named \"%s\"." % parsed.tensor_filter)
395+
output = cli_shared.error("There is no tensor filter named \"%s\"." %
396+
parsed.tensor_filter)
397+
_add_main_menu(output, node_name=None, enable_list_tensors=False)
398+
return output
325399

326400
data_to_show = self._debug_dump.find(filter_callable)
327401
else:
@@ -340,21 +414,28 @@ def list_tensors(self, args, screen_info=None):
340414
continue
341415

342416
rel_time = (dump.timestamp - self._debug_dump.t0) / 1000.0
343-
output.append("[%.3f ms] %s:%d" % (rel_time, dump.node_name,
344-
dump.output_slot))
417+
dumped_tensor_name = "%s:%d" % (dump.node_name, dump.output_slot)
418+
output.append("[%.3f ms] %s" % (rel_time, dumped_tensor_name))
419+
font_attr_segs[len(output) - 1] = [(
420+
len(output[-1]) - len(dumped_tensor_name), len(output[-1]),
421+
debugger_cli_common.MenuItem("", "pt %s" % dumped_tensor_name))]
345422
dump_count += 1
346423

347-
output.insert(0, "")
348-
349-
output = filter_strs + output
424+
filter_output.append("")
425+
filter_output.extend(debugger_cli_common.RichTextLines(
426+
output, font_attr_segs=font_attr_segs))
427+
output = filter_output
350428

351429
if parsed.tensor_filter:
352-
output.insert(0, "%d dumped tensor(s) passing filter \"%s\":" %
353-
(dump_count, parsed.tensor_filter))
430+
output.prepend([
431+
"%d dumped tensor(s) passing filter \"%s\":" %
432+
(dump_count, parsed.tensor_filter)
433+
])
354434
else:
355-
output.insert(0, "%d dumped tensor(s):" % dump_count)
435+
output.prepend(["%d dumped tensor(s):" % dump_count])
356436

357-
return debugger_cli_common.RichTextLines(output)
437+
_add_main_menu(output, node_name=None, enable_list_tensors=False)
438+
return output
358439

359440
def node_info(self, args, screen_info=None):
360441
"""Command handler for node_info.
@@ -383,8 +464,16 @@ def node_info(self, args, screen_info=None):
383464
parsed.node_name)
384465

385466
if not self._debug_dump.node_exists(node_name):
386-
return cli_shared.error(
467+
output = cli_shared.error(
387468
"There is no node named \"%s\" in the partition graphs" % node_name)
469+
_add_main_menu(
470+
output,
471+
node_name=None,
472+
enable_list_tensors=True,
473+
enable_node_info=False,
474+
enable_list_inputs=False,
475+
enable_list_outputs=False)
476+
return output
388477

389478
# TODO(cais): Provide UI glossary feature to explain to users what the
390479
# term "partition graph" means and how it is related to TF graph objects
@@ -422,7 +511,9 @@ def node_info(self, args, screen_info=None):
422511
if parsed.dumps:
423512
lines.extend(self._list_node_dumps(node_name))
424513

425-
return debugger_cli_common.RichTextLines(lines)
514+
output = debugger_cli_common.RichTextLines(lines)
515+
_add_main_menu(output, node_name=node_name, enable_node_info=False)
516+
return output
426517

427518
def list_inputs(self, args, screen_info=None):
428519
"""Command handler for inputs.
@@ -447,14 +538,19 @@ def list_inputs(self, args, screen_info=None):
447538

448539
parsed = self._arg_parsers["list_inputs"].parse_args(args)
449540

450-
return self._list_inputs_or_outputs(
541+
output = self._list_inputs_or_outputs(
451542
parsed.recursive,
452543
parsed.node_name,
453544
parsed.depth,
454545
parsed.control,
455546
parsed.op_type,
456547
do_outputs=False)
457548

549+
node_name = debug_data.get_node_name(parsed.node_name)
550+
_add_main_menu(output, node_name=node_name, enable_list_inputs=False)
551+
552+
return output
553+
458554
def print_tensor(self, args, screen_info=None):
459555
"""Command handler for print_tensor.
460556
@@ -484,16 +580,44 @@ def print_tensor(self, args, screen_info=None):
484580
command_parser.parse_tensor_name_with_slicing(parsed.tensor_name))
485581

486582
node_name, output_slot = debug_data.parse_node_or_tensor_name(tensor_name)
487-
if output_slot is None:
488-
return cli_shared.error("\"%s\" is not a valid tensor name" %
489-
parsed.tensor_name)
490-
491583
if (self._debug_dump.loaded_partition_graphs() and
492584
not self._debug_dump.node_exists(node_name)):
493-
return cli_shared.error(
585+
output = cli_shared.error(
494586
"Node \"%s\" does not exist in partition graphs" % node_name)
587+
_add_main_menu(
588+
output,
589+
node_name=None,
590+
enable_list_tensors=True,
591+
enable_print_tensor=False)
592+
return output
495593

496594
watch_keys = self._debug_dump.debug_watch_keys(node_name)
595+
if output_slot is None:
596+
output_slots = set()
597+
for watch_key in watch_keys:
598+
output_slots.add(int(watch_key.split(":")[1]))
599+
600+
if len(output_slots) == 1:
601+
# There is only one dumped tensor from this node, so there is no
602+
# ambiguity. Proceed to show the only dumped tensor.
603+
output_slot = list(output_slots)[0]
604+
else:
605+
# There are more than one dumped tensors from this node. Indicate as
606+
# such.
607+
# TODO(cais): Provide an output screen with command links for
608+
# convenience.
609+
lines = [
610+
"Node \"%s\" generated debug dumps from %s output slots:" %
611+
(node_name, len(output_slots)),
612+
"Please specify the output slot: %s:x." % node_name
613+
]
614+
output = debugger_cli_common.RichTextLines(lines)
615+
_add_main_menu(
616+
output,
617+
node_name=node_name,
618+
enable_list_tensors=True,
619+
enable_print_tensor=False)
620+
return output
497621

498622
# Find debug dump data that match the tensor name (node name + output
499623
# slot).
@@ -506,22 +630,24 @@ def print_tensor(self, args, screen_info=None):
506630

507631
if not matching_data:
508632
# No dump for this tensor.
509-
return cli_shared.error(
510-
"Tensor \"%s\" did not generate any dumps." % parsed.tensor_name)
633+
output = cli_shared.error("Tensor \"%s\" did not generate any dumps." %
634+
parsed.tensor_name)
511635
elif len(matching_data) == 1:
512636
# There is only one dump for this tensor.
513637
if parsed.number <= 0:
514-
return cli_shared.format_tensor(
638+
output = cli_shared.format_tensor(
515639
matching_data[0].get_tensor(),
516640
matching_data[0].watch_key,
517641
np_printoptions,
518642
print_all=parsed.print_all,
519643
tensor_slicing=tensor_slicing,
520644
highlight_options=highlight_options)
521645
else:
522-
return cli_shared.error(
646+
output = cli_shared.error(
523647
"Invalid number (%d) for tensor %s, which generated one dump." %
524648
(parsed.number, parsed.tensor_name))
649+
650+
_add_main_menu(output, node_name=node_name, enable_print_tensor=False)
525651
else:
526652
# There are more than one dumps for this tensor.
527653
if parsed.number < 0:
@@ -540,21 +666,24 @@ def print_tensor(self, args, screen_info=None):
540666
lines.append("For example:")
541667
lines.append(" print_tensor %s -n 0" % parsed.tensor_name)
542668

543-
return debugger_cli_common.RichTextLines(lines)
669+
output = debugger_cli_common.RichTextLines(lines)
544670
elif parsed.number >= len(matching_data):
545-
return cli_shared.error(
671+
output = cli_shared.error(
546672
"Specified number (%d) exceeds the number of available dumps "
547673
"(%d) for tensor %s" %
548674
(parsed.number, len(matching_data), parsed.tensor_name))
549675
else:
550-
return cli_shared.format_tensor(
676+
output = cli_shared.format_tensor(
551677
matching_data[parsed.number].get_tensor(),
552678
matching_data[parsed.number].watch_key + " (dump #%d)" %
553679
parsed.number,
554680
np_printoptions,
555681
print_all=parsed.print_all,
556682
tensor_slicing=tensor_slicing,
557683
highlight_options=highlight_options)
684+
_add_main_menu(output, node_name=node_name, enable_print_tensor=False)
685+
686+
return output
558687

559688
def list_outputs(self, args, screen_info=None):
560689
"""Command handler for inputs.
@@ -579,14 +708,19 @@ def list_outputs(self, args, screen_info=None):
579708

580709
parsed = self._arg_parsers["list_outputs"].parse_args(args)
581710

582-
return self._list_inputs_or_outputs(
711+
output = self._list_inputs_or_outputs(
583712
parsed.recursive,
584713
parsed.node_name,
585714
parsed.depth,
586715
parsed.control,
587716
parsed.op_type,
588717
do_outputs=True)
589718

719+
node_name = debug_data.get_node_name(parsed.node_name)
720+
_add_main_menu(output, node_name=node_name, enable_list_outputs=False)
721+
722+
return output
723+
590724
def _list_inputs_or_outputs(self,
591725
recursive,
592726
node_name,

0 commit comments

Comments
 (0)