Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add coroutines #97

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions coro/coro.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package coro

import (
"slices"

Check failure on line 4 in coro/coro.go

View workflow job for this annotation

GitHub Actions / all (1.20)

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.20.7/x64/src/slices)
)

const routineCancelled = "coroutine cancelled"

type Yield func()

func New(resume func(yield Yield)) *Routine[struct{}] {
return WithReturn(func(y YieldReturn[struct{}]) {
resume(func() {
y(struct{}{})
})
})
}

type YieldReturn[V any] func(V)

func WithReturn[V any](resume func(YieldReturn[V])) *Routine[V] {
r := &Routine[V]{ // 1 alloc
resumed: make(chan struct{}), // 1 alloc
done: make(chan V), // 1 alloc
status: Suspended,
}
go r.start(resume) // 3 allocs

return r
}

type Routine[V any] struct {
done chan V
resumed chan struct{}
status Status
}

func (r *Routine[V]) start(f func(YieldReturn[V])) { // 1 alloc
defer r.recoverAndDestroy()

_, ok := <-r.resumed // 2 allocs
if !ok {
panic(routineCancelled)
}

r.status = Running
f(r.yield)
}

func (r *Routine[V]) yield(v V) {
r.done <- v
r.status = Suspended
if _, ok := <-r.resumed; !ok {
panic(routineCancelled)
}
}

func (r *Routine[V]) recoverAndDestroy() {
p := recover()
if p != nil && p != routineCancelled {
panic("coroutine panicked")
}
r.status = Dead
close(r.done)
}

func (r *Routine[V]) Resume() (value V, hasMore bool) {
if r.status == Dead {
return
}

r.resumed <- struct{}{}
value, hasMore = <-r.done
return
}

func (r *Routine[V]) Status() Status {
return r.status
}

func (r *Routine[V]) Cancel() {
if r.status == Dead {
return
}

close(r.resumed)
<-r.done
}

type Status string

const (
// Normal Status = "normal" // This coroutine is currently waiting in coresume for another coroutine. (Either for the running coroutine, or for another normal coroutine)
Running Status = "running" // This is the coroutine that's currently running - aka the one that just called costatus.
Suspended Status = "suspended" // This coroutine is not running - either it has yielded or has never been resumed yet.
Dead Status = "dead" // This coroutine has either returned or died due to an error.
)

type Routines []*Routine[struct{}]

func (r Routines) ResumeAll() Routines {
for _, rout := range r {
rout.Resume()
}
return slices.DeleteFunc(r, func(r *Routine[struct{}]) bool {
return r.Status() == Dead
})
}
78 changes: 78 additions & 0 deletions coro/coro_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package coro_test

import (
"testing"

"github.com/elgopher/pi/coro"
)

func BenchmarkNew(b *testing.B) {
b.ReportAllocs()

var r *coro.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = coro.New(f2) // 7 allocs :( 4us on windows :( But on linux it is 1us and 5 allocs!
}

_ = r
}

func BenchmarkCreate(b *testing.B) {
b.ReportAllocs()

var r *coro.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = coro.WithReturn(f) // 6 allocs :( 4us on windows :( But on linux it is 1us and 5 allocs!
}

_ = r
}

func BenchmarkResume(b *testing.B) {
b.ReportAllocs()

var r *coro.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = coro.WithReturn(f) // 6 allocs
r.Resume() // 1 alloc, 0.8us :(
}
_ = r
}

func BenchmarkResumeUntilFinish(b *testing.B) {
b.ReportAllocs()

var r *coro.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = coro.WithReturn(f) // 6 allocs
r.Resume() // 1 alloc, 0.8us :(
r.Resume() // 1 alloc, 0.8us :(
}
_ = r
}

func BenchmarkCancel(b *testing.B) {
b.ReportAllocs()

var r *coro.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = coro.WithReturn(f) // 6 allocs
r.Cancel() // -2 alloc????
}
_ = r
}

//go:noinline
func f2(yield coro.Yield) {
yield()
}

//go:noinline
func f(yield coro.YieldReturn[struct{}]) {
yield(struct{}{})
}
2 changes: 2 additions & 0 deletions devtools/internal/lib/github_com-elgopher-pi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 91 additions & 0 deletions examples/coroutine/coroutine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"math/rand"
"net/http"

"github.com/elgopher/pi"
"github.com/elgopher/pi/coro"
"github.com/elgopher/pi/ebitengine"
)

var coroutines coro.Routines

func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()

pi.Update = func() {
if pi.MouseBtnp(pi.MouseLeft) {
//r := movePixel(pi.MousePos)
for j := 0; j < 8000; j++ { // (~6-9KB per COROUTINE). Pico-8 has 4000 coroutines limit
r := coro.New(func(yield coro.Yield) {
sleep(10, yield)
moveHero(10, 120, 5, 10, yield)
sleep(20, yield)
moveHero(120, 10, 2, 10, yield)
})
coroutines = append(coroutines, r) // complexCoroutine is 2 coroutines - 12-18KB in total
}
}
}

pi.Draw = func() {
pi.Cls()
coroutines = coroutines.ResumeAll()
//devtools.Export("coroutines", coroutines)
}

ebitengine.Run()
}

func movePixel(pos pi.Position, yield coro.Yield) {
for i := 0; i < 64; i++ {
pi.Set(pos.X+i, pos.Y+i, byte(rand.Intn(16)))
yield()
yield()
}
}

func moveHero(startX, stopX, minSpeed, maxSpeed int, yield coro.Yield) {
anim := coro.WithReturn(randomMove(startX, stopX, minSpeed, maxSpeed))

for {
x, hasMore := anim.Resume()
pi.Set(x, 20, 7)
if hasMore {
yield()
} else {
return
}
}
}

// Reusable coroutine which returns int.
func randomMove(start, stop, minSpeed, maxSpeed int) func(yield coro.YieldReturn[int]) {
pos := start

return func(yield coro.YieldReturn[int]) {
for {
speed := rand.Intn(maxSpeed - minSpeed)
if stop > start {
pos = pi.MinInt(stop, pos+speed) // move pos in stop direction by random speed
} else {
pos = pi.MaxInt(stop, pos-speed)
}

if pos == stop {
return
} else {
yield(pos)
}
}
}
}

func sleep(iterations int, yield coro.Yield) {
for i := 0; i < iterations; i++ {
yield()
}
}
Loading
Loading