Skip to content

Commit bb48d46

Browse files
committed
[GR-16295] Parallelize test execution for tagged unittests
PullRequest: graalpython/537
2 parents 5372b5b + 9cda534 commit bb48d46

File tree

8 files changed

+92
-19
lines changed

8 files changed

+92
-19
lines changed

graalpython/com.oracle.graal.python.test/src/graalpytest.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
#!/usr/bin/env mx python
4141
import _io
4242
import sys
43+
import time
44+
import _thread
4345

4446
os = sys.modules.get("posix", sys.modules.get("nt", None))
4547
if os is None:
@@ -51,6 +53,49 @@
5153

5254
verbose = False
5355

56+
print_lock = _thread.RLock()
57+
class ThreadPool():
58+
cnt_lock = _thread.RLock()
59+
cnt = 0
60+
if os.environ.get(b"ENABLE_THREADED_GRAALPYTEST") == b"true":
61+
maxcnt = min(os.cpu_count(), 16)
62+
sleep = time.sleep
63+
print("Running with %d threads" % maxcnt)
64+
else:
65+
sleep = lambda x: x
66+
maxcnt = 1
67+
68+
@classmethod
69+
def start(self, function):
70+
self.acquire_token()
71+
def runner():
72+
try:
73+
function()
74+
finally:
75+
self.release_token()
76+
_thread.start_new_thread(runner, ())
77+
self.sleep(0.5)
78+
79+
@classmethod
80+
def acquire_token(self):
81+
while True:
82+
with self.cnt_lock:
83+
if self.cnt < self.maxcnt:
84+
self.cnt += 1
85+
break
86+
self.sleep(1)
87+
88+
@classmethod
89+
def release_token(self):
90+
with self.cnt_lock:
91+
self.cnt -= 1
92+
93+
@classmethod
94+
def shutdown(self):
95+
self.sleep(2)
96+
while self.cnt > 0:
97+
self.sleep(2)
98+
5499

55100
def dump_truffle_ast(func):
56101
try:
@@ -72,7 +117,8 @@ def __init__(self):
72117

73118
def run_safely(self, func, print_immediately=False):
74119
if verbose:
75-
print(u"\n\t\u21B3 ", func.__name__, " ", end="")
120+
with print_lock:
121+
print(u"\n\t\u21B3 ", func.__name__, " ", end="")
76122
try:
77123
func()
78124
except BaseException as e:
@@ -102,10 +148,12 @@ def run_test(self, func):
102148
pass
103149
elif not hasattr(func, "__call__"):
104150
pass
105-
elif self.run_safely(func):
106-
self.success()
107151
else:
108-
self.failure()
152+
def do_run():
153+
r = self.run_safely(func)
154+
with print_lock:
155+
self.success() if r else self.failure()
156+
ThreadPool.start(do_run)
109157

110158
def success(self):
111159
self.passed += 1
@@ -318,6 +366,7 @@ def run(self):
318366
self.failed += testcase.failed
319367
if verbose:
320368
print()
369+
ThreadPool.shutdown()
321370
print("\n\nRan %d tests (%d passes, %d failures)" % (self.passed + self.failed, self.passed, self.failed))
322371
for e in self.exceptions:
323372
print(e)

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,14 @@ def working_tests():
7272
return working_tests
7373

7474

75+
class TestAllWorkingTests():
76+
pass
77+
78+
7579
WORKING_TESTS = working_tests()
7680
for idx, working_test in enumerate(WORKING_TESTS):
7781
def make_test_func(working_test):
78-
def fun():
82+
def fun(self):
7983
cmd = [sys.executable, "-S", "-m", "unittest"]
8084
for testpattern in working_test[1]:
8185
cmd.extend(["-k", testpattern])
@@ -88,7 +92,7 @@ def fun():
8892
return fun
8993

9094
test_f = make_test_func(working_test)
91-
globals()[test_f.__name__] = test_f
95+
setattr(TestAllWorkingTests, test_f.__name__, test_f)
9296
del test_f
9397

