Skip to content

Commit 7aaa597

Browse files
committed
[GR-76249] Implement sys._current_frames for GraalPy threads.
PullRequest: graalpython/4614
2 parents efec429 + 0c814fd commit 7aaa597

5 files changed

Lines changed: 206 additions & 47 deletions

File tree

graalpython/com.oracle.graal.python.test/src/tests/test_frame_tests.py

Lines changed: 96 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -222,30 +222,20 @@ class Foo:
222222
assert 'c' in locals()
223223
assert 'x' not in locals()
224224

225-
# GR-22089
226-
# def test_backref_from_traceback():
227-
# def bar():
228-
# raise RuntimeError
229-
#
230-
# def foo():
231-
# bar()
232-
#
233-
# try:
234-
# foo()
235-
# except Exception as e:
236-
# assert e.__traceback__.tb_frame.f_back.f_code == sys._getframe(0).f_back.f_code
237-
# assert e.__traceback__.tb_next.tb_next.tb_frame.f_back.f_code == foo.__code__
238-
# assert e.__traceback__.tb_next.tb_frame.f_back.f_code == test_backref_from_traceback.__code__
239225

226+
def test_backref_from_traceback():
227+
def bar():
228+
raise RuntimeError
240229

241-
def test_clearing_globals():
242-
global foo, junk
243-
foo = 123
244-
assert "foo" in globals().keys()
245-
assert "junk" not in globals().keys()
246-
junk = globals().clear()
247-
assert "foo" not in globals().keys()
248-
assert "junk" in globals().keys()
230+
def foo():
231+
bar()
232+
233+
try:
234+
foo()
235+
except Exception as e:
236+
assert e.__traceback__.tb_frame.f_back.f_code == sys._getframe(0).f_back.f_code
237+
assert e.__traceback__.tb_next.tb_next.tb_frame.f_back.f_code == foo.__code__
238+
assert e.__traceback__.tb_next.tb_frame.f_back.f_code == test_backref_from_traceback.__code__
249239

250240

251241
def test_frame_from_another_thread():
@@ -278,3 +268,86 @@ def target():
278268
assert frame.f_locals['b'] == 2
279269
event4.set()
280270
thread.join(timeout=60)
271+
272+
273+
OTHER_RUNNING_INNER = 'running_inner'
274+
OTHER_RUNNING_OUTER = 'running_outer'
275+
OTHER_TERMINATED = 'terminated'
276+
277+
278+
def current_frames_includes_other_thread(test_case):
279+
import sys, threading
280+
281+
ready = threading.Event()
282+
outer_ready = threading.Event()
283+
release_inner = threading.Event()
284+
release_outer = threading.Event()
285+
worker_ident = None
286+
287+
def target():
288+
nonlocal worker_ident
289+
worker_ident = threading.get_ident()
290+
def target_inner():
291+
my_local_var = 60
292+
ready.set()
293+
release_inner.wait(timeout=my_local_var)
294+
target_local_var = 13
295+
target_inner()
296+
outer_ready.set()
297+
release_outer.wait(60)
298+
return target_local_var
299+
300+
thread = threading.Thread(target=target)
301+
thread.start()
302+
try:
303+
assert ready.wait(timeout=60)
304+
frames = sys._current_frames()
305+
if test_case == OTHER_TERMINATED:
306+
release_inner.set()
307+
release_outer.set()
308+
thread.join(timeout=60)
309+
elif test_case == OTHER_RUNNING_OUTER:
310+
release_inner.set()
311+
assert outer_ready.wait(timeout=60)
312+
assert worker_ident in frames
313+
314+
frame = frames[worker_ident]
315+
while frame is not None and frame.f_code.co_name != "target_inner":
316+
frame = frame.f_back
317+
318+
assert frame is not None
319+
assert frame.f_code.co_name == "target_inner", frame.f_code.co_name
320+
assert "target_inner" in repr(frame)
321+
assert "my_local_var" in frame.f_locals
322+
323+
frame = frame.f_back
324+
assert frame is not None
325+
assert "target_inner" not in repr(frame)
326+
assert "target" in repr(frame)
327+
assert frame.f_code.co_name == "target"
328+
assert "target_local_var" in frame.f_locals
329+
finally:
330+
release_inner.set()
331+
release_outer.set()
332+
thread.join(timeout=60)
333+
334+
335+
def test_current_frames_includes_other_thread_terminated():
336+
current_frames_includes_other_thread(OTHER_TERMINATED)
337+
338+
def test_current_frames_includes_other_thread_in_inner():
339+
current_frames_includes_other_thread(OTHER_RUNNING_INNER)
340+
341+
def test_current_frames_includes_other_thread_in_outer():
342+
current_frames_includes_other_thread(OTHER_RUNNING_OUTER)
343+
344+
345+
# this must be the last test!
346+
def test_clearing_globals():
347+
global foo, junk
348+
foo = 123
349+
assert "foo" in globals().keys()
350+
assert "junk" not in globals().keys()
351+
junk = globals().clear()
352+
assert "foo" not in globals().keys()
353+
assert "junk" in globals().keys()

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SysModuleBuiltins.java

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,18 @@
141141
import java.lang.ref.Reference;
142142
import java.nio.ByteOrder;
143143
import java.nio.charset.Charset;
144+
import java.util.ArrayList;
144145
import java.util.Arrays;
145146
import java.util.Date;
146147
import java.util.HashSet;
147148
import java.util.List;
148149
import java.util.Set;
150+
import java.util.concurrent.CancellationException;
151+
import java.util.concurrent.ConcurrentHashMap;
152+
import java.util.concurrent.ExecutionException;
153+
import java.util.concurrent.Future;
154+
import java.util.concurrent.TimeUnit;
155+
import java.util.concurrent.TimeoutException;
149156

