Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/internal/lldb/lldb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,12 @@ func TestEmbeddedPluginIdentity(t *testing.T) {
"ChannelSyntheticProvider",
"LLGO_GOROUTINE_LAYOUTS",
"print_goroutines",
"print_goroutine",
"llgo status",
"llgo print",
"llgo vars",
"llgo goroutines",
"llgo goroutine",
} {
if !strings.Contains(source, want) {
t.Errorf("embedded plugin is missing %q", want)
Expand Down
104 changes: 97 additions & 7 deletions cmd/internal/lldb/llgo_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
LLGO_DEFAULT_MAX_CHILDREN = 256
LLGO_MAX_CONTAINER_SCAN_BUCKETS = 65536
LLGO_MAX_GOROUTINES = 65536
LLGO_MAX_STACK_FRAMES = 256
_TARGET_INFO_CACHE: Dict[Tuple[Any, ...], "LLGoTargetInfo"] = {}


Expand Down Expand Up @@ -154,6 +155,7 @@ class LLGoGoroutineLayout:
m_current_goroutine: str
m_p: str
m_id: str
m_procid: str
p_m: str
p_id: str
status_names: Tuple[Tuple[int, str], ...]
Expand All @@ -168,6 +170,7 @@ class LLGoGoroutineValue:
status_name: str
mid: int
pid: int
procid: int
ownership_linked: bool


Expand Down Expand Up @@ -271,6 +274,7 @@ def _goroutine_layouts() -> Dict[int, LLGoGoroutineLayout]:
m_current_goroutine=goroutine["m_current_goroutine"],
m_p=goroutine["m_p"],
m_id=goroutine["m_id"],
m_procid=goroutine["m_procid"],
p_m=goroutine["p_m"],
p_id=goroutine["p_id"],
status_names=status_names,
Expand Down Expand Up @@ -332,6 +336,8 @@ def register_commands(debugger: lldb.SBDebugger) -> None:
'command script add -f llgo_plugin.print_all_variables llgo vars')
debugger.HandleCommand(
'command script add -f llgo_plugin.print_goroutines llgo goroutines')
debugger.HandleCommand(
'command script add -f llgo_plugin.print_goroutine llgo goroutine')
if inspect_target(debugger.GetSelectedTarget()).supported:
register_type_formatters(debugger)

Expand Down Expand Up @@ -773,6 +779,9 @@ def _goroutine_values(target: lldb.SBTarget, process: lldb.SBProcess,
if not m_value or not m_value.IsValid():
raise ValueError(f"cannot read M for goroutine {goid}")
mid = _required_integer_field(m_value, layout.m_id)
procid_value = _value_as_int(
m_value.GetChildMemberWithName(layout.m_procid))
procid = procid_value if procid_value is not None else 0
current_g = _required_integer_field(
m_value, layout.m_current_goroutine)
p_address = _required_integer_field(m_value, layout.m_p)
Expand All @@ -792,6 +801,7 @@ def _goroutine_values(target: lldb.SBTarget, process: lldb.SBProcess,
status_name=status_names.get(status, f"status-{status}"),
mid=mid,
pid=pid,
procid=procid,
ownership_linked=(current_g == address and p_m == m_address),
))
address = next_address
Expand All @@ -800,6 +810,18 @@ def _goroutine_values(target: lldb.SBTarget, process: lldb.SBProcess,
return values


def _goroutine_thread(process: lldb.SBProcess,
goroutine: LLGoGoroutineValue) -> Optional[lldb.SBThread]:
if goroutine.procid == 0:
return None
for index in range(process.GetNumThreads()):
thread = process.GetThreadAtIndex(index)
if (thread and thread.IsValid() and
thread.GetThreadID() == goroutine.procid):
return thread
return None


def print_goroutines(debugger: lldb.SBDebugger, command: str,
result: lldb.SBCommandReturnObject,
_internal_dict: Dict[str, Any]) -> None:
Expand Down Expand Up @@ -827,13 +849,81 @@ def print_goroutines(debugger: lldb.SBDebugger, command: str,
if not goroutines:
result.AppendMessage("No live LLGo goroutines.")
return
result.AppendMessage("\n".join(
f"goroutine {goroutine.goid} [{goroutine.status_name}] "
f"parent={goroutine.parent_goid} m={goroutine.mid} "
f"p={goroutine.pid} ownership="
f"{'linked' if goroutine.ownership_linked else 'invalid'}"
for goroutine in goroutines
))
lines = []
for goroutine in goroutines:
thread = _goroutine_thread(process, goroutine)
thread_index = (str(thread.GetIndexID())
if thread is not None else "unavailable")
lines.append(
f"goroutine {goroutine.goid} [{goroutine.status_name}] "
f"parent={goroutine.parent_goid} m={goroutine.mid} "
f"p={goroutine.pid} thread={thread_index} ownership="
f"{'linked' if goroutine.ownership_linked else 'invalid'}")
result.AppendMessage("\n".join(lines))


def _frame_description(frame: lldb.SBFrame, index: int) -> str:
name = frame.GetFunctionName() or frame.GetSymbol().GetName() or "<unknown>"
description = f"frame #{index}: {name}"
line_entry = frame.GetLineEntry()
if line_entry and line_entry.IsValid():
file_spec = line_entry.GetFileSpec()
filename = file_spec.GetFilename() if file_spec else None
line = line_entry.GetLine()
if filename and line:
description += f" at {filename}:{line}"
return description


def print_goroutine(debugger: lldb.SBDebugger, command: str,
result: lldb.SBCommandReturnObject,
_internal_dict: Dict[str, Any]) -> None:
if not _require_supported_target(debugger, result):
return
match = re.fullmatch(r"\s*([0-9]+)\s+(?:bt|backtrace)\s*", command)
if match is None:
result.SetError("usage: llgo goroutine <id> bt")
return
process = _stopped_process(debugger, result)
if process is None:
return

target = debugger.GetSelectedTarget()
info = inspect_target(target)
layout = LLGO_GOROUTINE_LAYOUTS.get(info.runtime_layout_version)
if layout is None:
result.SetError("LLGo goroutine metadata is unavailable for this runtime.")
return
try:
goroutines = _goroutine_values(target, process, layout)
except ValueError as error:
result.SetError(str(error))
return

goid = int(match.group(1))
goroutine = next(
(candidate for candidate in goroutines if candidate.goid == goid),
None)
if goroutine is None:
result.SetError(f"LLGo goroutine {goid} is not live.")
return
thread = _goroutine_thread(process, goroutine)
if thread is None:
result.SetError(
f"LLGo goroutine {goid} has no matching debugger thread.")
return

frame_count = min(thread.GetNumFrames(), LLGO_MAX_STACK_FRAMES)
lines = [
f"goroutine {goid} [{goroutine.status_name}] "
f"thread {thread.GetIndexID()}:"
]
lines.extend(_frame_description(thread.GetFrameAtIndex(index), index)
for index in range(frame_count))
if thread.GetNumFrames() > frame_count:
lines.append(
f"... ({thread.GetNumFrames() - frame_count} more frames)")
result.AppendMessage("\n".join(lines))


def _value_as_int(value: lldb.SBValue) -> Optional[int]:
Expand Down
9 changes: 5 additions & 4 deletions cmd/llgo/lldbtest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ llgo lldb -lldb /opt/homebrew/bin/lldb -- --batch ./cl/_testdata/debug/out
The command embeds and loads the LLGo Python adapter, so an installed `llgo`
does not depend on a source checkout. `cmd/llgo/lldbtest/runlldb.sh` remains as
a thin compatibility wrapper. Adapter commands live under `llgo`, including
`llgo status`, `llgo print`, `llgo vars`, and `llgo goroutines`; stock LLDB
commands and aliases
`llgo status`, `llgo print`, `llgo vars`, `llgo goroutines`, and
`llgo goroutine ID bt`; stock LLDB commands and aliases
such as `p` and `v` are left unchanged. `llgo status` reports the recognized
debugger schema, runtime-layout version, target triple, pointer size, and byte
order. Unknown marker versions disable only the LLGo-specific commands; raw
Expand All @@ -53,8 +53,9 @@ goroutines. Maps expose
their length and typed key/value children, including indirect large entries;
channels expose length, capacity, closed state, and buffered values in receive
order. `llgo goroutines` reports each live goroutine's runtime state, parent,
and G/M/P ownership. Goroutine-aware stack selection is separate follow-up
work. Named container types are covered as well as predeclared types. Explicit
G/M/P ownership, and matching debugger thread. `llgo goroutine ID bt` prints
that goroutine's native stack without changing the selected LLDB thread. Named
container types are covered as well as predeclared types. Explicit
`llgo print` slice views respect LLDB's `target.max-children-count` setting.
Ordinary C targets and targets with unknown or ambiguous LLGo markers retain
LLDB's raw presentation.
Expand Down
4 changes: 3 additions & 1 deletion cmd/llgo/lldbtest/runtest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "LLGo debugger schema v1 (runtime layout v1); LLGo ABI v1; C ABI mode 2 (allfunc)" in result.GetOutput()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo print s", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutines", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()'
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutines", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutine 1 bt", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()'

# The LLGo formatter must not attach itself to an ordinary C target.
non_llgo_dir="$test_tmp_dir/non-llgo"
Expand All @@ -116,6 +117,7 @@ run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsuppor
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetOutput()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutines", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo goroutine 1 bt", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \
-o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()'

# Multiple marker versions are ambiguous even when one version is supported.
Expand Down
58 changes: 52 additions & 6 deletions cmd/llgo/lldbtest/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,10 @@ def test_case(marker: str, expectations: List[tuple]) -> TestCase:
test_case("goroutine_values", [
("goroutineReadySum", "3"),
("goroutines",
"count=3 roots=1 children=2 running=3 linked=3 unique-m=3 unique-p=3",
"count=3 roots=1 children=2 running=3 linked=3 mapped=3 "
"unique-m=3 unique-p=3 unique-threads=3",
"goroutines"),
("goroutine stacks", "root=1 children=2", "goroutine-stacks"),
]),
test_case("struct_values_initial", STRUCT_VALUES_INITIAL),
test_case("struct_values_updated", STRUCT_VALUES_UPDATED),
Expand Down Expand Up @@ -482,7 +484,7 @@ def require_print_error(self, expression: str, expected: str) -> None:
f"llgo print {expression!r} did not fail with {expected!r}: "
f"{result.GetOutput()!r} {result.GetError()!r}")

def get_goroutine_summary(self) -> Optional[str]:
def get_goroutines(self) -> Optional[List[Dict[str, Any]]]:
result = lldb.SBCommandReturnObject()
self.debugger.GetCommandInterpreter().HandleCommand(
"llgo goroutines", result)
Expand All @@ -492,7 +494,8 @@ def get_goroutine_summary(self) -> Optional[str]:
if line.strip()]
pattern = re.compile(
r"^goroutine ([0-9]+) \[([^]]+)\] parent=([0-9]+) "
r"m=(-?[0-9]+) p=(-?[0-9]+) ownership=(linked|invalid)$")
r"m=(-?[0-9]+) p=(-?[0-9]+) "
r"thread=(unavailable|[0-9]+) ownership=(linked|invalid)$")
goroutines = []
for line in lines:
match = pattern.fullmatch(line)
Expand All @@ -504,8 +507,15 @@ def get_goroutine_summary(self) -> Optional[str]:
"parent": int(match.group(3)),
"mid": int(match.group(4)),
"pid": int(match.group(5)),
"ownership": match.group(6),
"thread": match.group(6),
"ownership": match.group(7),
})
return goroutines

