Skip to content

Commit 08ccc11

Browse files
authored
Merge pull request #6084 from juj/sync_performance_now
Multithreading 17/N: Sync performance.now()
2 parents bf57398 + a936dcb commit 08ccc11

File tree

5 files changed

+205
-113
lines changed

5 files changed

+205
-113
lines changed

src/library.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4015,6 +4015,11 @@ LibraryManager.library = {
40154015
" var t = process['hrtime']();\n" +
40164016
" return t[0] * 1e3 + t[1] / 1e6;\n" +
40174017
" };\n" +
4018+
#if USE_PTHREADS
4019+
// Pthreads need their clocks synchronized to the execution of the main thread, so give them a special form of the function.
4020+
"} else if (ENVIRONMENT_IS_PTHREAD) {\n" +
4021+
" _emscripten_get_now = function() { return performance['now']() - __performance_now_clock_drift; };\n" +
4022+
#endif
40184023
"} else if (typeof dateNow !== 'undefined') {\n" +
40194024
" _emscripten_get_now = dateNow;\n" +
40204025
"} else if (typeof self === 'object' && self['performance'] && typeof self['performance']['now'] === 'function') {\n" +

src/library_pthread.js

Lines changed: 121 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -252,106 +252,115 @@ var LibraryPThread = {
252252
Module['print']('Preallocating ' + numWorkers + ' workers for a pthread spawn pool.');
253253

254254
var numWorkersLoaded = 0;
255+
var pthreadMainJs = 'pthread-main.js';
256+
// Allow HTML module to configure the location where the 'pthread-main.js' file will be loaded from,
257+
// either via Module.locateFile() function, or via Module.pthreadMainPrefixURL string. If neither
258+
// of these are passed, then the default URL 'pthread-main.js' relative to the main html file is loaded.
259+
if (typeof Module['locateFile'] === 'function') pthreadMainJs = Module['locateFile'](pthreadMainJs);
260+
else if (Module['pthreadMainPrefixURL']) pthreadMainJs = Module['pthreadMainPrefixURL'] + pthreadMainJs;
261+
255262
for (var i = 0; i < numWorkers; ++i) {
256-
var pthreadMainJs = 'pthread-main.js';
257-
// Allow HTML module to configure the location where the 'pthread-main.js' file will be loaded from,
258-
// either via Module.locateFile() function, or via Module.pthreadMainPrefixURL string. If neither
259-
// of these are passed, then the default URL 'pthread-main.js' relative to the main html file is loaded.
260-
if (typeof Module['locateFile'] === 'function') pthreadMainJs = Module['locateFile'](pthreadMainJs);
261-
else if (Module['pthreadMainPrefixURL']) pthreadMainJs = Module['pthreadMainPrefixURL'] + pthreadMainJs;
262263
var worker = new Worker(pthreadMainJs);
263264

264-
worker.onmessage = function(e) {
265-
var d = e.data;
266-
// TODO: Move the proxied call mechanism into a queue inside heap.
267-
if (d.proxiedCall) {
268-
var returnValue;
269-
var funcTable = (d.func >= 0) ? proxiedFunctionTable : ASM_CONSTS;
270-
var funcIdx = (d.func >= 0) ? d.func : (-1 - d.func);
271-
PThread.currentProxiedOperationCallerThread = worker.pthread.threadInfoStruct; // Sometimes we need to backproxy events to the calling thread (e.g. HTML5 DOM events handlers such as emscripten_set_mousemove_callback()), so keep track in a globally accessible variable about the thread that initiated the proxying.
272-
switch(d.proxiedCall & 31) {
273-
case 1: returnValue = funcTable[funcIdx](); break;
274-
case 2: returnValue = funcTable[funcIdx](d.p0); break;
275-
case 3: returnValue = funcTable[funcIdx](d.p0, d.p1); break;
276-
case 4: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2); break;
277-
case 5: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3); break;
278-
case 6: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4); break;
279-
case 7: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5); break;
280-
case 8: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5, d.p6); break;
281-
case 9: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5, d.p6, d.p7); break;
282-
case 10: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5, d.p6, d.p7, d.p8); break;
283-
default:
284-
if (d.proxiedCall) {
285-
Module['printErr']("worker sent an unknown proxied call idx " + d.proxiedCall);
286-
console.error(e.data);
287-
}
288-
break;
289-
}
290-
if (d.returnValue) {
291-
if (d.proxiedCall < 32) HEAP32[d.returnValue >> 2] = returnValue;
292-
else HEAPF64[d.returnValue >> 3] = returnValue;
265+
(function(worker) {
266+
worker.onmessage = function(e) {
267+
var d = e.data;
268+
// TODO: Move the proxied call mechanism into a queue inside heap.
269+
if (d.proxiedCall) {
270+
var returnValue;
271+
var funcTable = (d.func >= 0) ? proxiedFunctionTable : ASM_CONSTS;
272+
var funcIdx = (d.func >= 0) ? d.func : (-1 - d.func);
273+
PThread.currentProxiedOperationCallerThread = worker.pthread.threadInfoStruct; // Sometimes we need to backproxy events to the calling thread (e.g. HTML5 DOM events handlers such as emscripten_set_mousemove_callback()), so keep track in a globally accessible variable about the thread that initiated the proxying.
274+
switch(d.proxiedCall & 31) {
275+
case 1: returnValue = funcTable[funcIdx](); break;
276+
case 2: returnValue = funcTable[funcIdx](d.p0); break;
277+
case 3: returnValue = funcTable[funcIdx](d.p0, d.p1); break;
278+
case 4: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2); break;
279+
case 5: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3); break;
280+
case 6: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4); break;
281+
case 7: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5); break;
282+
case 8: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5, d.p6); break;
283+
case 9: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5, d.p6, d.p7); break;
284+
case 10: returnValue = funcTable[funcIdx](d.p0, d.p1, d.p2, d.p3, d.p4, d.p5, d.p6, d.p7, d.p8); break;
285+
default:
286+
if (d.proxiedCall) {
287+
Module['printErr']("worker sent an unknown proxied call idx " + d.proxiedCall);
288+
console.error(e.data);
289+
}
290+
break;
291+
}
292+
if (d.returnValue) {
293+
if (d.proxiedCall < 32) HEAP32[d.returnValue >> 2] = returnValue;
294+
else HEAPF64[d.returnValue >> 3] = returnValue;
295+
}
296+
var waitAddress = d.waitAddress;
297+
if (waitAddress) {
298+
Atomics.store(HEAP32, waitAddress >> 2, 1);
299+
Atomics.wake(HEAP32, waitAddress >> 2, 1);
300+
}
301+
return;
293302
}
294-
var waitAddress = d.waitAddress;
295-
if (waitAddress) {
296-
Atomics.store(HEAP32, waitAddress >> 2, 1);
297-
Atomics.wake(HEAP32, waitAddress >> 2, 1);
303+
304+
// If this message is intended to a recipient that is not the main thread, forward it to the target thread.
305+
if (d.targetThread && d.targetThread != _pthread_self()) {
306+
var thread = PThread.pthreads[d.targetThread];
307+
if (thread) {
308+
thread.worker.postMessage(e.data, d.transferList);
309+
} else {
310+
console.error('Internal error! Worker sent a message "' + d.cmd + '" to target pthread ' + d.targetThread + ', but that thread no longer exists!');
311+
}
312+
return;
298313
}
299-
return;
300-
}
301314

