Skip to content

Commit c2d061d

Browse files
authored
Re-merge A few updates around "transcript" (#92843) (#94067)
Problematic PR: #92843 Reverted by: #94088 The first PR added a test which fails in Linux builds (see the last few comments there). This PR contains all the changes in the first PR, plus the fix to the said test. --------- Co-authored-by: Roy Shi <[email protected]>
1 parent b61d7ec commit c2d061d

File tree

9 files changed

+158
-33
lines changed

9 files changed

+158
-33
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: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,10 @@ 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()
110+
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
110111

111112
# Make sure the "save-transcript" setting is on
112113
self.runCmd("settings set interpreter.save-transcript true")
@@ -118,8 +119,7 @@ def test_structured_transcript(self):
118119
res = lldb.SBCommandReturnObject()
119120
ci.HandleCommand("version", res)
120121
ci.HandleCommand("an-unknown-command", res)
121-
ci.HandleCommand("breakpoint set -f main.c -l %d" % self.line, res)
122-
ci.HandleCommand("r", res)
122+
ci.HandleCommand("br s -f main.c -l %d" % self.line, res)
123123
ci.HandleCommand("p a", res)
124124
ci.HandleCommand("statistics dump", res)
125125
total_number_of_commands = 6
@@ -130,56 +130,66 @@ 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

166-
# (lldb) r
167-
self.assertEqual(transcript[3]["command"], "r")
168-
# Process 25494 launched: '<path>/TestCommandInterpreterAPI.test_structured_transcript/a.out' (x86_64)
169-
self.assertIn("Process", transcript[3]["output"])
170-
self.assertIn("launched", transcript[3]["output"])
171-
self.assertEqual(transcript[3]["error"], "")
172-
173177
# (lldb) p a
174-
self.assertEqual(transcript[4],
178+
self.assertEqual(transcript[3],
175179
{
176180
"command": "p a",
177-
"output": "(int) 123\n",
178-
"error": "",
181+
"commandName": "dwim-print",
182+
"commandArguments": "-- a",
183+
"output": "",
184+
"error": "error: <user expression 0>:1:1: use of undeclared identifier 'a'\n 1 | a\n | ^\n",
179185
})
180186

181187
# (lldb) statistics dump
182-
statistics_dump = json.loads(transcript[5]["output"])
188+
self.assertEqual(transcript[4]["command"], "statistics dump")
189+
self.assertEqual(transcript[4]["commandName"], "statistics dump")
190+
self.assertEqual(transcript[4]["commandArguments"], "")
191+
self.assertEqual(transcript[4]["error"], "")
192+
statistics_dump = json.loads(transcript[4]["output"])
183193
# Dump result should be valid JSON
184194
self.assertTrue(statistics_dump is not json.JSONDecodeError)
185195
# Dump result should contain expected fields
@@ -189,15 +199,15 @@ def test_structured_transcript(self):
189199
self.assertIn("targets", statistics_dump)
190200

191201
def test_save_transcript_setting_default(self):
192-
ci = self.buildAndCreateTarget()
193-
res = lldb.SBCommandReturnObject()
202+
ci = self.dbg.GetCommandInterpreter()
203+
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
194204

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

199208
def test_save_transcript_setting_off(self):
200-
ci = self.buildAndCreateTarget()
209+
ci = self.dbg.GetCommandInterpreter()
210+
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
201211

202212
# Make sure the setting is off
203213
self.runCmd("settings set interpreter.save-transcript false")
@@ -208,8 +218,8 @@ def test_save_transcript_setting_off(self):
208218
self.assertEqual(transcript, [])
209219

210220
def test_save_transcript_setting_on(self):
211-
ci = self.buildAndCreateTarget()
212-
res = lldb.SBCommandReturnObject()
221+
ci = self.dbg.GetCommandInterpreter()
222+
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
213223

214224
# Make sure the setting is on
215225
self.runCmd("settings set interpreter.save-transcript true")
@@ -220,7 +230,7 @@ def test_save_transcript_setting_on(self):
220230
self.assertEqual(len(transcript), 1)
221231
self.assertEqual(transcript[0]["command"], "version")
222232

223-
def test_save_transcript_returns_copy(self):
233+
def test_get_transcript_returns_copy(self):
224234
"""
225235
Test that the returned structured data is *at least* a shallow copy.
226236
@@ -229,7 +239,8 @@ def test_save_transcript_returns_copy(self):
229239
because there is no logic in the command interpreter to modify a
230240
transcript item (representing a command) after it has been returned.
231241
"""
232-
ci = self.buildAndCreateTarget()
242+
ci = self.dbg.GetCommandInterpreter()
243+
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
233244

234245
# Make sure the setting is on
235246
self.runCmd("settings set interpreter.save-transcript true")

0 commit comments

Comments
 (0)