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