302-
// If this message is intended to a recipient that is not the main thread, forward it to the target thread.
303-
if (d.targetThread && d.targetThread != _pthread_self()) {
304-
var thread = PThread.pthreads[d.targetThread];
305-
if (thread) {
306-
thread.worker.postMessage(e.data, d.transferList);
315+
if (d.cmd === 'processQueuedMainThreadWork') {
316+
// TODO: Must post message to main Emscripten thread in PROXY_TO_WORKER mode.
317+
_emscripten_main_thread_process_queued_calls();
318+
} else if (d.cmd === 'spawnThread') {
319+
__spawn_thread(e.data);
320+
} else if (d.cmd === 'cleanupThread') {
321+
__cleanup_thread(d.thread);
322+
} else if (d.cmd === 'killThread') {
323+
__kill_thread(d.thread);
324+
} else if (d.cmd === 'cancelThread') {
325+
__cancel_thread(d.thread);
326+
} else if (d.cmd === 'loaded') {
327+
worker.loaded = true;
328+
// If this Worker is already pending to start running a thread, launch the thread now
329+
if (worker.runPthread) {
330+
worker.runPthread();
331+
delete worker.runPthread;
332+
}
333+
++numWorkersLoaded;
334+
if (numWorkersLoaded === numWorkers && onFinishedLoading) {
335+
onFinishedLoading();
336+
}
337+
} else if (d.cmd === 'print') {
338+
Module['print']('Thread ' + d.threadId + ': ' + d.text);
339+
} else if (d.cmd === 'printErr') {
340+
Module['printErr']('Thread ' + d.threadId + ': ' + d.text);
341+
} else if (d.cmd === 'alert') {
342+
alert('Thread ' + d.threadId + ': ' + d.text);
343+
} else if (d.cmd === 'exit') {
344+
// currently no-op
345+
} else if (d.cmd === 'cancelDone') {
346+
PThread.freeThreadData(worker.pthread);
347+
worker.pthread = undefined; // Detach the worker from the pthread object, and return it to the worker pool as an unused worker.
348+
PThread.unusedWorkerPool.push(worker);
349+
// TODO: Free if detached.
350+
PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker.pthread), 1); // Not a running Worker anymore.
351+
} else if (d.cmd === 'objectTransfer') {
352+
PThread.receiveObjectTransfer(e.data);
353+
} else if (e.data.target === 'setimmediate') {
354+
worker.postMessage(e.data); // Worker wants to postMessage() to itself to implement setImmediate() emulation.
307355
} else {
308-
console.error('Internal error! Worker sent a message "' + d.cmd + '" to target pthread ' + d.targetThread + ', but that thread no longer exists!');
356+
Module['printErr']("worker sent an unknown command " + d.cmd);
309357
}
310-
return;
311-
}
358+
};
312359

