xref: /aosp_15_r20/external/perfetto/ui/src/frontend/idle_detector.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {defer} from '../base/deferred';
16import {raf} from '../core/raf_scheduler';
17import {AppImpl} from '../core/app_impl';
18
19/**
20 * This class is exposed by index.ts as window.waitForPerfettoIdle() and is used
21 * by tests, to detect when we reach quiescence.
22 */
23
24const IDLE_HYSTERESIS_MS = 100;
25const TIMEOUT_MS = 30_000;
26
27export class IdleDetector {
28  private promise = defer<void>();
29  private deadline = performance.now() + TIMEOUT_MS;
30  private idleSince?: number;
31  private idleHysteresisMs = IDLE_HYSTERESIS_MS;
32
33  waitForPerfettoIdle(idleHysteresisMs = IDLE_HYSTERESIS_MS): Promise<void> {
34    this.idleSince = undefined;
35    this.idleHysteresisMs = idleHysteresisMs;
36    this.scheduleNextTask();
37    return this.promise;
38  }
39
40  private onIdleCallback() {
41    const now = performance.now();
42    if (now > this.deadline) {
43      this.promise.reject(
44        `Didn't reach idle within ${TIMEOUT_MS} ms, giving up` +
45          ` ${this.idleIndicators()}`,
46      );
47      return;
48    }
49    if (this.idleIndicators().every((x) => x)) {
50      this.idleSince = this.idleSince ?? now;
51      const idleDur = now - this.idleSince;
52      if (idleDur >= this.idleHysteresisMs) {
53        // We have been idle for more than the threshold, success.
54        this.promise.resolve();
55        return;
56      }
57      // We are idle, but not for long enough. keep waiting
58      this.scheduleNextTask();
59      return;
60    }
61    // Not idle, reset and repeat.
62    this.idleSince = undefined;
63    this.scheduleNextTask();
64  }
65
66  private scheduleNextTask() {
67    requestIdleCallback(() => this.onIdleCallback());
68  }
69
70  private idleIndicators() {
71    const reqsPending = AppImpl.instance.trace?.engine.numRequestsPending ?? 0;
72    return [
73      reqsPending === 0,
74      !raf.hasPendingRedraws,
75      !document.getAnimations().some((a) => a.playState === 'running'),
76      document.querySelector('.progress.progress-anim') == null,
77      document.querySelector('.omnibox.message-mode') == null,
78    ];
79  }
80}
81