[Lldb-commits] [lldb] Add new Python API `SBCommandInterpreter::GetTranscript()` (PR #90703)

via lldb-commits lldb-commits at lists.llvm.org
Mon May 20 13:19:23 PDT 2024


https://github.com/royitaqi updated https://github.com/llvm/llvm-project/pull/90703

>From 0fd67e2de7e702ce6f7353845454ea7ff9f980d6 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Tue, 30 Apr 2024 21:35:49 -0700
Subject: [PATCH 01/19] Add SBCommandInterpreter::GetTranscript()

---
 lldb/include/lldb/API/SBCommandInterpreter.h | 12 +++++++++---
 lldb/source/API/SBCommandInterpreter.cpp     |  7 ++++++-
 2 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/lldb/include/lldb/API/SBCommandInterpreter.h b/lldb/include/lldb/API/SBCommandInterpreter.h
index ba2e049204b8e..d65f06d676f91 100644
--- a/lldb/include/lldb/API/SBCommandInterpreter.h
+++ b/lldb/include/lldb/API/SBCommandInterpreter.h
@@ -247,13 +247,13 @@ class SBCommandInterpreter {
                                        lldb::SBStringList &matches,
                                        lldb::SBStringList &descriptions);
 
-  /// Returns whether an interrupt flag was raised either by the SBDebugger - 
+  /// Returns whether an interrupt flag was raised either by the SBDebugger -
   /// when the function is not running on the RunCommandInterpreter thread, or
   /// by SBCommandInterpreter::InterruptCommand if it is.  If your code is doing
-  /// interruptible work, check this API periodically, and interrupt if it 
+  /// interruptible work, check this API periodically, and interrupt if it
   /// returns true.
   bool WasInterrupted() const;
-  
+
   /// Interrupts the command currently executing in the RunCommandInterpreter
   /// thread.
   ///
@@ -318,6 +318,12 @@ class SBCommandInterpreter {
 
   SBStructuredData GetStatistics();
 
+  /// Returns a list of handled commands, output and error. Each element in
+  /// the list is a dictionary with three keys: "command" (string), "output"
+  /// (list of strings) and optionally "error" (list of strings). Each string
+  /// in "output" and "error" is a line (without EOL characteres).
+  SBStructuredData GetTranscript();
+
 protected:
   friend class lldb_private::CommandPluginInterfaceImplementation;
 
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index 83c0951c56db6..242b3f8f09c48 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -150,7 +150,7 @@ bool SBCommandInterpreter::WasInterrupted() const {
 
 bool SBCommandInterpreter::InterruptCommand() {
   LLDB_INSTRUMENT_VA(this);
-  
+
   return (IsValid() ? m_opaque_ptr->InterruptCommand() : false);
 }
 
@@ -571,6 +571,11 @@ SBStructuredData SBCommandInterpreter::GetStatistics() {
   return data;
 }
 
+SBStructuredData SBCommandInterpreter::GetTranscript() {
+  LLDB_INSTRUMENT_VA(this);
+  return SBStructuredData();
+}
+
 lldb::SBCommand SBCommandInterpreter::AddMultiwordCommand(const char *name,
                                                           const char *help) {
   LLDB_INSTRUMENT_VA(this, name, help);

>From a1c948ceabaccdc3407e0c4eae0ebc594a9b68b7 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Wed, 1 May 2024 13:45:47 -0700
Subject: [PATCH 02/19] Implement the new API

---
 .../lldb/Interpreter/CommandInterpreter.h     | 12 +++++--
 lldb/include/lldb/Utility/StructuredData.h    | 11 +++---
 lldb/source/API/SBCommandInterpreter.cpp      |  8 ++++-
 .../source/Interpreter/CommandInterpreter.cpp | 21 ++++++++++-
 lldb/source/Utility/StructuredData.cpp        | 35 +++++++++++++++++++
 5 files changed, 79 insertions(+), 8 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index 70a55a77465bf..9474c41c0dced 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -22,6 +22,7 @@
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/StreamString.h"
 #include "lldb/Utility/StringList.h"
+#include "lldb/Utility/StructuredData.h"
 #include "lldb/lldb-forward.h"
 #include "lldb/lldb-private.h"
 
@@ -241,7 +242,7 @@ class CommandInterpreter : public Broadcaster,
     eCommandTypesAllThem = 0xFFFF  //< all commands
   };
 
-  // The CommandAlias and CommandInterpreter both have a hand in 
+  // The CommandAlias and CommandInterpreter both have a hand in
   // substituting for alias commands.  They work by writing special tokens
   // in the template form of the Alias command, and then detecting them when the
   // command is executed.  These are the special tokens:
@@ -576,7 +577,7 @@ class CommandInterpreter : public Broadcaster,
   void SetEchoCommentCommands(bool enable);
 
   bool GetRepeatPreviousCommand() const;
-  
+
   bool GetRequireCommandOverwrite() const;
 
   const CommandObject::CommandMap &GetUserCommands() const {
@@ -647,6 +648,7 @@ class CommandInterpreter : public Broadcaster,
   }
 
   llvm::json::Value GetStatistics();
+  StructuredData::ArraySP GetTranscript() const;
 
 protected:
   friend class Debugger;
@@ -766,6 +768,12 @@ class CommandInterpreter : public Broadcaster,
   CommandUsageMap m_command_usages;
 
   StreamString m_transcript_stream;
+
+  /// Contains a list of handled commands, output and error. Each element in
+  /// the list is a dictionary with three keys: "command" (string), "output"
+  /// (list of strings) and optionally "error" (list of strings). Each string
+  /// in "output" and "error" is a line (without EOL characteres).
+  StructuredData::ArraySP m_transcript_structured;
 };
 
 } // namespace lldb_private
diff --git a/lldb/include/lldb/Utility/StructuredData.h b/lldb/include/lldb/Utility/StructuredData.h
index 5e63ef92fac3e..72fd035c23e47 100644
--- a/lldb/include/lldb/Utility/StructuredData.h
+++ b/lldb/include/lldb/Utility/StructuredData.h
@@ -290,6 +290,9 @@ class StructuredData {
 
     void GetDescription(lldb_private::Stream &s) const override;
 
+    static ArraySP SplitString(llvm::StringRef s, char separator, int maxSplit,
+                               bool keepEmpty);
+
   protected:
     typedef std::vector<ObjectSP> collection;
     collection m_items;
@@ -366,10 +369,10 @@ class StructuredData {
   class String : public Object {
   public:
     String() : Object(lldb::eStructuredDataTypeString) {}
-    explicit String(llvm::StringRef S)
-        : Object(lldb::eStructuredDataTypeString), m_value(S) {}
+    explicit String(llvm::StringRef s)
+        : Object(lldb::eStructuredDataTypeString), m_value(s) {}
 
-    void SetValue(llvm::StringRef S) { m_value = std::string(S); }
+    void SetValue(llvm::StringRef s) { m_value = std::string(s); }
 
     llvm::StringRef GetValue() { return m_value; }
 
@@ -432,7 +435,7 @@ class StructuredData {
       }
       return success;
     }
-      
+
     template <class IntType>
     bool GetValueForKeyAsInteger(llvm::StringRef key, IntType &result) const {
       ObjectSP value_sp = GetValueForKey(key);
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index 242b3f8f09c48..e96b5a047c64d 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -573,7 +573,13 @@ SBStructuredData SBCommandInterpreter::GetStatistics() {
 
 SBStructuredData SBCommandInterpreter::GetTranscript() {
   LLDB_INSTRUMENT_VA(this);
-  return SBStructuredData();
+
+  SBStructuredData data;
+  if (!IsValid())
+    return data;
+
+  data.m_impl_up->SetObjectSP(m_opaque_ptr->GetTranscript());
+  return data;
 }
 
 lldb::SBCommand SBCommandInterpreter::AddMultiwordCommand(const char *name,
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index 4c58ecc3c1848..b5f726d323465 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -51,6 +51,7 @@
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/State.h"
 #include "lldb/Utility/Stream.h"
+#include "lldb/Utility/StructuredData.h"
 #include "lldb/Utility/Timer.h"
 
 #include "lldb/Host/Config.h"
@@ -135,7 +136,8 @@ CommandInterpreter::CommandInterpreter(Debugger &debugger,
       m_skip_lldbinit_files(false), m_skip_app_init_files(false),
       m_comment_char('#'), m_batch_command_mode(false),
       m_truncation_warning(eNoOmission), m_max_depth_warning(eNoOmission),
-      m_command_source_depth(0) {
+      m_command_source_depth(0),
+      m_transcript_structured(std::make_shared<StructuredData::Array>()) {
   SetEventName(eBroadcastBitThreadShouldExit, "thread-should-exit");
   SetEventName(eBroadcastBitResetPrompt, "reset-prompt");
   SetEventName(eBroadcastBitQuitCommandReceived, "quit");
@@ -1891,6 +1893,10 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
 
   m_transcript_stream << "(lldb) " << command_line << '\n';
 
+  auto transcript_item = std::make_shared<StructuredData::Dictionary>();
+  transcript_item->AddStringItem("command", command_line);
+  m_transcript_structured->AddItem(transcript_item);
+
   bool empty_command = false;
   bool comment_command = false;
   if (command_string.empty())
@@ -2044,6 +2050,15 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
   m_transcript_stream << result.GetOutputData();
   m_transcript_stream << result.GetErrorData();
 
+  // Add output and error to the transcript item after splitting lines. In the
+  // future, other aspects of the command (e.g. perf) can be added, too.
+  transcript_item->AddItem(
+      "output", StructuredData::Array::SplitString(result.GetOutputData(), '\n',
+                                                   -1, false));
+  transcript_item->AddItem(
+      "error", StructuredData::Array::SplitString(result.GetErrorData(), '\n',
+                                                  -1, false));
+
   return result.Succeeded();
 }
 
@@ -3554,3 +3569,7 @@ llvm::json::Value CommandInterpreter::GetStatistics() {
     stats.try_emplace(command_usage.getKey(), command_usage.getValue());
   return stats;
 }
+
+StructuredData::ArraySP CommandInterpreter::GetTranscript() const {
+  return m_transcript_structured;
+}
diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index 7686d052c599c..278ec93168926 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -10,10 +10,13 @@
 #include "lldb/Utility/FileSpec.h"
 #include "lldb/Utility/Status.h"
 #include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/MemoryBuffer.h"
 #include <cerrno>
 #include <cinttypes>
 #include <cstdlib>
+#include <memory>
+#include <sstream>
 
 using namespace lldb_private;
 using namespace llvm;
@@ -289,3 +292,35 @@ void StructuredData::Null::GetDescription(lldb_private::Stream &s) const {
 void StructuredData::Generic::GetDescription(lldb_private::Stream &s) const {
   s.Printf("%p", m_object);
 }
+
+/// This is the same implementation as `StringRef::split`. Not depending on
+/// `StringRef::split` because it will involve a temporary `SmallVectorImpl`.
+StructuredData::ArraySP StructuredData::Array::SplitString(llvm::StringRef s,
+                                                           char separator,
+                                                           int maxSplit,
+                                                           bool keepEmpty) {
+  auto array_sp = std::make_shared<StructuredData::Array>();
+
+  // Count down from MaxSplit. When MaxSplit is -1, this will just split
+  // "forever". This doesn't support splitting more than 2^31 times
+  // intentionally; if we ever want that we can make MaxSplit a 64-bit integer
+  // but that seems unlikely to be useful.
+  while (maxSplit-- != 0) {
+    size_t idx = s.find(separator);
+    if (idx == llvm::StringLiteral::npos)
+      break;
+
+    // Push this split.
+    if (keepEmpty || idx > 0)
+      array_sp->AddStringItem(s.slice(0, idx));
+
+    // Jump forward.
+    s = s.slice(idx + 1, llvm::StringLiteral::npos);
+  }
+
+  // Push the tail.
+  if (keepEmpty || !s.empty())
+    array_sp->AddStringItem(s);
+
+  return array_sp;
+}

>From efc1c2037da00dacddc3e52812f93377d41d4f82 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Wed, 1 May 2024 14:45:48 -0700
Subject: [PATCH 03/19] Add unittest

---
 .../interpreter/TestCommandInterpreterAPI.py  | 42 +++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
index 8f9fbfc255bb0..93d36e3388941 100644
--- a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
+++ b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
@@ -1,5 +1,6 @@
 """Test the SBCommandInterpreter APIs."""
 
+import json
 import lldb
 from lldbsuite.test.decorators import *
 from lldbsuite.test.lldbtest import *
@@ -85,3 +86,44 @@ def test_command_output(self):
         self.assertEqual(res.GetOutput(), "")
         self.assertIsNotNone(res.GetError())
         self.assertEqual(res.GetError(), "")
+
+    def test_structured_transcript(self):
+        """Test structured transcript generation and retrieval."""
+        ci = self.dbg.GetCommandInterpreter()
+        self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
+
+        # Send a few commands through the command interpreter
+        res = lldb.SBCommandReturnObject()
+        ci.HandleCommand("version", res)
+        ci.HandleCommand("an-unknown-command", res)
+
+        # Retrieve the transcript and convert it into a Python object
+        transcript = ci.GetTranscript()
+        self.assertTrue(transcript.IsValid())
+
+        stream = lldb.SBStream()
+        self.assertTrue(stream)
+
+        error = transcript.GetAsJSON(stream)
+        self.assertSuccess(error)
+
+        transcript = json.loads(stream.GetData())
+
+        # Validate the transcript.
+        #
+        # Notes:
+        # 1. The following asserts rely on the exact output format of the
+        #    commands. Hopefully we are not changing them any time soon.
+        # 2. The transcript will contain a bunch of commands that are run
+        #    automatically. We only want to validate for the ones that are
+        #    handled in the above, hence the negative indices to find them.
+        self.assertEqual(transcript[-2]["command"], "version")
+        self.assertTrue("lldb version" in transcript[-2]["output"][0])
+        self.assertEqual(transcript[-1],
+            {
+                "command": "an-unknown-command",
+                "output": [],
+                "error": [
+                    "error: 'an-unknown-command' is not a valid command.",
+                ],
+            })

>From 6d1190df0ecae0fa49519545526636e84ee9b394 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Wed, 1 May 2024 15:20:38 -0700
Subject: [PATCH 04/19] Add more test asserts and some touch ups

---
 .../lldb/Interpreter/CommandInterpreter.h     |  2 +-
 .../source/Interpreter/CommandInterpreter.cpp |  2 +
 lldb/source/Utility/StructuredData.cpp        |  2 -
 .../interpreter/TestCommandInterpreterAPI.py  | 63 ++++++++++++++++---
 lldb/test/API/python_api/interpreter/main.c   |  5 +-
 5 files changed, 60 insertions(+), 14 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index 9474c41c0dced..c0846db8f2b8a 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -772,7 +772,7 @@ class CommandInterpreter : public Broadcaster,
   /// Contains a list of handled commands, output and error. Each element in
   /// the list is a dictionary with three keys: "command" (string), "output"
   /// (list of strings) and optionally "error" (list of strings). Each string
-  /// in "output" and "error" is a line (without EOL characteres).
+  /// in "output" and "error" is a line (without EOL characters).
   StructuredData::ArraySP m_transcript_structured;
 };
 
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index b5f726d323465..1ec1da437ba3a 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -1893,6 +1893,8 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
 
   m_transcript_stream << "(lldb) " << command_line << '\n';
 
+  // The same `transcript_item` will be used below to add output and error of
+  // the command.
   auto transcript_item = std::make_shared<StructuredData::Dictionary>();
   transcript_item->AddStringItem("command", command_line);
   m_transcript_structured->AddItem(transcript_item);
diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index 278ec93168926..7870334d708fe 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -15,8 +15,6 @@
 #include <cerrno>
 #include <cinttypes>
 #include <cstdlib>
-#include <memory>
-#include <sstream>
 
 using namespace lldb_private;
 using namespace llvm;
diff --git a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
index 93d36e3388941..e5cb4a18f7df6 100644
--- a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
+++ b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
@@ -89,6 +89,13 @@ def test_command_output(self):
 
     def test_structured_transcript(self):
         """Test structured transcript generation and retrieval."""
+        # Get command interpreter and create a target
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+
+        target = self.dbg.CreateTarget(exe)
+        self.assertTrue(target, VALID_TARGET)
+
         ci = self.dbg.GetCommandInterpreter()
         self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
 
@@ -96,6 +103,10 @@ def test_structured_transcript(self):
         res = lldb.SBCommandReturnObject()
         ci.HandleCommand("version", res)
         ci.HandleCommand("an-unknown-command", res)
+        ci.HandleCommand("breakpoint set -f main.c -l %d" % self.line, res)
+        ci.HandleCommand("r", res)
+        ci.HandleCommand("p a", res)
+        total_number_of_commands = 5
 
         # Retrieve the transcript and convert it into a Python object
         transcript = ci.GetTranscript()
@@ -109,17 +120,25 @@ def test_structured_transcript(self):
 
         transcript = json.loads(stream.GetData())
 
+        # The transcript will contain a bunch of commands that are run
+        # automatically. We only want to validate for the ones that are
+        # listed above, hence trimming to the last parts.
+        transcript = transcript[-total_number_of_commands:]
+
+        print(transcript)
+
         # Validate the transcript.
         #
-        # Notes:
-        # 1. The following asserts rely on the exact output format of the
-        #    commands. Hopefully we are not changing them any time soon.
-        # 2. The transcript will contain a bunch of commands that are run
-        #    automatically. We only want to validate for the ones that are
-        #    handled in the above, hence the negative indices to find them.
-        self.assertEqual(transcript[-2]["command"], "version")
-        self.assertTrue("lldb version" in transcript[-2]["output"][0])
-        self.assertEqual(transcript[-1],
+        # The following asserts rely on the exact output format of the
+        # commands. Hopefully we are not changing them any time soon.
+
+        # (lldb) version
+        self.assertEqual(transcript[0]["command"], "version")
+        self.assertTrue("lldb version" in transcript[0]["output"][0])
+        self.assertEqual(transcript[0]["error"], [])
+
+        # (lldb) an-unknown-command
+        self.assertEqual(transcript[1],
             {
                 "command": "an-unknown-command",
                 "output": [],
@@ -127,3 +146,29 @@ def test_structured_transcript(self):
                     "error: 'an-unknown-command' is not a valid command.",
                 ],
             })
+
+        # (lldb) breakpoint set -f main.c -l X
+        self.assertEqual(transcript[2],
+            {
+                "command": "breakpoint set -f main.c -l %d" % self.line,
+                "output": [
+                    "Breakpoint 1: where = a.out`main + 29 at main.c:5:5, address = 0x0000000100000f7d",
+                ],
+                "error": [],
+            })
+
+        # (lldb) r
+        self.assertEqual(transcript[3]["command"], "r")
+        self.assertTrue("Process" in transcript[3]["output"][0])
+        self.assertTrue("launched" in transcript[3]["output"][0])
+        self.assertEqual(transcript[3]["error"], [])
+
+        # (lldb) p a
+        self.assertEqual(transcript[4],
+            {
+                "command": "p a",
+                "output": [
+                    "(int) 123",
+                ],
+                "error": [],
+            })
diff --git a/lldb/test/API/python_api/interpreter/main.c b/lldb/test/API/python_api/interpreter/main.c
index 277aa54a4eea5..366ffde5fdef5 100644
--- a/lldb/test/API/python_api/interpreter/main.c
+++ b/lldb/test/API/python_api/interpreter/main.c
@@ -1,6 +1,7 @@
 #include <stdio.h>
 
 int main(int argc, char const *argv[]) {
-    printf("Hello world.\n");
-    return 0;
+  int a = 123;
+  printf("Hello world.\n");
+  return 0;
 }

>From 26a726b2f94713ef8508049115ab93ee91e9a836 Mon Sep 17 00:00:00 2001
From: royitaqi <royitaqi at users.noreply.github.com>
Date: Wed, 1 May 2024 15:27:33 -0700
Subject: [PATCH 05/19] Apply suggestions from code review

Co-authored-by: Med Ismail Bennani <ismail at bennani.ma>
---
 lldb/source/API/SBCommandInterpreter.cpp | 6 ++----
 lldb/source/Utility/StructuredData.cpp   | 4 ++--
 2 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index e96b5a047c64d..233a2f97fb9f1 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -575,10 +575,8 @@ SBStructuredData SBCommandInterpreter::GetTranscript() {
   LLDB_INSTRUMENT_VA(this);
 
   SBStructuredData data;
-  if (!IsValid())
-    return data;
-
-  data.m_impl_up->SetObjectSP(m_opaque_ptr->GetTranscript());
+  if (IsValid())
+    data.m_impl_up->SetObjectSP(m_opaque_ptr->GetTranscript());
   return data;
 }
 
diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index 7870334d708fe..4ca804cb76a74 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -299,9 +299,9 @@ StructuredData::ArraySP StructuredData::Array::SplitString(llvm::StringRef s,
                                                            bool keepEmpty) {
   auto array_sp = std::make_shared<StructuredData::Array>();
 
-  // Count down from MaxSplit. When MaxSplit is -1, this will just split
+  // Count down from `maxSplit`. When `maxSplit` is -1, this will just split
   // "forever". This doesn't support splitting more than 2^31 times
-  // intentionally; if we ever want that we can make MaxSplit a 64-bit integer
+  // intentionally; if we ever want that we can make `maxSplit` a 64-bit integer
   // but that seems unlikely to be useful.
   while (maxSplit-- != 0) {
     size_t idx = s.find(separator);

>From 52a310b8c236d252233b6e49de48a0c53eab9f45 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Wed, 1 May 2024 16:07:44 -0700
Subject: [PATCH 06/19] Move and add document for Array::SplitString

---
 lldb/include/lldb/Utility/StructuredData.h | 25 ++++++++++++++++++++--
 lldb/source/Utility/StructuredData.cpp     |  2 --
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/lldb/include/lldb/Utility/StructuredData.h b/lldb/include/lldb/Utility/StructuredData.h
index 72fd035c23e47..69db0caca2051 100644
--- a/lldb/include/lldb/Utility/StructuredData.h
+++ b/lldb/include/lldb/Utility/StructuredData.h
@@ -290,8 +290,29 @@ class StructuredData {
 
     void GetDescription(lldb_private::Stream &s) const override;
 
-    static ArraySP SplitString(llvm::StringRef s, char separator, int maxSplit,
-                               bool keepEmpty);
+    /// Creates an Array of substrings by splitting a string around the occurrences of a separator character.
+    ///
+    /// Note:
+    /// * This is almost the same API and implementation as `StringRef::split`.
+    /// * Not depending on `StringRef::split` because it will involve a
+    ///   temporary `SmallVectorImpl`.
+    ///
+    /// \param[in] s
+    ///   The input string.
+    ///
+    /// \param[in] separator
+    ///   The character to split on.
+    ///
+    /// \param[in] maxSplit
+    ///   The maximum number of times the string is split. If \a maxSplit is >= 0, at most \a maxSplit splits are done and consequently <= \a maxSplit + 1 elements are returned.
+    ///
+    /// \param[in] keepEmpty
+    ///   True if empty substrings should be returned. Empty substrings still count when considering \a maxSplit.
+    ///
+    /// \return
+    ///   An array containing the substrings. If \a maxSplit == -1 and \a keepEmpty == true, then the concatination of the array forms the input string.
+    static ArraySP SplitString(llvm::StringRef s, char separator, int maxSplit = -1,
+                               bool keepEmpty = true);
 
   protected:
     typedef std::vector<ObjectSP> collection;
diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index 4ca804cb76a74..7fa1063e5f01f 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -291,8 +291,6 @@ void StructuredData::Generic::GetDescription(lldb_private::Stream &s) const {
   s.Printf("%p", m_object);
 }
 
-/// This is the same implementation as `StringRef::split`. Not depending on
-/// `StringRef::split` because it will involve a temporary `SmallVectorImpl`.
 StructuredData::ArraySP StructuredData::Array::SplitString(llvm::StringRef s,
                                                            char separator,
                                                            int maxSplit,

>From 9beff0b2fdbac700f2aec6047ea90356ffecbce7 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Wed, 1 May 2024 16:53:25 -0700
Subject: [PATCH 07/19] Add unit test for Array::SplitString

---
 lldb/unittests/Utility/StructuredDataTest.cpp | 63 +++++++++++++++++++
 1 file changed, 63 insertions(+)

diff --git a/lldb/unittests/Utility/StructuredDataTest.cpp b/lldb/unittests/Utility/StructuredDataTest.cpp
index e536039f365a4..f107490946334 100644
--- a/lldb/unittests/Utility/StructuredDataTest.cpp
+++ b/lldb/unittests/Utility/StructuredDataTest.cpp
@@ -12,6 +12,7 @@
 #include "lldb/Utility/Status.h"
 #include "lldb/Utility/StreamString.h"
 #include "lldb/Utility/StructuredData.h"
+#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Path.h"
 
 using namespace lldb;
@@ -112,3 +113,65 @@ TEST(StructuredDataTest, ParseJSONFromFile) {
   object_sp->Dump(S, false);
   EXPECT_EQ("[1,2,3]", S.GetString());
 }
+
+struct ArraySplitStringTestCase {
+  llvm::StringRef s;
+  char separator;
+  int maxSplit;
+  bool keepEmpty;
+  std::vector<std::string> expected;
+};
+
+TEST(StructuredDataTest, ArraySplitString) {
+  ArraySplitStringTestCase test_cases[] = {
+      // Happy path
+      {
+          "1,2,,3",
+          ',',
+          -1,
+          true,
+          {"1", "2", "", "3"},
+      },
+      // No splits
+      {
+          "1,2,,3",
+          ',',
+          0,
+          true,
+          {"1,2,,3"},
+      },
+      // 1 split
+      {
+          "1,2,,3",
+          ',',
+          1,
+          true,
+          {"1", "2,,3"},
+      },
+      // No empty substrings
+      {
+          "1,2,,3",
+          ',',
+          -1,
+          false,
+          {"1", "2", "3"},
+      },
+      // Empty substrings count towards splits
+      {
+          ",1,2,3",
+          ',',
+          1,
+          false,
+          {"1,2,3"},
+      },
+  };
+  for (const auto &test_case : test_cases) {
+    auto array = StructuredData::Array::SplitString(
+        test_case.s, test_case.separator, test_case.maxSplit,
+        test_case.keepEmpty);
+    EXPECT_EQ(test_case.expected.size(), array->GetSize());
+    for (unsigned int i = 0; i < test_case.expected.size(); ++i) {
+      EXPECT_EQ(test_case.expected[i], array->GetItemAtIndexAsString(i)->str());
+    }
+  }
+}

>From 6c6c5c272511f8c62c7ef14eefe67904875b4d2c Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Wed, 1 May 2024 17:07:12 -0700
Subject: [PATCH 08/19] Fix format

---
 lldb/include/lldb/Utility/StructuredData.h | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/lldb/include/lldb/Utility/StructuredData.h b/lldb/include/lldb/Utility/StructuredData.h
index 69db0caca2051..8217d2bf33b80 100644
--- a/lldb/include/lldb/Utility/StructuredData.h
+++ b/lldb/include/lldb/Utility/StructuredData.h
@@ -290,7 +290,8 @@ class StructuredData {
 
     void GetDescription(lldb_private::Stream &s) const override;
 
-    /// Creates an Array of substrings by splitting a string around the occurrences of a separator character.
+    /// Creates an Array of substrings by splitting a string around the
+    /// occurrences of a separator character.
     ///
     /// Note:
     /// * This is almost the same API and implementation as `StringRef::split`.
@@ -304,15 +305,20 @@ class StructuredData {
     ///   The character to split on.
     ///
     /// \param[in] maxSplit
-    ///   The maximum number of times the string is split. If \a maxSplit is >= 0, at most \a maxSplit splits are done and consequently <= \a maxSplit + 1 elements are returned.
+    ///   The maximum number of times the string is split. If \a maxSplit is >=
+    ///   0, at most \a maxSplit splits are done and consequently <= \a maxSplit
+    ///   + 1 elements are returned.
     ///
     /// \param[in] keepEmpty
-    ///   True if empty substrings should be returned. Empty substrings still count when considering \a maxSplit.
+    ///   True if empty substrings should be returned. Empty substrings still
+    ///   count when considering \a maxSplit.
     ///
     /// \return
-    ///   An array containing the substrings. If \a maxSplit == -1 and \a keepEmpty == true, then the concatination of the array forms the input string.
-    static ArraySP SplitString(llvm::StringRef s, char separator, int maxSplit = -1,
-                               bool keepEmpty = true);
+    ///   An array containing the substrings. If \a maxSplit == -1 and \a
+    ///   keepEmpty == true, then the concatination of the array forms the input
+    ///   string.
+    static ArraySP SplitString(llvm::StringRef s, char separator,
+                               int maxSplit = -1, bool keepEmpty = true);
 
   protected:
     typedef std::vector<ObjectSP> collection;

>From fbf8e9ab4cfe232218edb1eb4fe0c22d9a68f4a9 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Wed, 1 May 2024 17:23:14 -0700
Subject: [PATCH 09/19] Improve Python API test reliability

---
 .../interpreter/TestCommandInterpreterAPI.py    | 17 ++++++-----------
 1 file changed, 6 insertions(+), 11 deletions(-)

diff --git a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
index e5cb4a18f7df6..5bb9f579ad13f 100644
--- a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
+++ b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
@@ -125,8 +125,6 @@ def test_structured_transcript(self):
         # listed above, hence trimming to the last parts.
         transcript = transcript[-total_number_of_commands:]
 
-        print(transcript)
-
         # Validate the transcript.
         #
         # The following asserts rely on the exact output format of the
@@ -147,18 +145,15 @@ def test_structured_transcript(self):
                 ],
             })
 
-        # (lldb) breakpoint set -f main.c -l X
-        self.assertEqual(transcript[2],
-            {
-                "command": "breakpoint set -f main.c -l %d" % self.line,
-                "output": [
-                    "Breakpoint 1: where = a.out`main + 29 at main.c:5:5, address = 0x0000000100000f7d",
-                ],
-                "error": [],
-            })
+        # (lldb) breakpoint set -f main.c -l <line>
+        self.assertEqual(transcript[2]["command"], "breakpoint set -f main.c -l %d" % self.line)
+        # Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address = 0x0000000100000f7d
+        self.assertTrue("Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address =" in transcript[2]["output"][0])
+        self.assertEqual(transcript[2]["error"], [])
 
         # (lldb) r
         self.assertEqual(transcript[3]["command"], "r")
+        # Process 25494 launched: '<path>/TestCommandInterpreterAPI.test_structured_transcript/a.out' (x86_64)
         self.assertTrue("Process" in transcript[3]["output"][0])
         self.assertTrue("launched" in transcript[3]["output"][0])
         self.assertEqual(transcript[3]["error"], [])

>From 836a719f0f2124627ebb0ea625486bcc8cdf3cd5 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Thu, 2 May 2024 21:08:46 -0700
Subject: [PATCH 10/19] Update implementation of Array::SplitString, and other
 smaller changes

---
 lldb/include/lldb/API/SBCommandInterpreter.h  |  2 +-
 .../lldb/Interpreter/CommandInterpreter.h     |  2 +-
 lldb/include/lldb/Utility/StructuredData.h    | 11 ++------
 .../source/Interpreter/CommandInterpreter.cpp |  6 ++--
 lldb/source/Utility/StructuredData.cpp        | 28 +++++--------------
 5 files changed, 15 insertions(+), 34 deletions(-)

diff --git a/lldb/include/lldb/API/SBCommandInterpreter.h b/lldb/include/lldb/API/SBCommandInterpreter.h
index d65f06d676f91..f56f7d844b0d1 100644
--- a/lldb/include/lldb/API/SBCommandInterpreter.h
+++ b/lldb/include/lldb/API/SBCommandInterpreter.h
@@ -321,7 +321,7 @@ class SBCommandInterpreter {
   /// Returns a list of handled commands, output and error. Each element in
   /// the list is a dictionary with three keys: "command" (string), "output"
   /// (list of strings) and optionally "error" (list of strings). Each string
-  /// in "output" and "error" is a line (without EOL characteres).
+  /// in "output" and "error" is a line (without EOL characters).
   SBStructuredData GetTranscript();
 
 protected:
diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index c0846db8f2b8a..0938ad6ae78ab 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -773,7 +773,7 @@ class CommandInterpreter : public Broadcaster,
   /// the list is a dictionary with three keys: "command" (string), "output"
   /// (list of strings) and optionally "error" (list of strings). Each string
   /// in "output" and "error" is a line (without EOL characters).
-  StructuredData::ArraySP m_transcript_structured;
+  StructuredData::ArraySP m_transcript;
 };
 
 } // namespace lldb_private
diff --git a/lldb/include/lldb/Utility/StructuredData.h b/lldb/include/lldb/Utility/StructuredData.h
index 8217d2bf33b80..563eb1b1a284a 100644
--- a/lldb/include/lldb/Utility/StructuredData.h
+++ b/lldb/include/lldb/Utility/StructuredData.h
@@ -293,11 +293,6 @@ class StructuredData {
     /// Creates an Array of substrings by splitting a string around the
     /// occurrences of a separator character.
     ///
-    /// Note:
-    /// * This is almost the same API and implementation as `StringRef::split`.
-    /// * Not depending on `StringRef::split` because it will involve a
-    ///   temporary `SmallVectorImpl`.
-    ///
     /// \param[in] s
     ///   The input string.
     ///
@@ -396,10 +391,10 @@ class StructuredData {
   class String : public Object {
   public:
     String() : Object(lldb::eStructuredDataTypeString) {}
-    explicit String(llvm::StringRef s)
-        : Object(lldb::eStructuredDataTypeString), m_value(s) {}
+    explicit String(llvm::StringRef S)
+        : Object(lldb::eStructuredDataTypeString), m_value(S) {}
 
-    void SetValue(llvm::StringRef s) { m_value = std::string(s); }
+    void SetValue(llvm::StringRef S) { m_value = std::string(S); }
 
     llvm::StringRef GetValue() { return m_value; }
 
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index 1ec1da437ba3a..d5e37dee186a6 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -137,7 +137,7 @@ CommandInterpreter::CommandInterpreter(Debugger &debugger,
       m_comment_char('#'), m_batch_command_mode(false),
       m_truncation_warning(eNoOmission), m_max_depth_warning(eNoOmission),
       m_command_source_depth(0),
-      m_transcript_structured(std::make_shared<StructuredData::Array>()) {
+      m_transcript(std::make_shared<StructuredData::Array>()) {
   SetEventName(eBroadcastBitThreadShouldExit, "thread-should-exit");
   SetEventName(eBroadcastBitResetPrompt, "reset-prompt");
   SetEventName(eBroadcastBitQuitCommandReceived, "quit");
@@ -1897,7 +1897,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
   // the command.
   auto transcript_item = std::make_shared<StructuredData::Dictionary>();
   transcript_item->AddStringItem("command", command_line);
-  m_transcript_structured->AddItem(transcript_item);
+  m_transcript->AddItem(transcript_item);
 
   bool empty_command = false;
   bool comment_command = false;
@@ -3573,5 +3573,5 @@ llvm::json::Value CommandInterpreter::GetStatistics() {
 }
 
 StructuredData::ArraySP CommandInterpreter::GetTranscript() const {
-  return m_transcript_structured;
+  return m_transcript;
 }
diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index 7fa1063e5f01f..e5f9a69fab2ab 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -295,28 +295,14 @@ StructuredData::ArraySP StructuredData::Array::SplitString(llvm::StringRef s,
                                                            char separator,
                                                            int maxSplit,
                                                            bool keepEmpty) {
-  auto array_sp = std::make_shared<StructuredData::Array>();
+  // Split the string into a small vector.
+  llvm::SmallVector<StringRef> small_vec;
+  s.split(small_vec, separator, maxSplit, keepEmpty);
 
-  // Count down from `maxSplit`. When `maxSplit` is -1, this will just split
-  // "forever". This doesn't support splitting more than 2^31 times
-  // intentionally; if we ever want that we can make `maxSplit` a 64-bit integer
-  // but that seems unlikely to be useful.
-  while (maxSplit-- != 0) {
-    size_t idx = s.find(separator);
-    if (idx == llvm::StringLiteral::npos)
-      break;
-
-    // Push this split.
-    if (keepEmpty || idx > 0)
-      array_sp->AddStringItem(s.slice(0, idx));
-
-    // Jump forward.
-    s = s.slice(idx + 1, llvm::StringLiteral::npos);
+  // Copy the substrings from the small vector into the output array.
+  auto array_sp = std::make_shared<StructuredData::Array>();
+  for (auto substring : small_vec) {
+    array_sp->AddStringItem(std::move(substring));
   }
-
-  // Push the tail.
-  if (keepEmpty || !s.empty())
-    array_sp->AddStringItem(s);
-
   return array_sp;
 }

>From 6f82462d7dff7c5d97c58a84b4eac9aca8d7fc99 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Mon, 6 May 2024 10:47:47 -0700
Subject: [PATCH 11/19] Update comment, more reliable test, minor fix

---
 lldb/source/Utility/StructuredData.cpp            |  2 +-
 .../interpreter/TestCommandInterpreterAPI.py      | 15 +++++++++++----
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index e5f9a69fab2ab..f4e87b7167a1f 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -302,7 +302,7 @@ StructuredData::ArraySP StructuredData::Array::SplitString(llvm::StringRef s,
   // Copy the substrings from the small vector into the output array.
   auto array_sp = std::make_shared<StructuredData::Array>();
   for (auto substring : small_vec) {
-    array_sp->AddStringItem(std::move(substring));
+    array_sp->AddStringItem(substring);
   }
   return array_sp;
 }
diff --git a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
index 5bb9f579ad13f..54179892eabda 100644
--- a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
+++ b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
@@ -120,9 +120,16 @@ def test_structured_transcript(self):
 
         transcript = json.loads(stream.GetData())
 
-        # The transcript will contain a bunch of commands that are run
-        # automatically. We only want to validate for the ones that are
-        # listed above, hence trimming to the last parts.
+        print('TRANSCRIPT')
+        print(transcript)
+
+        # The transcript will contain a bunch of commands that are from
+        # a general setup code. See `def setUpCommands(cls)` in
+        # `lldb/packages/Python/lldbsuite/test/lldbtest.py`.
+        # https://shorturl.at/bJKVW
+        #
+        # We only want to validate for the ones that are listed above, hence
+        # trimming to the last parts.
         transcript = transcript[-total_number_of_commands:]
 
         # Validate the transcript.
@@ -148,7 +155,7 @@ def test_structured_transcript(self):
         # (lldb) breakpoint set -f main.c -l <line>
         self.assertEqual(transcript[2]["command"], "breakpoint set -f main.c -l %d" % self.line)
         # Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address = 0x0000000100000f7d
-        self.assertTrue("Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address =" in transcript[2]["output"][0])
+        self.assertTrue("Breakpoint 1: where = a.out`main " in transcript[2]["output"][0])
         self.assertEqual(transcript[2]["error"], [])
 
         # (lldb) r

>From b00905d4b2d370fe05c277ad09c6325265d89f44 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Fri, 10 May 2024 13:19:16 -0700
Subject: [PATCH 12/19] Remove Array::SplitString + add test for JSON output

---
 lldb/include/lldb/Utility/StructuredData.h    | 25 ---------
 .../source/Interpreter/CommandInterpreter.cpp | 12 ++---
 lldb/source/Utility/StructuredData.cpp        | 16 ------
 .../interpreter/TestCommandInterpreterAPI.py  | 46 ++++++++--------
 lldb/unittests/Utility/StructuredDataTest.cpp | 54 -------------------
 5 files changed, 29 insertions(+), 124 deletions(-)

diff --git a/lldb/include/lldb/Utility/StructuredData.h b/lldb/include/lldb/Utility/StructuredData.h
index 563eb1b1a284a..78e3234e34f2a 100644
--- a/lldb/include/lldb/Utility/StructuredData.h
+++ b/lldb/include/lldb/Utility/StructuredData.h
@@ -290,31 +290,6 @@ class StructuredData {
 
     void GetDescription(lldb_private::Stream &s) const override;
 
-    /// Creates an Array of substrings by splitting a string around the
-    /// occurrences of a separator character.
-    ///
-    /// \param[in] s
-    ///   The input string.
-    ///
-    /// \param[in] separator
-    ///   The character to split on.
-    ///
-    /// \param[in] maxSplit
-    ///   The maximum number of times the string is split. If \a maxSplit is >=
-    ///   0, at most \a maxSplit splits are done and consequently <= \a maxSplit
-    ///   + 1 elements are returned.
-    ///
-    /// \param[in] keepEmpty
-    ///   True if empty substrings should be returned. Empty substrings still
-    ///   count when considering \a maxSplit.
-    ///
-    /// \return
-    ///   An array containing the substrings. If \a maxSplit == -1 and \a
-    ///   keepEmpty == true, then the concatination of the array forms the input
-    ///   string.
-    static ArraySP SplitString(llvm::StringRef s, char separator,
-                               int maxSplit = -1, bool keepEmpty = true);
-
   protected:
     typedef std::vector<ObjectSP> collection;
     collection m_items;
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index d5e37dee186a6..0f5900a135a52 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -2052,14 +2052,10 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
   m_transcript_stream << result.GetOutputData();
   m_transcript_stream << result.GetErrorData();
 
-  // Add output and error to the transcript item after splitting lines. In the
-  // future, other aspects of the command (e.g. perf) can be added, too.
-  transcript_item->AddItem(
-      "output", StructuredData::Array::SplitString(result.GetOutputData(), '\n',
-                                                   -1, false));
-  transcript_item->AddItem(
-      "error", StructuredData::Array::SplitString(result.GetErrorData(), '\n',
-                                                  -1, false));
+  // Add output and error to the transcript item. In the future, other aspects
+  // of the command (e.g. perf) can be added, too.
+  transcript_item->AddStringItem("output", result.GetOutputData());
+  transcript_item->AddStringItem("error", result.GetErrorData());
 
   return result.Succeeded();
 }
diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index f4e87b7167a1f..aab2ed32f3133 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -290,19 +290,3 @@ void StructuredData::Null::GetDescription(lldb_private::Stream &s) const {
 void StructuredData::Generic::GetDescription(lldb_private::Stream &s) const {
   s.Printf("%p", m_object);
 }
-
-StructuredData::ArraySP StructuredData::Array::SplitString(llvm::StringRef s,
-                                                           char separator,
-                                                           int maxSplit,
-                                                           bool keepEmpty) {
-  // Split the string into a small vector.
-  llvm::SmallVector<StringRef> small_vec;
-  s.split(small_vec, separator, maxSplit, keepEmpty);
-
-  // Copy the substrings from the small vector into the output array.
-  auto array_sp = std::make_shared<StructuredData::Array>();
-  for (auto substring : small_vec) {
-    array_sp->AddStringItem(substring);
-  }
-  return array_sp;
-}
diff --git a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
index 54179892eabda..d29cee8d0141c 100644
--- a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
+++ b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
@@ -106,7 +106,8 @@ def test_structured_transcript(self):
         ci.HandleCommand("breakpoint set -f main.c -l %d" % self.line, res)
         ci.HandleCommand("r", res)
         ci.HandleCommand("p a", res)
-        total_number_of_commands = 5
+        ci.HandleCommand("statistics dump", res)
+        total_number_of_commands = 6
 
         # Retrieve the transcript and convert it into a Python object
         transcript = ci.GetTranscript()
@@ -120,9 +121,6 @@ def test_structured_transcript(self):
 
         transcript = json.loads(stream.GetData())
 
-        print('TRANSCRIPT')
-        print(transcript)
-
         # The transcript will contain a bunch of commands that are from
         # a general setup code. See `def setUpCommands(cls)` in
         # `lldb/packages/Python/lldbsuite/test/lldbtest.py`.
@@ -132,45 +130,51 @@ def test_structured_transcript(self):
         # trimming to the last parts.
         transcript = transcript[-total_number_of_commands:]
 
-        # Validate the transcript.
+        # The following validates individual commands in the transcript.
         #
-        # The following asserts rely on the exact output format of the
+        # Note: Some of the asserts rely on the exact output format of the
         # commands. Hopefully we are not changing them any time soon.
 
         # (lldb) version
         self.assertEqual(transcript[0]["command"], "version")
-        self.assertTrue("lldb version" in transcript[0]["output"][0])
-        self.assertEqual(transcript[0]["error"], [])
+        self.assertTrue("lldb version" in transcript[0]["output"])
+        self.assertEqual(transcript[0]["error"], "")
 
         # (lldb) an-unknown-command
         self.assertEqual(transcript[1],
             {
                 "command": "an-unknown-command",
-                "output": [],
-                "error": [
-                    "error: 'an-unknown-command' is not a valid command.",
-                ],
+                "output": "",
+                "error": "error: 'an-unknown-command' is not a valid command.\n",
             })
 
         # (lldb) breakpoint set -f main.c -l <line>
         self.assertEqual(transcript[2]["command"], "breakpoint set -f main.c -l %d" % self.line)
         # Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address = 0x0000000100000f7d
-        self.assertTrue("Breakpoint 1: where = a.out`main " in transcript[2]["output"][0])
-        self.assertEqual(transcript[2]["error"], [])
+        self.assertTrue("Breakpoint 1: where = a.out`main " in transcript[2]["output"])
+        self.assertEqual(transcript[2]["error"], "")
 
         # (lldb) r
         self.assertEqual(transcript[3]["command"], "r")
         # Process 25494 launched: '<path>/TestCommandInterpreterAPI.test_structured_transcript/a.out' (x86_64)
-        self.assertTrue("Process" in transcript[3]["output"][0])
-        self.assertTrue("launched" in transcript[3]["output"][0])
-        self.assertEqual(transcript[3]["error"], [])
+        self.assertTrue("Process" in transcript[3]["output"])
+        self.assertTrue("launched" in transcript[3]["output"])
+        self.assertEqual(transcript[3]["error"], "")
 
         # (lldb) p a
         self.assertEqual(transcript[4],
             {
                 "command": "p a",
-                "output": [
-                    "(int) 123",
-                ],
-                "error": [],
+                "output": "(int) 123\n",
+                "error": "",
             })
+
+        # (lldb) statistics dump
+        statistics_dump = json.loads(transcript[5]["output"])
+        # Dump result should be valid JSON
+        self.assertTrue(statistics_dump is not json.JSONDecodeError)
+        # Dump result should contain expected fields
+        self.assertTrue("commands" in statistics_dump)
+        self.assertTrue("memory" in statistics_dump)
+        self.assertTrue("modules" in statistics_dump)
+        self.assertTrue("targets" in statistics_dump)
diff --git a/lldb/unittests/Utility/StructuredDataTest.cpp b/lldb/unittests/Utility/StructuredDataTest.cpp
index f107490946334..411ebebb67585 100644
--- a/lldb/unittests/Utility/StructuredDataTest.cpp
+++ b/lldb/unittests/Utility/StructuredDataTest.cpp
@@ -121,57 +121,3 @@ struct ArraySplitStringTestCase {
   bool keepEmpty;
   std::vector<std::string> expected;
 };
-
-TEST(StructuredDataTest, ArraySplitString) {
-  ArraySplitStringTestCase test_cases[] = {
-      // Happy path
-      {
-          "1,2,,3",
-          ',',
-          -1,
-          true,
-          {"1", "2", "", "3"},
-      },
-      // No splits
-      {
-          "1,2,,3",
-          ',',
-          0,
-          true,
-          {"1,2,,3"},
-      },
-      // 1 split
-      {
-          "1,2,,3",
-          ',',
-          1,
-          true,
-          {"1", "2,,3"},
-      },
-      // No empty substrings
-      {
-          "1,2,,3",
-          ',',
-          -1,
-          false,
-          {"1", "2", "3"},
-      },
-      // Empty substrings count towards splits
-      {
-          ",1,2,3",
-          ',',
-          1,
-          false,
-          {"1,2,3"},
-      },
-  };
-  for (const auto &test_case : test_cases) {
-    auto array = StructuredData::Array::SplitString(
-        test_case.s, test_case.separator, test_case.maxSplit,
-        test_case.keepEmpty);
-    EXPECT_EQ(test_case.expected.size(), array->GetSize());
-    for (unsigned int i = 0; i < test_case.expected.size(); ++i) {
-      EXPECT_EQ(test_case.expected[i], array->GetItemAtIndexAsString(i)->str());
-    }
-  }
-}

>From e2e26d83f270e5989f05f4bbe65760ffa8f1a036 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Fri, 10 May 2024 13:26:13 -0700
Subject: [PATCH 13/19] Clean-ups

---
 lldb/include/lldb/Utility/StructuredData.h    | 2 +-
 lldb/source/Utility/StructuredData.cpp        | 1 -
 lldb/unittests/Utility/StructuredDataTest.cpp | 9 ---------
 3 files changed, 1 insertion(+), 11 deletions(-)

diff --git a/lldb/include/lldb/Utility/StructuredData.h b/lldb/include/lldb/Utility/StructuredData.h
index 78e3234e34f2a..5e63ef92fac3e 100644
--- a/lldb/include/lldb/Utility/StructuredData.h
+++ b/lldb/include/lldb/Utility/StructuredData.h
@@ -432,7 +432,7 @@ class StructuredData {
       }
       return success;
     }
-
+      
     template <class IntType>
     bool GetValueForKeyAsInteger(llvm::StringRef key, IntType &result) const {
       ObjectSP value_sp = GetValueForKey(key);
diff --git a/lldb/source/Utility/StructuredData.cpp b/lldb/source/Utility/StructuredData.cpp
index aab2ed32f3133..7686d052c599c 100644
--- a/lldb/source/Utility/StructuredData.cpp
+++ b/lldb/source/Utility/StructuredData.cpp
@@ -10,7 +10,6 @@
 #include "lldb/Utility/FileSpec.h"
 #include "lldb/Utility/Status.h"
 #include "llvm/ADT/StringExtras.h"
-#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/MemoryBuffer.h"
 #include <cerrno>
 #include <cinttypes>
diff --git a/lldb/unittests/Utility/StructuredDataTest.cpp b/lldb/unittests/Utility/StructuredDataTest.cpp
index 411ebebb67585..e536039f365a4 100644
--- a/lldb/unittests/Utility/StructuredDataTest.cpp
+++ b/lldb/unittests/Utility/StructuredDataTest.cpp
@@ -12,7 +12,6 @@
 #include "lldb/Utility/Status.h"
 #include "lldb/Utility/StreamString.h"
 #include "lldb/Utility/StructuredData.h"
-#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Path.h"
 
 using namespace lldb;
@@ -113,11 +112,3 @@ TEST(StructuredDataTest, ParseJSONFromFile) {
   object_sp->Dump(S, false);
   EXPECT_EQ("[1,2,3]", S.GetString());
 }
-
-struct ArraySplitStringTestCase {
-  llvm::StringRef s;
-  char separator;
-  int maxSplit;
-  bool keepEmpty;
-  std::vector<std::string> expected;
-};

>From e21855f1a4cebc0f9f1d92b959f17c1b1cc298a6 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Mon, 13 May 2024 15:07:15 -0700
Subject: [PATCH 14/19] Use assertIn in python test

---
 .../interpreter/TestCommandInterpreterAPI.py     | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
index d29cee8d0141c..52506e7dc7bc7 100644
--- a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
+++ b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
@@ -137,7 +137,7 @@ def test_structured_transcript(self):
 
         # (lldb) version
         self.assertEqual(transcript[0]["command"], "version")
-        self.assertTrue("lldb version" in transcript[0]["output"])
+        self.assertIn("lldb version", transcript[0]["output"])
         self.assertEqual(transcript[0]["error"], "")
 
         # (lldb) an-unknown-command
@@ -151,14 +151,14 @@ def test_structured_transcript(self):
         # (lldb) breakpoint set -f main.c -l <line>
         self.assertEqual(transcript[2]["command"], "breakpoint set -f main.c -l %d" % self.line)
         # Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address = 0x0000000100000f7d
-        self.assertTrue("Breakpoint 1: where = a.out`main " in transcript[2]["output"])
+        self.assertIn("Breakpoint 1: where = a.out`main ", transcript[2]["output"])
         self.assertEqual(transcript[2]["error"], "")
 
         # (lldb) r
         self.assertEqual(transcript[3]["command"], "r")
         # Process 25494 launched: '<path>/TestCommandInterpreterAPI.test_structured_transcript/a.out' (x86_64)
-        self.assertTrue("Process" in transcript[3]["output"])
-        self.assertTrue("launched" in transcript[3]["output"])
+        self.assertIn("Process", transcript[3]["output"])
+        self.assertIn("launched", transcript[3]["output"])
         self.assertEqual(transcript[3]["error"], "")
 
         # (lldb) p a
@@ -174,7 +174,7 @@ def test_structured_transcript(self):
         # Dump result should be valid JSON
         self.assertTrue(statistics_dump is not json.JSONDecodeError)
         # Dump result should contain expected fields
-        self.assertTrue("commands" in statistics_dump)
-        self.assertTrue("memory" in statistics_dump)
-        self.assertTrue("modules" in statistics_dump)
-        self.assertTrue("targets" in statistics_dump)
+        self.assertIn("commands", statistics_dump)
+        self.assertIn("memory", statistics_dump)
+        self.assertIn("modules", statistics_dump)
+        self.assertIn("targets", statistics_dump)

>From 7a240d92a2d12f27499a43e8330d10d8030e2157 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Thu, 16 May 2024 21:56:11 -0700
Subject: [PATCH 15/19] Add a setting; add elapsed seconds; make a copy of the
 returned StructuredData::Array; add tests

---
 lldb/include/lldb/API/SBCommandInterpreter.h  |   8 +-
 .../lldb/Interpreter/CommandInterpreter.h     |   7 +-
 lldb/source/API/SBCommandInterpreter.cpp      |   4 +-
 .../source/Interpreter/CommandInterpreter.cpp |  48 ++++--
 .../Interpreter/InterpreterProperties.td      |   4 +
 .../interpreter/TestCommandInterpreterAPI.py  | 139 ++++++++++++++----
 6 files changed, 157 insertions(+), 53 deletions(-)

diff --git a/lldb/include/lldb/API/SBCommandInterpreter.h b/lldb/include/lldb/API/SBCommandInterpreter.h
index f56f7d844b0d1..a82f735c012ae 100644
--- a/lldb/include/lldb/API/SBCommandInterpreter.h
+++ b/lldb/include/lldb/API/SBCommandInterpreter.h
@@ -319,9 +319,11 @@ class SBCommandInterpreter {
   SBStructuredData GetStatistics();
 
   /// Returns a list of handled commands, output and error. Each element in
-  /// the list is a dictionary with three keys: "command" (string), "output"
-  /// (list of strings) and optionally "error" (list of strings). Each string
-  /// in "output" and "error" is a line (without EOL characters).
+  /// the list is a dictionary with the following keys/values:
+  /// - "command" (string): The command that was executed.
+  /// - "output" (string): The output of the command. Empty ("") if no output.
+  /// - "error" (string): The error of the command. Empty ("") if no error.
+  /// - "seconds" (float): The time it took to execute the command.
   SBStructuredData GetTranscript();
 
 protected:
diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index 0938ad6ae78ab..6d49ae1af1b8f 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -561,6 +561,9 @@ class CommandInterpreter : public Broadcaster,
   bool GetPromptOnQuit() const;
   void SetPromptOnQuit(bool enable);
 
+  bool GetSaveTranscript() const;
+  void SetSaveTranscript(bool enable);
+
   bool GetSaveSessionOnQuit() const;
   void SetSaveSessionOnQuit(bool enable);
 
@@ -648,7 +651,7 @@ class CommandInterpreter : public Broadcaster,
   }
 
   llvm::json::Value GetStatistics();
-  StructuredData::ArraySP GetTranscript() const;
+  const StructuredData::Array& GetTranscript() const;
 
 protected:
   friend class Debugger;
@@ -773,7 +776,7 @@ class CommandInterpreter : public Broadcaster,
   /// the list is a dictionary with three keys: "command" (string), "output"
   /// (list of strings) and optionally "error" (list of strings). Each string
   /// in "output" and "error" is a line (without EOL characters).
-  StructuredData::ArraySP m_transcript;
+  StructuredData::Array m_transcript;
 };
 
 } // namespace lldb_private
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index 233a2f97fb9f1..6a912f0d108b8 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -6,6 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "lldb/Utility/StructuredData.h"
 #include "lldb/lldb-types.h"
 
 #include "lldb/Interpreter/CommandInterpreter.h"
@@ -576,7 +577,8 @@ SBStructuredData SBCommandInterpreter::GetTranscript() {
 
   SBStructuredData data;
   if (IsValid())
-    data.m_impl_up->SetObjectSP(m_opaque_ptr->GetTranscript());
+    // A deep copy is performed by `std::make_shared` on the `StructuredData::Array`, via its implicitly-declared copy constructor. This ensures thread-safety between the user changing the returned `SBStructuredData` and the `CommandInterpreter` changing its internal `m_transcript`.
+    data.m_impl_up->SetObjectSP(std::make_shared<StructuredData::Array>(m_opaque_ptr->GetTranscript()));
   return data;
 }
 
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index 0f5900a135a52..ef2b6d39f3d35 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -136,8 +136,7 @@ CommandInterpreter::CommandInterpreter(Debugger &debugger,
       m_skip_lldbinit_files(false), m_skip_app_init_files(false),
       m_comment_char('#'), m_batch_command_mode(false),
       m_truncation_warning(eNoOmission), m_max_depth_warning(eNoOmission),
-      m_command_source_depth(0),
-      m_transcript(std::make_shared<StructuredData::Array>()) {
+      m_command_source_depth(0) {
   SetEventName(eBroadcastBitThreadShouldExit, "thread-should-exit");
   SetEventName(eBroadcastBitResetPrompt, "reset-prompt");
   SetEventName(eBroadcastBitQuitCommandReceived, "quit");
@@ -163,6 +162,17 @@ void CommandInterpreter::SetPromptOnQuit(bool enable) {
   SetPropertyAtIndex(idx, enable);
 }
 
+bool CommandInterpreter::GetSaveTranscript() const {
+  const uint32_t idx = ePropertySaveTranscript;
+  return GetPropertyAtIndexAs<bool>(
+      idx, g_interpreter_properties[idx].default_uint_value != 0);
+}
+
+void CommandInterpreter::SetSaveTranscript(bool enable) {
+  const uint32_t idx = ePropertySaveTranscript;
+  SetPropertyAtIndex(idx, enable);
+}
+
 bool CommandInterpreter::GetSaveSessionOnQuit() const {
   const uint32_t idx = ePropertySaveSessionOnQuit;
   return GetPropertyAtIndexAs<bool>(
@@ -1891,13 +1901,16 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
   else
     add_to_history = (lazy_add_to_history == eLazyBoolYes);
 
-  m_transcript_stream << "(lldb) " << command_line << '\n';
-
   // The same `transcript_item` will be used below to add output and error of
   // the command.
-  auto transcript_item = std::make_shared<StructuredData::Dictionary>();
-  transcript_item->AddStringItem("command", command_line);
-  m_transcript->AddItem(transcript_item);
+  StructuredData::DictionarySP transcript_item;
+  if (GetSaveTranscript()) {
+    m_transcript_stream << "(lldb) " << command_line << '\n';
+
+    transcript_item = std::make_shared<StructuredData::Dictionary>();
+    transcript_item->AddStringItem("command", command_line);
+    m_transcript.AddItem(transcript_item);
+  }
 
   bool empty_command = false;
   bool comment_command = false;
@@ -2002,7 +2015,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
   // Take care of things like setting up the history command & calling the
   // appropriate Execute method on the CommandObject, with the appropriate
   // arguments.
-
+  StatsDuration execute_time;
   if (cmd_obj != nullptr) {
     bool generate_repeat_command = add_to_history;
     // If we got here when empty_command was true, then this command is a
@@ -2043,19 +2056,24 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
         log, "HandleCommand, command line after removing command name(s): '%s'",
         remainder.c_str());
 
+    ElapsedTime elapsed(execute_time);
     cmd_obj->Execute(remainder.c_str(), result);
   }
 
   LLDB_LOGF(log, "HandleCommand, command %s",
             (result.Succeeded() ? "succeeded" : "did not succeed"));
 
-  m_transcript_stream << result.GetOutputData();
-  m_transcript_stream << result.GetErrorData();
+  // To test whether or not transcript should be saved, `transcript_item` is
+  // used instead of `GetSaveTrasncript()`. This is because the latter will
+  // fail when the command is "settings set interpreter.save-transcript true".
+  if (transcript_item) {
+    m_transcript_stream << result.GetOutputData();
+    m_transcript_stream << result.GetErrorData();
 
-  // Add output and error to the transcript item. In the future, other aspects
-  // of the command (e.g. perf) can be added, too.
-  transcript_item->AddStringItem("output", result.GetOutputData());
-  transcript_item->AddStringItem("error", result.GetErrorData());
+    transcript_item->AddStringItem("output", result.GetOutputData());
+    transcript_item->AddStringItem("error", result.GetErrorData());
+    transcript_item->AddFloatItem("seconds", execute_time.get().count());
+  }
 
   return result.Succeeded();
 }
@@ -3568,6 +3586,6 @@ llvm::json::Value CommandInterpreter::GetStatistics() {
   return stats;
 }
 
-StructuredData::ArraySP CommandInterpreter::GetTranscript() const {
+const StructuredData::Array& CommandInterpreter::GetTranscript() const {
   return m_transcript;
 }
diff --git a/lldb/source/Interpreter/InterpreterProperties.td b/lldb/source/Interpreter/InterpreterProperties.td
index 2155ee61ccffb..a5fccbbca091c 100644
--- a/lldb/source/Interpreter/InterpreterProperties.td
+++ b/lldb/source/Interpreter/InterpreterProperties.td
@@ -9,6 +9,10 @@ let Definition = "interpreter" in {
     Global,
     DefaultTrue,
     Desc<"If true, LLDB will prompt you before quitting if there are any live processes being debugged. If false, LLDB will quit without asking in any case.">;
+  def SaveTranscript: Property<"save-transcript", "Boolean">,
+    Global,
+    DefaultFalse,
+    Desc<"If true, commands will be saved into a transcript buffer for user access.">;
   def SaveSessionOnQuit: Property<"save-session-on-quit", "Boolean">,
     Global,
     DefaultFalse,
diff --git a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
index 52506e7dc7bc7..95643eef0d344 100644
--- a/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
+++ b/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
@@ -16,8 +16,7 @@ def setUp(self):
         # Find the line number to break on inside main.cpp.
         self.line = line_number("main.c", "Hello world.")
 
-    def test_with_process_launch_api(self):
-        """Test the SBCommandInterpreter APIs."""
+    def buildAndCreateTarget(self):
         self.build()
         exe = self.getBuildArtifact("a.out")
 
@@ -28,6 +27,11 @@ def test_with_process_launch_api(self):
         # Retrieve the associated command interpreter from our debugger.
         ci = self.dbg.GetCommandInterpreter()
         self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
+        return ci
+
+    def test_with_process_launch_api(self):
+        """Test the SBCommandInterpreter APIs."""
+        ci = self.buildAndCreateTarget()
 
         # Exercise some APIs....
 
@@ -87,19 +91,30 @@ def test_command_output(self):
         self.assertIsNotNone(res.GetError())
         self.assertEqual(res.GetError(), "")
 
+    def getTranscriptAsPythonObject(self, ci):
+        """Retrieve the transcript and convert it into a Python object"""
+        structured_data = ci.GetTranscript()
+        self.assertTrue(structured_data.IsValid())
+
+        stream = lldb.SBStream()
+        self.assertTrue(stream)
+
+        error = structured_data.GetAsJSON(stream)
+        self.assertSuccess(error)
+
+        return json.loads(stream.GetData())
+
     def test_structured_transcript(self):
         """Test structured transcript generation and retrieval."""
-        # Get command interpreter and create a target
-        self.build()
-        exe = self.getBuildArtifact("a.out")
-
-        target = self.dbg.CreateTarget(exe)
-        self.assertTrue(target, VALID_TARGET)
+        ci = self.buildAndCreateTarget()
 
-        ci = self.dbg.GetCommandInterpreter()
-        self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
+        # Make sure the "save-transcript" setting is on
+        self.runCmd("settings set interpreter.save-transcript true")
 
-        # Send a few commands through the command interpreter
+        # Send a few commands through the command interpreter.
+        #
+        # Using `ci.HandleCommand` because some commands will fail so that we
+        # can test the "error" field in the saved transcript.
         res = lldb.SBCommandReturnObject()
         ci.HandleCommand("version", res)
         ci.HandleCommand("an-unknown-command", res)
@@ -109,31 +124,25 @@ def test_structured_transcript(self):
         ci.HandleCommand("statistics dump", res)
         total_number_of_commands = 6
 
-        # Retrieve the transcript and convert it into a Python object
-        transcript = ci.GetTranscript()
-        self.assertTrue(transcript.IsValid())
+        # Get transcript as python object
+        transcript = self.getTranscriptAsPythonObject(ci)
 
-        stream = lldb.SBStream()
-        self.assertTrue(stream)
-
-        error = transcript.GetAsJSON(stream)
-        self.assertSuccess(error)
-
-        transcript = json.loads(stream.GetData())
-
-        # The transcript will contain a bunch of commands that are from
-        # a general setup code. See `def setUpCommands(cls)` in
-        # `lldb/packages/Python/lldbsuite/test/lldbtest.py`.
-        # https://shorturl.at/bJKVW
-        #
-        # We only want to validate for the ones that are listed above, hence
-        # trimming to the last parts.
-        transcript = transcript[-total_number_of_commands:]
+        # All commands should have expected fields.
+        for command in transcript:
+            self.assertIn("command", command)
+            self.assertIn("output", command)
+            self.assertIn("error", command)
+            self.assertIn("seconds", command)
 
         # The following validates individual commands in the transcript.
         #
-        # Note: Some of the asserts rely on the exact output format of the
-        # commands. Hopefully we are not changing them any time soon.
+        # Notes:
+        # 1. Some of the asserts rely on the exact output format of the
+        #    commands. Hopefully we are not changing them any time soon.
+        # 2. We are removing the "seconds" field from each command, so that
+        #    some of the validations below can be easier / more readable.
+        for command in transcript:
+            del(command["seconds"])
 
         # (lldb) version
         self.assertEqual(transcript[0]["command"], "version")
@@ -178,3 +187,69 @@ def test_structured_transcript(self):
         self.assertIn("memory", statistics_dump)
         self.assertIn("modules", statistics_dump)
         self.assertIn("targets", statistics_dump)
+
+    def test_save_transcript_setting_default(self):
+        ci = self.buildAndCreateTarget()
+        res = lldb.SBCommandReturnObject()
+
+        # The setting's default value should be "false"
+        self.runCmd("settings show interpreter.save-transcript", "interpreter.save-transcript (boolean) = false\n")
+        # self.assertEqual(res.GetOutput(), )
+
+    def test_save_transcript_setting_off(self):
+        ci = self.buildAndCreateTarget()
+
+        # Make sure the setting is off
+        self.runCmd("settings set interpreter.save-transcript false")
+
+        # The transcript should be empty after running a command
+        self.runCmd("version")
+        transcript = self.getTranscriptAsPythonObject(ci)
+        self.assertEqual(transcript, [])
+
+    def test_save_transcript_setting_on(self):
+        ci = self.buildAndCreateTarget()
+        res = lldb.SBCommandReturnObject()
+
+        # Make sure the setting is on
+        self.runCmd("settings set interpreter.save-transcript true")
+
+        # The transcript should contain one item after running a command
+        self.runCmd("version")
+        transcript = self.getTranscriptAsPythonObject(ci)
+        self.assertEqual(len(transcript), 1)
+        self.assertEqual(transcript[0]["command"], "version")
+
+    def test_save_transcript_returns_copy(self):
+        """
+        Test that the returned structured data is *at least* a shallow copy.
+
+        We believe that a deep copy *is* performed in `SBCommandInterpreter::GetTranscript`.
+        However, the deep copy cannot be tested and doesn't need to be tested,
+        because there is no logic in the command interpreter to modify a
+        transcript item (representing a command) after it has been returned.
+        """
+        ci = self.buildAndCreateTarget()
+
+        # Make sure the setting is on
+        self.runCmd("settings set interpreter.save-transcript true")
+
+        # Run commands and get the transcript as structured data
+        self.runCmd("version")
+        structured_data_1 = ci.GetTranscript()
+        self.assertTrue(structured_data_1.IsValid())
+        self.assertEqual(structured_data_1.GetSize(), 1)
+        self.assertEqual(structured_data_1.GetItemAtIndex(0).GetValueForKey("command").GetStringValue(100), "version")
+
+        # Run some more commands and get the transcript as structured data again
+        self.runCmd("help")
+        structured_data_2 = ci.GetTranscript()
+        self.assertTrue(structured_data_2.IsValid())
+        self.assertEqual(structured_data_2.GetSize(), 2)
+        self.assertEqual(structured_data_2.GetItemAtIndex(0).GetValueForKey("command").GetStringValue(100), "version")
+        self.assertEqual(structured_data_2.GetItemAtIndex(1).GetValueForKey("command").GetStringValue(100), "help")
+
+        # Now, the first structured data should remain unchanged
+        self.assertTrue(structured_data_1.IsValid())
+        self.assertEqual(structured_data_1.GetSize(), 1)
+        self.assertEqual(structured_data_1.GetItemAtIndex(0).GetValueForKey("command").GetStringValue(100), "version")

>From f4791a61738ae872ef84223cc9f1c152e5382607 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Thu, 16 May 2024 22:35:14 -0700
Subject: [PATCH 16/19] Fix format

---
 lldb/include/lldb/Interpreter/CommandInterpreter.h | 2 +-
 lldb/source/API/SBCommandInterpreter.cpp           | 9 +++++++--
 lldb/source/Interpreter/CommandInterpreter.cpp     | 2 +-
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index 6d49ae1af1b8f..2c6abbefb44c9 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -651,7 +651,7 @@ class CommandInterpreter : public Broadcaster,
   }
 
   llvm::json::Value GetStatistics();
-  const StructuredData::Array& GetTranscript() const;
+  const StructuredData::Array &GetTranscript() const;
 
 protected:
   friend class Debugger;
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index 6a912f0d108b8..5e071874f0111 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -577,8 +577,13 @@ SBStructuredData SBCommandInterpreter::GetTranscript() {
 
   SBStructuredData data;
   if (IsValid())
-    // A deep copy is performed by `std::make_shared` on the `StructuredData::Array`, via its implicitly-declared copy constructor. This ensures thread-safety between the user changing the returned `SBStructuredData` and the `CommandInterpreter` changing its internal `m_transcript`.
-    data.m_impl_up->SetObjectSP(std::make_shared<StructuredData::Array>(m_opaque_ptr->GetTranscript()));
+    // A deep copy is performed by `std::make_shared` on the
+    // `StructuredData::Array`, via its implicitly-declared copy constructor.
+    // This ensures thread-safety between the user changing the returned
+    // `SBStructuredData` and the `CommandInterpreter` changing its internal
+    // `m_transcript`.
+    data.m_impl_up->SetObjectSP(
+        std::make_shared<StructuredData::Array>(m_opaque_ptr->GetTranscript()));
   return data;
 }
 
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index ef2b6d39f3d35..811726e30af4d 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -3586,6 +3586,6 @@ llvm::json::Value CommandInterpreter::GetStatistics() {
   return stats;
 }
 
-const StructuredData::Array& CommandInterpreter::GetTranscript() const {
+const StructuredData::Array &CommandInterpreter::GetTranscript() const {
   return m_transcript;
 }

>From 7ffc57b18cbeaeb904db853b36c8e4422c2c6be4 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Thu, 16 May 2024 22:38:54 -0700
Subject: [PATCH 17/19] Attempt to revert unnecessary format changes

---
 lldb/include/lldb/API/SBCommandInterpreter.h       | 6 +++---
 lldb/include/lldb/Interpreter/CommandInterpreter.h | 4 ++--
 lldb/source/API/SBCommandInterpreter.cpp           | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/lldb/include/lldb/API/SBCommandInterpreter.h b/lldb/include/lldb/API/SBCommandInterpreter.h
index a82f735c012ae..8ac36344b3a79 100644
--- a/lldb/include/lldb/API/SBCommandInterpreter.h
+++ b/lldb/include/lldb/API/SBCommandInterpreter.h
@@ -247,13 +247,13 @@ class SBCommandInterpreter {
                                        lldb::SBStringList &matches,
                                        lldb::SBStringList &descriptions);
 
-  /// Returns whether an interrupt flag was raised either by the SBDebugger -
+  /// Returns whether an interrupt flag was raised either by the SBDebugger - 
   /// when the function is not running on the RunCommandInterpreter thread, or
   /// by SBCommandInterpreter::InterruptCommand if it is.  If your code is doing
-  /// interruptible work, check this API periodically, and interrupt if it
+  /// interruptible work, check this API periodically, and interrupt if it 
   /// returns true.
   bool WasInterrupted() const;
-
+  
   /// Interrupts the command currently executing in the RunCommandInterpreter
   /// thread.
   ///
diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index 2c6abbefb44c9..cbc5942f1c782 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -242,7 +242,7 @@ class CommandInterpreter : public Broadcaster,
     eCommandTypesAllThem = 0xFFFF  //< all commands
   };
 
-  // The CommandAlias and CommandInterpreter both have a hand in
+  // The CommandAlias and CommandInterpreter both have a hand in 
   // substituting for alias commands.  They work by writing special tokens
   // in the template form of the Alias command, and then detecting them when the
   // command is executed.  These are the special tokens:
@@ -580,7 +580,7 @@ class CommandInterpreter : public Broadcaster,
   void SetEchoCommentCommands(bool enable);
 
   bool GetRepeatPreviousCommand() const;
-
+  
   bool GetRequireCommandOverwrite() const;
 
   const CommandObject::CommandMap &GetUserCommands() const {
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index 5e071874f0111..7a35473283684 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -151,7 +151,7 @@ bool SBCommandInterpreter::WasInterrupted() const {
 
 bool SBCommandInterpreter::InterruptCommand() {
   LLDB_INSTRUMENT_VA(this);
-
+  
   return (IsValid() ? m_opaque_ptr->InterruptCommand() : false);
 }
 

>From 95aaa8bdf4c1f625602996fc770789cd6de52459 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Fri, 17 May 2024 09:17:26 -0700
Subject: [PATCH 18/19] Fix TestSessionSave.py; update a comment

---
 lldb/include/lldb/Interpreter/CommandInterpreter.h   | 10 ++++++----
 .../API/commands/session/save/TestSessionSave.py     | 12 ++++++++++++
 2 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index cbc5942f1c782..a336d614d02aa 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -772,10 +772,12 @@ class CommandInterpreter : public Broadcaster,
 
   StreamString m_transcript_stream;
 
-  /// Contains a list of handled commands, output and error. Each element in
-  /// the list is a dictionary with three keys: "command" (string), "output"
-  /// (list of strings) and optionally "error" (list of strings). Each string
-  /// in "output" and "error" is a line (without EOL characters).
+  /// Contains a list of handled commands and their details. Each element in
+  /// the list is a dictionary with the following keys/values:
+  /// - "command" (string): The command that was executed.
+  /// - "output" (string): The output of the command. Empty ("") if no output.
+  /// - "error" (string): The error of the command. Empty ("") if no error.
+  /// - "seconds" (float): The time it took to execute the command.
   StructuredData::Array m_transcript;
 };
 
diff --git a/lldb/test/API/commands/session/save/TestSessionSave.py b/lldb/test/API/commands/session/save/TestSessionSave.py
index 172a764523046..98985c66010bb 100644
--- a/lldb/test/API/commands/session/save/TestSessionSave.py
+++ b/lldb/test/API/commands/session/save/TestSessionSave.py
@@ -25,6 +25,12 @@ def test_session_save(self):
         raw = ""
         interpreter = self.dbg.GetCommandInterpreter()
 
+        # Make sure "save-transcript" is on, so that all the following setings
+        # and commands are saved into the trasncript. Note that this cannot be
+        # a part of the `settings`, because this command itself won't be saved
+        # into the transcript.
+        self.runCmd("settings set interpreter.save-transcript true")
+
         settings = [
             "settings set interpreter.echo-commands true",
             "settings set interpreter.echo-comment-commands true",
@@ -95,6 +101,12 @@ def test_session_save_on_quit(self):
         raw = ""
         interpreter = self.dbg.GetCommandInterpreter()
 
+        # Make sure "save-transcript" is on, so that all the following setings
+        # and commands are saved into the trasncript. Note that this cannot be
+        # a part of the `settings`, because this command itself won't be saved
+        # into the transcript.
+        self.runCmd("settings set interpreter.save-transcript true")
+
         td = tempfile.TemporaryDirectory()
 
         settings = [

>From d55005ad8bda067b7fe42b3bd48fd15534364846 Mon Sep 17 00:00:00 2001
From: Roy Shi <royshi at meta.com>
Date: Mon, 20 May 2024 16:19:05 -0400
Subject: [PATCH 19/19] Add comments the new setting

---
 lldb/include/lldb/Interpreter/CommandInterpreter.h | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index a336d614d02aa..ccc30cf4f1a82 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -770,6 +770,8 @@ class CommandInterpreter : public Broadcaster,
   typedef llvm::StringMap<uint64_t> CommandUsageMap;
   CommandUsageMap m_command_usages;
 
+  /// Turn on settings `interpreter.save-transcript` for LLDB to populate
+  /// this stream. Otherwise this stream is empty.
   StreamString m_transcript_stream;
 
   /// Contains a list of handled commands and their details. Each element in
@@ -778,6 +780,9 @@ class CommandInterpreter : public Broadcaster,
   /// - "output" (string): The output of the command. Empty ("") if no output.
   /// - "error" (string): The error of the command. Empty ("") if no error.
   /// - "seconds" (float): The time it took to execute the command.
+  ///
+  /// Turn on settings `interpreter.save-transcript` for LLDB to populate
+  /// this list. Otherwise this list is empty.
   StructuredData::Array m_transcript;
 };
 



More information about the lldb-commits mailing list