313-
if (d.cmd === 'processQueuedMainThreadWork') {
314-
// TODO: Must post message to main Emscripten thread in PROXY_TO_WORKER mode.
315-
_emscripten_main_thread_process_queued_calls();
316-
} else if (d.cmd === 'spawnThread') {
317-
__spawn_thread(e.data);
318-
} else if (d.cmd === 'cleanupThread') {
319-
__cleanup_thread(d.thread);
320-
} else if (d.cmd === 'killThread') {
321-
__kill_thread(d.thread);
322-
} else if (d.cmd === 'cancelThread') {
323-
__cancel_thread(d.thread);
324-
} else if (d.cmd === 'loaded') {
325-
++numWorkersLoaded;
326-
if (numWorkersLoaded === numWorkers && onFinishedLoading) {
327-
onFinishedLoading();
328-
}
329-
} else if (d.cmd === 'print') {
330-
Module['print']('Thread ' + d.threadId + ': ' + d.text);
331-
} else if (d.cmd === 'printErr') {
332-
Module['printErr']('Thread ' + d.threadId + ': ' + d.text);
333-
} else if (d.cmd === 'alert') {
334-
alert('Thread ' + d.threadId + ': ' + d.text);
335-
} else if (d.cmd === 'exit') {
336-
// currently no-op
337-
} else if (d.cmd === 'cancelDone') {
338-
PThread.freeThreadData(worker.pthread);
339-
worker.pthread = undefined; // Detach the worker from the pthread object, and return it to the worker pool as an unused worker.
340-
PThread.unusedWorkerPool.push(worker);
341-
// TODO: Free if detached.
342-
PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker.pthread), 1); // Not a running Worker anymore.
343-
} else if (d.cmd === 'objectTransfer') {
344-
PThread.receiveObjectTransfer(e.data);
345-
} else if (e.data.target === 'setimmediate') {
346-
worker.postMessage(e.data); // Worker wants to postMessage() to itself to implement setImmediate() emulation.
347-
} else {
348-
Module['printErr']("worker sent an unknown command " + d.cmd);
349-
}
350-
};
351-
352-
worker.onerror = function(e) {
353-
Module['printErr']('pthread sent an error! ' + e.filename + ':' + e.lineno + ': ' + e.message);
354-
};
360+
worker.onerror = function(e) {
361+
Module['printErr']('pthread sent an error! ' + e.filename + ':' + e.lineno + ': ' + e.message);
362+
};
363+
}(worker));
355364

356365
// Allocate tempDoublePtr for the worker. This is done here on the worker's behalf, since we may need to do this statically
357366
// if the runtime has not been loaded yet, etc. - so we just use getMemory, which is main-thread only.
@@ -477,22 +486,29 @@ var LibraryPThread = {
477486
#endif
478487

479488
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,
489+
var msg = {
490+
cmd: 'run',
491+
start_routine: threadParams.startRoutine,
492+
arg: threadParams.arg,
493+
threadInfoStruct: threadParams.pthread_ptr,
494+
selfThreadId: threadParams.pthread_ptr, // TODO: Remove this since thread ID is now the same as the thread address.
495+
parentThreadId: threadParams.parent_pthread_ptr,
496+
stackBase: threadParams.stackBase,
497+
stackSize: threadParams.stackSize,
491498
#if OFFSCREENCANVAS_SUPPORT
492-
moduleCanvasId: threadParams.moduleCanvasId,
493-
offscreenCanvases: threadParams.offscreenCanvases,
499+
moduleCanvasId: threadParams.moduleCanvasId,
500+
offscreenCanvases: threadParams.offscreenCanvases,
494501
#endif
495-
}, threadParams.transferList);
502+
};
503+
worker.runPthread = function() {
504+
// Ask the worker to start executing its pthread entry point function.
505+
msg.time = performance.now();
506+
worker.postMessage(msg, threadParams.transferList);
507+
};
508+
if (worker.loaded) {
509+
worker.runPthread();
510+
delete worker.runPthread;
511+
}
496512
},
497513

498514
#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+
}

0 commit comments

Comments
 (0)