150157
import com.oracle.graal.python.PythonLanguage;
151158
import com.oracle.graal.python.annotations.ArgumentClinic;
@@ -174,7 +181,7 @@
174181
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.HandlePointerConverter;
175182
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonToNativeInternalNode;
176183
import com.oracle.graal.python.builtins.objects.common.EconomicMapStorage;
177-
import com.oracle.graal.python.builtins.objects.common.HashingStorageNodes.HashingStorageSetItem;
184+
import com.oracle.graal.python.builtins.objects.common.ObjectHashMap;
178185
import com.oracle.graal.python.builtins.objects.dict.PDict;
179186
import com.oracle.graal.python.builtins.objects.exception.ExceptionNodes;
180187
import com.oracle.graal.python.builtins.objects.exception.GetEscapedExceptionNode;
@@ -208,6 +215,7 @@
208215
import com.oracle.graal.python.lib.PyNumberAsSizeNode;
209216
import com.oracle.graal.python.lib.PyObjectCallMethodObjArgs;
210217
import com.oracle.graal.python.lib.PyObjectGetAttr;
218+
import com.oracle.graal.python.lib.PyObjectHashNode;
211219
import com.oracle.graal.python.lib.PyObjectIsInstanceNode;
212220
import com.oracle.graal.python.lib.PyObjectLookupAttr;
213221
import com.oracle.graal.python.lib.PyObjectReprAsObjectNode;
@@ -225,7 +233,9 @@
225233
import com.oracle.graal.python.nodes.call.CallNode;
226234
import com.oracle.graal.python.nodes.call.special.LookupAndCallUnaryNode;
227235
import com.oracle.graal.python.nodes.call.special.SpecialMethodNotFound;
236+
import com.oracle.graal.python.nodes.frame.MaterializeFrameNode;
228237
import com.oracle.graal.python.nodes.frame.ReadFrameNode;
238+
import com.oracle.graal.python.nodes.frame.ReadFrameNode.AllPythonFramesSelector;
229239
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
230240
import com.oracle.graal.python.nodes.function.PythonBuiltinNode;
231241
import com.oracle.graal.python.nodes.function.builtins.PythonBinaryBuiltinNode;
@@ -238,6 +248,7 @@
238248
import com.oracle.graal.python.nodes.util.ExceptionStateNodes.GetCaughtExceptionNode;
239249
import com.oracle.graal.python.runtime.CallerFlags;
240250
import com.oracle.graal.python.runtime.ExecutionContext.BoundaryCallContext;
251+
import com.oracle.graal.python.runtime.GilNode;
241252
import com.oracle.graal.python.runtime.IndirectCallData.BoundaryCallData;
242253
import com.oracle.graal.python.runtime.PosixSupportLibrary;
243254
import com.oracle.graal.python.runtime.PythonContext;
@@ -251,8 +262,10 @@
251262
import com.oracle.truffle.api.CompilerDirectives;
252263
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
253264
import com.oracle.truffle.api.CompilerDirectives.ValueType;
265+
import com.oracle.truffle.api.ThreadLocalAction;
254266
import com.oracle.truffle.api.Truffle;
255267
import com.oracle.truffle.api.TruffleLanguage.Env;
268+
import com.oracle.truffle.api.TruffleSafepoint;
256269
import com.oracle.truffle.api.dsl.Bind;
257270
import com.oracle.truffle.api.dsl.Cached;
258271
import com.oracle.truffle.api.dsl.Cached.Shared;
@@ -265,6 +278,7 @@
265278
import com.oracle.truffle.api.dsl.NodeFactory;
266279
import com.oracle.truffle.api.dsl.Specialization;
267280
import com.oracle.truffle.api.exception.AbstractTruffleException;
281+
import com.oracle.truffle.api.frame.FrameInstance.FrameAccess;
268282
import com.oracle.truffle.api.frame.VirtualFrame;
269283
import com.oracle.truffle.api.nodes.Node;
270284
import com.oracle.truffle.api.strings.TruffleString;
@@ -883,22 +897,81 @@ static PFrame counted(VirtualFrame frame, int depth,
883897
@Builtin(name = "_current_frames")
884898
@GenerateNodeFactory
885899
abstract static class CurrentFrames extends PythonBuiltinNode {
900+
private static final long CURRENT_FRAMES_TIMEOUT_MILLIS = 20;
901+
886902
@Specialization
887903
Object currentFrames(VirtualFrame frame,
888904
@Bind Node inliningTarget,
889905
@Cached AuditNode auditNode,
890906
@Cached WarningsModuleBuiltins.WarnNode warnNode,
891907
@Cached ReadFrameNode readFrameNode,
892-
@Cached HashingStorageSetItem setHashingStorageItem,
908+
@Cached PyObjectHashNode hashNode,
909+
@Cached ObjectHashMap.PutNode putNode,
910+
@Bind PythonContext context,
893911
@Bind PythonLanguage language) {
894912
auditNode.audit(inliningTarget, "sys._current_frames");
895-
if (!getLanguage().singleThreadedAssumption.isValid()) {
896-
warnNode.warn(frame, RuntimeWarning, ErrorMessages.WARN_CURRENT_FRAMES_MULTITHREADED);
897-
}
898913
PFrame currentFrame = readFrameNode.getCurrentPythonFrame(frame);
899-
PDict result = PFactory.createDict(language);
900-
result.setDictStorage(setHashingStorageItem.execute(frame, inliningTarget, result.getDictStorage(), PThread.getThreadId(Thread.currentThread()), currentFrame));
901-
return result;
914+
EconomicMapStorage framesMap = collectCurrentFrames(inliningTarget, context, currentFrame);
915+
return PFactory.createDict(language, framesMap);
916+
}
917+
918+
@TruffleBoundary
919+
@SuppressWarnings("try")
920+
private static EconomicMapStorage collectCurrentFrames(Node inliningTarget, PythonContext context, PFrame currentFrame) {
921+
Thread currentThread = Thread.currentThread();
922+
Thread[] threads = context.getThreads();
923+
ArrayList<Thread> targetThreads = new ArrayList<>(threads.length);
924+
ConcurrentHashMap<Thread, Object> frames = new ConcurrentHashMap<>();
925+
frames.put(currentThread, escapedFrameOrPlaceholder(currentFrame));
926+
for (Thread thread : threads) {
927+
if (thread != currentThread && thread.isAlive()) {
928+
targetThreads.add(thread);
929+
}
930+
}
931+
if (!targetThreads.isEmpty()) {
932+
Thread[] threadArray = targetThreads.toArray(new Thread[0]);
933+
Future<Void> future = context.getEnv().submitThreadLocal(threadArray, new ThreadLocalAction(true, false) {
934+
@Override
935+
protected void perform(Access access) {
936+
PFrame pyFrame = ReadFrameNode.readFrameInThreadLocal(access, null, FrameAccess.READ_ONLY, AllPythonFramesSelector.INSTANCE, 0, CallerFlags.NEEDS_PFRAME,
937+
MaterializeFrameNode.getUncached());
938+
frames.put(access.getThread(), escapedFrameOrPlaceholder(pyFrame));
939+
}
940+
});
941+
boolean[] timedOut = new boolean[1];
942+
try (var gil = GilNode.uncachedRelease()) {
943+
TruffleSafepoint.setBlockedThreadInterruptible(inliningTarget, voidFuture -> {
944+
try {
945+
voidFuture.get(CURRENT_FRAMES_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
946+
} catch (TimeoutException e) {
947+
timedOut[0] = true;
948+
} catch (CancellationException e) {
949+
// Ignore cancellation; unanswered threads will get assigned NONE
950+
} catch (ExecutionException e) {
951+
throw new RuntimeException(e);
952+
}
953+
}, future);
954+
}
955+
if (timedOut[0]) {
956+
future.cancel(false);
957+
}
958+
}
959+
960+
EconomicMapStorage storage = EconomicMapStorage.create(targetThreads.size());
961+
for (Thread thread : targetThreads) {
962+
long key = PThread.getThreadId(thread);
963+
long hash = PyObjectHashNode.hash(key);
964+
ObjectHashMap.PutNode.putUncached(storage, key, hash, frames.getOrDefault(thread, PNone.NONE));
965+
}
966+
return storage;
967+
}
968+
969+
private static Object escapedFrameOrPlaceholder(PFrame frame) {
970+
if (frame != null) {
971+
frame.getRef().markAsEscaped();
972+
return frame;
973+
}
974+
return PNone.NONE;
902975
}
903976
}
904977

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/frame/FrameBuiltins.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ public abstract static class GetBackrefNode extends PythonBuiltinNode {
284284
@Specialization
285285
Object getBackref(VirtualFrame frame, PFrame self,
286286
@Cached ReadFrameNode readCallerFrame) {
287-
PFrame backref = readCallerFrame.getFrameForReference(frame, self.getRef(), 1, 0);
287+
PFrame backref = readCallerFrame.getFrameForReference(frame, self.getRef(), ReadFrameNode.AllPythonFramesSelector.INSTANCE, 1, 0,
288+
self.getThread());
288289
if (backref != null) {
289290
backref.getRef().markAsEscaped();
290291
return backref;

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,8 +1244,6 @@ public abstract class ErrorMessages {
12441244
public static final TruffleString WARN_IGNORE_UNIMPORTABLE_BREAKPOINT_S = tsLiteral("Ignoring unimportable $PYTHONBREAKPOINT: \"%s\"");
12451245
public static final TruffleString WARN_DEPRECTATED_SYS_CHECKINTERVAL = tsLiteral("sys.getcheckinterval() and sys.setcheckinterval() " +
12461246
"are deprecated. Use sys.getswitchinterval() instead.");
1247-
public static final TruffleString WARN_CURRENT_FRAMES_MULTITHREADED = tsLiteral(
1248-
"GraalPy doesn't support obtaining frames of other threads. That means python debuggers can only see the currently stopped thread");
12491247
public static final TruffleString WARN_ENCODING_ARGUMENT_NOT_SPECIFIED = tsLiteral("'encoding' argument not specified");
12501248
public static final TruffleString WARN_DELEGATION_OF_INT_TO_TRUNC_IS_DEPRECATED = tsLiteral("The delegation of int() to __trunc__ is deprecated.");
12511249

0 commit comments

Comments
 (0)