Skip to content

Commit ad884d9

Browse files
authored
A few updates around "transcript" (#92843)
# Changes 1. Changes to the structured transcript. 1. Add fields `commandName` and `commandArguments`. They will hold the name and the arguments string of the expanded/executed command (e.g. `breakpoint set` and `-f main.cpp -l 4`). This is not to be confused with the `command` field, which holds the user input (e.g. `br s -f main.cpp -l 4`). 2. Add field `timestampInEpochSeconds`. It will hold the timestamp when the command is executed. 3. Rename field `seconds` to `durationInSeconds`, to improve readability, especially since `timestampInEpochSeconds` is added. 2. When transcript is available and the newly added option `--transcript` is present, add the transcript to the output of `statistics dump`, as a JSON array under a new field `transcript`. 3. A few test name and comment changes.
1 parent 9a8b73c commit ad884d9

File tree

9 files changed

+147
-15
lines changed

9 files changed

+147
-15
lines changed

lldb/include/lldb/API/SBCommandInterpreter.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,17 @@ class SBCommandInterpreter {
320320

321321
/// Returns a list of handled commands, output and error. Each element in
322322
/// the list is a dictionary with the following keys/values:
323-
/// - "command" (string): The command that was executed.
323+
/// - "command" (string): The command that was given by the user.
324+
/// - "commandName" (string): The name of the executed command.
325+
/// - "commandArguments" (string): The arguments of the executed command.
324326
/// - "output" (string): The output of the command. Empty ("") if no output.
325327
/// - "error" (string): The error of the command. Empty ("") if no error.
326-
/// - "seconds" (float): The time it took to execute the command.
328+
/// - "durationInSeconds" (float): The time it took to execute the command.
329+
/// - "timestampInEpochSeconds" (int): The timestamp when the command is
330+
/// executed.
331+
///
332+
/// Turn on settings `interpreter.save-transcript` for LLDB to populate
333+
/// this list. Otherwise this list is empty.
327334
SBStructuredData GetTranscript();
328335

329336
protected:

lldb/include/lldb/Interpreter/CommandInterpreter.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -776,10 +776,14 @@ class CommandInterpreter : public Broadcaster,
776776

777777
/// Contains a list of handled commands and their details. Each element in
778778
/// the list is a dictionary with the following keys/values:
779-
/// - "command" (string): The command that was executed.
779+
/// - "command" (string): The command that was given by the user.
780+
/// - "commandName" (string): The name of the executed command.
781+
/// - "commandArguments" (string): The arguments of the executed command.
780782
/// - "output" (string): The output of the command. Empty ("") if no output.
781783
/// - "error" (string): The error of the command. Empty ("") if no error.
782-
/// - "seconds" (float): The time it took to execute the command.
784+
/// - "durationInSeconds" (float): The time it took to execute the command.
785+
/// - "timestampInEpochSeconds" (int): The timestamp when the command is
786+
/// executed.
783787
///
784788
/// Turn on settings `interpreter.save-transcript` for LLDB to populate
785789
/// this list. Otherwise this list is empty.

lldb/include/lldb/Target/Statistics.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ struct ConstStringStats {
133133
struct StatisticsOptions {
134134
bool summary_only = false;
135135
bool load_all_debug_info = false;
136+
bool include_transcript = false;
136137
};
137138

138139
/// A class that represents statistics for a since lldb_private::Target.

lldb/source/Commands/CommandObjectStats.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class CommandObjectStatsDump : public CommandObjectParsed {
8181
case 'f':
8282
m_stats_options.load_all_debug_info = true;
8383
break;
84+
case 't':
85+
m_stats_options.include_transcript = true;
86+
break;
8487
default:
8588
llvm_unreachable("Unimplemented option");
8689
}

lldb/source/Commands/Options.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,4 +1425,8 @@ let Command = "statistics dump" in {
14251425
Desc<"Dump the total possible debug info statistics. "
14261426
"Force loading all the debug information if not yet loaded, and collect "
14271427
"statistics with those.">;
1428+
def statistics_dump_transcript: Option<"transcript", "t">, Group<1>,
1429+
Desc<"If the setting interpreter.save-transcript is enabled and this "
1430+
"option is specified, include a JSON array with all commands the user and/"
1431+
"or scripts executed during a debug session.">;
14281432
}

lldb/source/Interpreter/CommandInterpreter.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77
//===----------------------------------------------------------------------===//
88

9+
#include <chrono>
910
#include <cstdlib>
1011
#include <limits>
1112
#include <memory>
@@ -1909,6 +1910,11 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
19091910

19101911
transcript_item = std::make_shared<StructuredData::Dictionary>();
19111912
transcript_item->AddStringItem("command", command_line);
1913+
transcript_item->AddIntegerItem(
1914+
"timestampInEpochSeconds",
1915+
std::chrono::duration_cast<std::chrono::seconds>(
1916+
std::chrono::system_clock::now().time_since_epoch())
1917+
.count());
19121918
m_transcript.AddItem(transcript_item);
19131919
}
19141920

@@ -2056,6 +2062,14 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
20562062
log, "HandleCommand, command line after removing command name(s): '%s'",
20572063
remainder.c_str());
20582064

2065+
// To test whether or not transcript should be saved, `transcript_item` is
2066+
// used instead of `GetSaveTrasncript()`. This is because the latter will
2067+
// fail when the command is "settings set interpreter.save-transcript true".
2068+
if (transcript_item) {
2069+
transcript_item->AddStringItem("commandName", cmd_obj->GetCommandName());
2070+
transcript_item->AddStringItem("commandArguments", remainder);
2071+
}
2072+
20592073
ElapsedTime elapsed(execute_time);
20602074
cmd_obj->Execute(remainder.c_str(), result);
20612075
}
@@ -2072,7 +2086,8 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
20722086

