Skip to content

Instantly share code, notes, and snippets.

@xavriley
Last active November 3, 2017 15:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xavriley/3c319f82d896762ee75647dfe91cdc00 to your computer and use it in GitHub Desktop.
Save xavriley/3c319f82d896762ee75647dfe91cdc00 to your computer and use it in GitHub Desktop.
Sonic Pi timing demo in Opal Ruby
// 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;
}
})
@xavriley
Copy link
Author

`Opal.defn(Opal.Object, '$sleep', TMP_1 = function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}, TMP_1.$arity=1);`

`
var TMP_2;
Opal.defn(Opal.Object, '$demo', TMP_2 = async function $$demo() {
var self = this; iter = TMP_2.$$p, block = $iter || nil;
  TMP_2.$$p = null;
  return self.$puts(block.$call());
}, TMP_2.$$arity=0);`

def baz(&block)
  demo block
end

baz do
  puts "in block"
  `await self.$sleep(2000)`
  puts "still in block"
end

This approach might work - namely hand rolling a function marked as async and passing a block.

Looking at the compiled JS it seems the block is also written as a (non-async) function so I'd have to fiddle with the Opal code generator.

@xavriley
Copy link
Author

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

@xavriley
Copy link
Author

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?

@xavriley
Copy link
Author

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.

@xavriley
Copy link
Author

checking the type might not be a great idea in general but yolo tc39/proposal-async-await#78

@xavriley
Copy link
Author

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

@xavriley
Copy link
Author

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)

@xavriley
Copy link
Author

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

@xavriley
Copy link
Author

xavriley commented Nov 3, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment