ぼくがかんがえたさいきょうのWasm(Emscripten/C++)テスト環境
TL; DR
WebAssembly でビルドした GoogleTest のテストコードのバイナリが、ブラウザで実行されているのにコンソールで実行結果が見えるよ!
ついでにベンチマークも出来るよ!
はじめに
WebAssembly のコードを C++ で書いたとき、テスト実行はどうしようというのが問題でした。
GoogleTest はあるものの、WebAssembly はブラウザで実行しないとならないし、
Emscripten は Wasmer などの他のランタイムでは動かないようだし…
そんな中、ふと見た emrun が気になりました。
結果から言えばやりたいことが出来る素晴らしいソリューションでした。
また、WebAssembly を書くモチベーションとして一番多いだろう理由は実行パフォーマンスだと思います。パフォーマンスを確認することも重要です。この記事では emrun を使った GoogleTest の実行方法に加えて、Google Benchmark を使ったWasmコードのベンチマークを VS Code で行う方法と GitHub Actions を使って継続的に行う方法について記述します。
ビルド環境
Emscripten を利用する C++ コードの開発環境ということで、まずは必要なものを揃えていきましょう。ぼくがかんがえたさいきょうのWasmビルド環境の記事で紹介した通り、VS Code Remote Containers 拡張を使ってビルドするのが色々手っ取り早いですが、今回の記事の範囲程度であればローカルホストに Emscripten の環境を用意するだけでも構わないです。
devcontainer 関連
まずは devcontainer.json
と Dockerfile
を .devcontainer
ディレクトリに用意しましょう。
{
"name": "Emscripten",
"build": {
"dockerfile": "Dockerfile",
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-vscode.cpptools",
"twxs.cmake"
],
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
ARG VARIANT=ubuntu-20.04
FROM mcr.microsoft.com/vscode/devcontainers/cpp:0-${VARIANT} AS base
RUN apt-get update && apt-get install -y libappindicator1 fonts-liberation libnss3 libdrm2 libgbm1 xdg-utils firefox
RUN curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
dpkg -i google-chrome-stable_current_amd64.deb && \
rm google-chrome-stable_current_amd64.deb
RUN curl -sSLO https://github.com/emscripten-core/emsdk/archive/refs/tags/3.0.0.tar.gz && \
mkdir /emsdk && \
tar zxvf 3.0.0.tar.gz -C /emsdk --strip-components=1 && \
chown -R vscode:vscode /emsdk && \
rm 3.0.0.tar.gz
RUN /emsdk/emsdk install latest && /emsdk/emsdk activate latest
RUN echo '. /emsdk/emsdk_env.sh > /dev/null 2>&1' >> /home/vscode/.bashrc
Docker イメージの中には Chrome と Firefox を入れておきます。Chrome はイメージをビルドしたときの最新版になるので、必要に応じてビルドし直したりして最新版を保つようにしましょう。
リポジトリ・エディタ設定関連
ついでに補助的な設定ファイルをいくつか。
build/
BasedOnStyle: 'LLVM'
[*]
indent_style = space
indent_size = 2
ソースコード
簡略化のため今回は全部のソースコードをリポジトリのルートに置きます。
メインとなる、ブラウザ JS から呼び出されることを目的とした関数群を wasm.cpp
に記述します。
今回は加算を行う add
と 減算を行う sub
を実装することにします。
#ifdef __EMSCRIPTEN__
#include "emscripten.h"
#else
#define EMSCRIPTEN_KEEPALIVE
#endif
extern "C" {
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
// simply return a + b
return a + b;
}
EMSCRIPTEN_KEEPALIVE
int sub(int a, int b) {
// Ooops!
return a + b;
}
}
おっと、いかにもコピペの弊害的なバグが入ってますが、今は見て見ぬふりをしてください。このバグをテストで検出できるか、後の方で確認してみます。
テストやベンチマークで呼び出すのでヘッダも書いておきましょう。
#ifndef WASM_HPP_
#define WASM_HPP_
extern "C" {
int add(int a, int b);
int sub(int a, int b);
}
#endif
もう一つ、emrun の実験のためのソースコードを書いておきます。Hello World についでに引数表示機能をつけたような単純なコードです(バリバリのC言語ソースコードですが、cpp拡張子にしてC++でビルドしておきます)。
#include <stdio.h>
int main(int argc, char *argv[]) {
// simple hello world
printf("Hello, world! argc:%d\n", argc);
for (int i = 0; i < argc; ++i) {
printf("argv[%d]:[%s]\n", i, argv[i]);
}
}
テストコードを記述する wasm_test.cpp
とベンチマークコードを記述する wasm_perf.cpp
、実行用のシェルスクリプト run_test.sh
も同じようにトップレベルに置くのですが、内容は後述します。
ビルド用ファイル
テスト・ベンチマークを含んだ CMakeLists.txt
はこのようになります。今回、Wasm を呼び出すコードを書くのをちょっとサボって .html を Emscripten に生成させています(set_property(TARGET wasm PROPERTY SUFFIX ".html")
の部分)。
cmake_minimum_required(VERSION 3.10)
project(wasm LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
# Main WebAssembly wasm/js/(sample)html
if(CMAKE_CROSSCOMPILING)
add_executable(wasm wasm.cpp)
set_property(TARGET wasm PROPERTY SUFFIX ".html")
endif()
# Hello world
add_executable(hello hello.cpp)
if(CMAKE_CROSSCOMPILING)
set_property(TARGET hello PROPERTY SUFFIX ".html")
target_link_options(hello PRIVATE "--emrun")
endif()
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.11.0
)
FetchContent_Declare(
googlebenchmark
GIT_REPOSITORY https://github.com/google/benchmark.git
GIT_TAG v1.6.0
)
set(BENCHMARK_ENABLE_TESTING OFF)
FetchContent_MakeAvailable(googletest googlebenchmark)
# Test
enable_testing()
include(GoogleTest)
add_executable(wasm_test wasm_test.cpp wasm.cpp)
target_link_libraries(wasm_test gtest_main)
if(CMAKE_CROSSCOMPILING)
file(COPY "${CMAKE_SOURCE_DIR}/run_test.sh" DESTINATION ${CMAKE_BINARY_DIR} USE_SOURCE_PERMISSIONS)
target_link_options(wasm_test PRIVATE "--emrun")
set_property(TARGET wasm_test PROPERTY SUFFIX ".html")
set_property(TARGET wasm_test PROPERTY CROSSCOMPILING_EMULATOR "./run_test.sh")
endif()
gtest_discover_tests(wasm_test DISCOVERY_TIMEOUT 20)
# Performance benchmark
add_executable(wasm_perf wasm_perf.cpp wasm.cpp)
target_link_libraries(wasm_perf benchmark_main)
if(CMAKE_CROSSCOMPILING)
target_link_options(wasm_perf PRIVATE "--emrun")
set_property(TARGET wasm_perf PROPERTY SUFFIX ".html")
endif()
ビルド
vscode ➜ /workspaces/docker-wasm-cmake-gtest (master ✗) $ mkdir -p build/wasm
vscode ➜ /workspaces/docker-wasm-cmake-gtest (master ✗) $ cd build/wasm
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master ✗) $ emcmake cmake ../..
configure: cmake ../.. -DCMAKE_TOOLCHAIN_FILE=/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_CROSSCOMPILING_EMULATOR=/emsdk/node/14.15.5_64bit/bin/node
-- Found Python: /usr/bin/python3.8 (found version "3.8.10") found components: Interpreter
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE
-- Found Git: /usr/bin/git (found version "2.25.1")
-- git version: v1.6.0 normalized to 1.6.0
-- Version: 1.6.0
-- Performing Test HAVE_CXX_FLAG_STD_CXX11
-- Performing Test HAVE_CXX_FLAG_STD_CXX11 - Success
-- Performing Test HAVE_CXX_FLAG_WALL
-- Performing Test HAVE_CXX_FLAG_WALL - Success
-- Performing Test HAVE_CXX_FLAG_WEXTRA
-- Performing Test HAVE_CXX_FLAG_WEXTRA - Success
-- Performing Test HAVE_CXX_FLAG_WSHADOW
-- Performing Test HAVE_CXX_FLAG_WSHADOW - Success
-- Performing Test HAVE_CXX_FLAG_WERROR
-- Performing Test HAVE_CXX_FLAG_WERROR - Success
-- Performing Test HAVE_CXX_FLAG_WSUGGEST_OVERRIDE
-- Performing Test HAVE_CXX_FLAG_WSUGGEST_OVERRIDE - Success
-- Performing Test HAVE_CXX_FLAG_PEDANTIC
-- Performing Test HAVE_CXX_FLAG_PEDANTIC - Success
-- Performing Test HAVE_CXX_FLAG_PEDANTIC_ERRORS
-- Performing Test HAVE_CXX_FLAG_PEDANTIC_ERRORS - Success
-- Performing Test HAVE_CXX_FLAG_WSHORTEN_64_TO_32
-- Performing Test HAVE_CXX_FLAG_WSHORTEN_64_TO_32 - Success
-- Performing Test HAVE_CXX_FLAG_FSTRICT_ALIASING
-- Performing Test HAVE_CXX_FLAG_FSTRICT_ALIASING - Success
-- Performing Test HAVE_CXX_FLAG_WNO_DEPRECATED_DECLARATIONS
-- Performing Test HAVE_CXX_FLAG_WNO_DEPRECATED_DECLARATIONS - Success
-- Performing Test HAVE_CXX_FLAG_WNO_DEPRECATED
-- Performing Test HAVE_CXX_FLAG_WNO_DEPRECATED - Success
-- Performing Test HAVE_CXX_FLAG_WSTRICT_ALIASING
-- Performing Test HAVE_CXX_FLAG_WSTRICT_ALIASING - Success
-- Performing Test HAVE_CXX_FLAG_WD654
-- Performing Test HAVE_CXX_FLAG_WD654 - Failed
-- Performing Test HAVE_CXX_FLAG_WTHREAD_SAFETY
-- Performing Test HAVE_CXX_FLAG_WTHREAD_SAFETY - Success
-- Performing Test HAVE_THREAD_SAFETY_ATTRIBUTES
-- Performing Test HAVE_THREAD_SAFETY_ATTRIBUTES -- failed to compile
-- Performing Test HAVE_CXX_FLAG_COVERAGE
-- Performing Test HAVE_CXX_FLAG_COVERAGE - Failed
-- Performing Test HAVE_STD_REGEX
CMake Warning at build/wasm/_deps/googlebenchmark-src/cmake/CXXFeatureCheck.cmake:43 (message):
If you see build failures due to cross compilation, try setting
HAVE_STD_REGEX to 0
Call Stack (most recent call first):
build/wasm/_deps/googlebenchmark-src/CMakeLists.txt:279 (cxx_feature_check)
-- Performing Test HAVE_STD_REGEX -- success
-- Performing Test HAVE_GNU_POSIX_REGEX
-- Performing Test HAVE_GNU_POSIX_REGEX -- failed to compile
-- Performing Test HAVE_POSIX_REGEX
CMake Warning at build/wasm/_deps/googlebenchmark-src/cmake/CXXFeatureCheck.cmake:43 (message):
If you see build failures due to cross compilation, try setting
HAVE_POSIX_REGEX to 0
Call Stack (most recent call first):
build/wasm/_deps/googlebenchmark-src/CMakeLists.txt:281 (cxx_feature_check)
-- Performing Test HAVE_POSIX_REGEX -- success
-- Performing Test HAVE_STEADY_CLOCK
CMake Warning at build/wasm/_deps/googlebenchmark-src/cmake/CXXFeatureCheck.cmake:43 (message):
If you see build failures due to cross compilation, try setting
HAVE_STEADY_CLOCK to 0
Call Stack (most recent call first):
build/wasm/_deps/googlebenchmark-src/CMakeLists.txt:290 (cxx_feature_check)
-- Performing Test HAVE_STEADY_CLOCK -- success
-- Configuring done
-- Generating done
-- Build files have been written to: /workspaces/docker-wasm-cmake-gtest/build/wasm
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master ✗) $ cmake --build . --config Release --parallel $(nproc)
Scanning dependencies of target wasm
Scanning dependencies of target gtest
Scanning dependencies of target hello
Scanning dependencies of target benchmark
[ 2%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[ 5%] Building CXX object CMakeFiles/wasm.dir/wasm.cpp.o
[ 7%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
[ 10%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/benchmark.cc.o
[ 12%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/benchmark_api_internal.cc.o
[ 17%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/benchmark_runner.cc.o
[ 17%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/colorprint.cc.o
[ 20%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/benchmark_name.cc.o
[ 22%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/commandlineflags.cc.o
[ 25%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/complexity.cc.o
[ 27%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/benchmark_register.cc.o
[ 30%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/console_reporter.cc.o
[ 32%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/counter.cc.o
[ 35%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/csv_reporter.cc.o
[ 42%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/reporter.cc.o
[ 42%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/perf_counters.cc.o
[ 42%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/sleep.cc.o
[ 45%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/json_reporter.cc.o
[ 47%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/statistics.cc.o
[ 50%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/string_util.cc.o
[ 55%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/timers.cc.o
[ 55%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark.dir/sysinfo.cc.o
[ 57%] Linking CXX executable hello.html
[ 60%] Linking CXX executable wasm.html
[ 60%] Built target wasm
[ 60%] Built target hello
[ 62%] Linking CXX static library libbenchmark.a
[ 62%] Built target benchmark
Scanning dependencies of target benchmark_main
[ 65%] Building CXX object _deps/googlebenchmark-build/src/CMakeFiles/benchmark_main.dir/benchmark_main.cc.o
[ 67%] Linking CXX static library ../../../lib/libgtest.a
[ 70%] Linking CXX static library libbenchmark_main.a
[ 70%] Built target gtest
Scanning dependencies of target gtest_main
Scanning dependencies of target gmock
[ 72%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
[ 75%] Building CXX object _deps/googletest-build/googlemock/CMakeFiles/gmock.dir/src/gmock-all.cc.o
[ 75%] Built target benchmark_main
Scanning dependencies of target wasm_perf
[ 80%] Building CXX object CMakeFiles/wasm_perf.dir/wasm_perf.cpp.o
[ 80%] Building CXX object CMakeFiles/wasm_perf.dir/wasm.cpp.o
[ 82%] Linking CXX executable wasm_perf.html
[ 85%] Linking CXX static library ../../../lib/libgtest_main.a
[ 85%] Built target wasm_perf
[ 85%] Built target gtest_main
Scanning dependencies of target wasm_test
[ 90%] Building CXX object CMakeFiles/wasm_test.dir/wasm.cpp.o
[ 90%] Building CXX object CMakeFiles/wasm_test.dir/wasm_test.cpp.o
[ 92%] Linking CXX static library ../../../lib/libgmock.a
[ 92%] Built target gmock
Scanning dependencies of target gmock_main
[ 95%] Building CXX object _deps/googletest-build/googlemock/CMakeFiles/gmock_main.dir/src/gmock_main.cc.o
[ 97%] Linking CXX executable wasm_test.html
[100%] Linking CXX static library ../../../lib/libgmock_main.a
[100%] Built target wasm_test
[100%] Built target gmock_main
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master ✗) $ ls
bin CTestTestfile.cmake hello.wasm wasm.html wasm_perf.wasm wasm_test.js
CMakeCache.txt _deps lib wasm.js 'wasm_test[1]_include.cmake' wasm_test.wasm
CMakeFiles hello.html Makefile wasm_perf.html 'wasm_test[1]_tests.cmake' wasm.wasm
cmake_install.cmake hello.js run_test.sh wasm_perf.js wasm_test.html
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master ✗) $
色々生成されました。意味ある生成物をピックアップすると…
- wasm.*: 本来欲しい WebAssembly のバイナリ関連
- wasm.wasm : ビルドされた WebAssembly のバイナリ
- wasm.js : 上の wasm をロードする JavaScript
- wasm.html : 上の .js を呼び出すサンプルコード
- hello.*: Hello World のバイナリ関連
- wasm_test.*: テストのバイナリ関連
- wasm_perf.*: パフォーマンスベンチマークのバイナリ関連
emrun
ここで改めて emrun について見てみましょう。
何が出来るかというのが Features の項に書いてあります。
- コマンドラインからブラウザで Emscripten で生成された html を開きます
- アプリケーション実行中の標準出力・標準エラー出力をターミナルに表示したりファイルに保存したりします
- コマンドライン引数を
main()
のargc, argv
として受け渡します -
exit(returncode)
でアプリケーションが終了したのを検知して実行を終了、リターンコードとして与えられたものを返します - 実行するブラウザは選択することが出来ます。
adb
経由で Android 上で実行することもできます
ここに書いてなくて注意が必要なのは、あくまでも WebAssembly をブラウザで実行して、上のことだけを行うということです。具体的に出来ないことを挙げると例えばファイルの読み書きは出来ません(一応、読む方だけならビルド時にちょっと工夫すれば wasm の中に組み込める)。また、環境変数も受け渡しされません。
続けて使い方が Quick how-to の項に書いてありますが、デスクトップ環境の話なので Docker だと少々事情が違います。--emrun
をリンカフラグとして指定するというのは共通です。
では、hello.cpp
をビルドして出来た hello.html
を実行してみましょう。
ビルド
まずはビルドの説明です。CMakeLists.txt
で関係あるのは以下の部分です。
add_executable(hello hello.cpp)
if(CMAKE_CROSSCOMPILING)
set_property(TARGET hello PROPERTY SUFFIX ".html")
target_link_options(hello PRIVATE "--emrun")
endif()
特別なことをやってるのは2か所。まずは set_property(TARGET hello PROPERTY SUFFIX ".html")
で生成物に拡張子 .html を付与しています。次に target_link_options(hello PRIVATE "--emrun")
で上にあったようにリンカフラグとして --emrun
を追加しています。
実行
まずは emrun が使えるブラウザ一覧を見てみます。firefox と chrome がありますね。
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master ✗) $ emrun --list_browsers
emrun has automatically found the following browsers in the default install locations on the system:
- firefox: Mozilla Firefox 94.0
- chrome: Google Chrome
You can pass the --browser <id> option to launch with the given browser above.
Even if your browser was not detected, you can use --browser /path/to/browser/executable to launch with that browser.
続いて普通に実行してみます。引数に a b c
を与えてみましょう。
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master) $ emrun --browser chrome --browser_args="--headless --remote-debugging-port=9222" --kill_exit hello.html a b c
[1202/202259.463804:ERROR:bus.cc(393)] Failed to connect to the bus: Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory
DevTools listening on ws://127.0.0.1:9222/devtools/browser/95379138-785f-4101-8053-c3f3540db74c
[1202/202259.489407:ERROR:sandbox_linux.cc(376)] InitializeSandbox() called with multiple threads in process gpu-process.
[1202/202259.493476:ERROR:command_buffer_proxy_impl.cc(125)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer.
Hello, world! argc:4
argv[0]:[./this.program]
argv[1]:[a]
argv[2]:[b]
argv[3]:[c]
なんか余計な出力も混ざってますがこれらは標準エラー出力に出ているのでリダイレクトで消すこともできます。argv[0]
が hello
とか hello.html
でないのは仕方ないんですかね…
一点、注意が必要で引数に --foo -b
など、ハイフンで始まる引数を与えたい場合は、その前に --
を渡す必要があります。
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master) $ emrun --browser chrome --browser_args="--headless --remote-debugging-port=9222" --kill_exit hello.html -- -a -b -c --foo --bar --baz
[1202/202605.181010:WARNING:discardable_shared_memory_manager.cc(198)] Less than 64MB of free space in temporary directory for shared memory files: 57
[1202/202605.182535:ERROR:bus.cc(393)] Failed to connect to the bus: Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory
[1202/202605.183494:ERROR:socket_posix.cc(150)] bind() failed: Address already in use (98)
[1202/202605.183540:ERROR:socket_posix.cc(150)] bind() failed: Cannot assign requested address (99)
[1202/202605.183565:ERROR:devtools_http_handler.cc(298)] Cannot start http server for devtools.
[1202/202605.207628:ERROR:sandbox_linux.cc(376)] InitializeSandbox() called with multiple threads in process gpu-process.
[1202/202605.210585:ERROR:command_buffer_proxy_impl.cc(125)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer.
Hello, world! argc:7
argv[0]:[./this.program]
argv[1]:[-a]
argv[2]:[-b]
argv[3]:[-c]
argv[4]:[--foo]
argv[5]:[--bar]
argv[6]:[--baz]
このように、WebAssembly にビルドした実行バイナリがまるでネイティブコードかのように実行されています。
emrun に与えているオプションを見ていきましょう。まず --browser chrome
は使うブラウザに Chrome を指定しています。--browser_args
でブラウザに与える引数を指定しています。ここでは --headless
と --remote-debugging-port=9222
を与えています。ちょっとくわしく追えていないのですが Chrome の場合は後者もつけないとヘッドレスモードで正常に動作しません。--kill_exit
をつけることで終了時にブラウザを kill します。つけてないとブラウザのプロセスが残ります。
run_test.sh
ここでちょっとしたシェルスクリプトを書いておきます。./run_test.sh <.html ファイル> <引数...>
の形式で実行できるようにするためのもので、CMake の中で CROSSCOMPILING_EMULATOR
に使います。
#!/bin/bash
HTML=$1
shift
# Chrome
# なぜかremote-debugging-portをつけないと動かない。rootのときは--no-sandboxも付ける
# exec emrun --browser chrome --browser_args="--headless --no-sandbox --disable-gpu --remote-debugging-port=9222" --kill_exit ${HTML} -- "$@" 2>/dev/null
exec emrun --browser chrome --browser_args="--headless --disable-gpu --remote-debugging-port=9222" --kill_exit ${HTML} -- "$@" 2>/dev/null
# Firefox
# Firefoxは多重起動できないので念のためstartでもkillしておく
# exec emrun --browser firefox --browser_args="--headless" --kill_start --kill_exit ${HTML} -- "$@"
テスト環境
テストコード
これでテストコードが書ける環境が整いました。早速テストを書いてみましょう(説明の都合上最低限のテストですが)。
#include "wasm.hpp"
#include "gtest/gtest.h"
TEST(WasmAdd, Test1) {
// 1 + 2
EXPECT_EQ(3, add(1, 2));
}
TEST(WasmSub, Test1) {
// 2 - 1
EXPECT_EQ(1, sub(2, 1));
}
実行
実行してみましょう。emrun の引数を全部書くのも面倒なので run_test.sh スクリプトを使います。
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master ✗) $ ./run_test.sh wasm_test.html --gtest_color=yes
Running main() from /workspaces/docker-wasm-cmake-gtest/build/wasm/_deps/googletest-src/googletest/src/gtest_main.cc
[==========] Running 2 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 1 test from WasmAdd
[ RUN ] WasmAdd.Test1
[ OK ] WasmAdd.Test1 (0 ms)
[----------] 1 test from WasmAdd (2 ms total)
[----------] 1 test from WasmSub
[ RUN ] WasmSub.Test1
/workspaces/docker-wasm-cmake-gtest/wasm_test.cpp:11: Failure
Expected equality of these values:
1
sub(2, 1)
Which is: 3
[ FAILED ] WasmSub.Test1 (8 ms)
[----------] 1 test from WasmSub (11 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 2 test suites ran. (70 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] WasmSub.Test1
1 FAILED TEST
上のコードフェンスでの張り付けでは色分けされてませんが、実行時に --gtest_color=yes
を付けているので、OK/FAILEDが色分けされています。
ctest
でも動作します。
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build/wasm (master ✗) $ ctest
Test project /workspaces/docker-wasm-cmake-gtest/build/wasm
Start 1: WasmAdd.Test1
1/2 Test #1: WasmAdd.Test1 .................... Passed 0.21 sec
Start 2: WasmSub.Test1
2/2 Test #2: WasmSub.Test1 ....................***Failed 0.22 sec
50% tests passed, 1 tests failed out of 2
Total Test time (real) = 0.42 sec
The following tests FAILED:
2 - WasmSub.Test1 (Failed)
Errors while running CTest
注意事項
ctest の -j 引数で並列実行すると、Chrome との通信が上手くいかなくなり正常に動作しません。
テストの中でファイルの読み書きをすることが出来ません。ただし、ファイルの読み込みはちょっと工夫すればできます。--embed-file を使います。
そもそもテストランナー自体もファイルの読み書きが出来ません。--gtest_output
やカバレッジの出力などが出来ないということになります。
テストのデバッグを行うことが出来ません。
パフォーマンスベンチマーク環境
Google Benchmark を使ってベンチマーク結果を取得してみましょう。サイトの説明を参考にちょこちょこっと書くとこんな感じになります。BENCHMARK_MAIN()
マクロを呼ぶ代わりに CMakeLists.txt
で benchmark_main
をリンクしてあります。
#include "wasm.hpp"
#include <benchmark/benchmark.h>
static void BM_Add(benchmark::State &state) {
for (auto _ : state) {
int ret = add(1, 2);
}
}
// Register the function as a benchmark
BENCHMARK(BM_Add);
// Define another benchmark
static void BM_Sub(benchmark::State &state) {
for (auto _ : state) {
int ret = sub(3, 2);
}
}
BENCHMARK(BM_Sub);
これも run_test.sh
経由で実行してみましょう。
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build (master ✗) $ ./run_test.sh wasm_perf.html --benchmark_color=true
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
BM_Add 5.67 ns 5.67 ns 121760306
BM_Sub 6.02 ns 6.02 ns 116472546
これもちゃんと色付けされて出力されます。
注意事項
次のように、--benchmark_out
という引数がありますが、テストと同じくベンチマークでもファイル出力は出来ません。標準出力のフォーマットを --benchmark_format
で変更しましょう。
vscode ➜ /workspaces/docker-wasm-cmake-gtest/build (master ✗) $ ./run_test.sh wasm_perf.html --help
benchmark [--benchmark_list_tests={true|false}]
[--benchmark_filter=<regex>]
[--benchmark_min_time=<min_time>]
[--benchmark_repetitions=<num_repetitions>]
[--benchmark_enable_random_interleaving={true|false}]
[--benchmark_report_aggregates_only={true|false}]
[--benchmark_display_aggregates_only={true|false}]
[--benchmark_format=<console|json|csv>]
[--benchmark_out=<filename>]
[--benchmark_out_format=<json|console|csv>]
[--benchmark_color={auto|true|false}]
[--benchmark_counters_tabular={true|false}]
[--benchmark_perf_counters=<counter>,...]
[--benchmark_context=<key>=<value>,...]
[--v=<verbosity>]
GitHub Actions
ここまでは VS Code Remote Containers で実行することを想定していましたが、せっかくなので GitHub Actions で継続的に実行されるようにしましょう。
ベンチマークについても benchmark-action/github-action-benchmark を使うことで、継続的なベンチマーク実行&結果のグラフ化が出来ます。
ビルド&テスト
ビルドとテストを行います。本当はフォーマットチェックとかを先にやった方がいいんでしょうが、今回は説明を割愛ということで…
name: CMake
on:
push:
branches: [master]
pull_request:
branches: [master]
env:
BUILD_TYPE: Release
EM_VERSION: 3.0.0
EM_CACHE_FOLDER: "emsdk-cache"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup cache
id: cache-system-libraries
uses: actions/cache@v2
with:
path: ${{env.EM_CACHE_FOLDER}}
key: ${{env.EM_VERSION}}-${{ runner.os }}
- name: Setup emsdk
uses: mymindstorm/setup-emsdk@v11
with:
# Make sure to set a version number!
version: ${{env.EM_VERSION}}
# This is the name of the cache folder.
# The cache folder will be placed in the build directory,
# so make sure it doesn't conflict with anything!
actions-cache-folder: ${{env.EM_CACHE_FOLDER}}
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
run: emcmake cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
- name: Build
# Build your program with the given configuration
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
- name: Test
working-directory: ${{github.workspace}}/build
# Execute tests defined by the CMake configuration.
# See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
run: ctest -C ${{env.BUILD_TYPE}}
パフォーマンスベンチマーク
事前に gh-pages ブランチを作って GitHub Pages がそこを参照するようにしておきます。Actions が勝手にグラフを作って dev/bench/
以下に置いてくれます。
あんまり細かい設定は詰めれていないです。また、サイトにも書いてありますが共用のランナーでパフォーマンスベンチマークすることに意味があるのか微妙なところもありますが、まあ記録を残すぞ、という程度の意味で…
name: Wasm Benchmark
on:
push:
branches:
- master
env:
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
BUILD_TYPE: Release
EM_VERSION: 3.0.0
EM_CACHE_FOLDER: "emsdk-cache"
permissions:
contents: write
deployments: write
jobs:
benchmark:
name: Run C++ benchmark example
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache Benchmark library
uses: actions/cache@v1
with:
path: ${{github.workspace}}/build/benchmark
key: ${{ runner.os }}-googlebenchmark-v1.5.0
- name: Setup cache
id: cache-system-libraries
uses: actions/cache@v2
with:
path: ${{env.EM_CACHE_FOLDER}}
key: ${{env.EM_VERSION}}-${{ runner.os }}
- name: Setup emsdk
uses: mymindstorm/setup-emsdk@v11
with:
# Make sure to set a version number!
version: ${{env.EM_VERSION}}
# This is the name of the cache folder.
# The cache folder will be placed in the build directory,
# so make sure it doesn't conflict with anything!
actions-cache-folder: "emsdk-cache"
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
run: emcmake cmake -B ${{github.workspace}}/build -DBENCHMARK_ENABLE_TESTING=OFF -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
- name: Build
# Build your program with the given configuration
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
- name: Run Benchmark
run: ./run_test.sh wasm_perf.html --benchmark_format=json | tee benchmark_result.json
working-directory: ${{github.workspace}}/build
- name: Store benchmark result
uses: benchmark-action/github-action-benchmark@v1
with:
name: C++ Benchmark
tool: "googlecpp"
output-file-path: ${{github.workspace}}/build/benchmark_result.json
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
# Show alert with commit comment on detecting possible performance regression
alert-threshold: "200%"
comment-on-alert: true
fail-on-alert: true
GitHub Pages の dev/bench/
にアクセスすると、こんな感じにベンチマーク結果がグラフとして表示されます。
ネイティブでのビルド
ところで、今回説明した CMakeLists.txt
では Emscripten 関係の設定をちょっと丁寧に if(CROSSCOMPILING)
でくくってあります。そのため、単純に cmake でビルドすればネイティブコードとしてテストとパフォーマンスベンチマークがビルドされます。実行例でわざわざ mkdir -p build/wasm
としておいたのはここで build/native
ディレクトリを作ってそちらにネイティブのバイナリを生成させるためです。
こんな感じにすればネイティブでデバッグ版バイナリが出来ます。
$ mkdir -p build/native
$ cd build/native
$ cmake ../..
$ cmake --build . --config Debug
また、build
ディレクトリに普通に cmake
することで、VS Code の CMake 関係の拡張機能を入れて簡単にデバッグが出来たりします。拡張機能を追加するときは単純に VS Code の UI でインストールすると、コンテナの再ビルドで消えてしまいますので、devcontainer.json
に書きましょう。
おわりに
いかがでしたでしょうか。WebAssembly としてビルドされたバイナリそのものに対してテストコードを実行出来て、パフォーマンスベンチマークを取得することも出来ました。きちんと WebAssembly としてビルドされたバイナリを、ブラウザそのもので実行しているので、コンパイラのバグやブラウザの Wasm ランタイムのバグがあってもテストがちゃんとコケます(まあこういうの踏んじゃうと解決は困難でしょうが)。wasm でデバッグすることこそ出来ないものの、同等のコードをネイティブビルドすることが出来ます。失敗するテストに対してデバッガを使ってデバッグしていく、ということもネイティブコードに対してなら出来ます。万全とは言えませんがかなり快適な開発環境が整ったのではないでしょうか。
この記事で説明したソースコードは kounoike/docker-wasm-cmake-gtest リポジトリにあります。
ベンチマークの結果は https://kounoike.github.io/docker-wasm-cmake-gtest/dev/bench/ にあります。
Discussion