diff --git a/include.xml b/include.xml index cade2ddaf..0763751cb 100644 --- a/include.xml +++ b/include.xml @@ -74,8 +74,11 @@
- - +
+ + + +
@@ -86,11 +89,13 @@ + - + + diff --git a/src/lime/_internal/backend/html5/HTML5Thread.hx b/src/lime/_internal/backend/html5/HTML5Thread.hx index 20843abb2..b4f3d22ba 100644 --- a/src/lime/_internal/backend/html5/HTML5Thread.hx +++ b/src/lime/_internal/backend/html5/HTML5Thread.hx @@ -1,56 +1,70 @@ package lime._internal.backend.html5; +import lime.app.Event; + #if macro import haxe.macro.Context; import haxe.macro.Expr; -import haxe.macro.Printer; import haxe.macro.Type; using haxe.macro.Context; using haxe.macro.TypeTools; +using haxe.macro.TypedExprTools; #else +// Not safe to import js package during macros. import js.Browser; +import js.html.MessageEvent; import js.html.URL; import js.html.Worker; import js.Lib; import js.lib.Function; import js.lib.Object; import js.Syntax; +// Same with classes that import lots of other things. import lime.app.Application; -import lime.app.Event; #end /** Emulates much of the `sys.thread.Thread` API using web workers. **/ class HTML5Thread { - #if !macro private static var __current:HTML5Thread = new HTML5Thread(Lib.global.location.href); - private static var __isWorker:Bool = Browser.window == null; + private static var __isWorker:Bool #if !macro = #if !haxe4 untyped __js__ #else Syntax.code #end ('typeof window == "undefined"') #end; private static var __messages:List = new List(); private static var __resolveMethods:ListVoid> = new List(); private static var __workerCount:Int = 0; + /** + The entry point into a worker script. + + Lime's output JS file normally does not begin on its own. Instead it + registers a `lime.embed()` callback for index.html to use. + + When this JS file is run as a web worker, it isn't running within + index.html, so `embed()` never gets called. Instead, `__init__()` + registers a message listener. + **/ private static function __init__():Void { - if (__isWorker) + #if !macro + if (#if !haxe4 untyped __js__ #else Syntax.code #end ('typeof window == "undefined"')) { - Lib.global.onmessage = onJobReceived; - } - } + Lib.global.onmessage = function(event:MessageEvent):Void + { + var job:WorkFunctionVoid> = event.data; - private static function onJobReceived(job:WorkFunctionVoid>):Void - { - try - { - job.dispatch(); + try + { + job.dispatch(); + Lib.global.onmessage = __current.dispatchMessage; + } + catch (e:Dynamic) + { + __current.destroy(); + } + } } - catch (e:Dynamic) - { - Lib.global.close(); - } - - Lib.global.onmessage = __current.dispatchMessage; + #end } public static inline function current():HTML5Thread @@ -60,18 +74,31 @@ class HTML5Thread { public static function create(job:WorkFunctionVoid>):HTML5Thread { + #if !macro + // Find the URL of the primary JS file. var url:URL = new URL(__current.__href); - url.pathname = Application.current.meta["file"]; - if (url.hash.length > 0) - { - url.hash += "_"; - } + url.pathname = url.pathname.substr(0, url.pathname.lastIndexOf("/") + 1) + + Application.current.meta["file"] + ".js"; + + // Use the hash to distinguish workers. + if (url.hash.length > 0) url.hash += "_"; url.hash += __workerCount; __workerCount++; + // Prepare to send the job. + job.makePortable(); + + // Create the worker. Because the worker's scope will not include a + // `window`, `HTML5Thread.__init__()` will add a listener. var thread:HTML5Thread = new HTML5Thread(url.href, new Worker(url.href, {name: url.hash})); + + // Send a message to the listener. thread.sendMessage(job); + return thread; + #else + return null; + #end } /** @@ -80,7 +107,7 @@ class HTML5Thread { @param preserveClasses Whether to call `preserveClasses()` first. **/ - public static function returnMessage(message:Message, transferList:Array = null, preserveClasses:Bool = true):Void + public static function returnMessage(message:Message, transferList:Array = null, preserveClasses:Bool = true):Void { if (__isWorker) { @@ -109,6 +136,7 @@ class HTML5Thread { private function new(href:String, worker:Worker = null) { + #if !macro __href = href; if (worker != null) @@ -117,14 +145,13 @@ class HTML5Thread { __worker.onmessage = dispatchMessage; onMessage = new EventVoid>(); } - else - { - // `HTML5Thread`'s instance functions all assume they're being - // called on the thread where this instance was created. Therefore, - // it isn't safe for `preserveClasses()` to actually preserve this - // class. (But if `worker` is defined it's already a lost cause.) - Reflect.setField(this, Message.PROTOTYPE_FIELD, null); - } + + // If an `HTML5Thread` instance is passed to a different thread than + // where it was created, all of its instance methods will behave + // incorrectly. You can still check equality, but that's it. Therefore, + // it's best to make `preserveClasses()` skip this class. + Message.disablePreserveClasses(this); + #end } /** @@ -133,8 +160,9 @@ class HTML5Thread { @param preserveClasses Whether to call `preserveClasses()` first. **/ - public function sendMessage(message:Message, transferList:Array = null, preserveClasses:Bool = true):Void + public function sendMessage(message:Message, transferList:Array = null, preserveClasses:Bool = true):Void { + #if !macro if (__worker != null) { if (preserveClasses) @@ -149,13 +177,19 @@ class HTML5Thread { // No need for `restoreClasses()` because it came from this thread. __messages.add(message); } + #end } - private function dispatchMessage(message:Message):Void + #if !macro + private function dispatchMessage(event:MessageEvent):Void { + var message:Message = event.data; message.restoreClasses(); - onMessage.dispatch(message); + if (onMessage != null) + { + onMessage.dispatch(message); + } if (__resolveMethods.isEmpty()) { @@ -166,12 +200,14 @@ class HTML5Thread { __resolveMethods.pop()(message); } } + #end /** Closes this thread unless it's the main thread. **/ public function destroy():Void { + #if !macro if (__worker != null) { __worker.terminate(); @@ -184,8 +220,13 @@ class HTML5Thread { } catch (e:Dynamic) {} } + #end + } + + public inline function isWorker():Bool + { + return __worker != null || __isWorker; } - #end /** Reads a message from the thread queue. Returns `null` if no argument is @@ -193,6 +234,7 @@ class HTML5Thread { @param block If true, uses the `await` keyword to wait for the next message. Requires the calling function to be `async`. + @see `lime.system.WorkOutput.JSAsync.async()` **/ public static macro function readMessage(block:ExprOf):Dynamic { @@ -226,46 +268,70 @@ abstract WorkFunction(WorkFunctionData) from Wor **/ public var portable(get, never):Bool; - // `@:from` would cause errors during the macro phase. Disabling `macro` - // during the macro phase allows other macros to call this statically. + #if macro + /** + Parses a chain of nested `EField` expressions. + @return An array of all identifiers in the chain, as strings. If the + chain began with something other than an identifier, it will be returned + as the `initialExpr`. For instance, `array[i].foo.bar` will result in + `chain == ["foo", "bar"]` and `initialExpr == array[i]`. + **/ + private static function parseFieldChain(chain:Expr):{ chain:Array, ?initialExpr:Expr } + { + switch(chain.expr) + { + case EConst(CIdent(ident)): + return { chain: [ident] }; + case EField(e, field): + var out = parseFieldChain(e); + out.chain.push(field); + return out; + default: + return { chain: [], initialExpr: chain }; + } + } + #end + + // `@:from` would cause errors during the macro phase. @:noCompletion @:dox(hide) #if !macro @:from #end public static #if !macro macro #end function fromFunction(func:ExprOf) { - #if !force_synchronous - return macro { + var defaultOutput:Expr = macro { func: $func }; - #else - trace(func); - #if haxe3 - trace("haxe3"); - #end - #if haxe4 - trace("haxe4"); - #end - #if haxe5 - trace("haxe5"); - #end - //trace(Context.resolveType()); - var parts:Array = new Printer().printExpr(func).split("."); - var functionName:String = parts.pop(); - var classPath:String; - if (parts.length > 0) + if (!Context.defined("lime-threads")) { - classPath = parts.join("."); + return defaultOutput; } else { - var classType:ClassType = Context.getLocalClass().get(); - classPath = classType.pack.join(".") + "." + classType.name; - } + // Haxe likes to pass `@:this this` instead of the actual + // expression, so use a roundabout method to convert back. As a + // happy side-effect, it fully qualifies the expression. + var qualifiedFunc:String = func.typeExpr().toString(true); - return macro { - classPath: $v{classPath}, - functionName: $v{functionName} - }; - #end + // Match the package, class name, and field name. + var matcher:EReg = ~/^((?:_?\w+\.)*[A-Z]\w*)\.(_*[a-z]\w*)$/; + if (!matcher.match(qualifiedFunc)) + { + if (Context.defined("lime-warn-portability")) + { + trace(qualifiedFunc); + Context.warning("Value doesn't appear to be a static function.", func.pos); + } + return defaultOutput; + } + + var classPath:String = matcher.matched(1); + var functionName:String = matcher.matched(2); + + return macro { + func: $func, + classPath: $v{classPath}, + functionName: $v{functionName} + }; + } } /** @@ -299,27 +365,6 @@ abstract WorkFunction(WorkFunctionData) from Wor #end return this.func; } - else if (this.sourceCode != null) - { - var parser:EReg = ~/^function\(((?:\w+,\s*)*)\)\s*\{(.+)\s*\}$/s; - if (parser.match(this.sourceCode)) - { - var paramsAndBody:Array = ~/,\s*/.split(parser.matched(1)); - paramsAndBody.push(parser.matched(2)); - - #if !macro - // Compile, binding an arbitrary `this` value. Yet another - // reason instance methods don't work. - this.func = #if haxe4 Syntax.code #else untyped __js__ #end - ("Function.apply({0}, {1})", Lib.global, paramsAndBody); - #end - return this.func; - } - else - { - throw 'Could not parse function source code: ${this.sourceCode}'; - } - } throw 'Object is not a valid WorkFunction: $this'; } @@ -331,19 +376,37 @@ abstract WorkFunction(WorkFunctionData) from Wor **/ public function makePortable(throwError:Bool = true):Bool { - if ((this.classPath == null || this.functionName == null) - && this.sourceCode == null && this.func != null) + if (this.func != null) { - #if !macro - this.sourceCode = (cast this.func:Function).toString(); - #end - } - - if (this.classPath != null && this.functionName != null - // The main reason instance methods don't work. - || this.sourceCode != null && this.sourceCode.indexOf("[native code]") < 0) - { - this.func = null; + // Make sure `classPath.functionName` points to the actual function. + if (this.classPath != null || this.functionName != null) + { + #if !macro + var func = #if !haxe4 untyped __js__ #else Syntax.code #end + ("$hxClasses[{0}] && $hxClasses[{0}][{1}]", this.classPath, this.functionName); + if (func != this.func) + { + throw 'Could not make ${this.functionName} portable. Either ${this.functionName} isn\'t static, or ${this.classPath} is something other than a class.'; + } + else + { + // All set. + this.func = null; + } + #end + } + else + { + // If you aren't sure why you got this message, make sure your + // variables are of type `WorkFunction`. + // This won't work: + // var f = MyClass.staticFunction; + // bgWorker.run(f); + // ...but this will: + // var f:WorkFunctionVoid> = MyClass.staticFunction; + // bgWorker.run(f); + throw "Only static class functions can be made portable. Set -Dlime-warn-portability to see which line caused this."; + } } return portable; @@ -361,10 +424,50 @@ abstract WorkFunction(WorkFunctionData) from Wor @:allow(lime._internal.backend.html5.HTML5Thread) abstract Message(Dynamic) from Dynamic to Dynamic { - #if !macro private static inline var PROTOTYPE_FIELD:String = "__prototype__"; + private static inline var SKIP_FIELD:String = "__skipPrototype__"; private static inline var RESTORE_FIELD:String = "__restoreFlag__"; + /** + Prevents `preserveClasses()` from working on the given object. + + Note: if its class isn't preserved, `cast(object, Foo)` will fail with + the unhelpful message "uncaught exception: Object" and no line number. + + @param recursive Whether to apply this to the object's children as well. + **/ + public static function disablePreserveClasses(object:Dynamic, recursive:Bool = false):Void + { + #if !macro + if (object == null + // Avoid looping. + || Reflect.hasField(object, SKIP_FIELD) + // Skip primitive types. + || !Std.isOfType(object, Object)) + { + return; + } + + try + { + Reflect.setField(object, Message.SKIP_FIELD, true); + } + catch (e:Dynamic) + { + // Probably a frozen object; no need to recurse. + return; + } + + if (recursive) + { + for (sub in Object.values(object)) + { + disablePreserveClasses(sub, true); + } + } + #end + } + /** Adds class information to this message and all children, so that it will survive being passed across threads. "Children" are the values returned @@ -372,8 +475,10 @@ abstract Message(Dynamic) from Dynamic to Dynamic **/ public function preserveClasses():Void { - // Avoid looping. - if (Reflect.hasField(this, PROTOTYPE_FIELD) + #if !macro + if (this == null + // Avoid looping. + || Reflect.hasField(this, PROTOTYPE_FIELD) // Skip primitive types. || !Std.isOfType(this, Object)) { @@ -381,7 +486,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic } // Preserve this object's class. - if (!Std.isOfType(this, Array)) + if (!Reflect.hasField(this, SKIP_FIELD) && !Std.isOfType(this, Array)) { try { @@ -399,6 +504,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic { (sub:Message).preserveClasses(); } + #end } /** @@ -408,6 +514,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic **/ private function restoreClasses(flag:Int = null):Void { + #if !macro // Attempt to choose a unique flag. if (flag == null) { @@ -428,9 +535,17 @@ abstract Message(Dynamic) from Dynamic to Dynamic } // Restore this object's class. - if (Reflect.field(this, PROTOTYPE_FIELD) != null) + if (!Reflect.hasField(this, SKIP_FIELD) && Reflect.field(this, PROTOTYPE_FIELD) != null) { - Reflect.setField(this, RESTORE_FIELD, flag); + try + { + Reflect.setField(this, RESTORE_FIELD, flag); + } + catch (e:Dynamic) + { + // Probably a frozen object; no need to continue. + return; + } try { @@ -446,13 +561,47 @@ abstract Message(Dynamic) from Dynamic to Dynamic { (sub:Message).restoreClasses(flag); } + #end } - #end } -private typedef WorkFunctionData = { +/** + Stores the class path and function name of a function, so that it can be + found again in the background thread. +**/ +typedef WorkFunctionData = { @:optional var classPath:String; @:optional var functionName:String; - @:optional var sourceCode:String; @:optional var func:T; }; + +#if macro +typedef Worker = Dynamic; +typedef URL = Dynamic; +class Object {} +class Browser +{ + public static var window:Dynamic; +} +class Lib +{ + public static var global:Dynamic = { location: {} }; +} +#end + +/** + An object to transfer, rather than copy. + + Abstract types like `lime.utils.Int32Array` and `openfl.utils.ByteArray` + can be automatically converted. However, extern classes like + `js.lib.Int32Array` typically can't. + + @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects +**/ +// Mozilla uses "transferable" and "transferrable" interchangeably, but the HTML +// specification only uses the former. +@:forward +abstract Transferable(Dynamic) #if macro from Dynamic + #else from js.lib.ArrayBuffer from js.html.MessagePort from js.html.ImageBitmap #end +{ +} diff --git a/src/lime/app/Future.hx b/src/lime/app/Future.hx index 8709d0b5b..39d24cc00 100644 --- a/src/lime/app/Future.hx +++ b/src/lime/app/Future.hx @@ -293,7 +293,7 @@ import lime.utils.Log; /** Creates a `Future` instance which has finished with a completion value - @param error The completion value to set + @param value The completion value to set @return A new `Future` instance **/ public static function withValue(value:T):Future diff --git a/src/lime/app/Promise.hx b/src/lime/app/Promise.hx index 1b77d6726..8b95ea254 100644 --- a/src/lime/app/Promise.hx +++ b/src/lime/app/Promise.hx @@ -7,7 +7,7 @@ package lime.app; `Future` values. While `Future` is meant to be read-only, `Promise` can be used to set the state of a future - for receipients of it's `Future` object. For example: + for recipients of it's `Future` object. For example: ```haxe function examplePromise ():Future { diff --git a/src/lime/system/BackgroundWorker.hx b/src/lime/system/BackgroundWorker.hx index 2b527f155..efc5a2fa5 100644 --- a/src/lime/system/BackgroundWorker.hx +++ b/src/lime/system/BackgroundWorker.hx @@ -29,17 +29,11 @@ import lime.system.WorkOutput; @:noDebug #end @:forward(canceled, completed, currentThreads, activeJobs, idleThreads, - onComplete, onError, onProgress, onRun, cancel) + minThreads, maxThreads, onComplete, onError, onProgress, onRun, cancel) abstract BackgroundWorker(ThreadPool) { private static var doWorkWrapper:WorkFunctionWorkOutput->Void>; - private static function doWorkImpl(state:State, output:WorkOutput):Void - { - // `dispatch()` will check if it's really a `WorkFunction`. - (state.doWork:WorkFunctionWorkOutput->Void>).dispatch(state.state, output); - } - @:deprecated("Instead pass the callback to BackgroundWorker.run().") @:noCompletion @:dox(hide) public var doWork(get, never):{ add: (Dynamic->Void) -> Void }; @@ -59,7 +53,7 @@ abstract BackgroundWorker(ThreadPool) { if (doWorkWrapper == null) { - doWorkWrapper = doWorkImpl; + doWorkWrapper = BackgroundWorkerFunctions.__doWork; } this = new ThreadPool(doWorkWrapper, mode, workLoad); } @@ -119,6 +113,13 @@ abstract BackgroundWorker(ThreadPool) this.__doWork = doWorkWrapper; } + #if html5 + if (this.mode == MULTI_THREADED) + { + doWork.makePortable(); + } + #end + this.queue({ state: state, doWork: doWork @@ -127,21 +128,34 @@ abstract BackgroundWorker(ThreadPool) // Getters & Setters - private function get_doWork():{ add: (Dynamic->Void) -> Void } + private function get_doWork() { return { add: function(callback:Dynamic->Void) { - // Hack: overwrite `__doWork` just for this one function. - this.__doWork = function(state:State, output:WorkOutput):Void - { - #if html5 - if (this.mode == MULTI_THREADED) - throw "Unsupported operation; instead pass the callback to BackgroundWorker.run()."; - #end - callback(state.state); - }; + #if html5 + if (this.mode == MULTI_THREADED) + throw "Unsupported operation; instead pass the callback to BackgroundWorker.run()."; + #end + // Hack: overwrite `__doWork` just for this one function. Hope + // it wasn't in use! + this.__doWork = #if html5 { func: #end + function(state:State, output:WorkOutput):Void + { + callback(state.state); + } + #if html5 } #end; } }; } } + +@:allow(lime.system.BackgroundWorker) +private class BackgroundWorkerFunctions +{ + private static function __doWork(state:State, output:WorkOutput):Void + { + // `dispatch()` will check if it's really a `WorkFunction`. + (state.doWork:WorkFunctionWorkOutput->Void>).dispatch(state.state, output); + } +} diff --git a/src/lime/system/ThreadPool.hx b/src/lime/system/ThreadPool.hx index 4eda6430c..51f645db5 100644 --- a/src/lime/system/ThreadPool.hx +++ b/src/lime/system/ThreadPool.hx @@ -167,7 +167,7 @@ class ThreadPool extends WorkOutput app's available time every frame. See `workIterations` for instructions to improve the accuracy of this estimate. **/ - public function new(?doWork:State->WorkOutput->Void, minThreads:Int = 0, maxThreads:Int = 1, mode:ThreadMode = null, ?workLoad:Float = 1/2) + public function new(?doWork:WorkFunctionWorkOutput->Void>, minThreads:Int = 0, maxThreads:Int = 1, mode:ThreadMode = null, ?workLoad:Float = 1/2) { super(mode); @@ -194,20 +194,20 @@ class ThreadPool extends WorkOutput Application.current.onUpdate.remove(__update); - #if lime_threads - for (thread in __idleThreads) - { - thread.sendMessage(new ThreadEvent(EXIT, null)); - } - __idleThreads.clear(); - #end - for (job in __activeJobs) { #if lime_threads if (job.thread != null) { - job.thread.sendMessage(new ThreadEvent(EXIT, null)); + if (idleThreads < minThreads) + { + job.thread.sendMessage(new ThreadEvent(WORK, null)); + __idleThreads.push(job.thread); + } + else + { + job.thread.sendMessage(new ThreadEvent(EXIT, null)); + } } #end @@ -299,80 +299,78 @@ class ThreadPool extends WorkOutput /** __Run this only on a background thread.__ - Retrieves pending jobs, runs them until complete, and repeats. + Retrieves jobs using `Thread.readMessage()`, runs them until complete, + and repeats. - On all targets except HTML5, the first thread message must be a - `WorkOutput` instance. Other than that, all thread messages should be - `WORK` or `EXIT` `ThreadEvent` instances. + Before any jobs, this function requires, in order: + + 1. A `WorkOutput` instance. (Omit this message in HTML5.) + 2. The `doWork` function. **/ private static function __executeThread():Void { - #if html5 - // HTML5 requires the `async` keyword, which is easiest to do inline. - #if haxe4 js.Syntax.code #else untyped __js__ #end ("(async {0})()", - function() { - #end + JSAsync.async({ + var output:WorkOutput = #if html5 new WorkOutput(MULTI_THREADED) #else cast(Thread.readMessage(true), WorkOutput) #end; + var doWork:WorkFunctionWorkOutput->Void> = Thread.readMessage(true); + var job:ThreadEvent = null; - var output:WorkOutput = #if html5 new WorkOutput(MULTI_THREADED) #else cast(Thread.readMessage(true), WorkOutput) #end; - var doWork:WorkFunctionWorkOutput->Void> = Thread.readMessage(true); - var job:ThreadEvent = null; - - while (true) - { - // Get a job. - if (job == null) + while (true) { - job = cast(Thread.readMessage(true), ThreadEvent); - - output.resetJobProgress(); - } - - if (job.event == EXIT) - { - return; - } - - if (job.event != WORK || job.state == null) - { - job = null; - continue; - } - - // Get to work. - var interruption:Dynamic = null; - try - { - while (!output.__jobComplete.value && (interruption = Thread.readMessage(false)) == null) + // Get a job. + if (job == null) { - output.workIterations.value++; - doWork.dispatch(job.state, output); + do + { + job = Thread.readMessage(true); + } + while (!Std.isOfType(job, ThreadEvent)); + + output.resetJobProgress(); } - } - catch (e) - { - output.sendError(e); - } - if (interruption == null || output.__jobComplete.value) - { - job = null; - } - else if(Std.isOfType(interruption, ThreadEvent)) - { - job = interruption; - output.resetJobProgress(); - } - else - { - // Ignore interruption and keep current job. - } + if (job.event == EXIT) + { + return; + } - // Do it all again. - } + if (job.event != WORK || job.state == null) + { + job = null; + continue; + } - #if html5 + // Get to work. + var interruption:Dynamic = null; + try + { + while (!output.__jobComplete.value && (interruption = Thread.readMessage(false)) == null) + { + output.workIterations.value++; + doWork.dispatch(job.state, output); + } + } + catch (e) + { + output.sendError(e); + } + + if (interruption == null || output.__jobComplete.value) + { + job = null; + } + else if(Std.isOfType(interruption, ThreadEvent)) + { + job = interruption; + output.resetJobProgress(); + } + else + { + // Ignore interruption and keep working. + } + + // Do it all again. + } }); - #end } #end @@ -399,7 +397,7 @@ class ThreadPool extends WorkOutput #end // Process the queue. - while (__jobQueue.length > 0 && currentThreads < maxThreads) + while (!__jobQueue.isEmpty() && activeJobs < maxThreads) { var job:ThreadEvent = __jobQueue.pop(); if (job.event != WORK) @@ -498,8 +496,6 @@ class ThreadPool extends WorkOutput #if lime_threads if (mode == MULTI_THREADED) { - __activeJobs.remove(threadEvent.associatedJob); - if (currentThreads > maxThreads || __jobQueue.isEmpty() && currentThreads > minThreads) { threadEvent.associatedJob.thread.sendMessage(new ThreadEvent(EXIT, null)); @@ -508,6 +504,8 @@ class ThreadPool extends WorkOutput { __idleThreads.push(threadEvent.associatedJob.thread); } + + __activeJobs.removeThread(threadEvent.associatedJob.thread); } #end @@ -517,12 +515,25 @@ class ThreadPool extends WorkOutput eventSource = null; } - if (currentThreads == 0) + if (completed) { Application.current.onUpdate.remove(__update); } } + #if lime_threads + private override function createThread(executeThread:WorkFunctionVoid>):Thread + { + var thread:Thread = super.createThread(executeThread); + #if !html5 + thread.sendMessage(this); + #end + thread.sendMessage(__doWork); + + return thread; + } + #end + // Getters & Setters private inline function get_activeJobs():Int @@ -540,20 +551,23 @@ class ThreadPool extends WorkOutput return activeJobs + idleThreads; } - // Note the distinction between `doWork` and `__doWork`. + // Note the distinction between `doWork` and `__doWork`: the former is for + // backwards compatibility, while the latter is always used. private function get_doWork():{ add: (Dynamic->Void) -> Void } { return { add: function(callback:Dynamic->Void) { - __doWork = function(state:State, output:WorkOutput):Void - { - #if html5 - if (mode == MULTI_THREADED) - throw "Unsupported operation; instead pass the callback to ThreadPool's constructor."; - #end - callback(state); - }; + #if html5 + if (mode == MULTI_THREADED) + throw "Unsupported operation; instead pass the callback to ThreadPool's constructor."; + #end + __doWork = #if html5 { func: #end + function(state:State, output:WorkOutput):Void + { + callback(state); + } + #if html5 } #end; } }; } diff --git a/src/lime/system/WorkOutput.hx b/src/lime/system/WorkOutput.hx index e473888e9..280d8d0a3 100644 --- a/src/lime/system/WorkOutput.hx +++ b/src/lime/system/WorkOutput.hx @@ -12,8 +12,11 @@ import cpp.vm.Tls; import neko.vm.Deque; import neko.vm.Thread; import neko.vm.Tls; -#elseif html5 +#end + +#if html5 import lime._internal.backend.html5.HTML5Thread as Thread; +import lime._internal.backend.html5.HTML5Thread.Transferable; #end #if macro @@ -75,12 +78,15 @@ class WorkOutput private var __activeJobs:ActiveJobs = new ActiveJobs(); /** - Single-threaded mode only; the `state` provided to the active job. If - available, should be included in new `ThreadEvent`s. + The `state` provided to the active job. Will only have a value during + `__update()` in single-threaded mode, and will otherwise be `null`. + + Include this when creating new `ThreadEvent`s. **/ private var __activeJob:Null = null; - private inline function new(mode:ThreadMode) { + private inline function new(mode:Null) + { workIterations.value = 0; __jobComplete.value = false; @@ -97,7 +103,7 @@ class WorkOutput @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects **/ #if (lime_threads && html5) inline #end - public function sendComplete(message:Dynamic = null #if (lime_threads && html5) , transferList:Array = null #end):Void + public function sendComplete(message:Dynamic = null #if (lime_threads && html5) , transferList:Array = null #end):Void { if (!__jobComplete.value) { @@ -120,7 +126,7 @@ class WorkOutput @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects **/ #if (lime_threads && html5) inline #end - public function sendError(message:Dynamic = null #if (lime_threads && html5) , transferList:Array = null #end):Void + public function sendError(message:Dynamic = null #if (lime_threads && html5) , transferList:Array = null #end):Void { if (!__jobComplete.value) { @@ -143,7 +149,7 @@ class WorkOutput @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects **/ #if (lime_threads && html5) inline #end - public function sendProgress(message:Dynamic = null #if (lime_threads && html5) , transferList:Array = null #end):Void + public function sendProgress(message:Dynamic = null #if (lime_threads && html5) , transferList:Array = null #end):Void { if (!__jobComplete.value) { @@ -167,25 +173,21 @@ class WorkOutput { var thread:Thread = Thread.create(executeThread); - // Platform-dependent initialization. #if html5 thread.onMessage.add(onMessageFromWorker.bind(thread)); - #else - thread.sendMessage(this); #end return thread; } #if html5 - private function onMessageFromWorker(thread:Thread, message:Dynamic):Void + private function onMessageFromWorker(thread:Thread, threadEvent:ThreadEvent):Void { - if (!Std.isOfType(message, ThreadEvent)) + if (threadEvent.event == null) { return; } - var threadEvent:ThreadEvent = message; threadEvent.associatedJob = __activeJobs.getByThread(thread); __jobOutput.add(threadEvent); @@ -366,6 +368,26 @@ class ThreadEvent } } +class JSAsync +{ + /** + In JavaScript, runs the given block of code within an `async` function, + enabling the `await` keyword. On other targets, runs the code normally. + **/ + public static macro function async(code:Expr):Expr + { + if (Context.defined("js")) + { + var jsCode:Expr = #if haxe4 macro js.Syntax.code #else macro untyped __js__ #end; + return macro $jsCode("(async {0})()", function() $code); + } + else + { + return code; + } + } +} + #if !(target.threaded || cpp || neko) @:forward(push, add) @:forward.new abstract Deque(List) from List to List diff --git a/src/lime/tools/Dependency.hx b/src/lime/tools/Dependency.hx index 3fd77369a..1136d89a3 100644 --- a/src/lime/tools/Dependency.hx +++ b/src/lime/tools/Dependency.hx @@ -5,6 +5,7 @@ class Dependency // TODO: Is "forceLoad" the best name? Implement "whole-archive" on GCC public var embed:Bool; public var forceLoad:Bool; + public var webWorker:Bool; public var name:String; public var path:String; diff --git a/src/lime/tools/ProjectXMLParser.hx b/src/lime/tools/ProjectXMLParser.hx index d6550f6d2..ee181169a 100644 --- a/src/lime/tools/ProjectXMLParser.hx +++ b/src/lime/tools/ProjectXMLParser.hx @@ -1752,6 +1752,11 @@ class ProjectXMLParser extends HXProject dependency.forceLoad = parseBool(element.att.resolve("force-load")); } + if (element.has.resolve("web-worker")) + { + dependency.webWorker = parseBool(element.att.resolve("web-worker")); + } + var i = dependencies.length; while (i-- > 0) diff --git a/templates/html5/output.js b/templates/html5/output.js index 2185850e7..ad3aac5d4 100644 --- a/templates/html5/output.js +++ b/templates/html5/output.js @@ -1,17 +1,25 @@ (function ($hx_exports, $global) { "use strict"; var $hx_script = (function (exports, global) { ::SOURCE_FILE:: }); -$hx_exports.lime = $hx_exports.lime || {}; -$hx_exports.lime.$scripts = $hx_exports.lime.$scripts || {}; -$hx_exports.lime.$scripts["::APP_FILE::"] = $hx_script; -$hx_exports.lime.embed = function(projectName) { var exports = {}; - var script = $hx_exports.lime.$scripts[projectName]; - if (!script) throw Error("Cannot find project name \"" + projectName + "\""); - script(exports, $global); - for (var key in exports) $hx_exports[key] = $hx_exports[key] || exports[key]; - var lime = exports.lime || window.lime; - if (lime && lime.embed && this != lime.embed) lime.embed.apply(lime, arguments); - return exports; -}; +::if false:: + If `window` is undefined, it means this script is running as a web worker. + In that case, there's no need for exports, and all we need to do is run the + static initializers. +::end::if(typeof window == "undefined") { + $hx_script({}, $global); +} else { + $hx_exports.lime = $hx_exports.lime || {}; + $hx_exports.lime.$scripts = $hx_exports.lime.$scripts || {}; + $hx_exports.lime.$scripts["::APP_FILE::"] = $hx_script; + $hx_exports.lime.embed = function(projectName) { var exports = {}; + var script = $hx_exports.lime.$scripts[projectName]; + if (!script) throw Error("Cannot find project name \"" + projectName + "\""); + script(exports, $global); + for (var key in exports) $hx_exports[key] = $hx_exports[key] || exports[key]; + var lime = exports.lime || window.lime; + if (lime && lime.embed && this != lime.embed) lime.embed.apply(lime, arguments); + return exports; + }; +} ::if false:: AMD compatibility: If define() is present we need to - call it, to define our module diff --git a/tools/platforms/HTML5Platform.hx b/tools/platforms/HTML5Platform.hx index de0c2f22d..cd63e16eb 100644 --- a/tools/platforms/HTML5Platform.hx +++ b/tools/platforms/HTML5Platform.hx @@ -93,6 +93,10 @@ class HTML5Platform extends PlatformTarget if (dependency.embed && StringTools.endsWith(dependency.path, ".js") && FileSystem.exists(dependency.path)) { var script = File.getContent(dependency.path); + if (!dependency.webWorker) + { + script = 'if(typeof window != "undefined") {\n' + script + "\n}"; + } context.embeddedLibraries.push(script); } }