-
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
753 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,108 @@ | ||
package coro | ||
|
||
import ( | ||
"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 | ||
}) | ||
} |
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,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{}{}) | ||
} |
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,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() | ||
} | ||
} |
Oops, something went wrong.