diff --git a/cmd/internal/lldb/lldb_test.go b/cmd/internal/lldb/lldb_test.go index 45709005e7..cbeedeb7a4 100644 --- a/cmd/internal/lldb/lldb_test.go +++ b/cmd/internal/lldb/lldb_test.go @@ -192,9 +192,12 @@ func TestEmbeddedPluginIdentity(t *testing.T) { "MapSyntheticProvider", "channel_summary", "ChannelSyntheticProvider", + "LLGO_GOROUTINE_LAYOUTS", + "print_goroutines", "llgo status", "llgo print", "llgo vars", + "llgo goroutines", } { if !strings.Contains(source, want) { t.Errorf("embedded plugin is missing %q", want) diff --git a/cmd/internal/lldb/llgo_plugin.py b/cmd/internal/lldb/llgo_plugin.py index cf4d213982..c05cc3df0c 100644 --- a/cmd/internal/lldb/llgo_plugin.py +++ b/cmd/internal/lldb/llgo_plugin.py @@ -15,6 +15,7 @@ LLGO_MAX_TYPE_NAME_BYTES = 4096 LLGO_DEFAULT_MAX_CHILDREN = 256 LLGO_MAX_CONTAINER_SCAN_BUCKETS = 65536 +LLGO_MAX_GOROUTINES = 65536 _TARGET_INFO_CACHE: Dict[Tuple[Any, ...], "LLGoTargetInfo"] = {} @@ -141,6 +142,35 @@ class LLGoChannelValue: element_size: int +@dataclass(frozen=True) +class LLGoGoroutineLayout: + head_symbol: str + goroutine_type: str + goroutine_next: str + goroutine_status: str + goroutine_id: str + goroutine_parent_id: str + goroutine_m: str + m_current_goroutine: str + m_p: str + m_id: str + p_m: str + p_id: str + status_names: Tuple[Tuple[int, str], ...] + + +@dataclass(frozen=True) +class LLGoGoroutineValue: + address: int + goid: int + parent_goid: int + status: int + status_name: str + mid: int + pid: int + ownership_linked: bool + + def _runtime_layouts() -> Dict[int, LLGoRuntimeLayout]: layouts = {} for version, raw in LLGO_DEBUGGER_SCHEMA.get( @@ -220,6 +250,39 @@ def _runtime_layouts() -> Dict[int, LLGoRuntimeLayout]: LLGO_RUNTIME_LAYOUTS = _runtime_layouts() +def _goroutine_layouts() -> Dict[int, LLGoGoroutineLayout]: + layouts = {} + for version, raw in LLGO_DEBUGGER_SCHEMA.get( + "runtime_layouts", {}).items(): + goroutine = raw.get("goroutine", {}) + try: + status_names = tuple(sorted( + (int(status), str(name)) + for status, name in goroutine["status_names"].items() + )) + layouts[int(version)] = LLGoGoroutineLayout( + head_symbol=goroutine["head_symbol"], + goroutine_type=goroutine["goroutine_type"], + goroutine_next=goroutine["next"], + goroutine_status=goroutine["status"], + goroutine_id=goroutine["id"], + goroutine_parent_id=goroutine["parent_id"], + goroutine_m=goroutine["m"], + m_current_goroutine=goroutine["m_current_goroutine"], + m_p=goroutine["m_p"], + m_id=goroutine["m_id"], + p_m=goroutine["p_m"], + p_id=goroutine["p_id"], + status_names=status_names, + ) + except (AttributeError, KeyError, TypeError, ValueError): + continue + return layouts + + +LLGO_GOROUTINE_LAYOUTS = _goroutine_layouts() + + @dataclass(frozen=True) class LLGoDebuggerRecord: record_version: int @@ -267,6 +330,8 @@ def register_commands(debugger: lldb.SBDebugger) -> None: 'command script add -f llgo_plugin.print_go_expression llgo print') debugger.HandleCommand( 'command script add -f llgo_plugin.print_all_variables llgo vars') + debugger.HandleCommand( + 'command script add -f llgo_plugin.print_goroutines llgo goroutines') if inspect_target(debugger.GetSelectedTarget()).supported: register_type_formatters(debugger) @@ -608,7 +673,7 @@ def _require_supported_target(debugger: lldb.SBDebugger, result: lldb.SBCommandR return False -def _selected_stopped_frame(debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> Optional[lldb.SBFrame]: +def _stopped_process(debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> Optional[lldb.SBProcess]: target = debugger.GetSelectedTarget() if not target or not target.IsValid(): result.SetError("LLGo command requires a valid target.") @@ -619,6 +684,13 @@ def _selected_stopped_frame(debugger: lldb.SBDebugger, result: lldb.SBCommandRet process.GetState() != lldb.eStateStopped): result.SetError("LLGo command requires a stopped process.") return None + return process + + +def _selected_stopped_frame(debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> Optional[lldb.SBFrame]: + process = _stopped_process(debugger, result) + if process is None: + return None thread = process.GetSelectedThread() if not thread or not thread.IsValid(): @@ -632,6 +704,138 @@ def _selected_stopped_frame(debugger: lldb.SBDebugger, result: lldb.SBCommandRet return frame +def _symbol_load_address(target: lldb.SBTarget, name: str) -> Optional[int]: + contexts = target.FindSymbols(name) + addresses = set() + for index in range(contexts.GetSize()): + symbol = contexts.GetContextAtIndex(index).GetSymbol() + if not symbol or not symbol.IsValid() or symbol.GetName() != name: + continue + address = symbol.GetStartAddress().GetLoadAddress(target) + if address != getattr(lldb, "LLDB_INVALID_ADDRESS", (1 << 64) - 1): + addresses.add(address) + if len(addresses) != 1: + return None + return next(iter(addresses)) + + +def _required_integer_field(value: lldb.SBValue, name: str) -> int: + field = value.GetChildMemberWithName(name) + result = _value_as_int(field) + if result is None: + raise ValueError(f"cannot read runtime field {name!r}") + return result + + +def _goroutine_values(target: lldb.SBTarget, process: lldb.SBProcess, + layout: LLGoGoroutineLayout) -> List[LLGoGoroutineValue]: + head_address = _symbol_load_address(target, layout.head_symbol) + if head_address is None: + raise ValueError( + "LLGo goroutine metadata is unavailable for this runtime.") + + error = lldb.SBError() + address = process.ReadPointerFromMemory(head_address, error) + if not error.Success(): + raise ValueError("cannot read the LLGo goroutine list") + + goroutine_type = target.FindFirstType(layout.goroutine_type) + if not goroutine_type or not goroutine_type.IsValid(): + raise ValueError("LLGo goroutine type metadata is unavailable") + + status_names = dict(layout.status_names) + visited = set() + values: List[LLGoGoroutineValue] = [] + while address != 0: + if address in visited: + raise ValueError("LLGo goroutine list contains a cycle") + if len(values) >= LLGO_MAX_GOROUTINES: + raise ValueError("LLGo goroutine list exceeds the safety limit") + visited.add(address) + + goroutine = target.CreateValueFromAddress( + "__llgo_g", lldb.SBAddress(address, target), goroutine_type) + if not goroutine or not goroutine.IsValid(): + raise ValueError("cannot read an LLGo goroutine") + + next_address = _required_integer_field( + goroutine, layout.goroutine_next) + status = _required_integer_field(goroutine, layout.goroutine_status) + goid = _required_integer_field(goroutine, layout.goroutine_id) + parent_goid = _required_integer_field( + goroutine, layout.goroutine_parent_id) + m_address = _required_integer_field(goroutine, layout.goroutine_m) + if m_address == 0: + raise ValueError(f"goroutine {goid} has no M") + + m_value = goroutine.GetChildMemberWithName( + layout.goroutine_m).Dereference() + 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) + current_g = _required_integer_field( + m_value, layout.m_current_goroutine) + p_address = _required_integer_field(m_value, layout.m_p) + if p_address == 0: + raise ValueError(f"goroutine {goid} has no P") + + p_value = m_value.GetChildMemberWithName(layout.m_p).Dereference() + if not p_value or not p_value.IsValid(): + raise ValueError(f"cannot read P for goroutine {goid}") + pid = _required_integer_field(p_value, layout.p_id) + p_m = _required_integer_field(p_value, layout.p_m) + values.append(LLGoGoroutineValue( + address=address, + goid=goid, + parent_goid=parent_goid, + status=status, + status_name=status_names.get(status, f"status-{status}"), + mid=mid, + pid=pid, + ownership_linked=(current_g == address and p_m == m_address), + )) + address = next_address + + values.sort(key=lambda value: value.goid) + return values + + +def print_goroutines(debugger: lldb.SBDebugger, command: str, + result: lldb.SBCommandReturnObject, + _internal_dict: Dict[str, Any]) -> None: + if not _require_supported_target(debugger, result): + return + if command.strip(): + result.SetError("usage: llgo goroutines") + 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 + + 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 + )) + + def _value_as_int(value: lldb.SBValue) -> Optional[int]: if not value or not value.IsValid(): return None diff --git a/cmd/llgo/lldbtest/README.md b/cmd/llgo/lldbtest/README.md index 6684d7608c..fde046215f 100644 --- a/cmd/llgo/lldbtest/README.md +++ b/cmd/llgo/lldbtest/README.md @@ -40,17 +40,21 @@ 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`, and `llgo vars`; stock LLDB commands and aliases +`llgo status`, `llgo print`, `llgo vars`, and `llgo goroutines`; 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 LLDB debugging remains available. For recognized LLGo targets, the adapter provides runtime-aware views for -strings, slices, interfaces, function values, maps, and channels. Maps expose +strings, slices, interfaces, function values, maps, channels, and live +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. Named container types are covered as well as predeclared types. Explicit +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 `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. diff --git a/cmd/llgo/lldbtest/main.go b/cmd/llgo/lldbtest/main.go index eefcec696a..cba906cdcd 100644 --- a/cmd/llgo/lldbtest/main.go +++ b/cmd/llgo/lldbtest/main.go @@ -104,6 +104,7 @@ type ContainerResults struct { } var containerResults *ContainerResults +var goroutineReadySum int func RuntimeInterfaceValues() { var nilAny any @@ -212,6 +213,31 @@ func InspectContainerValues( ) } +func RuntimeGoroutineValues() { + ready := make(chan int, 2) + release := make(chan struct{}) + results := make(chan int, 2) + for worker := 1; worker <= 2; worker++ { + go func(value int) { + ready <- value + <-release + results <- value * value + }(worker) + } + readySum := <-ready + <-ready + goroutineReadySum = readySum + InspectGoroutineValues(readySum) + close(release) + resultSum := <-results + <-results + if resultSum != 5 { + panic("goroutine result mismatch") + } +} + +func InspectGoroutineValues(readySum int) { + println(readySum) // LLDB_BREAK: goroutine_values +} + func RuntimeValues() { text := "hello" empty := "" @@ -497,6 +523,7 @@ func main() { RuntimeInterfaceValues() RuntimeFunctionValues() RuntimeContainerValues() + RuntimeGoroutineValues() println("called function with struct") i, err := FuncWithAllTypeParams( s.i8, s.i16, s.i32, s.i64, s.i, s.u8, s.u16, s.u32, s.u64, s.u, diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 5d0d5ea4f4..92287d40b2 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -93,7 +93,8 @@ run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \ -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.schema_version == 1 and info.runtime_layout_version == 1 and info.record_version == 1 and info.llgo_abi_version == 1 and info.cabi_mode == 2 and info.cabi_name == "allfunc" and info.pointer_size == lldb.target.GetAddressByteSize() and info.byte_order != "unknown"' \ -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 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()' # The LLGo formatter must not attach itself to an ordinary C target. non_llgo_dir="$test_tmp_dir/non-llgo" @@ -114,6 +115,7 @@ run_checked_lldb llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsuppor -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ -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("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' # Multiple marker versions are ambiguous even when one version is supported. diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index 74b0df4f9c..288815507e 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -3,6 +3,7 @@ import os import sys import argparse +import re import signal from dataclasses import dataclass, field from typing import List, Optional, Set, Dict, Any @@ -268,6 +269,12 @@ def test_case(marker: str, expectations: List[tuple]) -> TestCase: ("containerResults.closedHead", '"first"'), ("containerResults.closedOK", "true"), ]), + test_case("goroutine_values", [ + ("goroutineReadySum", "3"), + ("goroutines", + "count=3 roots=1 children=2 running=3 linked=3 unique-m=3 unique-p=3", + "goroutines"), + ]), test_case("struct_values_initial", STRUCT_VALUES_INITIAL), test_case("struct_values_updated", STRUCT_VALUES_UPDATED), test_case("struct_ptrs_initial", STRUCT_VALUES_INITIAL), @@ -475,6 +482,46 @@ 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]: + result = lldb.SBCommandReturnObject() + self.debugger.GetCommandInterpreter().HandleCommand( + "llgo goroutines", result) + if not result.Succeeded(): + return None + lines = [line.strip() for line in (result.GetOutput() or "").splitlines() + if line.strip()] + pattern = re.compile( + r"^goroutine ([0-9]+) \[([^]]+)\] parent=([0-9]+) " + r"m=(-?[0-9]+) p=(-?[0-9]+) ownership=(linked|invalid)$") + goroutines = [] + for line in lines: + match = pattern.fullmatch(line) + if match is None: + return None + goroutines.append({ + "goid": int(match.group(1)), + "status": match.group(2), + "parent": int(match.group(3)), + "mid": int(match.group(4)), + "pid": int(match.group(5)), + "ownership": match.group(6), + }) + ids = {goroutine["goid"] for goroutine in goroutines} + roots = sum(goroutine["parent"] == 0 for goroutine in goroutines) + children = sum( + goroutine["parent"] in ids and goroutine["parent"] != 0 + for goroutine in goroutines) + running = sum( + goroutine["status"] == "running" for goroutine in goroutines) + linked = sum( + goroutine["ownership"] == "linked" for goroutine in goroutines) + unique_m = len({goroutine["mid"] for goroutine in goroutines}) + unique_p = len({goroutine["pid"] for goroutine in goroutines}) + return ( + f"count={len(goroutines)} roots={roots} children={children} " + f"running={running} linked={linked} unique-m={unique_m} " + f"unique-p={unique_p}") + def cleanup(self) -> None: if self.process and self.process.IsValid(): self.process.Kill() @@ -662,6 +709,8 @@ def execute_single_variable_test(debugger: LLDBDebugger, test: Test) -> TestResu finally: debugger.debugger.HandleCommand( "settings set target.max-children-count 256") + elif test.mode == "goroutines": + actual_value = debugger.get_goroutine_summary() else: actual_value = debugger.get_variable_value(test.variable) if actual_value is None: diff --git a/runtime/internal/runtime/allg_lock_llgo.go b/runtime/internal/runtime/allg_lock_llgo.go new file mode 100644 index 0000000000..d71f62c3e1 --- /dev/null +++ b/runtime/internal/runtime/allg_lock_llgo.go @@ -0,0 +1,33 @@ +//go:build llgo && !baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" + +func lockAllg() { + for { + if _, ok := atomic.CompareAndExchange(&sched.allglock, uint32(0), uint32(1)); ok { + return + } + } +} + +func unlockAllg() { + atomic.Store(&sched.allglock, uint32(0)) +} diff --git a/runtime/internal/runtime/allg_lock_single.go b/runtime/internal/runtime/allg_lock_single.go new file mode 100644 index 0000000000..78a8aaeb95 --- /dev/null +++ b/runtime/internal/runtime/allg_lock_single.go @@ -0,0 +1,23 @@ +//go:build !llgo || baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +func lockAllg() {} + +func unlockAllg() {} diff --git a/runtime/internal/runtime/g_tls.go b/runtime/internal/runtime/g_tls.go index 7aa00577b4..30dee1695a 100644 --- a/runtime/internal/runtime/g_tls.go +++ b/runtime/internal/runtime/g_tls.go @@ -102,6 +102,7 @@ func destroyG(ptr c.Pointer) { if gp == nil { return } + unregisterG(gp) if gp.panic_ != nil { c.Free(gp.panic_) } diff --git a/runtime/internal/runtime/proc.go b/runtime/internal/runtime/proc.go index b9e35ef954..f17ca4f662 100644 --- a/runtime/internal/runtime/proc.go +++ b/runtime/internal/runtime/proc.go @@ -42,11 +42,17 @@ type runtimeContext struct { } var sched struct { - goidgen uint64 - midgen int64 - pidgen int32 + goidgen uint64 + midgen int64 + pidgen int32 + allglock uint32 } +// debuggerAllgV1 is the head of the live goroutine list consumed by the LLGo +// debugger adapter. The versioned Go symbol name is the stable contract; the +// runtime g layout remains selected through the debugger schema version. +var debuggerAllgV1 *g + // NewProc creates a new G running fn. // // The compiler turns a go statement into a call to NewProc. Unlike the old @@ -56,8 +62,11 @@ func NewProc(fn goroutineFunc, arg unsafe.Pointer, stackSize uintptr) { gp := newproc1(fn, arg, getg()) if errno := newm(gp.m, stackSize); errno != 0 { ctx := gp.context + root := ctx.root + unregisterG(gp) FreeRoot(arg) - FreeRoot(ctx.root) + ctx.root = nil + FreeRoot(root) panic("runtime: failed to create new OS thread") } } @@ -131,6 +140,7 @@ func mexit(mp *m) { casgstatus(gp, _Grunning, _Gdead) setpstatus(pp, _Pdead) + unregisterG(gp) pp.m = nil mp.p = nil @@ -168,9 +178,84 @@ func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { } setpstatus(pp, pstatus) pp.m = mp + registerG(gp) return gp } +func registerG(gp *g) { + lockAllg() + gp.alllink = debuggerAllgV1 + gp.allprev = nil + if debuggerAllgV1 != nil { + debuggerAllgV1.allprev = gp + } + debuggerAllgV1 = gp + unlockAllg() +} + +func unregisterG(gp *g) { + lockAllg() + if gp.allprev != nil { + gp.allprev.alllink = gp.alllink + } else if debuggerAllgV1 == gp { + debuggerAllgV1 = gp.alllink + } + if gp.alllink != nil { + gp.alllink.allprev = gp.allprev + } + gp.alllink = nil + gp.allprev = nil + unlockAllg() +} + +// AllGForTesting reports the live goroutine-list size and whether its forward +// and backward links agree. It is linked only by LLGo execution tests. +func AllGForTesting() (count int, linked bool) { + lockAllg() + var previous *g + for gp := debuggerAllgV1; gp != nil; gp = gp.alllink { + if gp.allprev != previous { + unlockAllg() + return count, false + } + previous = gp + count++ + if count > 1<<20 { + unlockAllg() + return count, false + } + } + unlockAllg() + return count, true +} + +// GInAllGForTesting reports whether target is present in the live goroutine +// list and whether all links traversed while looking for it are consistent. +// It is linked only by LLGo execution tests. +func GInAllGForTesting(target unsafe.Pointer) (found, linked bool) { + lockAllg() + want := (*g)(target) + var previous *g + count := 0 + for gp := debuggerAllgV1; gp != nil; gp = gp.alllink { + if gp.allprev != previous { + unlockAllg() + return false, false + } + if gp == want { + found = true + } + previous = gp + count++ + if count > 1<<20 { + unlockAllg() + return false, false + } + } + unlockAllg() + return found, true +} + // 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) { diff --git a/runtime/internal/runtime/proc_atomic.go b/runtime/internal/runtime/proc_atomic.go index eaec33b38f..fa1c897d2b 100644 --- a/runtime/internal/runtime/proc_atomic.go +++ b/runtime/internal/runtime/proc_atomic.go @@ -20,16 +20,17 @@ package runtime import "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" +// The clite atomic fetch operations return the value from before the update. func nextGoid(gp *g) uint64 { - return atomic.Add(&sched.goidgen, uint64(1)) + return atomic.Add(&sched.goidgen, uint64(1)) + 1 } func nextMid(mp *m) int64 { - return atomic.Add(&sched.midgen, int64(1)) + return atomic.Add(&sched.midgen, int64(1)) + 1 } func nextPid(pp *p) int32 { - return atomic.Add(&sched.pidgen, int32(1)) - 1 + return atomic.Add(&sched.pidgen, int32(1)) } func readgstatus(gp *g) uint32 { diff --git a/runtime/internal/runtime/runtime2.go b/runtime/internal/runtime/runtime2.go index 7787c8e225..6dabbd65e3 100644 --- a/runtime/internal/runtime/runtime2.go +++ b/runtime/internal/runtime/runtime2.go @@ -42,6 +42,8 @@ type g struct { panic_ unsafe.Pointer panicPCs panicPCStore m *m + alllink *g + allprev *g atomicstatus uint32 goid uint64 diff --git a/test/llgoext/runtime_g_test.go b/test/llgoext/runtime_g_test.go index 03fe1b2bc5..65d273c195 100644 --- a/test/llgoext/runtime_g_test.go +++ b/test/llgoext/runtime_g_test.go @@ -164,6 +164,55 @@ func TestRuntimeGMPLinks(t *testing.T) { } } +//go:linkname runtimeAllGForTesting github.com/goplus/llgo/runtime/internal/runtime.AllGForTesting +func runtimeAllGForTesting() (count int, linked bool) + +//go:linkname runtimeGInAllGForTesting github.com/goplus/llgo/runtime/internal/runtime.GInAllGForTesting +func runtimeGInAllGForTesting(gp unsafe.Pointer) (found, linked bool) + +func TestRuntimeAllGLifecycle(t *testing.T) { + runtimeGetGForTest() + baseline, linked := runtimeAllGForTesting() + if !linked || baseline < 1 { + t.Fatalf("initial allg = (%d, %v), want a linked non-empty list", baseline, linked) + } + + ready := make(chan unsafe.Pointer, 2) + release := make(chan struct{}) + done := make(chan struct{}, 2) + for i := 0; i < 2; i++ { + go func() { + ready <- runtimeGetGForTest() + <-release + done <- struct{}{} + }() + } + workers := [2]unsafe.Pointer{<-ready, <-ready} + if workers[0] == nil || workers[1] == nil || workers[0] == workers[1] { + t.Fatalf("worker G pointers = (%p, %p), want distinct non-nil values", workers[0], workers[1]) + } + for index, gp := range workers { + if found, linked := runtimeGInAllGForTesting(gp); !found || !linked { + t.Fatalf("live worker %d in allg = (%v, %v), want (true, true)", index, found, linked) + } + } + + close(release) + <-done + <-done + for attempts := 0; attempts < 1<<20; attempts++ { + first, firstLinked := runtimeGInAllGForTesting(workers[0]) + second, secondLinked := runtimeGInAllGForTesting(workers[1]) + if !first && !second && firstLinked && secondLinked { + return + } + } + first, firstLinked := runtimeGInAllGForTesting(workers[0]) + second, secondLinked := runtimeGInAllGForTesting(workers[1]) + t.Fatalf("finished workers in allg = (%v, %v, %v, %v), want (false, true, false, true)", + first, firstLinked, second, secondLinked) +} + type runtimeDeferProbeResult struct { before unsafe.Pointer inside unsafe.Pointer