GCC Code Coverage Report


Directory: ./
File: Timers.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 135 147 91.8%
Functions: 12 12 100.0%
Branches: 133 220 60.5%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include <algorithm>
16 #include <climits>
17 #include <list>
18 #include <mutex>
19 #include <string>
20 #include <map>
21 #include <vector>
22
23 #include "common.h"
24 #include "commands/Commands.h"
25
26 #include "EPollManager.h"
27 #include "Timers.h"
28
29 Timers Timers::INSTANCE;
30
31 2346 Timers::Timers() {
32 2346 }
33
34 // Named timers that fireTimersInternal() has copied out to fire and not yet
35 // fired, with an epoch bumped (under Timers::lock) whenever one is stopped or
36 // replaced. fireTimersInternal() copies every due timer before running any of
37 // them, so without this a timer that an earlier callback in the same pass
38 // stopped -- a plugin's unload stopping its own periodic timer, then deleting
39 // the plugin -- still ran once from its copy. An entry lives only while a
40 // copy is pending: a name per fired timer would grow without bound, since
41 // some timers take a fresh name every time (a per-play media stop, each pass
42 // of looping media). Kept here rather than in TimerInfo because Timers.h is
43 // plugin ABI.
44 struct PendingFire {
45 uint64_t epoch = 0;
46 int copies = 0;
47 };
48 static std::map<std::string, PendingFire> s_pendingFires;
49
50 673 static void BumpTimerEpoch(const std::string& name) {
51
1/2
✓ Branch 0 taken 673 times.
✗ Branch 1 not taken.
673 auto it = s_pendingFires.find(name);
52
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 623 times.
673 if (it != s_pendingFires.end()) {
53 50 it->second.epoch++;
54 }
55 673 }
56
57 // A caller's GetTimeMS() deadline as a GetMonotonicTimeMS() one.
58 3462 static long long WallToMonotonic(long long wallMS) {
59 3462 return GetMonotonicTimeMS() + (wallMS - GetTimeMS());
60 }
61
62 458 Timers::~Timers() {
63
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
458 for (auto a : timers) {
64 ✗ if (a) {
65 ✗ delete a;
66 }
67 }
68 458 timers.clear();
69 458 }
70 3347 void Timers::addTimer(const std::string& name, long long fireTimeMS, std::function<void()>&& callback) {
71
1/2
✓ Branch 0 taken 3347 times.
✗ Branch 1 not taken.
3347 std::unique_lock<std::mutex> l(lock);
72
1/2
✓ Branch 0 taken 3347 times.
✗ Branch 1 not taken.
3347 if (!name.empty()) {
73
2/2
✓ Branch 0 taken 7563 times.
✓ Branch 1 taken 2952 times.
10515 for (auto& a : timers) {
74
6/6
✓ Branch 0 taken 6624 times.
✓ Branch 1 taken 939 times.
✓ Branch 2 taken 395 times.
✓ Branch 3 taken 6229 times.
✓ Branch 4 taken 395 times.
✓ Branch 5 taken 7168 times.
7563 if (a && a->id == name) {
75
1/2
✓ Branch 0 taken 395 times.
✗ Branch 1 not taken.
395 BumpTimerEpoch(name);
76
1/2
✓ Branch 0 taken 395 times.
✗ Branch 1 not taken.
395 a->fireTimeMS = WallToMonotonic(fireTimeMS);
77
1/2
✓ Branch 0 taken 395 times.
✗ Branch 1 not taken.
395 a->callback = callback;
78
1/2
✓ Branch 0 taken 395 times.
✗ Branch 1 not taken.
395 a->commandPreset = "";
79
1/2
✓ Branch 0 taken 395 times.
✗ Branch 1 not taken.
395 updateTimers();
80 395 return;
81 }
82 }
83 }
84
2/4
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2952 times.
2952 TimerInfo* i = new TimerInfo;
85
1/2
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
2952 i->id = name;
86
1/2
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
2952 i->callback = callback;
87
1/2
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
2952 i->fireTimeMS = WallToMonotonic(fireTimeMS);
88
1/2
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
2952 timers.push_back(i);
89
1/2
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
2952 updateTimers();
90
2/2
✓ Branch 0 taken 2952 times.
✓ Branch 1 taken 395 times.
3347 }
91 115 void Timers::addTimer(const std::string& name, long long fireTimeMS, const std::string& preset) {
92
2/4
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 115 times.
115 if (preset == "") {
93 ✗ return;
94 }
95
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 std::unique_lock<std::mutex> l(lock);
96
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 if (!name.empty()) {
97
2/2
✓ Branch 0 taken 115 times.
✓ Branch 1 taken 115 times.
230 for (auto& a : timers) {
98
3/6
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 115 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 115 times.
115 if (a && a->id == name) {
99 // A preset timer is a one-shot preset and nothing else: keeping
100 // the old entry's callback and periodic rate turned a preset
101 // aimed at an internal timer's id ("FileMonitor") into a preset
102 // fired every 2s for good, and that timer's own work never ran.
103 ✗ BumpTimerEpoch(name);
104 ✗ a->fireTimeMS = WallToMonotonic(fireTimeMS);
105 ✗ a->commandPreset = preset;
106 ✗ a->callback = nullptr;
107 ✗ a->periodicRate = 0;
108 ✗ updateTimers();
109 ✗ return;
110 }
111 }
112 }
113
2/4
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 115 times.
115 TimerInfo* i = new TimerInfo;
114
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 i->id = name;
115
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 i->commandPreset = preset;
116
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 i->fireTimeMS = WallToMonotonic(fireTimeMS);
117
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 timers.push_back(i);
118
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 updateTimers();
119
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 }
120
121 2181 void Timers::addPeriodicTimer(const std::string& name, long long fireTimeMS, std::function<void()>&& callback) {
122 // Kept as a distinct overload (rather than a default argument) so the original
123 // 3-arg mangled symbol remains in libfpp.so for pre-built external plugins.
124 2181 addPeriodicTimer(name, fireTimeMS, std::move(callback), -1);
125 2181 }
126 2195 void Timers::addPeriodicTimer(const std::string& name, long long fireTimeMS, std::function<void()>&& callback,
127 long long initialDelayMS) {
128
2/2
✓ Branch 0 taken 2181 times.
✓ Branch 1 taken 14 times.
2195 long long firstDelay = initialDelayMS < 0 ? fireTimeMS : initialDelayMS;
129
1/2
✓ Branch 0 taken 2195 times.
✗ Branch 1 not taken.
2195 std::unique_lock<std::mutex> l(lock);
130
1/2
✓ Branch 0 taken 2195 times.
✗ Branch 1 not taken.
2195 if (!name.empty()) {
131
2/2
✓ Branch 0 taken 6261 times.
✓ Branch 1 taken 2175 times.
8436 for (auto& a : timers) {
132
6/6
✓ Branch 0 taken 6260 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 6240 times.
✓ Branch 4 taken 20 times.
✓ Branch 5 taken 6241 times.
6261 if (a && a->id == name) {
133
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 BumpTimerEpoch(name);
134
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 a->fireTimeMS = GetMonotonicTimeMS() + firstDelay;
135 20 a->periodicRate = fireTimeMS;
136
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 a->callback = callback;
137
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 a->commandPreset = "";
138
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 updateTimers();
139 20 return;
140 }
141 }
142 }
143
2/4
✓ Branch 0 taken 2175 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2175 times.
2175 TimerInfo* i = new TimerInfo;
144
1/2
✓ Branch 0 taken 2175 times.
✗ Branch 1 not taken.
2175 i->id = name;
145
1/2
✓ Branch 0 taken 2175 times.
✗ Branch 1 not taken.
2175 i->callback = callback;
146
1/2
✓ Branch 0 taken 2175 times.
✗ Branch 1 not taken.
2175 i->fireTimeMS = GetMonotonicTimeMS() + firstDelay;
147 2175 i->periodicRate = fireTimeMS;
148
1/2
✓ Branch 0 taken 2175 times.
✗ Branch 1 not taken.
2175 timers.push_back(i);
149
1/2
✓ Branch 0 taken 2175 times.
✗ Branch 1 not taken.
2175 updateTimers();
150
2/2
✓ Branch 0 taken 2175 times.
✓ Branch 1 taken 20 times.
2195 }
151 258 void Timers::stopPeriodicTimer(const std::string& name) {
152
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 std::unique_lock<std::mutex> l(lock);
153
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 if (!name.empty()) {
154 // Even if it isn't in the list: a one-shot already taken out to fire
155 // in the current pass must not run after being stopped either.
156
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 BumpTimerEpoch(name);
157
2/2
✓ Branch 0 taken 4612 times.
✓ Branch 1 taken 84 times.
4696 for (int x = 0; x < timers.size(); ++x) {
158 4612 auto a = timers[x];
159
5/6
✓ Branch 0 taken 4612 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 174 times.
✓ Branch 3 taken 4438 times.
✓ Branch 4 taken 174 times.
✓ Branch 5 taken 4438 times.
4612 if (a && a->id == name) {
160
1/2
✓ Branch 0 taken 174 times.
✗ Branch 1 not taken.
174 delete a;
161 174 timers[x] = nullptr;
162
1/2
✓ Branch 0 taken 174 times.
✗ Branch 1 not taken.
174 updateTimers();
163 174 return;
164 }
165 }
166 }
167
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 174 times.
258 }
168
169 20234 void Timers::fireTimersInternal(long long t) {
170 // Callbacks run with the lock released (they add and stop timers), so they
171 // must not run out of a TimerInfo that is still in the list: a
172 // stopPeriodicTimer() from another thread deletes it, and a periodic timer
173 // that stops or re-adds itself from its own callback destroys or replaces
174 // the std::function that is executing. Fire private copies instead.
175 20234 std::list<std::pair<TimerInfo, uint64_t>> toFire; // copy, epoch when copied
176
177
1/2
✓ Branch 0 taken 20234 times.
✗ Branch 1 not taken.
20234 std::unique_lock<std::mutex> l(lock);
178 20234 int m = timers.size();
179 20234 bool fired = false;
180
2/2
✓ Branch 0 taken 115502 times.
✓ Branch 1 taken 20234 times.
135736 for (int x = 0; x < m; ++x) {
181 115502 auto a = timers[x];
182
3/4
✓ Branch 0 taken 115502 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85320 times.
✓ Branch 3 taken 30182 times.
115502 if (a && (a->fireTimeMS <= t)) {
183 85320 uint64_t epoch = 0;
184
1/2
✓ Branch 0 taken 85320 times.
✗ Branch 1 not taken.
85320 if (!a->id.empty()) {
185
1/2
✓ Branch 0 taken 85320 times.
✗ Branch 1 not taken.
85320 PendingFire& pf = s_pendingFires[a->id];
186 85320 pf.copies++;
187 85320 epoch = pf.epoch;
188 }
189
2/2
✓ Branch 0 taken 82297 times.
✓ Branch 1 taken 3023 times.
85320 if (a->periodicRate) {
190
1/2
✓ Branch 0 taken 82297 times.
✗ Branch 1 not taken.
82297 a->fireTimeMS = GetMonotonicTimeMS() + a->periodicRate;
191
1/2
✓ Branch 0 taken 82297 times.
✗ Branch 1 not taken.
82297 toFire.emplace_back(*a, epoch);
192 } else {
193
1/2
✓ Branch 0 taken 3023 times.
✗ Branch 1 not taken.
3023 toFire.emplace_back(std::move(*a), epoch);
194
1/2
✓ Branch 0 taken 3023 times.
✗ Branch 1 not taken.
3023 delete a;
195 3023 timers[x] = nullptr;
196 }
197 }
198 }
199
1/2
✓ Branch 0 taken 20234 times.
✗ Branch 1 not taken.
20234 l.unlock();
200 // The list changed (one-shots taken out, periodic deadlines moved) even
201 // if every callback below turns out to be skipped, so re-arm regardless.
202 20234 fired = !toFire.empty();
203
2/2
✓ Branch 0 taken 85320 times.
✓ Branch 1 taken 20234 times.
105554 for (auto& [a, epoch] : toFire) {
204
1/2
✓ Branch 0 taken 85320 times.
✗ Branch 1 not taken.
85320 if (!a.id.empty()) {
205
1/2
✓ Branch 0 taken 85320 times.
✗ Branch 1 not taken.
85320 std::unique_lock<std::mutex> el(lock);
206
1/2
✓ Branch 0 taken 85320 times.
✗ Branch 1 not taken.
85320 auto it = s_pendingFires.find(a.id);
207 85320 bool stale = it->second.epoch != epoch;
208
1/2
✓ Branch 0 taken 85320 times.
✗ Branch 1 not taken.
85320 if (--it->second.copies == 0) {
209
1/2
✓ Branch 0 taken 85320 times.
✗ Branch 1 not taken.
85320 s_pendingFires.erase(it);
210 }
211
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 85270 times.
85320 if (stale) {
212 50 continue; // stopped or replaced by an earlier callback this pass
213 }
214
2/2
✓ Branch 0 taken 85270 times.
✓ Branch 1 taken 50 times.
85320 }
215
1/2
✓ Branch 0 taken 85270 times.
✗ Branch 1 not taken.
85270 fireTimer(&a);
216 }
217
1/2
✓ Branch 0 taken 20234 times.
✗ Branch 1 not taken.
20234 if (fired) {
218
1/2
✓ Branch 0 taken 20234 times.
✗ Branch 1 not taken.
20234 l.lock();
219
1/2
✓ Branch 0 taken 20234 times.
✗ Branch 1 not taken.
20234 updateTimers();
220 }
221 20234 }
222 26065 void Timers::updateTimers() {
223
2/2
✓ Branch 0 taken 140165 times.
✓ Branch 1 taken 26065 times.
166230 for (int x = 0; x < timers.size(); x++) {
224
6/6
✓ Branch 0 taken 141484 times.
✓ Branch 1 taken 1878 times.
✓ Branch 2 taken 3197 times.
✓ Branch 3 taken 138287 times.
✓ Branch 4 taken 3197 times.
✓ Branch 5 taken 140165 times.
143362 while (x < timers.size() && timers[x] == nullptr) {
225 3197 timers[x] = timers[timers.size() - 1];
226 3197 timers.resize(timers.size() - 1);
227 }
228 }
229
1/2
✓ Branch 0 taken 26065 times.
✗ Branch 1 not taken.
26065 if (!timers.empty()) {
230 // Worked out locally and stored once: fireTimers() reads nextTimer
231 // without the lock.
232 26065 long long next = LLONG_MAX;
233
2/2
✓ Branch 0 taken 138287 times.
✓ Branch 1 taken 26065 times.
164352 for (int x = 0; x < timers.size(); x++) {
234 138287 next = std::min(timers[x]->fireTimeMS, next);
235 }
236 26065 nextTimer = next;
237 26065 hasTimers = true;
238 // armTimer() takes a GetTimeMS() deadline; converted at this moment
239 // it is the same delay, whatever the wall clock does later.
240
3/6
✓ Branch 0 taken 26065 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26065 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26065 times.
✗ Branch 5 not taken.
26065 EPollManager::INSTANCE.armTimer(GetTimeMS() + (next - GetMonotonicTimeMS()));
241 } else {
242 ✗ hasTimers = false;
243 ✗ EPollManager::INSTANCE.disarmTimer();
244 }
245 26065 }
246
247 85270 void Timers::fireTimer(TimerInfo* t) {
248
2/2
✓ Branch 0 taken 85155 times.
✓ Branch 1 taken 115 times.
85270 if (t->commandPreset.empty()) {
249 85155 t->callback();
250 } else {
251 115 LogDebug(VB_COMMAND, "\"Trigger Command Preset In Future\" identifier \"%s\" triggering preset \"%s\"\n", t->id.c_str(), t->commandPreset.c_str());
252
2/4
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 115 times.
✗ Branch 3 not taken.
115 CommandManager::INSTANCE.TriggerPreset(t->commandPreset);
253 }
254 85270 }
255