diff options
Diffstat (limited to 'rts/js/thread.js')
-rw-r--r-- | rts/js/thread.js | 1432 |
1 files changed, 1432 insertions, 0 deletions
diff --git a/rts/js/thread.js b/rts/js/thread.js new file mode 100644 index 0000000000..b284b3209d --- /dev/null +++ b/rts/js/thread.js @@ -0,0 +1,1432 @@ +//#OPTIONS: CPP + +// preemptive threading support + +// run gc when this much time has passed (ms) +#ifndef GHCJS_GC_INTERVAL +#define GHCJS_GC_INTERVAL 1000 +#endif + +// preempt threads after the scheduling quantum (ms) +#ifndef GHCJS_SCHED_QUANTUM +#define GHCJS_SCHED_QUANTUM 25 +#endif + +// check sched quantum after 10*GHCJS_SCHED_CHECK calls +#ifndef GHCJS_SCHED_CHECK +#define GHCJS_SCHED_CHECK 1000 +#endif + +// yield to js after running haskell for GHCJS_BUSY_YIELD ms +#ifndef GHCJS_BUSY_YIELD +#define GHCJS_BUSY_YIELD 500 +#endif + +// Watch for insertion of null or undefined in the stack +//#define GHCJS_DEBUG_STACK + +#ifdef GHCJS_TRACE_SCHEDULER +function h$logSched() { if(arguments.length == 1) { + if(h$currentThread != null) { + h$log((Date.now()/1000) + " sched: " + h$threadString(h$currentThread) + + "[" + h$currentThread.mask + "," + + (h$currentThread.interruptible?1:0) + "," + + h$currentThread.excep.length + + "] -> " + arguments[0]); + } else { + h$log("sched: " + h$threadString(h$currentThread) + " -> " + arguments[0]); + } + } else { + h$log.apply(log,arguments); + } + } +#define TRACE_SCHEDULER(args...) h$logSched(args) +#else +#define TRACE_SCHEDULER(args...) +#endif + +#ifdef GHCJS_TRACE_CALLS +// print function to be called from trampoline and first few registers +function h$logCall(c) { + var f = c; + if(c && c.n) { + f = c.n; + } else { + f = c.toString().substring(0,20); // h$collectProps(c); + } + h$log(h$threadString(h$currentThread) + ":" + h$sp + " calling: " + f + " " + JSON.stringify([h$printReg(h$r1), h$printReg(h$r2), h$printReg(h$r3), h$printReg(h$r4), h$printReg(h$r5)])); + h$checkStack(c); +} +#endif + +var h$threadIdN = 0; + +// all threads except h$currentThread +// that are not finished/died can be found here +var h$threads = new h$Queue(); +var h$blocked = new h$Set(); + +/** @constructor */ +function h$Thread() { + this.tid = ++h$threadIdN; + this.status = THREAD_RUNNING; + this.stack = [h$done, 0, h$baseZCGHCziConcziSynczireportError, h$catch_e]; +#ifdef GHCJS_DEBUG_STACK + this.stack = new Proxy(this.stack, { + set(obj,prop,value) { + if (value === undefined || value === null) { + throw new Error("setting stack offset " + prop + " to " + value); + } + else { + return Reflect.set(...arguments); + } + } + }); +#endif + this.sp = 3; + this.mask = 0; // async exceptions masked (0 unmasked, 1: uninterruptible, 2: interruptible) + this.interruptible = false; // currently in an interruptible operation + this.excep = []; // async exceptions waiting for unmask of this thread + this.delayed = false; // waiting for threadDelay + this.blockedOn = null; // object on which thread is blocked + this.retryInterrupted = null; // how to retry blocking operation when interrupted + this.transaction = null; // for STM + this.noPreemption = false; + this.isSynchronous = false; + this.continueAsync = false; + this.m = 0; // gc mark + this.result = null; // result value (used for GHCJS.Foreign.Callback) + this.resultIsException = false; +#ifdef GHCJS_PROF + this.ccs = h$CCS_SYSTEM; // cost-centre stack +#endif + this._key = this.tid; // for storing in h$Set / h$Map +#ifdef GHCJS_DEBUG_ALLOC + h$debugAlloc_notifyAlloc(this); +#endif +} + +function h$rts_getThreadId(t) { + return t.tid; +} + +function h$cmp_thread(t1,t2) { + if(t1.tid < t2.tid) return -1; + if(t1.tid > t2.tid) return 1; + return 0; +} + +// description of the thread, if unlabeled then just the thread id +function h$threadString(t) { + if(t === null) { + return "<no thread>"; + } else if(t.label) { + var str = h$decodeUtf8z(t.label[0], t.label[1]); + return str + " (" + t.tid + ")"; + } else { + return (""+t.tid); + } +} + +function h$fork(a, inherit) { + h$r1 = h$forkThread(a, inherit); + return h$yield(); +} + +function h$forkThread(a, inherit) { + var t = new h$Thread(); + TRACE_SCHEDULER("sched: forking: " + h$threadString(t)) + if(inherit && h$currentThread) { + t.mask = h$currentThread.mask; + } +#ifdef GHCJS_PROF + t.ccs = h$CCS_MAIN; +#endif + // TRACE_SCHEDULER("sched: action forked: " + a.f.n) + t.stack[4] = h$ap_1_0; + t.stack[5] = a; + t.stack[6] = h$return; + t.sp = 6; + h$wakeupThread(t); + return t; +} + +function h$threadStatus(t) { + // status, capability, locked + RETURN_UBX_TUP3(t.status, 1, 0); +} + +function h$waitRead(fd) { + h$fds[fd].waitRead.push(h$currentThread); + h$currentThread.interruptible = true; + return h$blockThread(h$currentThread,fd,[h$waitRead,fd]); +} + +function h$waitWrite(fd) { + h$fds[fd].waitWrite.push(h$currentThread); + h$currentThread.interruptible = true; + return h$blockThread(h$currentThread,fd,[h$waitWrite,fd]); +} + +// threadDelay support: +var h$delayed = new h$HeapSet(); +function h$wakeupDelayed(now) { + while(h$delayed.size() > 0 && h$delayed.peekPrio() < now) { + var t = h$delayed.pop(); + TRACE_SCHEDULER("delay timeout expired: " + t.tid) + // might have been woken up early, don't wake up again in that case + if(t.delayed) { + t.delayed = false; + h$wakeupThread(t); + } + } +} + +function h$delayThread(time) { + var ms = time/1000; // we have no microseconds in JS + return h$delayThreadMs(ms); +} + +function h$sleep(secs) { + return h$delayThreadMs(secs*1000); +} + +function h$delayThreadMs(ms) { + var now = Date.now(); + TRACE_SCHEDULER("delaying " + h$threadString(h$currentThread) + " " + ms + "ms (" + (now+ms) + ")") + h$delayed.add(now+ms, h$currentThread); + h$currentThread.delayed = true; + h$currentThread.interruptible = true; + return h$blockThread(h$currentThread, h$delayed,[h$resumeDelayThread]); +} + +function h$resumeDelayThread() { + h$r1 = false; + return h$rs(); // stack[h$sp]; +} + +function h$yield() { + if(h$currentThread.isSynchronous) { + return h$stack[h$sp]; + } else { + h$sp += 2; + h$stack[h$sp-1] = h$r1; + h$stack[h$sp] = h$return; + h$currentThread.sp = h$sp; + return h$reschedule; + } +} + +// raise the async exception in the thread if not masked +function h$killThread(t, ex) { + TRACE_SCHEDULER("killThread: " + h$threadString(t)) + if(t === h$currentThread) { + // if target is self, then throw even if masked + h$sp += 2; + h$stack[h$sp-1] = h$r1; + h$stack[h$sp] = h$return; + return h$throw(ex,true); + } else { + TRACE_SCHEDULER("killThread mask: " + t.mask) + if(t.mask === 0 || (t.mask === 2 && t.interruptible)) { + if(t.stack) { // finished threads don't have a stack anymore + h$forceWakeupThread(t); + t.sp += 2; + t.stack[t.sp-1] = ex; + t.stack[t.sp] = h$raiseAsync_frame; + } + return h$stack ? h$stack[h$sp] : null; + } else { + t.excep.push([h$currentThread,ex]); + if(h$currentThread) { + h$currentThread.interruptible = true; + h$sp += 2; + h$stack[h$sp-1] = h$r1; + h$stack[h$sp] = h$return; + return h$blockThread(h$currentThread,t,null); + } else { + return null; + } + } + } +} + +function h$maskStatus() { + TRACE_SCHEDULER("mask status: " + h$currentThread.mask) + return h$currentThread.mask; +} + +function h$maskAsync(a) { + TRACE_SCHEDULER("mask: thread " + h$threadString(h$currentThread)) + if(h$currentThread.mask !== 2) { + if(h$currentThread.mask === 0 && h$stack[h$sp] !== h$maskFrame && h$stack[h$sp] !== h$maskUnintFrame) { + h$stack[++h$sp] = h$unmaskFrame; + } + if(h$currentThread.mask === 1) { + h$stack[++h$sp] = h$maskUnintFrame; + } + h$currentThread.mask = 2; + } + h$r1 = a; + return h$ap_1_0_fast(); +} + +function h$maskUnintAsync(a) { + TRACE_SCHEDULER("mask unint: thread " + h$threadString(h$currentThread)) + if(h$currentThread.mask !== 1) { + if(h$currentThread.mask === 2) { + h$stack[++h$sp] = h$maskFrame; + } else { + h$stack[++h$sp] = h$unmaskFrame; + } + h$currentThread.mask = 1; + } + h$r1 = a; + return h$ap_1_0_fast(); +} + +function h$unmaskAsync(a) { + TRACE_SCHEDULER("unmask: " + h$threadString(h$currentThread)) + if(h$currentThread.excep.length > 0) { + h$currentThread.mask = 0; + h$sp += 3; + h$stack[h$sp-2] = h$ap_1_0; + h$stack[h$sp-1] = a; + h$stack[h$sp] = h$return; + return h$reschedule; + } + if(h$currentThread.mask !== 0) { + if(h$stack[h$sp] !== h$unmaskFrame) { + if(h$currentThread.mask === 2) { + h$stack[++h$sp] = h$maskFrame; + } else { + h$stack[++h$sp] = h$maskUnintFrame; + } + } + h$currentThread.mask = 0; + } + h$r1 = a; + return h$ap_1_0_fast(); +} + +function h$pendingAsync() { + var t = h$currentThread; + return (t.excep.length > 0 && (t.mask === 0 || (t.mask === 2 && t.interruptible))); +} + +// post the first of the queued async exceptions to +// this thread, restore frame is in thread if alreadySuspended + +function h$postAsync(alreadySuspended,next) { + var t = h$currentThread; + var v = t.excep.shift(); + TRACE_SCHEDULER("posting async to " + h$threadString(t) + " mask status: " + t.mask + " remaining exceptions: " + t.excep.length) + var tposter = v[0]; // posting thread, blocked + var ex = v[1]; // the exception + if(v !== null && tposter !== null) { + h$wakeupThread(tposter); + } + if(!alreadySuspended) { + h$suspendCurrentThread(next); + } + h$sp += 2; + h$stack[h$sp-1] = ex; + h$stack[h$sp] = h$raiseAsync_frame; + t.sp = h$sp; +} + +// wakeup thread, thread has already been removed +// from any queues it was blocked on +function h$wakeupThread(t) { + TRACE_SCHEDULER("sched: waking up: " + h$threadString(t)) + if(t.status === THREAD_BLOCKED) { + t.blockedOn = null; + t.status = THREAD_RUNNING; + h$blocked.remove(t); + } + t.interruptible = false; + t.retryInterrupted = null; + h$threads.enqueue(t); + h$startMainLoop(); +} + +// force wakeup, remove this thread from any +// queue it's blocked on +function h$forceWakeupThread(t) { + TRACE_SCHEDULER("forcing wakeup of: " + h$threadString(t)) + if(t.status === THREAD_BLOCKED) { + h$removeThreadBlock(t); + h$wakeupThread(t); + } +} + +function h$removeThreadBlock(t) { + var i; + if(t.status === THREAD_BLOCKED) { + var o = t.blockedOn; + if(o === null || o === undefined) { + throw ("h$removeThreadBlock: blocked on null or undefined: " + h$threadString(t)); + } else if(o === h$delayed) { + // thread delayed + h$delayed.remove(t); + t.delayed = false; + } else if(o instanceof h$MVar) { + TRACE_SCHEDULER("blocked on MVar") + TRACE_SCHEDULER("MVar before: " + o.readers.length() + " " + o.writers.length() + " " + o.waiters.length) + // fixme this is rather inefficient + var r, rq = new h$Queue(); + while((r = o.readers.dequeue()) !== null) { + if(r !== t) rq.enqueue(r); + } + var w, wq = new h$Queue(); + while ((w = o.writers.dequeue()) !== null) { + if(w[0] !== t) wq.enqueue(w); + } + o.readers = rq; + o.writers = wq; + if(o.waiters) { + var wa = [], wat; + for(i=0;i<o.waiters.length;i++) { + wat = o.waiters[i]; + if(wat !== t) wa.push(wat); + } + o.waiters = wa; + } + TRACE_SCHEDULER("MVar after: " + o.readers.length() + " " + o.writers.length() + " " + o.waiters.length) +/* } else if(o instanceof h$Fd) { + TRACE_SCHEDULER("blocked on fd") + h$removeFromArray(o.waitRead,t); + h$removeFromArray(o.waitWrite,t); */ + } else if(o instanceof h$Thread) { + TRACE_SCHEDULER("blocked on async exception") + // set thread (first in pair) to null, exception will still be delivered + // but this thread not woken up again + // fixme: are these the correct semantics? + for(i=0;i<o.excep.length;i++) { + if(o.excep[i][0] === t) { + o.excep[i][0] = null; + break; + } + } + } else if (o instanceof h$TVarsWaiting) { + h$stmRemoveBlockedThread(o, t) + } else if(IS_BLACKHOLE(o)) { + TRACE_SCHEDULER("blocked on blackhole") + h$removeFromArray(BLACKHOLE_QUEUE(o),t); + } else { + throw ("h$removeThreadBlock: blocked on unknown object: " + h$collectProps(o)); + } + if(t.retryInterrupted) { + t.sp+=2; + t.stack[t.sp-1] = t.retryInterrupted; + t.stack[t.sp] = h$retryInterrupted; + } + } +} + +function h$removeFromArray(a,o) { + var i; + while((i = a.indexOf(o)) !== -1) { + a.splice(i,1); + } +} + +function h$finishThread(t) { + TRACE_SCHEDULER("sched: finishing: " + h$threadString(t)) + t.status = THREAD_FINISHED; + h$blocked.remove(t); + t.stack = null; + t.mask = 0; + for(var i=0;i<t.excep.length;i++) { + var v = t.excep[i]; + var tposter = v[0]; + if(v !== null && tposter !== null) { + h$wakeupThread(tposter); + } + } + t.excep = []; +} + +function h$blockThread(t,o,resume) { + TRACE_SCHEDULER("sched: blocking: " + h$threadString(t)) + if(t !== h$currentThread) { + throw "h$blockThread: blocked thread is not the current thread"; + } + if(o === undefined || o === null) { + throw ("h$blockThread, no block object: " + h$threadString(t)); + } + t.status = THREAD_BLOCKED; + t.blockedOn = o; + t.retryInterrupted = resume; + t.sp = h$sp; + h$blocked.add(t); + return h$reschedule; +} + +// the main scheduler, called from h$mainLoop +// returns null if nothing to do, otherwise +// the next function to run +var h$lastGc = Date.now(); +var h$gcInterval = GHCJS_GC_INTERVAL; // ms +function h$scheduler(next) { + TRACE_SCHEDULER("sched: scheduler: " + h$sp) + // if we have a running synchronous thread, the only thing we can do is continue + if(h$currentThread && + h$currentThread.isSynchronous && + h$currentThread.status === THREAD_RUNNING) { + return next; + } + var now = Date.now(); + h$wakeupDelayed(now); + // find the next runnable thread in the run queue + // remove non-runnable threads + if(h$currentThread && h$pendingAsync()) { + TRACE_SCHEDULER("sched: received async exception, continuing thread") + if(h$currentThread.status !== THREAD_RUNNING) { + h$forceWakeupThread(h$currentThread); + h$currentThread.status = THREAD_RUNNING; + } + h$postAsync(next === h$reschedule, next); + return h$stack[h$sp]; + } + var t; + while(t = h$threads.dequeue()) { + if(t.status === THREAD_RUNNING) { break; } + } + // if no other runnable threads, just continue current (if runnable) + if(t === null) { + TRACE_SCHEDULER("sched: no other runnable threads") + if(h$currentThread && h$currentThread.status === THREAD_RUNNING) { + // do gc after a while + if(now - h$lastGc > h$gcInterval) { + // save active data for the thread on its stack + if(next !== h$reschedule && next !== null) { + h$suspendCurrentThread(next); + next = h$stack[h$sp]; + } + var ct = h$currentThread; + h$currentThread = null; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + h$gc(ct); + h$currentThread = ct; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + // gc might replace the stack of a thread, so reload it + h$stack = h$currentThread.stack; + h$sp = h$currentThread.sp + } + TRACE_SCHEDULER("sched: continuing: " + h$threadString(h$currentThread)) + return (next===h$reschedule || next === null)?h$stack[h$sp]:next; // just continue + } else { + TRACE_SCHEDULER("sched: pausing") + h$currentThread = null; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + // We could set a timer here so we do a gc even if Haskell pauses for a long time. + // However, currently this isn't necessary because h$mainLoop always sets a timer + // before it pauses. + if(now - h$lastGc > h$gcInterval) + h$gc(null); + return null; // pause the haskell runner + } + } else { // runnable thread in t, switch to it + TRACE_SCHEDULER("sched: switching to: " + h$threadString(t)) + if(h$currentThread !== null) { + if(h$currentThread.status === THREAD_RUNNING) { + h$threads.enqueue(h$currentThread); + } + // if h$reschedule called, thread takes care of suspend + if(next !== h$reschedule && next !== null) { + TRACE_SCHEDULER("sched: suspending: " + h$threadString(h$currentThread)) + // suspend thread: push h$restoreThread stack frame + h$suspendCurrentThread(next); + } else { + TRACE_SCHEDULER("sched: no suspend needed, reschedule called from: " + h$threadString(h$currentThread)) + h$currentThread.sp = h$sp; + } + if(h$pendingAsync()) h$postAsync(true, next); + } else { + TRACE_SCHEDULER("sched: no suspend needed, no running thread") + } + // gc if needed + if(now - h$lastGc > h$gcInterval) { + h$currentThread = null; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + h$gc(t); + } + // schedule new one + h$currentThread = t; + h$stack = t.stack; + h$sp = t.sp; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + TRACE_SCHEDULER("sched: scheduling " + h$threadString(t) + " sp: " + h$sp) + // TRACE_SCHEDULER("sp thing: " + h$stack[h$sp].n) + // h$dumpStackTop(h$stack,0,h$sp); + return h$stack[h$sp]; + } +} + +function h$scheduleMainLoop() { + TRACE_SCHEDULER("scheduling next main loop wakeup") + if(h$mainLoopImmediate) return; + h$clearScheduleMainLoop(); + if(h$delayed.size() === 0) { +#ifndef GHCJS_BROWSER + if(typeof setTimeout !== 'undefined') { +#endif + TRACE_SCHEDULER("scheduling main loop wakeup in " + h$gcInterval + "ms") + h$mainLoopTimeout = setTimeout(h$mainLoop, h$gcInterval); +#ifndef GHCJS_BROWSER + } +#endif + return; + } + var now = Date.now(); + var delay = Math.min(Math.max(h$delayed.peekPrio()-now, 0), h$gcInterval); +#ifndef GHCJS_BROWSER + if(typeof setTimeout !== 'undefined') { +#endif + if(delay >= 1) { + TRACE_SCHEDULER("scheduling main loop wakeup in " + delay + "ms") + // node.js 0.10.30 has trouble with non-integral delays + h$mainLoopTimeout = setTimeout(h$mainLoop, Math.round(delay)); + } else { + h$mainLoopImmediate = setImmediate(h$mainLoop); + } +#ifndef GHCJS_BROWSER + } +#endif +} + +var h$animationFrameMainLoop = false; +#ifdef GHCJS_ANIMATIONFRAME_MAINLOOP +h$animationFrameMainLoop = true; +#endif + +function h$clearScheduleMainLoop() { + if(h$mainLoopTimeout) { + clearTimeout(h$mainLoopTimeout); + h$mainLoopTimeout = null; + } + if(h$mainLoopImmediate) { + clearImmediate(h$mainLoopImmediate); + h$mainLoopImmediate = null; + } + if(h$mainLoopFrame) { + cancelAnimationFrame(h$mainLoopFrame); + h$mainLoopFrame = null; + } +} + +function h$startMainLoop() { + TRACE_SCHEDULER("start main loop: " + h$running) + if(h$running) return; +#ifndef GHCJS_BROWSER + if(typeof setTimeout !== 'undefined') { +#endif + if(!h$mainLoopImmediate) { + h$clearScheduleMainLoop(); + h$mainLoopImmediate = setImmediate(h$mainLoop); + } +#ifndef GHCJS_BROWSER + } else { + while(true) { + // the try/catch block appears to prevent a crash with + // Safari on iOS 10, even though this path is never taken + // in a browser. + try { + h$mainLoop(); + } catch(e) { + throw e; + } + } + } +#endif +} + +#if defined(GHCJS_TRACE_CALLS) || defined(GHCJS_TRACE_STACK) +var h$traceCallsTicks = 0; +#ifndef GHCJS_TRACE_CALLS_DELAY +#define GHCJS_TRACE_CALLS_DELAY 0 +#endif +var h$traceCallsDelay = GHCJS_TRACE_CALLS_DELAY; +#endif + +var h$busyYield = GHCJS_BUSY_YIELD; +var h$schedQuantum = GHCJS_SCHED_QUANTUM; + +var h$mainLoopImmediate = null; // immediate id if main loop has been scheduled immediately +var h$mainLoopTimeout = null; // timeout id if main loop has been scheduled with a timeout +var h$mainLoopFrame = null; // timeout id if main loop has been scheduled with an animation frame +var h$running = false; +var h$nextThread = null; +function h$mainLoop() { +#ifdef GHCJS_PROF + h$runProf(h$actualMainLoop); +} +function h$actualMainLoop() { +#endif + if(h$running) return; + h$clearScheduleMainLoop(); + if(h$currentThread) { + h$scheduleMainLoop(); + return; + } + h$running = true; + h$runInitStatic(); + h$currentThread = h$nextThread; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + if(h$nextThread !== null) { + h$stack = h$currentThread.stack; + h$sp = h$currentThread.sp; + } + var c = null; + var start = Date.now(); + do { + c = h$scheduler(c); + if(c === null) { // no running threads + h$nextThread = null; + h$running = false; + h$currentThread = null; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + h$scheduleMainLoop(); + return; + } + // yield to js after h$busyYield (default value GHCJS_BUSY_YIELD) + if(!h$currentThread.isSynchronous && Date.now() - start > h$busyYield) { + TRACE_SCHEDULER("yielding to js") + if(c !== h$reschedule) h$suspendCurrentThread(c); + h$nextThread = h$currentThread; + h$currentThread = null; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + h$running = false; + if(h$animationFrameMainLoop) { + h$mainLoopFrame = requestAnimationFrame(h$mainLoop); + } else { + h$mainLoopImmediate = setImmediate(h$mainLoop); + } + return; + } +#ifdef GHCJS_NO_CATCH_MAINLOOP + // for debugging purposes only, may leave threads in inconsistent state! + c = h$runThreadSlice(c); +#else + c = h$runThreadSliceCatch(c); +#endif + } while(true); +} + +function h$runThreadSliceCatch(c) { + try { + return h$runThreadSlice(c); + } catch(e) { + // uncaught exception in haskell code, kill thread +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + c = null; + if(h$stack && h$stack[0] === h$doneMain_e) { + h$stack = null; + h$reportMainLoopException(e, true); + h$doneMain_e(); + } else { + h$stack = null; + h$reportMainLoopException(e, false); + } + h$finishThread(h$currentThread); + h$currentThread.status = THREAD_DIED; + h$currentThread = null; + } + return h$reschedule; +} + +/* + run thread h$currentThread for a single time slice + + - c: the next function to call from the trampoline + + returns: + the next function to call in this thread + + preconditions: + h$currentThread is the thread to run + h$stack is the stack of this thread + h$sp is the stack pointer + + any global variables needed to pass arguments have been set + the caller has to update the thread state object + */ +function h$runThreadSlice(c) { + var count, scheduled = Date.now(); + while(c !== h$reschedule && + (h$currentThread.noPreemption || h$currentThread.isSynchronous || + (Date.now() - scheduled < h$schedQuantum))) { + count = 0; + while(c !== h$reschedule && ++count < GHCJS_SCHED_CHECK) { +#if defined(GHCJS_TRACE_CALLS) || defined(GHCJS_TRACE_STACK) + h$traceCallsTicks++; + if(h$traceCallsTicks % 1000000 === 0) h$log("ticks: " + h$traceCallsTicks); +#endif +#ifdef GHCJS_TRACE_CALLS + if(h$traceCallsDelay >= 0 && h$traceCallsTicks >= h$traceCallsDelay) h$logCall(c); +#endif +#ifdef GHCJS_TRACE_STACK + if(h$traceCallsDelay >= 0 && h$traceCallsTicks >= h$traceCallsDelay) h$logStack(c); +#endif + c = c(); +#if !defined(GHCJS_TRACE_CALLS) && !defined(GHCJS_TRACE_STACK) && !defined(GHCJS_SCHED_DEBUG) + c = c(); + c = c(); + c = c(); + c = c(); + c = c(); + c = c(); + c = c(); + c = c(); + c = c(); +#endif + } + if(c === h$reschedule && + (h$currentThread.noPreemption || h$currentThread.isSynchronous) && + h$currentThread.status === THREAD_BLOCKED) { + c = h$handleBlockedSyncThread(c); + } + } + return c; +} + +function h$reportMainLoopException(e, isMainThread) { + if(e instanceof h$ThreadAbortedError) return; + var main = isMainThread ? " main" : ""; + h$log("uncaught exception in Haskell" + main + " thread: " + e.toString()); + if(e.stack) h$log(e.stack); + if (h$isNode()) { + process.exit(1); + } +} + + +function h$handleBlockedSyncThread(c) { + TRACE_SCHEDULER("handling blocked sync thread") + /* + if we have a blocked synchronous/non-preemptible thread, + and it's blocked on a black hole, first try to clear + it. + */ + var bo = h$currentThread.blockedOn; + if(h$currentThread.status === THREAD_BLOCKED && + IS_BLACKHOLE(bo) && + h$runBlackholeThreadSync(bo)) { + TRACE_SCHEDULER("blackhole succesfully removed") + c = h$stack[h$sp]; + } + /* + if still blocked, then either fall back to async, + or throw a WouldBlock exception + */ + if(h$currentThread.isSynchronous && h$currentThread.status === THREAD_BLOCKED) { + if(h$currentThread.continueAsync) { + h$currentThread.isSynchronous = false; + h$currentThread.continueAsync = false; + } else if(h$currentThread.isSynchronous) { + TRACE_SCHEDULER("blocking synchronous thread: exception") + h$sp += 2; + h$currentThread.sp = h$sp; + h$stack[h$sp-1] = h$baseZCGHCziJSziPrimziInternalziwouldBlock; + h$stack[h$sp] = h$raiseAsync_frame; + h$forceWakeupThread(h$currentThread); + c = h$raiseAsync_frame; + } // otherwise a non-preemptible thread, keep it in the same state + } + return c; +} + +// run the supplied IO action in a new thread +// returns immediately, thread is started in background +function h$run(a) { + TRACE_SCHEDULER("sched: starting thread") + var t = h$forkThread(a, false); + h$startMainLoop(); + return t; +} + +/** @constructor */ +function h$WouldBlock() { + +} + +h$WouldBlock.prototype.toString = function() { + return "Haskell Operation would block"; +} + +/** @constructor */ +function h$HaskellException(msg) { + this._msg = msg; +} + +h$HaskellException.prototype.toString = function() { + return this._msg; +} + +function h$setCurrentThreadResultWouldBlock() { + h$currentThread.result = new h$WouldBlock(); + h$currentThread.resultIsException = true; +} + +function h$setCurrentThreadResultJSException(e) { + h$currentThread.result = e; + h$currentThread.resultIsException = true; +} + +function h$setCurrentThreadResultHaskellException(msg) { + h$currentThread.result = new h$HaskellException(msg); + h$currentThread.resultIsException = true; +} + +function h$setCurrentThreadResultValue(v) { + h$currentThread.result = v; + h$currentThread.resultIsException = false; +} + +/* + run a Haskell (IO JSVal) action synchronously, returning + the result. Uncaught Haskell exceptions are thrown as a + h$HaskellException. If the action could not finish due to + blocking, a h$WouldBlock exception is thrown instead. + + - a: the (IO JSVal) action + - cont: continue async if blocked + (the call to h$runSyncReturn would still throw h$WouldBlock, + since there would be no return value) + + returns: the result of the IO action + */ +function h$runSyncReturn(a, cont) { + var t = new h$Thread(); + TRACE_SCHEDULER("h$runSyncReturn created thread: " + h$threadString(t)) + var aa = MK_AP1(h$baseZCGHCziJSziPrimziInternalzisetCurrentThreadResultValue, a); + h$runSyncAction(t, aa, cont); + if(t.status === THREAD_FINISHED) { + if(t.resultIsException) { + throw t.result; + } else { + return t.result; + } + } else if(t.status === THREAD_BLOCKED) { + throw new h$WouldBlock(); + } else { + throw new Error("h$runSyncReturn: Unexpected thread status: " + t.status); + } +} + +/* + run a Haskell IO action synchronously, ignoring the result + or any exception in the Haskell code + + - a: the IO action + - cont: continue async if blocked + + returns: true if the action ran to completion, false otherwise + + throws: any uncaught Haskell or JS exception except WouldBlock + */ +function h$runSync(a, cont) { + var t = new h$Thread(); + TRACE_SCHEDULER("h$runSync created thread: " + h$threadString(t)) + h$runSyncAction(t, a, cont); + if(t.resultIsException) { + if(t.result instanceof h$WouldBlock) { + return false; + } else { + throw t.result; + } + } + return t.status === THREAD_FINISHED; +} + +function h$runSyncAction(t, a, cont) { + h$runInitStatic(); + var c = h$return; + t.stack[2] = h$baseZCGHCziJSziPrimziInternalzisetCurrentThreadResultException; + t.stack[4] = h$ap_1_0; + t.stack[5] = a; + t.stack[6] = h$return; + t.sp = 6; + t.status = THREAD_RUNNING; +#ifdef GHCJS_PROF + // fixme this looks wrong + // t.ccs = h$currentThread.ccs; // TODO: not sure about this +#endif + t.isSynchronous = true; + t.continueAsync = cont; + var ct = h$currentThread; + var csp = h$sp; + var cr1 = h$r1; // do we need to save more than this? + var caught = false, excep = null; + h$currentThread = t; + h$stack = t.stack; + h$sp = t.sp; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + try { + c = h$runThreadSlice(c); + if(c !== h$reschedule) { + throw new Error("h$runSyncAction: h$reschedule expected"); + } + } catch(e) { + h$finishThread(h$currentThread); + h$currentThread.status = THREAD_DIED; + caught = true; + excep = e; + } + if(ct !== null) { + h$currentThread = ct; + h$stack = ct.stack; + h$sp = csp; + h$r1 = cr1; + } else { + h$currentThread = null; + h$stack = null; + } +#ifdef GHCJS_PROF + // fixme? + h$reportCurrentCcs(); +#endif + if(t.status !== THREAD_FINISHED && !cont) { + h$removeThreadBlock(t); + h$finishThread(t); + } + if(caught) throw excep; +} + +// run other threads synchronously until the blackhole is 'freed' +// returns true for success, false for failure, a thread blocks +function h$runBlackholeThreadSync(bh) { + TRACE_SCHEDULER("trying to remove black hole") + var ct = h$currentThread; + var sp = h$sp; + var success = false; + var bhs = []; + var currentBh = bh; + // we don't handle async exceptions here, + // don't run threads with pending exceptions + if(BLACKHOLE_TID(bh).excep.length > 0) { + TRACE_SCHEDULER("aborting due to queued async exceptions") + return false; + } + h$currentThread = BLACKHOLE_TID(bh); + h$stack = h$currentThread.stack; + h$sp = h$currentThread.sp; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + var c = (h$currentThread.status === THREAD_RUNNING)?h$stack[h$sp]:h$reschedule; + TRACE_SCHEDULER("switched thread status running: " + (h$currentThread.status === THREAD_RUNNING)) + try { + while(true) { + while(c !== h$reschedule && IS_BLACKHOLE(currentBh)) { + c = c(); + c = c(); + c = c(); + c = c(); + c = c(); + } + if(c === h$reschedule) { + // perhaps new blackhole, then continue with that thread, + // otherwise fail + if(IS_BLACKHOLE(h$currentThread.blockedOn)) { + TRACE_SCHEDULER("following another black hole") + bhs.push(currentBh); + currentBh = h$currentThread.blockedOn; + h$currentThread = BLACKHOLE_TID(h$currentThread.blockedOn); + if(h$currentThread.excep.length > 0) { + break; + } + h$stack = h$currentThread.stack; + h$sp = h$currentThread.sp; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + c = (h$currentThread.status === THREAD_RUNNING)?h$stack[h$sp]:h$reschedule; + } else { + TRACE_SCHEDULER("thread blocked on something that's not a black hole, failing") + break; + } + } else { // blackhole updated: suspend thread and pick up the old one + TRACE_SCHEDULER("blackhole updated, switching back (" + h$sp + ")") + TRACE_SCHEDULER("next: " + c.toString()) + h$suspendCurrentThread(c); + if(bhs.length > 0) { + TRACE_SCHEDULER("to next black hole") + currentBh = bhs.pop(); + h$currentThread = BLACKHOLE_TID(currentBh); + h$stack = h$currentThread.stack; + h$sp = h$currentThread.sp; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + } else { + TRACE_SCHEDULER("last blackhole removed, success!") + success = true; + break; + } + } + } + } catch(e) { } + // switch back to original thread + h$sp = sp; + h$stack = ct.stack; + h$currentThread = ct; +#ifdef GHCJS_PROF + h$reportCurrentCcs(); +#endif + return success; +} + +function h$syncThreadState(tid) { + return (tid.isSynchronous ? 1 : 0) | + ((tid.continueAsync || !tid.isSynchronous) ? 2 : 0) | + ((tid.noPreemption || tid.isSynchronous) ? 4 : 0); +} + +// run the supplied IO action in a main thread +// (program exits when this thread finishes) +function h$main(a) { + var t = new h$Thread(); +#ifdef GHCJS_PROF + t.ccs = a.cc; +#endif + //TRACE_SCHEDULER("sched: starting main thread") + t.stack[0] = h$doneMain_e; +#ifndef GHCJS_BROWSER + if(!h$isBrowser() && !h$isGHCJSi()) { + t.stack[2] = h$baseZCGHCziTopHandlerzitopHandler; + } +#endif + t.stack[4] = h$ap_1_0; + t.stack[5] = h$flushStdout; + t.stack[6] = h$return; + t.stack[7] = h$ap_1_0; + t.stack[8] = a; + t.stack[9] = h$return; + t.sp = 9; + t.label = [h$encodeUtf8("main"), 0]; + h$wakeupThread(t); + h$startMainLoop(); + return t; +} + +function h$doneMain() { +#ifndef GHCJS_BROWSER + if(h$isGHCJSi()) { + if(h$currentThread.stack) { + global.h$GHCJSi.done(h$currentThread); + } + } else { +#endif + h$exitProcess(0); +#ifndef GHCJS_BROWSER + } +#endif + h$finishThread(h$currentThread); + return h$reschedule; +} + +/** @constructor */ +function h$ThreadAbortedError(code) { + this.code = code; +} + +h$ThreadAbortedError.prototype.toString = function() { + return "Thread aborted, exit code: " + this.code; +} + +function h$exitProcess(code) { +#ifndef GHCJS_BROWSER + if(h$isNode()) { + process.exit(code); + } else if(h$isJvm()) { + java.lang.System.exit(code); + } else if(h$isJsShell()) { + quit(code); + } else if(h$isJsCore()) { + if(h$base_stdoutLeftover.val !== null) print(h$base_stdoutLeftover.val); + if(h$base_stderrLeftover.val !== null) debug(h$base_stderrLeftover.val); + // jsc does not support returning a nonzero value, print it instead + if(code !== 0) debug("GHCJS JSC exit status: " + code); + quit(); + } else { +#endif + if(h$currentThread) { + h$finishThread(h$currentThread); + h$stack = null; + throw new h$ThreadAbortedError(code); + } +#ifndef GHCJS_BROWSER + } +#endif +} + +// MVar support +var h$mvarId = 0; +/** @constructor */ +function h$MVar() { + TRACE_SCHEDULER("h$MVar constructor") + this.val = null; + this.readers = new h$Queue(); + this.writers = new h$Queue(); + this.waiters = null; // waiting for a value in the MVar with ReadMVar + this.m = 0; // gc mark + this.id = ++h$mvarId; +#ifdef GHCJS_DEBUG_ALLOC + h$debugAlloc_notifyAlloc(this); +#endif +} + +// set the MVar to empty unless there are writers +function h$notifyMVarEmpty(mv) { + var w = mv.writers.dequeue(); + if(w !== null) { + var thread = w[0]; + var val = w[1]; + TRACE_SCHEDULER("notifyMVarEmpty(" + mv.id + "): writer ready: " + h$threadString(thread)) + mv.val = val; + // thread is null if some JavaScript outside Haskell wrote to the MVar + if(thread !== null) { + h$wakeupThread(thread); + } + } else { + TRACE_SCHEDULER("notifyMVarEmpty(" + mv.id + "): no writers") + mv.val = null; + } + TRACE_SCHEDULER("notifyMVarEmpty(" + mv.id + "): " + mv.val) +} + +// set the MVar to val unless there are readers +function h$notifyMVarFull(mv,val) { + if(mv.waiters && mv.waiters.length > 0) { + for(var i=0;i<mv.waiters.length;i++) { + var w = mv.waiters[i]; + TRACE_SCHEDULER("notifyMVarFull: notifying waiter: " + h$threadString(w)) + w.sp += 2; + w.stack[w.sp-1] = val; + w.stack[w.sp] = h$return; + h$wakeupThread(w); + } + mv.waiters = null; + } + var r = mv.readers.dequeue(); + if(r !== null) { + TRACE_SCHEDULER("notifyMVarFull(" + mv.id + "): reader ready: " + h$threadString(r)) + r.sp += 2; + r.stack[r.sp-1] = val; + r.stack[r.sp] = h$return; + h$wakeupThread(r); + mv.val = null; + } else { + TRACE_SCHEDULER("notifyMVarFull(" + mv.id + "): no readers") + mv.val = val; + } + TRACE_SCHEDULER("notifyMVarFull(" + mv.id + "): " + mv.val) +} + +function h$takeMVar(mv) { + TRACE_SCHEDULER("h$takeMVar(" + mv.id + "): " + mv.val + " " + h$threadString(h$currentThread)) + if(mv.val !== null) { + h$r1 = mv.val; + h$notifyMVarEmpty(mv); + return h$stack[h$sp]; + } else { + mv.readers.enqueue(h$currentThread); + h$currentThread.interruptible = true; + return h$blockThread(h$currentThread,mv,[h$takeMVar,mv]); + } +} + +function h$tryTakeMVar(mv) { + TRACE_SCHEDULER("h$tryTakeMVar(" + mv.id + "): " + mv.val) + if(mv.val === null) { + RETURN_UBX_TUP2(0, null); + } else { + var v = mv.val; + h$notifyMVarEmpty(mv); + RETURN_UBX_TUP2(1, v); + } +} + +function h$readMVar(mv) { + TRACE_SCHEDULER("h$readMVar(" + mv.id + "): " + mv.val) + if(mv.val === null) { + if(mv.waiters) { + mv.waiters.push(h$currentThread); + } else { + mv.waiters = [h$currentThread]; + } + h$currentThread.interruptible = true; + return h$blockThread(h$currentThread,mv,[h$readMVar,mv]); + } else { + h$r1 = mv.val; + return h$stack[h$sp]; + } +} + +function h$putMVar(mv,val) { + TRACE_SCHEDULER("h$putMVar(" + mv.id + "): " + mv.val) + if(mv.val !== null) { + mv.writers.enqueue([h$currentThread,val]); + h$currentThread.interruptible = true; + return h$blockThread(h$currentThread,mv,[h$putMVar,mv,val]); + } else { + h$notifyMVarFull(mv,val); + return h$stack[h$sp]; + } +} + +function h$tryPutMVar(mv,val) { + TRACE_SCHEDULER("h$tryPutMVar(" + mv.id + "): " + mv.val) + if(mv.val !== null) { + return 0; + } else { + h$notifyMVarFull(mv,val); + return 1; + } +} + +// box up a JavaScript value and write it to the MVar synchronously +function h$writeMVarJs1(mv,val) { + var v = MK_DATA1_1(val); + if(mv.val !== null) { + TRACE_SCHEDULER("h$writeMVarJs1: was full") + mv.writers.enqueue([null,v]); + } else { + TRACE_SCHEDULER("h$writeMVarJs1: was empty") + h$notifyMVarFull(mv,v); + } +} + +function h$writeMVarJs2(mv,val1,val2) { + var v = MK_DATA1_2(val1, val2); + if(mv.val !== null) { + TRACE_SCHEDULER("h$writeMVarJs2: was full") + mv.writers.enqueue([null,v]); + } else { + TRACE_SCHEDULER("h$writeMVarJs2: was empty") + h$notifyMVarFull(mv,v); + } +} + +// IORef support +/** @constructor */ +function h$MutVar(v) { + this.val = v; + this.m = 0; +#ifdef GHCJS_DEBUG_ALLOC + h$debugAlloc_notifyAlloc(this); +#endif +} + +function h$atomicModifyMutVar(mv, fun) { + var oldVal = mv.val; + var thunk = MK_AP1(fun, oldVal); + mv.val = thunk; + RETURN_UBX_TUP2(oldVal, thunk); +} + +function h$atomicModifyMutVar2(mv, fun) { + var oldVal = mv.val; + var thunk = MK_AP1(fun, oldVal); + mv.val = MK_SELECT1(thunk); + RETURN_UBX_TUP2(oldVal, thunk); +} + +// Black holes and updates +// caller must save registers on stack +function h$blockOnBlackhole(c) { + TRACE_SCHEDULER("blackhole, blocking: " + h$collectProps(c)) + if(BLACKHOLE_TID(c) === h$currentThread) { + TRACE_SCHEDULER("NonTermination") + return h$throw(h$baseZCControlziExceptionziBasezinonTermination, true); + } + TRACE_SCHEDULER("blackhole, blocking thread: " + h$threadString(h$currentThread)) + if(BLACKHOLE_QUEUE(c) === null) { + SET_BLACKHOLE_QUEUE(c,[h$currentThread]); + } else { + BLACKHOLE_QUEUE(c).push(h$currentThread); + } + return h$blockThread(h$currentThread,c,[h$resumeBlockOnBlackhole,c]); +} + +function h$resumeBlockOnBlackhole(c) { + h$r1 = c; + return h$ap_0_0_fast(); +} + +// async exception happened in a black hole, make a thunk +// to resume the computation +// var h$debugResumableId = 0; +function h$makeResumable(bh,start,end,extra) { + var s = h$stack.slice(start,end+1); + if(extra) { + s = s.concat(extra); + } +// TRACE_SCHEDULER("making resumable " + (h$debugResumableId+1) + ", stack: ") +// h$dumpStackTop(s,0,s.length-1); + MAKE_RESUMABLE(bh, s); +} + +var h$enabled_capabilities = h$newByteArray(4); +h$enabled_capabilities.i3[0] = 1; + +function h$rtsSupportsBoundThreads() { + return 0; +} + +function h$rts_setMainThread(t) { + +} + +// async foreign calls +function h$mkForeignCallback(x) { + return function() { + if(x.mv === null) { // callback called synchronously + x.mv = arguments; + } else { + h$notifyMVarFull(x.mv, MK_DATA1_1(arguments)); + h$mainLoop(); + } + } +} + +// event listeners through MVar +function h$makeMVarListener(mv, stopProp, stopImmProp, preventDefault) { + var f = function(event) { + TRACE_SCHEDULER("MVar listener callback") + h$writeMVarJs1(mv,event); + if(stopProp) { event.stopPropagation(); } + if(stopImmProp) { event.stopImmediatePropagation(); } + if(preventDefault) { event.preventDefault(); } + } + f.root = mv; + return f; +} + +function h$rs() { + return h$stack[h$sp]; +} + +const rts_isThreaded = 0; |