Skip to content

Commit

Permalink
Merge "ui: add unified scrollTo" into main
Browse files Browse the repository at this point in the history
  • Loading branch information
primiano authored and Gerrit Code Review committed Sep 9, 2024
2 parents 180231f + ddff4be commit 0b5423d
Show file tree
Hide file tree
Showing 23 changed files with 343 additions and 344 deletions.
9 changes: 9 additions & 0 deletions ui/src/common/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {TraceInfo} from '../public/trace_info';
import {Workspace, WorkspaceManager} from '../public/workspace';
import {Migrate, Store} from '../base/store';
import {LegacyDetailsPanel} from '../public/details_panel';
import {scrollTo, ScrollToArgs} from '../public/scroll_helper';
import {LegacySelection, SelectionOpts} from '../public/selection';

// Every plugin gets its own PluginContext. This is how we keep track
// what each plugin is doing and how we can blame issues on particular
Expand Down Expand Up @@ -187,6 +189,9 @@ class PluginContextTraceImpl implements Trace, Disposable {
get selection() {
return globals.selectionManager.selection;
},
setLegacy(args: LegacySelection, opts?: SelectionOpts) {
globals.selectionManager.setLegacy(args, opts);
},
clear() {
globals.selectionManager.clear();
},
Expand All @@ -205,6 +210,10 @@ class PluginContextTraceImpl implements Trace, Disposable {
this.trash.use(unregister);
}

scrollTo(args: ScrollToArgs): void {
scrollTo(args);
}

get pluginId(): string {
return this.ctx.pluginId;
}
Expand Down
144 changes: 144 additions & 0 deletions ui/src/core/scroll_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (C) 2024 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 {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
import {time} from '../base/time';
import {escapeCSSSelector} from '../base/utils';
import {ScrollToArgs} from 'src/public/scroll_helper';
import {TraceInfo} from '../public/trace_info';
import {Workspace} from '../public/workspace';
import {raf} from './raf_scheduler';
import {TimelineImpl} from './timeline';
import {TrackManagerImpl} from './track_manager';

// A helper class to help jumping to tracks and time ranges.
// This class must NOT alter in any way the selection status. That
// responsibility belongs to SelectionManager (which uses this).
export class ScrollHelper {
constructor(
private traceInfo: TraceInfo,
private timeline: TimelineImpl,
private workspace: Workspace,
private trackManager: TrackManagerImpl,
) {}

// See comments in ScrollToArgs for the intended semantics.
scrollTo(args: ScrollToArgs) {
const {time, track} = args;
raf.scheduleRedraw();

if (time !== undefined) {
if (time.end === undefined) {
this.timeline.panToTimestamp(time.start);
} else if (time.viewPercentage !== undefined) {
this.focusHorizontalRangePercentage(
time.start,
time.end,
time.viewPercentage,
);
} else {
this.focusHorizontalRange(time.start, time.end);
}
}

if (track !== undefined) {
this.verticalScrollToTrack(track.uri, track.expandGroup ?? false);
}
}

private focusHorizontalRangePercentage(
start: time,
end: time,
viewPercentage: number,
): void {
const aoi = HighPrecisionTimeSpan.fromTime(start, end);

if (viewPercentage <= 0.0 || viewPercentage > 1.0) {
console.warn(
'Invalid value for [viewPercentage]. ' +
'Value must be between 0.0 (exclusive) and 1.0 (inclusive).',
);
// Default to 50%.
viewPercentage = 0.5;
}
const paddingPercentage = 1.0 - viewPercentage;
const halfPaddingTime = (aoi.duration * paddingPercentage) / 2;
this.timeline.updateVisibleTimeHP(aoi.pad(halfPaddingTime));
}

private focusHorizontalRange(start: time, end: time): void {
const visible = this.timeline.visibleWindow;
const aoi = HighPrecisionTimeSpan.fromTime(start, end);
const fillRatio = 5; // Default amount to make the AOI fill the viewport
const padRatio = (fillRatio - 1) / 2;

// If the area of interest already fills more than half the viewport, zoom
// out so that the AOI fills 20% of the viewport
if (aoi.duration * 2 > visible.duration) {
const padded = aoi.pad(aoi.duration * padRatio);
this.timeline.updateVisibleTimeHP(padded);
} else {
// Center visible window on the middle of the AOI, preserving zoom level.
const newStart = aoi.midpoint.subNumber(visible.duration / 2);

// Adjust the new visible window if it intersects with the trace boundaries.
// It's needed to make the "update the zoom level if visible window doesn't
// change" logic reliable.
const newVisibleWindow = new HighPrecisionTimeSpan(
newStart,
visible.duration,
).fitWithin(this.traceInfo.start, this.traceInfo.end);

// If preserving the zoom doesn't change the visible window, consider this
// to be the "second" hotkey press, so just make the AOI fill 20% of the
// viewport
if (newVisibleWindow.equals(visible)) {
const padded = aoi.pad(aoi.duration * padRatio);
this.timeline.updateVisibleTimeHP(padded);
} else {
this.timeline.updateVisibleTimeHP(newVisibleWindow);
}
}
}

private verticalScrollToTrack(trackUri: string, openGroup: boolean) {
const dom = document.querySelector('#track_' + escapeCSSSelector(trackUri));
if (dom) {
// block: 'nearest' means that it will only scroll if the track is not
// currently in view.
dom.scrollIntoView({behavior: 'smooth', block: 'nearest'});
return;
}

// If we get here, the element for this track was not present in the DOM,
// this might be because it's inside a collapsed group.
// Find the track node in the current workspace, and reveal it.
const trackNode = this.workspace.getTrackByUri(trackUri);
if (!trackNode) return;

if (openGroup) {
trackNode.reveal();
this.trackManager.scrollToTrackUriOnCreate = trackUri;
return;
}

// Find the first closed ancestor of our target track.
const groupNode = trackNode.closestVisibleAncestor;
if (groupNode) {
document
.querySelector('#track_' + groupNode.uri)
?.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}
}
7 changes: 7 additions & 0 deletions ui/src/core/track_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export interface TrackRenderer {
export class TrackManagerImpl implements TrackManager {
private tracks = new Registry<TrackFSM>((x) => x.desc.uri);

// This property is written by scroll_helper.ts and read&cleared by the
// track_panel.ts. This exist for the following use case: the user wants to
// scroll to track X, but X is not visible because it's in a collapsed group.
// So we want to stash this information in a place that track_panel.ts can
// access when creating dom elements.
scrollToTrackUriOnCreate?: string;

registerTrack(trackDesc: TrackDescriptor): Disposable {
return this.tracks.register(new TrackFSM(trackDesc));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import {Icons} from '../../base/semantic_icons';
import {duration, Time, time} from '../../base/time';
import {exists} from '../../base/utils';
import {globals} from '../../frontend/globals';
import {
focusHorizontalRange,
verticalScrollToTrack,
} from '../../frontend/scroll_helper';
import {SliceSqlId} from '../../trace_processor/sql_utils/core_types';
import {Engine} from '../../trace_processor/engine';
import {LONG, NUM, STR} from '../../trace_processor/query_result';
Expand All @@ -30,6 +26,7 @@ import {
CauseThread,
ScrollJankCauseMap,
} from './scroll_jank_cause_map';
import {scrollTo} from '../../public/scroll_helper';

const UNKNOWN_NAME = 'Unknown';

Expand Down Expand Up @@ -202,10 +199,17 @@ export function getCauseLink(
{
icon: Icons.UpdateSelection,
onclick: () => {
verticalScrollToTrack(trackUris[0], true);
scrollTo({
track: {uri: trackUris[0], expandGroup: true},
});
if (exists(ts) && exists(dur)) {
focusHorizontalRange(ts, Time.fromRaw(ts + dur), 0.3);

scrollTo({
time: {
start: ts,
end: Time.fromRaw(ts + dur),
viewPercentage: 0.3,
},
});
globals.selectionManager.setArea({
start: ts,
end: Time.fromRaw(ts + dur),
Expand Down
7 changes: 5 additions & 2 deletions ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import m from 'mithril';
import {Icons} from '../../base/semantic_icons';
import {duration, time, Time} from '../../base/time';
import {globals} from '../../frontend/globals';
import {scrollToTrackAndTs} from '../../frontend/scroll_helper';
import {SliceSqlId} from '../../trace_processor/sql_utils/core_types';
import {Engine} from '../../trace_processor/engine';
import {LONG, NUM} from '../../trace_processor/query_result';
Expand All @@ -30,6 +29,7 @@ import {
CHROME_EVENT_LATENCY_TRACK_KIND,
SCROLL_JANK_V3_TRACK_KIND,
} from '../../public/track_kinds';
import {scrollTo} from '../../public/scroll_helper';

interface BasicSlice {
// ID of slice.
Expand Down Expand Up @@ -189,7 +189,10 @@ export class ScrollJankSliceRef
detailsPanelConfig: track.detailsPanelConfig,
});

scrollToTrackAndTs(trackUri, vnode.attrs.ts, true);
scrollTo({
track: {uri: trackUri, expandGroup: true},
time: {start: vnode.attrs.ts},
});
},
},
vnode.attrs.name,
Expand Down
4 changes: 1 addition & 3 deletions ui/src/core_plugins/track_utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
globals,
} from '../../frontend/globals';
import {OmniboxMode} from '../../core/omnibox_manager';
import {verticalScrollToTrack} from '../../frontend/scroll_helper';
import {Trace} from '../../public/trace';
import {PromptOption} from '../../public/omnibox';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
Expand Down Expand Up @@ -61,8 +60,7 @@ class TrackUtilsPlugin implements PerfettoPlugin {
sortedOptions,
);
if (selectedUri === undefined) return; // Prompt cancelled.

verticalScrollToTrack(selectedUri, true);
ctx.scrollTo({track: {uri: selectedUri, expandGroup: true}});
const traceTime = globals.traceContext;
globals.selectionManager.setArea({
start: traceTime.start,
Expand Down
22 changes: 11 additions & 11 deletions ui/src/frontend/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import {SearchManagerImpl} from '../core/search_manager';
import {SearchResult} from '../public/search';
import {selectCurrentSearchResult} from './search_handler';
import {WorkspaceManagerImpl} from '../core/workspace_manager';
import {ScrollHelper} from '../core/scroll_helper';
import {setScrollToFunction} from '../public/scroll_helper';

const INSTANT_FOCUS_DURATION = 1n;
const INCOMPLETE_SLICE_DURATION = 30_000n;
Expand Down Expand Up @@ -218,12 +220,6 @@ class Globals {
private _workspaceManager = new WorkspaceManagerImpl();
readonly omnibox = new OmniboxManagerImpl();

// TODO(primiano): this is a hack to work around circular deps in globals.
// This function is injected by scroll_helper.ts. Sort out once globals are no
// more.
verticalScrollToTrack?: (trackUri: string, openGroup?: boolean) => void;

scrollToTrackUri?: string;
httpRpcState: HttpRpcState = {connected: false};
showPanningHint = false;
permalinkHash?: string;
Expand Down Expand Up @@ -261,17 +257,21 @@ class Globals {
// tracks
this._trackManager = new TrackManagerImpl();

const scrollHelper = new ScrollHelper(
traceCtx,
this._timeline,
this._workspaceManager.currentWorkspace,
this._trackManager,
);
setScrollToFunction((args) => scrollHelper.scrollTo(args));

this._searchManager = new SearchManagerImpl({
timeline: this._timeline,
trackManager: this._trackManager,
workspace: this._workspaceManager.currentWorkspace,
engine,
onResultStep: (step: SearchResult) => {
selectCurrentSearchResult(
step,
this._selectionManager,
assertExists(this.verticalScrollToTrack),
);
selectCurrentSearchResult(step, this._selectionManager, scrollHelper);
},
});

Expand Down
18 changes: 9 additions & 9 deletions ui/src/frontend/keyboard_event_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

import {exists} from '../base/utils';
import {Actions} from '../common/actions';
import {scrollTo} from '../public/scroll_helper';
import {Flow, globals} from './globals';
import {focusHorizontalRange, verticalScrollToTrack} from './scroll_helper';

type Direction = 'Forward' | 'Backward';

Expand Down Expand Up @@ -116,16 +116,16 @@ export function moveByFocusedFlow(direction: Direction): void {
}
}

// TODO(primiano): this will be moved to SelectionManager but I need first to
// disentangle some dependencies.
export async function findCurrentSelection() {
const selection = globals.selectionManager.legacySelection;
if (selection === null) return;
if (!exists(selection)) return;

const range = await globals.findTimeRangeOfSelection();
if (exists(range)) {
focusHorizontalRange(range.start, range.end);
}

if (selection.trackUri) {
verticalScrollToTrack(selection.trackUri, true);
}
const trackUri = selection.trackUri;
scrollTo({
time: exists(range) ? {start: range.start, end: range.end} : undefined,
track: exists(trackUri) ? {uri: trackUri, expandGroup: true} : undefined,
});
}
6 changes: 4 additions & 2 deletions ui/src/frontend/post_message_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {showModal} from '../widgets/modal';
import {initCssConstants} from './css_constants';
import {globals} from './globals';
import {toggleHelp} from './help_modal';
import {focusHorizontalRange} from './scroll_helper';
import {scrollTo} from '../public/scroll_helper';

const TRUSTED_ORIGINS_KEY = 'trustedOrigins';

Expand Down Expand Up @@ -282,7 +282,9 @@ async function scrollToTimeRange(
} else {
const start = Time.fromSeconds(postedScrollToRange.timeStart);
const end = Time.fromSeconds(postedScrollToRange.timeEnd);
focusHorizontalRange(start, end, postedScrollToRange.viewPercentage);
scrollTo({
time: {start, end, viewPercentage: postedScrollToRange.viewPercentage},
});
}
}

Expand Down
Loading

0 comments on commit 0b5423d

Please sign in to comment.