Skip to content

Commit

Permalink
runtime, sycall/js/callback: add support for callbacks from JavaScrip…
Browse files Browse the repository at this point in the history
…t (WIP)

Change-Id: Ib8701cfa0536d10d69bd541c85b0e2a754eb54fb
  • Loading branch information
neelance committed May 19, 2018
1 parent 92efd83 commit 773eb46
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 45 deletions.
37 changes: 35 additions & 2 deletions misc/wasm/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
go: {
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
this.exited = true;
this.exit(mem().getInt32(sp + 8, true));
},

Expand All @@ -143,6 +144,11 @@
mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
},

// func scheduleCallback(delay int64)
"runtime.scheduleCallback": (sp) => {
setTimeout(() => { this._resolveCallbackPromise(); }, getInt64(sp + 8));
},

// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
crypto.getRandomValues(loadSlice(sp + 8));
Expand Down Expand Up @@ -270,7 +276,19 @@

async run(instance) {
this._inst = instance;
this._values = [undefined, null, global, this._inst.exports.mem]; // TODO: garbage collection
this._values = [ // TODO: garbage collection
undefined,
null,
global,
this._inst.exports.mem,
() => {
if (this.exited) {
throw new Error('bad callback: Go program has already exited (hint: use "select {}")');
}
setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
},
];
this.exited = false;

const mem = new DataView(this._inst.exports.mem.buffer)

Expand Down Expand Up @@ -304,7 +322,16 @@
offset += 8;
});

this._inst.exports.run(argc, argv);
while (true) {
const callbackPromise = new Promise((resolve) => {
this._resolveCallbackPromise = resolve;
});
this._inst.exports.run(argc, argv);
if (this.exited) {
break;
}
await callbackPromise;
}
}
}