def get_goroutine_summary(self) -> Optional[str]:
goroutines = self.get_goroutines()
if goroutines is None:
return None
ids = {goroutine["goid"] for goroutine in goroutines}
roots = sum(goroutine["parent"] == 0 for goroutine in goroutines)
children = sum(
Expand All @@ -515,12 +525,46 @@ def get_goroutine_summary(self) -> Optional[str]:
goroutine["status"] == "running" for goroutine in goroutines)
linked = sum(
goroutine["ownership"] == "linked" for goroutine in goroutines)
mapped = sum(
goroutine["thread"] != "unavailable" for goroutine in goroutines)
unique_m = len({goroutine["mid"] for goroutine in goroutines})
unique_p = len({goroutine["pid"] for goroutine in goroutines})
unique_threads = len({goroutine["thread"] for goroutine in goroutines
if goroutine["thread"] != "unavailable"})
return (
f"count={len(goroutines)} roots={roots} children={children} "
f"running={running} linked={linked} unique-m={unique_m} "
f"unique-p={unique_p}")
f"running={running} linked={linked} mapped={mapped} "
f"unique-m={unique_m} unique-p={unique_p} "
f"unique-threads={unique_threads}")

def get_goroutine_stack_summary(self) -> Optional[str]:
goroutines = self.get_goroutines()
if goroutines is None:
return None
roots = [goroutine for goroutine in goroutines
if goroutine["parent"] == 0]
if len(roots) != 1:
return None
children = [goroutine for goroutine in goroutines
if goroutine["parent"] == roots[0]["goid"]]
if len(children) != 2:
return None

