diff --git a/docs/ImportAll.hx b/docs/ImportAll.hx index ed451cc3d..be172c7d0 100644 --- a/docs/ImportAll.hx +++ b/docs/ImportAll.hx @@ -112,7 +112,6 @@ import lime.net.HTTPRequest; import lime.net.HTTPRequestHeader; import lime.net.HTTPRequestMethod; import lime.net.URIParser; -import lime.system.BackgroundWorker; import lime.system.CFFI; import lime.system.CFFIPointer; import lime.system.Clipboard; diff --git a/include.xml b/include.xml index c9151e4ab..0763751cb 100644 --- a/include.xml +++ b/include.xml @@ -74,6 +74,12 @@
+
+ + + +
+
@@ -83,11 +89,13 @@ + - + + diff --git a/src/lime/_internal/backend/html5/HTML5Thread.hx b/src/lime/_internal/backend/html5/HTML5Thread.hx new file mode 100644 index 000000000..08e0b2b85 --- /dev/null +++ b/src/lime/_internal/backend/html5/HTML5Thread.hx @@ -0,0 +1,633 @@ +package lime._internal.backend.html5; + +import lime.app.Event; + +#if macro +import haxe.macro.Context; +import haxe.macro.Expr; +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.*; +import js.Lib; +#if haxe4 +import js.lib.Function; +import js.lib.Object; +import js.lib.Promise; +import js.Syntax; +#else +import js.Promise; +#end +// Same with classes that import lots of other things. +import lime.app.Application; +#end + +/** + Emulates much of the `sys.thread.Thread` API using web workers. +**/ +class HTML5Thread { + private static var __current:HTML5Thread = new HTML5Thread(Lib.global.location.href); + 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 !macro + if (#if !haxe4 untyped __js__ #else Syntax.code #end ('typeof window == "undefined"')) + { + Lib.global.onmessage = function(event:MessageEvent):Void + { + var job:WorkFunctionVoid> = event.data; + + try + { + Lib.global.onmessage = __current.dispatchMessage; + job.dispatch(); + } + catch (e:Dynamic) + { + __current.destroy(); + } + }; + } + #end + } + + public static inline function current():HTML5Thread + { + return __current; + } + + 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 = 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)); + + // Run `job` on the new thread. + thread.sendMessage(job); + + return thread; + #else + return null; + #end + } + + #if !macro + private static inline function zeroDelay():Promise + { + return new Promise(function(resolve, _):Void + { + js.Lib.global.setTimeout(resolve); + }); + } + #end + + /** + Reads a message from the thread queue. Returns `null` if no message is + available. This may only be called inside an `async` function. + @param block If true, waits for the next message before returning. + @see `lime.system.WorkOutput.JSAsync.async()` + **/ + public static macro function readMessage(block:ExprOf):Dynamic + { + var jsCode:Expr = macro #if haxe4 js.Syntax.code #else untyped __js__ #end; + + // `onmessage` events are only received when the main function is + // suspended, so we must insert `await` even if `block` is false. + // TODO: find a more efficient way to read messages. + var zeroDelayExpr:Expr = macro @:privateAccess + $jsCode("await {0}", lime._internal.backend.html5.HTML5Thread.zeroDelay()) + .then(function(_) return lime._internal.backend.html5.HTML5Thread.__messages.pop()); + + switch (block.expr) + { + case EConst(CIdent("false")): + return zeroDelayExpr; + default: + return macro if ($block && @:privateAccess lime._internal.backend.html5.HTML5Thread.__messages.isEmpty()) + { + $jsCode("await {0}", new #if haxe4 js.lib.Promise #else js.Promise #end + (function(resolve, _):Void + { + @:privateAccess lime._internal.backend.html5.HTML5Thread.__resolveMethods.add(resolve); + } + )); + } + else + $zeroDelayExpr; + } + } + + /** + Sends a message back to the thread that spawned this worker. Has no + effect if called from the main thread. + + @param preserveClasses Whether to call `preserveClasses()` first. + **/ + public static function returnMessage(message:Message, transferList:Array = null, preserveClasses:Bool = true):Void + { + if (__isWorker) + { + if (preserveClasses) + { + message.preserveClasses(); + } + + Lib.global.postMessage(message, transferList); + } + } + + @:op(A == B) private static inline function equals(a:HTML5Thread, b:HTML5Thread):Bool + { + return a.__href == b.__href; + } + + /** + Dispatches only messages coming from this `HTML5Thread`. Only available + in the case of `HTML5Thread.create()`; never available via `current()`. + **/ + public var onMessage(default, null):NullVoid>>; + + private var __href:String; + private var __worker:Null; + + private function new(href:String, worker:Worker = null) + { + #if !macro + __href = href; + + if (worker != null) + { + __worker = worker; + __worker.onmessage = dispatchMessage; + onMessage = new EventVoid>(); + } + + // 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 + } + + /** + Send a message to the thread queue. This message can be read using + `readMessage()` or by listening for the `onMessage` event. + + @param preserveClasses Whether to call `preserveClasses()` first. + **/ + public function sendMessage(message:Message, transferList:Array = null, preserveClasses:Bool = true):Void + { + #if !macro + if (__worker != null) + { + if (preserveClasses) + { + message.preserveClasses(); + } + + __worker.postMessage(message, transferList); + } + else + { + // No need for `restoreClasses()` because it came from this thread. + __messages.add(message); + } + #end + } + + #if !macro + private function dispatchMessage(event:MessageEvent):Void + { + var message:Message = event.data; + message.restoreClasses(); + + if (onMessage != null) + { + onMessage.dispatch(message); + } + + if (__resolveMethods.isEmpty()) + { + __messages.add(message); + } + else + { + __resolveMethods.pop()(message); + } + } + #end + + /** + Closes this thread unless it's the main thread. + **/ + public function destroy():Void + { + #if !macro + if (__worker != null) + { + __worker.terminate(); + } + else if (__isWorker) + { + try + { + Lib.global.close(); + } + catch (e:Dynamic) {} + } + #end + } + + public inline function isWorker():Bool + { + return __worker != null || __isWorker; + } +} + +abstract WorkFunction(WorkFunctionData) from WorkFunctionData +{ + /** + Whether this function is ready to copy across threads. If not, call + `makePortable()` before sending it. + **/ + public var portable(get, never):Bool; + + #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) + { + var defaultOutput:Expr = macro { + func: $func + }; + + if (!Context.defined("lime-threads")) + { + return defaultOutput; + } + else + { + // 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); + + // 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")) + { + 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} + }; + } + } + + /** + Executes this function on the current thread. + **/ + public macro function dispatch(self:ExprOf>, args:Array):Expr + { + return macro $self.toFunction()($a{args}); + } + + #if haxe4 @:to #end + public function toFunction():T + { + if (this.func != null) + { + return this.func; + } + else if (this.classPath != null && this.functionName != null) + { + #if !macro + this.func = #if !haxe4 untyped __js__ #else Syntax.code #end + ("$hxClasses[{0}][{1}]", this.classPath, this.functionName); + #end + return this.func; + } + + throw 'Object is not a valid WorkFunction: $this'; + } + + /** + Attempts to make this function safe for passing across threads. + @return Whether this function is now portable. If false, try a static + function instead, and make sure you aren't using `bind()`. + **/ + public function makePortable(throwError:Bool = true):Bool + { + if (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; + } + + // Getters & Setters + + private inline function get_portable():Bool + { + return this.func == null; + } +} + +/** + 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 func:T; +}; + +@:forward +@:allow(lime._internal.backend.html5.HTML5Thread) +abstract Message(Dynamic) from Dynamic to Dynamic +{ + private static inline var PROTOTYPE_FIELD:String = "__prototype__"; + private static inline var SKIP_FIELD:String = "__skipPrototype__"; + private static inline var RESTORE_FIELD:String = "__restoreFlag__"; + + #if !macro + private static inline function skip(object:Dynamic):Bool + { + // Skip `null` for obvious reasons. + return object == null + // No need to preserve a primitive type. + || !#if (haxe_ver >= 4.2) Std.isOfType #else untyped __js__ #end (object, Object) + // Objects with this field have been deliberately excluded. + || Reflect.field(object, SKIP_FIELD) == true + // A `Uint8Array` (the type used by `haxe.io.Bytes`) can have + // thousands or millions of fields, which can take entire seconds to + // enumerate. This also applies to `Int8Array`, `Float64Array`, etc. + || object.byteLength != null && object.byteOffset != null + && object.buffer != null + && #if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (object.buffer, #if haxe4 js.lib.ArrayBuffer #else js.html.ArrayBuffer #end); + } + #end + + /** + 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. + **/ + public static function disablePreserveClasses(object:Dynamic):Void + { + #if !macro + if (skip(object)) + { + return; + } + + Reflect.setField(object, Message.SKIP_FIELD, 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 + by `Object.values()`. + **/ + public function preserveClasses():Void + { + #if !macro + if (skip(this) || Reflect.hasField(this, PROTOTYPE_FIELD)) + { + return; + } + + // Preserve this object's class. + if (!#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (this, Array)) + { + try + { + if (this.__class__ != null) + { + #if haxe4 + Reflect.setField(this, PROTOTYPE_FIELD, this.__class__.__name__); + #else + Reflect.setField(this, PROTOTYPE_FIELD, this.__class__.__name__.join(".")); + #end + } + else + { + Reflect.setField(this, PROTOTYPE_FIELD, null); + } + } + catch (e:Dynamic) + { + // Probably a frozen object; no need to recurse. + return; + } + + // While usually it's the user's job not to include any functions, + // enums come with a built-in `toString` function that needs to be + // removed, and it isn't fair to ask the user to know that. + if (#if haxe4 Syntax.code #else untyped __js__ #end ('typeof {0}.toString == "function"', this)) + { + Reflect.deleteField(this, "toString"); + } + } + + // Recurse. + for (child in Object.values(this)) + { + (child:Message).preserveClasses(); + } + #end + } + + /** + Restores the class information preserved by `preserveClasses()`. + + @param flag Leave this `null`. + **/ + private function restoreClasses(flag:Int = null):Void + { + #if !macro + // Attempt to choose a unique flag. + if (flag == null) + { + // JavaScript's limit is 2^53; Haxe 3's limit is much lower. + flag = Std.int(Math.random() * 0x7FFFFFFF); + if (Reflect.field(this, RESTORE_FIELD) == flag) + { + flag++; + } + } + + if (skip(this) || Reflect.field(this, RESTORE_FIELD) == flag) + { + return; + } + + try + { + Reflect.setField(this, RESTORE_FIELD, flag); + } + catch (e:Dynamic) + { + // Probably a frozen object; no need to continue. + return; + } + + // Restore this object's class. + if (Reflect.field(this, PROTOTYPE_FIELD) != null) + { + try + { + Object.setPrototypeOf(this, + #if haxe4 Syntax.code #else untyped __js__ #end + ("$hxClasses[{0}].prototype", Reflect.field(this, PROTOTYPE_FIELD))); + } + catch (e:Dynamic) {} + } + + // Recurse. + for (child in Object.values(this)) + { + (child:Message).restoreClasses(flag); + } + #end + } +} + +#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 lime.utils.ArrayBuffer from js.html.MessagePort from js.html.ImageBitmap #end +{ +} + +#if (!haxe4 && !macro) +@:native("Object") +extern class Object { + static function setPrototypeOf(obj:T, prototype:Null<{}>):T; + @:pure static function values(obj:{}):Array; + static var prototype(default, never):Dynamic; +} +#end diff --git a/src/lime/_internal/backend/native/NativeHTTPRequest.hx b/src/lime/_internal/backend/native/NativeHTTPRequest.hx index 9f7c65186..cc441b363 100644 --- a/src/lime/_internal/backend/native/NativeHTTPRequest.hx +++ b/src/lime/_internal/backend/native/NativeHTTPRequest.hx @@ -14,6 +14,7 @@ import lime.net.HTTPRequest; import lime.net.HTTPRequestHeader; import lime.net.HTTPRequestMethod; import lime.system.ThreadPool; +import lime.system.WorkOutput; #if sys #if haxe4 import sys.thread.Deque; @@ -282,13 +283,12 @@ class NativeHTTPRequest if (localThreadPool == null) { localThreadPool = new ThreadPool(0, 1); - localThreadPool.doWork.add(localThreadPool_doWork); localThreadPool.onProgress.add(localThreadPool_onProgress); localThreadPool.onComplete.add(localThreadPool_onComplete); localThreadPool.onError.add(localThreadPool_onError); } - localThreadPool.queue({instance: this, uri: uri}); + localThreadPool.run(localThreadPool_doWork, {instance: this, uri: uri}); } else { @@ -316,7 +316,6 @@ class NativeHTTPRequest if (multiThreadPool == null) { multiThreadPool = new ThreadPool(0, 1); - multiThreadPool.doWork.add(multiThreadPool_doWork); multiThreadPool.onProgress.add(multiThreadPool_onProgress); multiThreadPool.onComplete.add(multiThreadPool_onComplete); } @@ -324,7 +323,7 @@ class NativeHTTPRequest if (!multiThreadPoolRunning) { multiThreadPoolRunning = true; - multiThreadPool.queue(); + multiThreadPool.run(multiThreadPool_doWork, multi); } if (multiProgressTimer == null) @@ -399,7 +398,7 @@ class NativeHTTPRequest return output.length; } - private static function localThreadPool_doWork(state:Dynamic):Void + private static function localThreadPool_doWork(state:Dynamic, output:WorkOutput):Void { var instance:NativeHTTPRequest = state.instance; var path:String = state.uri; @@ -420,7 +419,7 @@ class NativeHTTPRequest if (path == null #if (sys && !android) || !FileSystem.exists(path) #end) { - localThreadPool.sendError({instance: instance, promise: instance.promise, error: "Cannot load file: " + path}); + output.sendError({instance: instance, promise: instance.promise, error: "Cannot load file: " + path}); } else { @@ -428,18 +427,18 @@ class NativeHTTPRequest if (instance.bytes != null) { - localThreadPool.sendProgress( + output.sendProgress( { instance: instance, promise: instance.promise, bytesLoaded: instance.bytes.length, bytesTotal: instance.bytes.length }); - localThreadPool.sendComplete({instance: instance, promise: instance.promise, result: instance.bytes}); + output.sendComplete({instance: instance, promise: instance.promise, result: instance.bytes}); } else { - localThreadPool.sendError({instance: instance, promise: instance.promise, error: "Cannot load file: " + path}); + output.sendError({instance: instance, promise: instance.promise, error: "Cannot load file: " + path}); } } } @@ -492,7 +491,7 @@ class NativeHTTPRequest promise.progress(state.bytesLoaded, state.bytesTotal); } - private static function multiThreadPool_doWork(_):Void + private static function multiThreadPool_doWork(multi:CURLMulti, output:WorkOutput):Void { while (true) { @@ -510,7 +509,7 @@ class NativeHTTPRequest if (message == null && multi.runningHandles == 0) { - multiThreadPool.sendComplete(); + output.sendComplete(); break; } @@ -525,7 +524,7 @@ class NativeHTTPRequest multi.removeHandle(curl); curl.cleanup(); - multiThreadPool.sendProgress({curl: curl, result: message.result, status: status}); + output.sendProgress({curl: curl, result: message.result, status: status}); message = multi.infoRead(); } } @@ -540,7 +539,7 @@ class NativeHTTPRequest if (curl != null) { multiAddHandle.push(curl); - multiThreadPool.queue(); + multiThreadPool.run(multiThreadPool_doWork, multi); } else { diff --git a/src/lime/app/Future.hx b/src/lime/app/Future.hx index 9a7fc9103..e00b56847 100644 --- a/src/lime/app/Future.hx +++ b/src/lime/app/Future.hx @@ -2,6 +2,7 @@ package lime.app; import lime.system.System; import lime.system.ThreadPool; +import lime.system.WorkOutput; import lime.utils.Log; /** @@ -66,34 +67,17 @@ import lime.utils.Log; @:noCompletion private var __progressListeners:ArrayInt->Void>; /** - Create a new `Future` instance - @param work (Optional) A function to execute - @param async (Optional) If a function is specified, whether to execute it asynchronously where supported + @param work Deprecated; use `Future.withEventualValue()` instead. + @param useThreads Deprecated; use `Future.withEventualValue()` instead. **/ - public function new(work:Void->T = null, async:Bool = false) + public function new(work:WorkFunctionT> = null, useThreads:Bool = false) { if (work != null) { - if (async) - { - var promise = new Promise(); - promise.future = this; + var promise = new Promise(); + promise.future = this; - FutureWork.queue({promise: promise, work: work}); - } - else - { - try - { - value = work(); - isComplete = true; - } - catch (e:Dynamic) - { - error = e; - isError = true; - } - } + FutureWork.run(dispatchWorkFunction, work, promise, useThreads ? MULTI_THREADED : SINGLE_THREADED, true); } } @@ -104,9 +88,9 @@ import lime.utils.Log; { var promise = new Promise(); - onComplete.add(function(data) promise.complete(data), true); - if (onError != null) onError.add(function(error) promise.error(error), true); - if (onProgress != null) onProgress.add(function(progress, total) promise.progress(progress, total), true); + onComplete.add(promise.complete, true); + if (onError != null) onError.add(promise.error, true); + if (onProgress != null) onProgress.add(promise.progress, true); return promise.future; } @@ -198,17 +182,6 @@ import lime.utils.Log; **/ public function ready(waitTime:Int = -1):Future { - #if js - if (isComplete || isError) - { - return this; - } - else - { - Log.warn("Cannot block thread in JavaScript"); - return this; - } - #else if (isComplete || isError) { return this; @@ -216,20 +189,34 @@ import lime.utils.Log; else { var time = System.getTimer(); + var prevTime = time; var end = time + waitTime; while (!isComplete && !isError && time <= end) { - #if sys - Sys.sleep(0.01); - #end + if (FutureWork.activeJobs < 1) + { + Log.error('Cannot block for a Future without a "work" function.'); + return this; + } + if (FutureWork.singleThreadPool != null && FutureWork.singleThreadPool.activeJobs > 0) + { + @:privateAccess FutureWork.singleThreadPool.__update(time - prevTime); + } + else + { + #if sys + Sys.sleep(0.01); + #end + } + + prevTime = time; time = System.getTimer(); } return this; } - #end } /** @@ -301,7 +288,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 @@ -311,50 +298,200 @@ import lime.utils.Log; future.value = value; return future; } + + /** + Creates a `Future` instance which will asynchronously compute a value. + + Once `work()` returns a non-null value, the `Future` will finish with that value. + If `work()` throws an error, the `Future` will finish with that error instead. + @param work A function that computes a value of type `T`. + @param state An argument to pass to `work()`. As this may be used on another thread, the + main thread must not access or modify `state` until the `Future` finishes. + @param mode Whether to use real threads as opposed to virtual threads. Virtual threads rely + on cooperative multitasking, meaning `work()` must return periodically to allow other code + enough time to run. In these cases, `work()` should return null to signal that it isn't finished. + @return A new `Future` instance. + @see https://en.wikipedia.org/wiki/Cooperative_multitasking + **/ + public static function withEventualValue(work:WorkFunction Null>, state:State, mode:ThreadMode = #if html5 SINGLE_THREADED #else MULTI_THREADED #end):Future + { + var future = new Future(); + var promise = new Promise(); + promise.future = future; + + FutureWork.run(work, state, promise, mode); + + return future; + } + + /** + (For backwards compatibility.) Dispatches the given zero-argument function. + **/ + @:noCompletion private static function dispatchWorkFunction(work:WorkFunction T>):Null + { + return work.dispatch(); + } } +/** + The class that handles asynchronous `work` functions passed to `new Future()`. +**/ #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end -@:dox(hide) private class FutureWork +@:dox(hide) class FutureWork { - private static var threadPool:ThreadPool; + @:allow(lime.app.Future) + private static var singleThreadPool:ThreadPool; + #if lime_threads + private static var multiThreadPool:ThreadPool; + // It isn't safe to pass a promise object to a web worker. + private static var promises:Map> = new Map(); + #end + public static var minThreads(default, set):Int = 0; + public static var maxThreads(default, set):Int = 1; + public static var activeJobs(get, never):Int; - public static function queue(state:Dynamic = null):Void + private static function getPool(mode:ThreadMode):ThreadPool { - if (threadPool == null) - { - threadPool = new ThreadPool(); - threadPool.doWork.add(threadPool_doWork); - threadPool.onComplete.add(threadPool_onComplete); - threadPool.onError.add(threadPool_onError); + #if lime_threads + if (mode == MULTI_THREADED) { + if(multiThreadPool == null) { + multiThreadPool = new ThreadPool(minThreads, maxThreads, MULTI_THREADED); + multiThreadPool.onComplete.add(multiThreadPool_onComplete); + multiThreadPool.onError.add(multiThreadPool_onError); + } + return multiThreadPool; } + #end + if(singleThreadPool == null) { + singleThreadPool = new ThreadPool(minThreads, maxThreads, SINGLE_THREADED); + singleThreadPool.onComplete.add(singleThreadPool_onComplete); + singleThreadPool.onError.add(singleThreadPool_onError); + } + return singleThreadPool; + } - threadPool.queue(state); + @:allow(lime.app.Future) + private static function run(work:WorkFunctionNull>, state:State, promise:Promise, mode:ThreadMode = MULTI_THREADED, legacyCode:Bool = false):Void + { + var bundle = {work: work, state: state, promise: promise, legacyCode: legacyCode}; + + #if lime_threads + if (mode == MULTI_THREADED) + { + #if html5 + work.makePortable(); + #end + + bundle.promise = null; + } + #end + + var jobID:Int = getPool(mode).run(threadPool_doWork, bundle); + + #if lime_threads + if (mode == MULTI_THREADED) + { + promises[jobID] = (cast promise:Promise); + } + #end } // Event Handlers - private static function threadPool_doWork(state:Dynamic):Void + private static function threadPool_doWork(bundle:{work:WorkFunctionDynamic>, state:State, legacyCode:Bool}, output:WorkOutput):Void { try { - var result = state.work(); - threadPool.sendComplete({promise: state.promise, result: result}); + var result = bundle.work.dispatch(bundle.state); + if (result != null || bundle.legacyCode) + { + #if (lime_threads && html5) + bundle.work.makePortable(); + #end + output.sendComplete(result); + } } catch (e:Dynamic) { - threadPool.sendError({promise: state.promise, error: e}); + #if (lime_threads && html5) + bundle.work.makePortable(); + #end + output.sendError(e); } } - private static function threadPool_onComplete(state:Dynamic):Void + private static function singleThreadPool_onComplete(result:Dynamic):Void { - state.promise.complete(state.result); + singleThreadPool.activeJob.state.promise.complete(result); } - private static function threadPool_onError(state:Dynamic):Void + private static function singleThreadPool_onError(error:Dynamic):Void { - state.promise.error(state.error); + singleThreadPool.activeJob.state.promise.error(error); + } + + #if lime_threads + private static function multiThreadPool_onComplete(result:Dynamic):Void + { + var promise:Promise = promises[multiThreadPool.activeJob.id]; + promises.remove(multiThreadPool.activeJob.id); + promise.complete(result); + } + + private static function multiThreadPool_onError(error:Dynamic):Void + { + var promise:Promise = promises[multiThreadPool.activeJob.id]; + promises.remove(multiThreadPool.activeJob.id); + promise.error(error); + } + #end + + // Getters & Setters + @:noCompletion private static inline function set_minThreads(value:Int):Int + { + if (singleThreadPool != null) + { + singleThreadPool.minThreads = value; + } + #if lime_threads + if (multiThreadPool != null) + { + multiThreadPool.minThreads = value; + } + #end + return minThreads = value; + } + + @:noCompletion private static inline function set_maxThreads(value:Int):Int + { + if (singleThreadPool != null) + { + singleThreadPool.maxThreads = value; + } + #if lime_threads + if (multiThreadPool != null) + { + multiThreadPool.maxThreads = value; + } + #end + return maxThreads = value; + } + + @:noCompletion private static function get_activeJobs():Int + { + var sum:Int = 0; + if (singleThreadPool != null) + { + sum += singleThreadPool.activeJobs; + } + #if lime_threads + if (multiThreadPool != null) + { + sum += multiThreadPool.activeJobs; + } + #end + return sum; } } 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/graphics/Image.hx b/src/lime/graphics/Image.hx index 04c74e6fe..741047f68 100644 --- a/src/lime/graphics/Image.hx +++ b/src/lime/graphics/Image.hx @@ -994,7 +994,7 @@ class Image return promise.future; #else - return new Future(function() return fromBytes(bytes), true); + return Future.withEventualValue(fromBytes, bytes, MULTI_THREADED); #end } diff --git a/src/lime/media/AudioBuffer.hx b/src/lime/media/AudioBuffer.hx index 2c76a9d87..ff067229a 100644 --- a/src/lime/media/AudioBuffer.hx +++ b/src/lime/media/AudioBuffer.hx @@ -309,9 +309,9 @@ class AudioBuffer public static function loadFromFiles(paths:Array):Future { + #if (js && html5 && lime_howlerjs) var promise = new Promise(); - #if (js && html5 && lime_howlerjs) var audioBuffer = AudioBuffer.fromFiles(paths); if (audioBuffer != null) @@ -332,11 +332,11 @@ class AudioBuffer { promise.error(null); } - #else - promise.completeWith(new Future(function() return fromFiles(paths), true)); - #end return promise.future; + #else + return Future.withEventualValue(fromFiles, paths, MULTI_THREADED); + #end } private static function __getCodec(bytes:Bytes):String diff --git a/src/lime/system/BackgroundWorker.hx b/src/lime/system/BackgroundWorker.hx index 2fe928fdb..f9d922e7c 100644 --- a/src/lime/system/BackgroundWorker.hx +++ b/src/lime/system/BackgroundWorker.hx @@ -1,170 +1,3 @@ package lime.system; -import lime.app.Application; -import lime.app.Event; -#if sys -#if haxe4 -import sys.thread.Deque; -import sys.thread.Thread; -#elseif cpp -import cpp.vm.Deque; -import cpp.vm.Thread; -#elseif neko -import neko.vm.Deque; -import neko.vm.Thread; -#end -#end -#if !lime_debug -@:fileXml('tags="haxe,release"') -@:noDebug -#end -class BackgroundWorker -{ - private static var MESSAGE_COMPLETE = "__COMPLETE__"; - private static var MESSAGE_ERROR = "__ERROR__"; - - public var canceled(default, null):Bool; - public var completed(default, null):Bool; - public var doWork = new EventVoid>(); - public var onComplete = new EventVoid>(); - public var onError = new EventVoid>(); - public var onProgress = new EventVoid>(); - - @:noCompletion private var __runMessage:Dynamic; - #if (cpp || neko) - @:noCompletion private var __messageQueue:Deque; - @:noCompletion private var __workerThread:Thread; - #end - - public function new() {} - - public function cancel():Void - { - canceled = true; - - #if (cpp || neko) - __workerThread = null; - #end - } - - public function run(message:Dynamic = null):Void - { - canceled = false; - completed = false; - __runMessage = message; - - #if (cpp || neko) - __messageQueue = new Deque(); - __workerThread = Thread.create(__doWork); - - // TODO: Better way to do this - - if (Application.current != null) - { - Application.current.onUpdate.add(__update); - } - #else - __doWork(); - #end - } - - public function sendComplete(message:Dynamic = null):Void - { - completed = true; - - #if (cpp || neko) - __messageQueue.add(MESSAGE_COMPLETE); - __messageQueue.add(message); - #else - if (!canceled) - { - canceled = true; - onComplete.dispatch(message); - } - #end - } - - public function sendError(message:Dynamic = null):Void - { - #if (cpp || neko) - __messageQueue.add(MESSAGE_ERROR); - __messageQueue.add(message); - #else - if (!canceled) - { - canceled = true; - onError.dispatch(message); - } - #end - } - - public function sendProgress(message:Dynamic = null):Void - { - #if (cpp || neko) - __messageQueue.add(message); - #else - if (!canceled) - { - onProgress.dispatch(message); - } - #end - } - - @:noCompletion private function __doWork():Void - { - doWork.dispatch(__runMessage); - - // #if (cpp || neko) - // - // __messageQueue.add (MESSAGE_COMPLETE); - // - // #else - // - // if (!canceled) { - // - // canceled = true; - // onComplete.dispatch (null); - // - // } - // - // #end - } - - @:noCompletion private function __update(deltaTime:Int):Void - { - #if (cpp || neko) - var message = __messageQueue.pop(false); - - if (message != null) - { - if (message == MESSAGE_ERROR) - { - Application.current.onUpdate.remove(__update); - - if (!canceled) - { - canceled = true; - onError.dispatch(__messageQueue.pop(false)); - } - } - else if (message == MESSAGE_COMPLETE) - { - Application.current.onUpdate.remove(__update); - - if (!canceled) - { - canceled = true; - onComplete.dispatch(__messageQueue.pop(false)); - } - } - else - { - if (!canceled) - { - onProgress.dispatch(message); - } - } - } - #end - } -} +typedef BackgroundWorker = ThreadPool; diff --git a/src/lime/system/ThreadPool.hx b/src/lime/system/ThreadPool.hx index 479ad17b5..cafaaa45d 100644 --- a/src/lime/system/ThreadPool.hx +++ b/src/lime/system/ThreadPool.hx @@ -1,244 +1,718 @@ package lime.system; -import haxe.Constraints.Function; import lime.app.Application; import lime.app.Event; -#if sys -#if haxe4 -import sys.thread.Deque; +import lime.system.WorkOutput; +import lime.utils.Log; +#if target.threaded import sys.thread.Thread; #elseif cpp -import cpp.vm.Deque; import cpp.vm.Thread; #elseif neko -import neko.vm.Deque; import neko.vm.Thread; +#elseif html5 +import lime._internal.backend.html5.HTML5Thread as Thread; #end -#end +/** + A simple and thread-safe way to run a one or more asynchronous jobs. It + manages a queue of jobs, starting new ones once the old ones are done. + + It can also keep a certain number of threads (configurable via `minThreads`) + running in the background even when no jobs are available. This avoids the + not-insignificant overhead of stopping and restarting threads. + + Sample usage: + + var threadPool:ThreadPool = new ThreadPool(); + threadPool.onComplete.add(onFileProcessed); + + threadPool.maxThreads = 3; + for(url in urls) + { + threadPool.run(processFile, url); + } + + For thread safety, the worker function should only give output through the + `WorkOutput` object it receives. Calling `output.sendComplete()` will + trigger an `onComplete` event on the main thread. + + @see `lime.system.WorkOutput.WorkFunction` for important information about + `doWork`. + @see https://player03.com/openfl/threads-guide/ for a tutorial. +**/ #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end -class ThreadPool +class ThreadPool extends WorkOutput { - public var currentThreads(default, null):Int; - public var doWork = new EventVoid>(); - public var maxThreads:Int; - public var minThreads:Int; - public var onComplete = new EventVoid>(); - public var onError = new EventVoid>(); - public var onProgress = new EventVoid>(); - public var onRun = new EventVoid>(); + #if (haxe4 && lime_threads) + /** + A thread or null value to be compared against `Thread.current()`. Don't + do anything with this other than check for equality. - #if (cpp || neko) - @:noCompletion private var __synchronous:Bool; - @:noCompletion private var __workCompleted:Int; - @:noCompletion private var __workIncoming = new Deque(); - @:noCompletion private var __workQueued:Int; - @:noCompletion private var __workResult = new Deque(); + Unavailable in Haxe 3 as thread equality checking doesn't work there. + **/ + private static var __mainThread:Thread = + #if html5 + !Thread.current().isWorker() ? Thread.current() : null; + #else + Thread.current(); + #end #end - public function new(minThreads:Int = 0, maxThreads:Int = 1) + /** + Indicates that no further events will be dispatched. + **/ + public var canceled(default, null):Bool = false; + + /** + Indicates that the latest job finished successfully, and no other job + has been started/is ongoing. + **/ + public var completed(default, null):Bool = false; + + /** + The number of live threads in this pool, including both active and idle + threads. Does not count threads that have been instructed to shut down. + + In single-threaded mode, this will equal `activeJobs`. + **/ + public var currentThreads(get, never):Int; + + /** + The number of jobs actively being executed. + **/ + public var activeJobs(get, never):Int; + + /** + The number of live threads in this pool that aren't currently working on + anything. In single-threaded mode, this will always be 0. + **/ + public var idleThreads(get, never):Int; + + /** + __Set this only from the main thread.__ + + The maximum number of live threads this pool can have at once. If this + value decreases, active jobs will still be allowed to finish. + + You can set this in single-threaded mode, but it's rarely useful. For + instance, suppose you have six jobs, each of which takes about a second. + If you leave `maxThreads` at 1, then one will finish every second for + six seconds. If you set `maxThreads = 6`, then none will finish for five + seconds, and then they'll all finish at once. The total duration is + unchanged, but none of them finish early. + **/ + public var maxThreads:Int; + + /** + __Set this only from the main thread.__ + + The number of threads that will be kept alive at all times, even if + there's no work to do. Setting this won't add new threads, it'll just + keep existing ones running. + + Has no effect in single-threaded mode. + **/ + public var minThreads:Int; + + /** + Dispatched on the main thread when `doWork` calls `sendComplete()`. + Dispatched at most once per job. + **/ + public var onComplete(default, null) = new EventVoid>(); + /** + Dispatched on the main thread when `doWork` calls `sendError()`. + Dispatched at most once per job. + **/ + public var onError(default, null) = new EventVoid>(); + /** + Dispatched on the main thread when `doWork` calls `sendProgress()`. May + be dispatched any number of times per job. + **/ + public var onProgress(default, null) = new EventVoid>(); + /** + Dispatched on the main thread when a new job begins. Dispatched exactly + once per job. + **/ + public var onRun(default, null) = new EventVoid>(); + + @:deprecated("Instead pass the callback to ThreadPool.run().") + @:noCompletion @:dox(hide) public var doWork(get, never):{ add: (Dynamic->Void)->Void }; + private var __doWork:WorkFunctionWorkOutput->Void>; + + private var __activeJobs:JobList = new JobList(); + + #if lime_threads + /** + The set of threads actively running a job. + **/ + private var __activeThreads:Map = new Map(); + + /** + A list of idle threads. Not to be confused with `idleThreads`, a public + variable equal to `__idleThreads.length`. + **/ + private var __idleThreads:List = new List(); + #end + + private var __jobQueue:JobList = new JobList(); + + private var __workPerFrame:Float; + + /** + __Call this only from the main thread.__ + + @param doWork A single function capable of performing all of this pool's + jobs. Always provide `doWork`, even though it's marked as optional. + @param mode Defaults to `MULTI_THREADED` on most targets, but + `SINGLE_THREADED` in HTML5. In HTML5, `MULTI_THREADED` mode uses web + workers, which impose additional restrictions. + @param workLoad (Single-threaded mode only) A rough estimate of how much + of the app's time should be spent on this `ThreadPool`. For instance, + the default value of 1/2 means this worker will take up about half the + app's available time every frame. See `workIterations` for instructions + to improve the accuracy of this estimate. + **/ + public function new(minThreads:Int = 0, maxThreads:Int = 1, mode:ThreadMode = null, workLoad:Float = 1/2) { + super(mode); + + __workPerFrame = workLoad / Application.current.window.frameRate; + this.minThreads = minThreads; this.maxThreads = maxThreads; - - currentThreads = 0; - - #if (cpp || neko) - __workQueued = 0; - __workCompleted = 0; - #end - - #if (emscripten || force_synchronous) - __synchronous = true; - #end } - // public function cancel (id:String):Void { - // - // - // - // } - // public function isCanceled (id:String):Bool { - // - // - // - // } - public function queue(state:Dynamic = null):Void + /** + Cancels all active and queued jobs. In multi-threaded mode, leaves + `minThreads` idle threads running. + @param error If not null, this error will be dispatched for each active + or queued job. + **/ + public function cancel(error:Dynamic = null):Void { - #if (cpp || neko) - // TODO: Better way to handle this? - - if (Application.current != null && Application.current.window != null && !__synchronous) + #if (haxe4 && lime_threads) + if (Thread.current() != __mainThread) { - __workIncoming.add(new ThreadPoolMessage(WORK, state)); - __workQueued++; + throw "Call cancel() only from the main thread."; + } + #end - if (currentThreads < maxThreads && currentThreads < (__workQueued - __workCompleted)) + Application.current.onUpdate.remove(__update); + + // Cancel active jobs, leaving `minThreads` idle threads. + for (job in __activeJobs) + { + #if lime_threads + if (mode == MULTI_THREADED) { - currentThreads++; - Thread.create(__doWork); - } - - if (!Application.current.onUpdate.has(__update)) - { - Application.current.onUpdate.add(__update); - } - } - else - { - __synchronous = true; - runWork(state); - } - #else - runWork(state); - #end - } - - public function sendComplete(state:Dynamic = null):Void - { - #if (cpp || neko) - if (!__synchronous) - { - __workResult.add(new ThreadPoolMessage(COMPLETE, state)); - return; - } - #end - - onComplete.dispatch(state); - } - - public function sendError(state:Dynamic = null):Void - { - #if (cpp || neko) - if (!__synchronous) - { - __workResult.add(new ThreadPoolMessage(ERROR, state)); - return; - } - #end - - onError.dispatch(state); - } - - public function sendProgress(state:Dynamic = null):Void - { - #if (cpp || neko) - if (!__synchronous) - { - __workResult.add(new ThreadPoolMessage(PROGRESS, state)); - return; - } - #end - - onProgress.dispatch(state); - } - - @:noCompletion private function runWork(state:Dynamic = null):Void - { - #if (cpp || neko) - if (!__synchronous) - { - __workResult.add(new ThreadPoolMessage(WORK, state)); - doWork.dispatch(state); - return; - } - #end - - onRun.dispatch(state); - doWork.dispatch(state); - } - - #if (cpp || neko) - @:noCompletion private function __doWork():Void - { - while (true) - { - var message = __workIncoming.pop(true); - - if (message.type == WORK) - { - runWork(message.state); - } - else if (message.type == EXIT) - { - break; - } - } - } - - @:noCompletion private function __update(deltaTime:Int):Void - { - if (__workQueued > __workCompleted) - { - var message = __workResult.pop(false); - - while (message != null) - { - switch (message.type) + var thread:Thread = __activeThreads[job.id]; + if (idleThreads < minThreads) { - case WORK: - onRun.dispatch(message.state); + thread.sendMessage(new ThreadEvent(WORK, null, null)); + __idleThreads.push(thread); + } + else + { + thread.sendMessage(new ThreadEvent(EXIT, null, null)); + } + } + #end - case PROGRESS: - onProgress.dispatch(message.state); + if (error != null) + { + if (job.duration == 0) + { + job.duration = timestamp() - job.startTime; + } - case COMPLETE, ERROR: - __workCompleted++; + activeJob = job; + onError.dispatch(error); + activeJob = null; + } + } + __activeJobs.clear(); - if ((currentThreads > (__workQueued - __workCompleted) && currentThreads > minThreads) - || currentThreads > maxThreads) + #if lime_threads + // Cancel idle threads if there are more than the minimum. + while (idleThreads > minThreads) + { + __idleThreads.pop().sendMessage(new ThreadEvent(EXIT, null, null)); + } + #end + + // Clear the job queue. + if (error != null) + { + for (job in __jobQueue) + { + activeJob = job; + onError.dispatch(error); + } + } + __jobQueue.clear(); + + __jobComplete.value = false; + activeJob = null; + completed = false; + canceled = true; + } + + /** + Cancels one active or queued job. Does not dispatch an error event. + @param job A `JobData` object, or a job's unique `id`, `state`, or + `doWork` function. + @return Whether a job was canceled. + **/ + public function cancelJob(job:JobIdentifier):Bool + { + var data:JobData = __activeJobs.get(job); + + if (data != null) + { + #if lime_threads + var thread:Thread = __activeThreads[data.id]; + if (thread != null) + { + thread.sendMessage(new ThreadEvent(WORK, null, null)); + __activeThreads.remove(data.id); + __idleThreads.push(thread); + } + #end + + return __activeJobs.remove(data); + } + + return __jobQueue.remove(__jobQueue.get(job)); + } + + /** + Alias for `ThreadPool.run()`. + **/ + @:noCompletion public inline function queue(doWork:WorkFunctionWorkOutput->Void> = null, state:State = null):Int + { + return run(doWork, state); + } + + /** + Queues a new job, to be run once a thread becomes available. + @return The job's unique ID. + **/ + public function run(doWork:WorkFunctionWorkOutput->Void> = null, state:State = null):Int + { + #if (haxe4 && lime_threads) + if (Thread.current() != __mainThread) + { + throw "Call run() only from the main thread."; + } + #end + + if (doWork == null) + { + if (__doWork == null) + { + throw "run() requires doWork argument."; + } + else + { + doWork = __doWork; + } + } + + if (state == null) + { + state = {}; + } + + var job:JobData = new JobData(doWork, state); + __jobQueue.add(job); + completed = false; + canceled = false; + + if (!Application.current.onUpdate.has(__update)) + { + Application.current.onUpdate.add(__update); + } + + return job.id; + } + + #if lime_threads + /** + __Run this only on a background thread.__ + + Retrieves jobs using `Thread.readMessage()`, runs them until complete, + and repeats. + + On all targets besides HTML5, the first message must be a `WorkOutput`. + **/ + private static function __executeThread():Void + { + JSAsync.async({ + var output:WorkOutput = #if html5 new WorkOutput(MULTI_THREADED) #else cast(Thread.readMessage(true), WorkOutput) #end; + var event:ThreadEvent = null; + + while (true) + { + // Get a job. + if (event == null) + { + do + { + event = Thread.readMessage(true); + } + while (!#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (event, ThreadEvent)); + + output.resetJobProgress(); + } + + if (event.event == EXIT) + { + // Quit working. + #if html5 + Thread.current().destroy(); + #end + return; + } + + if (event.event != WORK || event.job == null) + { + // Go idle. + event = null; + continue; + } + + // Get to work. + output.activeJob = event.job; + + var interruption:Dynamic = null; + try + { + while (!output.__jobComplete.value && (interruption = Thread.readMessage(false)) == null) + { + output.workIterations.value++; + event.job.doWork.dispatch(event.job.state, output); + } + } + catch (e:Dynamic) + { + output.sendError(e); + } + + output.activeJob = null; + + if (interruption == null || output.__jobComplete.value) + { + // Work is done; wait for more. + event = null; + } + else if(#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (interruption, ThreadEvent)) + { + // Work on the new job. + event = interruption; + output.resetJobProgress(); + } + else + { + // Ignore interruption and keep working. + } + + // Do it all again. + } + }); + } + #end + + private static inline function timestamp():Float + { + #if sys + return Sys.cpuTime(); + #else + return haxe.Timer.stamp(); + #end + } + + /** + Schedules (in multi-threaded mode) or runs (in single-threaded mode) the + job queue, then processes incoming events. + **/ + private function __update(deltaTime:Int):Void + { + #if (haxe4 && lime_threads) + if (Thread.current() != __mainThread) + { + return; + } + #end + + // Process the queue. + while (!__jobQueue.isEmpty() && activeJobs < maxThreads) + { + var job:JobData = __jobQueue.pop(); + + job.startTime = timestamp(); + __activeJobs.push(job); + + #if lime_threads + if (mode == MULTI_THREADED) + { + #if html5 + job.doWork.makePortable(); + #end + + var thread:Thread = __idleThreads.isEmpty() ? createThread(__executeThread) : __idleThreads.pop(); + __activeThreads[job.id] = thread; + thread.sendMessage(new ThreadEvent(WORK, null, job)); + } + #end + } + + // Run the next single-threaded job. + if (mode == SINGLE_THREADED && activeJobs > 0) + { + activeJob = __activeJobs.pop(); + var state:State = activeJob.state; + + __jobComplete.value = false; + workIterations.value = 0; + + var startTime:Float = timestamp(); + var timeElapsed:Float = 0; + try + { + do + { + workIterations.value++; + activeJob.doWork.dispatch(state, this); + timeElapsed = timestamp() - startTime; + } + while (!__jobComplete.value && timeElapsed < __workPerFrame); + } + catch (e:Dynamic) + { + sendError(e); + } + + activeJob.duration += timeElapsed; + + // Add this job to the end of the list, to cycle through. + __activeJobs.add(activeJob); + + activeJob = null; + } + + var threadEvent:ThreadEvent; + while ((threadEvent = __jobOutput.pop(false)) != null) + { + if (!__activeJobs.exists(threadEvent.job)) + { + // Ignore events from canceled jobs. + continue; + } + + // Get by ID because in HTML5, the object will have been cloned, + // which will interfere with attempts to test equality. + activeJob = __activeJobs.getByID(threadEvent.job.id); + + if (mode == MULTI_THREADED) + { + activeJob.duration = timestamp() - activeJob.startTime; + } + + switch (threadEvent.event) + { + case WORK: + onRun.dispatch(threadEvent.message); + + case PROGRESS: + onProgress.dispatch(threadEvent.message); + + case COMPLETE, ERROR: + if (threadEvent.event == COMPLETE) + { + onComplete.dispatch(threadEvent.message); + } + else + { + onError.dispatch(threadEvent.message); + } + + __activeJobs.remove(activeJob); + + #if lime_threads + if (mode == MULTI_THREADED) + { + var thread:Thread = __activeThreads[activeJob.id]; + __activeThreads.remove(activeJob.id); + + if (currentThreads > maxThreads || __jobQueue.isEmpty() && currentThreads > minThreads) { - currentThreads--; - __workIncoming.add(new ThreadPoolMessage(EXIT, null)); - } - - if (message.type == COMPLETE) - { - onComplete.dispatch(message.state); + thread.sendMessage(new ThreadEvent(EXIT, null, null)); } else { - onError.dispatch(message.state); + __idleThreads.push(thread); } + } + #end - default: - } + completed = threadEvent.event == COMPLETE && activeJobs == 0 && __jobQueue.isEmpty(); - message = __workResult.pop(false); + default: } + + activeJob = null; } - else + + if (completed) { - // TODO: Add sleep if keeping minThreads running with no work? - - if (currentThreads == 0 && minThreads <= 0 && Application.current != null) - { - Application.current.onUpdate.remove(__update); - } + 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 + + return thread; + } #end -} -private enum ThreadPoolMessageType -{ - COMPLETE; - ERROR; - EXIT; - PROGRESS; - WORK; -} + // Getters & Setters -private class ThreadPoolMessage -{ - public var state:Dynamic; - public var type:ThreadPoolMessageType; - - public function new(type:ThreadPoolMessageType, state:Dynamic) + private inline function get_activeJobs():Int { - this.type = type; - this.state = state; + return __activeJobs.length; + } + + private inline function get_idleThreads():Int + { + return #if lime_threads __idleThreads.length #else 0 #end; + } + + private inline function get_currentThreads():Int + { + return activeJobs + idleThreads; + } + + // 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) + { + #if html5 + if (mode == MULTI_THREADED) + throw "Unsupported operation; instead pass the callback to ThreadPool's constructor."; + #end + __doWork = #if (lime_threads && html5) { func: #end + function(state:State, output:WorkOutput):Void + { + callback(state); + } + #if (lime_threads && html5) } #end; + } + }; } } + +@:forward +abstract JobList(List) +{ + public inline function new() + { + this = new List(); + } + + public inline function exists(job:JobData):Bool + { + return getByID(job.id) != null; + } + + public inline function remove(job:JobData):Bool + { + return this.remove(job) || removeByID(job.id); + } + + public inline function removeByID(id:Int):Bool + { + return this.remove(getByID(id)); + } + + public function getByID(id:Int):JobData + { + for (job in this) + { + if (job.id == id) + { + return job; + } + } + return null; + } + + public function get(jobIdentifier:JobIdentifier):JobData + { + switch (jobIdentifier) + { + case ID(id): + return getByID(id); + case FUNCTION(doWork): + for (job in this) + { + if (job.doWork == doWork) + { + return job; + } + } + case STATE(state): + for (job in this) + { + if (job.state == state) + { + return job; + } + } + } + return null; + } +} + +/** + A piece of data that uniquely represents a job. This can be the integer ID + (and integers will be assumed to be such), the `doWork` function, or the + `JobData` object itself. Failing any of those, a value will be assumed to be + the job's `state`. + + Caution: if the provided data isn't unique, such as a `doWork` function + that's in use by multiple jobs, the wrong job may be selected or canceled. +**/ +@:forward +abstract JobIdentifier(JobIdentifierImpl) from JobIdentifierImpl { + @:from private static inline function fromJob(job:JobData):JobIdentifier { + return ID(job.id); + } + @:from private static inline function fromID(id:Int):JobIdentifier { + return ID(id); + } + @:from private static inline function fromFunction(doWork:WorkFunctionWorkOutput->Void>):JobIdentifier { + return FUNCTION(doWork); + } + @:from private static inline function fromState(state:State):JobIdentifier { + return STATE(state); + } +} + +private enum JobIdentifierImpl +{ + ID(id:Int); + FUNCTION(doWork:WorkFunctionWorkOutput->Void>); + STATE(state:State); +} diff --git a/src/lime/system/WorkOutput.hx b/src/lime/system/WorkOutput.hx new file mode 100644 index 000000000..4205068fa --- /dev/null +++ b/src/lime/system/WorkOutput.hx @@ -0,0 +1,434 @@ +package lime.system; + +#if target.threaded +import sys.thread.Deque; +import sys.thread.Thread; +import sys.thread.Tls; +#elseif cpp +import cpp.vm.Deque; +import cpp.vm.Thread; +import cpp.vm.Tls; +#elseif neko +import neko.vm.Deque; +import neko.vm.Thread; +import neko.vm.Tls; +#end + +#if html5 +import lime._internal.backend.html5.HTML5Thread as Thread; +import lime._internal.backend.html5.HTML5Thread.Transferable; +#end + +#if macro +import haxe.macro.Expr; + +using haxe.macro.Context; +#end + +// In addition to `WorkOutput`, this module contains a number of small enums, +// abstracts, and classes used by all of Lime's threading classes. + +/** + Functions and variables available to the `doWork` function. For instance, + the `sendProgress()`, `sendComplete()`, and `sendError()` functions allow + returning output. + + `doWork` should exclusively use `WorkOutput` to communicate with the main + thread. On many targets it's also possible to access static or instance + variables, but this isn't thread safe and won't work in HTML5. +**/ +class WorkOutput +{ + /** + Thread-local storage. Tracks how many times `doWork` has been called for + the current job, including (if applicable) the ongoing call. + + In single-threaded mode, it only counts the number of calls this frame. + This helps you adjust `doWork`'s length: too few iterations per frame + means `workLoad` may be inaccurate, while too many may add overhead. + **/ + public var workIterations(default, null):Tls = new Tls(); + + /** + Whether background threads are being/will be used. If threads aren't + available on this target, `mode` will always be `SINGLE_THREADED`. + **/ + public var mode(get, never):ThreadMode; + #if lime_threads + /** + __Set this only via the constructor.__ + **/ + private var __mode:ThreadMode; + #end + + /** + Messages sent by active jobs, received by the main thread. + **/ + private var __jobOutput:Deque = new Deque(); + /** + Thread-local storage. Tracks whether `sendError()` or `sendComplete()` + was called by this job. + **/ + private var __jobComplete:Tls = new Tls(); + + /** + The job that is currently running on this thread, or the job that + triggered the ongoing `onComplete`, `onError`, or `onProgress` event. + Will be null in all other cases. + **/ + public var activeJob(get, set):Null; + @:noCompletion private var __activeJob:Tls = new Tls(); + + private inline function new(mode:Null) + { + workIterations.value = 0; + __jobComplete.value = false; + + #if lime_threads + __mode = mode != null ? mode : #if html5 SINGLE_THREADED #else MULTI_THREADED #end; + #end + } + + /** + Dispatches `onComplete` on the main thread, with the given message. + `doWork` should return after calling this. + + If using web workers, you can also pass a list of transferable objects. + @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects + **/ + public function sendComplete(message:Dynamic = null, transferList:Array = null):Void + { + if (!__jobComplete.value) + { + __jobComplete.value = true; + + #if (lime_threads && html5) + if (mode == MULTI_THREADED) + { + activeJob.doWork.makePortable(); + Thread.returnMessage(new ThreadEvent(COMPLETE, message, activeJob), transferList); + } + else + #end + __jobOutput.add(new ThreadEvent(COMPLETE, message, activeJob)); + } + } + + /** + Dispatches `onError` on the main thread, with the given message. + `doWork` should return after calling this. + + If using web workers, you can also pass a list of transferable objects. + @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects + **/ + public function sendError(message:Dynamic = null, transferList:Array = null):Void + { + if (!__jobComplete.value) + { + __jobComplete.value = true; + + #if (lime_threads && html5) + if (mode == MULTI_THREADED) + { + activeJob.doWork.makePortable(); + Thread.returnMessage(new ThreadEvent(ERROR, message, activeJob), transferList); + } + else + #end + __jobOutput.add(new ThreadEvent(ERROR, message, activeJob)); + } + } + + /** + Dispatches `onProgress` on the main thread, with the given message. This + can be called any number of times per job. + + If using web workers, you can also pass a list of transferable objects. + @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects + **/ + public function sendProgress(message:Dynamic = null, transferList:Array = null):Void + { + if (!__jobComplete.value) + { + #if (lime_threads && html5) + if (mode == MULTI_THREADED) + { + activeJob.doWork.makePortable(); + Thread.returnMessage(new ThreadEvent(PROGRESS, message, activeJob), transferList); + } + else + #end + __jobOutput.add(new ThreadEvent(PROGRESS, message, activeJob)); + } + } + + private inline function resetJobProgress():Void + { + __jobComplete.value = false; + workIterations.value = 0; + } + + #if lime_threads + private function createThread(executeThread:WorkFunctionVoid>):Thread + { + var thread:Thread = Thread.create(executeThread); + + #if html5 + thread.onMessage.add(function(event:ThreadEvent) { + __jobOutput.add(event); + }); + #end + + return thread; + } + #end + + // Getters & Setters + + private inline function get_mode():ThreadMode + { + #if lime_threads + return __mode; + #else + return SINGLE_THREADED; + #end + } + + private inline function get_activeJob():JobData + { + return __activeJob.value; + } + private inline function set_activeJob(value:JobData):JobData + { + return __activeJob.value = value; + } +} + +@:enum abstract ThreadMode(Bool) +{ + /** + All work will be done on the main thread, during `Application.onUpdate`. + + To avoid lag spikes, `doWork` should return after completing a fraction + of a frame's worth of work, storing its progress in `state`. It will be + called again with the same `state` next frame, or this frame if there's + still time. + + @see https://en.wikipedia.org/wiki/Green_threads + @see https://en.wikipedia.org/wiki/Cooperative_multitasking + **/ + var SINGLE_THREADED = false; + + /** + All work will be done on a background thread. + + Unlike single-threaded mode, there is no risk of causing lag spikes. + Even so, `doWork` should return periodically, to allow canceling the + thread. If not canceled, `doWork` will be called again immediately. + + In HTML5, web workers will be used to achieve this. This means `doWork` + must be a static function, and you can't use `bind()`. Web workers also + impose a longer delay each time `doWork` returns, so it shouldn't return + as often in multi-threaded mode as in single-threaded mode. + **/ + var MULTI_THREADED = true; +} + +/** + A function that performs asynchronous work. This can either be work on + another thread ("multi-threaded mode"), or it can represent a virtual + thread ("single-threaded mode"). + + In single-threaded mode, the work function shouldn't complete the job all at + once, as the main thread would lock up. Instead, it should perform a + fraction of the job each time it's called. `ThreadPool` provides the + function with a persistent `State` argument that can track progress. + Alternatively, you may be able to bind your own `State` argument. + + Caution: if using multi-threaded mode in HTML5, this must be a static + function and binding arguments is forbidden. Compile with + `-Dlime-warn-portability` to highlight functions that won't work. + + The exact length of `doWork` can vary, but single-threaded mode will run + more smoothly if it's short enough to run several times per frame. +**/ +#if (lime_threads && html5) +typedef WorkFunction = lime._internal.backend.html5.HTML5Thread.WorkFunction; +#else +abstract WorkFunction(T) from T to T +{ + /** + Executes this function with the given arguments. + **/ + public macro function dispatch(self:Expr, args:Array):Expr + { + switch (self.typeof().follow().toComplexType()) + { + case TPath({ sub: "WorkFunction", params: [TPType(t)] }): + return macro ($self:$t)($a{args}); + default: + throw "Underlying function type not found."; + } + } +} +#end + +/** + An argument of any type to pass to the `doWork` function. Though `doWork` + only accepts a single argument, you can pass multiple values as part of an + anonymous structure. (Or an array, or a class.) + + // Does not work: too many arguments. + // threadPool.run(doWork, argument0, argument1, argument2); + + // Works: all arguments are combined into one `State` object. + threadPool.run(doWork, { arg0: argument0, arg1: argument1, arg2: argument2 }); + + // Alternatives that also work, if everything is the correct type. + threadPool.run(doWork, [argument0, argument1, argument2]); + threadPool.run(doWork, new DoWorkArgs(argument0, argument1, argument2)); + + Any changes made to this object will persist if and when `doWork` is called + again for the same job. (See `WorkFunction` for instructions on how to do + this.) This is the recommended way to store `doWork`'s progress. + + Caution: after passing an object to `doWork`, avoid accessing or modifying + that object from the main thread, and avoid passing it to other threads. + Doing either may lead to race conditions. If you need to store an object, + pass a clone of that object to `doWork`. +**/ +typedef State = Dynamic; + +class JobData +{ + private static var nextID:Int = 0; + /** + `JobData` instances will regularly be copied in HTML5, so checking + equality won't work. Instead, compare identifiers. + **/ + public var id(default, null):Int; + + /** + The function responsible for carrying out the job. + **/ + public var doWork(default, null):WorkFunctionWorkOutput->Void>; + + /** + The original `State` object passed to the job. Avoid modifying this + object if the job is running in multi-threaded mode. + **/ + public var state(default, null):State; + + /** + The total time spent on this job. + + In multi-threaded mode, this includes the overhead for sending messages, + plus any time spent waiting for a canceled job to return. The latter + delay can be reduced by returning at regular intervals. + **/ + @:allow(lime.system.WorkOutput) + public var duration(default, null):Float = 0; + + @:allow(lime.system.WorkOutput) + private var startTime:Float = 0; + + @:allow(lime.system.WorkOutput) + private inline function new(doWork:WorkFunctionWorkOutput->Void>, state:State) + { + id = nextID++; + this.doWork = doWork; + this.state = state; + } +} + +@:enum abstract ThreadEventType(String) +{ + /** + Sent by the background thread, indicating completion. + **/ + var COMPLETE = "COMPLETE"; + /** + Sent by the background thread, indicating failure. + **/ + var ERROR = "ERROR"; + /** + Sent by the background thread. + **/ + var PROGRESS = "PROGRESS"; + /** + Sent by the main thread, indicating that the provided job should begin + in place of any ongoing job. If `state == null`, the existing job will + stop and the thread will go idle. (To run a job with no argument, set + `state = {}` instead.) + **/ + var WORK = "WORK"; + /** + Sent by the main thread to shut down a thread. + **/ + var EXIT = "EXIT"; +} + +class ThreadEvent +{ + public var event(default, null):ThreadEventType; + public var message(default, null):State; + public var job(default, null):JobData; + + public inline function new(event:ThreadEventType, message:State, job:JobData) + { + this.event = event; + this.message = message; + this.job = job; + } +} + +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; + } + } +} + +// Define platform-specific types + +#if target.threaded +// Haxe 3 compatibility: "target.threaded" can't go in parentheses. +#elseif !(cpp || neko) +@:forward(push, add) +abstract Deque(List) from List to List +{ + public inline function new() + { + this = new List(); + } + + public inline function pop(block:Bool):Null + { + return this.pop(); + } +} + +class Tls +{ + public var value:T; + + public inline function new() {} +} +#end + +#if !html5 +typedef Transferable = Dynamic; +#end 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 3ffeadf62..a4dd4ce85 100644 --- a/src/lime/tools/ProjectXMLParser.hx +++ b/src/lime/tools/ProjectXMLParser.hx @@ -1757,6 +1757,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/src/lime/ui/FileDialog.hx b/src/lime/ui/FileDialog.hx index d1ac15372..2f11b0bea 100644 --- a/src/lime/ui/FileDialog.hx +++ b/src/lime/ui/FileDialog.hx @@ -5,7 +5,7 @@ import haxe.io.Path; import lime._internal.backend.native.NativeCFFI; import lime.app.Event; import lime.graphics.Image; -import lime.system.BackgroundWorker; +import lime.system.ThreadPool; import lime.utils.ArrayBuffer; import lime.utils.Resource; #if hl @@ -98,9 +98,45 @@ class FileDialog if (type == null) type = FileDialogType.OPEN; #if desktop - var worker = new BackgroundWorker(); + var worker = new ThreadPool(); - worker.doWork.add(function(_) + worker.onComplete.add(function(result) + { + switch (type) + { + case OPEN, OPEN_DIRECTORY, SAVE: + var path:String = cast result; + + if (path != null) + { + // Makes sure the filename ends with extension + if (type == SAVE && filter != null && path.indexOf(".") == -1) + { + path += "." + filter; + } + + onSelect.dispatch(path); + } + else + { + onCancel.dispatch(); + } + + case OPEN_MULTIPLE: + var paths:Array = cast result; + + if (paths != null && paths.length > 0) + { + onSelectMultiple.dispatch(paths); + } + else + { + onCancel.dispatch(); + } + } + }); + + worker.run(function(_, __) { switch (type) { @@ -182,44 +218,6 @@ class FileDialog } }); - worker.onComplete.add(function(result) - { - switch (type) - { - case OPEN, OPEN_DIRECTORY, SAVE: - var path:String = cast result; - - if (path != null) - { - // Makes sure the filename ends with extension - if (type == SAVE && filter != null && path.indexOf(".") == -1) - { - path += "." + filter; - } - - onSelect.dispatch(path); - } - else - { - onCancel.dispatch(); - } - - case OPEN_MULTIPLE: - var paths:Array = cast result; - - if (paths != null && paths.length > 0) - { - onSelectMultiple.dispatch(paths); - } - else - { - onCancel.dispatch(); - } - } - }); - - worker.run(); - return true; #else onCancel.dispatch(); @@ -241,24 +239,7 @@ class FileDialog public function open(filter:String = null, defaultPath:String = null, title:String = null):Bool { #if desktop - var worker = new BackgroundWorker(); - - worker.doWork.add(function(_) - { - #if linux - if (title == null) title = "Open File"; - #end - - var path = null; - #if hl - var bytes = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); - if (bytes != null) path = @:privateAccess String.fromUTF8(cast bytes); - #else - path = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); - #end - - worker.sendComplete(path); - }); + var worker = new ThreadPool(); worker.onComplete.add(function(path:String) { @@ -276,7 +257,22 @@ class FileDialog onCancel.dispatch(); }); - worker.run(); + worker.run(function(_, __) + { + #if linux + if (title == null) title = "Open File"; + #end + + var path = null; + #if hl + var bytes = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); + if (bytes != null) path = @:privateAccess String.fromUTF8(cast bytes); + #else + path = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath); + #end + + worker.sendComplete(path); + }); return true; #else @@ -309,24 +305,7 @@ class FileDialog } #if desktop - var worker = new BackgroundWorker(); - - worker.doWork.add(function(_) - { - #if linux - if (title == null) title = "Save File"; - #end - - var path = null; - #if hl - var bytes = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); - path = @:privateAccess String.fromUTF8(cast bytes); - #else - path = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); - #end - - worker.sendComplete(path); - }); + var worker = new ThreadPool(); worker.onComplete.add(function(path:String) { @@ -344,7 +323,22 @@ class FileDialog onCancel.dispatch(); }); - worker.run(); + worker.run(function(_, __) + { + #if linux + if (title == null) title = "Save File"; + #end + + var path = null; + #if hl + var bytes = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); + path = @:privateAccess String.fromUTF8(cast bytes); + #else + path = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath); + #end + + worker.sendComplete(path); + }); return true; #elseif (js && html5) 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 e982ff914..c55b22676 100644 --- a/tools/platforms/HTML5Platform.hx +++ b/tools/platforms/HTML5Platform.hx @@ -159,6 +159,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); } }