/src/mozilla-central/dom/base/TimeoutManager.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
3 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
4 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
5 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
6 | | |
7 | | #include "TimeoutManager.h" |
8 | | #include "nsContentUtils.h" |
9 | | #include "nsGlobalWindow.h" |
10 | | #include "mozilla/Logging.h" |
11 | | #include "mozilla/PerformanceCounter.h" |
12 | | #include "mozilla/StaticPrefs.h" |
13 | | #include "mozilla/Telemetry.h" |
14 | | #include "mozilla/ThrottledEventQueue.h" |
15 | | #include "mozilla/TimeStamp.h" |
16 | | #include "nsIDocShell.h" |
17 | | #include "nsINamed.h" |
18 | | #include "nsITimeoutHandler.h" |
19 | | #include "mozilla/dom/DocGroup.h" |
20 | | #include "mozilla/dom/TabGroup.h" |
21 | | #include "OrderedTimeoutIterator.h" |
22 | | #include "TimeoutExecutor.h" |
23 | | #include "TimeoutBudgetManager.h" |
24 | | #include "mozilla/net/WebSocketEventService.h" |
25 | | #include "mozilla/MediaManager.h" |
26 | | |
27 | | using namespace mozilla; |
28 | | using namespace mozilla::dom; |
29 | | |
30 | | static LazyLogModule gLog("Timeout"); |
31 | | |
32 | | static int32_t gRunningTimeoutDepth = 0; |
33 | | |
34 | | // The default shortest interval/timeout we permit |
35 | 0 | #define DEFAULT_MIN_CLAMP_TIMEOUT_VALUE 4 // 4ms |
36 | 0 | #define DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE 1000 // 1000ms |
37 | 0 | #define DEFAULT_MIN_TRACKING_TIMEOUT_VALUE 4 // 4ms |
38 | 0 | #define DEFAULT_MIN_TRACKING_BACKGROUND_TIMEOUT_VALUE 1000 // 1000ms |
39 | | static int32_t gMinClampTimeoutValue = 0; |
40 | | static int32_t gMinBackgroundTimeoutValue = 0; |
41 | | static int32_t gMinTrackingTimeoutValue = 0; |
42 | | static int32_t gMinTrackingBackgroundTimeoutValue = 0; |
43 | | static int32_t gTimeoutThrottlingDelay = 0; |
44 | | static bool gAnnotateTrackingChannels = false; |
45 | | |
46 | 0 | #define DEFAULT_BACKGROUND_BUDGET_REGENERATION_FACTOR 100 // 1ms per 100ms |
47 | 0 | #define DEFAULT_FOREGROUND_BUDGET_REGENERATION_FACTOR 1 // 1ms per 1ms |
48 | 0 | #define DEFAULT_BACKGROUND_THROTTLING_MAX_BUDGET 50 // 50ms |
49 | 0 | #define DEFAULT_FOREGROUND_THROTTLING_MAX_BUDGET -1 // infinite |
50 | 0 | #define DEFAULT_BUDGET_THROTTLING_MAX_DELAY 15000 // 15s |
51 | 0 | #define DEFAULT_ENABLE_BUDGET_TIMEOUT_THROTTLING false |
52 | | static int32_t gBackgroundBudgetRegenerationFactor = 0; |
53 | | static int32_t gForegroundBudgetRegenerationFactor = 0; |
54 | | static int32_t gBackgroundThrottlingMaxBudget = 0; |
55 | | static int32_t gForegroundThrottlingMaxBudget = 0; |
56 | | static int32_t gBudgetThrottlingMaxDelay = 0; |
57 | | static bool gEnableBudgetTimeoutThrottling = false; |
58 | | |
59 | | // static |
60 | | const uint32_t TimeoutManager::InvalidFiringId = 0; |
61 | | |
62 | | namespace |
63 | | { |
64 | | double |
65 | | GetRegenerationFactor(bool aIsBackground) |
66 | 0 | { |
67 | 0 | // Lookup function for "dom.timeout.{background, |
68 | 0 | // foreground}_budget_regeneration_rate". |
69 | 0 |
|
70 | 0 | // Returns the rate of regeneration of the execution budget as a |
71 | 0 | // fraction. If the value is 1.0, the amount of time regenerated is |
72 | 0 | // equal to time passed. At this rate we regenerate 1ms/ms. If it is |
73 | 0 | // 0.01 the amount regenerated is 1% of time passed. At this rate we |
74 | 0 | // regenerate 1ms/100ms, etc. |
75 | 0 | double denominator = |
76 | 0 | std::max(aIsBackground ? gBackgroundBudgetRegenerationFactor |
77 | 0 | : gForegroundBudgetRegenerationFactor, |
78 | 0 | 1); |
79 | 0 | return 1.0 / denominator; |
80 | 0 | } |
81 | | |
82 | | TimeDuration |
83 | | GetMaxBudget(bool aIsBackground) |
84 | 0 | { |
85 | 0 | // Lookup function for "dom.timeout.{background, |
86 | 0 | // foreground}_throttling_max_budget". |
87 | 0 |
|
88 | 0 | // Returns how high a budget can be regenerated before being |
89 | 0 | // clamped. If this value is less or equal to zero, |
90 | 0 | // TimeDuration::Forever() is implied. |
91 | 0 | int32_t maxBudget = aIsBackground ? gBackgroundThrottlingMaxBudget |
92 | 0 | : gForegroundThrottlingMaxBudget; |
93 | 0 | return maxBudget > 0 ? TimeDuration::FromMilliseconds(maxBudget) |
94 | 0 | : TimeDuration::Forever(); |
95 | 0 | } |
96 | | |
97 | | TimeDuration |
98 | | GetMinBudget(bool aIsBackground) |
99 | 0 | { |
100 | 0 | // The minimum budget is computed by looking up the maximum allowed |
101 | 0 | // delay and computing how long time it would take to regenerate |
102 | 0 | // that budget using the regeneration factor. This number is |
103 | 0 | // expected to be negative. |
104 | 0 | return TimeDuration::FromMilliseconds( |
105 | 0 | - gBudgetThrottlingMaxDelay / |
106 | 0 | std::max(aIsBackground ? gBackgroundBudgetRegenerationFactor |
107 | 0 | : gForegroundBudgetRegenerationFactor, |
108 | 0 | 1)); |
109 | 0 | } |
110 | | } // namespace |
111 | | |
112 | | // |
113 | | |
114 | | bool |
115 | | TimeoutManager::IsBackground() const |
116 | 0 | { |
117 | 0 | return !IsActive() && mWindow.IsBackgroundInternal(); |
118 | 0 | } |
119 | | |
120 | | bool |
121 | | TimeoutManager::IsActive() const |
122 | 0 | { |
123 | 0 | // A window is considered active if: |
124 | 0 | // * It is a chrome window |
125 | 0 | // * It is playing audio |
126 | 0 | // |
127 | 0 | // Note that a window can be considered active if it is either in the |
128 | 0 | // foreground or in the background. |
129 | 0 |
|
130 | 0 | if (mWindow.IsChromeWindow()) { |
131 | 0 | return true; |
132 | 0 | } |
133 | 0 | |
134 | 0 | // Check if we're playing audio |
135 | 0 | if (mWindow.AsInner()->IsPlayingAudio()) { |
136 | 0 | return true; |
137 | 0 | } |
138 | 0 | |
139 | 0 | return false; |
140 | 0 | } |
141 | | |
142 | | |
143 | | uint32_t |
144 | | TimeoutManager::CreateFiringId() |
145 | 0 | { |
146 | 0 | uint32_t id = mNextFiringId; |
147 | 0 | mNextFiringId += 1; |
148 | 0 | if (mNextFiringId == InvalidFiringId) { |
149 | 0 | mNextFiringId += 1; |
150 | 0 | } |
151 | 0 |
|
152 | 0 | mFiringIdStack.AppendElement(id); |
153 | 0 |
|
154 | 0 | return id; |
155 | 0 | } |
156 | | |
157 | | void |
158 | | TimeoutManager::DestroyFiringId(uint32_t aFiringId) |
159 | 0 | { |
160 | 0 | MOZ_DIAGNOSTIC_ASSERT(!mFiringIdStack.IsEmpty()); |
161 | 0 | MOZ_DIAGNOSTIC_ASSERT(mFiringIdStack.LastElement() == aFiringId); |
162 | 0 | mFiringIdStack.RemoveLastElement(); |
163 | 0 | } |
164 | | |
165 | | bool |
166 | | TimeoutManager::IsValidFiringId(uint32_t aFiringId) const |
167 | 0 | { |
168 | 0 | return !IsInvalidFiringId(aFiringId); |
169 | 0 | } |
170 | | |
171 | | TimeDuration |
172 | | TimeoutManager::MinSchedulingDelay() const |
173 | 0 | { |
174 | 0 | if (IsActive()) { |
175 | 0 | return TimeDuration(); |
176 | 0 | } |
177 | 0 | |
178 | 0 | bool isBackground = mWindow.IsBackgroundInternal(); |
179 | 0 |
|
180 | 0 | // If a window isn't active as defined by TimeoutManager::IsActive() |
181 | 0 | // and we're throttling timeouts using an execution budget, we |
182 | 0 | // should adjust the minimum scheduling delay if we have used up all |
183 | 0 | // of our execution budget. Note that a window can be active or |
184 | 0 | // inactive regardless of wether it is in the foreground or in the |
185 | 0 | // background. Throttling using a budget depends largely on the |
186 | 0 | // regeneration factor, which can be specified separately for |
187 | 0 | // foreground and background windows. |
188 | 0 | // |
189 | 0 | // The value that we compute is the time in the future when we again |
190 | 0 | // have a positive execution budget. We do this by taking the |
191 | 0 | // execution budget into account, which if it positive implies that |
192 | 0 | // we have time left to execute, and if it is negative implies that |
193 | 0 | // we should throttle it until the budget again is positive. The |
194 | 0 | // factor used is the rate of budget regeneration. |
195 | 0 | // |
196 | 0 | // We clamp the delay to be less than or equal to |
197 | 0 | // gBudgetThrottlingMaxDelay to not entirely starve the timeouts. |
198 | 0 | // |
199 | 0 | // Consider these examples assuming we should throttle using |
200 | 0 | // budgets: |
201 | 0 | // |
202 | 0 | // mExecutionBudget is 20ms |
203 | 0 | // factor is 1, which is 1 ms/ms |
204 | 0 | // delay is 0ms |
205 | 0 | // then we will compute the minimum delay: |
206 | 0 | // max(0, - 20 * 1) = 0 |
207 | 0 | // |
208 | 0 | // mExecutionBudget is -50ms |
209 | 0 | // factor is 0.1, which is 1 ms/10ms |
210 | 0 | // delay is 1000ms |
211 | 0 | // then we will compute the minimum delay: |
212 | 0 | // max(1000, - (- 50) * 1/0.1) = max(1000, 500) = 1000 |
213 | 0 | // |
214 | 0 | // mExecutionBudget is -15ms |
215 | 0 | // factor is 0.01, which is 1 ms/100ms |
216 | 0 | // delay is 1000ms |
217 | 0 | // then we will compute the minimum delay: |
218 | 0 | // max(1000, - (- 15) * 1/0.01) = max(1000, 1500) = 1500 |
219 | 0 | TimeDuration unthrottled = |
220 | 0 | isBackground ? TimeDuration::FromMilliseconds(gMinBackgroundTimeoutValue) |
221 | 0 | : TimeDuration(); |
222 | 0 | if (BudgetThrottlingEnabled(isBackground) && |
223 | 0 | mExecutionBudget < TimeDuration()) { |
224 | 0 | // Only throttle if execution budget is less than 0 |
225 | 0 | double factor = 1.0 / GetRegenerationFactor(mWindow.IsBackgroundInternal()); |
226 | 0 | return TimeDuration::Max(unthrottled, -mExecutionBudget.MultDouble(factor)); |
227 | 0 | } |
228 | 0 | // |
229 | 0 | return unthrottled; |
230 | 0 | } |
231 | | |
232 | | nsresult |
233 | | TimeoutManager::MaybeSchedule(const TimeStamp& aWhen, const TimeStamp& aNow) |
234 | 0 | { |
235 | 0 | MOZ_DIAGNOSTIC_ASSERT(mExecutor); |
236 | 0 |
|
237 | 0 | // Before we can schedule the executor we need to make sure that we |
238 | 0 | // have an updated execution budget. |
239 | 0 | UpdateBudget(aNow); |
240 | 0 | return mExecutor->MaybeSchedule(aWhen, MinSchedulingDelay()); |
241 | 0 | } |
242 | | |
243 | | bool |
244 | | TimeoutManager::IsInvalidFiringId(uint32_t aFiringId) const |
245 | 0 | { |
246 | 0 | // Check the most common ways to invalidate a firing id first. |
247 | 0 | // These should be quite fast. |
248 | 0 | if (aFiringId == InvalidFiringId || |
249 | 0 | mFiringIdStack.IsEmpty()) { |
250 | 0 | return true; |
251 | 0 | } |
252 | 0 | |
253 | 0 | if (mFiringIdStack.Length() == 1) { |
254 | 0 | return mFiringIdStack[0] != aFiringId; |
255 | 0 | } |
256 | 0 | |
257 | 0 | // Next do a range check on the first and last items in the stack |
258 | 0 | // of active firing ids. This is a bit slower. |
259 | 0 | uint32_t low = mFiringIdStack[0]; |
260 | 0 | uint32_t high = mFiringIdStack.LastElement(); |
261 | 0 | MOZ_DIAGNOSTIC_ASSERT(low != high); |
262 | 0 | if (low > high) { |
263 | 0 | // If the first element is bigger than the last element in the |
264 | 0 | // stack, that means mNextFiringId wrapped around to zero at |
265 | 0 | // some point. |
266 | 0 | Swap(low, high); |
267 | 0 | } |
268 | 0 | MOZ_DIAGNOSTIC_ASSERT(low < high); |
269 | 0 |
|
270 | 0 | if (aFiringId < low || aFiringId > high) { |
271 | 0 | return true; |
272 | 0 | } |
273 | 0 | |
274 | 0 | // Finally, fall back to verifying the firing id is not anywhere |
275 | 0 | // in the stack. This could be slow for a large stack, but that |
276 | 0 | // should be rare. It can only happen with deeply nested event |
277 | 0 | // loop spinning. For example, a page that does a lot of timers |
278 | 0 | // and a lot of sync XHRs within those timers could be slow here. |
279 | 0 | return !mFiringIdStack.Contains(aFiringId); |
280 | 0 | } |
281 | | |
282 | | // The number of nested timeouts before we start clamping. HTML5 says 1, WebKit |
283 | | // uses 5. |
284 | 0 | #define DOM_CLAMP_TIMEOUT_NESTING_LEVEL 5u |
285 | | |
286 | | TimeDuration |
287 | 0 | TimeoutManager::CalculateDelay(Timeout* aTimeout) const { |
288 | 0 | MOZ_DIAGNOSTIC_ASSERT(aTimeout); |
289 | 0 | TimeDuration result = aTimeout->mInterval; |
290 | 0 |
|
291 | 0 | if (aTimeout->mNestingLevel >= DOM_CLAMP_TIMEOUT_NESTING_LEVEL) { |
292 | 0 | result = TimeDuration::Max( |
293 | 0 | result, TimeDuration::FromMilliseconds(gMinClampTimeoutValue)); |
294 | 0 | } |
295 | 0 |
|
296 | 0 | if (aTimeout->mIsTracking && mThrottleTrackingTimeouts) { |
297 | 0 | result = TimeDuration::Max( |
298 | 0 | result, TimeDuration::FromMilliseconds(gMinTrackingTimeoutValue)); |
299 | 0 | } |
300 | 0 |
|
301 | 0 | return result; |
302 | 0 | } |
303 | | |
304 | | PerformanceCounter* |
305 | | TimeoutManager::GetPerformanceCounter() |
306 | 0 | { |
307 | 0 | if (!StaticPrefs::dom_performance_enable_scheduler_timing()) { |
308 | 0 | return nullptr; |
309 | 0 | } |
310 | 0 | nsIDocument* doc = mWindow.GetDocument(); |
311 | 0 | if (doc) { |
312 | 0 | dom::DocGroup* docGroup = doc->GetDocGroup(); |
313 | 0 | if (docGroup) { |
314 | 0 | return docGroup->GetPerformanceCounter(); |
315 | 0 | } |
316 | 0 | } |
317 | 0 | return nullptr; |
318 | 0 | } |
319 | | |
320 | | void |
321 | | TimeoutManager::RecordExecution(Timeout* aRunningTimeout, |
322 | | Timeout* aTimeout) |
323 | 0 | { |
324 | 0 | if (!StaticPrefs::dom_performance_enable_scheduler_timing() && |
325 | 0 | mWindow.IsChromeWindow()) { |
326 | 0 | return; |
327 | 0 | } |
328 | 0 | |
329 | 0 | TimeoutBudgetManager& budgetManager = TimeoutBudgetManager::Get(); |
330 | 0 | TimeStamp now = TimeStamp::Now(); |
331 | 0 |
|
332 | 0 | if (aRunningTimeout) { |
333 | 0 | // If we're running a timeout callback, record any execution until |
334 | 0 | // now. |
335 | 0 | TimeDuration duration = budgetManager.RecordExecution( |
336 | 0 | now, aRunningTimeout, mWindow.IsBackgroundInternal()); |
337 | 0 | budgetManager.MaybeCollectTelemetry(now); |
338 | 0 |
|
339 | 0 | UpdateBudget(now, duration); |
340 | 0 |
|
341 | 0 | // This is an ad-hoc way to use the counters for the timers |
342 | 0 | // that should be removed at somepoint. See Bug 1482834 |
343 | 0 | PerformanceCounter* counter = GetPerformanceCounter(); |
344 | 0 | if (counter) { |
345 | 0 | counter->IncrementExecutionDuration(duration.ToMicroseconds()); |
346 | 0 | } |
347 | 0 | } |
348 | 0 |
|
349 | 0 | if (aTimeout) { |
350 | 0 | // If we're starting a new timeout callback, start recording. |
351 | 0 | budgetManager.StartRecording(now); |
352 | 0 | PerformanceCounter* counter = GetPerformanceCounter(); |
353 | 0 | if (counter) { |
354 | 0 | counter->IncrementDispatchCounter(DispatchCategory(TaskCategory::Timer)); |
355 | 0 | } |
356 | 0 | } else { |
357 | 0 | // Else stop by clearing the start timestamp. |
358 | 0 | budgetManager.StopRecording(); |
359 | 0 | } |
360 | 0 | } |
361 | | |
362 | | void |
363 | | TimeoutManager::UpdateBudget(const TimeStamp& aNow, const TimeDuration& aDuration) |
364 | 0 | { |
365 | 0 | if (mWindow.IsChromeWindow()) { |
366 | 0 | return; |
367 | 0 | } |
368 | 0 | |
369 | 0 | // The budget is adjusted by increasing it with the time since the |
370 | 0 | // last budget update factored with the regeneration rate. If a |
371 | 0 | // runnable has executed, subtract that duration from the |
372 | 0 | // budget. The budget updated without consideration of wether the |
373 | 0 | // window is active or not. If throttling is enabled and the window |
374 | 0 | // is active and then becomes inactive, an overdrawn budget will |
375 | 0 | // still be counted against the minimum delay. |
376 | 0 | bool isBackground = mWindow.IsBackgroundInternal(); |
377 | 0 | if (BudgetThrottlingEnabled(isBackground)) { |
378 | 0 | double factor = GetRegenerationFactor(isBackground); |
379 | 0 | TimeDuration regenerated = (aNow - mLastBudgetUpdate).MultDouble(factor); |
380 | 0 | // Clamp the budget to the range of minimum and maximum allowed budget. |
381 | 0 | mExecutionBudget = TimeDuration::Max( |
382 | 0 | GetMinBudget(isBackground), |
383 | 0 | TimeDuration::Min(GetMaxBudget(isBackground), |
384 | 0 | mExecutionBudget - aDuration + regenerated)); |
385 | 0 | } else { |
386 | 0 | // If budget throttling isn't enabled, reset the execution budget |
387 | 0 | // to the max budget specified in preferences. Always doing this |
388 | 0 | // will catch the case of BudgetThrottlingEnabled going from |
389 | 0 | // returning true to returning false. This prevent us from looping |
390 | 0 | // in RunTimeout, due to totalTimeLimit being set to zero and no |
391 | 0 | // timeouts being executed, even though budget throttling isn't |
392 | 0 | // active at the moment. |
393 | 0 | mExecutionBudget = GetMaxBudget(isBackground); |
394 | 0 | } |
395 | 0 |
|
396 | 0 | mLastBudgetUpdate = aNow; |
397 | 0 | } |
398 | | |
399 | 0 | #define TRACKING_SEPARATE_TIMEOUT_BUCKETING_STRATEGY 0 // Consider all timeouts coming from tracking scripts as tracking |
400 | | // These strategies are useful for testing. |
401 | 0 | #define ALL_NORMAL_TIMEOUT_BUCKETING_STRATEGY 1 // Consider all timeouts as normal |
402 | 0 | #define ALTERNATE_TIMEOUT_BUCKETING_STRATEGY 2 // Put every other timeout in the list of tracking timeouts |
403 | 0 | #define RANDOM_TIMEOUT_BUCKETING_STRATEGY 3 // Put timeouts into either the normal or tracking timeouts list randomly |
404 | | static int32_t gTimeoutBucketingStrategy = 0; |
405 | | |
406 | 0 | #define DEFAULT_TIMEOUT_THROTTLING_DELAY -1 // Only positive integers cause us to introduce a delay for |
407 | | // timeout throttling. |
408 | | |
409 | | // The longest interval (as PRIntervalTime) we permit, or that our |
410 | | // timer code can handle, really. See DELAY_INTERVAL_LIMIT in |
411 | | // nsTimerImpl.h for details. |
412 | 0 | #define DOM_MAX_TIMEOUT_VALUE DELAY_INTERVAL_LIMIT |
413 | | |
414 | | uint32_t TimeoutManager::sNestingLevel = 0; |
415 | | |
416 | | namespace { |
417 | | |
418 | | // The maximum number of milliseconds to allow consecutive timer callbacks |
419 | | // to run in a single event loop runnable. |
420 | 0 | #define DEFAULT_MAX_CONSECUTIVE_CALLBACKS_MILLISECONDS 4 |
421 | | uint32_t gMaxConsecutiveCallbacksMilliseconds; |
422 | | |
423 | | // Only propagate the open window click permission if the setTimeout() is equal |
424 | | // to or less than this value. |
425 | 0 | #define DEFAULT_DISABLE_OPEN_CLICK_DELAY 0 |
426 | | int32_t gDisableOpenClickDelay; |
427 | | |
428 | | } // anonymous namespace |
429 | | |
430 | | TimeoutManager::TimeoutManager(nsGlobalWindowInner& aWindow) |
431 | | : mWindow(aWindow), |
432 | | mExecutor(new TimeoutExecutor(this)), |
433 | | mNormalTimeouts(*this), |
434 | | mTrackingTimeouts(*this), |
435 | | mTimeoutIdCounter(1), |
436 | | mNextFiringId(InvalidFiringId + 1), |
437 | | mRunningTimeout(nullptr), |
438 | | mIdleCallbackTimeoutCounter(1), |
439 | | mLastBudgetUpdate(TimeStamp::Now()), |
440 | | mExecutionBudget(GetMaxBudget(mWindow.IsBackgroundInternal())), |
441 | | mThrottleTimeouts(false), |
442 | | mThrottleTrackingTimeouts(false), |
443 | | mBudgetThrottleTimeouts(false) |
444 | 0 | { |
445 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
446 | 0 | ("TimeoutManager %p created, tracking bucketing %s\n", |
447 | 0 | this, gAnnotateTrackingChannels ? "enabled" : "disabled")); |
448 | 0 | } |
449 | | |
450 | | TimeoutManager::~TimeoutManager() |
451 | 0 | { |
452 | 0 | MOZ_DIAGNOSTIC_ASSERT(mWindow.IsDying()); |
453 | 0 | MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeoutsTimer); |
454 | 0 |
|
455 | 0 | mExecutor->Shutdown(); |
456 | 0 |
|
457 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
458 | 0 | ("TimeoutManager %p destroyed\n", this)); |
459 | 0 | } |
460 | | |
461 | | /* static */ |
462 | | void |
463 | | TimeoutManager::Initialize() |
464 | 0 | { |
465 | 0 | Preferences::AddIntVarCache(&gMinClampTimeoutValue, |
466 | 0 | "dom.min_timeout_value", |
467 | 0 | DEFAULT_MIN_CLAMP_TIMEOUT_VALUE); |
468 | 0 | Preferences::AddIntVarCache(&gMinBackgroundTimeoutValue, |
469 | 0 | "dom.min_background_timeout_value", |
470 | 0 | DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE); |
471 | 0 | Preferences::AddIntVarCache(&gMinTrackingTimeoutValue, |
472 | 0 | "dom.min_tracking_timeout_value", |
473 | 0 | DEFAULT_MIN_TRACKING_TIMEOUT_VALUE); |
474 | 0 | Preferences::AddIntVarCache(&gMinTrackingBackgroundTimeoutValue, |
475 | 0 | "dom.min_tracking_background_timeout_value", |
476 | 0 | DEFAULT_MIN_TRACKING_BACKGROUND_TIMEOUT_VALUE); |
477 | 0 | Preferences::AddIntVarCache(&gTimeoutBucketingStrategy, |
478 | 0 | "dom.timeout_bucketing_strategy", |
479 | 0 | TRACKING_SEPARATE_TIMEOUT_BUCKETING_STRATEGY); |
480 | 0 | Preferences::AddIntVarCache(&gTimeoutThrottlingDelay, |
481 | 0 | "dom.timeout.throttling_delay", |
482 | 0 | DEFAULT_TIMEOUT_THROTTLING_DELAY); |
483 | 0 |
|
484 | 0 | Preferences::AddBoolVarCache(&gAnnotateTrackingChannels, |
485 | 0 | "privacy.trackingprotection.annotate_channels", |
486 | 0 | false); |
487 | 0 |
|
488 | 0 | Preferences::AddUintVarCache(&gMaxConsecutiveCallbacksMilliseconds, |
489 | 0 | "dom.timeout.max_consecutive_callbacks_ms", |
490 | 0 | DEFAULT_MAX_CONSECUTIVE_CALLBACKS_MILLISECONDS); |
491 | 0 |
|
492 | 0 | Preferences::AddIntVarCache(&gDisableOpenClickDelay, |
493 | 0 | "dom.disable_open_click_delay", |
494 | 0 | DEFAULT_DISABLE_OPEN_CLICK_DELAY); |
495 | 0 | Preferences::AddIntVarCache(&gBackgroundBudgetRegenerationFactor, |
496 | 0 | "dom.timeout.background_budget_regeneration_rate", |
497 | 0 | DEFAULT_BACKGROUND_BUDGET_REGENERATION_FACTOR); |
498 | 0 | Preferences::AddIntVarCache(&gForegroundBudgetRegenerationFactor, |
499 | 0 | "dom.timeout.foreground_budget_regeneration_rate", |
500 | 0 | DEFAULT_FOREGROUND_BUDGET_REGENERATION_FACTOR); |
501 | 0 | Preferences::AddIntVarCache(&gBackgroundThrottlingMaxBudget, |
502 | 0 | "dom.timeout.background_throttling_max_budget", |
503 | 0 | DEFAULT_BACKGROUND_THROTTLING_MAX_BUDGET); |
504 | 0 | Preferences::AddIntVarCache(&gForegroundThrottlingMaxBudget, |
505 | 0 | "dom.timeout.foreground_throttling_max_budget", |
506 | 0 | DEFAULT_FOREGROUND_THROTTLING_MAX_BUDGET); |
507 | 0 | Preferences::AddIntVarCache(&gBudgetThrottlingMaxDelay, |
508 | 0 | "dom.timeout.budget_throttling_max_delay", |
509 | 0 | DEFAULT_BUDGET_THROTTLING_MAX_DELAY); |
510 | 0 | Preferences::AddBoolVarCache(&gEnableBudgetTimeoutThrottling, |
511 | 0 | "dom.timeout.enable_budget_timer_throttling", |
512 | 0 | DEFAULT_ENABLE_BUDGET_TIMEOUT_THROTTLING); |
513 | 0 | } |
514 | | |
515 | | uint32_t |
516 | | TimeoutManager::GetTimeoutId(Timeout::Reason aReason) |
517 | | { |
518 | | switch (aReason) { |
519 | | case Timeout::Reason::eIdleCallbackTimeout: |
520 | | return ++mIdleCallbackTimeoutCounter; |
521 | | case Timeout::Reason::eTimeoutOrInterval: |
522 | | default: |
523 | | return ++mTimeoutIdCounter; |
524 | | } |
525 | | } |
526 | | |
527 | | bool |
528 | | TimeoutManager::IsRunningTimeout() const |
529 | 0 | { |
530 | 0 | return mRunningTimeout; |
531 | 0 | } |
532 | | |
533 | | nsresult |
534 | | TimeoutManager::SetTimeout(nsITimeoutHandler* aHandler, |
535 | | int32_t interval, bool aIsInterval, |
536 | | Timeout::Reason aReason, int32_t* aReturn) |
537 | 0 | { |
538 | 0 | // If we don't have a document (we could have been unloaded since |
539 | 0 | // the call to setTimeout was made), do nothing. |
540 | 0 | nsCOMPtr<nsIDocument> doc = mWindow.GetExtantDoc(); |
541 | 0 | if (!doc) { |
542 | 0 | return NS_OK; |
543 | 0 | } |
544 | 0 | |
545 | 0 | // Disallow negative intervals. |
546 | 0 | interval = std::max(0, interval); |
547 | 0 |
|
548 | 0 | // Make sure we don't proceed with an interval larger than our timer |
549 | 0 | // code can handle. (Note: we already forced |interval| to be non-negative, |
550 | 0 | // so the uint32_t cast (to avoid compiler warnings) is ok.) |
551 | 0 | uint32_t maxTimeoutMs = PR_IntervalToMilliseconds(DOM_MAX_TIMEOUT_VALUE); |
552 | 0 | if (static_cast<uint32_t>(interval) > maxTimeoutMs) { |
553 | 0 | interval = maxTimeoutMs; |
554 | 0 | } |
555 | 0 |
|
556 | 0 | RefPtr<Timeout> timeout = new Timeout(); |
557 | 0 | timeout->mWindow = &mWindow; |
558 | 0 | timeout->mIsInterval = aIsInterval; |
559 | 0 | timeout->mInterval = TimeDuration::FromMilliseconds(interval); |
560 | 0 | timeout->mScriptHandler = aHandler; |
561 | 0 | timeout->mReason = aReason; |
562 | 0 |
|
563 | 0 | // No popups from timeouts by default |
564 | 0 | timeout->mPopupState = openAbused; |
565 | 0 |
|
566 | 0 | switch (gTimeoutBucketingStrategy) { |
567 | 0 | default: |
568 | 0 | case TRACKING_SEPARATE_TIMEOUT_BUCKETING_STRATEGY: { |
569 | 0 | const char* filename = nullptr; |
570 | 0 | uint32_t dummyLine = 0, dummyColumn = 0; |
571 | 0 | aHandler->GetLocation(&filename, &dummyLine, &dummyColumn); |
572 | 0 | timeout->mIsTracking = doc->IsScriptTracking(nsDependentCString(filename)); |
573 | 0 |
|
574 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
575 | 0 | ("Classified timeout %p set from %s as %stracking\n", |
576 | 0 | timeout.get(), filename, timeout->mIsTracking ? "" : "non-")); |
577 | 0 | break; |
578 | 0 | } |
579 | 0 | case ALL_NORMAL_TIMEOUT_BUCKETING_STRATEGY: |
580 | 0 | // timeout->mIsTracking is already false! |
581 | 0 | MOZ_DIAGNOSTIC_ASSERT(!timeout->mIsTracking); |
582 | 0 |
|
583 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
584 | 0 | ("Classified timeout %p unconditionally as normal\n", |
585 | 0 | timeout.get())); |
586 | 0 | break; |
587 | 0 | case ALTERNATE_TIMEOUT_BUCKETING_STRATEGY: |
588 | 0 | timeout->mIsTracking = (mTimeoutIdCounter % 2) == 0; |
589 | 0 |
|
590 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
591 | 0 | ("Classified timeout %p as %stracking (alternating mode)\n", |
592 | 0 | timeout.get(), timeout->mIsTracking ? "" : "non-")); |
593 | 0 | break; |
594 | 0 | case RANDOM_TIMEOUT_BUCKETING_STRATEGY: |
595 | 0 | timeout->mIsTracking = (rand() % 2) == 0; |
596 | 0 |
|
597 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
598 | 0 | ("Classified timeout %p as %stracking (random mode)\n", |
599 | 0 | timeout.get(), timeout->mIsTracking ? "" : "non-")); |
600 | 0 | break; |
601 | 0 | } |
602 | 0 |
|
603 | 0 | timeout->mNestingLevel = sNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL |
604 | 0 | ? sNestingLevel + 1 : sNestingLevel; |
605 | 0 |
|
606 | 0 | // Now clamp the actual interval we will use for the timer based on |
607 | 0 | TimeDuration realInterval = CalculateDelay(timeout); |
608 | 0 | TimeStamp now = TimeStamp::Now(); |
609 | 0 | timeout->SetWhenOrTimeRemaining(now, realInterval); |
610 | 0 |
|
611 | 0 | // If we're not suspended, then set the timer. |
612 | 0 | if (!mWindow.IsSuspended()) { |
613 | 0 | nsresult rv = MaybeSchedule(timeout->When(), now); |
614 | 0 | if (NS_FAILED(rv)) { |
615 | 0 | return rv; |
616 | 0 | } |
617 | 0 | } |
618 | 0 | |
619 | 0 | if (gRunningTimeoutDepth == 0 && |
620 | 0 | nsContentUtils::GetPopupControlState() < openBlocked) { |
621 | 0 | // This timeout is *not* set from another timeout and it's set |
622 | 0 | // while popups are enabled. Propagate the state to the timeout if |
623 | 0 | // its delay (interval) is equal to or less than what |
624 | 0 | // "dom.disable_open_click_delay" is set to (in ms). |
625 | 0 |
|
626 | 0 | // This is checking |interval|, not realInterval, on purpose, |
627 | 0 | // because our lower bound for |realInterval| could be pretty high |
628 | 0 | // in some cases. |
629 | 0 | if (interval <= gDisableOpenClickDelay) { |
630 | 0 | timeout->mPopupState = nsContentUtils::GetPopupControlState(); |
631 | 0 | } |
632 | 0 | } |
633 | 0 |
|
634 | 0 | Timeouts::SortBy sort(mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining |
635 | 0 | : Timeouts::SortBy::TimeWhen); |
636 | 0 | if (timeout->mIsTracking) { |
637 | 0 | mTrackingTimeouts.Insert(timeout, sort); |
638 | 0 | } else { |
639 | 0 | mNormalTimeouts.Insert(timeout, sort); |
640 | 0 | } |
641 | 0 |
|
642 | 0 | timeout->mTimeoutId = GetTimeoutId(aReason); |
643 | 0 | *aReturn = timeout->mTimeoutId; |
644 | 0 |
|
645 | 0 | MOZ_LOG(gLog, |
646 | 0 | LogLevel::Debug, |
647 | 0 | ("Set%s(TimeoutManager=%p, timeout=%p, delay=%i, " |
648 | 0 | "minimum=%f, throttling=%s, state=%s(%s), realInterval=%f) " |
649 | 0 | "returned %stracking timeout ID %u, budget=%d\n", |
650 | 0 | aIsInterval ? "Interval" : "Timeout", |
651 | 0 | this, timeout.get(), interval, |
652 | 0 | (CalculateDelay(timeout) - timeout->mInterval).ToMilliseconds(), |
653 | 0 | mThrottleTimeouts |
654 | 0 | ? "yes" |
655 | 0 | : (mThrottleTimeoutsTimer ? "pending" : "no"), |
656 | 0 | IsActive() ? "active" : "inactive", |
657 | 0 | mWindow.IsBackgroundInternal() ? "background" : "foreground", |
658 | 0 | realInterval.ToMilliseconds(), |
659 | 0 | timeout->mIsTracking ? "" : "non-", |
660 | 0 | timeout->mTimeoutId, |
661 | 0 | int(mExecutionBudget.ToMilliseconds()))); |
662 | 0 |
|
663 | 0 | return NS_OK; |
664 | 0 | } |
665 | | |
666 | | void |
667 | | TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason) |
668 | 0 | { |
669 | 0 | uint32_t timerId = (uint32_t)aTimerId; |
670 | 0 |
|
671 | 0 | bool firstTimeout = true; |
672 | 0 | bool deferredDeletion = false; |
673 | 0 |
|
674 | 0 | ForEachUnorderedTimeoutAbortable([&](Timeout* aTimeout) { |
675 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
676 | 0 | ("Clear%s(TimeoutManager=%p, timeout=%p, aTimerId=%u, ID=%u, tracking=%d)\n", aTimeout->mIsInterval ? "Interval" : "Timeout", |
677 | 0 | this, aTimeout, timerId, aTimeout->mTimeoutId, |
678 | 0 | int(aTimeout->mIsTracking))); |
679 | 0 |
|
680 | 0 | if (aTimeout->mTimeoutId == timerId && aTimeout->mReason == aReason) { |
681 | 0 | if (aTimeout->mRunning) { |
682 | 0 | /* We're running from inside the aTimeout. Mark this |
683 | 0 | aTimeout for deferred deletion by the code in |
684 | 0 | RunTimeout() */ |
685 | 0 | aTimeout->mIsInterval = false; |
686 | 0 | deferredDeletion = true; |
687 | 0 | } |
688 | 0 | else { |
689 | 0 | /* Delete the aTimeout from the pending aTimeout list */ |
690 | 0 | aTimeout->remove(); |
691 | 0 | } |
692 | 0 | return true; // abort! |
693 | 0 | } |
694 | 0 |
|
695 | 0 | firstTimeout = false; |
696 | 0 |
|
697 | 0 | return false; |
698 | 0 | }); |
699 | 0 |
|
700 | 0 | // We don't need to reschedule the executor if any of the following are true: |
701 | 0 | // * If the we weren't cancelling the first timeout, then the executor's |
702 | 0 | // state doesn't need to change. It will only reflect the next soonest |
703 | 0 | // Timeout. |
704 | 0 | // * If we did cancel the first Timeout, but its currently running, then |
705 | 0 | // RunTimeout() will handle rescheduling the executor. |
706 | 0 | // * If the window has become suspended then we should not start executing |
707 | 0 | // Timeouts. |
708 | 0 | if (!firstTimeout || deferredDeletion || mWindow.IsSuspended()) { |
709 | 0 | return; |
710 | 0 | } |
711 | 0 | |
712 | 0 | // Stop the executor and restart it at the next soonest deadline. |
713 | 0 | mExecutor->Cancel(); |
714 | 0 |
|
715 | 0 | OrderedTimeoutIterator iter(mNormalTimeouts, mTrackingTimeouts); |
716 | 0 | Timeout* nextTimeout = iter.Next(); |
717 | 0 | if (nextTimeout) { |
718 | 0 | MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When())); |
719 | 0 | } |
720 | 0 | } |
721 | | |
722 | | void |
723 | | TimeoutManager::RunTimeout(const TimeStamp& aNow, const TimeStamp& aTargetDeadline) |
724 | 0 | { |
725 | 0 | MOZ_DIAGNOSTIC_ASSERT(!aNow.IsNull()); |
726 | 0 | MOZ_DIAGNOSTIC_ASSERT(!aTargetDeadline.IsNull()); |
727 | 0 |
|
728 | 0 | MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended()); |
729 | 0 | if (mWindow.IsSuspended()) { |
730 | 0 | return; |
731 | 0 | } |
732 | 0 | |
733 | 0 | // Limit the overall time spent in RunTimeout() to reduce jank. |
734 | 0 | uint32_t totalTimeLimitMS = std::max(1u, gMaxConsecutiveCallbacksMilliseconds); |
735 | 0 | const TimeDuration totalTimeLimit = |
736 | 0 | TimeDuration::Min(TimeDuration::FromMilliseconds(totalTimeLimitMS), |
737 | 0 | TimeDuration::Max(TimeDuration(), mExecutionBudget)); |
738 | 0 |
|
739 | 0 | // Allow up to 25% of our total time budget to be used figuring out which |
740 | 0 | // timers need to run. This is the initial loop in this method. |
741 | 0 | const TimeDuration initialTimeLimit = |
742 | 0 | TimeDuration::FromMilliseconds(totalTimeLimit.ToMilliseconds() / 4); |
743 | 0 |
|
744 | 0 | // Ammortize overhead from from calling TimeStamp::Now() in the initial |
745 | 0 | // loop, though, by only checking for an elapsed limit every N timeouts. |
746 | 0 | const uint32_t kNumTimersPerInitialElapsedCheck = 100; |
747 | 0 |
|
748 | 0 | // Start measuring elapsed time immediately. We won't potentially expire |
749 | 0 | // the time budget until at least one Timeout has run, though. |
750 | 0 | TimeStamp now(aNow); |
751 | 0 | TimeStamp start = now; |
752 | 0 |
|
753 | 0 | uint32_t firingId = CreateFiringId(); |
754 | 0 | auto guard = MakeScopeExit([&] { |
755 | 0 | DestroyFiringId(firingId); |
756 | 0 | }); |
757 | 0 |
|
758 | 0 | // Make sure that the window and the script context don't go away as |
759 | 0 | // a result of running timeouts |
760 | 0 | nsCOMPtr<nsIScriptGlobalObject> windowKungFuDeathGrip(&mWindow); |
761 | 0 | // Silence the static analysis error about windowKungFuDeathGrip. Accessing |
762 | 0 | // members of mWindow here is safe, because the lifetime of TimeoutManager is |
763 | 0 | // the same as the lifetime of the containing nsGlobalWindow. |
764 | 0 | Unused << windowKungFuDeathGrip; |
765 | 0 |
|
766 | 0 | // A native timer has gone off. See which of our timeouts need |
767 | 0 | // servicing |
768 | 0 | TimeStamp deadline; |
769 | 0 |
|
770 | 0 | if (aTargetDeadline > now) { |
771 | 0 | // The OS timer fired early (which can happen due to the timers |
772 | 0 | // having lower precision than TimeStamp does). Set |deadline| to |
773 | 0 | // be the time when the OS timer *should* have fired so that any |
774 | 0 | // timers that *should* have fired *will* be fired now. |
775 | 0 |
|
776 | 0 | deadline = aTargetDeadline; |
777 | 0 | } else { |
778 | 0 | deadline = now; |
779 | 0 | } |
780 | 0 |
|
781 | 0 | TimeStamp nextDeadline; |
782 | 0 | uint32_t numTimersToRun = 0; |
783 | 0 |
|
784 | 0 | // The timeout list is kept in deadline order. Discover the latest timeout |
785 | 0 | // whose deadline has expired. On some platforms, native timeout events fire |
786 | 0 | // "early", but we handled that above by setting deadline to aTargetDeadline |
787 | 0 | // if the timer fired early. So we can stop walking if we get to timeouts |
788 | 0 | // whose When() is greater than deadline, since once that happens we know |
789 | 0 | // nothing past that point is expired. |
790 | 0 | { |
791 | 0 | // Use a nested scope in order to make sure the strong references held by |
792 | 0 | // the iterator are freed after the loop. |
793 | 0 | OrderedTimeoutIterator expiredIter(mNormalTimeouts, mTrackingTimeouts); |
794 | 0 |
|
795 | 0 | while (true) { |
796 | 0 | Timeout* timeout = expiredIter.Next(); |
797 | 0 | if (!timeout || totalTimeLimit.IsZero() || timeout->When() > deadline) { |
798 | 0 | if (timeout) { |
799 | 0 | nextDeadline = timeout->When(); |
800 | 0 | } |
801 | 0 | break; |
802 | 0 | } |
803 | 0 |
|
804 | 0 | if (IsInvalidFiringId(timeout->mFiringId)) { |
805 | 0 | // Mark any timeouts that are on the list to be fired with the |
806 | 0 | // firing depth so that we can reentrantly run timeouts |
807 | 0 | timeout->mFiringId = firingId; |
808 | 0 |
|
809 | 0 | numTimersToRun += 1; |
810 | 0 |
|
811 | 0 | // Run only a limited number of timers based on the configured maximum. |
812 | 0 | if (numTimersToRun % kNumTimersPerInitialElapsedCheck == 0) { |
813 | 0 | now = TimeStamp::Now(); |
814 | 0 | TimeDuration elapsed(now - start); |
815 | 0 | if (elapsed >= initialTimeLimit) { |
816 | 0 | nextDeadline = timeout->When(); |
817 | 0 | break; |
818 | 0 | } |
819 | 0 | } |
820 | 0 | } |
821 | 0 | |
822 | 0 | expiredIter.UpdateIterator(); |
823 | 0 | } |
824 | 0 | } |
825 | 0 |
|
826 | 0 | now = TimeStamp::Now(); |
827 | 0 |
|
828 | 0 | // Wherever we stopped in the timer list, schedule the executor to |
829 | 0 | // run for the next unexpired deadline. Note, this *must* be done |
830 | 0 | // before we start executing any content script handlers. If one |
831 | 0 | // of them spins the event loop the executor must already be scheduled |
832 | 0 | // in order for timeouts to fire properly. |
833 | 0 | if (!nextDeadline.IsNull()) { |
834 | 0 | // Note, we verified the window is not suspended at the top of |
835 | 0 | // method and the window should not have been suspended while |
836 | 0 | // executing the loop above since it doesn't call out to js. |
837 | 0 | MOZ_DIAGNOSTIC_ASSERT(!mWindow.IsSuspended()); |
838 | 0 | MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextDeadline, now)); |
839 | 0 | } |
840 | 0 |
|
841 | 0 | // Maybe the timeout that the event was fired for has been deleted |
842 | 0 | // and there are no others timeouts with deadlines that make them |
843 | 0 | // eligible for execution yet. Go away. |
844 | 0 | if (!numTimersToRun) { |
845 | 0 | return; |
846 | 0 | } |
847 | 0 | |
848 | 0 | // Now we need to search the normal and tracking timer list at the same |
849 | 0 | // time to run the timers in the scheduled order. |
850 | 0 | |
851 | 0 | // We stop iterating each list when we go past the last expired timeout from |
852 | 0 | // that list that we have observed above. That timeout will either be the |
853 | 0 | // next item after the last timeout we looked at or nullptr if we have |
854 | 0 | // exhausted the entire list while looking for the last expired timeout. |
855 | 0 | { |
856 | 0 | // Use a nested scope in order to make sure the strong references held by |
857 | 0 | // the iterator are freed after the loop. |
858 | 0 | OrderedTimeoutIterator runIter(mNormalTimeouts, mTrackingTimeouts); |
859 | 0 | while (true) { |
860 | 0 | RefPtr<Timeout> timeout = runIter.Next(); |
861 | 0 | if (!timeout) { |
862 | 0 | // We have run out of timeouts! |
863 | 0 | break; |
864 | 0 | } |
865 | 0 | runIter.UpdateIterator(); |
866 | 0 |
|
867 | 0 | // We should only execute callbacks for the set of expired Timeout |
868 | 0 | // objects we computed above. |
869 | 0 | if (timeout->mFiringId != firingId) { |
870 | 0 | // If the FiringId does not match, but is still valid, then this is |
871 | 0 | // a TImeout for another RunTimeout() on the call stack. Just |
872 | 0 | // skip it. |
873 | 0 | if (IsValidFiringId(timeout->mFiringId)) { |
874 | 0 | continue; |
875 | 0 | } |
876 | 0 | |
877 | 0 | // If, however, the FiringId is invalid then we have reached Timeout |
878 | 0 | // objects beyond the list we calculated above. This can happen |
879 | 0 | // if the Timeout just beyond our last expired Timeout is cancelled |
880 | 0 | // by one of the callbacks we've just executed. In this case we |
881 | 0 | // should just stop iterating. We're done. |
882 | 0 | else { |
883 | 0 | break; |
884 | 0 | } |
885 | 0 | } |
886 | 0 | |
887 | 0 | MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended()); |
888 | 0 | if (mWindow.IsSuspended()) { |
889 | 0 | break; |
890 | 0 | } |
891 | 0 | |
892 | 0 | // The timeout is on the list to run at this depth, go ahead and |
893 | 0 | // process it. |
894 | 0 | |
895 | 0 | // Get the script context (a strong ref to prevent it going away) |
896 | 0 | // for this timeout and ensure the script language is enabled. |
897 | 0 | nsCOMPtr<nsIScriptContext> scx = mWindow.GetContextInternal(); |
898 | 0 |
|
899 | 0 | if (!scx) { |
900 | 0 | // No context means this window was closed or never properly |
901 | 0 | // initialized for this language. This timer will never fire |
902 | 0 | // so just remove it. |
903 | 0 | timeout->remove(); |
904 | 0 | continue; |
905 | 0 | } |
906 | 0 | |
907 | 0 | // This timeout is good to run |
908 | 0 | bool timeout_was_cleared = mWindow.RunTimeoutHandler(timeout, scx); |
909 | 0 |
|
910 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
911 | 0 | ("Run%s(TimeoutManager=%p, timeout=%p, tracking=%d) returned %d\n", timeout->mIsInterval ? "Interval" : "Timeout", |
912 | 0 | this, timeout.get(), |
913 | 0 | int(timeout->mIsTracking), |
914 | 0 | !!timeout_was_cleared)); |
915 | 0 |
|
916 | 0 | if (timeout_was_cleared) { |
917 | 0 | // Make sure the iterator isn't holding any Timeout objects alive. |
918 | 0 | runIter.Clear(); |
919 | 0 |
|
920 | 0 | // Since ClearAllTimeouts() was called the lists should be empty. |
921 | 0 | MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts()); |
922 | 0 |
|
923 | 0 | return; |
924 | 0 | } |
925 | 0 | |
926 | 0 | // If we need to reschedule a setInterval() the delay should be |
927 | 0 | // calculated based on when its callback started to execute. So |
928 | 0 | // save off the last time before updating our "now" timestamp to |
929 | 0 | // account for its callback execution time. |
930 | 0 | TimeStamp lastCallbackTime = now; |
931 | 0 | now = TimeStamp::Now(); |
932 | 0 |
|
933 | 0 | // If we have a regular interval timer, we re-schedule the |
934 | 0 | // timeout, accounting for clock drift. |
935 | 0 | bool needsReinsertion = RescheduleTimeout(timeout, lastCallbackTime, now); |
936 | 0 |
|
937 | 0 | // Running a timeout can cause another timeout to be deleted, so |
938 | 0 | // we need to reset the pointer to the following timeout. |
939 | 0 | runIter.UpdateIterator(); |
940 | 0 |
|
941 | 0 | timeout->remove(); |
942 | 0 |
|
943 | 0 | if (needsReinsertion) { |
944 | 0 | // Insert interval timeout onto the corresponding list sorted in |
945 | 0 | // deadline order. AddRefs timeout. |
946 | 0 | if (runIter.PickedTrackingIter()) { |
947 | 0 | mTrackingTimeouts.Insert(timeout, |
948 | 0 | mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining |
949 | 0 | : Timeouts::SortBy::TimeWhen); |
950 | 0 | } else { |
951 | 0 | mNormalTimeouts.Insert(timeout, |
952 | 0 | mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining |
953 | 0 | : Timeouts::SortBy::TimeWhen); |
954 | 0 | } |
955 | 0 | } |
956 | 0 |
|
957 | 0 | // Check to see if we have run out of time to execute timeout handlers. |
958 | 0 | // If we've exceeded our time budget then terminate the loop immediately. |
959 | 0 | TimeDuration elapsed = now - start; |
960 | 0 | if (elapsed >= totalTimeLimit) { |
961 | 0 | // We ran out of time. Make sure to schedule the executor to |
962 | 0 | // run immediately for the next timer, if it exists. Its possible, |
963 | 0 | // however, that the last timeout handler suspended the window. If |
964 | 0 | // that happened then we must skip this step. |
965 | 0 | if (!mWindow.IsSuspended()) { |
966 | 0 | RefPtr<Timeout> timeout = runIter.Next(); |
967 | 0 | if (timeout) { |
968 | 0 | // If we ran out of execution budget we need to force a |
969 | 0 | // reschedule. By cancelling the executor we will not run |
970 | 0 | // immediately, but instead reschedule to the minimum |
971 | 0 | // scheduling delay. |
972 | 0 | if (mExecutionBudget < TimeDuration()) { |
973 | 0 | mExecutor->Cancel(); |
974 | 0 | } |
975 | 0 |
|
976 | 0 | MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(timeout->When(), now)); |
977 | 0 | } |
978 | 0 | } |
979 | 0 | break; |
980 | 0 | } |
981 | 0 | } |
982 | 0 | } |
983 | 0 | } |
984 | | |
985 | | bool |
986 | | TimeoutManager::RescheduleTimeout(Timeout* aTimeout, |
987 | | const TimeStamp& aLastCallbackTime, |
988 | | const TimeStamp& aCurrentNow) |
989 | 0 | { |
990 | 0 | MOZ_DIAGNOSTIC_ASSERT(aLastCallbackTime <= aCurrentNow); |
991 | 0 |
|
992 | 0 | if (!aTimeout->mIsInterval) { |
993 | 0 | return false; |
994 | 0 | } |
995 | 0 | |
996 | 0 | // Automatically increase the nesting level when a setInterval() |
997 | 0 | // is rescheduled just as if it was using a chained setTimeout(). |
998 | 0 | if (aTimeout->mNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL) { |
999 | 0 | aTimeout->mNestingLevel += 1; |
1000 | 0 | } |
1001 | 0 |
|
1002 | 0 | // Compute time to next timeout for interval timer. |
1003 | 0 | // Make sure nextInterval is at least CalculateDelay(). |
1004 | 0 | TimeDuration nextInterval = CalculateDelay(aTimeout); |
1005 | 0 |
|
1006 | 0 | TimeStamp firingTime = aLastCallbackTime + nextInterval; |
1007 | 0 | TimeDuration delay = firingTime - aCurrentNow; |
1008 | 0 |
|
1009 | 0 | // And make sure delay is nonnegative; that might happen if the timer |
1010 | 0 | // thread is firing our timers somewhat early or if they're taking a long |
1011 | 0 | // time to run the callback. |
1012 | 0 | if (delay < TimeDuration(0)) { |
1013 | 0 | delay = TimeDuration(0); |
1014 | 0 | } |
1015 | 0 |
|
1016 | 0 | aTimeout->SetWhenOrTimeRemaining(aCurrentNow, delay); |
1017 | 0 |
|
1018 | 0 | if (mWindow.IsSuspended()) { |
1019 | 0 | return true; |
1020 | 0 | } |
1021 | 0 | |
1022 | 0 | nsresult rv = MaybeSchedule(aTimeout->When(), aCurrentNow); |
1023 | 0 | NS_ENSURE_SUCCESS(rv, false); |
1024 | 0 |
|
1025 | 0 | return true; |
1026 | 0 | } |
1027 | | |
1028 | | void |
1029 | | TimeoutManager::ClearAllTimeouts() |
1030 | 0 | { |
1031 | 0 | bool seenRunningTimeout = false; |
1032 | 0 |
|
1033 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
1034 | 0 | ("ClearAllTimeouts(TimeoutManager=%p)\n", this)); |
1035 | 0 |
|
1036 | 0 | if (mThrottleTimeoutsTimer) { |
1037 | 0 | mThrottleTimeoutsTimer->Cancel(); |
1038 | 0 | mThrottleTimeoutsTimer = nullptr; |
1039 | 0 | } |
1040 | 0 |
|
1041 | 0 | mExecutor->Cancel(); |
1042 | 0 |
|
1043 | 0 | ForEachUnorderedTimeout([&](Timeout* aTimeout) { |
1044 | 0 | /* If RunTimeout() is higher up on the stack for this |
1045 | 0 | window, e.g. as a result of document.write from a timeout, |
1046 | 0 | then we need to reset the list insertion point for |
1047 | 0 | newly-created timeouts in case the user adds a timeout, |
1048 | 0 | before we pop the stack back to RunTimeout. */ |
1049 | 0 | if (mRunningTimeout == aTimeout) { |
1050 | 0 | seenRunningTimeout = true; |
1051 | 0 | } |
1052 | 0 |
|
1053 | 0 | // Set timeout->mCleared to true to indicate that the timeout was |
1054 | 0 | // cleared and taken out of the list of timeouts |
1055 | 0 | aTimeout->mCleared = true; |
1056 | 0 | }); |
1057 | 0 |
|
1058 | 0 | // Clear out our list |
1059 | 0 | mNormalTimeouts.Clear(); |
1060 | 0 | mTrackingTimeouts.Clear(); |
1061 | 0 | } |
1062 | | |
1063 | | void |
1064 | | TimeoutManager::Timeouts::Insert(Timeout* aTimeout, SortBy aSortBy) |
1065 | 0 | { |
1066 | 0 |
|
1067 | 0 | // Start at mLastTimeout and go backwards. Stop if we see a Timeout with a |
1068 | 0 | // valid FiringId since those timers are currently being processed by |
1069 | 0 | // RunTimeout. This optimizes for the common case of insertion at the end. |
1070 | 0 | Timeout* prevSibling; |
1071 | 0 | for (prevSibling = GetLast(); |
1072 | 0 | prevSibling && |
1073 | 0 | // This condition needs to match the one in SetTimeoutOrInterval that |
1074 | 0 | // determines whether to set When() or TimeRemaining(). |
1075 | 0 | (aSortBy == SortBy::TimeRemaining ? |
1076 | 0 | prevSibling->TimeRemaining() > aTimeout->TimeRemaining() : |
1077 | 0 | prevSibling->When() > aTimeout->When()) && |
1078 | 0 | // Check the firing ID last since it will evaluate true in the vast |
1079 | 0 | // majority of cases. |
1080 | 0 | mManager.IsInvalidFiringId(prevSibling->mFiringId); |
1081 | 0 | prevSibling = prevSibling->getPrevious()) { |
1082 | 0 | /* Do nothing; just searching */ |
1083 | 0 | } |
1084 | 0 |
|
1085 | 0 | // Now link in aTimeout after prevSibling. |
1086 | 0 | if (prevSibling) { |
1087 | 0 | prevSibling->setNext(aTimeout); |
1088 | 0 | } else { |
1089 | 0 | InsertFront(aTimeout); |
1090 | 0 | } |
1091 | 0 |
|
1092 | 0 | aTimeout->mFiringId = InvalidFiringId; |
1093 | 0 | } |
1094 | | |
1095 | | Timeout* |
1096 | | TimeoutManager::BeginRunningTimeout(Timeout* aTimeout) |
1097 | 0 | { |
1098 | 0 | Timeout* currentTimeout = mRunningTimeout; |
1099 | 0 | mRunningTimeout = aTimeout; |
1100 | 0 | ++gRunningTimeoutDepth; |
1101 | 0 |
|
1102 | 0 | RecordExecution(currentTimeout, aTimeout); |
1103 | 0 | return currentTimeout; |
1104 | 0 | } |
1105 | | |
1106 | | void |
1107 | | TimeoutManager::EndRunningTimeout(Timeout* aTimeout) |
1108 | 0 | { |
1109 | 0 | --gRunningTimeoutDepth; |
1110 | 0 |
|
1111 | 0 | RecordExecution(mRunningTimeout, aTimeout); |
1112 | 0 | mRunningTimeout = aTimeout; |
1113 | 0 | } |
1114 | | |
1115 | | void |
1116 | | TimeoutManager::UnmarkGrayTimers() |
1117 | 0 | { |
1118 | 0 | ForEachUnorderedTimeout([](Timeout* aTimeout) { |
1119 | 0 | if (aTimeout->mScriptHandler) { |
1120 | 0 | aTimeout->mScriptHandler->MarkForCC(); |
1121 | 0 | } |
1122 | 0 | }); |
1123 | 0 | } |
1124 | | |
1125 | | void |
1126 | | TimeoutManager::Suspend() |
1127 | 0 | { |
1128 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
1129 | 0 | ("Suspend(TimeoutManager=%p)\n", this)); |
1130 | 0 |
|
1131 | 0 | if (mThrottleTimeoutsTimer) { |
1132 | 0 | mThrottleTimeoutsTimer->Cancel(); |
1133 | 0 | mThrottleTimeoutsTimer = nullptr; |
1134 | 0 | } |
1135 | 0 |
|
1136 | 0 | mExecutor->Cancel(); |
1137 | 0 | } |
1138 | | |
1139 | | void |
1140 | | TimeoutManager::Resume() |
1141 | 0 | { |
1142 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
1143 | 0 | ("Resume(TimeoutManager=%p)\n", this)); |
1144 | 0 |
|
1145 | 0 | // When Suspend() has been called after IsDocumentLoaded(), but the |
1146 | 0 | // throttle tracking timer never managed to fire, start the timer |
1147 | 0 | // again. |
1148 | 0 | if (mWindow.AsInner()->IsDocumentLoaded() && !mThrottleTimeouts) { |
1149 | 0 | MaybeStartThrottleTimeout(); |
1150 | 0 | } |
1151 | 0 |
|
1152 | 0 | OrderedTimeoutIterator iter(mNormalTimeouts, mTrackingTimeouts); |
1153 | 0 | Timeout* nextTimeout = iter.Next(); |
1154 | 0 | if (nextTimeout) { |
1155 | 0 | MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When())); |
1156 | 0 | } |
1157 | 0 | } |
1158 | | |
1159 | | void |
1160 | | TimeoutManager::Freeze() |
1161 | 0 | { |
1162 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
1163 | 0 | ("Freeze(TimeoutManager=%p)\n", this)); |
1164 | 0 |
|
1165 | 0 | TimeStamp now = TimeStamp::Now(); |
1166 | 0 | ForEachUnorderedTimeout([&](Timeout* aTimeout) { |
1167 | 0 | // Save the current remaining time for this timeout. We will |
1168 | 0 | // re-apply it when the window is Thaw()'d. This effectively |
1169 | 0 | // shifts timers to the right as if time does not pass while |
1170 | 0 | // the window is frozen. |
1171 | 0 | TimeDuration delta(0); |
1172 | 0 | if (aTimeout->When() > now) { |
1173 | 0 | delta = aTimeout->When() - now; |
1174 | 0 | } |
1175 | 0 | aTimeout->SetWhenOrTimeRemaining(now, delta); |
1176 | 0 | MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta); |
1177 | 0 | }); |
1178 | 0 | } |
1179 | | |
1180 | | void |
1181 | | TimeoutManager::Thaw() |
1182 | 0 | { |
1183 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
1184 | 0 | ("Thaw(TimeoutManager=%p)\n", this)); |
1185 | 0 |
|
1186 | 0 | TimeStamp now = TimeStamp::Now(); |
1187 | 0 |
|
1188 | 0 | ForEachUnorderedTimeout([&](Timeout* aTimeout) { |
1189 | 0 | // Set When() back to the time when the timer is supposed to fire. |
1190 | 0 | aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining()); |
1191 | 0 | MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull()); |
1192 | 0 | }); |
1193 | 0 | } |
1194 | | |
1195 | | void |
1196 | | TimeoutManager::UpdateBackgroundState() |
1197 | 0 | { |
1198 | 0 | mExecutionBudget = GetMaxBudget(mWindow.IsBackgroundInternal()); |
1199 | 0 |
|
1200 | 0 | // When the window moves to the background or foreground we should |
1201 | 0 | // reschedule the TimeoutExecutor in case the MinSchedulingDelay() |
1202 | 0 | // changed. Only do this if the window is not suspended and we |
1203 | 0 | // actually have a timeout. |
1204 | 0 | if (!mWindow.IsSuspended()) { |
1205 | 0 | OrderedTimeoutIterator iter(mNormalTimeouts, mTrackingTimeouts); |
1206 | 0 | Timeout* nextTimeout = iter.Next(); |
1207 | 0 | if (nextTimeout) { |
1208 | 0 | mExecutor->Cancel(); |
1209 | 0 | MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When())); |
1210 | 0 | } |
1211 | 0 | } |
1212 | 0 | } |
1213 | | |
1214 | | bool |
1215 | | TimeoutManager::IsTimeoutTracking(uint32_t aTimeoutId) |
1216 | 0 | { |
1217 | 0 | return mTrackingTimeouts.ForEachAbortable([&](Timeout* aTimeout) { |
1218 | 0 | return aTimeout->mTimeoutId == aTimeoutId; |
1219 | 0 | }); |
1220 | 0 | } |
1221 | | |
1222 | | namespace { |
1223 | | |
1224 | | class ThrottleTimeoutsCallback final : public nsITimerCallback |
1225 | | , public nsINamed |
1226 | | { |
1227 | | public: |
1228 | | explicit ThrottleTimeoutsCallback(nsGlobalWindowInner* aWindow) |
1229 | | : mWindow(aWindow) |
1230 | 0 | { |
1231 | 0 | } |
1232 | | |
1233 | | NS_DECL_ISUPPORTS |
1234 | | NS_DECL_NSITIMERCALLBACK |
1235 | | |
1236 | | NS_IMETHOD GetName(nsACString& aName) override |
1237 | 0 | { |
1238 | 0 | aName.AssignLiteral("ThrottleTimeoutsCallback"); |
1239 | 0 | return NS_OK; |
1240 | 0 | } |
1241 | | |
1242 | | private: |
1243 | 0 | ~ThrottleTimeoutsCallback() {} |
1244 | | |
1245 | | private: |
1246 | | // The strong reference here keeps the Window and hence the TimeoutManager |
1247 | | // object itself alive. |
1248 | | RefPtr<nsGlobalWindowInner> mWindow; |
1249 | | }; |
1250 | | |
1251 | | NS_IMPL_ISUPPORTS(ThrottleTimeoutsCallback, nsITimerCallback, nsINamed) |
1252 | | |
1253 | | NS_IMETHODIMP |
1254 | | ThrottleTimeoutsCallback::Notify(nsITimer* aTimer) |
1255 | 0 | { |
1256 | 0 | mWindow->AsInner()->TimeoutManager().StartThrottlingTimeouts(); |
1257 | 0 | mWindow = nullptr; |
1258 | 0 | return NS_OK; |
1259 | 0 | } |
1260 | | |
1261 | | } |
1262 | | |
1263 | | bool |
1264 | | TimeoutManager::BudgetThrottlingEnabled(bool aIsBackground) const |
1265 | 0 | { |
1266 | 0 | // A window can be throttled using budget if |
1267 | 0 | // * It isn't active |
1268 | 0 | // * If it isn't using WebRTC |
1269 | 0 | // * If it hasn't got open WebSockets |
1270 | 0 | // * If it hasn't got active IndexedDB databases |
1271 | 0 |
|
1272 | 0 | // Note that we allow both foreground and background to be |
1273 | 0 | // considered for budget throttling. What determines if they are if |
1274 | 0 | // budget throttling is enabled is the max budget. |
1275 | 0 | if ((aIsBackground ? gBackgroundThrottlingMaxBudget |
1276 | 0 | : gForegroundThrottlingMaxBudget) < 0) { |
1277 | 0 | return false; |
1278 | 0 | } |
1279 | 0 | |
1280 | 0 | if (!mBudgetThrottleTimeouts || IsActive()) { |
1281 | 0 | return false; |
1282 | 0 | } |
1283 | 0 | |
1284 | 0 | // Check if there are any active IndexedDB databases |
1285 | 0 | if (mWindow.AsInner()->HasActiveIndexedDBDatabases()) { |
1286 | 0 | return false; |
1287 | 0 | } |
1288 | 0 | |
1289 | 0 | // Check if we have active PeerConnection |
1290 | 0 | if (mWindow.AsInner()->HasActivePeerConnections()) { |
1291 | 0 | return false; |
1292 | 0 | } |
1293 | 0 | |
1294 | 0 | if (mWindow.AsInner()->HasOpenWebSockets()) { |
1295 | 0 | return false; |
1296 | 0 | } |
1297 | 0 | |
1298 | 0 | return true; |
1299 | 0 | } |
1300 | | |
1301 | | void |
1302 | | TimeoutManager::StartThrottlingTimeouts() |
1303 | 0 | { |
1304 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
1305 | 0 | MOZ_DIAGNOSTIC_ASSERT(mThrottleTimeoutsTimer); |
1306 | 0 |
|
1307 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
1308 | 0 | ("TimeoutManager %p started to throttle tracking timeouts\n", this)); |
1309 | 0 |
|
1310 | 0 | MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts); |
1311 | 0 | mThrottleTimeouts = true; |
1312 | 0 | mThrottleTrackingTimeouts = true; |
1313 | 0 | mBudgetThrottleTimeouts = gEnableBudgetTimeoutThrottling; |
1314 | 0 | mThrottleTimeoutsTimer = nullptr; |
1315 | 0 | } |
1316 | | |
1317 | | void |
1318 | | TimeoutManager::OnDocumentLoaded() |
1319 | 0 | { |
1320 | 0 | // The load event may be firing again if we're coming back to the page by |
1321 | 0 | // navigating through the session history, so we need to ensure to only call |
1322 | 0 | // this when mThrottleTimeouts hasn't been set yet. |
1323 | 0 | if (!mThrottleTimeouts) { |
1324 | 0 | MaybeStartThrottleTimeout(); |
1325 | 0 | } |
1326 | 0 | } |
1327 | | |
1328 | | void |
1329 | | TimeoutManager::MaybeStartThrottleTimeout() |
1330 | 0 | { |
1331 | 0 | if (gTimeoutThrottlingDelay <= 0 || |
1332 | 0 | mWindow.IsDying() || mWindow.IsSuspended()) { |
1333 | 0 | return; |
1334 | 0 | } |
1335 | 0 | |
1336 | 0 | MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts); |
1337 | 0 |
|
1338 | 0 | MOZ_LOG(gLog, LogLevel::Debug, |
1339 | 0 | ("TimeoutManager %p delaying tracking timeout throttling by %dms\n", |
1340 | 0 | this, gTimeoutThrottlingDelay)); |
1341 | 0 |
|
1342 | 0 | nsCOMPtr<nsITimerCallback> callback = |
1343 | 0 | new ThrottleTimeoutsCallback(&mWindow); |
1344 | 0 |
|
1345 | 0 | NS_NewTimerWithCallback(getter_AddRefs(mThrottleTimeoutsTimer), |
1346 | 0 | callback, gTimeoutThrottlingDelay, nsITimer::TYPE_ONE_SHOT, |
1347 | 0 | EventTarget()); |
1348 | 0 | } |
1349 | | |
1350 | | void |
1351 | | TimeoutManager::BeginSyncOperation() |
1352 | 0 | { |
1353 | 0 | // If we're beginning a sync operation, the currently running |
1354 | 0 | // timeout will be put on hold. To not get into an inconsistent |
1355 | 0 | // state, where the currently running timeout appears to take time |
1356 | 0 | // equivalent to the period of us spinning up a new event loop, |
1357 | 0 | // record what we have and stop recording until we reach |
1358 | 0 | // EndSyncOperation. |
1359 | 0 | RecordExecution(mRunningTimeout, nullptr); |
1360 | 0 | } |
1361 | | |
1362 | | void |
1363 | | TimeoutManager::EndSyncOperation() |
1364 | 0 | { |
1365 | 0 | // If we're running a timeout, restart the measurement from here. |
1366 | 0 | RecordExecution(nullptr, mRunningTimeout); |
1367 | 0 | } |
1368 | | |
1369 | | nsIEventTarget* |
1370 | | TimeoutManager::EventTarget() |
1371 | 0 | { |
1372 | 0 | return mWindow.EventTargetFor(TaskCategory::Timer); |
1373 | 0 | } |