Skip to content

Commit 68fd6ea

Browse files
committed
Add engine profiler
1 parent 2480b52 commit 68fd6ea

4 files changed

Lines changed: 335 additions & 1 deletion

File tree

Source/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ cmake_minimum_required(VERSION 3.12)
44
project(McBopomofoCore VERSION 3.0)
55

66
option(ENABLE_TEST "Build Test" On)
7+
option(ENABLE_ENGINE_PROFILE "Build the engine profiling workload" Off)
78

89
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.23.0")
910
find_package(GTest)
@@ -34,4 +35,3 @@ if (ENABLE_TEST)
3435
endif ()
3536

3637
add_subdirectory(Engine)
37-

Source/Engine/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,12 @@ if (ENABLE_TEST)
138138
add_dependencies(runParselessPhraseDBBenchmark ParselessPhraseDBBenchmark)
139139
endif ()
140140
endif ()
141+
142+
if (ENABLE_ENGINE_PROFILE)
143+
add_executable(EngineProfile
144+
EngineProfile.cpp)
145+
target_link_libraries(EngineProfile
146+
McBopomofoLMLib
147+
MandarinLib
148+
gramambular2_lib)
149+
endif ()

Source/Engine/EngineProfile.cpp

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright (c) 2026 and onwards The McBopomofo Authors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person
4+
// obtaining a copy of this software and associated documentation
5+
// files (the "Software"), to deal in the Software without
6+
// restriction, including without limitation the rights to use,
7+
// copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the
9+
// Software is furnished to do so, subject to the following
10+
// conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be
13+
// included in all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
// OTHER DEALINGS IN THE SOFTWARE.
23+
24+
#include <charconv>
25+
#include <chrono>
26+
#include <filesystem>
27+
#include <iostream>
28+
#include <memory>
29+
#include <optional>
30+
#include <string>
31+
#include <string_view>
32+
#include <vector>
33+
34+
#if defined(__APPLE__)
35+
#include <os/log.h>
36+
#include <os/signpost.h>
37+
#endif
38+
39+
#include "Mandarin/Mandarin.h"
40+
#include "McBopomofoLM.h"
41+
#include "gramambular2/reading_grid.h"
42+
43+
namespace {
44+
45+
using Formosa::Gramambular2::ReadingGrid;
46+
using Formosa::Mandarin::BopomofoKeyboardLayout;
47+
using Formosa::Mandarin::BopomofoReadingBuffer;
48+
using McBopomofo::McBopomofoLM;
49+
50+
// Prevent the compiler from optimizing away the workload result.
51+
template <typename T>
52+
void DoNotOptimize(const T& value) {
53+
asm volatile("" : : "r"(&value) : "memory");
54+
}
55+
56+
struct ProfilingScenario {
57+
const char* identifier;
58+
std::vector<std::string> keySequences;
59+
std::string expectedOutput;
60+
};
61+
62+
struct ProfilingScenarioResult {
63+
const char* identifier;
64+
size_t iterations;
65+
};
66+
67+
const std::vector<ProfilingScenario>& ProfilingScenarios() {
68+
static const std::vector<ProfilingScenario> scenarios = {
69+
{
70+
"short",
71+
{"su3", "cl3"},
72+
"你好",
73+
},
74+
{
75+
"medium",
76+
{"vul3", "a94", "5j4", "up", "gj", "bj4", "z83"},
77+
"小麥注音輸入法",
78+
},
79+
{
80+
"long",
81+
{"ji3", "yjo4", "1j4", "s/6", "j;4", "ru4", "2k7", "g4", "w8", "a93",
82+
"rm6", "y7", "g6", "2k7", "1o4", "u/3"},
83+
"我最不能忘記的是他買橘子時的背影",
84+
},
85+
};
86+
return scenarios;
87+
}
88+
89+
#if defined(__APPLE__)
90+
os_log_t ProfilingLog() {
91+
static os_log_t log =
92+
os_log_create("org.openvanilla.McBopomofo.EngineProfile",
93+
OS_LOG_CATEGORY_POINTS_OF_INTEREST);
94+
return log;
95+
}
96+
#endif
97+
98+
class ScenarioInterval {
99+
public:
100+
explicit ScenarioInterval(const char* identifier) {
101+
#if defined(__APPLE__)
102+
os_signpost_interval_begin(ProfilingLog(), OS_SIGNPOST_ID_EXCLUSIVE,
103+
"Profiling Scenario", "identifier=%{public}s",
104+
identifier);
105+
#else
106+
static_cast<void>(identifier);
107+
#endif
108+
}
109+
110+
~ScenarioInterval() {
111+
#if defined(__APPLE__)
112+
os_signpost_interval_end(ProfilingLog(), OS_SIGNPOST_ID_EXCLUSIVE,
113+
"Profiling Scenario");
114+
#endif
115+
}
116+
117+
ScenarioInterval(const ScenarioInterval&) = delete;
118+
ScenarioInterval& operator=(const ScenarioInterval&) = delete;
119+
};
120+
121+
class EngineProfilingWorkload {
122+
public:
123+
explicit EngineProfilingWorkload(
124+
const std::filesystem::path& languageModelPath)
125+
: languageModel_(std::make_shared<McBopomofoLM>()),
126+
grid_(languageModel_),
127+
readingBuffer_(BopomofoKeyboardLayout::StandardLayout()) {
128+
languageModel_->loadLanguageModel(languageModelPath.c_str());
129+
}
130+
131+
[[nodiscard]] bool isLoaded() const {
132+
return languageModel_->isDataModelLoaded();
133+
}
134+
135+
std::string runScenario(const ProfilingScenario& scenario) {
136+
grid_.clear();
137+
readingBuffer_.clear();
138+
139+
ReadingGrid::WalkResult walk;
140+
for (const std::string& keySequence : scenario.keySequences) {
141+
for (char key : keySequence) {
142+
readingBuffer_.combineKey(key);
143+
}
144+
grid_.insertReading(readingBuffer_.composedString());
145+
readingBuffer_.clear();
146+
walk = grid_.walk();
147+
}
148+
149+
std::string text;
150+
for (const std::string& value : walk.valuesAsStrings()) {
151+
text += value;
152+
}
153+
return text;
154+
}
155+
156+
private:
157+
std::shared_ptr<McBopomofoLM> languageModel_;
158+
ReadingGrid grid_;
159+
BopomofoReadingBuffer readingBuffer_;
160+
};
161+
162+
std::optional<std::chrono::seconds> ParseProfileDuration(
163+
std::string_view value) {
164+
int duration = 0;
165+
const char* begin = value.data();
166+
const char* end = begin + value.size();
167+
const auto [position, error] = std::from_chars(begin, end, duration);
168+
if (error != std::errc{} || position != end) {
169+
return std::nullopt;
170+
}
171+
if (duration <= 0 || duration > 3600) {
172+
return std::nullopt;
173+
}
174+
return std::chrono::seconds{duration};
175+
}
176+
177+
std::filesystem::path ResolveLanguageModelPath(
178+
const std::filesystem::path& executablePath) {
179+
const std::filesystem::path executableDirectory =
180+
std::filesystem::absolute(executablePath)
181+
.lexically_normal()
182+
.parent_path();
183+
return executableDirectory.parent_path() / "Data" / "data.txt";
184+
}
185+
186+
bool VerifyWorkload(EngineProfilingWorkload& workload) {
187+
for (const ProfilingScenario& scenario : ProfilingScenarios()) {
188+
if (workload.runScenario(scenario) != scenario.expectedOutput) {
189+
return false;
190+
}
191+
}
192+
return true;
193+
}
194+
195+
size_t RunScenarioForDuration(EngineProfilingWorkload& workload,
196+
const ProfilingScenario& scenario,
197+
std::chrono::steady_clock::duration duration) {
198+
const ScenarioInterval interval(scenario.identifier);
199+
const auto deadline = std::chrono::steady_clock::now() + duration;
200+
size_t iterations = 0;
201+
do {
202+
const std::string result = workload.runScenario(scenario);
203+
DoNotOptimize(result);
204+
++iterations;
205+
} while (std::chrono::steady_clock::now() < deadline);
206+
return iterations;
207+
}
208+
209+
std::vector<ProfilingScenarioResult> RunProfilingScenarios(
210+
EngineProfilingWorkload& workload, std::chrono::seconds duration) {
211+
const std::vector<ProfilingScenario>& scenarios = ProfilingScenarios();
212+
const std::chrono::steady_clock::duration totalDuration = duration;
213+
const auto scenarioDuration = totalDuration / scenarios.size();
214+
215+
std::vector<ProfilingScenarioResult> results;
216+
results.reserve(scenarios.size());
217+
for (const ProfilingScenario& scenario : scenarios) {
218+
results.push_back({
219+
scenario.identifier,
220+
RunScenarioForDuration(workload, scenario, scenarioDuration),
221+
});
222+
}
223+
return results;
224+
}
225+
226+
} // namespace
227+
228+
int main(int argc, char* argv[]) {
229+
if (argc != 2) {
230+
std::cerr << "Usage: " << argv[0] << " <PROFILE_DURATION>\n";
231+
return 1;
232+
}
233+
234+
const auto profileDuration = ParseProfileDuration(argv[1]);
235+
if (!profileDuration.has_value()) {
236+
std::cerr << "Profile duration must be an integer between 1 and 3600.\n";
237+
return 1;
238+
}
239+
240+
const std::filesystem::path languageModelPath =
241+
ResolveLanguageModelPath(argv[0]);
242+
EngineProfilingWorkload workload(languageModelPath);
243+
if (!workload.isLoaded()) {
244+
std::cerr << "Failed to load production language model data: "
245+
<< languageModelPath << '\n';
246+
return 1;
247+
}
248+
249+
if (!VerifyWorkload(workload)) {
250+
std::cerr << "Profiling workload verification failed.\n";
251+
return 1;
252+
}
253+
254+
const std::vector<ProfilingScenarioResult> results =
255+
RunProfilingScenarios(workload, *profileDuration);
256+
for (const ProfilingScenarioResult& result : results) {
257+
std::cout << result.identifier << "_iterations=" << result.iterations
258+
<< '\n';
259+
}
260+
return 0;
261+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6+
SOURCE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
7+
BUILD_DIR="${BUILD_DIR:-/tmp/McBopomofoEngineProfilerBuild}"
8+
NEON_BUILD_DIR="${NEON_BUILD_DIR:-$BUILD_DIR-NEON}"
9+
OUTPUT_DIR="${OUTPUT_DIR:-$SOURCE_DIR/Engine/Report}"
10+
PYTHON="${PYTHON:-python3}"
11+
PROFILE_DURATION="${PROFILE_DURATION:-20}"
12+
TRACE_TIME_LIMIT="${TRACE_TIME_LIMIT:-$((PROFILE_DURATION + 10))s}"
13+
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
14+
15+
if [[ "$(uname -s)" != "Darwin" ]]; then
16+
echo "Time Profiler requires macOS." >&2
17+
exit 1
18+
fi
19+
20+
mkdir -p "$OUTPUT_DIR"
21+
22+
make -C "$SOURCE_DIR/Data" "PYTHON=$PYTHON" all
23+
24+
profile_engine() {
25+
local result_name="$1"
26+
local build_dir="$2"
27+
local neon_enabled="$3"
28+
local trace_path="$OUTPUT_DIR/$result_name-$TIMESTAMP.trace"
29+
local target_log="$OUTPUT_DIR/$result_name-$TIMESTAMP.log"
30+
local profile_binary="$build_dir/Engine/EngineProfile"
31+
32+
mkdir -p "$build_dir/Data"
33+
cp "$SOURCE_DIR/Data/data.txt" "$build_dir/Data/data.txt"
34+
cmake \
35+
-S "$SOURCE_DIR" \
36+
-B "$build_dir" \
37+
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
38+
-DENABLE_TEST=OFF \
39+
-DENABLE_ENGINE_PROFILE=ON \
40+
-DENABLE_EXPERIMENTAL_SIMD_SUPPORT_NEON="$neon_enabled"
41+
cmake --build "$build_dir" --target EngineProfile -j
42+
43+
xcrun xctrace record \
44+
--template "Time Profiler" \
45+
--time-limit "$TRACE_TIME_LIMIT" \
46+
--output "$trace_path" \
47+
--target-stdout "$target_log" \
48+
--no-prompt \
49+
--launch \
50+
-- "$profile_binary" "$PROFILE_DURATION"
51+
52+
local scenario
53+
for scenario in short medium long; do
54+
if ! grep -q "^${scenario}_iterations=" "$target_log"; then
55+
echo "Profiling workload did not complete successfully. See: $target_log" >&2
56+
exit 1
57+
fi
58+
done
59+
60+
echo "Time Profiler trace: $trace_path"
61+
}
62+
63+
profile_engine "engine" "$BUILD_DIR" OFF
64+
profile_engine "engine-neon" "$NEON_BUILD_DIR" ON

0 commit comments

Comments
 (0)