-
-
Save xavriley/3c319f82d896762ee75647dfe91cdc00 to your computer and use it in GitHub Desktop.
// Proof of concept for blocking sleep with iteration using jsfiddle | |
// note the parallel iteration | |
function sleep(ms) { | |
return new Promise(resolve => setTimeout(resolve, ms)); | |
} | |
async function demo() { | |
console.log("boom") | |
await sleep(1000); | |
} | |
async function demo2() { | |
while(true) { | |
await demo(); | |
} | |
} | |
async function demo3() { | |
console.log("tick") | |
await sleep(500); | |
} | |
async function demo4() { | |
while(true) { | |
await demo3(); | |
} | |
} | |
demo2(); | |
demo4(); |
# takes 20 seconds to run so be patient | |
start_time = Time.now.to_f | |
sched_ahead = 0.5 | |
virtual_time = start_time + sched_ahead | |
10.times do | |
sleep_time = 2 | |
# calculate an "ideal" time that we'd like to sleep for | |
# and cache it | |
virtual_time = virtual_time + sleep_time | |
# get the real time | |
# This will most likely be a few milliseconds further on | |
# than our ideal time because of computation time etc. | |
now = Time.now.to_f | |
puts now | |
# sleep for slightly less than our ideal time, | |
# taking into account the delay | |
sleep(virtual_time - now) | |
end |
window.AudioContext = window.AudioContext || window.webkitAudioContext; | |
/// custom buffer loader | |
/// see http://www.html5rocks.com/en/tutorials/webaudio/intro/ | |
function BufferLoader(context, urlList, callback) { | |
this.context = context; | |
this.urlList = urlList; | |
this.onload = callback; | |
this.bufferList = new Array(); | |
this.loadCount = 0; | |
} | |
BufferLoader.prototype.loadBuffer = function (url, index) { | |
// Load buffer asynchronously | |
var request = new XMLHttpRequest(); | |
request.open("GET", url, true); | |
request.responseType = "arraybuffer"; | |
var loader = this; | |
request.onload = function () { | |
// Asynchronously decode the audio file data in request.response | |
loader.context.decodeAudioData( | |
request.response, | |
function (buffer) { | |
if (!buffer) { | |
alert('error decoding file data: ' + url); | |
return; | |
} | |
loader.bufferList[index] = buffer; | |
if (++loader.loadCount == loader.urlList.length) loader.onload(loader.bufferList); | |
}, | |
function (error) { | |
console.error('decodeAudioData error', error); | |
}); | |
} | |
request.onerror = function (e) { | |
alert('BufferLoader: XHR error'); | |
console.log(e); | |
} | |
request.send(); | |
} | |
BufferLoader.prototype.load = function () { | |
for (var i = 0; i < this.urlList.length; ++i) | |
this.loadBuffer(this.urlList[i], i); | |
} | |
/// setup audio context and start loading samples | |
var actx = | |
new AudioContext(), | |
blst, | |
bLoader = new BufferLoader( | |
actx, [ | |
'https://dl.dropboxusercontent.com/s/mide1jl8ks3hdmd/drum_cymbal_closed.flac', | |
'https://dl.dropboxusercontent.com/s/mide1jl8ks3hdmd/drum_cymbal_closed.flac', | |
'https://dl.dropboxusercontent.com/s/zc5nu4pm4oree63/bd_haus.flac'], | |
done), | |
isReady = false; | |
/// start loading the samples | |
bLoader.load(); | |
/// when samples are loaded update status | |
function done(bl) { | |
blst = bl; | |
isReady = true; | |
$('#status').html('Ready!'); | |
} | |
/// this sets up chain so we can play audio | |
function play(i) { | |
var src = actx.createBufferSource(); | |
src.buffer = blst[i]; | |
src.connect(actx.destination); | |
src.start(0); | |
} | |
/// check keys | |
$(window).bind("keydown", function (key) { | |
if (!isReady) return; | |
switch (parseInt(key.which, 10)) { | |
case 65: | |
play(0); | |
break; | |
case 83: | |
play(1); | |
break; | |
case 68: | |
play(2); | |
break; | |
} | |
}) |
things I don't understand
If I make a bridged class with
%x{
var bridge_class_demo = Object.getPrototypeOf(async function(){}).constructor;
bridge_class_demo.prototype.$sleep = function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); };
}
class BridgeFoo < `bridge_class_demo`
def self.baz
puts "in block"
#`await self.$sleep(2000)`
puts "still in block"
end
end
BridgeFoo.baz
can I get the class to detect if its prototype is an async function and then signal that to any new functions which are created under it?
What are rewriters? Can I get them to do this for me?
I'm hoping to use Opal to implement a subset of Sonic Pi (a Ruby based live coding music environment http://sonic-pi.net/) in the browser with web audio. Most of that should be straightforward but a key part of that API is how it uses multiple threads with calls to sleep
.
While I don't have threads in JS, I can approximate a non-blocking sleep with the following:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function kick() {
console.log("boom")
await sleep(1000);
}
async function hihats() {
console.log("tick")
await sleep(500);
console.log("tick")
await sleep(500);
}
// viewing the console shows these execute concurrently
kick();
hihats();
In terms of porting to Opal, I had the following idea. If a class is bridged from an AsyncFunction
, the parser could check for
(async function(){}).constructor.name === "AsyncFunction"
and add the async
keyword in front of the function call for generated methods. That would allow for something like the following:
%x{
var async_bridge_class_demo = Object.getPrototypeOf(async function(){}).constructor;
async_bridge_class_demo.prototype.$sleep = function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); };
}
class BridgeFoo < `async_bridge_class_demo`
def self.baz
puts "in block"
`await #{sleep(2000)}`
puts "still in block"
end
end
BridgeFoo.baz
At the moment this fails because the await
keyword is placed inside a non-async generated function.
Firstly, does this seem like a reasonable approach? Secondly, is this the kind of thing that I'd use a rewriter for?
Ideally I'd like to be able to make the await
implicit so that sleep(2000)
doesn't have to be placed in backticks but I can't think of a better way at present.
checking the type might not be a great idea in general but yolo tc39/proposal-async-await#78
other option - can a parsed block be passed to a bridged method? That way I could define the async method in JS, then bridge it and pass a block to that
suggested approach from Gitter - use the JS
constant to mark functions
require 'js'
class Foo
include JS
JS.async def self.bar
JS.await puts "x"
end
end
Foo.bar
This would create the following sexp
$ bundle exec opal --sexp test.js.rb
s(:begin,
s(:send, nil, :require,
s(:str, "js")),
s(:class,
s(:const, nil, :Foo), nil,
s(:begin,
s(:send, nil, :include,
s(:const, nil, :JS)),
s(:send,
s(:const, nil, :JS), :async,
s(:defs,
s(:self), :bar,
s(:args),
s(:send,
s(:const, nil, :JS), :await,
s(:send, nil, :puts,
s(:str, "x"))))))),
s(:send,
s(:const, nil, :Foo), :bar))
In this case we could use a Rewriter to define a transform for s(:const, nil, :JS), :async,
to s(:async, ...)
(or something). We can then write a new node type in lib/opal/nodes/async.rb
to push the async
keyword (I think)
Another sexp example - maybe adding a rewriter for blocks only would be simpler?
require 'js'
class Foo
include JS
def self.bar(&block)
JS.async block.call
end
end
Foo.bar do
JS.await sleep(2)
end
$ bundle exec opal --sexp test.js.rb
s(:begin,
s(:send, nil, :require,
s(:str, "js")),
s(:class,
s(:const, nil, :Foo), nil,
s(:begin,
s(:send, nil, :include,
s(:const, nil, :JS)),
s(:defs,
s(:self), :bar,
s(:args,
s(:blockarg, :block)),
s(:send,
s(:const, nil, :JS), :async,
s(:send,
s(:lvar, :block), :call))))),
s(:send,
s(:const, nil, :Foo), :bar,
s(:iter,
s(:args),
s(:send,
s(:const, nil, :JS), :await,
s(:send, nil, :sleep,
s(:int, 2))))))
I'm using this gist as a sort of work log as I try things out. I did have second thoughts around contributing to Opal based on some history around that particular project. I was going to include the following statement in my commit message but after engaging with the core committers there I agreed to use a different forum. I'm including the comment here for posterity:
Finally, if my contributions to the project are accepted I agree to the
terms of the code of conduct. For my own peace of mind though, I would
like to go on record to say that I don't share the views of other
maintainers on the issue of transgender discrimination in particular.
I don't want to make any more political statements than necessary as
this project has already hosted a great deal of discussion. However, as a sibling of
mine is a) currently transitioning gender and b) working towards a career in coding I
don't feel I can ignore the issue with a clear conscience.
Rather than walking away or ignoring the project I have chosen to engage and to
contribute if I can. My view is that I'd like to move forward with
positivity but this doesn't imply that I condone or support the views
I've seen expressed by maintainers elsewhere.
thinking about it that might be the thing to do. Create a special method in Opal that marks a block as async and then also implement a sleep with
await
prepended to it for use in said block