// This file is part of Gopher2600. // // Gopher2600 is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Gopher2600 is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Gopher2600. If not, see . package performance import ( "errors" "fmt" "io" "time" "github.com/jetsetilly/gopher2600/cartridgeloader" "github.com/jetsetilly/gopher2600/debugger/govern" "github.com/jetsetilly/gopher2600/hardware" "github.com/jetsetilly/gopher2600/hardware/television" "github.com/jetsetilly/gopher2600/setup" ) // sentinal error returned by Run() loop. var timedOut = errors.New("performance timed out") // Check the performance of the emulator using the supplied cartridge. // // Emulation will run of specificed duration and will create a cpu, memory // profile, a trace (or a combination of those) as defined by the Profile // argument. func Check(output io.Writer, profile Profile, cartload cartridgeloader.Loader, spec string, uncapped bool, duration string) error { var err error tv, err := television.NewTelevision(spec) if err != nil { return err } defer tv.End() // set fps cap on television tv.SetFPSCap(!uncapped) // create vcs vcs, err := hardware.NewVCS(tv, nil, nil) if err != nil { return fmt.Errorf("performance: %w", err) } // attach cartridge to the vcs err = setup.AttachCartridge(vcs, cartload, true) if err != nil { return fmt.Errorf("performance: %w", err) } // parse supplied duration dur, err := time.ParseDuration(duration) if err != nil { return fmt.Errorf("performance: %w", err) } // get starting frame number (should be 0) startFrame := tv.GetCoords().Frame // run for specified period of time runner := func() error { // setup trigger that expires when duration has elapsed. signals true // when duration has expired. signals false to indicate that // performance measurement should start timerChan := make(chan bool) // force a two second leadtime to allow framerate to settle down and // then restart timer for the specified duration // // the two second leadtime will put false on the timerChan. the // conclusion of the reset of the time will put true on the timerChan. go func() { time.AfterFunc(2*time.Second, func() { // signal parent function that 2 second leadtime has elapsed timerChan <- false // race condition when GetCoords() is called time.AfterFunc(dur, func() { timerChan <- true }) }) }() // only check for end of measurement period every PerformanceBrake CPU // instructions. checking the timerChan is relatively expensive (worth // a frame a two every 5 seconds) performanceBrake := 0 // run until specified time elapses err = vcs.Run(func() (govern.State, error) { for { performanceBrake++ if performanceBrake >= hardware.PerformanceBrake { performanceBrake = 0 select { case v := <-timerChan: // timerChan has returned true, which means measurement // period has finished, return false to cause vcs.Run() to // return if v { return govern.Ending, timedOut } // timerChan has returned false which indicates that the // leadtime has concluded. this means the performance // measurement has begun and we should record the start // frame. startFrame = tv.GetCoords().Frame default: return govern.Running, nil } } return govern.Running, nil } }) return err } // launch runner directly or through the CPU profiler, depending on // supplied arguments err = RunProfiler(profile, "performance", runner) if err != nil && !errors.Is(err, timedOut) { return fmt.Errorf("performance: %w", err) } // get ending frame number endFrame := vcs.TV.GetCoords().Frame // calculate performance numFrames := endFrame - startFrame fps, accuracy := CalcFPS(tv, numFrames, dur.Seconds()) output.Write([]byte(fmt.Sprintf("%.2f fps (%d frames in %.2f seconds) %.1f%%\n", fps, numFrames, dur.Seconds(), accuracy))) return nil }