| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * Concepts used here: | |
| 7 * | |
| 8 * "manager" - A manager contains one or more isolates, schedules their | |
| 9 * execution, and performs other plumbing on their behalf. The isolate | |
| 10 * present at the creation of the manager is designated as its "root isolate". | |
| 11 * A manager may, for example, be implemented on a web Worker. | |
| 12 * | |
| 13 * [_Manager] - State present within a manager (exactly once, as a global). | |
| 14 * | |
| 15 * [_ManagerStub] - A handle held within one manager that allows interaction | |
| 16 * with another manager. A target manager may be addressed by zero or more | |
| 17 * [_ManagerStub]s. | |
| 18 * | |
| 19 */ | |
| 20 | |
| 21 /** | |
| 22 * A native object that is shared across isolates. This object is visible to all | |
| 23 * isolates running under the same manager (either UI or background web worker). | |
| 24 * | |
| 25 * This is code that is intended to 'escape' the isolate boundaries in order to | |
| 26 * implement the semantics of isolates in JavaScript. Without this we would have | |
| 27 * been forced to implement more code (including the top-level event loop) in | |
| 28 * JavaScript itself. | |
| 29 */ | |
| 30 // TODO(eub, sigmund): move the "manager" to be entirely in JS. | |
| 31 // Running any Dart code outside the context of an isolate gives it | |
| 32 // the change to break the isolate abstraction. | |
| 33 _Manager get _globalState() native "return \$globalState;"; | |
| 34 set _globalState(_Manager val) native "\$globalState = val;"; | |
| 35 | |
| 36 void _fillStatics(context) native @""" | |
| 37 $globals = context.isolateStatics; | |
| 38 $static_init(); | |
| 39 """; | |
| 40 | |
| 41 ReceivePort _lazyPort; | |
| 42 ReceivePort get _port() { | |
| 43 if (_lazyPort === null) { | |
| 44 _lazyPort = new ReceivePort(); | |
| 45 } | |
| 46 return _lazyPort; | |
| 47 } | |
| 48 | |
| 49 SendPort _spawnFunction(void topLevelFunction()) { | |
| 50 final name = _IsolateNatives._getJSFunctionName(topLevelFunction); | |
| 51 if (name == null) { | |
| 52 throw new UnsupportedOperationException( | |
| 53 "only top-level functions can be spawned."); | |
| 54 } | |
| 55 return _IsolateNatives._spawn(name, null, false); | |
| 56 } | |
| 57 | |
| 58 SendPort _spawnUri(String uri) { | |
| 59 return _IsolateNatives._spawn(null, uri, false); | |
| 60 } | |
| 61 | |
| 62 /** State associated with the current manager. See [globalState]. */ | |
| 63 // TODO(sigmund): split in multiple classes: global, thread, main-worker states? | |
| 64 class _Manager { | |
| 65 | |
| 66 /** Next available isolate id within this [_Manager]. */ | |
| 67 int nextIsolateId = 0; | |
| 68 | |
| 69 /** id assigned to this [_Manager]. */ | |
| 70 int currentManagerId = 0; | |
| 71 | |
| 72 /** | |
| 73 * Next available manager id. Only used by the main manager to assign a unique | |
| 74 * id to each manager created by it. | |
| 75 */ | |
| 76 int nextManagerId = 1; | |
| 77 | |
| 78 /** Context for the currently running [Isolate]. */ | |
| 79 _IsolateContext currentContext = null; | |
| 80 | |
| 81 /** Context for the root [Isolate] that first run in this [_Manager]. */ | |
| 82 _IsolateContext rootContext = null; | |
| 83 | |
| 84 /** The top-level event loop. */ | |
| 85 _EventLoop topEventLoop; | |
| 86 | |
| 87 /** Whether this program is running from the command line. */ | |
| 88 bool fromCommandLine; | |
| 89 | |
| 90 /** Whether this [_Manager] is running as a web worker. */ | |
| 91 bool isWorker; | |
| 92 | |
| 93 /** Whether we support spawning web workers. */ | |
| 94 bool supportsWorkers; | |
| 95 | |
| 96 /** | |
| 97 * Whether to use web workers when implementing isolates. Set to false for | |
| 98 * debugging/testing. | |
| 99 */ | |
| 100 bool get useWorkers() => supportsWorkers; | |
| 101 | |
| 102 /** | |
| 103 * Whether to use the web-worker JSON-based message serialization protocol. By | |
| 104 * default this is only used with web workers. For debugging, you can force | |
| 105 * using this protocol by changing this field value to [true]. | |
| 106 */ | |
| 107 bool get needSerialization() => useWorkers; | |
| 108 | |
| 109 /** | |
| 110 * Registry of isolates. Isolates must be registered if, and only if, receive | |
| 111 * ports are alive. Normally no open receive-ports means that the isolate is | |
| 112 * dead, but DOM callbacks could resurrect it. | |
| 113 */ | |
| 114 Map<int, _IsolateContext> isolates; | |
| 115 | |
| 116 /** Reference to the main [_Manager]. Null in the main [_Manager] itself. */ | |
| 117 _ManagerStub mainManager; | |
| 118 | |
| 119 /** Registry of active [_ManagerStub]s. Only used in the main [_Manager]. */ | |
| 120 Map<int, _ManagerStub> managers; | |
| 121 | |
| 122 _Manager() { | |
| 123 _nativeDetectEnvironment(); | |
| 124 topEventLoop = new _EventLoop(); | |
| 125 isolates = new Map<int, _IsolateContext>(); | |
| 126 managers = new Map<int, _ManagerStub>(); | |
| 127 if (isWorker) { // "if we are not the main manager ourself" is the intent. | |
| 128 mainManager = new _MainManagerStub(); | |
| 129 _nativeInitWorkerMessageHandler(); | |
| 130 } | |
| 131 } | |
| 132 | |
| 133 void _nativeDetectEnvironment() native @""" | |
| 134 this.isWorker = $isWorker; | |
| 135 this.supportsWorkers = $supportsWorkers; | |
| 136 this.fromCommandLine = typeof(window) == 'undefined'; | |
| 137 """; | |
| 138 | |
| 139 void _nativeInitWorkerMessageHandler() native @""" | |
| 140 $globalThis.onmessage = function (e) { | |
| 141 _IsolateNatives._processWorkerMessage(this.mainManager, e); | |
| 142 } | |
| 143 """; | |
| 144 /*: TODO: check that _processWorkerMessage is not discarded while treeshaking. | |
| 145 """ { | |
| 146 _IsolateNatives._processWorkerMessage(null, null); | |
| 147 } | |
| 148 */ | |
| 149 | |
| 150 | |
| 151 /** Close the worker running this code if all isolates are done. */ | |
| 152 void maybeCloseWorker() { | |
| 153 if (isolates.isEmpty()) { | |
| 154 mainManager.postMessage(_serializeMessage({'command': 'close'})); | |
| 155 } | |
| 156 } | |
| 157 } | |
| 158 | |
| 159 /** Context information tracked for each isolate. */ | |
| 160 class _IsolateContext { | |
| 161 /** Current isolate id. */ | |
| 162 int id; | |
| 163 | |
| 164 /** Registry of receive ports currently active on this isolate. */ | |
| 165 Map<int, ReceivePort> ports; | |
| 166 | |
| 167 /** Holds isolate globals (statics and top-level properties). */ | |
| 168 var isolateStatics; // native object containing all globals of an isolate. | |
| 169 | |
| 170 _IsolateContext() { | |
| 171 id = _globalState.nextIsolateId++; | |
| 172 ports = new Map<int, ReceivePort>(); | |
| 173 initGlobals(); | |
| 174 } | |
| 175 | |
| 176 // these are filled lazily the first time the isolate starts running. | |
| 177 void initGlobals() native @'$initGlobals(this);'; | |
| 178 | |
| 179 /** | |
| 180 * Run [code] in the context of the isolate represented by [this]. Note this | |
| 181 * is called from JavaScript (see $wrap_call in corejs.dart). | |
| 182 */ | |
| 183 Dynamic eval(Function code) { | |
| 184 var old = _globalState.currentContext; | |
| 185 _globalState.currentContext = this; | |
| 186 this._setGlobals(); | |
| 187 var result = null; | |
| 188 try { | |
| 189 result = code(); | |
| 190 } finally { | |
| 191 _globalState.currentContext = old; | |
| 192 if (old != null) old._setGlobals(); | |
| 193 } | |
| 194 return result; | |
| 195 } | |
| 196 | |
| 197 void _setGlobals() native @'$setGlobals(this);'; | |
| 198 | |
| 199 /** Lookup a port registered for this isolate. */ | |
| 200 ReceivePort lookup(int portId) => ports[portId]; | |
| 201 | |
| 202 /** Register a port on this isolate. */ | |
| 203 void register(int portId, ReceivePort port) { | |
| 204 if (ports.containsKey(portId)) { | |
| 205 throw new Exception("Registry: ports must be registered only once."); | |
| 206 } | |
| 207 ports[portId] = port; | |
| 208 _globalState.isolates[id] = this; // indicate this isolate is active | |
| 209 } | |
| 210 | |
| 211 /** Unregister a port on this isolate. */ | |
| 212 void unregister(int portId) { | |
| 213 ports.remove(portId); | |
| 214 if (ports.isEmpty()) { | |
| 215 _globalState.isolates.remove(id); // indicate this isolate is not active | |
| 216 } | |
| 217 } | |
| 218 } | |
| 219 | |
| 220 /** Represent the event loop on a javascript thread (DOM or worker). */ | |
| 221 class _EventLoop { | |
| 222 Queue<_IsolateEvent> events; | |
| 223 | |
| 224 _EventLoop() : events = new Queue<_IsolateEvent>(); | |
| 225 | |
| 226 void enqueue(isolate, fn, msg) { | |
| 227 events.addLast(new _IsolateEvent(isolate, fn, msg)); | |
| 228 } | |
| 229 | |
| 230 _IsolateEvent dequeue() { | |
| 231 if (events.isEmpty()) return null; | |
| 232 return events.removeFirst(); | |
| 233 } | |
| 234 | |
| 235 /** Process a single event, if any. */ | |
| 236 bool runIteration() { | |
| 237 final event = dequeue(); | |
| 238 if (event == null) { | |
| 239 if (_globalState.isWorker) { | |
| 240 _globalState.maybeCloseWorker(); | |
| 241 } else if (_globalState.rootContext != null && | |
| 242 _globalState.isolates.containsKey( | |
| 243 _globalState.rootContext.id) && | |
| 244 _globalState.fromCommandLine && | |
| 245 _globalState.rootContext.ports.isEmpty()) { | |
| 246 // We want to reach here only on the main [_Manager] and only | |
| 247 // on the command-line. In the browser the isolate might | |
| 248 // still be alive due to DOM callbacks, but the presumption is | |
| 249 // that on the command-line, no future events can be injected | |
| 250 // into the event queue once it's empty. Node has setTimeout | |
| 251 // so this presumption is incorrect there. We think(?) that | |
| 252 // in d8 this assumption is valid. | |
| 253 throw new Exception("Program exited with open ReceivePorts."); | |
| 254 } | |
| 255 return false; | |
| 256 } | |
| 257 event.process(); | |
| 258 return true; | |
| 259 } | |
| 260 | |
| 261 /** | |
| 262 * Runs multiple iterations of the run-loop. If possible, each iteration is | |
| 263 * run asynchronously. | |
| 264 */ | |
| 265 void _runHelper() { | |
| 266 // [_window] is defined in timer_provider.dart. | |
| 267 if (_window != null) { | |
| 268 // Run each iteration from the browser's top event loop. | |
| 269 void next() { | |
| 270 if (!runIteration()) return; | |
| 271 _window.setTimeout(next, 0); | |
| 272 } | |
| 273 next(); | |
| 274 } else { | |
| 275 // Run synchronously until no more iterations are available. | |
| 276 while (runIteration()) {} | |
| 277 } | |
| 278 } | |
| 279 | |
| 280 /** | |
| 281 * Call [_runHelper] but ensure that worker exceptions are propragated. Note | |
| 282 * this is called from JavaScript (see $wrap_call in corejs.dart). | |
| 283 */ | |
| 284 void run() { | |
| 285 if (!_globalState.isWorker) { | |
| 286 _runHelper(); | |
| 287 } else { | |
| 288 try { | |
| 289 _runHelper(); | |
| 290 } catch(var e, var trace) { | |
| 291 _globalState.mainManager.postMessage(_serializeMessage( | |
| 292 {'command': 'error', 'msg': '$e\n$trace' })); | |
| 293 } | |
| 294 } | |
| 295 } | |
| 296 } | |
| 297 | |
| 298 /** An event in the top-level event queue. */ | |
| 299 class _IsolateEvent { | |
| 300 _IsolateContext isolate; | |
| 301 Function fn; | |
| 302 String message; | |
| 303 | |
| 304 _IsolateEvent(this.isolate, this.fn, this.message); | |
| 305 | |
| 306 void process() { | |
| 307 isolate.eval(fn); | |
| 308 } | |
| 309 } | |
| 310 | |
| 311 /** An interface for a stub used to interact with a manager. */ | |
| 312 interface _ManagerStub { | |
| 313 get id(); | |
| 314 void set id(int i); | |
| 315 void set onmessage(Function f); | |
| 316 void postMessage(msg); | |
| 317 void terminate(); | |
| 318 } | |
| 319 | |
| 320 /** A stub for interacting with the main manager. */ | |
| 321 class _MainManagerStub implements _ManagerStub { | |
| 322 get id() => 0; | |
| 323 void set id(int i) { throw new NotImplementedException(); } | |
| 324 void set onmessage(f) { | |
| 325 throw new Exception("onmessage should not be set on MainManagerStub"); | |
| 326 } | |
| 327 void postMessage(msg) native @"$globalThis.postMessage(msg);"; | |
| 328 void terminate() {} // Nothing useful to do here. | |
| 329 } | |
| 330 | |
| 331 /** | |
| 332 * A stub for interacting with a manager built on a web worker. The | |
| 333 * type Worker is also defined in 'dart:dom_deprecated', but we define | |
| 334 * it here to avoid introducing a dependency from corelib to dom. This | |
| 335 * definition uses a 'hidden' type (* prefix on the native name) to | |
| 336 * enforce that the type is defined dynamically only when web workers | |
| 337 * are actually available. | |
| 338 */ | |
| 339 class _WorkerStub implements _ManagerStub native "*Worker" { | |
| 340 get id() native "return this.id;"; | |
| 341 void set id(i) native "this.id = i;"; | |
| 342 void set onmessage(f) native "this.onmessage = f;"; | |
| 343 void postMessage(msg) native "return this.postMessage(msg);"; | |
| 344 // terminate() is implemented by Worker. | |
| 345 abstract void terminate(); | |
| 346 } | |
| 347 | |
| 348 final String _SPAWNED_SIGNAL = "spawned"; | |
| 349 | |
| 350 class _IsolateNatives { | |
| 351 | |
| 352 /** | |
| 353 * The src url for the script tag that loaded this code. Used to create | |
| 354 * JavaScript workers. | |
| 355 */ | |
| 356 static String get _thisScript() native @"return $thisScriptUrl"; | |
| 357 | |
| 358 /** Starts a new worker with the given URL. */ | |
| 359 static _WorkerStub _newWorker(url) native "return new Worker(url);"; | |
| 360 | |
| 361 /** | |
| 362 * Assume that [e] is a browser message event and extract its message data. | |
| 363 * We don't import the dom explicitly so, when workers are disabled, this | |
| 364 * library can also run on top of nodejs. | |
| 365 */ | |
| 366 static _getEventData(e) native "return e.data"; | |
| 367 | |
| 368 /** | |
| 369 * Process messages on a worker, either to control the worker instance or to | |
| 370 * pass messages along to the isolate running in the worker. | |
| 371 */ | |
| 372 static void _processWorkerMessage(sender, e) { | |
| 373 var msg = _deserializeMessage(_getEventData(e)); | |
| 374 switch (msg['command']) { | |
| 375 case 'start': | |
| 376 _globalState.currentManagerId = msg['id']; | |
| 377 Function entryPoint = _getJSFunctionFromName(msg['functionName']); | |
| 378 var replyTo = _deserializeMessage(msg['replyTo']); | |
| 379 _globalState.topEventLoop.enqueue(new _IsolateContext(), function() { | |
| 380 _startIsolate(entryPoint, replyTo); | |
| 381 }, 'worker-start'); | |
| 382 _globalState.topEventLoop.run(); | |
| 383 break; | |
| 384 case 'spawn-worker': | |
| 385 _spawnWorker(msg['functionName'], msg['uri'], msg['replyPort']); | |
| 386 break; | |
| 387 case 'message': | |
| 388 msg['port'].send(msg['msg'], msg['replyTo']); | |
| 389 _globalState.topEventLoop.run(); | |
| 390 break; | |
| 391 case 'close': | |
| 392 _log("Closing Worker"); | |
| 393 _globalState.managers.remove(sender.id); | |
| 394 sender.terminate(); | |
| 395 _globalState.topEventLoop.run(); | |
| 396 break; | |
| 397 case 'log': | |
| 398 _log(msg['msg']); | |
| 399 break; | |
| 400 case 'print': | |
| 401 if (_globalState.isWorker) { | |
| 402 _globalState.mainManager.postMessage( | |
| 403 _serializeMessage({'command': 'print', 'msg': msg})); | |
| 404 } else { | |
| 405 print(msg['msg']); | |
| 406 } | |
| 407 break; | |
| 408 case 'error': | |
| 409 throw msg['msg']; | |
| 410 } | |
| 411 } | |
| 412 | |
| 413 /** Log a message, forwarding to the main [_Manager] if appropriate. */ | |
| 414 static _log(msg) { | |
| 415 if (_globalState.isWorker) { | |
| 416 _globalState.mainManager.postMessage( | |
| 417 _serializeMessage({'command': 'log', 'msg': msg })); | |
| 418 } else { | |
| 419 try { | |
| 420 _consoleLog(msg); | |
| 421 } catch(var e, var trace) { | |
| 422 throw new Exception(trace); | |
| 423 } | |
| 424 } | |
| 425 } | |
| 426 | |
| 427 static void _consoleLog(msg) native "\$globalThis.console.log(msg);"; | |
| 428 | |
| 429 /** | |
| 430 * Extract the constructor of runnable, so it can be allocated in another | |
| 431 * isolate. | |
| 432 */ | |
| 433 static Dynamic _getJSConstructor(Isolate runnable) native """ | |
| 434 return runnable.constructor; | |
| 435 """; | |
| 436 | |
| 437 /** Extract the constructor name of a runnable */ | |
| 438 // TODO(sigmund): find a browser-generic way to support this. | |
| 439 static Dynamic _getJSConstructorName(Isolate runnable) native """ | |
| 440 return runnable.constructor.name; | |
| 441 """; | |
| 442 | |
| 443 /** Find a constructor given its name. */ | |
| 444 static Dynamic _getJSConstructorFromName(String factoryName) native """ | |
| 445 return \$globalThis[factoryName]; | |
| 446 """; | |
| 447 | |
| 448 static Dynamic _getJSFunctionFromName(String functionName) native """ | |
| 449 return \$globalThis[functionName]; | |
| 450 """; | |
| 451 | |
| 452 /** | |
| 453 * Get a string name for the function, if possible. The result for | |
| 454 * anonymous functions is browser-dependent -- it may be "" or "anonymous" | |
| 455 * but you should probably not count on this. | |
| 456 */ | |
| 457 static String _getJSFunctionName(Function f) | |
| 458 native @"return f.$name || (void 0);"; | |
| 459 | |
| 460 /** Create a new JavaScript object instance given its constructor. */ | |
| 461 static Dynamic _allocate(var ctor) native "return new ctor();"; | |
| 462 | |
| 463 // TODO(sigmund): clean up above, after we make the new API the default: | |
| 464 | |
| 465 static _spawn(String functionName, String uri, bool isLight) { | |
| 466 Completer<SendPort> completer = new Completer<SendPort>(); | |
| 467 ReceivePort port = new ReceivePort(); | |
| 468 port.receive((msg, SendPort replyPort) { | |
| 469 port.close(); | |
| 470 assert(msg == _SPAWNED_SIGNAL); | |
| 471 completer.complete(replyPort); | |
| 472 }); | |
| 473 | |
| 474 SendPort signalReply = port.toSendPort(); | |
| 475 | |
| 476 if (_globalState.useWorkers && !isLight) { | |
| 477 _startWorker(functionName, uri, signalReply); | |
| 478 } else { | |
| 479 _startNonWorker(functionName, uri, signalReply); | |
| 480 } | |
| 481 return new _BufferingSendPort( | |
| 482 _globalState.currentContext.id, completer.future); | |
| 483 } | |
| 484 | |
| 485 static SendPort _startWorker( | |
| 486 String functionName, String uri, SendPort replyPort) { | |
| 487 if (_globalState.isWorker) { | |
| 488 _globalState.mainManager.postMessage(_serializeMessage({ | |
| 489 'command': 'spawn-worker', | |
| 490 'functionName': functionName, | |
| 491 'uri': uri, | |
| 492 'replyPort': replyPort})); | |
| 493 } else { | |
| 494 _spawnWorker(functionName, uri, replyPort); | |
| 495 } | |
| 496 } | |
| 497 | |
| 498 static SendPort _startNonWorker( | |
| 499 String functionName, String uri, SendPort replyPort) { | |
| 500 // TODO(eub): support IE9 using an iframe -- Dart issue 1702. | |
| 501 if (uri != null) throw new UnsupportedOperationException( | |
| 502 "Currently spawnUri is not supported without web workers."); | |
| 503 _globalState.topEventLoop.enqueue(new _IsolateContext(), function() { | |
| 504 final func = _getJSFunctionFromName(functionName); | |
| 505 _startIsolate(func, replyPort); | |
| 506 }, 'nonworker start'); | |
| 507 } | |
| 508 | |
| 509 static void _startIsolate(Function topLevel, SendPort replyTo) { | |
| 510 _fillStatics(_globalState.currentContext); | |
| 511 _lazyPort = new ReceivePort(); | |
| 512 replyTo.send(_SPAWNED_SIGNAL, port.toSendPort()); | |
| 513 topLevel(); | |
| 514 } | |
| 515 | |
| 516 /** | |
| 517 * Spawns an isolate in a worker. [factoryName] is the Javascript constructor | |
| 518 * name for the isolate entry point class. | |
| 519 */ | |
| 520 static void _spawnWorker(functionName, uri, replyPort) { | |
| 521 if (functionName == null) functionName = 'main'; | |
| 522 if (uri == null) uri = _thisScript; | |
| 523 if (!(new Uri.fromString(uri).isAbsolute())) { | |
| 524 // The constructor of dom workers requires an absolute URL. If we use a | |
| 525 // relative path we will get a DOM exception. | |
| 526 String prefix = _thisScript.substring(0, _thisScript.lastIndexOf('/')); | |
| 527 uri = "$prefix/$uri"; | |
| 528 } | |
| 529 final worker = _newWorker(uri); | |
| 530 worker.onmessage = (e) { _processWorkerMessage(worker, e); }; | |
| 531 var workerId = _globalState.nextManagerId++; | |
| 532 // We also store the id on the worker itself so that we can unregister it. | |
| 533 worker.id = workerId; | |
| 534 _globalState.managers[workerId] = worker; | |
| 535 worker.postMessage(_serializeMessage({ | |
| 536 'command': 'start', | |
| 537 'id': workerId, | |
| 538 // Note: we serialize replyPort twice because the child worker needs to | |
| 539 // first deserialize the worker id, before it can correctly deserialize | |
| 540 // the port (port deserialization is sensitive to what is the current | |
| 541 // workerId). | |
| 542 'replyTo': _serializeMessage(replyPort), | |
| 543 'functionName': functionName })); | |
| 544 } | |
| 545 } | |
| OLD | NEW |