9498

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ public final class PythonLanguage extends TruffleLanguage<PythonContext> {
121121

122122
private static final Object[] CONTEXT_INSENSITIVE_SINGLETONS = new Object[]{PNone.NONE, PNone.NO_VALUE, PEllipsis.INSTANCE, PNotImplemented.NOT_IMPLEMENTED};
123123

124+
/*
125+
* We need to store this here, because the check is on the language and can come from a thread
126+
* that has no context, but we enable or disable threads with a context option. So we store this
127+
* here when a context is created.
128+
*/
129+
private Boolean isWithThread = null;
130+
124131
public static int getNumberOfSpecialSingletons() {
125132
return CONTEXT_INSENSITIVE_SINGLETONS.length;
126133
}
@@ -153,14 +160,14 @@ protected void finalizeContext(PythonContext context) {
153160
protected boolean areOptionsCompatible(OptionValues firstOptions, OptionValues newOptions) {
154161
// internal sources were marked during context initialization
155162
return (firstOptions.get(PythonOptions.ExposeInternalSources).equals(newOptions.get(PythonOptions.ExposeInternalSources)) &&
163+
// we cache WithThread on the lanugage
164+
firstOptions.get(PythonOptions.WithThread).equals(newOptions.get(PythonOptions.WithThread)) &&
156165
// we cache CatchAllExceptions hard on TryExceptNode
157166
firstOptions.get(PythonOptions.CatchAllExceptions).equals(newOptions.get(PythonOptions.CatchAllExceptions)));
158167
}
159168

160169
private boolean areOptionsCompatibleWithPreinitializedContext(OptionValues firstOptions, OptionValues newOptions) {
161170
return (areOptionsCompatible(firstOptions, newOptions) &&
162-
// we cache WithThread in SysConfigModuleBuiltins
163-
firstOptions.get(PythonOptions.WithThread).equals(newOptions.get(PythonOptions.WithThread)) &&
164171
// disabling TRegex has an effect on the _sre Python functions that are
165172
// dynamically created
166173
firstOptions.get(PythonOptions.WithTRegex).equals(newOptions.get(PythonOptions.WithTRegex)));
@@ -180,6 +187,8 @@ protected boolean patchContext(PythonContext context, Env newEnv) {
180187

181188
@Override
182189
protected PythonContext createContext(Env env) {
190+
assert this.isWithThread == null || this.isWithThread == PythonOptions.isWithThread(env) : "conflicting thread options in the same language!";
191+
this.isWithThread = PythonOptions.isWithThread(env);
183192
ensureHomeInOptions(env);
184193
Python3Core newCore = new Python3Core(new PythonParserImpl());
185194
return new PythonContext(this, env, newCore);
@@ -604,7 +613,7 @@ protected boolean isThreadAccessAllowed(Thread thread, boolean singleThreaded) {
604613
if (singleThreaded) {
605614
return super.isThreadAccessAllowed(thread, singleThreaded);
606615
}
607-
return PythonOptions.isWithThread();
616+
return isWithThread;
608617
}
609618

610619
@Override

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public class SysConfigModuleBuiltins extends PythonBuiltins {
6767
@Override
6868
public void initialize(PythonCore core) {
6969
super.initialize(core);
70-
STATIC_CONFIG_OPTIONS.put("WITH_THREAD", PythonOptions.isWithThread() ? 1 : 0);
70+
STATIC_CONFIG_OPTIONS.put("WITH_THREAD", PythonOptions.isWithThread(core.getContext().getEnv()) ? 1 : 0);
7171
}
7272

7373
@Override

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/referencetype/ReferenceTypeBuiltins.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,15 @@ public int hash(PReferenceType self) {
103103
return self.getHash();
104104
}
105105

106-
@Fallback
107-
public int hash(@SuppressWarnings("unused") Object self) {
106+
@Specialization(guards = "self.getObject() == null")
107+
public int hashGone(@SuppressWarnings("unused") PReferenceType self) {
108108
throw raise(PythonErrorType.TypeError, "weak object has gone away");
109109
}
110+
111+
@Fallback
112+
public int hashWrong(@SuppressWarnings("unused") Object self) {
113+
throw raise(PythonErrorType.TypeError, "descriptor '__hash__' requires a 'weakref' object but received a '%p'", self);
114+
}
110115
}
111116

112117
// ref.__repr__

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonOptions.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.oracle.graal.python.PythonLanguage;
2929
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
3030
import com.oracle.truffle.api.Option;
31+
import com.oracle.truffle.api.TruffleLanguage.Env;
3132

3233
import org.graalvm.options.OptionCategory;
3334
import org.graalvm.options.OptionDescriptors;
@@ -162,6 +163,11 @@ public static <T> T getOption(PythonContext context, OptionKey<T> key) {
162163
return context.getOptions().get(key);
163164
}
164165

166+
@TruffleBoundary
167+
public static <T> T getOption(Env env, OptionKey<T> key) {
168+
return env.getOptions().get(key);
169+
}
170+
165171
@TruffleBoundary
166172
public static int getIntOption(PythonContext context, OptionKey<Integer> key) {
167173
if (context == null) {
@@ -198,8 +204,8 @@ public static int getMinLazyStringLength() {
198204
return getOption(PythonLanguage.getContextRef().get(), MinLazyStringLength);
199205
}
200206

201-
public static boolean isWithThread() {
202-
return getOption(PythonLanguage.getContextRef().get(), WithThread);
207+
public static boolean isWithThread(Env env) {
208+
return getOption(env, WithThread);
203209
}
204210

205211
public static boolean getEnableForcedSplits() {

graalpython/lib-graalpython/_thread.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ def _set_sentinel():
5555

5656

5757
def load():
58-
import sys
59-
filename = sys.graal_python_stdlib_home + ("/_dummy_thread.py")
60-
_dummy_thread = __import__(filename, "_thread")
6158
if not _sysconfig.get_config_vars().get('WITH_THREAD'):
59+
import sys
60+
filename = sys.graal_python_stdlib_home + ("/_dummy_thread.py")
61+
_dummy_thread = __import__(filename, "_thread")
6262
sys.modules["_thread"] = _dummy_thread
6363
load()
6464
del load

mx.graalpython/mx_graalpython.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,8 @@ def graalpython_gate_runner(args, tasks):
360360

361361
with Task('GraalPython Python tests', tasks, tags=[GraalPythonTags.tagged]) as task:
362362
if task:
363-
with set_env(ENABLE_CPYTHON_TAGGED_UNITTESTS="true"):
364-
run_python_unittests(python_gvm(), paths=["test_tagged_unittests.py"])
363+
with set_env(ENABLE_CPYTHON_TAGGED_UNITTESTS="true", ENABLE_THREADED_GRAALPYTEST="true"):
364+
run_python_unittests(python_gvm(), args=["--python.WithThread=true"], paths=["test_tagged_unittests.py"])
365365

366366
# Unittests on SVM
367367
with Task('GraalPython tests on SVM', tasks, tags=[GraalPythonTags.svmunit]) as task:

0 commit comments

Comments
 (0)