diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 000000000..804d6ff91 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,61 @@ +name: CodSpeed + +on: + push: + branches: [master, opencc-wasm-develop] + pull_request: + branches: [master, opencc-wasm-develop] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + benchmarks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-cache: true + repository-cache: true + + - name: Configure BuildBuddy + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + shell: bash + env: + BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} + run: | + if [ -n "${BUILDBUDDY_API_KEY}" ]; then + { + echo "build --config=buildbuddy" + echo "build --remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}" + echo "build --bes_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}" + } >> .bazelrc.user + echo "BuildBuddy enabled." + else + echo "BUILDBUDDY_API_KEY not set; skipping BuildBuddy config." + fi + + - name: Build benchmark + run: >- + bazel build + --@codspeed_google_benchmark_compat//:codspeed_mode=simulation + --compilation_mode=dbg + --copt=-O2 + --copt=-DNDEBUG + //src/benchmark:performance + + - name: Prepare temp directory + run: mkdir -p /tmp/opencc_bench_temp + + - uses: CodSpeedHQ/action@v3 + with: + token: ${{ secrets.CODSPEED_TOKEN_FRANKSLIN }} + run: >- + OPENCC_BENCHMARK_TEMP_DIR=/tmp/opencc_bench_temp + bazel-bin/src/benchmark/performance + '--benchmark_filter=BM_ConvertLongText.*ocd2' diff --git a/MODULE.bazel b/MODULE.bazel index 5c0d637d5..6c880eb16 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,6 +8,7 @@ module( bazel_dep(name = "aspect_bazel_lib", version = "2.22.5") bazel_dep(name = "aspect_rules_js", version = "3.2.2") +bazel_dep(name = "codspeed_google_benchmark_compat", version = "2.1.0", dev_dependency = True) bazel_dep(name = "darts-clone", version = "0.32") bazel_dep(name = "googletest", version = "1.17.0.bcr.2", dev_dependency = True) bazel_dep(name = "marisa-trie", version = "0.3.1.bcr.2") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 4a8708542..9ccec429b 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -69,6 +69,10 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/codspeed_core/2.1.0/MODULE.bazel": "78df8ed75ea5a223f92f2286666d09eb335c9cc2b9532ad6f4b633db17ca009e", + "https://bcr.bazel.build/modules/codspeed_core/2.1.0/source.json": "a0398f69220f76845e55f05a41b74f9c276cdb5cd289cf6084922d6284302c5c", + "https://bcr.bazel.build/modules/codspeed_google_benchmark_compat/2.1.0/MODULE.bazel": "53f7154a75180ae4a1ce1f3cb36d546a6e9f3eb0010a7dd4ed484068cb76df43", + "https://bcr.bazel.build/modules/codspeed_google_benchmark_compat/2.1.0/source.json": "2ca9d1aefbc226daa589e543128d5803d44f4eb73e0ecbbcf3fd89a6c75720a6", "https://bcr.bazel.build/modules/darts-clone/0.32/MODULE.bazel": "bdd235e31dd7f2538ff8b3ab3ef09c831349b141afca587d32b487d75c502361", "https://bcr.bazel.build/modules/darts-clone/0.32/source.json": "c65158c152e276f3c59dc0fc0fa746f1ff601e23b0a09812e024fe563e4dc99c", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", @@ -88,6 +92,8 @@ "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", + "https://bcr.bazel.build/modules/libpfm/4.11.0.bcr.1/MODULE.bazel": "e5362dadc90aab6724c83a2cc1e67cbed9c89a05d97fb1f90053c8deb1e445c8", + "https://bcr.bazel.build/modules/libpfm/4.11.0.bcr.1/source.json": "0646414d9037f8aad148781dd760bec90b0b25ac12fda5e03f8aadbd6b9c61e6", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/marisa-trie/0.3.1.bcr.2/MODULE.bazel": "8d987298c829b651f00541c89c235185516629c0d17c3d67607ef37e7ad9f322", "https://bcr.bazel.build/modules/marisa-trie/0.3.1.bcr.2/source.json": "72d4c66d20ab99c7ec3659683b57011fa2caa57230d3ba3c9a918d82cdab6109", diff --git a/src/benchmark/BUILD.bazel b/src/benchmark/BUILD.bazel new file mode 100644 index 000000000..518998d1f --- /dev/null +++ b/src/benchmark/BUILD.bazel @@ -0,0 +1,26 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary") + +package(default_visibility = ["//visibility:public"]) + +cc_binary( + name = "performance", + testonly = True, + srcs = ["Performance.cpp"], + includes = [".."], + deps = [ + "@codspeed_google_benchmark_compat//:benchmark", + "//deps/rapidjson-1.1.0:rapidjson", + "//src:exception_lib", + "//src:simple_converter_lib", + "//src:test_utils_utf8_lib", + "@bazel_tools//tools/cpp/runfiles", + ], + data = [ + "//data/config:config", + "//data/dictionary:CJK_Compatibility_Ideographs.txt", + "//data/dictionary:binary_dictionaries", + "//data/dictionary:text_dictionaries", + "//src/tools:command_line", + "//test/benchmark:benchmark_data", + ], +) diff --git a/src/benchmark/Performance.cpp b/src/benchmark/Performance.cpp index 63078cc63..f0df2a40f 100644 --- a/src/benchmark/Performance.cpp +++ b/src/benchmark/Performance.cpp @@ -44,6 +44,13 @@ #include "SimpleConverter.hpp" #include "TestUtilsUTF8.hpp" +#ifdef BAZEL +#include "tools/cpp/runfiles/runfiles.h" +using BazelRunfiles = bazel::tools::cpp::runfiles::Runfiles; +static const char* g_argv0 = nullptr; +static std::unique_ptr g_runfiles; +#endif + namespace opencc { namespace { @@ -51,6 +58,12 @@ namespace { using JSONDocument = rapidjson::Document; using JSONValue = rapidjson::Value; +// Returns the value of environment variable `name`, or nullptr if unset/empty. +const char* GetEnv(const char* name) noexcept { + const char* val = std::getenv(name); + return (val != nullptr && *val != '\0') ? val : nullptr; +} + struct BenchmarkConfig { std::string name; std::string path; @@ -107,7 +120,14 @@ std::string GetBaseNameWithoutExtension(const std::string& path) { } std::string BenchmarkTempDirectory() { + if (const char* e = GetEnv("OPENCC_BENCHMARK_TEMP_DIR")) { + return std::string(e) + "/"; + } +#ifdef PROJECT_BINARY_DIR return std::string(PROJECT_BINARY_DIR) + "/src/benchmark/"; +#else + return std::string("/tmp/opencc_benchmark/"); +#endif } bool IsAbsolutePath(const std::string& path) { @@ -151,23 +171,59 @@ std::string ResolveConfigPath(const std::string& config_name) { ".json"; } #endif + if (const char* e = GetEnv("OPENCC_BENCHMARK_DATA_DIR")) { + return std::string(e) + "/" + config_name + ".json"; + } +#ifdef BAZEL + if (g_runfiles) { + for (const char* prefix : {"_main", "opencc~"}) { + const std::string path = g_runfiles->Rlocation( + std::string(prefix) + "/data/config/" + config_name + ".json"); + if (IsRegularFile(path)) return path; + } + } + return config_name + ".json"; +#elif defined(PROJECT_SOURCE_DIR) return std::string(PROJECT_SOURCE_DIR) + "/data/config/" + config_name + ".json"; +#else + return config_name + ".json"; +#endif } std::string ResolveTextDictionaryPath(const std::string& dict_file_name) { const std::string text_file_name = ReplaceSuffix(dict_file_name, ".ocd2", ".txt"); - const std::string source_path = - std::string(PROJECT_SOURCE_DIR) + "/data/dictionary/" + text_file_name; - if (IsRegularFile(source_path)) { - return source_path; + + if (const char* e = GetEnv("OPENCC_BENCHMARK_TEXT_DIR")) { + const std::string path = std::string(e) + "/" + text_file_name; + if (IsRegularFile(path)) return path; + throw FileNotFound(text_file_name); } - const std::string generated_path = - std::string(PROJECT_BINARY_DIR) + "/data/" + text_file_name; - if (IsRegularFile(generated_path)) { - return generated_path; +#ifdef BAZEL + if (g_runfiles) { + for (const char* prefix : {"_main", "opencc~"}) { + const std::string path = g_runfiles->Rlocation( + std::string(prefix) + "/data/dictionary/" + text_file_name); + if (IsRegularFile(path)) return path; + } } +#else +#ifdef PROJECT_SOURCE_DIR + { + const std::string source_path = + std::string(PROJECT_SOURCE_DIR) + "/data/dictionary/" + text_file_name; + if (IsRegularFile(source_path)) return source_path; + } +#endif +#ifdef PROJECT_BINARY_DIR + { + const std::string generated_path = + std::string(PROJECT_BINARY_DIR) + "/data/" + text_file_name; + if (IsRegularFile(generated_path)) return generated_path; + } +#endif +#endif throw FileNotFound(text_file_name); } @@ -410,6 +466,17 @@ std::vector InitializationConfigs() { return configs; } +std::vector LongTextConversionConfigs() { + std::vector configs = { + "hk2s", "hk2t", "jp2t", "s2hk", "s2t", "s2tw", "s2twp", + "t2hk", "t2jp", "t2s", "t2tw", "tw2s", "tw2sp", "tw2t", + }; +#ifdef OPENCC_BENCHMARK_JIEBA_CONFIG_DIR + configs.push_back("s2twp_jieba"); +#endif + return configs; +} + std::vector ConversionConfigs() { std::vector configs = {"s2t", "s2twp"}; #ifdef OPENCC_BENCHMARK_JIEBA_CONFIG_DIR @@ -431,9 +498,9 @@ std::vector BuildBenchmarkConfigs( GetTemporaryTextConfigRegistry().Create(source_path, "text", "benchmark-text"), }); - } catch (const FileNotFound&) { - // Some legacy benchmark configs only ship precompiled ocd2 artifacts. - // Skip the text-json variant when there is no source text dictionary. + } catch (const Exception&) { + // Skip the text-json variant when the config can't be read or text + // dictionaries are unavailable (e.g., Bazel build before runfiles init). } } return configs; @@ -458,15 +525,51 @@ bool IsJiebaConfig(const BenchmarkConfig& config) { #endif std::string OpenccBinaryPath() { + if (const char* e = GetEnv("OPENCC_BENCHMARK_OPENCC_PATH")) return e; +#ifdef BAZEL + if (g_runfiles) { + for (const char* prefix : {"_main", "opencc~"}) { + const std::string path = + g_runfiles->Rlocation(std::string(prefix) + "/src/tools/command_line"); + if (IsRegularFile(path)) return path; + } + } +#endif +#ifdef OPENCC_BENCHMARK_OPENCC_PATH return OPENCC_BENCHMARK_OPENCC_PATH; +#else + return "opencc"; +#endif } std::string SourceConfigDirectory() { + if (const char* e = GetEnv("OPENCC_BENCHMARK_DATA_DIR")) return e; +#ifdef BAZEL + if (g_runfiles) { + const std::string path = g_runfiles->Rlocation("_main/data/config"); + if (!path.empty()) return path; + } +#endif +#ifdef PROJECT_SOURCE_DIR return std::string(PROJECT_SOURCE_DIR) + "/data/config"; +#else + return "."; +#endif } std::string BuildDataDirectory() { + if (const char* e = GetEnv("OPENCC_BENCHMARK_DATA_DIR")) return e; +#ifdef BAZEL + if (g_runfiles) { + const std::string path = g_runfiles->Rlocation("_main/data/dictionary"); + if (!path.empty()) return path; + } +#endif +#ifdef PROJECT_BINARY_DIR return std::string(PROJECT_BINARY_DIR) + "/data"; +#else + return "."; +#endif } struct MeasuredResult { @@ -534,8 +637,19 @@ void RunCommandLineConversion(const BenchmarkConfig& config, } SimpleConverter* Initialize(const BenchmarkConfig& config) { - chdir(PROJECT_BINARY_DIR "/data"); + if (const char* e = GetEnv("OPENCC_BENCHMARK_DATA_DIR")) { + chdir(e); + } +#ifdef PROJECT_BINARY_DIR + else { + chdir(PROJECT_BINARY_DIR "/data"); + } +#endif +#ifdef BAZEL + return new SimpleConverter(config.path, std::vector{}, g_argv0); +#else return new SimpleConverter(config.path); +#endif } void Convert(const SimpleConverter* converter, std::string_view text) { @@ -595,9 +709,12 @@ std::string FormatCounterValue(const benchmark::BenchmarkReporter::Run& run, std::string BenchmarkGroupName(const benchmark::BenchmarkReporter::Run& run) { const std::string name = run.benchmark_name(); - const std::string::size_type slash_pos = name.find('/'); + const std::string::size_type colons_pos = name.find("::"); + const std::string bare_name = + colons_pos != std::string::npos ? name.substr(colons_pos + 2) : name; + const std::string::size_type slash_pos = bare_name.find('/'); const std::string prefix = - slash_pos == std::string::npos ? name : name.substr(0, slash_pos); + slash_pos == std::string::npos ? bare_name : bare_name.substr(0, slash_pos); if (prefix == "BM_Initialization") { return "Initialization"; } @@ -755,8 +872,23 @@ void SetCommandLineCounters(benchmark::State& state, size_t total_input_bytes, } std::string ReadText(const std::string& filename) { - const std::string benchmark_data_dir = PROJECT_SOURCE_DIR "/test/benchmark/"; - const std::string data_path = benchmark_data_dir + filename; + std::string data_path; + if (const char* e = GetEnv("OPENCC_BENCHMARK_TEST_DATA_DIR")) { + data_path = std::string(e) + "/" + filename; + } else { +#ifdef BAZEL + if (g_runfiles) { + data_path = g_runfiles->Rlocation("_main/test/benchmark/" + filename); + } + if (data_path.empty() || !IsRegularFile(data_path)) { + data_path = filename; + } +#elif defined(PROJECT_SOURCE_DIR) + data_path = std::string(PROJECT_SOURCE_DIR) + "/test/benchmark/" + filename; +#else + data_path = filename; +#endif + } std::ifstream stream(data_path.c_str()); return std::string((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); @@ -873,24 +1005,26 @@ bool RegisterBenchmarks() { BuildBenchmarkConfigs(InitializationConfigs()); for (const BenchmarkConfig& config : initialization_configs) { benchmark::RegisterBenchmark( - ("BM_Initialization/" + config.name).c_str(), + ("Performance::BM_Initialization/" + config.name).c_str(), [config](benchmark::State& state) { BM_Initialization(state, config); }) ->Unit(benchmark::kMicrosecond); } - const std::vector conversion_configs = - BuildBenchmarkConfigs(ConversionConfigs()); - for (const BenchmarkConfig& config : conversion_configs) { + const std::vector long_text_configs = + BuildBenchmarkConfigs(LongTextConversionConfigs()); + for (const BenchmarkConfig& config : long_text_configs) { benchmark::RegisterBenchmark( - ("BM_ConvertLongText/" + config.name).c_str(), + ("Performance::BM_ConvertLongText/" + config.name).c_str(), [config](benchmark::State& state) { BM_ConvertLongText(state, config); }) ->Unit(benchmark::kMillisecond); } + const std::vector conversion_configs = + BuildBenchmarkConfigs(ConversionConfigs()); for (const BenchmarkConfig& config : conversion_configs) { for (const int iteration : {100, 1000, 10000, 100000}) { benchmark::RegisterBenchmark( - ("BM_Convert/" + config.name + "/" + std::to_string(iteration)) + ("Performance::BM_Convert/" + config.name + "/" + std::to_string(iteration)) .c_str(), [config, iteration](benchmark::State& state) { BM_Convert(state, config, iteration); @@ -901,7 +1035,7 @@ bool RegisterBenchmarks() { for (const BenchmarkConfig& config : conversion_configs) { benchmark::RegisterBenchmark( - ("BM_CommandLineLongText/" + config.name).c_str(), + ("Performance::BM_CommandLineLongText/" + config.name).c_str(), [config](benchmark::State& state) { BM_CommandLineLongText(state, config); }) @@ -912,7 +1046,7 @@ bool RegisterBenchmarks() { for (const BenchmarkConfig& config : conversion_configs) { for (const int iteration : {100, 1000, 10000, 100000}) { benchmark::RegisterBenchmark( - ("BM_CommandLine/" + config.name + "/" + std::to_string(iteration)) + ("Performance::BM_CommandLine/" + config.name + "/" + std::to_string(iteration)) .c_str(), [config, iteration](benchmark::State& state) { BM_CommandLine(state, config, iteration); @@ -924,13 +1058,23 @@ bool RegisterBenchmarks() { return true; } -const bool kBenchmarksRegistered = RegisterBenchmarks(); - } // namespace +void DoRegisterBenchmarks() { + RegisterBenchmarks(); +} + } // namespace opencc int main(int argc, char** argv) { +#ifdef BAZEL + g_argv0 = argc > 0 ? argv[0] : nullptr; + { + std::string err; + g_runfiles.reset(BazelRunfiles::Create(g_argv0 != nullptr ? g_argv0 : "", &err)); + } +#endif + opencc::DoRegisterBenchmarks(); ::benchmark::Initialize(&argc, argv); if (::benchmark::ReportUnrecognizedArguments(argc, argv)) { return 1; diff --git a/test/benchmark/BUILD.bazel b/test/benchmark/BUILD.bazel new file mode 100644 index 000000000..bfef96d64 --- /dev/null +++ b/test/benchmark/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "benchmark_data", + srcs = ["zuozhuan.txt"], +)