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:
11
include.xml
11
include.xml
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user