前面花了幾個章節製作的康威生命遊戲,功能已經大致上完成了,不過我們還無法確定其效能是否還有可以優化的空間,必須要用一些方式來偵測才行。



偵測Canvas的FPS(Frames Per Second)

測量效能的指標,最常用的自然就是整個遊戲畫面的FPS了。

我們可以新增以下這個JavaScript模組,將其存檔為www/src/fps.js

const BUFFER_SIZE = 60;

export default class Fps {
    constructor(element) {
        this.fps = element;
        this.frames = [];
        this.lastFrameTimeStamp = performance.now();

        this.fps.innerHTML = '  FPS<br/>' + 'Mean: ' + 0 + '<br/>' + ' Min: ' + 0 + '<br/>' + ' Max: ' + 0;
    }

    render() {
        let now = performance.now();

        let interval = now - this.lastFrameTimeStamp;

        this.lastFrameTimeStamp = now;

        let fps = 1 / interval * 1000;

        this.frames.push(fps);
        if (this.frames.length > BUFFER_SIZE) {
            this.frames.shift();
        }

        let min = Infinity;
        let max = -Infinity;
        let sum = 0;
        for (let i = 0; i < this.frames.length; ++i) {
            sum += this.frames[i];
            min = Math.min(this.frames[i], min);
            max = Math.max(this.frames[i], max);
        }
        let mean = sum / this.frames.length;

        this.fps.innerHTML = '  FPS<br/>' + 'Mean: ' + Math.round(mean) + '<br/>' + ' Min: ' + Math.round(min) + '<br/>' + ' Max: ' + Math.round(max);
    }

    show() {
        this.fps.classList.remove("fps-hide");
    }

    hide() {
        this.fps.classList.add("fps-hide");
    }
}

這個模組可以用來測量遊戲畫面的FPS,並且輸出到某個HTML元素中。

所以我們就來新增一個專門顯示FPS的元素到HTML網頁中吧!

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Conway's Game of Life</title>
    <link href="./css/bundle.min.css" rel="stylesheet">
</head>
<body>
    <div id="fps" class="fps-hide"></div>
    <div id="controls">
        <button id="play-pause">Play</button>
        <button id="next">Next</button>
        <button id="clear">Clear</button>
    </div>
    <canvas id="game-of-life-canvas"></canvas>
    <script src="./js/bundle.min.js"></script>
</body>
</html>

SCSS/CSS中也要加入以下兩個元素:

#fps {
  font-family: monospace, monospace;
  color: #FFFFFF;
  background-color: rgba(0, 0, 0, 0.5);
  position: fixed;
  bottom: 0;
  left: 0;
  padding: 5px;
}

.fps-hide {
  display: none !important;
}

接著就是在原先的JavaScript程式中引用www/src/fps.js模組,並建立Fps的實體,在renderLoop函數中呼叫Fpsrender方法。當然,為了讓FPS偵測結果能夠在遊戲播放時顯示在HTML網頁上,播放與暫停按鈕的事件也要去呼叫showhide方法。

程式碼修改如下:

import Fps from './fps.js';

...

import('../../pkg/wasm_game_of_life')
    ...
    .then(module => {
        import('../../pkg/wasm_game_of_life_bg')
            .then(wasm => {
                ...

                let fps = new Fps(document.getElementById("fps"));

                let renderLoop = () => {
                    fps.render();

                    universe.tick();

                    drawCells();

                    if (!pause) {
                        requestAnimationFrame(renderLoop);
                    }
                };

                ...

                document.getElementById("play-pause").addEventListener("click", event => {
                    if (pause) {
                        pause = false;
                        requestAnimationFrame(renderLoop);
                
                        fps.show();
                
                        ...
                    } else {
                        pause = true;
                
                        fps.hide();
                
                        ...
                    }
                });
            });
    });

修改完成後,在康威生命遊戲開始播放時,HTML網頁的左下角就會出現FPS的偵測結果了。

rust-webassembly-conways-game-of-life-performance

正常情況下,目前我們製作的康威生命遊戲的平均FPS是能達到60的!

測量某段程式的執行時間

利用console提供的timetimeEnd函數,我們可以很輕易地測量某段程式的執行時間。

例如將JavaScript程式改寫如下:

import('../../pkg/wasm_game_of_life')
    ...
    .then(module => {
        import('../../pkg/wasm_game_of_life_bg')
            .then(wasm => {
                ...

                let renderLoop = () => {
                    ...

                    console.time('tick_tock');

                universe.tick();

                    console.timeEnd('tick_tock');

                    ...
                };

                ...
            });
    });

