mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1281 lines
37 KiB
C++
1281 lines
37 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
|
* vim: set ts=8 sw=4 et tw=99:
|
|
*
|
|
* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is JavaScript shell workers.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla Corporation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2010
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Jason Orendorff <jorendorff@mozilla.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either of the GNU General Public License Version 2 or later (the "GPL"),
|
|
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
#ifdef JS_THREADSAFE
|
|
|
|
#include <algorithm>
|
|
#include <string.h>
|
|
#include "prthread.h"
|
|
#include "prlock.h"
|
|
#include "prcvar.h"
|
|
#include "jsapi.h"
|
|
#include "jscntxt.h"
|
|
#include "jshashtable.h"
|
|
#include "jsstdint.h"
|
|
#include "jslock.h"
|
|
#include "jsvector.h"
|
|
#include "jsworkers.h"
|
|
|
|
extern size_t gMaxStackSize;
|
|
|
|
/*
|
|
* JavaScript shell workers.
|
|
*
|
|
* == Object lifetime rules ==
|
|
*
|
|
* - The ThreadPool lasts from init() to finish().
|
|
*
|
|
* - The ThreadPool owns the MainQueue and the WorkerQueue. Those live from
|
|
* the time the first Worker is created until finish().
|
|
*
|
|
* - Each JS Worker object has the same lifetime as the corresponding C++
|
|
* Worker object. A Worker is live if (a) the Worker JSObject is still
|
|
* live; (b) the Worker has an incoming event pending or running; (c) it
|
|
* has sent an outgoing event to its parent that is still pending; or (d)
|
|
* it has any live child Workers.
|
|
*
|
|
* - finish() continues to wait for events until all threads are idle.
|
|
*
|
|
* Event objects, however, are basically C++-only. The JS Event objects are
|
|
* just plain old JSObjects. They don't keep anything alive.
|
|
*
|
|
* == Locking scheme ==
|
|
*
|
|
* When mixing mutexes and the JSAPI request model, there are two choices:
|
|
*
|
|
* - Always nest the mutexes in requests. Since threads in requests are not
|
|
* supposed to block, this means the mutexes must be only briefly held.
|
|
*
|
|
* - Never nest the mutexes in requests. Since this allows threads to race
|
|
* with the GC, trace() methods must go through the mutexes just like
|
|
* everyone else.
|
|
*
|
|
* This code uses the latter approach for all locks.
|
|
*
|
|
* In one case, a thread holding a Worker's mutex can acquire the mutex of one
|
|
* of its child Workers. See Worker::terminateSelf. (This can't deadlock because
|
|
* the parent-child relationship is a partial order.)
|
|
*/
|
|
|
|
namespace js {
|
|
namespace workers {
|
|
|
|
template <class T, class AllocPolicy>
|
|
class Queue {
|
|
private:
|
|
typedef Vector<T, 4, AllocPolicy> Vec;
|
|
Vec v1;
|
|
Vec v2;
|
|
Vec *front;
|
|
Vec *back;
|
|
|
|
// Queue is not copyable.
|
|
Queue(const Queue &);
|
|
Queue & operator=(const Queue &);
|
|
|
|
public:
|
|
Queue() : front(&v1), back(&v2) {}
|
|
bool push(T t) { return back->append(t); }
|
|
bool empty() { return front->empty() && back->empty(); }
|
|
|
|
T pop() {
|
|
if (front->empty()) {
|
|
std::reverse(back->begin(), back->end());
|
|
Vec *tmp = front;
|
|
front = back;
|
|
back = tmp;
|
|
}
|
|
T item = front->back();
|
|
front->popBack();
|
|
return item;
|
|
}
|
|
|
|
void clear() {
|
|
v1.clear();
|
|
v2.clear();
|
|
}
|
|
|
|
void trace(JSTracer *trc) {
|
|
for (T *p = v1.begin(); p != v1.end(); p++)
|
|
(*p)->trace(trc);
|
|
for (T *p = v2.begin(); p != v2.end(); p++)
|
|
(*p)->trace(trc);
|
|
}
|
|
};
|
|
|
|
class Event;
|
|
class ThreadPool;
|
|
class Worker;
|
|
|
|
class WorkerParent {
|
|
protected:
|
|
typedef HashSet<Worker *, DefaultHasher<Worker *>, SystemAllocPolicy> ChildSet;
|
|
ChildSet children;
|
|
|
|
bool initWorkerParent() { return children.init(8); }
|
|
|
|
public:
|
|
virtual JSLock *getLock() = 0;
|
|
virtual ThreadPool *getThreadPool() = 0;
|
|
virtual bool post(Event *item) = 0; // false on OOM or queue closed
|
|
virtual void trace(JSTracer *trc) = 0;
|
|
|
|
bool addChild(Worker *w) {
|
|
AutoLock hold(getLock());
|
|
return children.put(w) != NULL;
|
|
}
|
|
|
|
// This must be called only from GC or when all threads are shut down. It
|
|
// does not bother with locking.
|
|
void removeChild(Worker *w) {
|
|
ChildSet::Ptr p = children.lookup(w);
|
|
JS_ASSERT(p);
|
|
children.remove(p);
|
|
}
|
|
|
|
void disposeChildren();
|
|
};
|
|
|
|
template <class T>
|
|
class ThreadSafeQueue
|
|
{
|
|
protected:
|
|
Queue<T, SystemAllocPolicy> queue;
|
|
JSLock *lock;
|
|
PRCondVar *condvar;
|
|
bool closed;
|
|
|
|
private:
|
|
Vector<T, 8, SystemAllocPolicy> busy;
|
|
|
|
protected:
|
|
ThreadSafeQueue() : lock(NULL), condvar(NULL), closed(false) {}
|
|
|
|
~ThreadSafeQueue() {
|
|
if (condvar)
|
|
JS_DESTROY_CONDVAR(condvar);
|
|
if (lock)
|
|
JS_DESTROY_LOCK(lock);
|
|
}
|
|
|
|
// Called by take() with the lock held.
|
|
virtual bool shouldStop() { return closed; }
|
|
|
|
public:
|
|
bool initThreadSafeQueue() {
|
|
JS_ASSERT(!lock);
|
|
JS_ASSERT(!condvar);
|
|
return (lock = JS_NEW_LOCK()) && (condvar = JS_NEW_CONDVAR(lock));
|
|
}
|
|
|
|
bool post(T t) {
|
|
AutoLock hold(lock);
|
|
if (closed)
|
|
return false;
|
|
if (queue.empty())
|
|
JS_NOTIFY_ALL_CONDVAR(condvar);
|
|
return queue.push(t);
|
|
}
|
|
|
|
void close() {
|
|
AutoLock hold(lock);
|
|
closed = true;
|
|
queue.clear();
|
|
JS_NOTIFY_ALL_CONDVAR(condvar);
|
|
}
|
|
|
|
// The caller must hold the lock.
|
|
bool take(T *t) {
|
|
while (queue.empty()) {
|
|
if (shouldStop())
|
|
return false;
|
|
JS_WAIT_CONDVAR(condvar, JS_NO_TIMEOUT);
|
|
}
|
|
*t = queue.pop();
|
|
busy.append(*t);
|
|
return true;
|
|
}
|
|
|
|
// The caller must hold the lock.
|
|
void drop(T item) {
|
|
for (T *p = busy.begin(); p != busy.end(); p++) {
|
|
if (*p == item) {
|
|
*p = busy.back();
|
|
busy.popBack();
|
|
return;
|
|
}
|
|
}
|
|
JS_NOT_REACHED("removeBusy");
|
|
}
|
|
|
|
bool lockedIsIdle() { return busy.empty() && queue.empty(); }
|
|
|
|
bool isIdle() {
|
|
AutoLock hold(lock);
|
|
return lockedIsIdle();
|
|
}
|
|
|
|
void wake() {
|
|
AutoLock hold(lock);
|
|
JS_NOTIFY_ALL_CONDVAR(condvar);
|
|
}
|
|
|
|
void trace(JSTracer *trc) {
|
|
AutoLock hold(lock);
|
|
for (T *p = busy.begin(); p != busy.end(); p++)
|
|
(*p)->trace(trc);
|
|
queue.trace(trc);
|
|
}
|
|
};
|
|
|
|
class MainQueue;
|
|
|
|
class Event
|
|
{
|
|
protected:
|
|
virtual ~Event() { JS_ASSERT(!data); }
|
|
|
|
WorkerParent *recipient;
|
|
Worker *child;
|
|
uint64 *data;
|
|
size_t nbytes;
|
|
|
|
public:
|
|
enum Result { fail = JS_FALSE, ok = JS_TRUE, forwardToParent };
|
|
|
|
virtual void destroy(JSContext *cx) {
|
|
JS_free(cx, data);
|
|
#ifdef DEBUG
|
|
data = NULL;
|
|
#endif
|
|
delete this;
|
|
}
|
|
|
|
void setChildAndRecipient(Worker *aChild, WorkerParent *aRecipient) {
|
|
child = aChild;
|
|
recipient = aRecipient;
|
|
}
|
|
|
|
bool deserializeData(JSContext *cx, jsval *vp) {
|
|
return !!JS_ReadStructuredClone(cx, data, nbytes, vp);
|
|
}
|
|
|
|
virtual Result process(JSContext *cx) = 0;
|
|
|
|
inline void trace(JSTracer *trc);
|
|
|
|
template <class EventType>
|
|
static EventType *createEvent(JSContext *cx, WorkerParent *recipient, Worker *child,
|
|
jsval v)
|
|
{
|
|
uint64 *data;
|
|
size_t nbytes;
|
|
if (!JS_WriteStructuredClone(cx, v, &data, &nbytes))
|
|
return NULL;
|
|
|
|
EventType *event = new EventType;
|
|
if (!event) {
|
|
JS_ReportOutOfMemory(cx);
|
|
return NULL;
|
|
}
|
|
event->recipient = recipient;
|
|
event->child = child;
|
|
event->data = data;
|
|
event->nbytes = nbytes;
|
|
return event;
|
|
}
|
|
|
|
Result dispatch(JSContext *cx, JSObject *thisobj, const char *dataPropName,
|
|
const char *methodName, Result noHandler)
|
|
{
|
|
if (!data)
|
|
return fail;
|
|
|
|
JSBool found;
|
|
if (!JS_HasProperty(cx, thisobj, methodName, &found))
|
|
return fail;
|
|
if (!found)
|
|
return noHandler;
|
|
|
|
// Create event object.
|
|
jsval v;
|
|
if (!deserializeData(cx, &v))
|
|
return fail;
|
|
JSObject *obj = JS_NewObject(cx, NULL, NULL, NULL);
|
|
if (!obj || !JS_DefineProperty(cx, obj, dataPropName, v, NULL, NULL, 0))
|
|
return fail;
|
|
|
|
// Call event handler.
|
|
jsval argv[1] = { OBJECT_TO_JSVAL(obj) };
|
|
jsval rval = JSVAL_VOID;
|
|
return Result(JS_CallFunctionName(cx, thisobj, methodName, 1, argv, &rval));
|
|
}
|
|
};
|
|
|
|
typedef ThreadSafeQueue<Event *> EventQueue;
|
|
|
|
class MainQueue : public EventQueue, public WorkerParent
|
|
{
|
|
private:
|
|
ThreadPool *threadPool;
|
|
|
|
public:
|
|
explicit MainQueue(ThreadPool *tp) : threadPool(tp) {}
|
|
|
|
~MainQueue() {
|
|
JS_ASSERT(queue.empty());
|
|
}
|
|
|
|
bool init() { return initThreadSafeQueue() && initWorkerParent(); }
|
|
|
|
void destroy(JSContext *cx) {
|
|
while (!queue.empty())
|
|
queue.pop()->destroy(cx);
|
|
delete this;
|
|
}
|
|
|
|
virtual JSLock *getLock() { return lock; }
|
|
virtual ThreadPool *getThreadPool() { return threadPool; }
|
|
|
|
protected:
|
|
virtual bool shouldStop();
|
|
|
|
public:
|
|
virtual bool post(Event *event) { return EventQueue::post(event); }
|
|
|
|
virtual void trace(JSTracer *trc);
|
|
|
|
void traceChildren(JSTracer *trc) { EventQueue::trace(trc); }
|
|
|
|
JSBool mainThreadWork(JSContext *cx, bool continueOnError) {
|
|
JSAutoSuspendRequest suspend(cx);
|
|
AutoLock hold(lock);
|
|
|
|
Event *event;
|
|
while (take(&event)) {
|
|
JS_RELEASE_LOCK(lock);
|
|
Event::Result result;
|
|
{
|
|
JSAutoRequest req(cx);
|
|
result = event->process(cx);
|
|
if (result == Event::forwardToParent) {
|
|
// FIXME - pointlessly truncates the string to 8 bits
|
|
jsval data;
|
|
const char *s;
|
|
if (event->deserializeData(cx, &data) &&
|
|
JSVAL_IS_STRING(data) &&
|
|
(s = JS_GetStringBytesZ(cx, JSVAL_TO_STRING(data)))) {
|
|
JS_ReportError(cx, "%s", s);
|
|
} else {
|
|
JS_ReportOutOfMemory(cx);
|
|
}
|
|
result = Event::fail;
|
|
}
|
|
if (result == Event::fail && continueOnError) {
|
|
if (JS_IsExceptionPending(cx) && !JS_ReportPendingException(cx))
|
|
JS_ClearPendingException(cx);
|
|
result = Event::ok;
|
|
}
|
|
}
|
|
JS_ACQUIRE_LOCK(lock);
|
|
drop(event);
|
|
event->destroy(cx);
|
|
if (result != Event::ok)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* A queue of workers.
|
|
*
|
|
* We keep a queue of workers with pending events, rather than a queue of
|
|
* events, so that two threads won't try to run a Worker at the same time.
|
|
*/
|
|
class WorkerQueue : public ThreadSafeQueue<Worker *>
|
|
{
|
|
private:
|
|
MainQueue *main;
|
|
|
|
public:
|
|
explicit WorkerQueue(MainQueue *main) : main(main) {}
|
|
|
|
void work();
|
|
};
|
|
|
|
/* The top-level object that owns everything else. */
|
|
class ThreadPool
|
|
{
|
|
private:
|
|
enum { threadCount = 6 };
|
|
|
|
JSObject *obj;
|
|
WorkerHooks *hooks;
|
|
MainQueue *mq;
|
|
WorkerQueue *wq;
|
|
PRThread *threads[threadCount];
|
|
int32_t terminating;
|
|
|
|
static JSClass jsClass;
|
|
|
|
static void start(void* arg) {
|
|
((WorkerQueue *) arg)->work();
|
|
}
|
|
|
|
explicit ThreadPool(WorkerHooks *hooks) : hooks(hooks), mq(NULL), wq(NULL), terminating(0) {
|
|
for (int i = 0; i < threadCount; i++)
|
|
threads[i] = NULL;
|
|
}
|
|
|
|
public:
|
|
~ThreadPool() {
|
|
JS_ASSERT(!mq);
|
|
JS_ASSERT(!wq);
|
|
JS_ASSERT(!threads[0]);
|
|
}
|
|
|
|
static ThreadPool *create(JSContext *cx, WorkerHooks *hooks) {
|
|
ThreadPool *tp = new ThreadPool(hooks);
|
|
if (!tp) {
|
|
JS_ReportOutOfMemory(cx);
|
|
return NULL;
|
|
}
|
|
|
|
JSObject *obj = JS_NewObject(cx, &jsClass, NULL, NULL);
|
|
if (!obj || !JS_SetPrivate(cx, obj, tp)) {
|
|
delete tp;
|
|
return NULL;
|
|
}
|
|
tp->obj = obj;
|
|
return tp;
|
|
}
|
|
|
|
JSObject *asObject() { return obj; }
|
|
WorkerHooks *getHooks() { return hooks; }
|
|
WorkerQueue *getWorkerQueue() { return wq; }
|
|
MainQueue *getMainQueue() { return mq; }
|
|
bool isTerminating() { return terminating != 0; }
|
|
|
|
/*
|
|
* Main thread only. Requires request (to prevent GC, which could see the
|
|
* object in an inconsistent state).
|
|
*/
|
|
bool start(JSContext *cx) {
|
|
JS_ASSERT(!mq && !wq);
|
|
mq = new MainQueue(this);
|
|
if (!mq || !mq->init()) {
|
|
mq->destroy(cx);
|
|
mq = NULL;
|
|
return false;
|
|
}
|
|
wq = new WorkerQueue(mq);
|
|
if (!wq || !wq->initThreadSafeQueue()) {
|
|
delete wq;
|
|
wq = NULL;
|
|
mq->destroy(cx);
|
|
mq = NULL;
|
|
return false;
|
|
}
|
|
JSAutoSuspendRequest suspend(cx);
|
|
bool ok = true;
|
|
for (int i = 0; i < threadCount; i++) {
|
|
threads[i] = PR_CreateThread(PR_USER_THREAD, start, wq, PR_PRIORITY_NORMAL,
|
|
PR_LOCAL_THREAD, PR_JOINABLE_THREAD, 0);
|
|
if (!threads[i]) {
|
|
shutdown(cx);
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
void terminateAll(JSRuntime *rt) {
|
|
// See comment about JS_ATOMIC_SET in the implementation of
|
|
// JS_TriggerOperationCallback.
|
|
JS_ATOMIC_SET(&terminating, 1);
|
|
JS_TriggerAllOperationCallbacks(rt);
|
|
}
|
|
|
|
/* This context is used only to free memory. */
|
|
void shutdown(JSContext *cx) {
|
|
wq->close();
|
|
for (int i = 0; i < threadCount; i++) {
|
|
if (threads[i]) {
|
|
PR_JoinThread(threads[i]);
|
|
threads[i] = NULL;
|
|
}
|
|
}
|
|
|
|
delete wq;
|
|
wq = NULL;
|
|
|
|
mq->disposeChildren();
|
|
mq->destroy(cx);
|
|
mq = NULL;
|
|
terminating = 0;
|
|
}
|
|
|
|
private:
|
|
static void jsTraceThreadPool(JSTracer *trc, JSObject *obj) {
|
|
ThreadPool *tp = unwrap(trc->context, obj);
|
|
if (tp->mq) {
|
|
tp->mq->traceChildren(trc);
|
|
tp->wq->trace(trc);
|
|
}
|
|
}
|
|
|
|
|
|
static void jsFinalize(JSContext *cx, JSObject *obj) {
|
|
if (ThreadPool *tp = unwrap(cx, obj))
|
|
delete tp;
|
|
}
|
|
|
|
public:
|
|
static ThreadPool *unwrap(JSContext *cx, JSObject *obj) {
|
|
JS_ASSERT(JS_GET_CLASS(cx, obj) == &jsClass);
|
|
return (ThreadPool *) JS_GetPrivate(cx, obj);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* A Worker is always in one of 4 states, except when it is being initialized
|
|
* or destroyed, or its lock is held:
|
|
* - idle (!terminated && current == NULL && events.empty())
|
|
* - enqueued (!terminated && current == NULL && !events.empty())
|
|
* - busy (!terminated && current != NULL)
|
|
* - terminated (terminated && current == NULL && events.empty())
|
|
*
|
|
* Separately, there is a terminateFlag that other threads can set
|
|
* asynchronously to tell the Worker to terminate.
|
|
*/
|
|
class Worker : public WorkerParent
|
|
{
|
|
private:
|
|
ThreadPool *threadPool;
|
|
WorkerParent *parent;
|
|
JSObject *object; // Worker object exposed to parent
|
|
JSContext *context;
|
|
JSLock *lock;
|
|
Queue<Event *, SystemAllocPolicy> events; // owning pointers to pending events
|
|
Event *current;
|
|
bool terminated;
|
|
int32_t terminateFlag;
|
|
|
|
static JSClass jsWorkerClass;
|
|
|
|
Worker()
|
|
: threadPool(NULL), parent(NULL), object(NULL),
|
|
context(NULL), lock(NULL), current(NULL), terminated(false), terminateFlag(0) {}
|
|
|
|
bool init(JSContext *parentcx, WorkerParent *parent, JSObject *obj) {
|
|
JS_ASSERT(!threadPool && !this->parent && !object && !lock);
|
|
|
|
if (!initWorkerParent() || !parent->addChild(this))
|
|
return false;
|
|
threadPool = parent->getThreadPool();
|
|
this->parent = parent;
|
|
this->object = obj;
|
|
lock = JS_NEW_LOCK();
|
|
return lock &&
|
|
createContext(parentcx, parent) &&
|
|
JS_SetPrivate(parentcx, obj, this);
|
|
}
|
|
|
|
bool createContext(JSContext *parentcx, WorkerParent *parent) {
|
|
JSRuntime *rt = JS_GetRuntime(parentcx);
|
|
context = JS_NewContext(rt, 8192);
|
|
if (!context)
|
|
return false;
|
|
|
|
// The Worker has a strong reference to the global; see jsTraceWorker.
|
|
// JSOPTION_UNROOTED_GLOBAL ensures that when the worker becomes
|
|
// unreachable, it and its global object can be collected. Otherwise
|
|
// the cx->globalObject root would keep them both alive forever.
|
|
JS_SetOptions(context, JS_GetOptions(parentcx) | JSOPTION_UNROOTED_GLOBAL |
|
|
JSOPTION_DONT_REPORT_UNCAUGHT);
|
|
JS_SetVersion(context, JS_GetVersion(parentcx));
|
|
JS_SetContextPrivate(context, this);
|
|
JS_SetOperationCallback(context, jsOperationCallback);
|
|
JS_BeginRequest(context);
|
|
|
|
JSObject *global = threadPool->getHooks()->newGlobalObject(context);
|
|
JSObject *post, *proto, *ctor;
|
|
if (!global)
|
|
goto bad;
|
|
JS_SetGlobalObject(context, global);
|
|
|
|
// Because the Worker is completely isolated from the rest of the
|
|
// runtime, and because any pending events on a Worker keep the Worker
|
|
// alive, this postMessage function cannot be called after the Worker
|
|
// is collected. Therefore it's safe to stash a pointer (a weak
|
|
// reference) to the C++ Worker object in the reserved slot.
|
|
post = JS_GetFunctionObject(JS_DefineFunction(context, global, "postMessage",
|
|
(JSNative) jsPostMessageToParent, 1, 0));
|
|
if (!post || !JS_SetReservedSlot(context, post, 0, PRIVATE_TO_JSVAL(this)))
|
|
goto bad;
|
|
|
|
proto = JS_InitClass(context, global, NULL, &jsWorkerClass, jsConstruct, 1,
|
|
NULL, jsMethods, NULL, NULL);
|
|
if (!proto)
|
|
goto bad;
|
|
|
|
ctor = JS_GetConstructor(context, proto);
|
|
if (!ctor || !JS_SetReservedSlot(context, ctor, 0, PRIVATE_TO_JSVAL(this)))
|
|
goto bad;
|
|
|
|
JS_EndRequest(context);
|
|
JS_ClearContextThread(context);
|
|
return true;
|
|
|
|
bad:
|
|
JS_EndRequest(context);
|
|
JS_DestroyContext(context);
|
|
context = NULL;
|
|
return false;
|
|
}
|
|
|
|
static void jsTraceWorker(JSTracer *trc, JSObject *obj) {
|
|
JS_ASSERT(JS_GET_CLASS(trc->context, obj) == &jsWorkerClass);
|
|
if (Worker *w = (Worker *) JS_GetPrivate(trc->context, obj)) {
|
|
w->parent->trace(trc);
|
|
w->events.trace(trc);
|
|
if (w->current)
|
|
w->current->trace(trc);
|
|
JS_CALL_OBJECT_TRACER(trc, JS_GetGlobalObject(w->context), "Worker global");
|
|
}
|
|
}
|
|
|
|
static void jsFinalize(JSContext *cx, JSObject *obj) {
|
|
JS_ASSERT(JS_GET_CLASS(cx, obj) == &jsWorkerClass);
|
|
if (Worker *w = (Worker *) JS_GetPrivate(cx, obj))
|
|
delete w;
|
|
}
|
|
|
|
static JSBool jsOperationCallback(JSContext *cx) {
|
|
Worker *w = (Worker *) JS_GetContextPrivate(cx);
|
|
JSAutoSuspendRequest suspend(cx); // avoid nesting w->lock in a request
|
|
return !w->checkTermination();
|
|
}
|
|
|
|
static JSBool jsResolveGlobal(JSContext *cx, JSObject *obj, jsid id, uintN flags,
|
|
JSObject **objp)
|
|
{
|
|
JSBool resolved;
|
|
|
|
if (!JS_ResolveStandardClass(cx, obj, id, &resolved))
|
|
return false;
|
|
if (resolved)
|
|
*objp = obj;
|
|
|
|
return true;
|
|
}
|
|
|
|
static JSBool jsPostMessageToParent(JSContext *cx, uintN argc, jsval *vp);
|
|
static JSBool jsPostMessageToChild(JSContext *cx, uintN argc, jsval *vp);
|
|
static JSBool jsTerminate(JSContext *cx, uintN argc, jsval *vp);
|
|
|
|
bool checkTermination() {
|
|
AutoLock hold(lock);
|
|
return lockedCheckTermination();
|
|
}
|
|
|
|
bool lockedCheckTermination() {
|
|
if (terminateFlag || threadPool->isTerminating()) {
|
|
terminateSelf();
|
|
terminateFlag = 0;
|
|
}
|
|
return terminated;
|
|
}
|
|
|
|
// Caller must hold the lock.
|
|
void terminateSelf() {
|
|
terminated = true;
|
|
while (!events.empty())
|
|
events.pop()->destroy(context);
|
|
|
|
// Tell the children to shut down too. An arbitrarily silly amount of
|
|
// processing could happen before the whole tree is terminated; but
|
|
// this way we don't have to worry about blowing the C stack.
|
|
for (ChildSet::Enum e(children); !e.empty(); e.popFront())
|
|
e.front()->setTerminateFlag(); // note: nesting locks here
|
|
}
|
|
|
|
public:
|
|
~Worker() {
|
|
if (parent)
|
|
parent->removeChild(this);
|
|
dispose();
|
|
}
|
|
|
|
void dispose() {
|
|
JS_ASSERT(!current);
|
|
while (!events.empty())
|
|
events.pop()->destroy(context);
|
|
if (lock) {
|
|
JS_DESTROY_LOCK(lock);
|
|
lock = NULL;
|
|
}
|
|
if (context) {
|
|
JS_SetContextThread(context);
|
|
JS_DestroyContextNoGC(context);
|
|
context = NULL;
|
|
}
|
|
object = NULL;
|
|
|
|
// Do not call parent->removeChild(). This is called either from
|
|
// ~Worker, which calls it for us; or from parent->disposeChildren or
|
|
// Worker::create, which require that it not be called.
|
|
parent = NULL;
|
|
disposeChildren();
|
|
}
|
|
|
|
static Worker *create(JSContext *parentcx, WorkerParent *parent,
|
|
JSString *scriptName, JSObject *obj);
|
|
|
|
JSObject *asObject() { return object; }
|
|
|
|
JSObject *getGlobal() { return JS_GetGlobalObject(context); }
|
|
|
|
WorkerParent *getParent() { return parent; }
|
|
|
|
virtual JSLock *getLock() { return lock; }
|
|
|
|
virtual ThreadPool *getThreadPool() { return threadPool; }
|
|
|
|
bool post(Event *event) {
|
|
AutoLock hold(lock);
|
|
if (terminated)
|
|
return false;
|
|
if (!current && events.empty() && !threadPool->getWorkerQueue()->post(this))
|
|
return false;
|
|
return events.push(event);
|
|
}
|
|
|
|
void setTerminateFlag() {
|
|
AutoLock hold(lock);
|
|
terminateFlag = true;
|
|
if (current)
|
|
JS_TriggerOperationCallback(context);
|
|
}
|
|
|
|
void processOneEvent();
|
|
|
|
/* Trace method to be called from C++. */
|
|
void trace(JSTracer *trc) {
|
|
// Just mark the JSObject. If we haven't already been marked,
|
|
// jsTraceWorker will be called, at which point we'll trace referents.
|
|
JS_CALL_OBJECT_TRACER(trc, object, "queued Worker");
|
|
}
|
|
|
|
static bool getWorkerParentFromConstructor(JSContext *cx, JSObject *ctor, WorkerParent **p) {
|
|
jsval v;
|
|
if (!JS_GetReservedSlot(cx, ctor, 0, &v))
|
|
return false;
|
|
if (JSVAL_IS_VOID(v)) {
|
|
// This means ctor is the root Worker constructor (created in
|
|
// Worker::initWorkers as opposed to Worker::createContext, which sets up
|
|
// Worker sandboxes) and nothing is initialized yet.
|
|
if (!JS_GetReservedSlot(cx, ctor, 1, &v))
|
|
return false;
|
|
ThreadPool *threadPool = (ThreadPool *) JSVAL_TO_PRIVATE(v);
|
|
if (!threadPool->start(cx))
|
|
return false;
|
|
WorkerParent *parent = threadPool->getMainQueue();
|
|
if (!JS_SetReservedSlot(cx, ctor, 0, PRIVATE_TO_JSVAL(parent))) {
|
|
threadPool->shutdown(cx);
|
|
return false;
|
|
}
|
|
*p = parent;
|
|
return true;
|
|
}
|
|
*p = (WorkerParent *) JSVAL_TO_PRIVATE(v);
|
|
return true;
|
|
}
|
|
|
|
static JSBool jsConstruct(JSContext *cx, uintN argc, jsval *vp) {
|
|
WorkerParent *parent;
|
|
if (!getWorkerParentFromConstructor(cx, JSVAL_TO_OBJECT(JS_CALLEE(cx, vp)), &parent))
|
|
return false;
|
|
|
|
|
|
JSString *scriptName = JS_ValueToString(cx, argc ? JS_ARGV(cx, vp)[0] : JSVAL_VOID);
|
|
if (!scriptName)
|
|
return false;
|
|
|
|
JSObject *obj = JS_NewObject(cx, &jsWorkerClass, NULL, NULL);
|
|
if (!obj || !create(cx, parent, scriptName, obj))
|
|
return false;
|
|
JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(obj));
|
|
return true;
|
|
}
|
|
|
|
static JSFunctionSpec jsMethods[3];
|
|
static JSFunctionSpec jsStaticMethod[2];
|
|
|
|
static ThreadPool *initWorkers(JSContext *cx, WorkerHooks *hooks, JSObject *global,
|
|
JSObject **objp) {
|
|
// Create the ThreadPool object and its JSObject wrapper.
|
|
ThreadPool *threadPool = ThreadPool::create(cx, hooks);
|
|
if (!threadPool)
|
|
return NULL;
|
|
|
|
// Root the ThreadPool JSObject early.
|
|
*objp = threadPool->asObject();
|
|
|
|
// Create the Worker constructor.
|
|
JSObject *proto = JS_InitClass(cx, global, NULL, &jsWorkerClass,
|
|
jsConstruct, 1,
|
|
NULL, jsMethods, NULL, NULL);
|
|
if (!proto)
|
|
return NULL;
|
|
|
|
// Stash a pointer to the ThreadPool in constructor reserved slot 1.
|
|
// It will be used later when lazily creating the MainQueue.
|
|
JSObject *ctor = JS_GetConstructor(cx, proto);
|
|
if (!JS_SetReservedSlot(cx, ctor, 1, PRIVATE_TO_JSVAL(threadPool)))
|
|
return NULL;
|
|
|
|
return threadPool;
|
|
}
|
|
};
|
|
|
|
class InitEvent : public Event
|
|
{
|
|
public:
|
|
static InitEvent *create(JSContext *cx, Worker *worker, JSString *scriptName) {
|
|
return createEvent<InitEvent>(cx, worker, worker, STRING_TO_JSVAL(scriptName));
|
|
}
|
|
|
|
Result process(JSContext *cx) {
|
|
jsval s;
|
|
if (!deserializeData(cx, &s))
|
|
return fail;
|
|
JS_ASSERT(JSVAL_IS_STRING(s));
|
|
const char *filename = JS_GetStringBytesZ(cx, JSVAL_TO_STRING(s));
|
|
if (!filename)
|
|
return fail;
|
|
|
|
JSScript *script = JS_CompileFile(cx, child->getGlobal(), filename);
|
|
if (!script)
|
|
return fail;
|
|
|
|
AutoValueRooter rval(cx);
|
|
JSBool ok = JS_ExecuteScript(cx, child->getGlobal(), script, Jsvalify(rval.addr()));
|
|
JS_DestroyScript(cx, script);
|
|
return Result(ok);
|
|
}
|
|
};
|
|
|
|
class DownMessageEvent : public Event
|
|
{
|
|
public:
|
|
static DownMessageEvent *create(JSContext *cx, Worker *child, jsval data) {
|
|
return createEvent<DownMessageEvent>(cx, child, child, data);
|
|
}
|
|
|
|
Result process(JSContext *cx) {
|
|
return dispatch(cx, child->getGlobal(), "data", "onmessage", ok);
|
|
}
|
|
};
|
|
|
|
class UpMessageEvent : public Event
|
|
{
|
|
public:
|
|
static UpMessageEvent *create(JSContext *cx, Worker *child, jsval data) {
|
|
return createEvent<UpMessageEvent>(cx, child->getParent(), child, data);
|
|
}
|
|
|
|
Result process(JSContext *cx) {
|
|
return dispatch(cx, child->asObject(), "data", "onmessage", ok);
|
|
}
|
|
};
|
|
|
|
class ErrorEvent : public Event
|
|
{
|
|
public:
|
|
static ErrorEvent *create(JSContext *cx, Worker *child) {
|
|
JSString *data = NULL;
|
|
jsval exc;
|
|
if (JS_GetPendingException(cx, &exc)) {
|
|
AutoValueRooter tvr(cx, Valueify(exc));
|
|
JS_ClearPendingException(cx);
|
|
|
|
// Determine what error message to put in the error event.
|
|
// If exc.message is a string, use that; otherwise use String(exc).
|
|
// (This is a little different from what web workers do.)
|
|
if (JSVAL_IS_OBJECT(exc)) {
|
|
jsval msg;
|
|
if (!JS_GetProperty(cx, JSVAL_TO_OBJECT(exc), "message", &msg))
|
|
JS_ClearPendingException(cx);
|
|
else if (JSVAL_IS_STRING(msg))
|
|
data = JSVAL_TO_STRING(msg);
|
|
}
|
|
if (!data) {
|
|
data = JS_ValueToString(cx, exc);
|
|
if (!data)
|
|
return NULL;
|
|
}
|
|
}
|
|
return createEvent<ErrorEvent>(cx, child->getParent(), child,
|
|
data ? STRING_TO_JSVAL(data) : JSVAL_VOID);
|
|
}
|
|
|
|
Result process(JSContext *cx) {
|
|
return dispatch(cx, child->asObject(), "message", "onerror", forwardToParent);
|
|
}
|
|
};
|
|
|
|
} /* namespace workers */
|
|
} /* namespace js */
|
|
|
|
using namespace js::workers;
|
|
|
|
void
|
|
WorkerParent::disposeChildren()
|
|
{
|
|
for (ChildSet::Enum e(children); !e.empty(); e.popFront()) {
|
|
e.front()->dispose();
|
|
e.removeFront();
|
|
}
|
|
}
|
|
|
|
bool
|
|
MainQueue::shouldStop()
|
|
{
|
|
// Note: This deliberately nests WorkerQueue::lock in MainQueue::lock.
|
|
// Releasing MainQueue::lock would risk a race -- isIdle() could return
|
|
// false, but the workers could become idle before we reacquire
|
|
// MainQueue::lock and go to sleep, and we would wait on the condvar
|
|
// forever.
|
|
return closed || threadPool->getWorkerQueue()->isIdle();
|
|
}
|
|
|
|
void
|
|
MainQueue::trace(JSTracer *trc)
|
|
{
|
|
JS_CALL_OBJECT_TRACER(trc, threadPool->asObject(), "MainQueue");
|
|
}
|
|
|
|
void
|
|
WorkerQueue::work() {
|
|
AutoLock hold(lock);
|
|
|
|
Worker *w;
|
|
while (take(&w)) { // can block outside the mutex
|
|
JS_RELEASE_LOCK(lock);
|
|
w->processOneEvent(); // enters request on w->context
|
|
JS_ACQUIRE_LOCK(lock);
|
|
drop(w);
|
|
|
|
if (lockedIsIdle()) {
|
|
JS_RELEASE_LOCK(lock);
|
|
main->wake();
|
|
JS_ACQUIRE_LOCK(lock);
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool mswin =
|
|
#ifdef XP_WIN
|
|
true
|
|
#else
|
|
false
|
|
#endif
|
|
;
|
|
|
|
template <class Ch> bool
|
|
IsAbsolute(const Ch *filename)
|
|
{
|
|
return filename[0] == '/' ||
|
|
(mswin && (filename[0] == '\\' || (filename[0] != '\0' && filename[1] == ':')));
|
|
}
|
|
|
|
// Note: base is a filename, not a directory name.
|
|
static JSString *
|
|
ResolveRelativePath(JSContext *cx, const char *base, JSString *filename)
|
|
{
|
|
size_t fileLen = JS_GetStringLength(filename);
|
|
const jschar *fileChars = JS_GetStringCharsZ(cx, filename);
|
|
if (!fileChars)
|
|
return NULL;
|
|
|
|
if (IsAbsolute(fileChars))
|
|
return filename;
|
|
|
|
// Strip off the filename part of base.
|
|
size_t dirLen = -1;
|
|
for (size_t i = 0; base[i]; i++) {
|
|
if (base[i] == '/' || (mswin && base[i] == '\\'))
|
|
dirLen = i;
|
|
}
|
|
|
|
// If base is relative and contains no directories, use filename unchanged.
|
|
if (!IsAbsolute(base) && dirLen == (size_t) -1)
|
|
return filename;
|
|
|
|
// Otherwise return base[:dirLen + 1] + filename.
|
|
js::Vector<jschar, 0, js::ContextAllocPolicy> result(cx);
|
|
size_t nchars;
|
|
if (!JS_DecodeBytes(cx, base, dirLen + 1, NULL, &nchars))
|
|
return NULL;
|
|
if (!result.reserve(dirLen + 1 + fileLen)) {
|
|
JS_ReportOutOfMemory(cx);
|
|
return NULL;
|
|
}
|
|
JS_ALWAYS_TRUE(result.resize(dirLen + 1));
|
|
if (!JS_DecodeBytes(cx, base, dirLen + 1, result.begin(), &nchars))
|
|
return NULL;
|
|
JS_ALWAYS_TRUE(result.append(fileChars, fileLen));
|
|
return JS_NewUCStringCopyN(cx, result.begin(), result.length());
|
|
}
|
|
|
|
Worker *
|
|
Worker::create(JSContext *parentcx, WorkerParent *parent, JSString *scriptName, JSObject *obj)
|
|
{
|
|
Worker *w = new Worker();
|
|
if (!w || !w->init(parentcx, parent, obj)) {
|
|
delete w;
|
|
return NULL;
|
|
}
|
|
|
|
JSStackFrame *frame = JS_GetScriptedCaller(parentcx, NULL);
|
|
const char *base = JS_GetScriptFilename(parentcx, JS_GetFrameScript(parentcx, frame));
|
|
JSString *scriptPath = ResolveRelativePath(parentcx, base, scriptName);
|
|
if (!scriptPath)
|
|
return NULL;
|
|
|
|
// Post an InitEvent to run the initialization script.
|
|
Event *event = InitEvent::create(parentcx, w, scriptPath);
|
|
if (!event)
|
|
return NULL;
|
|
if (!w->events.push(event) || !w->threadPool->getWorkerQueue()->post(w)) {
|
|
event->destroy(parentcx);
|
|
JS_ReportOutOfMemory(parentcx);
|
|
w->dispose();
|
|
return NULL;
|
|
}
|
|
return w;
|
|
}
|
|
|
|
void
|
|
Worker::processOneEvent()
|
|
{
|
|
Event *event;
|
|
{
|
|
AutoLock hold1(lock);
|
|
if (lockedCheckTermination() || events.empty())
|
|
return;
|
|
|
|
event = current = events.pop();
|
|
}
|
|
|
|
JS_SetContextThread(context);
|
|
JS_SetNativeStackQuota(context, gMaxStackSize);
|
|
|
|
Event::Result result;
|
|
{
|
|
JSAutoRequest req(context);
|
|
result = event->process(context);
|
|
}
|
|
|
|
// Note: we have to leave the above request before calling parent->post or
|
|
// checkTermination, both of which acquire locks.
|
|
if (result == Event::forwardToParent) {
|
|
event->setChildAndRecipient(this, parent);
|
|
if (parent->post(event)) {
|
|
event = NULL; // to prevent it from being deleted below
|
|
} else {
|
|
JS_ReportOutOfMemory(context);
|
|
result = Event::fail;
|
|
}
|
|
}
|
|
if (result == Event::fail && !checkTermination()) {
|
|
JSAutoRequest req(context);
|
|
Event *err = ErrorEvent::create(context, this);
|
|
if (err && !parent->post(err)) {
|
|
JS_ReportOutOfMemory(context);
|
|
err->destroy(context);
|
|
err = NULL;
|
|
}
|
|
if (!err) {
|
|
// FIXME - out of memory, probably should panic
|
|
}
|
|
}
|
|
|
|
if (event)
|
|
event->destroy(context);
|
|
JS_ClearContextThread(context);
|
|
|
|
{
|
|
AutoLock hold2(lock);
|
|
current = NULL;
|
|
if (!lockedCheckTermination() && !events.empty()) {
|
|
// Re-enqueue this worker. OOM here effectively kills the worker.
|
|
if (!threadPool->getWorkerQueue()->post(this))
|
|
JS_ReportOutOfMemory(context);
|
|
}
|
|
}
|
|
}
|
|
|
|
JSBool
|
|
Worker::jsPostMessageToParent(JSContext *cx, uintN argc, jsval *vp)
|
|
{
|
|
jsval workerval;
|
|
if (!JS_GetReservedSlot(cx, JSVAL_TO_OBJECT(JS_CALLEE(cx, vp)), 0, &workerval))
|
|
return false;
|
|
Worker *w = (Worker *) JSVAL_TO_PRIVATE(workerval);
|
|
|
|
{
|
|
JSAutoSuspendRequest suspend(cx); // avoid nesting w->lock in a request
|
|
if (w->checkTermination())
|
|
return false;
|
|
}
|
|
|
|
jsval data = argc > 0 ? JS_ARGV(cx, vp)[0] : JSVAL_VOID;
|
|
Event *event = UpMessageEvent::create(cx, w, data);
|
|
if (!event)
|
|
return false;
|
|
if (!w->parent->post(event)) {
|
|
event->destroy(cx);
|
|
JS_ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
JS_SET_RVAL(cx, vp, JSVAL_VOID);
|
|
return true;
|
|
}
|
|
|
|
JSBool
|
|
Worker::jsPostMessageToChild(JSContext *cx, uintN argc, jsval *vp)
|
|
{
|
|
JSObject *workerobj = JS_THIS_OBJECT(cx, vp);
|
|
if (!workerobj)
|
|
return false;
|
|
Worker *w = (Worker *) JS_GetInstancePrivate(cx, workerobj, &jsWorkerClass, JS_ARGV(cx, vp));
|
|
if (!w) {
|
|
if (!JS_IsExceptionPending(cx))
|
|
JS_ReportError(cx, "Worker was shut down");
|
|
return false;
|
|
}
|
|
|
|
jsval data = argc > 0 ? JS_ARGV(cx, vp)[0] : JSVAL_VOID;
|
|
Event *event = DownMessageEvent::create(cx, w, data);
|
|
if (!event)
|
|
return false;
|
|
if (!w->post(event)) {
|
|
JS_ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
JS_SET_RVAL(cx, vp, JSVAL_VOID);
|
|
return true;
|
|
}
|
|
|
|
JSBool
|
|
Worker::jsTerminate(JSContext *cx, uintN argc, jsval *vp)
|
|
{
|
|
JS_SET_RVAL(cx, vp, JSVAL_VOID);
|
|
|
|
JSObject *workerobj = JS_THIS_OBJECT(cx, vp);
|
|
if (!workerobj)
|
|
return false;
|
|
Worker *w = (Worker *) JS_GetInstancePrivate(cx, workerobj, &jsWorkerClass, JS_ARGV(cx, vp));
|
|
if (!w)
|
|
return !JS_IsExceptionPending(cx); // ok to terminate twice
|
|
|
|
JSAutoSuspendRequest suspend(cx);
|
|
w->setTerminateFlag();
|
|
return true;
|
|
}
|
|
|
|
void
|
|
Event::trace(JSTracer *trc)
|
|
{
|
|
if (recipient)
|
|
recipient->trace(trc);
|
|
if (child)
|
|
JS_CALL_OBJECT_TRACER(trc, child->asObject(), "worker");
|
|
}
|
|
|
|
JSClass ThreadPool::jsClass = {
|
|
"ThreadPool", JSCLASS_HAS_PRIVATE | JSCLASS_MARK_IS_TRACE,
|
|
JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
|
|
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, jsFinalize,
|
|
NULL, NULL, NULL, NULL,
|
|
NULL, NULL, JS_CLASS_TRACE(jsTraceThreadPool), NULL
|
|
};
|
|
|
|
JSClass Worker::jsWorkerClass = {
|
|
"Worker", JSCLASS_HAS_PRIVATE | JSCLASS_MARK_IS_TRACE,
|
|
JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
|
|
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, jsFinalize,
|
|
NULL, NULL, NULL, NULL,
|
|
NULL, NULL, JS_CLASS_TRACE(jsTraceWorker), NULL
|
|
};
|
|
|
|
JSFunctionSpec Worker::jsMethods[3] = {
|
|
JS_FN("postMessage", Worker::jsPostMessageToChild, 1, 0),
|
|
JS_FN("terminate", Worker::jsTerminate, 0, 0),
|
|
JS_FS_END
|
|
};
|
|
|
|
ThreadPool *
|
|
js::workers::init(JSContext *cx, WorkerHooks *hooks, JSObject *global, JSObject **rootp)
|
|
{
|
|
return Worker::initWorkers(cx, hooks, global, rootp);
|
|
}
|
|
|
|
void
|
|
js::workers::terminateAll(JSRuntime *rt, ThreadPool *tp)
|
|
{
|
|
tp->terminateAll(rt);
|
|
}
|
|
|
|
void
|
|
js::workers::finish(JSContext *cx, ThreadPool *tp)
|
|
{
|
|
if (MainQueue *mq = tp->getMainQueue()) {
|
|
JS_ALWAYS_TRUE(mq->mainThreadWork(cx, true));
|
|
tp->shutdown(cx);
|
|
}
|
|
}
|
|
|
|
#endif /* JS_THREADSAFE */
|