expected_functions = [(roots[0], "main.InspectGoroutineValues")]
expected_functions.extend(
(goroutine, "main.RuntimeGoroutineValues")
for goroutine in children)
for goroutine, expected_function in expected_functions:
result = lldb.SBCommandReturnObject()
self.debugger.GetCommandInterpreter().HandleCommand(
f"llgo goroutine {goroutine['goid']} bt", result)
output = result.GetOutput() or ""
if (not result.Succeeded() or
expected_function not in output or
f"goroutine {goroutine['goid']} [running] thread "
not in output):
return None
return f"root={len(roots)} children={len(children)}"

def cleanup(self) -> None:
if self.process and self.process.IsValid():
Expand Down Expand Up @@ -711,6 +755,8 @@ def execute_single_variable_test(debugger: LLDBDebugger, test: Test) -> TestResu
"settings set target.max-children-count 256")
elif test.mode == "goroutines":
actual_value = debugger.get_goroutine_summary()
elif test.mode == "goroutine-stacks":
actual_value = debugger.get_goroutine_stack_summary()
else:
actual_value = debugger.get_variable_value(test.variable)
if actual_value is None:
Expand Down
3 changes: 2 additions & 1 deletion runtime/internal/runtime/g_global.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ var currentG *g

func getg() *g {
if currentG == nil {
currentG = initRuntimeContext(new(runtimeContext), nil, _Grunning)
setg(initRuntimeContext(new(runtimeContext), nil, _Grunning))
}
return currentG
}

