Performance?: Don't unload chunks so eagerly

Previously: the Chunker tracks with chunks were gotten during the
current game tick and the N-1 and N-2 ticks, and chunks not accessed in
two ticks were freed immediately.

Now: they go into a "garbage collection" pool with a minimum number of
game ticks to free. So if they're needed again, they're saved from the
gc pool. F3 overlay data shows the count of the gc pool.
This commit is contained in:
Noah 2022-05-07 17:16:03 -07:00
parent 315c8a81a0
commit ffc2c6f69b
6 changed files with 29 additions and 9 deletions

View File

@ -30,6 +30,8 @@ New levels:
* **Swimming** (Tutorial, Lesson 5) - a tutorial level to learn how * **Swimming** (Tutorial, Lesson 5) - a tutorial level to learn how
"water pixels" work with some moderately safe platforming puzzles "water pixels" work with some moderately safe platforming puzzles
included. included.
* **Night Sky** (Azulian Tag) - a moderately difficult Azulian Tag level
with relatively few enemies but plenty of tricky platforming.
* Some of the existing levels have had minor updates to take advantage * Some of the existing levels have had minor updates to take advantage
of newer game features, such as the water being re-done for the Castle of newer game features, such as the water being re-done for the Castle
level. level.

View File

@ -134,8 +134,9 @@ var (
EagerRenderLevelChunks = true EagerRenderLevelChunks = true
// Number of chunks margin outside the Canvas Viewport for the LoadingViewport. // Number of chunks margin outside the Canvas Viewport for the LoadingViewport.
LoadingViewportMarginChunks = render.NewPoint(8, 4) // hoz, vert LoadingViewportMarginChunks = render.NewPoint(10, 8) // hoz, vert
CanvasLoadUnloadModuloTicks uint64 = 2 CanvasLoadUnloadModuloTicks uint64 = 4
CanvasChunkFreeChoppingBlockTicks uint64 = 128 // number of ticks old a chunk is to free it
) )
// Edit Mode Values // Edit Mode Values

View File

@ -294,7 +294,7 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
} }
if s.UI.Canvas != nil { if s.UI.Canvas != nil {
inside, outside := s.UI.Canvas.LoadUnloadMetrics() inside, outside := s.UI.Canvas.LoadUnloadMetrics()
*s.debLoadingViewport = fmt.Sprintf("%d in %d out %d cached", inside, outside, s.UI.Canvas.Chunker().CacheSize()) *s.debLoadingViewport = fmt.Sprintf("%d in %d out %d cached %d gc", inside, outside, s.UI.Canvas.Chunker().CacheSize(), s.UI.Canvas.Chunker().GCSize())
} }
// Has the window been resized? // Has the window been resized?

View File

@ -40,8 +40,9 @@ type Chunker struct {
// are actively wanted by the game. // are actively wanted by the game.
lastTick uint64 // NOTE: tracks from shmem.Tick lastTick uint64 // NOTE: tracks from shmem.Tick
chunkRequestsThisTick map[render.Point]interface{} chunkRequestsThisTick map[render.Point]interface{}
requestsN1 map[render.Point]interface{} requestsN1 map[render.Point]interface{} // chunks accessed last tick
requestsN2 map[render.Point]interface{} requestsN2 map[render.Point]interface{} // 2 ticks ago (to free soon)
chunksToFree map[render.Point]uint64 // chopping block (free after X ticks)
requestMu sync.Mutex requestMu sync.Mutex
// The palette reference from first call to Inflate() // The palette reference from first call to Inflate()
@ -57,6 +58,7 @@ func NewChunker(size int) *Chunker {
chunkRequestsThisTick: map[render.Point]interface{}{}, chunkRequestsThisTick: map[render.Point]interface{}{},
requestsN1: map[render.Point]interface{}{}, requestsN1: map[render.Point]interface{}{},
requestsN2: map[render.Point]interface{}{}, requestsN2: map[render.Point]interface{}{},
chunksToFree: map[render.Point]uint64{},
} }
} }
@ -388,15 +390,25 @@ func (c *Chunker) FreeCaches() int {
delete_coords = []render.Point{} delete_coords = []render.Point{}
) )
// Chunks not requested this last tick, unload from the cache. // Chunks requested 2 ticks ago but not this tick, put on the chopping
// block to free them later.
for coord := range requestsN2 { for coord := range requestsN2 {
// Old point not requested recently? // Old point not requested recently?
if _, ok := requestsThisTick[coord]; !ok { if _, ok := requestsThisTick[coord]; !ok {
c.chunksToFree[coord] = shmem.Tick + balance.CanvasChunkFreeChoppingBlockTicks
}
}
// From the chopping block, see if scheduled chunks to free are ready.
for coord, expireAt := range c.chunksToFree {
if shmem.Tick > expireAt {
delete_coords = append(delete_coords, coord) delete_coords = append(delete_coords, coord)
} }
} }
// Free any eligible chunks NOW.
for _, coord := range delete_coords { for _, coord := range delete_coords {
delete(c.chunksToFree, coord)
c.FreeChunk(coord) c.FreeChunk(coord)
} }

View File

@ -82,7 +82,7 @@ func (c *Chunker) MigrateZipfile(zf *zip.Writer) error {
continue continue
} }
log.Info("Copy existing chunk %s", file.Name) log.Debug("Copy existing chunk %s", file.Name)
if err := zf.Copy(file); err != nil { if err := zf.Copy(file); err != nil {
return err return err
} }
@ -105,7 +105,7 @@ func (c *Chunker) MigrateZipfile(zf *zip.Writer) error {
} }
filename := fmt.Sprintf("chunks/%d/%s.json", c.Layer, coord.String()) filename := fmt.Sprintf("chunks/%d/%s.json", c.Layer, coord.String())
log.Info("Flush in-memory chunks to %s", filename) log.Debug("Flush in-memory chunks to %s", filename)
chunk.ToZipfile(zf, filename) chunk.ToZipfile(zf, filename)
} }
@ -130,6 +130,11 @@ func (c *Chunker) CacheSize() int {
return len(c.Chunks) return len(c.Chunks)
} }
// GCSize returns the number of chunks pending free (not accessed in 2+ ticks)
func (c *Chunker) GCSize() int {
return len(c.chunksToFree)
}
// ToZipfile writes just a chunk's data into a zipfile. // ToZipfile writes just a chunk's data into a zipfile.
func (c *Chunk) ToZipfile(zf *zip.Writer, filename string) error { func (c *Chunk) ToZipfile(zf *zip.Writer, filename string) error {
writer, err := zf.Create(filename) writer, err := zf.Create(filename)

View File

@ -711,7 +711,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
*s.debViewport = s.drawing.Viewport().String() *s.debViewport = s.drawing.Viewport().String()
*s.debScroll = s.drawing.Scroll.String() *s.debScroll = s.drawing.Scroll.String()
inside, outside := s.drawing.LoadUnloadMetrics() inside, outside := s.drawing.LoadUnloadMetrics()
*s.debLoadUnload = fmt.Sprintf("%d in %d out %d cached", inside, outside, s.drawing.Chunker().CacheSize()) *s.debLoadUnload = fmt.Sprintf("%d in %d out %d cached %d gc", inside, outside, s.drawing.Chunker().CacheSize(), s.drawing.Chunker().GCSize())
// Update the timer. // Update the timer.
s.timerLabel.Text = savegame.FormatDuration(time.Since(s.startTime)) s.timerLabel.Text = savegame.FormatDuration(time.Since(s.startTime))