|  | // Copyright (C) 2019 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | import {Actions} from '../common/actions'; | 
|  | import {getContainingTrackId} from '../common/state'; | 
|  | import {fromNs, TimeSpan, toNs} from '../common/time'; | 
|  |  | 
|  | import {globals} from './globals'; | 
|  |  | 
|  | const INCOMPLETE_SLICE_TIME_S = 0.00003; | 
|  |  | 
|  | /** | 
|  | * Given a timestamp, if |ts| is not currently in view move the view to | 
|  | * center |ts|, keeping the same zoom level. | 
|  | */ | 
|  | export function horizontalScrollToTs(ts: number) { | 
|  | const startNs = toNs(globals.frontendLocalState.visibleWindowTime.start); | 
|  | const endNs = toNs(globals.frontendLocalState.visibleWindowTime.end); | 
|  | const currentViewNs = endNs - startNs; | 
|  | if (ts < startNs || ts > endNs) { | 
|  | // TODO(taylori): This is an ugly jump, we should do a smooth pan instead. | 
|  | globals.frontendLocalState.updateVisibleTime(new TimeSpan( | 
|  | fromNs(ts - currentViewNs / 2), fromNs(ts + currentViewNs / 2))); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Given a start and end timestamp (in ns), move the view to center this range | 
|  | * and zoom to a level where the range is 1/5 of the viewport. | 
|  | */ | 
|  | export function horizontalScrollAndZoomToRange(startTs: number, endTs: number) { | 
|  | const visibleDur = globals.frontendLocalState.visibleWindowTime.end - | 
|  | globals.frontendLocalState.visibleWindowTime.start; | 
|  | let selectDur = endTs - startTs; | 
|  | if (toNs(selectDur) === -1) {  // Unfinished slice | 
|  | selectDur = INCOMPLETE_SLICE_TIME_S; | 
|  | endTs = startTs; | 
|  | } | 
|  | const viewStartNs = toNs(globals.frontendLocalState.visibleWindowTime.start); | 
|  | const viewEndNs = toNs(globals.frontendLocalState.visibleWindowTime.end); | 
|  | if (selectDur / visibleDur < 0.05 || startTs < viewStartNs || | 
|  | endTs > viewEndNs) { | 
|  | globals.frontendLocalState.updateVisibleTime( | 
|  | new TimeSpan(startTs - (selectDur * 2), endTs + (selectDur * 2))); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Given a track id, find a track with that id and scroll it into view. If the | 
|  | * track is nested inside a track group, scroll to that track group instead. | 
|  | * If |openGroup| then open the track group and scroll to the track. | 
|  | */ | 
|  | export function verticalScrollToTrack( | 
|  | trackId: string|number, openGroup = false) { | 
|  | const trackIdString = `${trackId}`; | 
|  | const track = document.querySelector('#track_' + trackIdString); | 
|  |  | 
|  | if (track) { | 
|  | // block: 'nearest' means that it will only scroll if the track is not | 
|  | // currently in view. | 
|  | track.scrollIntoView({behavior: 'smooth', block: 'nearest'}); | 
|  | return; | 
|  | } | 
|  |  | 
|  | let trackGroup = null; | 
|  | const trackGroupId = getContainingTrackId(globals.state, trackIdString); | 
|  | if (trackGroupId) { | 
|  | trackGroup = document.querySelector('#track_' + trackGroupId); | 
|  | } | 
|  |  | 
|  | if (!trackGroupId || !trackGroup) { | 
|  | console.error(`Can't scroll, track (${trackIdString}) not found.`); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // The requested track is inside a closed track group, either open the track | 
|  | // group and scroll to the track or just scroll to the track group. | 
|  | if (openGroup) { | 
|  | // After the track exists in the dom, it will be scrolled to. | 
|  | globals.frontendLocalState.scrollToTrackId = trackId; | 
|  | globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId})); | 
|  | return; | 
|  | } else { | 
|  | trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'}); | 
|  | } | 
|  | } | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Scroll vertically and horizontally to reach track (|trackId|) at |ts|. | 
|  | */ | 
|  | export function scrollToTrackAndTs( | 
|  | trackId: string|number|undefined, ts: number, openGroup = false) { | 
|  | if (trackId !== undefined) { | 
|  | verticalScrollToTrack(trackId, openGroup); | 
|  | } | 
|  | horizontalScrollToTs(ts); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the UI track Id that is associated with the given |traceTrackId| in | 
|  | * the trace_processor. Due to concepts like Async tracks and TrackGroups this | 
|  | * is not always a one to one mapping. | 
|  | */ | 
|  | export function findUiTrackId(traceTrackId: number) { | 
|  | for (const [uiTrackId, trackState] of Object.entries(globals.state.tracks)) { | 
|  | const config = trackState.config as {trackId: number}; | 
|  | if (config.trackId === traceTrackId) return uiTrackId; | 
|  | } | 
|  | return null; | 
|  | } |