func setg(gp *g) {
currentG = gp
setMProcID(gp)
}
2 changes: 2 additions & 0 deletions runtime/internal/runtime/g_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func setg(gp *g) {
}
}
currentG = uintptr(unsafe.Pointer(gp))
setMProcID(gp)
}

func setAutoG(gp *g) c.Int {
Expand All @@ -94,6 +95,7 @@ func setAutoG(gp *g) c.Int {
}
currentG = uintptr(unsafe.Pointer(gp))
currentGHasLifecycle = true
setMProcID(gp)
return 0
}

Expand Down
15 changes: 15 additions & 0 deletions runtime/internal/runtime/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g {
return gp
}

func setMProcID(gp *g) {
if gp != nil && gp.m != nil {
gp.m.procid = currentThreadID()
}
}

func registerG(gp *g) {
lockAllg()
gp.alllink = debuggerAllgV1
Expand Down Expand Up @@ -256,6 +262,15 @@ func GInAllGForTesting(target unsafe.Pointer) (found, linked bool) {
return found, true
}

// ProcIDForTesting reports the OS thread identity used by debuggers.
func ProcIDForTesting() uint64 {
gp := getg()
if gp == nil || gp.m == nil {
return 0
}
return gp.m.procid
}

// GMPForTesting reports the current runtime ownership graph. It is kept
// internal to the compiler runtime and linked only by LLGo execution tests.
func GMPForTesting() (goid, parentGoid uint64, mid int64, pid int32, gstatus, pstatus uint32, linked bool) {
Expand Down
Loading
Loading