20732087
transcript_item->AddStringItem("output", result.GetOutputData());
20742088
transcript_item->AddStringItem("error", result.GetErrorData());
2075-
transcript_item->AddFloatItem("seconds", execute_time.get().count());
2089+
transcript_item->AddFloatItem("durationInSeconds",
2090+
execute_time.get().count());
20762091
}
20772092

20782093
return result.Succeeded();

lldb/source/Target/Statistics.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "lldb/Target/Process.h"
1717
#include "lldb/Target/Target.h"
1818
#include "lldb/Target/UnixSignals.h"
19+
#include "lldb/Utility/StructuredData.h"
1920

2021
using namespace lldb;
2122
using namespace lldb_private;
@@ -225,6 +226,7 @@ llvm::json::Value DebuggerStats::ReportStatistics(
225226

226227
const bool summary_only = options.summary_only;
227228
const bool load_all_debug_info = options.load_all_debug_info;
229+
const bool include_transcript = options.include_transcript;
228230

229231
json::Array json_targets;
230232
json::Array json_modules;
@@ -364,5 +366,36 @@ llvm::json::Value DebuggerStats::ReportStatistics(
364366
global_stats.try_emplace("commands", std::move(cmd_stats));
365367
}
366368

369+
if (include_transcript) {
370+
// When transcript is available, add it to the to-be-returned statistics.
371+
//
372+
// NOTE:
373+
// When the statistics is polled by an LLDB command:
374+
// - The transcript in the returned statistics *will NOT* contain the
375+
// returned statistics itself (otherwise infinite recursion).
376+
// - The returned statistics *will* be written to the internal transcript
377+
// buffer. It *will* appear in the next statistcs or transcript poll.
378+
//
379+
// For example, let's say the following commands are run in order:
380+
// - "version"
381+
// - "statistics dump" <- call it "A"
382+
// - "statistics dump" <- call it "B"
383+
// The output of "A" will contain the transcript of "version" and
384+
// "statistics dump" (A), with the latter having empty output. The output
385+
// of B will contain the trascnript of "version", "statistics dump" (A),
386+
// "statistics dump" (B), with A's output populated and B's output empty.
387+
const StructuredData::Array &transcript =
388+
debugger.GetCommandInterpreter().GetTranscript();
389+
if (transcript.GetSize() != 0) {
390+
std::string buffer;
391+
llvm::raw_string_ostream ss(buffer);
392+
json::OStream json_os(ss);
393+
transcript.Serialize(json_os);
394+
if (auto json_transcript = llvm::json::parse(ss.str()))
395+
global_stats.try_emplace("transcript",
396+
std::move(json_transcript.get()));
397+
}
398+
}
399+
367400
return std::move(global_stats);
368401
}

