diff --git a/src/inputwindow.cpp b/src/inputwindow.cpp index f0fec5a..c871956 100644 --- a/src/inputwindow.cpp +++ b/src/inputwindow.cpp @@ -1178,9 +1178,12 @@ void InputWindow::scheduleFrame() { }); frameTimer_->setOneShot(); } else { + // setOneShot() arms the source for one more shot. Do not call + // setEnabled(true) after it, that switches back to a repeating source + // whose time has already passed, which the event loop then fires on + // every iteration (100% CPU). See tests/timer_rearm.cpp. frameTimer_->setTime(nextFrame); frameTimer_->setOneShot(); - frameTimer_->setEnabled(true); } } diff --git a/tests/timer_rearm.cpp b/tests/timer_rearm.cpp new file mode 100644 index 0000000..8719658 --- /dev/null +++ b/tests/timer_rearm.cpp @@ -0,0 +1,77 @@ +// Regression test for the animation frame timer. +// +// Re-arming a Fcitx5 time event for one more shot has to be setTime() followed by +// setOneShot(). Calling setEnabled(true) afterwards switches the source back to +// SD_EVENT_ON, and with a time that has already passed the event loop fires it +// again on every iteration, which keeps the main thread at 100% CPU. +// +// g++ -std=c++20 tests/timer_rearm.cpp -o /tmp/timer_rearm \ +// $(pkg-config --cflags --libs Fcitx5Utils) && /tmp/timer_rearm +#include +#include + +#include +#include +#include + +using Clock = std::chrono::steady_clock; + +static uint64_t usecIn(std::chrono::milliseconds ms) { + return std::chrono::duration_cast( + (Clock::now() + ms).time_since_epoch()) + .count(); +} + +// Arms a timer twice, like two animations in a row, and counts how often the +// callback runs during 200 ms. +static int run(bool buggy) { + fcitx::EventLoop loop; + int fired = 0; + std::unique_ptr timer; + auto arm = [&] { + if (!timer) { + timer = loop.addTimeEvent(CLOCK_MONOTONIC, usecIn(std::chrono::milliseconds(10)), 1000, + [&](fcitx::EventSourceTime *, uint64_t) { + fired++; + return true; + }); + timer->setOneShot(); + } else { + timer->setTime(usecIn(std::chrono::milliseconds(10))); + timer->setOneShot(); + if (buggy) { + timer->setEnabled(true); // what scheduleFrame used to do + } + } + }; + auto stop = loop.addTimeEvent(CLOCK_MONOTONIC, usecIn(std::chrono::milliseconds(200)), 1000, + [&](fcitx::EventSourceTime *, uint64_t) { + loop.exit(); + return true; + }); + auto second = loop.addTimeEvent(CLOCK_MONOTONIC, usecIn(std::chrono::milliseconds(60)), 1000, + [&](fcitx::EventSourceTime *, uint64_t) { + arm(); // the second animation + return true; + }); + arm(); // the first animation + loop.exec(); + return fired; +} + +int main() { + const int buggy = run(true); + const int fixed = run(false); + std::printf("old pattern (setTime, setOneShot, setEnabled): %d callbacks in 200 ms\n", buggy); + std::printf("new pattern (setTime, setOneShot): %d callbacks in 200 ms\n", fixed); + // Two animations, so two shots are right. + if (fixed != 2) { + std::printf("FAIL: expected exactly 2 callbacks\n"); + return 1; + } + if (buggy <= 2) { + std::printf("note: the old pattern did not misbehave on this build\n"); + } + std::printf("ok\n"); + return 0; +}