Fix web worker errors.

It took a lot of work to get web workers to work, but web workers finally work!

`transferList` doesn't seem to work, though. It makes the object
inaccessible as expected, but it doesn't seem to affect performance.
This commit is contained in:
Joseph Cloutier
2022-02-28 19:15:19 -05:00
parent 218f763977
commit 630aa1a62f
11 changed files with 463 additions and 241 deletions

View File

@@ -74,8 +74,11 @@
<section unless="display"> <section unless="display">
<!-- TODO: use target.threaded keyword, add html5 to the list. --> <section unless="force-synchronous || force_synchronous">
<define name="lime-threads" if="cs || neko || cpp || java || python || hl" unless="force-synchronous || force_synchronous || emscripten" /> <haxedef name="lime-threads" if="neko || cpp || html5" unless="emscripten" />
<!-- `target.threaded` isn't available, so enumerate the targets instead. -->
<haxedef name="lime-threads" if="cs || java || python || hl" unless="${${haxe_ver} < 4}" />
</section>
<section if="cpp ${${haxe_ver} < 3.3}" unless="static_link"> <section if="cpp ${${haxe_ver} < 3.3}" unless="static_link">
<ndll name="std" haxelib="hxcpp" /> <ndll name="std" haxelib="hxcpp" />
@@ -86,11 +89,13 @@
<ndll name="lime" if="native" unless="lime-console static_link || lime-switch static_link" /> <ndll name="lime" if="native" unless="lime-console static_link || lime-switch static_link" />
<dependency name="extension-api" path="dependencies/extension-api" if="android" /> <dependency name="extension-api" path="dependencies/extension-api" if="android" />
<dependency path="dependencies/howler.min.js" if="html5 howlerjs" embed="true" /> <dependency path="dependencies/howler.min.js" if="html5 howlerjs" embed="true" />
<dependency path="dependencies/pako.min.js" if="html5" embed="true" /> <dependency path="dependencies/pako.min.js" if="html5" embed="true" web-worker="true" />
<dependency path="dependencies/FileSaver.min.js" if="html5" embed="true" /> <dependency path="dependencies/FileSaver.min.js" if="html5" embed="true" />
<dependency path="dependencies/webgl-debug.js" if="html5 webgl-debug" embed="true" /> <dependency path="dependencies/webgl-debug.js" if="html5 webgl-debug" embed="true" />
<dependency path="dependencies/stats.min.js" if="html5 stats" embed="true" /> <dependency path="dependencies/stats.min.js" if="html5 stats" embed="true" />
<dependency path="dependencies/angle/d3dcompiler_47.dll" if="windows angle" unless="static_link" /> <dependency path="dependencies/angle/d3dcompiler_47.dll" if="windows angle" unless="static_link" />
<dependency path="dependencies/angle/libegl.dll" if="windows angle" unless="static_link" /> <dependency path="dependencies/angle/libegl.dll" if="windows angle" unless="static_link" />
<dependency path="dependencies/angle/libglesv2.dll" if="windows angle" unless="static_link" /> <dependency path="dependencies/angle/libglesv2.dll" if="windows angle" unless="static_link" />

View File