lldb/test/API/commands/statistics/basic/TestStats.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,3 +623,50 @@ def test_had_frame_variable_errors(self):
623623
# Verify that the top level statistic that aggregates the number of
624624
# modules with debugInfoHadVariableErrors is greater than zero
625625
self.assertGreater(stats["totalModuleCountWithVariableErrors"], 0)
626+
627+
def test_transcript_happy_path(self):
628+
"""
629+
Test "statistics dump" and the transcript information.
630+
"""
631+
self.build()
632+
exe = self.getBuildArtifact("a.out")
633+
target = self.createTestTarget(file_path=exe)
634+
self.runCmd("settings set interpreter.save-transcript true")
635+
self.runCmd("version")
636+
637+
# Verify the output of a first "statistics dump"
638+
debug_stats = self.get_stats("--transcript")
639+
self.assertIn("transcript", debug_stats)
640+
transcript = debug_stats["transcript"]
641+
self.assertEqual(len(transcript), 2)
642+
self.assertEqual(transcript[0]["commandName"], "version")
643+
self.assertEqual(transcript[1]["commandName"], "statistics dump")
644+
# The first "statistics dump" in the transcript should have no output
645+
self.assertNotIn("output", transcript[1])
646+
647+
# Verify the output of a second "statistics dump"
648+
debug_stats = self.get_stats("--transcript")
649+
self.assertIn("transcript", debug_stats)
650+
transcript = debug_stats["transcript"]
651+
self.assertEqual(len(transcript), 3)
652+
self.assertEqual(transcript[0]["commandName"], "version")
653+
self.assertEqual(transcript[1]["commandName"], "statistics dump")
654+
# The first "statistics dump" in the transcript should have output now
655+
self.assertIn("output", transcript[1])
656+
self.assertEqual(transcript[2]["commandName"], "statistics dump")
657+
# The second "statistics dump" in the transcript should have no output
658+
self.assertNotIn("output", transcript[2])
659+
660+
def test_transcript_should_not_exist_when_not_asked_for(self):
661+
"""
662+
Test "statistics dump" and the transcript information.
663+
"""
664+
self.build()
665+
exe = self.getBuildArtifact("a.out")
666+
target = self.createTestTarget(file_path=exe)
667+
self.runCmd("settings set interpreter.save-transcript true")
668+
self.runCmd("version")
669+
670+
# Verify the output of a first "statistics dump"
671+
debug_stats = self.get_stats() # Not with "--transcript"
672+
self.assertNotIn("transcript", debug_stats)

lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def getTranscriptAsPythonObject(self, ci):
104104

105105
return json.loads(stream.GetData())
106106

107-
def test_structured_transcript(self):
107+
def test_get_transcript(self):
108108
"""Test structured transcript generation and retrieval."""
109109
ci = self.buildAndCreateTarget()
110110

@@ -118,7 +118,7 @@ def test_structured_transcript(self):
118118
res = lldb.SBCommandReturnObject()
119119
ci.HandleCommand("version", res)
120120
ci.HandleCommand("an-unknown-command", res)
121-
ci.HandleCommand("breakpoint set -f main.c -l %d" % self.line, res)
121+
ci.HandleCommand("br s -f main.c -l %d" % self.line, res)
122122
ci.HandleCommand("r", res)
123123
ci.HandleCommand("p a", res)
124124
ci.HandleCommand("statistics dump", res)
@@ -130,41 +130,54 @@ def test_structured_transcript(self):
130130
# All commands should have expected fields.
131131
for command in transcript:
132132
self.assertIn("command", command)
133+
# Unresolved commands don't have "commandName"/"commandArguments".
134+
# We will validate these fields below, instead of here.
133135
self.assertIn("output", command)
134136
self.assertIn("error", command)
135-
self.assertIn("seconds", command)
137+
self.assertIn("durationInSeconds", command)
138+
self.assertIn("timestampInEpochSeconds", command)
136139

