I'm implementing a fps counter for gospeccy and I'd like to use goroutines
and channels, of course. The idea is to create a service that streams the
fps values calculated over a range of timings provided by the client code
in a given time interval.
I ended up with something like this (implementation + test):
I'd like to know your opinion about this approach. In particular, I'm not
sure about the way I handle the non-blocking stream of fps values. Thank
you in advance!
Andrea
<<CODE
package spectrum
import (
"testing"
"time"
)
const (
second = 1e9
ms = 1e6
)
type FpsCounter struct {
// The channel on which timings are sent from client code
Timings chan<- int64
// Client code receives fps values from this channel
Fps <-chan float
// Same as Timings but the end we use
timings <-chan int64
timeInterval int64
// Same as Fps but the end we use
fps chan<- float
// The ticker that triggers the calculation of average fps
ticker <-chan int64
}
func NewFpsCounter(timeInterval int64) *FpsCounter {
timings := make(chan int64)
fps := make(chan float)
fpsCounter := &FpsCounter{timings, fps, timings, timeInterval, fps, time.Tick(timeInterval)}
var (
sum, numSamples int64
lastFps float
)
// Non-blocking fps stream
go func() {
for {
fpsCounter.fps <- lastFps
}
}()
// Wait for timings and calculate the sum
go func() {
for t := range timings {
sum += t
numSamples++
}
}()
// Calculate average fps and reset variables every tick
go func() {
for {
<-fpsCounter.ticker
if numSamples > 0 {
avgTime := sum / numSamples
lastFps = 1 / (float(avgTime) / second)
}
sum, numSamples = 0, 0
}
}()
return fpsCounter
}
// Helper for TestFpsCounter
func loopFor(timeInterval int64, block func(elapsedTime int64)) {
var elapsedTime int64
startTime := time.Nanoseconds()
for elapsedTime < timeInterval {
block(elapsedTime)
elapsedTime = time.Nanoseconds() - startTime
}
}
func TestFpsCounter(t *testing.T) {
// Collect timings every second
var timeInterval int64 = 1 * second
// Create a new FpsCounter service with the given timeInterval
fpsCounter := NewFpsCounter(timeInterval)
// Simulate a three seconds emulator loop
loopFor(3 * second, func(elapsedTime int64) {
fpsCounter.Timings <- 20 * ms // Send a dummy time of
// 20ms (50 fps)
fps := <-fpsCounter.Fps // Receive fps from the
// FpsCounter service
// After 2 seconds average fps should be 50
if elapsedTime > 2 * second {
if fps != 50 {
t.Errorf("fps should be 50 but got %f", fps)
}
}
})
}
CODE
--
Andrea Fazzi @ alcacoop.it
Read my blog at http://freecella.blogspot.com
Follow me on http://twitter.com/remogatto
i think you can do better by avoiding the shared variables
("share memory by communicating", right ? :-))
see the attached for one possibility. only barely tested.
Roger,
thank you very much, you show me a better and more idiomatic way of doing
it. It was the a good opportunity to read carefully the documentation about
select statements. I refactored[1] just a bit your solution getting rid of
few useless non-public struct fields (timings and ticker). Now it looks
much better.
Thank you,
Andrea
[1] - http://gist.github.com/469721
Hi,
surely your suggested design is fashinating :) My doubts with it are about
performances: I see a lot of per-frame communication between goroutines!
Currently, video memory is written *directly* on the host video surface
through the DisplayAccessor interface, there is not post-processing nor
filtering at all. This lead to highter performances, I guess. What do you
think?
Andrea
--
Andrea Fazzi @ alcacoop.it
Ok, it's definitely worth a try, thanks for the benchmarking. Would you
like to provide a patch? :) Well... At least I'll rely on your feedback if
I'll end up with something working in this sense :) This could be an
interesting way to exploit Go's pecularities in emulation...
With regard to the original question about the fps counter, I think it's a
good thing running it on separate goroutines. The counter calculates the
average fps from an average of the timings sent to it, collected during a
given time interval. The measure of the time is received by a time.Tick
channel. This receive operation is blocking thus I run it on a detached
goroutine. Do you see better/more idiomatic alternatives?
Cheers,
Andrea
--
Andrea Fazzi @ alcacoop.it