-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
736 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package co | ||
|
||
import ( | ||
"slices" | ||
) | ||
|
||
const routineCancelled = "coroutine cancelled" | ||
|
||
type Yield[V any] func(V) | ||
|
||
func Create[V any](f func(Yield[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(f) // 3 allocs | ||
|
||
return r | ||
} | ||
|
||
type Routine[V any] struct { | ||
done chan V | ||
resumed chan struct{} | ||
status Status | ||
} | ||
|
||
func (r *Routine[V]) start(f func(Yield[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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package co_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/elgopher/pi/co" | ||
) | ||
|
||
func BenchmarkCreate(b *testing.B) { | ||
b.ReportAllocs() | ||
|
||
var r *co.Routine[struct{}] | ||
|
||
for i := 0; i < b.N; i++ { | ||
r = co.Create(f) // 6 allocs :( 12us :( | ||
} | ||
|
||
_ = r | ||
} | ||
|
||
func BenchmarkResume(b *testing.B) { | ||
b.ReportAllocs() | ||
|
||
var r *co.Routine[struct{}] | ||
|
||
for i := 0; i < b.N; i++ { | ||
r = co.Create(f) // 6 allocs | ||
r.Resume() // 1 alloc, 0.8us :( | ||
} | ||
_ = r | ||
} | ||
|
||
func BenchmarkResumeUntilFinish(b *testing.B) { | ||
b.ReportAllocs() | ||
|
||
var r *co.Routine[struct{}] | ||
|
||
for i := 0; i < b.N; i++ { | ||
r = co.Create(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 *co.Routine[struct{}] | ||
|
||
for i := 0; i < b.N; i++ { | ||
r = co.Create(f) // 6 allocs | ||
r.Cancel() // -2 alloc???? | ||
} | ||
_ = r | ||
} | ||
|
||
//go:noinline | ||
func f(yield co.Yield[struct{}]) { | ||
yield(struct{}{}) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package main | ||
|
||
import ( | ||
"math/rand" | ||
"net/http" | ||
|
||
"github.com/elgopher/pi" | ||
"github.com/elgopher/pi/co" | ||
"github.com/elgopher/pi/ebitengine" | ||
) | ||
|
||
var coroutines co.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++ { // (~9KB per COROUTINE). Pico-8 has 4000 coroutines limit | ||
coroutines = append(coroutines, co.Create(complexCoroutine())) // complexCoroutine is 2 coroutines - 18KB in total | ||
} | ||
} | ||
} | ||
|
||
pi.Draw = func() { | ||
pi.Cls() | ||
coroutines = coroutines.ResumeAll() | ||
//devtools.Export("coroutines", coroutines) | ||
} | ||
|
||
ebitengine.Run() | ||
} | ||
|
||
func movePixel(pos pi.Position) func(yield co.Yield[struct{}]) { | ||
return func(yield co.Yield[struct{}]) { | ||
for i := 0; i < 64; i++ { | ||
pi.Set(pos.X+i, pos.Y+i, byte(rand.Intn(16))) | ||
yield(struct{}{}) | ||
yield(struct{}{}) | ||
} | ||
} | ||
} | ||
|
||
func moveHero(startX, stopX, minSpeed, maxSpeed int) func(yield co.Yield[struct{}]) { | ||
anim := co.Create(randomMove(startX, stopX, minSpeed, maxSpeed)) | ||
|
||
return func(yield co.Yield[struct{}]) { | ||
for { | ||
x, hasMore := anim.Resume() | ||
pi.Set(x, 20, 7) | ||
if hasMore { | ||
yield(struct{}{}) | ||
} else { | ||
return | ||
} | ||
|
||
} | ||
} | ||
} | ||
|
||
// Reusable coroutine which returns int. | ||
func randomMove(start, stop, minSpeed, maxSpeed int) func(yield co.Yield[int]) { | ||
pos := start | ||
|
||
return func(yield co.Yield[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 complexCoroutine() func(yield co.Yield[struct{}]) { | ||
return func(yield co.Yield[struct{}]) { | ||
sleep(10)(yield) | ||
moveHero(10, 120, 5, 10)(yield) | ||
sleep(20)(yield) | ||
moveHero(120, 10, 2, 10)(yield) | ||
} | ||
} | ||
|
||
func sleep(iterations int) func(yield co.Yield[struct{}]) { | ||
return func(yield co.Yield[struct{}]) { | ||
for i := 0; i < iterations; i++ { | ||
yield(struct{}{}) | ||
} | ||
} | ||
} |
Oops, something went wrong.