| 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 |