Skip to content

Commit ef3e423

Browse files
committed
Synchronize wallclock times reported by emscripten_get_now() across all pthreads for consistency.
1 parent 9fc522a commit ef3e423

File tree

5 files changed

+111
-22
lines changed

5 files changed

+111
-22
lines changed

src/library.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3979,6 +3979,11 @@ LibraryManager.library = {
39793979
" var t = process['hrtime']();\n" +
39803980
" return t[0] * 1e3 + t[1] / 1e6;\n" +
39813981
" };\n" +
3982+
#if USE_PTHREADS
3983+
// Pthreads need their clocks synchronized to the execution of the main thread, so give them a special form of the function.
3984+
"} else if (ENVIRONMENT_IS_PTHREAD) {\n" +
3985+
" _emscripten_get_now = function() { return performance['now']() - __performance_now_clock_drift; };\n" +
3986+
#endif
39823987
"} else if (typeof dateNow !== 'undefined') {\n" +
39833988
" _emscripten_get_now = dateNow;\n" +
39843989
"} else if (typeof self === 'object' && self['performance'] && typeof self['performance']['now'] === 'function') {\n" +

src/library_pthread.js

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,12 @@ var LibraryPThread = {
322322
} else if (d.cmd === 'cancelThread') {
323323
__cancel_thread(d.thread);
324324
} else if (d.cmd === 'loaded') {
325+
worker.loaded = true;
326+
// If this Worker is already pending to start running a thread, launch the thread now
327+
if (worker.runPthread) {
328+
worker.runPthread();
329+
delete worker.runPthread;
330+
}
325331
++numWorkersLoaded;
326332
if (numWorkersLoaded === numWorkers && onFinishedLoading) {
327333
onFinishedLoading();
@@ -477,22 +483,29 @@ var LibraryPThread = {
477483
#endif
478484

479485
worker.pthread = pthread;
480-
481-
// Ask the worker to start executing its pthread entry point function.
482-
worker.postMessage({
483-
cmd: 'run',
484-
start_routine: threadParams.startRoutine,
485-
arg: threadParams.arg,
486-
threadInfoStruct: threadParams.pthread_ptr,
487-
selfThreadId: threadParams.pthread_ptr, // TODO: Remove this since thread ID is now the same as the thread address.
488-
parentThreadId: threadParams.parent_pthread_ptr,
489-
stackBase: threadParams.stackBase,
490-
stackSize: threadParams.stackSize,
486+
var msg = {
487+
cmd: 'run',
488+
start_routine: threadParams.startRoutine,
489+
arg: threadParams.arg,
490+
threadInfoStruct: threadParams.pthread_ptr,
491+
selfThreadId: threadParams.pthread_ptr, // TODO: Remove this since thread ID is now the same as the thread address.
492+
parentThreadId: threadParams.parent_pthread_ptr,
493+
stackBase: threadParams.stackBase,
494+
stackSize: threadParams.stackSize,
491495
#if OFFSCREENCANVAS_SUPPORT
492-
moduleCanvasId: threadParams.moduleCanvasId,
493-
offscreenCanvases: threadParams.offscreenCanvases,
496+
moduleCanvasId: threadParams.moduleCanvasId,
497+
offscreenCanvases: threadParams.offscreenCanvases,
494498
#endif
495-
}, threadParams.transferList);
499+
};
500+
worker.runPthread = function() {
501+
// Ask the worker to start executing its pthread entry point function.
502+
msg.time = performance.now();
503+
worker.postMessage(msg, threadParams.transferList);
504+
};
505+
if (worker.loaded) {
506+
worker.runPthread();
507+
delete worker.runPthread;
508+
}
496509
},
497510

498511
#if USE_PTHREADS

src/pthread-main.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ var DYNAMIC_BASE = 0;
2323

2424
var ENVIRONMENT_IS_PTHREAD = true;
2525

26+
// performance.now() is specced to return a wallclock time in msecs since that Web Worker/main thread launched. However for pthreads this can cause
27+
// subtle problems in emscripten_get_now() as this essentially would measure time from pthread_create(), meaning that the clocks between each threads
28+
// would be wildly out of sync. Therefore sync all pthreads to the clock on the main browser thread, so that different threads see a somewhat
29+
// coherent clock across each of them (+/- 0.1msecs in testing)
30+
var __performance_now_clock_drift = 0;
31+
2632
// Cannot use console.log or console.error in a web worker, since that would risk a browser deadlock! https://bugzilla.mozilla.org/show_bug.cgi?id=1049091
2733
// Therefore implement custom logging facility for threads running in a worker, which queue the messages to main thread to print.
2834
var Module = {};
@@ -106,6 +112,7 @@ this.onmessage = function(e) {
106112
} else if (e.data.cmd === 'objectTransfer') {
107113
PThread.receiveObjectTransfer(e.data);
108114
} else if (e.data.cmd === 'run') { // This worker was idle, and now should start executing its pthread entry point.
115+
__performance_now_clock_drift = performance.now() - e.data.time; // Sync up to the clock of the main thread.
109116
threadInfoStruct = e.data.threadInfoStruct;
110117
__register_pthread_ptr(threadInfoStruct, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0); // Pass the thread address inside the asm.js scope to store it for fast access that avoids the need for a FFI out.
111118
assert(threadInfoStruct);
@@ -126,17 +133,18 @@ this.onmessage = function(e) {
126133
//#endif
127134

128135
PThread.receiveObjectTransfer(e.data);
129-
130136
PThread.setThreadStatus(_pthread_self(), 1/*EM_THREAD_STATUS_RUNNING*/);
131137

132138
try {
133-
// HACK: Some code in the wild has instead signatures of form 'void *ThreadMain()', which seems to be ok in native code.
134-
// To emulate supporting both in test suites, use the following form. This is brittle!
135-
if (typeof Module['asm']['dynCall_ii'] !== 'undefined') {
136-
result = Module['asm'].dynCall_ii(e.data.start_routine, e.data.arg); // pthread entry points are always of signature 'void *ThreadMain(void *arg)'
137-
} else {
138-
result = Module['asm'].dynCall_i(e.data.start_routine); // as a hack, try signature 'i' as fallback.
139-
}
139+
// pthread entry points are always of signature 'void *ThreadMain(void *arg)'
140+
// Native codebases sometimes spawn threads with other thread entry point signatures,
141+
// such as void ThreadMain(void *arg), void *ThreadMain(), or void ThreadMain().
142+
// That is not acceptable per C/C++ specification, but x86 compiler ABI extensions
143+
// enable that to work. If you find the following line to crash, either change the signature
144+
// to "proper" void *ThreadMain(void *arg) form, or try linking with the Emscripten linker
145+
// flag -s EMULATE_FUNCTION_POINTER_CASTS=1 to add in emulation for this x86 ABI extension.
146+
result = Module['asm'].dynCall_ii(e.data.start_routine, e.data.arg);
147+
140148
//#if STACK_OVERFLOW_CHECK
141149
if (typeof checkStackCookie === 'function') checkStackCookie();
142150
//#endif
@@ -163,6 +171,10 @@ this.onmessage = function(e) {
163171
}
164172
} else if (e.data.target === 'setimmediate') {
165173
// no-op
174+
} else if (e.data.cmd === 'processThreadQueue') {
175+
if (threadInfoStruct) { // If this thread is actually running?
176+
_emscripten_current_thread_process_queued_calls();
177+
}
166178
} else {
167179
Module['printErr']('pthread-main.js received unknown command ' + e.data.cmd);
168180
console.error(e.data);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#include <pthread.h>
2+
#include <emscripten.h>
3+
#include <emscripten/threading.h>
4+
#include <math.h>
5+
6+
volatile int threadStarted = 0;
7+
volatile int timeReceived = 0;
8+
volatile double mainThreadTime;
9+
10+
void wait(volatile int *address)
11+
{
12+
int state = emscripten_atomic_load_u32((void*)address);
13+
while(state == 0)
14+
state = emscripten_atomic_load_u32((void*)address);
15+
}
16+
17+
void wake(volatile int *address)
18+
{
19+
emscripten_atomic_store_u32((void*)address, 1);
20+
}
21+
22+
void *thread_main(void *arg)
23+
{
24+
wake(&threadStarted);
25+
wait(&timeReceived);
26+
double pthreadTime = emscripten_get_now();
27+
double timeDifference = pthreadTime - mainThreadTime;
28+
printf("Time difference between pthread and main thread is %f msecs.\n", timeDifference);
29+
30+
#ifdef REPORT_RESULT
31+
REPORT_RESULT(fabs(timeDifference) < 200); // The time difference here should be well less than 1 msec, but test against 200msecs to be super-sure.
32+
#endif
33+
return 0;
34+
}
35+
36+
void busy_sleep(double msecs)
37+
{
38+
double end = emscripten_get_now() + msecs;
39+
while(emscripten_get_now() < end)
40+
;
41+
}
42+
43+
int main()
44+
{
45+
// Cause a one second delay between main() and pthread start that might have a chance to drift the wallclocks on emscripten_get_now().
46+
busy_sleep(1000);
47+
48+
pthread_t thread;
49+
pthread_create(&thread, NULL, thread_main, NULL);
50+
wait(&threadStarted);
51+
mainThreadTime = emscripten_get_now();
52+
wake(&timeReceived);
53+
54+
EM_ASM(Module['noExitRuntime']=true);
55+
}

tests/test_browser.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3349,6 +3349,10 @@ def test_pthread_global_data_initialization(self):
33493349
for args in [[], ['-O3']]:
33503350
self.btest(path_from_root('tests', 'pthread', 'test_pthread_global_data_initialization.c'), expected='20', args=args+mem_init_mode+['-s', 'USE_PTHREADS=1', '-s', 'PROXY_TO_PTHREAD=1'], also_wasm=False)
33513351

3352+
# Test that emscripten_get_now() reports coherent wallclock times across all pthreads, instead of each pthread independently reporting wallclock times since the launch of that pthread.
3353+
def test_pthread_clock_drift(self):
3354+
self.btest(path_from_root('tests', 'pthread', 'test_pthread_clock_drift.cpp'), expected='1', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PROXY_TO_PTHREAD=1'])
3355+
33523356
# test atomicrmw i64
33533357
def test_atomicrmw_i64(self):
33543358
Popen([PYTHON, EMCC, path_from_root('tests', 'atomicrmw_i64.ll'), '-s', 'USE_PTHREADS=1', '-s', 'IN_TEST_HARNESS=1', '-o', 'test.html']).communicate()

0 commit comments

Comments
 (0)