@@ -1,56 +1,70 @@
package lime._internal.backend.html5; package lime._internal.backend.html5;
import lime.app.Event;
#if macro #if macro
import haxe.macro.Context; import haxe.macro.Context;
import haxe.macro.Expr; import haxe.macro.Expr;
import haxe.macro.Printer;
import haxe.macro.Type; import haxe.macro.Type;
using haxe.macro.Context; using haxe.macro.Context;
using haxe.macro.TypeTools; using haxe.macro.TypeTools;
using haxe.macro.TypedExprTools;
#else #else
// Not safe to import js package during macros.
import js.Browser; import js.Browser;
import js.html.MessageEvent;
import js.html.URL; import js.html.URL;
import js.html.Worker; import js.html.Worker;
import js.Lib; import js.Lib;
import js.lib.Function; import js.lib.Function;
import js.lib.Object; import js.lib.Object;
import js.Syntax; import js.Syntax;
// Same with classes that import lots of other things.
import lime.app.Application; import lime.app.Application;
import lime.app.Event;
#end #end
/** /**
Emulates much of the `sys.thread.Thread` API using web workers. Emulates much of the `sys.thread.Thread` API using web workers.
**/ **/
class HTML5Thread { class HTML5Thread {
#if !macro
private static var __current:HTML5Thread = new HTML5Thread(Lib.global.location.href); 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<Dynamic> = new List(); private static var __messages:List<Dynamic> = new List();
private static var __resolveMethods:List<Dynamic->Void> = new List(); private static var __resolveMethods:List<Dynamic->Void> = new List();
private static var __workerCount:Int = 0; 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 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:WorkFunction<Void->Void> = event.data;
private static function onJobReceived(job:WorkFunction<Void->Void>):Void try
{ {
try job.dispatch();
{ Lib.global.onmessage = __current.dispatchMessage;
job.dispatch(); }
catch (e:Dynamic)
{
__current.destroy();
}
}
} }
catch (e:Dynamic) #end
{
Lib.global.close();
}
Lib.global.onmessage = __current.dispatchMessage;
} }
public static inline function current():HTML5Thread public static inline function current():HTML5Thread
@@ -60,18 +74,31 @@ class HTML5Thread {
public static function create(job:WorkFunction<Void->Void>):HTML5Thread public static function create(job:WorkFunction<Void->Void>):HTML5Thread
{ {
#if !macro
// Find the URL of the primary JS file.
var url:URL = new URL(__current.__href); var url:URL = new URL(__current.__href);
url.pathname = Application.current.meta["file"]; url.pathname = url.pathname.substr(0, url.pathname.lastIndexOf("/") + 1)
if (url.hash.length > 0) + Application.current.meta["file"] + ".js";
{
url.hash += "_"; // Use the hash to distinguish workers.
} if (url.hash.length > 0) url.hash += "_";
url.hash += __workerCount; url.hash += __workerCount;
__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})); var thread:HTML5Thread = new HTML5Thread(url.href, new Worker(url.href, {name: url.hash}));
// Send a message to the listener.
thread.sendMessage(job); thread.sendMessage(job);
return thread; return thread;
#else
return null;
#end
} }
/** /**
@@ -80,7 +107,7 @@ class HTML5Thread {
@param preserveClasses Whether to call `preserveClasses()` first. @param preserveClasses Whether to call `preserveClasses()` first.
**/ **/
public static function returnMessage(message:Message, transferList:Array<Dynamic> = null, preserveClasses:Bool = true):Void public static function returnMessage(message:Message, transferList:Array<Transferable> = null, preserveClasses:Bool = true):Void
{ {
if (__isWorker) if (__isWorker)
{ {
@@ -109,6 +136,7 @@ class HTML5Thread {
private function new(href:String, worker:Worker = null) private function new(href:String, worker:Worker = null)
{ {
#if !macro
__href = href; __href = href;
if (worker != null) if (worker != null)
@@ -117,14 +145,13 @@ class HTML5Thread {
__worker.onmessage = dispatchMessage; __worker.onmessage = dispatchMessage;
onMessage = new Event<Dynamic->Void>(); onMessage = new Event<Dynamic->Void>();
} }
else
{ // If an `HTML5Thread` instance is passed to a different thread than
// `HTML5Thread`'s instance functions all assume they're being // where it was created, all of its instance methods will behave
// called on the thread where this instance was created. Therefore, // incorrectly. You can still check equality, but that's it. Therefore,
// it isn't safe for `preserveClasses()` to actually preserve this // it's best to make `preserveClasses()` skip this class.
// class. (But if `worker` is defined it's already a lost cause.) Message.disablePreserveClasses(this);
Reflect.setField(this, Message.PROTOTYPE_FIELD, null); #end
}
} }
/** /**
@@ -133,8 +160,9 @@ class HTML5Thread {
@param preserveClasses Whether to call `preserveClasses()` first. @param preserveClasses Whether to call `preserveClasses()` first.
**/ **/
public function sendMessage(message:Message, transferList:Array<Dynamic> = null, preserveClasses:Bool = true):Void public function sendMessage(message:Message, transferList:Array<Transferable> = null, preserveClasses:Bool = true):Void
{ {
#if !macro
if (__worker != null) if (__worker != null)
{ {
if (preserveClasses) if (preserveClasses)
@@ -149,13 +177,19 @@ class HTML5Thread {
// No need for `restoreClasses()` because it came from this thread. // No need for `restoreClasses()` because it came from this thread.
__messages.add(message); __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(); message.restoreClasses();
onMessage.dispatch(message); if (onMessage != null)
{
onMessage.dispatch(message);
}
if (__resolveMethods.isEmpty()) if (__resolveMethods.isEmpty())
{ {
@@ -166,12 +200,14 @@ class HTML5Thread {
__resolveMethods.pop()(message); __resolveMethods.pop()(message);
} }
} }
#end
/** /**
Closes this thread unless it's the main thread. Closes this thread unless it's the main thread.
**/ **/
public function destroy():Void public function destroy():Void
{ {
#if !macro
if (__worker != null) if (__worker != null)
{ {
__worker.terminate(); __worker.terminate();
@@ -184,8 +220,13 @@ class HTML5Thread {
} }
catch (e:Dynamic) {} 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 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 @param block If true, uses the `await` keyword to wait for the next
message. Requires the calling function to be `async`. message. Requires the calling function to be `async`.
@see `lime.system.WorkOutput.JSAsync.async()`
**/ **/
public static macro function readMessage(block:ExprOf<Bool>):Dynamic public static macro function readMessage(block:ExprOf<Bool>):Dynamic
{ {
@@ -226,46 +268,70 @@ abstract WorkFunction<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
**/ **/
public var portable(get, never):Bool; public var portable(get, never):Bool;
// `@:from` would cause errors during the macro phase. Disabling `macro` #if macro
// during the macro phase allows other macros to call this statically. /**
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<String>, ?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 @:noCompletion @:dox(hide) #if !macro @:from #end
public static #if !macro macro #end function fromFunction(func:ExprOf<haxe.Constraints.Function>) public static #if !macro macro #end function fromFunction(func:ExprOf<haxe.Constraints.Function>)
{ {
#if !force_synchronous var defaultOutput:Expr = macro {
return macro {
func: $func 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<String> = new Printer().printExpr(func).split(".");
var functionName:String = parts.pop();
var classPath:String; if (!Context.defined("lime-threads"))
if (parts.length > 0)
{ {
classPath = parts.join("."); return defaultOutput;
} }
else else
{ {
var classType:ClassType = Context.getLocalClass().get(); // Haxe likes to pass `@:this this` instead of the actual
classPath = classType.pack.join(".") + "." + classType.name; // 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 { // Match the package, class name, and field name.
classPath: $v{classPath}, var matcher:EReg = ~/^((?:_?\w+\.)*[A-Z]\w*)\.(_*[a-z]\w*)$/;
functionName: $v{functionName} if (!matcher.match(qualifiedFunc))
}; {
#end 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<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
#end #end
return this.func; 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<String> = ~/,\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'; throw 'Object is not a valid WorkFunction: $this';
} }
@@ -331,19 +376,37 @@ abstract WorkFunction<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
**/ **/
public function makePortable(throwError:Bool = true):Bool public function makePortable(throwError:Bool = true):Bool
{ {
if ((this.classPath == null || this.functionName == null) if (this.func != null)
&& this.sourceCode == null && this.func != null)
{ {
#if !macro // Make sure `classPath.functionName` points to the actual function.
this.sourceCode = (cast this.func:Function).toString(); if (this.classPath != null || this.functionName != null)
#end {
} #if !macro
var func = #if !haxe4 untyped __js__ #else Syntax.code #end
if (this.classPath != null && this.functionName != null ("$hxClasses[{0}] && $hxClasses[{0}][{1}]", this.classPath, this.functionName);
// The main reason instance methods don't work. if (func != this.func)
|| this.sourceCode != null && this.sourceCode.indexOf("[native code]") < 0) {
{ throw 'Could not make ${this.functionName} portable. Either ${this.functionName} isn\'t static, or ${this.classPath} is something other than a class.';
this.func = null; }
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:WorkFunction<Dynamic->Void> = 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; return portable;
@@ -361,10 +424,50 @@ abstract WorkFunction<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
@:allow(lime._internal.backend.html5.HTML5Thread) @:allow(lime._internal.backend.html5.HTML5Thread)
abstract Message(Dynamic) from Dynamic to Dynamic abstract Message(Dynamic) from Dynamic to Dynamic
{ {
#if !macro
private static inline var PROTOTYPE_FIELD:String = "__prototype__"; private static inline var PROTOTYPE_FIELD:String = "__prototype__";
private static inline var SKIP_FIELD:String = "__skipPrototype__";
private static inline var RESTORE_FIELD:String = "__restoreFlag__"; 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 Adds class information to this message and all children, so that it will
survive being passed across threads. "Children" are the values returned 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 public function preserveClasses():Void
{ {
// Avoid looping. #if !macro
if (Reflect.hasField(this, PROTOTYPE_FIELD) if (this == null
// Avoid looping.
|| Reflect.hasField(this, PROTOTYPE_FIELD)
// Skip primitive types. // Skip primitive types.
|| !Std.isOfType(this, Object)) || !Std.isOfType(this, Object))
{ {
@@ -381,7 +486,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic
} }
// Preserve this object's class. // Preserve this object's class.
if (!Std.isOfType(this, Array)) if (!Reflect.hasField(this, SKIP_FIELD) && !Std.isOfType(this, Array))
{ {
try try
{ {
@@ -399,6 +504,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic
{ {
(sub:Message).preserveClasses(); (sub:Message).preserveClasses();
} }
#end
} }
/** /**
@@ -408,6 +514,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic
**/ **/
private function restoreClasses(flag:Int = null):Void private function restoreClasses(flag:Int = null):Void
{ {
#if !macro
// Attempt to choose a unique flag. // Attempt to choose a unique flag.
if (flag == null) if (flag == null)
{ {
@@ -428,9 +535,17 @@ abstract Message(Dynamic) from Dynamic to Dynamic
} }
// Restore this object's class. // 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 try
{ {
@@ -446,13 +561,47 @@ abstract Message(Dynamic) from Dynamic to Dynamic
{ {
(sub:Message).restoreClasses(flag); (sub:Message).restoreClasses(flag);
} }
#end
} }
#end
} }
private typedef WorkFunctionData<T:haxe.Constraints.Function> = { /**
Stores the class path and function name of a function, so that it can be
found again in the background thread.
**/
typedef WorkFunctionData<T:haxe.Constraints.Function> = {
@:optional var classPath:String; @:optional var classPath:String;
@:optional var functionName:String; @:optional var functionName:String;
@:optional var sourceCode:String;
@:optional var func:T; @: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
{
}

View File

@@ -293,7 +293,7 @@ import lime.utils.Log;
/** /**
Creates a `Future` instance which has finished with a completion value 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 @return A new `Future` instance
**/ **/
public static function withValue<T>(value:T):Future<T> public static function withValue<T>(value:T):Future<T>

View File

@@ -7,7 +7,7 @@ package lime.app;
`Future` values. `Future` values.
While `Future` is meant to be read-only, `Promise` can be used to set the state of a future 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 ```haxe
function examplePromise ():Future<String> { function examplePromise ():Future<String> {

View File

@@ -29,17 +29,11 @@ import lime.system.WorkOutput;
@:noDebug @:noDebug
#end #end
@:forward(canceled, completed, currentThreads, activeJobs, idleThreads, @:forward(canceled, completed, currentThreads, activeJobs, idleThreads,
onComplete, onError, onProgress, onRun, cancel) minThreads, maxThreads, onComplete, onError, onProgress, onRun, cancel)
abstract BackgroundWorker(ThreadPool) abstract BackgroundWorker(ThreadPool)
{ {
private static var doWorkWrapper:WorkFunction<State->WorkOutput->Void>; private static var doWorkWrapper:WorkFunction<State->WorkOutput->Void>;
private static function doWorkImpl(state:State, output:WorkOutput):Void
{
// `dispatch()` will check if it's really a `WorkFunction`.
(state.doWork:WorkFunction<State->WorkOutput->Void>).dispatch(state.state, output);
}
@:deprecated("Instead pass the callback to BackgroundWorker.run().") @:deprecated("Instead pass the callback to BackgroundWorker.run().")
@:noCompletion @:dox(hide) public var doWork(get, never):{ add: (Dynamic->Void) -> Void }; @:noCompletion @:dox(hide) public var doWork(get, never):{ add: (Dynamic->Void) -> Void };
@@ -59,7 +53,7 @@ abstract BackgroundWorker(ThreadPool)
{ {
if (doWorkWrapper == null) if (doWorkWrapper == null)
{ {
doWorkWrapper = doWorkImpl; doWorkWrapper = BackgroundWorkerFunctions.__doWork;
} }
this = new ThreadPool(doWorkWrapper, mode, workLoad); this = new ThreadPool(doWorkWrapper, mode, workLoad);
} }
@@ -119,6 +113,13 @@ abstract BackgroundWorker(ThreadPool)
this.__doWork = doWorkWrapper; this.__doWork = doWorkWrapper;
} }
#if html5
if (this.mode == MULTI_THREADED)
{
doWork.makePortable();
}
#end
this.queue({ this.queue({
state: state, state: state,
doWork: doWork doWork: doWork
@@ -127,21 +128,34 @@ abstract BackgroundWorker(ThreadPool)
// Getters & Setters // Getters & Setters
private function get_doWork():{ add: (Dynamic->Void) -> Void } private function get_doWork()
{ {
return { return {
add: function(callback:Dynamic->Void) add: function(callback:Dynamic->Void)
{ {
// Hack: overwrite `__doWork` just for this one function. #if html5
this.__doWork = function(state:State, output:WorkOutput):Void if (this.mode == MULTI_THREADED)
{ throw "Unsupported operation; instead pass the callback to BackgroundWorker.run().";
#if html5 #end
if (this.mode == MULTI_THREADED) // Hack: overwrite `__doWork` just for this one function. Hope
throw "Unsupported operation; instead pass the callback to BackgroundWorker.run()."; // it wasn't in use!
#end this.__doWork = #if html5 { func: #end
callback(state.state); 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:WorkFunction<State->WorkOutput->Void>).dispatch(state.state, output);
}
}

View File

@@ -167,7 +167,7 @@ class ThreadPool extends WorkOutput
app's available time every frame. See `workIterations` for instructions app's available time every frame. See `workIterations` for instructions
to improve the accuracy of this estimate. 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:WorkFunction<State->WorkOutput->Void>, minThreads:Int = 0, maxThreads:Int = 1, mode:ThreadMode = null, ?workLoad:Float = 1/2)
{ {
super(mode); super(mode);
@@ -194,20 +194,20 @@ class ThreadPool extends WorkOutput
Application.current.onUpdate.remove(__update); 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) for (job in __activeJobs)
{ {
#if lime_threads #if lime_threads
if (job.thread != null) 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 #end
@@ -299,80 +299,78 @@ class ThreadPool extends WorkOutput
/** /**
__Run this only on a background thread.__ __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 Before any jobs, this function requires, in order:
`WorkOutput` instance. Other than that, all thread messages should be
`WORK` or `EXIT` `ThreadEvent` instances. 1. A `WorkOutput` instance. (Omit this message in HTML5.)
2. The `doWork` function.
**/ **/
private static function __executeThread():Void private static function __executeThread():Void
{ {
#if html5 JSAsync.async({
// HTML5 requires the `async` keyword, which is easiest to do inline. var output:WorkOutput = #if html5 new WorkOutput(MULTI_THREADED) #else cast(Thread.readMessage(true), WorkOutput) #end;
#if haxe4 js.Syntax.code #else untyped __js__ #end ("(async {0})()", var doWork:WorkFunction<State->WorkOutput->Void> = Thread.readMessage(true);
function() { var job:ThreadEvent = null;
#end
var output:WorkOutput = #if html5 new WorkOutput(MULTI_THREADED) #else cast(Thread.readMessage(true), WorkOutput) #end; while (true)
var doWork:WorkFunction<State->WorkOutput->Void> = Thread.readMessage(true);
var job:ThreadEvent = null;
while (true)
{
// Get a job.
if (job == null)
{ {
job = cast(Thread.readMessage(true), ThreadEvent); // Get a job.
if (job == null)
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)
{ {
output.workIterations.value++; do
doWork.dispatch(job.state, output); {
job = Thread.readMessage(true);
}
while (!Std.isOfType(job, ThreadEvent));
output.resetJobProgress();
} }
}
catch (e)
{
output.sendError(e);
}
if (interruption == null || output.__jobComplete.value) if (job.event == EXIT)
{ {
job = null; return;
} }
else if(Std.isOfType(interruption, ThreadEvent))
{
job = interruption;
output.resetJobProgress();
}
else
{
// Ignore interruption and keep current job.
}
// 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 #end
@@ -399,7 +397,7 @@ class ThreadPool extends WorkOutput
#end #end
// Process the queue. // Process the queue.
while (__jobQueue.length > 0 && currentThreads < maxThreads) while (!__jobQueue.isEmpty() && activeJobs < maxThreads)
{ {
var job:ThreadEvent = __jobQueue.pop(); var job:ThreadEvent = __jobQueue.pop();
if (job.event != WORK) if (job.event != WORK)
@@ -498,8 +496,6 @@ class ThreadPool extends WorkOutput
#if lime_threads #if lime_threads
if (mode == MULTI_THREADED) if (mode == MULTI_THREADED)
{ {
__activeJobs.remove(threadEvent.associatedJob);
if (currentThreads > maxThreads || __jobQueue.isEmpty() && currentThreads > minThreads) if (currentThreads > maxThreads || __jobQueue.isEmpty() && currentThreads > minThreads)
{ {
threadEvent.associatedJob.thread.sendMessage(new ThreadEvent(EXIT, null)); threadEvent.associatedJob.thread.sendMessage(new ThreadEvent(EXIT, null));
@@ -508,6 +504,8 @@ class ThreadPool extends WorkOutput
{ {
__idleThreads.push(threadEvent.associatedJob.thread); __idleThreads.push(threadEvent.associatedJob.thread);
} }
__activeJobs.removeThread(threadEvent.associatedJob.thread);
} }
#end #end
@@ -517,12 +515,25 @@ class ThreadPool extends WorkOutput
eventSource = null; eventSource = null;
} }
if (currentThreads == 0) if (completed)
{ {
Application.current.onUpdate.remove(__update); Application.current.onUpdate.remove(__update);
} }
} }
#if lime_threads
private override function createThread(executeThread:WorkFunction<Void->Void>):Thread
{
var thread:Thread = super.createThread(executeThread);
#if !html5
thread.sendMessage(this);
#end
thread.sendMessage(__doWork);
return thread;
}
#end
// Getters & Setters // Getters & Setters
private inline function get_activeJobs():Int private inline function get_activeJobs():Int
@@ -540,20 +551,23 @@ class ThreadPool extends WorkOutput
return activeJobs + idleThreads; 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 } private function get_doWork():{ add: (Dynamic->Void) -> Void }
{ {
return { return {
add: function(callback:Dynamic->Void) add: function(callback:Dynamic->Void)
{ {
__doWork = function(state:State, output:WorkOutput):Void #if html5
{ if (mode == MULTI_THREADED)
#if html5 throw "Unsupported operation; instead pass the callback to ThreadPool's constructor.";
if (mode == MULTI_THREADED) #end
throw "Unsupported operation; instead pass the callback to ThreadPool's constructor."; __doWork = #if html5 { func: #end
#end function(state:State, output:WorkOutput):Void
callback(state); {
}; callback(state);
}
#if html5 } #end;
} }
}; };
} }

View File

@@ -12,8 +12,11 @@ import cpp.vm.Tls;
import neko.vm.Deque; import neko.vm.Deque;
import neko.vm.Thread; import neko.vm.Thread;
import neko.vm.Tls; import neko.vm.Tls;
#elseif html5 #end
#if html5
import lime._internal.backend.html5.HTML5Thread as Thread; import lime._internal.backend.html5.HTML5Thread as Thread;
import lime._internal.backend.html5.HTML5Thread.Transferable;
#end #end
#if macro #if macro
@@ -75,12 +78,15 @@ class WorkOutput
private var __activeJobs:ActiveJobs = new ActiveJobs(); private var __activeJobs:ActiveJobs = new ActiveJobs();
/** /**
Single-threaded mode only; the `state` provided to the active job. If The `state` provided to the active job. Will only have a value during
available, should be included in new `ThreadEvent`s. `__update()` in single-threaded mode, and will otherwise be `null`.
Include this when creating new `ThreadEvent`s.
**/ **/
private var __activeJob:Null<State> = null; private var __activeJob:Null<State> = null;
private inline function new(mode:ThreadMode) { private inline function new(mode:Null<ThreadMode>)
{
workIterations.value = 0; workIterations.value = 0;
__jobComplete.value = false; __jobComplete.value = false;
@@ -97,7 +103,7 @@ class WorkOutput
@see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
**/ **/
#if (lime_threads && html5) inline #end #if (lime_threads && html5) inline #end
public function sendComplete(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Dynamic> = null #end):Void public function sendComplete(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Transferable> = null #end):Void
{ {
if (!__jobComplete.value) if (!__jobComplete.value)
{ {
@@ -120,7 +126,7 @@ class WorkOutput
@see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
**/ **/
#if (lime_threads && html5) inline #end #if (lime_threads && html5) inline #end
public function sendError(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Dynamic> = null #end):Void public function sendError(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Transferable> = null #end):Void
{ {
if (!__jobComplete.value) if (!__jobComplete.value)
{ {
@@ -143,7 +149,7 @@ class WorkOutput
@see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects @see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
**/ **/
#if (lime_threads && html5) inline #end #if (lime_threads && html5) inline #end
public function sendProgress(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Dynamic> = null #end):Void public function sendProgress(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Transferable> = null #end):Void
{ {
if (!__jobComplete.value) if (!__jobComplete.value)
{ {
@@ -167,25 +173,21 @@ class WorkOutput
{ {
var thread:Thread = Thread.create(executeThread); var thread:Thread = Thread.create(executeThread);
// Platform-dependent initialization.
#if html5 #if html5
thread.onMessage.add(onMessageFromWorker.bind(thread)); thread.onMessage.add(onMessageFromWorker.bind(thread));
#else
thread.sendMessage(this);
#end #end
return thread; return thread;
} }
#if html5 #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; return;
} }
var threadEvent:ThreadEvent = message;
threadEvent.associatedJob = __activeJobs.getByThread(thread); threadEvent.associatedJob = __activeJobs.getByThread(thread);
__jobOutput.add(threadEvent); __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) #if !(target.threaded || cpp || neko)
@:forward(push, add) @:forward.new @:forward(push, add) @:forward.new
abstract Deque<T>(List<T>) from List<T> to List<T> abstract Deque<T>(List<T>) from List<T> to List<T>

View File

@@ -5,6 +5,7 @@ class Dependency
// TODO: Is "forceLoad" the best name? Implement "whole-archive" on GCC // TODO: Is "forceLoad" the best name? Implement "whole-archive" on GCC
public var embed:Bool; public var embed:Bool;
public var forceLoad:Bool; public var forceLoad:Bool;
public var webWorker:Bool;
public var name:String; public var name:String;
public var path:String; public var path:String;

View File

@@ -1752,6 +1752,11 @@ class ProjectXMLParser extends HXProject
dependency.forceLoad = parseBool(element.att.resolve("force-load")); 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; var i = dependencies.length;
while (i-- > 0) while (i-- > 0)

View File

@@ -1,17 +1,25 @@
(function ($hx_exports, $global) { "use strict"; var $hx_script = (function (exports, global) { ::SOURCE_FILE:: (function ($hx_exports, $global) { "use strict"; var $hx_script = (function (exports, global) { ::SOURCE_FILE::
}); });
$hx_exports.lime = $hx_exports.lime || {}; ::if false::
$hx_exports.lime.$scripts = $hx_exports.lime.$scripts || {}; If `window` is undefined, it means this script is running as a web worker.
$hx_exports.lime.$scripts["::APP_FILE::"] = $hx_script; In that case, there's no need for exports, and all we need to do is run the
$hx_exports.lime.embed = function(projectName) { var exports = {}; static initializers.
var script = $hx_exports.lime.$scripts[projectName]; ::end::if(typeof window == "undefined") {
if (!script) throw Error("Cannot find project name \"" + projectName + "\""); $hx_script({}, $global);
script(exports, $global); } else {
for (var key in exports) $hx_exports[key] = $hx_exports[key] || exports[key]; $hx_exports.lime = $hx_exports.lime || {};
var lime = exports.lime || window.lime; $hx_exports.lime.$scripts = $hx_exports.lime.$scripts || {};
if (lime && lime.embed && this != lime.embed) lime.embed.apply(lime, arguments); $hx_exports.lime.$scripts["::APP_FILE::"] = $hx_script;
return exports; $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 false::
AMD compatibility: If define() is present we need to AMD compatibility: If define() is present we need to
- call it, to define our module - call it, to define our module

View File

@@ -93,6 +93,10 @@ class HTML5Platform extends PlatformTarget
if (dependency.embed && StringTools.endsWith(dependency.path, ".js") && FileSystem.exists(dependency.path)) if (dependency.embed && StringTools.endsWith(dependency.path, ".js") && FileSystem.exists(dependency.path))
{ {
var script = File.getContent(dependency.path); var script = File.getContent(dependency.path);
if (!dependency.webWorker)
{
script = 'if(typeof window != "undefined") {\n' + script + "\n}";
}
context.embeddedLibraries.push(script); context.embeddedLibraries.push(script);
} }
} }