A race condition in the animations manager can leave a dangling timerEvent() callback firing at a high frequency (>120 FPS) in the main loop even though there is no active animations. An update of the animations manager returns directly when there is no active animations. If there is at least one active animation, it stops the timer and schedules a new update before updating animations. Depending on the Integration implementation, the scheduling call can be postponed after the current update. The actual postponed call unconditionally schedules an update by starting the timer. The issue is that in the meantime the last remaining animation could have been removed and, when the timer callback would be fired, the update would return directly (since there is no active animations) without being able to stop. The explanation above ignores the updateQueued() cases of the postponed call for simplicity. These cases do not result in infinite updates like the timer case but still imply one useless (invoked) update. This fix adds a condition in the postponed call ensuring there is at least one active animation before processing. telegramdesktop/tdesktop#3640 telegramdesktop/tdesktop#4854 telegramdesktop/tdesktop#5436
211 lines
4.2 KiB
C++
211 lines
4.2 KiB
C++
// This file is part of Desktop App Toolkit,
|
|
// a set of libraries for developing nice desktop applications.
|
|
//
|
|
// For license and copyright information please follow this link:
|
|
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
|
//
|
|
#include "ui/effects/animations.h"
|
|
|
|
#include "ui/ui_utility.h"
|
|
#include "base/invoke_queued.h"
|
|
|
|
#include <QtCore/QPointer>
|
|
|
|
#include <crl/crl_on_main.h>
|
|
#include <crl/crl.h>
|
|
#include <rpl/filter.h>
|
|
#include <range/v3/algorithm/remove_if.hpp>
|
|
#include <range/v3/algorithm/remove.hpp>
|
|
#include <range/v3/algorithm/find.hpp>
|
|
|
|
namespace Ui {
|
|
namespace Animations {
|
|
namespace {
|
|
|
|
constexpr auto kAnimationTick = crl::time(1000) / 120;
|
|
constexpr auto kIgnoreUpdatesTimeout = crl::time(4);
|
|
|
|
Manager *ManagerInstance = nullptr;
|
|
|
|
} // namespace
|
|
|
|
void Basic::start() {
|
|
Expects(ManagerInstance != nullptr);
|
|
|
|
if (animating()) {
|
|
restart();
|
|
} else {
|
|
ManagerInstance->start(this);
|
|
}
|
|
}
|
|
|
|
void Basic::stop() {
|
|
Expects(ManagerInstance != nullptr);
|
|
|
|
if (animating()) {
|
|
ManagerInstance->stop(this);
|
|
}
|
|
}
|
|
|
|
void Basic::restart() {
|
|
Expects(_started >= 0);
|
|
|
|
_started = crl::now();
|
|
|
|
Ensures(_started >= 0);
|
|
}
|
|
|
|
void Basic::markStarted() {
|
|
Expects(_started < 0);
|
|
|
|
_started = crl::now();
|
|
|
|
Ensures(_started >= 0);
|
|
}
|
|
|
|
void Basic::markStopped() {
|
|
Expects(_started >= 0);
|
|
|
|
_started = -1;
|
|
}
|
|
|
|
Manager::Manager() {
|
|
Expects(ManagerInstance == nullptr);
|
|
|
|
ManagerInstance = this;
|
|
|
|
crl::on_main_update_requests(
|
|
) | rpl::filter([=] {
|
|
return (_lastUpdateTime + kIgnoreUpdatesTimeout < crl::now());
|
|
}) | rpl::start_with_next([=] {
|
|
update();
|
|
}, _lifetime);
|
|
}
|
|
|
|
Manager::~Manager() {
|
|
Expects(ManagerInstance == this);
|
|
Expects(_active.empty());
|
|
Expects(_starting.empty());
|
|
|
|
ManagerInstance = nullptr;
|
|
}
|
|
|
|
void Manager::start(not_null<Basic*> animation) {
|
|
_forceImmediateUpdate = true;
|
|
if (_updating) {
|
|
_starting.emplace_back(animation.get());
|
|
} else {
|
|
schedule();
|
|
_active.emplace_back(animation.get());
|
|
}
|
|
}
|
|
|
|
void Manager::stop(not_null<Basic*> animation) {
|
|
if (empty(_active) && empty(_starting)) {
|
|
return;
|
|
}
|
|
const auto value = animation.get();
|
|
const auto proj = &ActiveBasicPointer::get;
|
|
auto &list = _updating ? _starting : _active;
|
|
list.erase(ranges::remove(list, value, proj), end(list));
|
|
|
|
if (_updating) {
|
|
const auto i = ranges::find(_active, value, proj);
|
|
if (i != end(_active)) {
|
|
*i = nullptr;
|
|
_removedWhileUpdating = true;
|
|
}
|
|
} else if (empty(_active)) {
|
|
stopTimer();
|
|
}
|
|
}
|
|
|
|
void Manager::update() {
|
|
if (_active.empty() || _updating || _scheduled) {
|
|
return;
|
|
}
|
|
const auto now = crl::now();
|
|
if (_forceImmediateUpdate) {
|
|
_forceImmediateUpdate = false;
|
|
}
|
|
schedule();
|
|
|
|
_updating = true;
|
|
const auto guard = gsl::finally([&] { _updating = false; });
|
|
|
|
_lastUpdateTime = now;
|
|
const auto isFinished = [&](const ActiveBasicPointer &element) {
|
|
return !element.call(now);
|
|
};
|
|
_active.erase(ranges::remove_if(_active, isFinished), end(_active));
|
|
|
|
if (_removedWhileUpdating) {
|
|
_removedWhileUpdating = false;
|
|
const auto proj = &ActiveBasicPointer::get;
|
|
_active.erase(ranges::remove(_active, nullptr, proj), end(_active));
|
|
}
|
|
|
|
if (!empty(_starting)) {
|
|
_active.insert(
|
|
end(_active),
|
|
std::make_move_iterator(begin(_starting)),
|
|
std::make_move_iterator(end(_starting)));
|
|
_starting.clear();
|
|
}
|
|
}
|
|
|
|
void Manager::updateQueued() {
|
|
Expects(_timerId == 0);
|
|
|
|
_timerId = -1;
|
|
InvokeQueued(delayedCallGuard(), [=] {
|
|
Expects(_timerId < 0);
|
|
|
|
_timerId = 0;
|
|
update();
|
|
});
|
|
}
|
|
|
|
void Manager::schedule() {
|
|
if (_scheduled || _timerId < 0) {
|
|
return;
|
|
}
|
|
stopTimer();
|
|
|
|
_scheduled = true;
|
|
PostponeCall(delayedCallGuard(), [=] {
|
|
_scheduled = false;
|
|
if (_active.empty()) {
|
|
return;
|
|
}
|
|
if (_forceImmediateUpdate) {
|
|
_forceImmediateUpdate = false;
|
|
updateQueued();
|
|
} else {
|
|
const auto next = _lastUpdateTime + kAnimationTick;
|
|
const auto now = crl::now();
|
|
if (now < next) {
|
|
_timerId = startTimer(next - now, Qt::PreciseTimer);
|
|
} else {
|
|
updateQueued();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
not_null<const QObject*> Manager::delayedCallGuard() const {
|
|
return static_cast<const QObject*>(this);
|
|
}
|
|
|
|
void Manager::stopTimer() {
|
|
if (_timerId > 0) {
|
|
killTimer(base::take(_timerId));
|
|
}
|
|
}
|
|
|
|
void Manager::timerEvent(QTimerEvent *e) {
|
|
update();
|
|
}
|
|
|
|
} // namespace Animations
|
|
} // namespace Ui
|