perfetto-ui: Preperation for adding counter details
A couple of preparatory changes for adding details for counters:
- Fix a bug where drag_gesture_handler missed out of window mouseup
and ended up in a bad state.
- Optimise counter rendering for the common case where counter value does
not change.
- Highlight location of data point on counter track.
- Add a resizeable details window.
Change-Id: I9784f92e79fc812903bc1a235c76d66b1cf20010
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index ebb2eed..0c95dfe 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -126,6 +126,8 @@
.page {
grid-area: page;
position: relative;
+ display: flex;
+ flex-direction: column
}
.alerts {
@@ -152,6 +154,7 @@
.home-page {
text-align: center;
padding-top: 20vh;
+ align-items: center;
}
.home-page .logo {
@@ -327,12 +330,24 @@
}
.pan-and-zoom-content {
- height: 100%;
+ flex: 1;
position: relative;
display: flex;
flex-flow: column nowrap;
}
+.details-content {
+ .handle {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAFCAYAAACeuGYRAAAAFUlEQVR42mPYvXv3f1IxA1lgcNsEAEynfFEloq9lAAAAAElFTkSuQmCC) 0 0 no-repeat; // http://
+ background-color: #fafafa;
+ background-position: center center;
+ border: 1px solid rgba(0,0,0,0.1);
+ cursor: row-resize;
+ height: 12px;
+ min-height: 12px;
+ }
+}
+
.overview-timeline {
height: 100px;
}
diff --git a/ui/src/base/binary_search.ts b/ui/src/base/binary_search.ts
new file mode 100644
index 0000000..e81a4a7
--- /dev/null
+++ b/ui/src/base/binary_search.ts
@@ -0,0 +1,49 @@
+// Copyright (C) 2018 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.
+
+
+function searchImpl(
+ haystack: Float64Array, needle: number, i: number, j: number): number {
+ if (i === j) return -1;
+ if (i + 1 === j) {
+ return (needle >= haystack[i]) ? i : -1;
+ }
+
+ const mid = Math.floor((j - i) / 2) + i;
+ const midValue = haystack[mid];
+ if (needle < midValue) {
+ return searchImpl(haystack, needle, i, mid);
+ } else {
+ return searchImpl(haystack, needle, mid, j);
+ }
+}
+
+export function search(haystack: Float64Array, needle: number): number {
+ return searchImpl(haystack, needle, 0, haystack.length);
+}
+
+
+export function searchSegment(
+ haystack: Float64Array, needle: number): [number, number] {
+ if (!haystack.length) return [-1, -1];
+
+ const left = search(haystack, needle);
+ if (left === -1) {
+ return [left, 0];
+ } else if (left + 1 === haystack.length) {
+ return [left, -1];
+ } else {
+ return [left, left + 1];
+ }
+}
diff --git a/ui/src/base/binary_search_unittest.ts b/ui/src/base/binary_search_unittest.ts
new file mode 100644
index 0000000..ee653ae
--- /dev/null
+++ b/ui/src/base/binary_search_unittest.ts
@@ -0,0 +1,40 @@
+// Copyright (C) 2018 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 {search, searchSegment} from './binary_search';
+
+test('binarySearch', () => {
+ expect(search(Float64Array.of(), 100)).toEqual(-1);
+ expect(search(Float64Array.of(42), 42)).toEqual(0);
+ expect(search(Float64Array.of(42), 43)).toEqual(0);
+ expect(search(Float64Array.of(42), 41)).toEqual(-1);
+ expect(search(Float64Array.of(42, 43), 42)).toEqual(0);
+ expect(search(Float64Array.of(42, 43), 43)).toEqual(1);
+ expect(search(Float64Array.of(42, 43), 44)).toEqual(1);
+ expect(search(Float64Array.of(42, 43, 44), 41)).toEqual(-1);
+ expect(search(Float64Array.of(42, 43, 44), 42)).toEqual(0);
+ expect(search(Float64Array.of(42, 43, 44), 43)).toEqual(1);
+ expect(search(Float64Array.of(42, 43, 44), 44)).toEqual(2);
+ expect(search(Float64Array.of(42, 43, 44), 45)).toEqual(2);
+});
+
+test('searchSegment', () => {
+ expect(searchSegment(Float64Array.of(), 100)).toEqual([-1, -1]);
+
+ expect(searchSegment(Float64Array.of(42), 41)).toEqual([-1, 0]);
+ expect(searchSegment(Float64Array.of(42), 42)).toEqual([0, -1]);
+ expect(searchSegment(Float64Array.of(42), 43)).toEqual([0, -1]);
+
+ expect(searchSegment(Float64Array.of(42, 44), 42)).toEqual([0, 1]);
+});
diff --git a/ui/src/frontend/drag_gesture_handler.ts b/ui/src/frontend/drag_gesture_handler.ts
index 6971d69..8171b18 100644
--- a/ui/src/frontend/drag_gesture_handler.ts
+++ b/ui/src/frontend/drag_gesture_handler.ts
@@ -38,6 +38,9 @@
}
private onMouseMove(e: MouseEvent) {
+ if (e.buttons === 0) {
+ return this.onMouseUp(e);
+ }
this.onDrag(
e.clientX - this.clientRect!.left, e.clientY - this.clientRect!.top);
e.stopPropagation();
@@ -49,4 +52,4 @@
this.onDragFinished();
e.stopPropagation();
}
-}
\ No newline at end of file
+}
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index d4ac2ec..50af60e 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -162,9 +162,9 @@
}
private updateCanvasDimensions() {
- this.canvasHeight = this.attrs.doesScroll ?
- this.parentHeight * this.canvasOverdrawFactor :
- this.totalPanelHeight;
+ this.canvasHeight = Math.floor(
+ this.attrs.doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
+ this.totalPanelHeight);
const ctx = assertExists(this.ctx);
const canvas = assertExists(ctx.canvas);
canvas.style.height = `${this.canvasHeight}px`;
@@ -176,7 +176,8 @@
private repositionCanvas() {
const canvas = assertExists(assertExists(this.ctx).canvas);
- const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
+ const canvasYStart =
+ Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
canvas.style.transform = `translateY(${canvasYStart}px)`;
}
@@ -222,7 +223,7 @@
if (!this.ctx) return;
this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
const canvasYStart =
- Math.ceil(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
+ Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
let panelYStart = 0;
const panels = assertExists(this.attrs).panels;
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 01ee096..de19a83 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -130,7 +130,7 @@
attrs.track.onMouseOut();
globals.rafScheduler.scheduleRedraw();
},
- }, );
+ });
}
}
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 06ab0ff..2da695e 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -18,6 +18,7 @@
import {TimeSpan} from '../common/time';
import {copyToClipboard} from './clipboard';
+import {DragGestureHandler} from './drag_gesture_handler';
import {globals} from './globals';
import {HeaderPanel} from './header_panel';
import {OverviewTimelinePanel} from './overview_timeline_panel';
@@ -30,8 +31,8 @@
import {TRACK_SHELL_WIDTH} from './track_panel';
import {TrackPanel} from './track_panel';
-
const MAX_ZOOM_SPAN_SEC = 1e-4; // 0.1 ms.
+const DRAG_HANDLE_HEIGHT_PX = 12;
class QueryTable extends Panel {
view() {
@@ -85,6 +86,51 @@
renderCanvas() {}
}
+interface DragHandleAttrs {
+ height: number;
+ resize: (height: number) => void;
+}
+
+class DragHandle implements m.ClassComponent<DragHandleAttrs> {
+ private dragStartHeight = 0;
+ private height = 0;
+ private resize: undefined|((height: number) => void);
+
+ oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
+ this.resize = attrs.resize;
+ this.height = attrs.height;
+ const elem = dom as HTMLElement;
+ new DragGestureHandler(
+ elem,
+ this.onDrag.bind(this),
+ this.onDragStart.bind(this),
+ this.onDragEnd.bind(this));
+ }
+
+ onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
+ this.resize = attrs.resize;
+ this.height = attrs.height;
+ }
+
+ onDrag(_x: number, y: number) {
+ if (this.resize) {
+ const newHeight = this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y;
+ this.resize(Math.floor(newHeight));
+ }
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+
+ onDragStart(_x: number, _y: number) {
+ this.dragStartHeight = this.height;
+ }
+
+ onDragEnd() {}
+
+ view() {
+ return m('.handle');
+ }
+}
+
/**
* Top-most level component for the viewer page. Holds tracks, brush timeline,
* panels, and everything else that's part of the main trace viewer page.
@@ -92,6 +138,7 @@
class TraceViewer implements m.ClassComponent {
private onResize: () => void = () => {};
private zoomContent?: PanAndZoomHandler;
+ private detailsHeight = 100;
oncreate(vnode: m.CVnodeDOM) {
const frontendLocalState = globals.frontendLocalState;
@@ -194,7 +241,15 @@
m('.scrolling-panel-container', m(PanelContainer, {
doesScroll: true,
panels: scrollingPanels,
- }))));
+ }))),
+ m('.details-content',
+ {style: {height: `${this.detailsHeight}px`}},
+ m(DragHandle, {
+ resize: (height: number) => {
+ this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX);
+ },
+ height: this.detailsHeight,
+ })));
}
}
diff --git a/ui/src/tracks/counter/frontend.ts b/ui/src/tracks/counter/frontend.ts
index 88fe587..912b4f1 100644
--- a/ui/src/tracks/counter/frontend.ts
+++ b/ui/src/tracks/counter/frontend.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {searchSegment} from '../../base/binary_search';
import {assertTrue} from '../../base/logging';
import {Actions} from '../../common/actions';
import {TrackState} from '../../common/state';
@@ -45,6 +46,8 @@
private reqPending = false;
private mouseXpos = 0;
private hoveredValue: number|undefined = undefined;
+ private hoveredTs: number|undefined = undefined;
+ private hoveredTsEnd: number|undefined = undefined;
constructor(trackState: TrackState) {
super(trackState);
@@ -121,13 +124,13 @@
for (let i = 0; i < data.values.length; i++) {
const value = data.values[i];
const startTime = data.timestamps[i];
+ const nextY = zeroY - Math.round((value / yRange) * RECT_HEIGHT);
+ if (nextY === lastY) continue;
lastX = Math.floor(timeScale.timeToPx(startTime));
ctx.lineTo(lastX, lastY);
-
- const height = Math.round((value / yRange) * RECT_HEIGHT);
- lastY = zeroY - height;
- ctx.lineTo(lastX, lastY);
+ ctx.lineTo(lastX, nextY);
+ lastY = nextY;
}
ctx.lineTo(endPx, lastY);
ctx.lineTo(endPx, zeroY);
@@ -143,20 +146,38 @@
ctx.lineTo(endPx, zeroY);
ctx.closePath();
ctx.stroke();
+ ctx.setLineDash([]);
ctx.font = '10px Google Sans';
- if (this.hoveredValue !== undefined) {
- // Draw a vertical bar to highlight the mouse cursor.
- ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
- const height = Math.round(RECT_HEIGHT * this.hoveredValue / yMax);
- ctx.fillRect(
- this.mouseXpos, MARGIN_TOP + RECT_HEIGHT - height, 2, height);
-
+ if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
// TODO(hjd): Add units.
const text = `value: ${this.hoveredValue.toLocaleString()}`;
const width = ctx.measureText(text).width;
+ ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
+ ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
+
+ const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
+ const xEnd = this.hoveredTsEnd === undefined ?
+ endPx :
+ Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
+ const y = zeroY - Math.round((this.hoveredValue / yRange) * RECT_HEIGHT);
+
+ // Highlight line.
+ ctx.beginPath();
+ ctx.moveTo(xStart, y);
+ ctx.lineTo(xEnd, y);
+ ctx.lineWidth = 3;
+ ctx.stroke();
+ ctx.lineWidth = 1;
+
+ // Draw change marker.
+ ctx.beginPath();
+ ctx.arc(xStart, y, 3 /*r*/, 0 /*start angle*/, 2 * Math.PI /*end angle*/);
+ ctx.fill();
+ ctx.stroke();
+
// Draw the tooltip.
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fillRect(this.mouseXpos + 5, MARGIN_TOP, width + 16, RECT_HEIGHT);
@@ -188,15 +209,22 @@
this.mouseXpos = x;
const {timeScale} = globals.frontendLocalState;
const time = timeScale.pxToTime(x);
- this.hoveredValue = undefined;
- for (let i = 0; i < data.values.length; i++) {
- if (data.timestamps[i] > time) break;
- this.hoveredValue = data.values[i];
- }
+
+ const [left, right] = searchSegment(data.timestamps, time);
+ this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
+ this.hoveredTsEnd = right === -1 ? undefined : data.timestamps[right];
+ this.hoveredValue = left === -1 ? undefined : data.values[left];
+
+ // for (let i = 0; i < data.values.length; i++) {
+ // if (data.timestamps[i] > time) break;
+ // this.hoveredTs = data.timestamps[i];
+ // this.hoveredValue = data.values[i];
+ //}
}
onMouseOut() {
this.hoveredValue = undefined;
+ this.hoveredTs = undefined;
}
}