Expand All @@ -319,6 +346,12 @@
go.env = process.env;
go.exit = process.exit;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
process.on("exit", () => { // Node.js exits if no callback is pending
if (!go.exited) {
console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!");
process.exit(1);
}
});
return go.run(result.instance);
}).catch((err) => {
console.error(err);
Expand Down
1 change: 1 addition & 0 deletions src/cmd/internal/obj/wasm/a.out.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ const (
REG_RET1
REG_RET2
REG_RET3
REG_RUN

// locals
REG_R0
Expand Down
11 changes: 9 additions & 2 deletions src/cmd/internal/obj/wasm/wasmobj.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var Register = map[string]int16{
"RET1": REG_RET1,
"RET2": REG_RET2,
"RET3": REG_RET3,
"RUN": REG_RUN,

"R0": REG_R0,
"R1": REG_R1,
Expand Down Expand Up @@ -528,6 +529,12 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
p = appendp(p, AI32Add)
p = appendp(p, ASet, regAddr(REG_SP))

if ret.To.Type == obj.TYPE_CONST && ret.To.Offset == 1 {
p = appendp(p, AI32Const, constAddr(1))
p = appendp(p, AReturn)
break
}

// not switching goroutine, return 0
p = appendp(p, AI32Const, constAddr(0))
p = appendp(p, AReturn)
Expand Down Expand Up @@ -726,7 +733,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
}
reg := p.From.Reg
switch {
case reg >= REG_PC_F && reg <= REG_RET3:
case reg >= REG_PC_F && reg <= REG_RUN:
w.WriteByte(0x23) // get_global
writeUleb128(w, uint64(reg-REG_PC_F))
case reg >= REG_R0 && reg <= REG_F15:
Expand All @@ -743,7 +750,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
}
reg := p.To.Reg
switch {
case reg >= REG_PC_F && reg <= REG_RET3:
case reg >= REG_PC_F && reg <= REG_RUN:
w.WriteByte(0x24) // set_global
writeUleb128(w, uint64(reg-REG_PC_F))
case reg >= REG_R0 && reg <= REG_F15:
Expand Down
1 change: 1 addition & 0 deletions src/cmd/link/internal/wasm/asm.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ func writeGlobalSec(ctxt *ld.Link) {
I64, // 6: RET1
I64, // 7: RET2
I64, // 8: RET3
I32, // 9: RUN
}

writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/lock_futex.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall()
return ok
}

func pauseSchedulerUntilCallback() bool {
return false
}

func checkTimeouts() {}
93 changes: 80 additions & 13 deletions src/runtime/lock_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@

package runtime

import (
_ "unsafe"
)

// js/wasm has no support for threads yet. There is no preemption.
// Waiting for a mutex or timeout is implemented as a busy loop
// while allowing other goroutines to run.
// Waiting for a mutex is as simple as allowing other goroutines
// to run until the mutex got unlocked.

const (
mutex_unlocked = 0
mutex_locked = 1

note_cleared = 0
note_woken = 1
note_timeout = 2

active_spin = 4
active_spin_cnt = 30
passive_spin = 1
Expand All @@ -35,15 +43,18 @@ func unlock(l *mutex) {

// One-time notifications.
func noteclear(n *note) {
n.key = 0
n.key = note_cleared
}

func notewakeup(n *note) {
if n.key != 0 {
print("notewakeup - double wakeup (", n.key, ")\n")
if n.key == note_woken {
throw("notewakeup - double wakeup")
}
n.key = 1
timeout := n.key == note_timeout
n.key = note_woken
if !timeout {
goready(notes[n], 1)
}
}

func notesleep(n *note) {
Expand All @@ -55,21 +66,77 @@ func notetsleep(n *note, ns int64) bool {
return false
}

type noteWithTimeout struct {
gp *g
deadline int64
}

var notes = make(map[*note]*g)
var notesWithTimeout = make(map[*note]noteWithTimeout)

// same as runtime·notetsleep, but called on user g (not g0)
func notetsleepg(n *note, ns int64) bool {
gp := getg()
if gp == gp.m.g0 {
throw("notetsleepg on g0")
}

deadline := nanotime() + ns
for {
if n.key != 0 {
return true
if ns >= 0 {
delay := ns/1000000 + 2
if delay > 1<<31-1 {
delay = 1<<31 - 1 // cap to max int32
}
Gosched()
if ns >= 0 && nanotime() >= deadline {
return false

notes[n] = gp
notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: nanotime() + ns}
scheduleCallback(delay)
gopark(nil, nil, waitReasonSleep, traceEvGoBlockRecv, 1)
delete(notes, n)
delete(notesWithTimeout, n)

return n.key == note_woken
}

for n.key != note_woken {
notes[n] = gp
gopark(nil, nil, waitReasonSleep, traceEvGoBlockRecv, 1)
delete(notes, n)
}
return true
}

func checkTimeouts() {
now := nanotime()
for n, nt := range notesWithTimeout {
if n.key == note_cleared && now > nt.deadline {
n.key = note_timeout
goready(nt.gp, 1)
}
}
}

var waitingForCallback []*g

//go:linkname sleepUntilCallback syscall/js.sleepUntilCallback
func sleepUntilCallback() {
waitingForCallback = append(waitingForCallback, getg())
gopark(nil, nil, waitReasonSleep, traceEvGoBlockRecv, 1)
}

func pauseSchedulerUntilCallback() bool {
if len(waitingForCallback) == 0 {
return false
}

pause()
checkTimeouts()
for _, gp := range waitingForCallback {
goready(gp, 1)
}
waitingForCallback = waitingForCallback[:0]
return true
}

func pause()

func scheduleCallback(delay int64)
6 changes: 6 additions & 0 deletions src/runtime/lock_sema.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall()
return ok
}

func pauseSchedulerUntilCallback() bool {
return false
}

func checkTimeouts() {}
8 changes: 8 additions & 0 deletions src/runtime/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ func forcegchelper() {
// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
checkTimeouts()
mcall(gosched_m)
}

Expand All @@ -281,6 +282,9 @@ func goschedguarded() {
// Reasons should be unique and descriptive.
// Do not re-use reasons, add new ones.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
Expand Down Expand Up @@ -2315,6 +2319,10 @@ stop:
return gp, false
}

if pauseSchedulerUntilCallback() {
goto top
}

// Before we drop our P, make a snapshot of the allp slice,
// which can change underfoot once we no longer block
// safe-points. We don't need to snapshot the contents because
Expand Down
Loading

0 comments on commit 773eb46

Please sign in to comment.