Merge "Optimise create_view_function to avoid recreating statements on every iteration"
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index ad91f01..d0e33c4 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -312,8 +312,10 @@
.query-error {
padding: 20px 10px;
color: hsl(-10, 50%, 50%);
- font-family: "Roboto Condensed", sans-serif;
+ font-family: "Roboto Mono", sans-serif;
+ font-size: 12px;
font-weight: 300;
+ white-space: pre;
}
.dropdown {
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 2f525e6..19954e4 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -38,7 +38,13 @@
} from './dragndrop_logic';
import {createEmptyState} from './empty_state';
import {DEFAULT_VIEWING_OPTION, PERF_SAMPLES_KEY} from './flamegraph_util';
-import {traceEventBegin, traceEventEnd, TraceEventScope} from './metatracing';
+import {
+ MetatraceTrackId,
+ traceEvent,
+ traceEventBegin,
+ traceEventEnd,
+ TraceEventScope,
+} from './metatracing';
import {
AdbRecordingTarget,
Area,
@@ -538,7 +544,8 @@
if (statusTraceEvent) {
traceEventEnd(statusTraceEvent);
}
- statusTraceEvent = traceEventBegin(args.msg);
+ statusTraceEvent =
+ traceEventBegin(args.msg, {track: MetatraceTrackId.kOmniboxStatus});
state.status = args;
},
@@ -1061,7 +1068,13 @@
},
setCurrentTab(state: StateDraft, args: {tab: string|undefined}) {
- state.currentTab = args.tab;
+ traceEvent('setCurrentTab', () => {
+ state.currentTab = args.tab;
+ }, {
+ args: {
+ tab: args.tab ?? '<undefined>',
+ },
+ });
},
toggleAllTrackGroups(state: StateDraft, args: {collapsed: boolean}) {
diff --git a/ui/src/common/metatracing.ts b/ui/src/common/metatracing.ts
index c26371d..d1032ce 100644
--- a/ui/src/common/metatracing.ts
+++ b/ui/src/common/metatracing.ts
@@ -19,7 +19,15 @@
import {toNs} from './time';
const METATRACING_BUFFER_SIZE = 100000;
-const JS_THREAD_ID = 2;
+
+export enum MetatraceTrackId {
+ // 1 is reserved for the Trace Processor track.
+ // Events emitted by the JS main thread.
+ kMainThread = 2,
+ // Async track for the status (e.g. "loading tracks") shown to the user
+ // in the omnibox.
+ kOmniboxStatus = 3,
+}
import MetatraceCategories = perfetto.protos.MetatraceCategories;
@@ -67,20 +75,29 @@
eventName: string;
startNs: number;
durNs: number;
+ track: MetatraceTrackId;
+ args?: {[key: string]: string};
}
const traceEvents: TraceEvent[] = [];
function readMetatrace(): Uint8Array {
const eventToPacket = (e: TraceEvent): TracePacket => {
+ const metatraceEvent = PerfettoMetatrace.create({
+ eventName: e.eventName,
+ threadId: e.track,
+ eventDurationNs: e.durNs,
+ });
+ for (const [key, value] of Object.entries(e.args ?? {})) {
+ metatraceEvent.args.push(PerfettoMetatrace.Arg.create({
+ key,
+ value,
+ }));
+ }
return TracePacket.create({
timestamp: e.startNs,
timestampClockId: 1,
- perfettoMetatrace: PerfettoMetatrace.create({
- eventName: e.eventName,
- threadId: JS_THREAD_ID,
- eventDurationNs: e.durNs,
- }),
+ perfettoMetatrace: metatraceEvent,
});
};
const packets: TracePacket[] = [];
@@ -93,8 +110,14 @@
return Trace.encode(trace).finish();
}
+interface TraceEventParams {
+ track?: MetatraceTrackId;
+ args?: {[key: string]: string};
+}
+
export type TraceEventScope = {
- startNs: number, eventName: string;
+ startNs: number; eventName: string;
+ params?: TraceEventParams;
};
const correctedTimeOrigin = new Date().getTime() - performance.now();
@@ -103,10 +126,23 @@
return toNs((correctedTimeOrigin + performance.now()) / 1000);
}
-export function traceEventBegin(eventName: string): TraceEventScope {
+export function traceEvent<T>(
+ name: string, event: () => T, params?: TraceEventParams): T {
+ const scope = traceEventBegin(name, params);
+ try {
+ const result = event();
+ return result;
+ } finally {
+ traceEventEnd(scope);
+ }
+}
+
+export function traceEventBegin(
+ eventName: string, params?: TraceEventParams): TraceEventScope {
return {
eventName,
startNs: now(),
+ params: params,
};
}
@@ -117,6 +153,8 @@
eventName: traceEvent.eventName,
startNs: traceEvent.startNs,
durNs: now() - traceEvent.startNs,
+ track: traceEvent.params?.track ?? MetatraceTrackId.kMainThread,
+ args: traceEvent.params?.args,
});
while (traceEvents.length > METATRACING_BUFFER_SIZE) {
traceEvents.shift();
diff --git a/ui/src/frontend/analyze_page.ts b/ui/src/frontend/analyze_page.ts
index dc0025f..e062888 100644
--- a/ui/src/frontend/analyze_page.ts
+++ b/ui/src/frontend/analyze_page.ts
@@ -65,6 +65,7 @@
globals.rafScheduler.scheduleFullRedraw();
});
}
+ globals.rafScheduler.scheduleDelayedFullRedraw();
}
function getEngine(): EngineProxy|undefined {
diff --git a/ui/src/frontend/bottom_tab.ts b/ui/src/frontend/bottom_tab.ts
index e192969..0e28ba6 100644
--- a/ui/src/frontend/bottom_tab.ts
+++ b/ui/src/frontend/bottom_tab.ts
@@ -17,9 +17,10 @@
import {Actions} from '../common/actions';
import {EngineProxy} from '../common/engine';
+import {traceEvent} from '../common/metatracing';
import {Registry} from '../common/registry';
-import {globals} from './globals';
+import {globals} from './globals';
import {Panel, PanelSize, PanelVNode} from './panel';
export interface NewBottomTabArgs {
@@ -211,23 +212,34 @@
// created panel (which can be used in the future to close it).
addTab(args: AddTabArgs): AddTabResult {
const uuid = uuidv4();
- const newPanel = bottomTabRegistry.get(args.kind).create({
- engine: this.engine,
- uuid,
- config: args.config,
- tag: args.tag,
- });
+ return traceEvent('addTab', () => {
+ const newPanel = bottomTabRegistry.get(args.kind).create({
+ engine: this.engine,
+ uuid,
+ config: args.config,
+ tag: args.tag,
+ });
- this.pendingTabs.push({
- tab: newPanel,
- args,
- startTime: window.performance.now(),
- });
- this.flushPendingTabs();
+ this.pendingTabs.push({
+ tab: newPanel,
+ args,
+ startTime: window.performance.now(),
+ });
+ this.flushPendingTabs();
- return {
- uuid,
- };
+ return {
+ uuid,
+ };
+ }, {
+ args: {
+ 'uuid': uuid,
+ 'kind': args.kind,
+ 'tag': args.tag ?? '<undefined>',
+ 'config': JSON.stringify(
+ args.config,
+ (_, value) => typeof value === 'bigint' ? value.toString() : value),
+ },
+ });
}
closeTabByTag(tag: string) {
@@ -301,19 +313,30 @@
// The first tab is not ready yet, wait.
return;
}
- this.pendingTabs.shift();
- const index =
- args.tag ? this.tabs.findIndex((tab) => tab.tag === args.tag) : -1;
- if (index === -1) {
- this.tabs.push(tab);
- } else {
- this.tabs[index] = tab;
- }
+ traceEvent('addPendingTab', () => {
+ this.pendingTabs.shift();
- if (args.select === undefined || args.select === true) {
- globals.dispatch(Actions.setCurrentTab({tab: tabSelectionKey(tab)}));
- }
+ const index =
+ args.tag ? this.tabs.findIndex((tab) => tab.tag === args.tag) : -1;
+ if (index === -1) {
+ this.tabs.push(tab);
+ } else {
+ this.tabs[index] = tab;
+ }
+
+ if (args.select === undefined || args.select === true) {
+ globals.dispatch(Actions.setCurrentTab({tab: tabSelectionKey(tab)}));
+ }
+ // setCurrentTab will usually schedule a redraw, but not if we replace
+ // the tab with the same tag, so we force an update here.
+ globals.rafScheduler.scheduleFullRedraw();
+ }, {
+ args: {
+ 'uuid': tab.uuid,
+ 'is_loading': tab.isLoading().toString(),
+ },
+ });
}
}
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index ad75cb6..1239ef4 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -19,6 +19,7 @@
import {Actions} from '../common/actions';
import {QueryResponse} from '../common/queries';
import {Row} from '../common/query_result';
+import {formatDurationShort, tpDurationFromNanos} from '../common/time';
import {Anchor} from './anchor';
import {copyToClipboard, queryResponseToClipboard} from './clipboard';
@@ -30,7 +31,6 @@
import {Button} from './widgets/button';
import {Callout} from './widgets/callout';
import {DetailsShell} from './widgets/details_shell';
-import {exists} from './widgets/utils';
interface QueryTableRowAttrs {
row: Row;
@@ -223,11 +223,14 @@
}
renderTitle(resp?: QueryResponse) {
- if (exists(resp)) {
- return `Query result - ${Math.round(resp.durationMs)} ms`;
- } else {
+ if (!resp) {
return 'Query - running';
}
+ const result = resp.error ? 'error' : `${resp.rows.length} rows`;
+ const msToNs = 1e6;
+ const dur =
+ formatDurationShort(tpDurationFromNanos(resp.durationMs * msToNs));
+ return `Query result (${result}) - ${dur}`;
}
renderButtons(
diff --git a/ui/src/frontend/raf_scheduler.ts b/ui/src/frontend/raf_scheduler.ts
index 4571e7e..1c7f0c5 100644
--- a/ui/src/frontend/raf_scheduler.ts
+++ b/ui/src/frontend/raf_scheduler.ts
@@ -109,6 +109,19 @@
this.maybeScheduleAnimationFrame(true);
}
+ // Schedule a full redraw to happen after a short delay (50 ms).
+ // This is done to prevent flickering / visual noise and allow the UI to fetch
+ // the initial data from the Trace Processor.
+ // There is a chance that someone else schedules a full redraw in the
+ // meantime, forcing the flicker, but in practice it works quite well and
+ // avoids a lot of complexity for the callers.
+ scheduleDelayedFullRedraw() {
+ // 50ms is half of the responsiveness threshold (100ms):
+ // https://web.dev/rail/#response-process-events-in-under-50ms
+ const delayMs = 50;
+ setTimeout(() => this.scheduleFullRedraw(), delayMs);
+ }
+
syncDomRedraw(nowMs: number) {
const redrawStart = debugNow();
this._syncDomRedraw(nowMs);
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index d7d1c20..5d0b54b 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -110,10 +110,10 @@
select
row_number() over () as id,
${sliceColumns.ts} as ts,
- cast(${dur} as int) as dur,
+ ifnull(cast(${dur} as int), -1) as dur,
printf('%s', ${sliceColumns.name}) as name
${argColumns.length > 0 ? ',' : ''}
- ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',')}
+ ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
from ${sqlViewName}
)
select