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">
<!-- TODO: use target.threaded keyword, add html5 to the list. -->
<define name="lime-threads" if="cs || neko || cpp || java || python || hl" unless="force-synchronous || force_synchronous || emscripten" />
<section unless="force-synchronous || force_synchronous">
<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">
<ndll name="std" haxelib="hxcpp" />
@@ -86,11 +89,13 @@
<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 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/webgl-debug.js" if="html5 webgl-debug" 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/libegl.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;
import lime.app.Event;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Printer;
import haxe.macro.Type;
using haxe.macro.Context;
using haxe.macro.TypeTools;
using haxe.macro.TypedExprTools;
#else
// Not safe to import js package during macros.
import js.Browser;
import js.html.MessageEvent;
import js.html.URL;
import js.html.Worker;
import js.Lib;
import js.lib.Function;
import js.lib.Object;
import js.Syntax;
// Same with classes that import lots of other things.
import lime.app.Application;
import lime.app.Event;
#end
/**
Emulates much of the `sys.thread.Thread` API using web workers.
**/
class HTML5Thread {
#if !macro
private static var __current:HTML5Thread = new HTML5Thread(Lib.global.location.href);
private static var __isWorker:Bool = Browser.window == null;
private static var __isWorker:Bool #if !macro = #if !haxe4 untyped __js__ #else Syntax.code #end ('typeof window == "undefined"') #end;
private static var __messages:List<Dynamic> = new List();
private static var __resolveMethods:List<Dynamic->Void> = new List();
private static var __workerCount:Int = 0;
/**
The entry point into a worker script.
Lime's output JS file normally does not begin on its own. Instead it
registers a `lime.embed()` callback for index.html to use.
When this JS file is run as a web worker, it isn't running within
index.html, so `embed()` never gets called. Instead, `__init__()`
registers a message listener.
**/
private static function __init__():Void
{
if (__isWorker)
#if !macro
if (#if !haxe4 untyped __js__ #else Syntax.code #end ('typeof window == "undefined"'))
{
Lib.global.onmessage = onJobReceived;
}
}
Lib.global.onmessage = function(event:MessageEvent):Void
{
var job:WorkFunction<Void->Void> = event.data;
private static function onJobReceived(job:WorkFunction<Void->Void>):Void
{
try
{
job.dispatch();
try
{
job.dispatch();
Lib.global.onmessage = __current.dispatchMessage;
}
catch (e:Dynamic)
{
__current.destroy();
}
}
}
catch (e:Dynamic)
{
Lib.global.close();
}
Lib.global.onmessage = __current.dispatchMessage;
#end
}
public static inline function current():HTML5Thread
@@ -60,18 +74,31 @@ class HTML5Thread {
public static function create(job:WorkFunction<Void->Void>):HTML5Thread
{
#if !macro
// Find the URL of the primary JS file.
var url:URL = new URL(__current.__href);
url.pathname = Application.current.meta["file"];
if (url.hash.length > 0)
{
url.hash += "_";
}
url.pathname = url.pathname.substr(0, url.pathname.lastIndexOf("/") + 1)
+ Application.current.meta["file"] + ".js";
// Use the hash to distinguish workers.
if (url.hash.length > 0) url.hash += "_";
url.hash += __workerCount;
__workerCount++;
// Prepare to send the job.
job.makePortable();
// Create the worker. Because the worker's scope will not include a
// `window`, `HTML5Thread.__init__()` will add a listener.
var thread:HTML5Thread = new HTML5Thread(url.href, new Worker(url.href, {name: url.hash}));
// Send a message to the listener.
thread.sendMessage(job);
return thread;
#else
return null;
#end
}
/**
@@ -80,7 +107,7 @@ class HTML5Thread {
@param preserveClasses Whether to call `preserveClasses()` first.
**/
public static function returnMessage(message:Message, transferList:Array<Dynamic> = null, preserveClasses:Bool = true):Void
public static function returnMessage(message:Message, transferList:Array<Transferable> = null, preserveClasses:Bool = true):Void
{
if (__isWorker)
{
@@ -109,6 +136,7 @@ class HTML5Thread {
private function new(href:String, worker:Worker = null)
{
#if !macro
__href = href;
if (worker != null)
@@ -117,14 +145,13 @@ class HTML5Thread {
__worker.onmessage = dispatchMessage;
onMessage = new Event<Dynamic->Void>();
}
else
{
// `HTML5Thread`'s instance functions all assume they're being
// called on the thread where this instance was created. Therefore,
// it isn't safe for `preserveClasses()` to actually preserve this
// class. (But if `worker` is defined it's already a lost cause.)
Reflect.setField(this, Message.PROTOTYPE_FIELD, null);
}
// If an `HTML5Thread` instance is passed to a different thread than
// where it was created, all of its instance methods will behave
// incorrectly. You can still check equality, but that's it. Therefore,
// it's best to make `preserveClasses()` skip this class.
Message.disablePreserveClasses(this);
#end
}
/**
@@ -133,8 +160,9 @@ class HTML5Thread {
@param preserveClasses Whether to call `preserveClasses()` first.
**/
public function sendMessage(message:Message, transferList:Array<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 (preserveClasses)
@@ -149,13 +177,19 @@ class HTML5Thread {
// No need for `restoreClasses()` because it came from this thread.
__messages.add(message);
}
#end
}
private function dispatchMessage(message:Message):Void
#if !macro
private function dispatchMessage(event:MessageEvent):Void
{
var message:Message = event.data;
message.restoreClasses();
onMessage.dispatch(message);
if (onMessage != null)
{
onMessage.dispatch(message);
}
if (__resolveMethods.isEmpty())
{
@@ -166,12 +200,14 @@ class HTML5Thread {
__resolveMethods.pop()(message);
}
}
#end
/**
Closes this thread unless it's the main thread.
**/
public function destroy():Void
{
#if !macro
if (__worker != null)
{
__worker.terminate();
@@ -184,8 +220,13 @@ class HTML5Thread {
}
catch (e:Dynamic) {}
}
#end
}
public inline function isWorker():Bool
{
return __worker != null || __isWorker;
}
#end
/**
Reads a message from the thread queue. Returns `null` if no argument is
@@ -193,6 +234,7 @@ class HTML5Thread {
@param block If true, uses the `await` keyword to wait for the next
message. Requires the calling function to be `async`.
@see `lime.system.WorkOutput.JSAsync.async()`
**/
public static macro function readMessage(block:ExprOf<Bool>):Dynamic
{
@@ -226,46 +268,70 @@ abstract WorkFunction<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
**/
public var portable(get, never):Bool;
// `@:from` would cause errors during the macro phase. Disabling `macro`
// during the macro phase allows other macros to call this statically.
#if macro
/**
Parses a chain of nested `EField` expressions.
@return An array of all identifiers in the chain, as strings. If the
chain began with something other than an identifier, it will be returned
as the `initialExpr`. For instance, `array[i].foo.bar` will result in
`chain == ["foo", "bar"]` and `initialExpr == array[i]`.
**/
private static function parseFieldChain(chain:Expr):{ chain:Array<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
public static #if !macro macro #end function fromFunction(func:ExprOf<haxe.Constraints.Function>)
{
#if !force_synchronous
return macro {
var defaultOutput:Expr = macro {
func: $func
};
#else
trace(func);
#if haxe3
trace("haxe3");
#end
#if haxe4
trace("haxe4");
#end
#if haxe5
trace("haxe5");
#end
//trace(Context.resolveType());
var parts:Array<String> = new Printer().printExpr(func).split(".");
var functionName:String = parts.pop();
var classPath:String;
if (parts.length > 0)
if (!Context.defined("lime-threads"))
{
classPath = parts.join(".");
return defaultOutput;
}
else
{
var classType:ClassType = Context.getLocalClass().get();
classPath = classType.pack.join(".") + "." + classType.name;
}
// Haxe likes to pass `@:this this` instead of the actual
// expression, so use a roundabout method to convert back. As a
// happy side-effect, it fully qualifies the expression.
var qualifiedFunc:String = func.typeExpr().toString(true);
return macro {
classPath: $v{classPath},
functionName: $v{functionName}
};
#end
// Match the package, class name, and field name.
var matcher:EReg = ~/^((?:_?\w+\.)*[A-Z]\w*)\.(_*[a-z]\w*)$/;
if (!matcher.match(qualifiedFunc))
{
if (Context.defined("lime-warn-portability"))
{
trace(qualifiedFunc);
Context.warning("Value doesn't appear to be a static function.", func.pos);
}
return defaultOutput;
}
var classPath:String = matcher.matched(1);
var functionName:String = matcher.matched(2);
return macro {
func: $func,
classPath: $v{classPath},
functionName: $v{functionName}
};
}
}
/**
@@ -299,27 +365,6 @@ abstract WorkFunction<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
#end
return this.func;
}
else if (this.sourceCode != null)
{
var parser:EReg = ~/^function\(((?:\w+,\s*)*)\)\s*\{(.+)\s*\}$/s;
if (parser.match(this.sourceCode))
{
var paramsAndBody:Array<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';
}
@@ -331,19 +376,37 @@ abstract WorkFunction<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
**/
public function makePortable(throwError:Bool = true):Bool
{
if ((this.classPath == null || this.functionName == null)
&& this.sourceCode == null && this.func != null)
if (this.func != null)
{
#if !macro
this.sourceCode = (cast this.func:Function).toString();
#end
}
if (this.classPath != null && this.functionName != null
// The main reason instance methods don't work.
|| this.sourceCode != null && this.sourceCode.indexOf("[native code]") < 0)
{
this.func = null;
// Make sure `classPath.functionName` points to the actual function.
if (this.classPath != null || this.functionName != null)
{
#if !macro
var func = #if !haxe4 untyped __js__ #else Syntax.code #end
("$hxClasses[{0}] && $hxClasses[{0}][{1}]", this.classPath, this.functionName);
if (func != this.func)
{
throw 'Could not make ${this.functionName} portable. Either ${this.functionName} isn\'t static, or ${this.classPath} is something other than a class.';
}
else
{
// All set.
this.func = null;
}
#end
}
else
{
// If you aren't sure why you got this message, make sure your
// variables are of type `WorkFunction`.
// This won't work:
// var f = MyClass.staticFunction;
// bgWorker.run(f);
// ...but this will:
// var f: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;
@@ -361,10 +424,50 @@ abstract WorkFunction<T:haxe.Constraints.Function>(WorkFunctionData<T>) from Wor
@:allow(lime._internal.backend.html5.HTML5Thread)
abstract Message(Dynamic) from Dynamic to Dynamic
{
#if !macro
private static inline var PROTOTYPE_FIELD:String = "__prototype__";
private static inline var SKIP_FIELD:String = "__skipPrototype__";
private static inline var RESTORE_FIELD:String = "__restoreFlag__";
/**
Prevents `preserveClasses()` from working on the given object.
Note: if its class isn't preserved, `cast(object, Foo)` will fail with
the unhelpful message "uncaught exception: Object" and no line number.
@param recursive Whether to apply this to the object's children as well.
**/
public static function disablePreserveClasses(object:Dynamic, recursive:Bool = false):Void
{
#if !macro
if (object == null
// Avoid looping.
|| Reflect.hasField(object, SKIP_FIELD)
// Skip primitive types.
|| !Std.isOfType(object, Object))
{
return;
}
try
{
Reflect.setField(object, Message.SKIP_FIELD, true);
}
catch (e:Dynamic)
{
// Probably a frozen object; no need to recurse.
return;
}
if (recursive)
{
for (sub in Object.values(object))
{
disablePreserveClasses(sub, true);
}
}
#end
}
/**
Adds class information to this message and all children, so that it will
survive being passed across threads. "Children" are the values returned
@@ -372,8 +475,10 @@ abstract Message(Dynamic) from Dynamic to Dynamic
**/
public function preserveClasses():Void
{
// Avoid looping.
if (Reflect.hasField(this, PROTOTYPE_FIELD)
#if !macro
if (this == null
// Avoid looping.
|| Reflect.hasField(this, PROTOTYPE_FIELD)
// Skip primitive types.
|| !Std.isOfType(this, Object))
{
@@ -381,7 +486,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic
}
// Preserve this object's class.
if (!Std.isOfType(this, Array))
if (!Reflect.hasField(this, SKIP_FIELD) && !Std.isOfType(this, Array))
{
try
{
@@ -399,6 +504,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic
{
(sub:Message).preserveClasses();
}
#end
}
/**
@@ -408,6 +514,7 @@ abstract Message(Dynamic) from Dynamic to Dynamic
**/
private function restoreClasses(flag:Int = null):Void
{
#if !macro
// Attempt to choose a unique flag.
if (flag == null)
{
@@ -428,9 +535,17 @@ abstract Message(Dynamic) from Dynamic to Dynamic
}
// Restore this object's class.
if (Reflect.field(this, PROTOTYPE_FIELD) != null)
if (!Reflect.hasField(this, SKIP_FIELD) && Reflect.field(this, PROTOTYPE_FIELD) != null)
{
Reflect.setField(this, RESTORE_FIELD, flag);
try
{
Reflect.setField(this, RESTORE_FIELD, flag);
}
catch (e:Dynamic)
{
// Probably a frozen object; no need to continue.
return;
}
try
{
@@ -446,13 +561,47 @@ abstract Message(Dynamic) from Dynamic to Dynamic
{
(sub:Message).restoreClasses(flag);
}
#end
}
#end
}
private typedef WorkFunctionData<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 functionName:String;
@:optional var sourceCode:String;
@:optional var func:T;
};
#if macro
typedef Worker = Dynamic;
typedef URL = Dynamic;
class Object {}
class Browser
{
public static var window:Dynamic;
}
class Lib
{
public static var global:Dynamic = { location: {} };
}
#end
/**
An object to transfer, rather than copy.
Abstract types like `lime.utils.Int32Array` and `openfl.utils.ByteArray`
can be automatically converted. However, extern classes like
`js.lib.Int32Array` typically can't.
@see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
**/
// Mozilla uses "transferable" and "transferrable" interchangeably, but the HTML
// specification only uses the former.
@:forward
abstract Transferable(Dynamic) #if macro from Dynamic
#else from js.lib.ArrayBuffer from js.html.MessagePort from js.html.ImageBitmap #end
{
}

View File

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

View File

@@ -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<String> {

View File

@@ -29,17 +29,11 @@ import lime.system.WorkOutput;
@:noDebug
#end
@:forward(canceled, completed, currentThreads, activeJobs, idleThreads,
onComplete, onError, onProgress, onRun, cancel)
minThreads, maxThreads, onComplete, onError, onProgress, onRun, cancel)
abstract BackgroundWorker(ThreadPool)
{
private static var doWorkWrapper: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().")
@:noCompletion @:dox(hide) public var doWork(get, never):{ add: (Dynamic->Void) -> Void };
@@ -59,7 +53,7 @@ abstract BackgroundWorker(ThreadPool)
{
if (doWorkWrapper == null)
{
doWorkWrapper = doWorkImpl;
doWorkWrapper = BackgroundWorkerFunctions.__doWork;
}
this = new ThreadPool(doWorkWrapper, mode, workLoad);
}
@@ -119,6 +113,13 @@ abstract BackgroundWorker(ThreadPool)
this.__doWork = doWorkWrapper;
}
#if html5
if (this.mode == MULTI_THREADED)
{
doWork.makePortable();
}
#end
this.queue({
state: state,
doWork: doWork
@@ -127,21 +128,34 @@ abstract BackgroundWorker(ThreadPool)
// Getters & Setters
private function get_doWork():{ add: (Dynamic->Void) -> Void }
private function get_doWork()
{
return {
add: function(callback:Dynamic->Void)
{
// Hack: overwrite `__doWork` just for this one function.
this.__doWork = function(state:State, output:WorkOutput):Void
{
#if html5
if (this.mode == MULTI_THREADED)
throw "Unsupported operation; instead pass the callback to BackgroundWorker.run().";
#end
callback(state.state);
};
#if html5
if (this.mode == MULTI_THREADED)
throw "Unsupported operation; instead pass the callback to BackgroundWorker.run().";
#end
// Hack: overwrite `__doWork` just for this one function. Hope
// it wasn't in use!
this.__doWork = #if html5 { func: #end
function(state:State, output:WorkOutput):Void
{
callback(state.state);
}
#if html5 } #end;
}
};
}
}
@:allow(lime.system.BackgroundWorker)
private class BackgroundWorkerFunctions
{
private static function __doWork(state:State, output:WorkOutput):Void
{
// `dispatch()` will check if it's really a `WorkFunction`.
(state.doWork: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
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);
@@ -194,20 +194,20 @@ class ThreadPool extends WorkOutput
Application.current.onUpdate.remove(__update);
#if lime_threads
for (thread in __idleThreads)
{
thread.sendMessage(new ThreadEvent(EXIT, null));
}
__idleThreads.clear();
#end
for (job in __activeJobs)
{
#if lime_threads
if (job.thread != null)
{
job.thread.sendMessage(new ThreadEvent(EXIT, null));
if (idleThreads < minThreads)
{
job.thread.sendMessage(new ThreadEvent(WORK, null));
__idleThreads.push(job.thread);
}
else
{
job.thread.sendMessage(new ThreadEvent(EXIT, null));
}
}
#end
@@ -299,80 +299,78 @@ class ThreadPool extends WorkOutput
/**
__Run this only on a background thread.__
Retrieves pending jobs, runs them until complete, and repeats.
Retrieves jobs using `Thread.readMessage()`, runs them until complete,
and repeats.
On all targets except HTML5, the first thread message must be a
`WorkOutput` instance. Other than that, all thread messages should be
`WORK` or `EXIT` `ThreadEvent` instances.
Before any jobs, this function requires, in order:
1. A `WorkOutput` instance. (Omit this message in HTML5.)
2. The `doWork` function.
**/
private static function __executeThread():Void
{
#if html5
// HTML5 requires the `async` keyword, which is easiest to do inline.
#if haxe4 js.Syntax.code #else untyped __js__ #end ("(async {0})()",
function() {
#end
JSAsync.async({
var output:WorkOutput = #if html5 new WorkOutput(MULTI_THREADED) #else cast(Thread.readMessage(true), WorkOutput) #end;
var doWork:WorkFunction<State->WorkOutput->Void> = Thread.readMessage(true);
var job:ThreadEvent = null;
var output:WorkOutput = #if html5 new WorkOutput(MULTI_THREADED) #else cast(Thread.readMessage(true), WorkOutput) #end;
var doWork:WorkFunction<State->WorkOutput->Void> = Thread.readMessage(true);
var job:ThreadEvent = null;
while (true)
{
// Get a job.
if (job == null)
while (true)
{
job = cast(Thread.readMessage(true), ThreadEvent);
output.resetJobProgress();
}
if (job.event == EXIT)
{
return;
}
if (job.event != WORK || job.state == null)
{
job = null;
continue;
}
// Get to work.
var interruption:Dynamic = null;
try
{
while (!output.__jobComplete.value && (interruption = Thread.readMessage(false)) == null)
// Get a job.
if (job == null)
{
output.workIterations.value++;
doWork.dispatch(job.state, output);
do
{
job = Thread.readMessage(true);
}
while (!Std.isOfType(job, ThreadEvent));
output.resetJobProgress();
}
}
catch (e)
{
output.sendError(e);
}
if (interruption == null || output.__jobComplete.value)
{
job = null;
}
else if(Std.isOfType(interruption, ThreadEvent))
{
job = interruption;
output.resetJobProgress();
}
else
{
// Ignore interruption and keep current job.
}
if (job.event == EXIT)
{
return;
}
// Do it all again.
}
if (job.event != WORK || job.state == null)
{
job = null;
continue;
}
#if html5
// Get to work.
var interruption:Dynamic = null;
try
{
while (!output.__jobComplete.value && (interruption = Thread.readMessage(false)) == null)
{
output.workIterations.value++;
doWork.dispatch(job.state, output);
}
}
catch (e)
{
output.sendError(e);
}
if (interruption == null || output.__jobComplete.value)
{
job = null;
}
else if(Std.isOfType(interruption, ThreadEvent))
{
job = interruption;
output.resetJobProgress();
}
else
{
// Ignore interruption and keep working.
}
// Do it all again.
}
});
#end
}
#end
@@ -399,7 +397,7 @@ class ThreadPool extends WorkOutput
#end
// Process the queue.
while (__jobQueue.length > 0 && currentThreads < maxThreads)
while (!__jobQueue.isEmpty() && activeJobs < maxThreads)
{
var job:ThreadEvent = __jobQueue.pop();
if (job.event != WORK)
@@ -498,8 +496,6 @@ class ThreadPool extends WorkOutput
#if lime_threads
if (mode == MULTI_THREADED)
{
__activeJobs.remove(threadEvent.associatedJob);
if (currentThreads > maxThreads || __jobQueue.isEmpty() && currentThreads > minThreads)
{
threadEvent.associatedJob.thread.sendMessage(new ThreadEvent(EXIT, null));
@@ -508,6 +504,8 @@ class ThreadPool extends WorkOutput
{
__idleThreads.push(threadEvent.associatedJob.thread);
}
__activeJobs.removeThread(threadEvent.associatedJob.thread);
}
#end
@@ -517,12 +515,25 @@ class ThreadPool extends WorkOutput
eventSource = null;
}
if (currentThreads == 0)
if (completed)
{
Application.current.onUpdate.remove(__update);
}
}
#if lime_threads
private override function createThread(executeThread:WorkFunction<Void->Void>):Thread
{
var thread:Thread = super.createThread(executeThread);
#if !html5
thread.sendMessage(this);
#end
thread.sendMessage(__doWork);
return thread;
}
#end
// Getters & Setters
private inline function get_activeJobs():Int
@@ -540,20 +551,23 @@ class ThreadPool extends WorkOutput
return activeJobs + idleThreads;
}
// Note the distinction between `doWork` and `__doWork`.
// Note the distinction between `doWork` and `__doWork`: the former is for
// backwards compatibility, while the latter is always used.
private function get_doWork():{ add: (Dynamic->Void) -> Void }
{
return {
add: function(callback:Dynamic->Void)
{
__doWork = function(state:State, output:WorkOutput):Void
{
#if html5
if (mode == MULTI_THREADED)
throw "Unsupported operation; instead pass the callback to ThreadPool's constructor.";
#end
callback(state);
};
#if html5
if (mode == MULTI_THREADED)
throw "Unsupported operation; instead pass the callback to ThreadPool's constructor.";
#end
__doWork = #if html5 { func: #end
function(state:State, output:WorkOutput):Void
{
callback(state);
}
#if html5 } #end;
}
};
}

View File

@@ -12,8 +12,11 @@ import cpp.vm.Tls;
import neko.vm.Deque;
import neko.vm.Thread;
import neko.vm.Tls;
#elseif html5
#end
#if html5
import lime._internal.backend.html5.HTML5Thread as Thread;
import lime._internal.backend.html5.HTML5Thread.Transferable;
#end
#if macro
@@ -75,12 +78,15 @@ class WorkOutput
private var __activeJobs:ActiveJobs = new ActiveJobs();
/**
Single-threaded mode only; the `state` provided to the active job. If
available, should be included in new `ThreadEvent`s.
The `state` provided to the active job. Will only have a value during
`__update()` in single-threaded mode, and will otherwise be `null`.
Include this when creating new `ThreadEvent`s.
**/
private var __activeJob:Null<State> = null;
private inline function new(mode:ThreadMode) {
private inline function new(mode:Null<ThreadMode>)
{
workIterations.value = 0;
__jobComplete.value = false;
@@ -97,7 +103,7 @@ class WorkOutput
@see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
**/
#if (lime_threads && html5) inline #end
public function sendComplete(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Dynamic> = null #end):Void
public function sendComplete(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Transferable> = null #end):Void
{
if (!__jobComplete.value)
{
@@ -120,7 +126,7 @@ class WorkOutput
@see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
**/
#if (lime_threads && html5) inline #end
public function sendError(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Dynamic> = null #end):Void
public function sendError(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Transferable> = null #end):Void
{
if (!__jobComplete.value)
{
@@ -143,7 +149,7 @@ class WorkOutput
@see https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
**/
#if (lime_threads && html5) inline #end
public function sendProgress(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Dynamic> = null #end):Void
public function sendProgress(message:Dynamic = null #if (lime_threads && html5) , transferList:Array<Transferable> = null #end):Void
{
if (!__jobComplete.value)
{
@@ -167,25 +173,21 @@ class WorkOutput
{
var thread:Thread = Thread.create(executeThread);
// Platform-dependent initialization.
#if html5
thread.onMessage.add(onMessageFromWorker.bind(thread));
#else
thread.sendMessage(this);
#end
return thread;
}
#if html5
private function onMessageFromWorker(thread:Thread, message:Dynamic):Void
private function onMessageFromWorker(thread:Thread, threadEvent:ThreadEvent):Void
{
if (!Std.isOfType(message, ThreadEvent))
if (threadEvent.event == null)
{
return;
}
var threadEvent:ThreadEvent = message;
threadEvent.associatedJob = __activeJobs.getByThread(thread);
__jobOutput.add(threadEvent);
@@ -366,6 +368,26 @@ class ThreadEvent
}
}
class JSAsync
{
/**
In JavaScript, runs the given block of code within an `async` function,
enabling the `await` keyword. On other targets, runs the code normally.
**/
public static macro function async(code:Expr):Expr
{
if (Context.defined("js"))
{
var jsCode:Expr = #if haxe4 macro js.Syntax.code #else macro untyped __js__ #end;
return macro $jsCode("(async {0})()", function() $code);
}
else
{
return code;
}
}
}
#if !(target.threaded || cpp || neko)
@:forward(push, add) @:forward.new
abstract Deque<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
public var embed:Bool;
public var forceLoad:Bool;
public var webWorker:Bool;
public var name:String;
public var path:String;

View File

@@ -1752,6 +1752,11 @@ class ProjectXMLParser extends HXProject
dependency.forceLoad = parseBool(element.att.resolve("force-load"));
}
if (element.has.resolve("web-worker"))
{
dependency.webWorker = parseBool(element.att.resolve("web-worker"));
}
var i = dependencies.length;
while (i-- > 0)

View File

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

View File

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