137140
# The following validates individual commands in the transcript.
138141
#
139142
# Notes:
140143
# 1. Some of the asserts rely on the exact output format of the
141144
# commands. Hopefully we are not changing them any time soon.
142-
# 2. We are removing the "seconds" field from each command, so that
143-
# some of the validations below can be easier / more readable.
145+
# 2. We are removing the time-related fields from each command, so
146+
# that some of the validations below can be easier / more readable.
144147
for command in transcript:
145-
del(command["seconds"])
148+
del command["durationInSeconds"]
149+
del command["timestampInEpochSeconds"]
146150

147151
# (lldb) version
148152
self.assertEqual(transcript[0]["command"], "version")
153+
self.assertEqual(transcript[0]["commandName"], "version")
154+
self.assertEqual(transcript[0]["commandArguments"], "")
149155
self.assertIn("lldb version", transcript[0]["output"])
150156
self.assertEqual(transcript[0]["error"], "")
151157

152158
# (lldb) an-unknown-command
153159
self.assertEqual(transcript[1],
154160
{
155161
"command": "an-unknown-command",
162+
# Unresolved commands don't have "commandName"/"commandArguments"
156163
"output": "",
157164
"error": "error: 'an-unknown-command' is not a valid command.\n",
158165
})
159166

160-
# (lldb) breakpoint set -f main.c -l <line>
161-
self.assertEqual(transcript[2]["command"], "breakpoint set -f main.c -l %d" % self.line)
167+
# (lldb) br s -f main.c -l <line>
168+
self.assertEqual(transcript[2]["command"], "br s -f main.c -l %d" % self.line)
169+
self.assertEqual(transcript[2]["commandName"], "breakpoint set")
170+
self.assertEqual(
171+
transcript[2]["commandArguments"], "-f main.c -l %d" % self.line
172+
)
162173
# Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address = 0x0000000100000f7d
163174
self.assertIn("Breakpoint 1: where = a.out`main ", transcript[2]["output"])
164175
self.assertEqual(transcript[2]["error"], "")
165176

166177
# (lldb) r
167178
self.assertEqual(transcript[3]["command"], "r")
179+
self.assertEqual(transcript[3]["commandName"], "process launch")
180+
self.assertEqual(transcript[3]["commandArguments"], "-X true --")
168181
# Process 25494 launched: '<path>/TestCommandInterpreterAPI.test_structured_transcript/a.out' (x86_64)
169182
self.assertIn("Process", transcript[3]["output"])
170183
self.assertIn("launched", transcript[3]["output"])
@@ -174,11 +187,17 @@ def test_structured_transcript(self):
174187
self.assertEqual(transcript[4],
175188
{
176189
"command": "p a",
190+
"commandName": "dwim-print",
191+
"commandArguments": "-- a",
177192
"output": "(int) 123\n",
178193
"error": "",
179194
})
180195

181196
# (lldb) statistics dump
197+
self.assertEqual(transcript[5]["command"], "statistics dump")
198+
self.assertEqual(transcript[5]["commandName"], "statistics dump")
199+
self.assertEqual(transcript[5]["commandArguments"], "")
200+
self.assertEqual(transcript[5]["error"], "")
182201
statistics_dump = json.loads(transcript[5]["output"])
183202
# Dump result should be valid JSON
184203
self.assertTrue(statistics_dump is not json.JSONDecodeError)
@@ -194,7 +213,6 @@ def test_save_transcript_setting_default(self):
194213

195214
# The setting's default value should be "false"
196215
self.runCmd("settings show interpreter.save-transcript", "interpreter.save-transcript (boolean) = false\n")
197-
# self.assertEqual(res.GetOutput(), )
198216

199217
def test_save_transcript_setting_off(self):
200218
ci = self.buildAndCreateTarget()
@@ -220,7 +238,7 @@ def test_save_transcript_setting_on(self):
220238
self.assertEqual(len(transcript), 1)
221239
self.assertEqual(transcript[0]["command"], "version")
222240

223-
def test_save_transcript_returns_copy(self):
241+
def test_get_transcript_returns_copy(self):
224242
"""
225243
Test that the returned structured data is *at least* a shallow copy.
226244

0 commit comments

Comments
 (0)