xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.TimelineSync/index.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2024 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril';
16*6dbdd20aSAndroid Build Coastguard Workerimport {Trace} from '../../public/trace';
17*6dbdd20aSAndroid Build Coastguard Workerimport {PerfettoPlugin} from '../../public/plugin';
18*6dbdd20aSAndroid Build Coastguard Workerimport {Time, TimeSpan} from '../../base/time';
19*6dbdd20aSAndroid Build Coastguard Workerimport {redrawModal, showModal} from '../../widgets/modal';
20*6dbdd20aSAndroid Build Coastguard Workerimport {assertExists} from '../../base/logging';
21*6dbdd20aSAndroid Build Coastguard Worker
22*6dbdd20aSAndroid Build Coastguard Workerconst PLUGIN_ID = 'dev.perfetto.TimelineSync';
23*6dbdd20aSAndroid Build Coastguard Workerconst DEFAULT_BROADCAST_CHANNEL = `${PLUGIN_ID}#broadcastChannel`;
24*6dbdd20aSAndroid Build Coastguard Workerconst VIEWPORT_UPDATE_THROTTLE_TIME_FOR_SENDING_AFTER_RECEIVING_MS = 1_000;
25*6dbdd20aSAndroid Build Coastguard Workerconst BIGINT_PRECISION_MULTIPLIER = 1_000_000_000n;
26*6dbdd20aSAndroid Build Coastguard Workerconst ADVERTISE_PERIOD_MS = 10_000;
27*6dbdd20aSAndroid Build Coastguard Workerconst DEFAULT_SESSION_ID = 1;
28*6dbdd20aSAndroid Build Coastguard Workertype ClientId = number;
29*6dbdd20aSAndroid Build Coastguard Workertype SessionId = number;
30*6dbdd20aSAndroid Build Coastguard Worker
31*6dbdd20aSAndroid Build Coastguard Worker/**
32*6dbdd20aSAndroid Build Coastguard Worker * Synchronizes the timeline of 2 or more perfetto traces.
33*6dbdd20aSAndroid Build Coastguard Worker *
34*6dbdd20aSAndroid Build Coastguard Worker * To trigger the sync, the command needs to be executed on one tab. It will
35*6dbdd20aSAndroid Build Coastguard Worker * prompt a list of other tabs to keep in sync. Each tab advertise itself
36*6dbdd20aSAndroid Build Coastguard Worker * on a BroadcastChannel upon trace load.
37*6dbdd20aSAndroid Build Coastguard Worker *
38*6dbdd20aSAndroid Build Coastguard Worker * This is able to sync between traces recorded at different times, even if
39*6dbdd20aSAndroid Build Coastguard Worker * their durations don't match. The initial viewport bound for each trace is
40*6dbdd20aSAndroid Build Coastguard Worker * selected when the enable command is called.
41*6dbdd20aSAndroid Build Coastguard Worker */
42*6dbdd20aSAndroid Build Coastguard Workerexport default class implements PerfettoPlugin {
43*6dbdd20aSAndroid Build Coastguard Worker  static readonly id = PLUGIN_ID;
44*6dbdd20aSAndroid Build Coastguard Worker  private _chan?: BroadcastChannel;
45*6dbdd20aSAndroid Build Coastguard Worker  private _ctx?: Trace;
46*6dbdd20aSAndroid Build Coastguard Worker  private _traceLoadTime = 0;
47*6dbdd20aSAndroid Build Coastguard Worker  // Attached to broadcast messages to allow other windows to remap viewports.
48*6dbdd20aSAndroid Build Coastguard Worker  private readonly _clientId: ClientId = Math.floor(Math.random() * 1_000_000);
49*6dbdd20aSAndroid Build Coastguard Worker  // Used to throttle sending updates after one has been received.
50*6dbdd20aSAndroid Build Coastguard Worker  private _lastReceivedUpdateMillis: number = 0;
51*6dbdd20aSAndroid Build Coastguard Worker  private _lastViewportBounds?: ViewportBounds;
52*6dbdd20aSAndroid Build Coastguard Worker  private _advertisedClients = new Map<ClientId, ClientInfo>();
53*6dbdd20aSAndroid Build Coastguard Worker  private _sessionId: SessionId = 0;
54*6dbdd20aSAndroid Build Coastguard Worker  // Used when the url passes ?dev.perfetto.TimelineSync:enable to auto-enable
55*6dbdd20aSAndroid Build Coastguard Worker  // timeline sync on trace load.
56*6dbdd20aSAndroid Build Coastguard Worker  private _sessionidFromUrl: SessionId = 0;
57*6dbdd20aSAndroid Build Coastguard Worker
58*6dbdd20aSAndroid Build Coastguard Worker  // Contains the Viewport bounds of this window when it received the first sync
59*6dbdd20aSAndroid Build Coastguard Worker  // message from another one. This is used to re-scale timestamps, so that we
60*6dbdd20aSAndroid Build Coastguard Worker  // can sync 2 (or more!) traces with different length.
61*6dbdd20aSAndroid Build Coastguard Worker  // The initial viewport will be the one visible when the command is enabled.
62*6dbdd20aSAndroid Build Coastguard Worker  private _initialBoundsForSibling = new Map<
63*6dbdd20aSAndroid Build Coastguard Worker    ClientId,
64*6dbdd20aSAndroid Build Coastguard Worker    ViewportBoundsSnapshot
65*6dbdd20aSAndroid Build Coastguard Worker  >();
66*6dbdd20aSAndroid Build Coastguard Worker
67*6dbdd20aSAndroid Build Coastguard Worker  async onTraceLoad(ctx: Trace) {
68*6dbdd20aSAndroid Build Coastguard Worker    ctx.commands.registerCommand({
69*6dbdd20aSAndroid Build Coastguard Worker      id: `dev.perfetto.SplitScreen#enableTimelineSync`,
70*6dbdd20aSAndroid Build Coastguard Worker      name: 'Enable timeline sync with other Perfetto UI tabs',
71*6dbdd20aSAndroid Build Coastguard Worker      callback: () => this.showTimelineSyncDialog(),
72*6dbdd20aSAndroid Build Coastguard Worker    });
73*6dbdd20aSAndroid Build Coastguard Worker    ctx.commands.registerCommand({
74*6dbdd20aSAndroid Build Coastguard Worker      id: `dev.perfetto.SplitScreen#disableTimelineSync`,
75*6dbdd20aSAndroid Build Coastguard Worker      name: 'Disable timeline sync',
76*6dbdd20aSAndroid Build Coastguard Worker      callback: () => this.disableTimelineSync(this._sessionId),
77*6dbdd20aSAndroid Build Coastguard Worker    });
78*6dbdd20aSAndroid Build Coastguard Worker    ctx.commands.registerCommand({
79*6dbdd20aSAndroid Build Coastguard Worker      id: `dev.perfetto.SplitScreen#toggleTimelineSync`,
80*6dbdd20aSAndroid Build Coastguard Worker      name: 'Toggle timeline sync with other PerfettoUI tabs',
81*6dbdd20aSAndroid Build Coastguard Worker      callback: () => this.toggleTimelineSync(),
82*6dbdd20aSAndroid Build Coastguard Worker      defaultHotkey: 'Mod+Alt+S',
83*6dbdd20aSAndroid Build Coastguard Worker    });
84*6dbdd20aSAndroid Build Coastguard Worker
85*6dbdd20aSAndroid Build Coastguard Worker    // Start advertising this tab. This allows the command run in other
86*6dbdd20aSAndroid Build Coastguard Worker    // instances to discover us.
87*6dbdd20aSAndroid Build Coastguard Worker    this._chan = new BroadcastChannel(DEFAULT_BROADCAST_CHANNEL);
88*6dbdd20aSAndroid Build Coastguard Worker    this._chan.onmessage = this.onmessage.bind(this);
89*6dbdd20aSAndroid Build Coastguard Worker    document.addEventListener('visibilitychange', () => this.advertise());
90*6dbdd20aSAndroid Build Coastguard Worker    window.addEventListener('focus', () => this.advertise());
91*6dbdd20aSAndroid Build Coastguard Worker    setInterval(() => this.advertise(), ADVERTISE_PERIOD_MS);
92*6dbdd20aSAndroid Build Coastguard Worker
93*6dbdd20aSAndroid Build Coastguard Worker    // Allow auto-enabling of timeline sync from the URI. The user can
94*6dbdd20aSAndroid Build Coastguard Worker    // optionally specify a session id, otherwise we just use a default one.
95*6dbdd20aSAndroid Build Coastguard Worker    const m = /dev.perfetto.TimelineSync:enable(=\d+)?/.exec(location.hash);
96*6dbdd20aSAndroid Build Coastguard Worker    if (m !== null) {
97*6dbdd20aSAndroid Build Coastguard Worker      this._sessionidFromUrl = m[1]
98*6dbdd20aSAndroid Build Coastguard Worker        ? parseInt(m[1].substring(1))
99*6dbdd20aSAndroid Build Coastguard Worker        : DEFAULT_SESSION_ID;
100*6dbdd20aSAndroid Build Coastguard Worker    }
101*6dbdd20aSAndroid Build Coastguard Worker
102*6dbdd20aSAndroid Build Coastguard Worker    this._ctx = ctx;
103*6dbdd20aSAndroid Build Coastguard Worker    this._traceLoadTime = Date.now();
104*6dbdd20aSAndroid Build Coastguard Worker    this.advertise();
105*6dbdd20aSAndroid Build Coastguard Worker    if (this._sessionidFromUrl !== 0) {
106*6dbdd20aSAndroid Build Coastguard Worker      this.enableTimelineSync(this._sessionidFromUrl);
107*6dbdd20aSAndroid Build Coastguard Worker    }
108*6dbdd20aSAndroid Build Coastguard Worker    ctx.trash.defer(() => {
109*6dbdd20aSAndroid Build Coastguard Worker      this.disableTimelineSync(this._sessionId);
110*6dbdd20aSAndroid Build Coastguard Worker      this._ctx = undefined;
111*6dbdd20aSAndroid Build Coastguard Worker    });
112*6dbdd20aSAndroid Build Coastguard Worker  }
113*6dbdd20aSAndroid Build Coastguard Worker
114*6dbdd20aSAndroid Build Coastguard Worker  private advertise() {
115*6dbdd20aSAndroid Build Coastguard Worker    if (this._ctx === undefined) return; // Don't advertise if no trace loaded.
116*6dbdd20aSAndroid Build Coastguard Worker    this._chan?.postMessage({
117*6dbdd20aSAndroid Build Coastguard Worker      perfettoSync: {
118*6dbdd20aSAndroid Build Coastguard Worker        cmd: 'MSG_ADVERTISE',
119*6dbdd20aSAndroid Build Coastguard Worker        title: document.title,
120*6dbdd20aSAndroid Build Coastguard Worker        traceLoadTime: this._traceLoadTime,
121*6dbdd20aSAndroid Build Coastguard Worker      },
122*6dbdd20aSAndroid Build Coastguard Worker      clientId: this._clientId,
123*6dbdd20aSAndroid Build Coastguard Worker    } as SyncMessage);
124*6dbdd20aSAndroid Build Coastguard Worker  }
125*6dbdd20aSAndroid Build Coastguard Worker
126*6dbdd20aSAndroid Build Coastguard Worker  private toggleTimelineSync() {
127*6dbdd20aSAndroid Build Coastguard Worker    if (this._sessionId === 0) {
128*6dbdd20aSAndroid Build Coastguard Worker      this.showTimelineSyncDialog();
129*6dbdd20aSAndroid Build Coastguard Worker    } else {
130*6dbdd20aSAndroid Build Coastguard Worker      this.disableTimelineSync(this._sessionId);
131*6dbdd20aSAndroid Build Coastguard Worker    }
132*6dbdd20aSAndroid Build Coastguard Worker  }
133*6dbdd20aSAndroid Build Coastguard Worker
134*6dbdd20aSAndroid Build Coastguard Worker  private showTimelineSyncDialog() {
135*6dbdd20aSAndroid Build Coastguard Worker    let clientsSelect: HTMLSelectElement;
136*6dbdd20aSAndroid Build Coastguard Worker
137*6dbdd20aSAndroid Build Coastguard Worker    // This nested function is invoked when the modal dialog buton is pressed.
138*6dbdd20aSAndroid Build Coastguard Worker    const doStartSession = () => {
139*6dbdd20aSAndroid Build Coastguard Worker      // Disable any prior session.
140*6dbdd20aSAndroid Build Coastguard Worker      this.disableTimelineSync(this._sessionId);
141*6dbdd20aSAndroid Build Coastguard Worker      const selectedClients = new Array<ClientId>();
142*6dbdd20aSAndroid Build Coastguard Worker      const sel = assertExists(clientsSelect).selectedOptions;
143*6dbdd20aSAndroid Build Coastguard Worker      for (let i = 0; i < sel.length; i++) {
144*6dbdd20aSAndroid Build Coastguard Worker        const clientId = parseInt(sel[i].value);
145*6dbdd20aSAndroid Build Coastguard Worker        if (!isNaN(clientId)) selectedClients.push(clientId);
146*6dbdd20aSAndroid Build Coastguard Worker      }
147*6dbdd20aSAndroid Build Coastguard Worker      selectedClients.push(this._clientId); // Always add ourselves.
148*6dbdd20aSAndroid Build Coastguard Worker      this._sessionId = Math.floor(Math.random() * 1_000_000);
149*6dbdd20aSAndroid Build Coastguard Worker      this._chan?.postMessage({
150*6dbdd20aSAndroid Build Coastguard Worker        perfettoSync: {
151*6dbdd20aSAndroid Build Coastguard Worker          cmd: 'MSG_SESSION_START',
152*6dbdd20aSAndroid Build Coastguard Worker          sessionId: this._sessionId,
153*6dbdd20aSAndroid Build Coastguard Worker          clients: selectedClients,
154*6dbdd20aSAndroid Build Coastguard Worker        },
155*6dbdd20aSAndroid Build Coastguard Worker        clientId: this._clientId,
156*6dbdd20aSAndroid Build Coastguard Worker      } as SyncMessage);
157*6dbdd20aSAndroid Build Coastguard Worker      this._initialBoundsForSibling.clear();
158*6dbdd20aSAndroid Build Coastguard Worker      this.scheduleViewportUpdateMessage();
159*6dbdd20aSAndroid Build Coastguard Worker    };
160*6dbdd20aSAndroid Build Coastguard Worker
161*6dbdd20aSAndroid Build Coastguard Worker    // The function below is called on every mithril render pass. It's important
162*6dbdd20aSAndroid Build Coastguard Worker    // that this function re-computes the list of other clients on every pass.
163*6dbdd20aSAndroid Build Coastguard Worker    // The user will go to other tabs (which causes an advertise due to the
164*6dbdd20aSAndroid Build Coastguard Worker    // visibilitychange listener) and come back on here while the modal dialog
165*6dbdd20aSAndroid Build Coastguard Worker    // is still being displayed.
166*6dbdd20aSAndroid Build Coastguard Worker    const renderModalContents = (): m.Children => {
167*6dbdd20aSAndroid Build Coastguard Worker      const children: m.Children = [];
168*6dbdd20aSAndroid Build Coastguard Worker      this.purgeInactiveClients();
169*6dbdd20aSAndroid Build Coastguard Worker      const clients = Array.from(this._advertisedClients.entries());
170*6dbdd20aSAndroid Build Coastguard Worker      clients.sort((a, b) => b[1].traceLoadTime - a[1].traceLoadTime);
171*6dbdd20aSAndroid Build Coastguard Worker      for (const [clientId, info] of clients) {
172*6dbdd20aSAndroid Build Coastguard Worker        const opened = new Date(info.traceLoadTime).toLocaleTimeString();
173*6dbdd20aSAndroid Build Coastguard Worker        const attrs: {value: number; selected?: boolean} = {value: clientId};
174*6dbdd20aSAndroid Build Coastguard Worker        if (this._advertisedClients.size === 1) {
175*6dbdd20aSAndroid Build Coastguard Worker          attrs.selected = true;
176*6dbdd20aSAndroid Build Coastguard Worker        }
177*6dbdd20aSAndroid Build Coastguard Worker        children.push(m('option', attrs, `${info.title} (${opened})`));
178*6dbdd20aSAndroid Build Coastguard Worker      }
179*6dbdd20aSAndroid Build Coastguard Worker      return m(
180*6dbdd20aSAndroid Build Coastguard Worker        'div',
181*6dbdd20aSAndroid Build Coastguard Worker        {style: 'display: flex;  flex-direction: column;'},
182*6dbdd20aSAndroid Build Coastguard Worker        m(
183*6dbdd20aSAndroid Build Coastguard Worker          'div',
184*6dbdd20aSAndroid Build Coastguard Worker          'Select the perfetto UI tab(s) you want to keep in sync ' +
185*6dbdd20aSAndroid Build Coastguard Worker            '(Ctrl+Click to select many).',
186*6dbdd20aSAndroid Build Coastguard Worker        ),
187*6dbdd20aSAndroid Build Coastguard Worker        m(
188*6dbdd20aSAndroid Build Coastguard Worker          'div',
189*6dbdd20aSAndroid Build Coastguard Worker          "If you don't see the trace listed here, temporarily focus the " +
190*6dbdd20aSAndroid Build Coastguard Worker            'corresponding browser tab and then come back here.',
191*6dbdd20aSAndroid Build Coastguard Worker        ),
192*6dbdd20aSAndroid Build Coastguard Worker        m(
193*6dbdd20aSAndroid Build Coastguard Worker          'select[multiple=multiple][size=8]',
194*6dbdd20aSAndroid Build Coastguard Worker          {
195*6dbdd20aSAndroid Build Coastguard Worker            oncreate: (vnode: m.VnodeDOM) => {
196*6dbdd20aSAndroid Build Coastguard Worker              clientsSelect = vnode.dom as HTMLSelectElement;
197*6dbdd20aSAndroid Build Coastguard Worker            },
198*6dbdd20aSAndroid Build Coastguard Worker          },
199*6dbdd20aSAndroid Build Coastguard Worker          children,
200*6dbdd20aSAndroid Build Coastguard Worker        ),
201*6dbdd20aSAndroid Build Coastguard Worker      );
202*6dbdd20aSAndroid Build Coastguard Worker    };
203*6dbdd20aSAndroid Build Coastguard Worker
204*6dbdd20aSAndroid Build Coastguard Worker    showModal({
205*6dbdd20aSAndroid Build Coastguard Worker      title: 'Synchronize timeline across several tabs',
206*6dbdd20aSAndroid Build Coastguard Worker      content: renderModalContents,
207*6dbdd20aSAndroid Build Coastguard Worker      buttons: [
208*6dbdd20aSAndroid Build Coastguard Worker        {
209*6dbdd20aSAndroid Build Coastguard Worker          primary: true,
210*6dbdd20aSAndroid Build Coastguard Worker          text: `Synchronize timelines`,
211*6dbdd20aSAndroid Build Coastguard Worker          action: doStartSession,
212*6dbdd20aSAndroid Build Coastguard Worker        },
213*6dbdd20aSAndroid Build Coastguard Worker      ],
214*6dbdd20aSAndroid Build Coastguard Worker    });
215*6dbdd20aSAndroid Build Coastguard Worker  }
216*6dbdd20aSAndroid Build Coastguard Worker
217*6dbdd20aSAndroid Build Coastguard Worker  private enableTimelineSync(sessionId: SessionId) {
218*6dbdd20aSAndroid Build Coastguard Worker    if (sessionId === this._sessionId) return; // Already in this session id.
219*6dbdd20aSAndroid Build Coastguard Worker    this._sessionId = sessionId;
220*6dbdd20aSAndroid Build Coastguard Worker    this._initialBoundsForSibling.clear();
221*6dbdd20aSAndroid Build Coastguard Worker    this.scheduleViewportUpdateMessage();
222*6dbdd20aSAndroid Build Coastguard Worker  }
223*6dbdd20aSAndroid Build Coastguard Worker
224*6dbdd20aSAndroid Build Coastguard Worker  private disableTimelineSync(sessionId: SessionId, skipMsg = false) {
225*6dbdd20aSAndroid Build Coastguard Worker    if (sessionId !== this._sessionId || this._sessionId === 0) return;
226*6dbdd20aSAndroid Build Coastguard Worker
227*6dbdd20aSAndroid Build Coastguard Worker    if (!skipMsg) {
228*6dbdd20aSAndroid Build Coastguard Worker      this._chan?.postMessage({
229*6dbdd20aSAndroid Build Coastguard Worker        perfettoSync: {
230*6dbdd20aSAndroid Build Coastguard Worker          cmd: 'MSG_SESSION_STOP',
231*6dbdd20aSAndroid Build Coastguard Worker          sessionId: this._sessionId,
232*6dbdd20aSAndroid Build Coastguard Worker        },
233*6dbdd20aSAndroid Build Coastguard Worker        clientId: this._clientId,
234*6dbdd20aSAndroid Build Coastguard Worker      } as SyncMessage);
235*6dbdd20aSAndroid Build Coastguard Worker    }
236*6dbdd20aSAndroid Build Coastguard Worker    this._sessionId = 0;
237*6dbdd20aSAndroid Build Coastguard Worker    this._initialBoundsForSibling.clear();
238*6dbdd20aSAndroid Build Coastguard Worker  }
239*6dbdd20aSAndroid Build Coastguard Worker
240*6dbdd20aSAndroid Build Coastguard Worker  private shouldThrottleViewportUpdates() {
241*6dbdd20aSAndroid Build Coastguard Worker    return (
242*6dbdd20aSAndroid Build Coastguard Worker      Date.now() - this._lastReceivedUpdateMillis <=
243*6dbdd20aSAndroid Build Coastguard Worker      VIEWPORT_UPDATE_THROTTLE_TIME_FOR_SENDING_AFTER_RECEIVING_MS
244*6dbdd20aSAndroid Build Coastguard Worker    );
245*6dbdd20aSAndroid Build Coastguard Worker  }
246*6dbdd20aSAndroid Build Coastguard Worker
247*6dbdd20aSAndroid Build Coastguard Worker  private scheduleViewportUpdateMessage() {
248*6dbdd20aSAndroid Build Coastguard Worker    if (!this.active) return;
249*6dbdd20aSAndroid Build Coastguard Worker    const currentViewport = this.getCurrentViewportBounds();
250*6dbdd20aSAndroid Build Coastguard Worker    if (
251*6dbdd20aSAndroid Build Coastguard Worker      (!this._lastViewportBounds ||
252*6dbdd20aSAndroid Build Coastguard Worker        !this._lastViewportBounds.equals(currentViewport)) &&
253*6dbdd20aSAndroid Build Coastguard Worker      !this.shouldThrottleViewportUpdates()
254*6dbdd20aSAndroid Build Coastguard Worker    ) {
255*6dbdd20aSAndroid Build Coastguard Worker      this.sendViewportBounds(currentViewport);
256*6dbdd20aSAndroid Build Coastguard Worker      this._lastViewportBounds = currentViewport;
257*6dbdd20aSAndroid Build Coastguard Worker    }
258*6dbdd20aSAndroid Build Coastguard Worker    requestAnimationFrame(this.scheduleViewportUpdateMessage.bind(this));
259*6dbdd20aSAndroid Build Coastguard Worker  }
260*6dbdd20aSAndroid Build Coastguard Worker
261*6dbdd20aSAndroid Build Coastguard Worker  private sendViewportBounds(viewportBounds: ViewportBounds) {
262*6dbdd20aSAndroid Build Coastguard Worker    this._chan?.postMessage({
263*6dbdd20aSAndroid Build Coastguard Worker      perfettoSync: {
264*6dbdd20aSAndroid Build Coastguard Worker        cmd: 'MSG_SET_VIEWPORT',
265*6dbdd20aSAndroid Build Coastguard Worker        sessionId: this._sessionId,
266*6dbdd20aSAndroid Build Coastguard Worker        viewportBounds,
267*6dbdd20aSAndroid Build Coastguard Worker      },
268*6dbdd20aSAndroid Build Coastguard Worker      clientId: this._clientId,
269*6dbdd20aSAndroid Build Coastguard Worker    } as SyncMessage);
270*6dbdd20aSAndroid Build Coastguard Worker  }
271*6dbdd20aSAndroid Build Coastguard Worker
272*6dbdd20aSAndroid Build Coastguard Worker  private onmessage(msg: MessageEvent) {
273*6dbdd20aSAndroid Build Coastguard Worker    if (this._ctx === undefined) return; // Trace unloaded
274*6dbdd20aSAndroid Build Coastguard Worker    if (!('perfettoSync' in msg.data)) return;
275*6dbdd20aSAndroid Build Coastguard Worker    this._ctx.scheduleFullRedraw('force');
276*6dbdd20aSAndroid Build Coastguard Worker    const msgData = msg.data as SyncMessage;
277*6dbdd20aSAndroid Build Coastguard Worker    const sync = msgData.perfettoSync;
278*6dbdd20aSAndroid Build Coastguard Worker    switch (sync.cmd) {
279*6dbdd20aSAndroid Build Coastguard Worker      case 'MSG_ADVERTISE':
280*6dbdd20aSAndroid Build Coastguard Worker        if (msgData.clientId !== this._clientId) {
281*6dbdd20aSAndroid Build Coastguard Worker          this._advertisedClients.set(msgData.clientId, {
282*6dbdd20aSAndroid Build Coastguard Worker            title: sync.title,
283*6dbdd20aSAndroid Build Coastguard Worker            traceLoadTime: sync.traceLoadTime,
284*6dbdd20aSAndroid Build Coastguard Worker            lastHeartbeat: Date.now(),
285*6dbdd20aSAndroid Build Coastguard Worker          });
286*6dbdd20aSAndroid Build Coastguard Worker          this.purgeInactiveClients();
287*6dbdd20aSAndroid Build Coastguard Worker          redrawModal();
288*6dbdd20aSAndroid Build Coastguard Worker        }
289*6dbdd20aSAndroid Build Coastguard Worker        break;
290*6dbdd20aSAndroid Build Coastguard Worker      case 'MSG_SESSION_START':
291*6dbdd20aSAndroid Build Coastguard Worker        if (sync.clients.includes(this._clientId)) {
292*6dbdd20aSAndroid Build Coastguard Worker          this.enableTimelineSync(sync.sessionId);
293*6dbdd20aSAndroid Build Coastguard Worker        }
294*6dbdd20aSAndroid Build Coastguard Worker        break;
295*6dbdd20aSAndroid Build Coastguard Worker      case 'MSG_SESSION_STOP':
296*6dbdd20aSAndroid Build Coastguard Worker        this.disableTimelineSync(sync.sessionId, /* skipMsg= */ true);
297*6dbdd20aSAndroid Build Coastguard Worker        break;
298*6dbdd20aSAndroid Build Coastguard Worker      case 'MSG_SET_VIEWPORT':
299*6dbdd20aSAndroid Build Coastguard Worker        if (sync.sessionId === this._sessionId) {
300*6dbdd20aSAndroid Build Coastguard Worker          this.onViewportSyncReceived(sync.viewportBounds, msgData.clientId);
301*6dbdd20aSAndroid Build Coastguard Worker        }
302*6dbdd20aSAndroid Build Coastguard Worker        break;
303*6dbdd20aSAndroid Build Coastguard Worker    }
304*6dbdd20aSAndroid Build Coastguard Worker  }
305*6dbdd20aSAndroid Build Coastguard Worker
306*6dbdd20aSAndroid Build Coastguard Worker  private onViewportSyncReceived(
307*6dbdd20aSAndroid Build Coastguard Worker    requestViewBounds: ViewportBounds,
308*6dbdd20aSAndroid Build Coastguard Worker    source: ClientId,
309*6dbdd20aSAndroid Build Coastguard Worker  ) {
310*6dbdd20aSAndroid Build Coastguard Worker    if (!this.active) return;
311*6dbdd20aSAndroid Build Coastguard Worker    this.cacheSiblingInitialBoundIfNeeded(requestViewBounds, source);
312*6dbdd20aSAndroid Build Coastguard Worker    const remappedViewport = this.remapViewportBounds(
313*6dbdd20aSAndroid Build Coastguard Worker      requestViewBounds,
314*6dbdd20aSAndroid Build Coastguard Worker      source,
315*6dbdd20aSAndroid Build Coastguard Worker    );
316*6dbdd20aSAndroid Build Coastguard Worker    if (!this.getCurrentViewportBounds().equals(remappedViewport)) {
317*6dbdd20aSAndroid Build Coastguard Worker      this._lastReceivedUpdateMillis = Date.now();
318*6dbdd20aSAndroid Build Coastguard Worker      this._lastViewportBounds = remappedViewport;
319*6dbdd20aSAndroid Build Coastguard Worker      this._ctx?.timeline.setViewportTime(
320*6dbdd20aSAndroid Build Coastguard Worker        remappedViewport.start,
321*6dbdd20aSAndroid Build Coastguard Worker        remappedViewport.end,
322*6dbdd20aSAndroid Build Coastguard Worker      );
323*6dbdd20aSAndroid Build Coastguard Worker    }
324*6dbdd20aSAndroid Build Coastguard Worker  }
325*6dbdd20aSAndroid Build Coastguard Worker
326*6dbdd20aSAndroid Build Coastguard Worker  private cacheSiblingInitialBoundIfNeeded(
327*6dbdd20aSAndroid Build Coastguard Worker    requestViewBounds: ViewportBounds,
328*6dbdd20aSAndroid Build Coastguard Worker    source: ClientId,
329*6dbdd20aSAndroid Build Coastguard Worker  ) {
330*6dbdd20aSAndroid Build Coastguard Worker    if (!this._initialBoundsForSibling.has(source)) {
331*6dbdd20aSAndroid Build Coastguard Worker      this._initialBoundsForSibling.set(source, {
332*6dbdd20aSAndroid Build Coastguard Worker        thisWindow: this.getCurrentViewportBounds(),
333*6dbdd20aSAndroid Build Coastguard Worker        otherWindow: requestViewBounds,
334*6dbdd20aSAndroid Build Coastguard Worker      });
335*6dbdd20aSAndroid Build Coastguard Worker    }
336*6dbdd20aSAndroid Build Coastguard Worker  }
337*6dbdd20aSAndroid Build Coastguard Worker
338*6dbdd20aSAndroid Build Coastguard Worker  private remapViewportBounds(
339*6dbdd20aSAndroid Build Coastguard Worker    otherWindowBounds: ViewportBounds,
340*6dbdd20aSAndroid Build Coastguard Worker    source: ClientId,
341*6dbdd20aSAndroid Build Coastguard Worker  ): ViewportBounds {
342*6dbdd20aSAndroid Build Coastguard Worker    const initialSnapshot = this._initialBoundsForSibling.get(source)!;
343*6dbdd20aSAndroid Build Coastguard Worker    const otherInitial = initialSnapshot.otherWindow;
344*6dbdd20aSAndroid Build Coastguard Worker    const thisInitial = initialSnapshot.thisWindow;
345*6dbdd20aSAndroid Build Coastguard Worker
346*6dbdd20aSAndroid Build Coastguard Worker    const [l, r] = this.percentageChange(
347*6dbdd20aSAndroid Build Coastguard Worker      otherInitial.start,
348*6dbdd20aSAndroid Build Coastguard Worker      otherInitial.end,
349*6dbdd20aSAndroid Build Coastguard Worker      otherWindowBounds.start,
350*6dbdd20aSAndroid Build Coastguard Worker      otherWindowBounds.end,
351*6dbdd20aSAndroid Build Coastguard Worker    );
352*6dbdd20aSAndroid Build Coastguard Worker    const thisWindowInitialLength = thisInitial.end - thisInitial.start;
353*6dbdd20aSAndroid Build Coastguard Worker
354*6dbdd20aSAndroid Build Coastguard Worker    return new TimeSpan(
355*6dbdd20aSAndroid Build Coastguard Worker      Time.fromRaw(
356*6dbdd20aSAndroid Build Coastguard Worker        thisInitial.start +
357*6dbdd20aSAndroid Build Coastguard Worker          (thisWindowInitialLength * l) / BIGINT_PRECISION_MULTIPLIER,
358*6dbdd20aSAndroid Build Coastguard Worker      ),
359*6dbdd20aSAndroid Build Coastguard Worker      Time.fromRaw(
360*6dbdd20aSAndroid Build Coastguard Worker        thisInitial.start +
361*6dbdd20aSAndroid Build Coastguard Worker          (thisWindowInitialLength * r) / BIGINT_PRECISION_MULTIPLIER,
362*6dbdd20aSAndroid Build Coastguard Worker      ),
363*6dbdd20aSAndroid Build Coastguard Worker    );
364*6dbdd20aSAndroid Build Coastguard Worker  }
365*6dbdd20aSAndroid Build Coastguard Worker
366*6dbdd20aSAndroid Build Coastguard Worker  /*
367*6dbdd20aSAndroid Build Coastguard Worker   * Returns the percentage (*1e9) of the starting point inside
368*6dbdd20aSAndroid Build Coastguard Worker   * [initialL, initialR] of [currentL, currentR].
369*6dbdd20aSAndroid Build Coastguard Worker   *
370*6dbdd20aSAndroid Build Coastguard Worker   * A few examples:
371*6dbdd20aSAndroid Build Coastguard Worker   * - If current == initial, the output is expected to be [0,1e9]
372*6dbdd20aSAndroid Build Coastguard Worker   * - If current  is inside the initial -> [>0, < 1e9]
373*6dbdd20aSAndroid Build Coastguard Worker   * - If current is completely outside initial to the right -> [>1e9, >>1e9].
374*6dbdd20aSAndroid Build Coastguard Worker   * - If current is completely outside initial to the left -> [<<0, <0]
375*6dbdd20aSAndroid Build Coastguard Worker   */
376*6dbdd20aSAndroid Build Coastguard Worker  private percentageChange(
377*6dbdd20aSAndroid Build Coastguard Worker    initialL: bigint,
378*6dbdd20aSAndroid Build Coastguard Worker    initialR: bigint,
379*6dbdd20aSAndroid Build Coastguard Worker    currentL: bigint,
380*6dbdd20aSAndroid Build Coastguard Worker    currentR: bigint,
381*6dbdd20aSAndroid Build Coastguard Worker  ): [bigint, bigint] {
382*6dbdd20aSAndroid Build Coastguard Worker    const initialW = initialR - initialL;
383*6dbdd20aSAndroid Build Coastguard Worker    const dtL = currentL - initialL;
384*6dbdd20aSAndroid Build Coastguard Worker    const dtR = currentR - initialL;
385*6dbdd20aSAndroid Build Coastguard Worker    return [this.divide(dtL, initialW), this.divide(dtR, initialW)];
386*6dbdd20aSAndroid Build Coastguard Worker  }
387*6dbdd20aSAndroid Build Coastguard Worker
388*6dbdd20aSAndroid Build Coastguard Worker  private divide(a: bigint, b: bigint): bigint {
389*6dbdd20aSAndroid Build Coastguard Worker    // Let's not lose precision
390*6dbdd20aSAndroid Build Coastguard Worker    return (a * BIGINT_PRECISION_MULTIPLIER) / b;
391*6dbdd20aSAndroid Build Coastguard Worker  }
392*6dbdd20aSAndroid Build Coastguard Worker
393*6dbdd20aSAndroid Build Coastguard Worker  private getCurrentViewportBounds(): ViewportBounds {
394*6dbdd20aSAndroid Build Coastguard Worker    return this._ctx!.timeline.visibleWindow.toTimeSpan();
395*6dbdd20aSAndroid Build Coastguard Worker  }
396*6dbdd20aSAndroid Build Coastguard Worker
397*6dbdd20aSAndroid Build Coastguard Worker  private purgeInactiveClients() {
398*6dbdd20aSAndroid Build Coastguard Worker    const now = Date.now();
399*6dbdd20aSAndroid Build Coastguard Worker    const TIMEOUT_MS = 30_000;
400*6dbdd20aSAndroid Build Coastguard Worker    for (const [clientId, info] of this._advertisedClients.entries()) {
401*6dbdd20aSAndroid Build Coastguard Worker      if (now - info.lastHeartbeat < TIMEOUT_MS) continue;
402*6dbdd20aSAndroid Build Coastguard Worker      this._advertisedClients.delete(clientId);
403*6dbdd20aSAndroid Build Coastguard Worker    }
404*6dbdd20aSAndroid Build Coastguard Worker  }
405*6dbdd20aSAndroid Build Coastguard Worker
406*6dbdd20aSAndroid Build Coastguard Worker  private get active() {
407*6dbdd20aSAndroid Build Coastguard Worker    return this._sessionId !== 0;
408*6dbdd20aSAndroid Build Coastguard Worker  }
409*6dbdd20aSAndroid Build Coastguard Worker}
410*6dbdd20aSAndroid Build Coastguard Worker
411*6dbdd20aSAndroid Build Coastguard Workertype ViewportBounds = TimeSpan;
412*6dbdd20aSAndroid Build Coastguard Worker
413*6dbdd20aSAndroid Build Coastguard Workerinterface ViewportBoundsSnapshot {
414*6dbdd20aSAndroid Build Coastguard Worker  thisWindow: ViewportBounds;
415*6dbdd20aSAndroid Build Coastguard Worker  otherWindow: ViewportBounds;
416*6dbdd20aSAndroid Build Coastguard Worker}
417*6dbdd20aSAndroid Build Coastguard Worker
418*6dbdd20aSAndroid Build Coastguard Workerinterface MsgSetViewport {
419*6dbdd20aSAndroid Build Coastguard Worker  cmd: 'MSG_SET_VIEWPORT';
420*6dbdd20aSAndroid Build Coastguard Worker  sessionId: SessionId;
421*6dbdd20aSAndroid Build Coastguard Worker  viewportBounds: ViewportBounds;
422*6dbdd20aSAndroid Build Coastguard Worker}
423*6dbdd20aSAndroid Build Coastguard Worker
424*6dbdd20aSAndroid Build Coastguard Workerinterface MsgAdvertise {
425*6dbdd20aSAndroid Build Coastguard Worker  cmd: 'MSG_ADVERTISE';
426*6dbdd20aSAndroid Build Coastguard Worker  title: string;
427*6dbdd20aSAndroid Build Coastguard Worker  traceLoadTime: number;
428*6dbdd20aSAndroid Build Coastguard Worker}
429*6dbdd20aSAndroid Build Coastguard Worker
430*6dbdd20aSAndroid Build Coastguard Workerinterface MsgSessionStart {
431*6dbdd20aSAndroid Build Coastguard Worker  cmd: 'MSG_SESSION_START';
432*6dbdd20aSAndroid Build Coastguard Worker  sessionId: SessionId;
433*6dbdd20aSAndroid Build Coastguard Worker  clients: ClientId[];
434*6dbdd20aSAndroid Build Coastguard Worker}
435*6dbdd20aSAndroid Build Coastguard Worker
436*6dbdd20aSAndroid Build Coastguard Workerinterface MsgSessionStop {
437*6dbdd20aSAndroid Build Coastguard Worker  cmd: 'MSG_SESSION_STOP';
438*6dbdd20aSAndroid Build Coastguard Worker  sessionId: SessionId;
439*6dbdd20aSAndroid Build Coastguard Worker}
440*6dbdd20aSAndroid Build Coastguard Worker
441*6dbdd20aSAndroid Build Coastguard Worker// In case of new messages, they should be "or-ed" here.
442*6dbdd20aSAndroid Build Coastguard Workertype SyncMessages =
443*6dbdd20aSAndroid Build Coastguard Worker  | MsgSetViewport
444*6dbdd20aSAndroid Build Coastguard Worker  | MsgAdvertise
445*6dbdd20aSAndroid Build Coastguard Worker  | MsgSessionStart
446*6dbdd20aSAndroid Build Coastguard Worker  | MsgSessionStop;
447*6dbdd20aSAndroid Build Coastguard Worker
448*6dbdd20aSAndroid Build Coastguard Workerinterface SyncMessage {
449*6dbdd20aSAndroid Build Coastguard Worker  perfettoSync: SyncMessages;
450*6dbdd20aSAndroid Build Coastguard Worker  clientId: ClientId;
451*6dbdd20aSAndroid Build Coastguard Worker}
452*6dbdd20aSAndroid Build Coastguard Worker
453*6dbdd20aSAndroid Build Coastguard Workerinterface ClientInfo {
454*6dbdd20aSAndroid Build Coastguard Worker  title: string;
455*6dbdd20aSAndroid Build Coastguard Worker  lastHeartbeat: number; // Datetime.now() of the last MSG_ADVERTISE.
456*6dbdd20aSAndroid Build Coastguard Worker  traceLoadTime: number; // Datetime.now() of the onTraceLoad().
457*6dbdd20aSAndroid Build Coastguard Worker}
458