這邊要注意的是,使用consoletimetimeEnd函數時可傳入一個標籤,用來分辨要用哪個計時器。

如下圖所示,當康威生命遊戲正在播放時,網頁瀏覽器的主控台中就會顯示出tick方法的執行時間。

rust-webassembly-conways-game-of-life-performance

除此之外,在網頁瀏覽器提供的效能檢查工具中,也可以看到consoletimetimeEnd函數所測量到的執行時間。

rust-webassembly-conways-game-of-life-performance

找出使效能變差的程式碼

在32x32尺寸的培養皿下,執行一次requestAnimationFrame函數的所需時間約為5毫秒,執行一次tick所花的時間為1毫秒左右。為了使執行時間更容易觀察,我們先改變JavaScript程式,將培養皿的尺寸加到128x128,這樣執行一次requestAnimationFrame函數的所需時間就大概是42毫秒。在這個尺寸的培養皿下,執行一次tick所花的時間為4毫秒左右。所以我們從增長比例來判斷可以知道tick不會是造成requestAnimationFrame函數執行時間增長的主因。

那麼會大大影響執行速度的因素大概就是drawCells函數了。經過不斷地修改程式和測量效能,我們可以發現,在迴圈中指派ctx.fillStyle的值會嚴重地影響程式效能。所以drawCells函數需要被改寫成以下這樣:

import('../../pkg/wasm_game_of_life')
    ...
    .then(module => {
        import('../../pkg/wasm_game_of_life_bg')
            .then(wasm => {
                ...

                let drawCells = () => {
                    ...

                    ctx.fillStyle = BLOCK_COLOR;

                    for (let row = 0; row < HEIGHT; ++row) {
                        for (let column = 0; column < WIDTH; ++column) {
                            let index = universe.get_index(row, column);

                            if (blocks[index] !== module.Block.NoCell) {
                                continue;
                            }

                            let x = column * size + border_size + margin_size;
                            let y = row * size + border_size + margin_size;

                            ctx.fillRect(x, y, block_size, block_size);
                            ctx.strokeRect(x, y, block_size, block_size);
                        }
                    }

                    ctx.fillStyle = CELL_COLOR;

                    for (let row = 0; row < HEIGHT; ++row) {
                        for (let column = 0; column < WIDTH; ++column) {
                            let index = universe.get_index(row, column);

                            if (blocks[index] !== module.Block.HasCell) {
                                continue;
                            }

                            let x = column * size + border_size + margin_size;
                            let y = row * size + border_size + margin_size;

                            ctx.fillRect(x, y, block_size, block_size);
                            ctx.strokeRect(x, y, block_size, block_size);
                        }
                    }
                };

                ...
            });
    });

如此修改完之後,requestAnimationFrame函數的執行所需時間只剩下28毫秒左右,已被顯著地優化!

限制FPS

由於我們使用requestAnimationFrame函數來繪製畫面,這個函數會根據螢幕的刷新頻率來決定其執行的速度。一般螢幕的刷新頻率為60Hz,所以requestAnimationFrame函數繪製畫面的速度也大概是60fps。不過在這個康威生命遊戲中,過快的FPS會導致遊戲畫面中的活細胞像是發瘋一般亂竄,因此最好加個機制來限制畫面的更新頻率。

將JavaScript程式改寫如下:

...

const MAX_FPS = 15;

...

import('../../pkg/wasm_game_of_life')
    ...
    .then(module => {
        import('../../pkg/wasm_game_of_life_bg')
            .then(wasm => {
                ...

                let startTime = Date.now();
                let fpsInterval = 1 / MAX_FPS * 1000;

                let renderLoop = () => {
                    let now = Date.now();

                    if (now - startTime > fpsInterval) {
                        startTime = now;

                        fps.render();

                        universe.tick();

                        drawCells();
                    }

                    if (!pause) {
                        requestAnimationFrame(renderLoop);
                    }
                };

                ...
            });
    });

將康威生命遊戲的最大FPS限制在15之後,遊戲體驗變得更好了呢!

rust-webassembly-conways-game-of-life-performance

總結

康威生命遊戲至此可以說是已經製作完成了,展示網頁的連結如下:

https://tool.magiclen.org/game-of-life

在下一個章節中,我們要來針對WebAssembly程式的大小進行深入的探討。

下一章:縮小Web­Assembly程式