Files
justmeandClaude Sonnet 5.5 bf3c88acc8 Fix the candidate window freezing the input method at 100% CPU
The animation frame timer was re-armed with setTime(), setOneShot() and then
setEnabled(true). In the systemd event loop backend setEnabled(true) means
SD_EVENT_ON, a repeating source, and setOneShot() had just set SD_EVENT_ONESHOT.
The re-armed timer was therefore repeating at a time that had already passed,
and the event loop fired it again on every iteration: the main thread of Fcitx5
sat at 100% CPU and typing froze. The first animation was not affected, the
timer is only re-armed from the second one on, which is why it showed up as a
sudden freeze after typing for a while.

setOneShot() alone is correct. tests/timer_rearm.cpp arms a timer twice the way
the window does: with the old calls it fires 198725 times in 200 ms, with the
fix exactly twice.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 23:36:54 +08:00

78 lines
3.0 KiB
C++

// 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 <fcitx-utils/event.h>
#include <fcitx-utils/log.h>
#include <chrono>
#include <cstdio>
#include <memory>
using Clock = std::chrono::steady_clock;
static uint64_t usecIn(std::chrono::milliseconds ms) {
return std::chrono::duration_cast<std::chrono::microseconds>(
(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<fcitx::EventSourceTime> 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;
}