Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0453f01
brush: Add first prototype
gignat-dev Mar 5, 2026
4cf7ebf
nit: format
gignat-dev Mar 5, 2026
6bacad2
add trace limit setting
gignat-dev Mar 5, 2026
bfe7db1
use TextInput
gignat-dev Mar 5, 2026
ab5a586
sidebar a bit more narrower
gignat-dev Mar 5, 2026
8a8f497
basic recent queries section
gignat-dev Mar 5, 2026
7d1d962
improve home page
gignat-dev Mar 5, 2026
4f54384
improve storage
gignat-dev Mar 5, 2026
2d1983a
resize query editor
gignat-dev Mar 5, 2026
cd48bc1
add /core/local_storage to bigtrace dependencies
gignat-dev Mar 5, 2026
677fc79
ui: Clean up and reorganize BigTrace code
gignat-dev Mar 24, 2026
1b1282d
cleanup
gignat-dev Mar 24, 2026
bea3215
cleanup
gignat-dev Mar 24, 2026
34d859a
nit
gignat-dev Mar 24, 2026
7dbff5e
format
gignat-dev Mar 24, 2026
a25a1c4
use mithril routing
gignat-dev Mar 24, 2026
73a0790
linkify the link column
gignat-dev Mar 24, 2026
46a71f6
renamed files from manager to storage
gignat-dev Mar 25, 2026
418826b
how to get started card make them clickable
gignat-dev Mar 25, 2026
ac09c76
change default endpoint to brush-googleapis
gignat-dev Mar 25, 2026
b1bb7ad
Merge remote-tracking branch 'origin/main' into brush_v0_clean
gignat-dev Mar 25, 2026
5a5544a
fix styles
gignat-dev Mar 25, 2026
607e7ad
Merge origin/main into brush_v0_clean
gignat-dev Mar 26, 2026
ee5a4e9
bigtrace-ui: cleanup
gignat-dev Mar 26, 2026
0872a1e
bt-ui: overall cleanup + query tabs
gignat-dev Mar 27, 2026
1e26063
add execution time
gignat-dev Mar 29, 2026
eb1bed4
save result limit in storage
gignat-dev Mar 29, 2026
2eeafe2
add time on failure
gignat-dev Mar 29, 2026
2cd2f45
add help shortcut
gignat-dev Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions python/tools/check_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,14 @@
),

# Bigtrace deps.
('/bigtrace/*', ['/base/*', '/widgets/*', '/trace_processor/*']),
('/bigtrace/*', [
'/bigtrace/*', '/base/*', '/widgets/*', '/trace_processor/*',
'/components/*', '/public/*', '/frontend/theme_provider',
'/core/live_reload', '/core/raf_scheduler', '/core/local_storage'
]),

# TODO(primiano): misc tech debt.
('/public/lib/extensions', '/frontend/*'),
('/bigtrace/index', ['/core/live_reload', '/core/raf_scheduler']),
('/plugins/dev.perfetto.HeapProfile/*', '/frontend/trace_converter'),
]

Expand Down
1 change: 0 additions & 1 deletion ui/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# NOTE: eslint.config.js and ui/format-sources also depend on this file.
node_modules
src/service_worker
src/bigtrace
src/gen
src/test/diff_viewer
out
Expand Down
174 changes: 160 additions & 14 deletions ui/src/bigtrace/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 The Android Open Source Project
// Copyright (C) 2026 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.
Expand All @@ -17,8 +17,17 @@ import '../base/static_initializers';
import m from 'mithril';
import {defer} from '../base/deferred';
import {reportError, addErrorHandler, ErrorDetails} from '../base/logging';
import {initLiveReloadIfLocalhost} from '../core/live_reload';
import {raf} from '../core/raf_scheduler';
import {initLiveReload} from '../core/live_reload';
import {settingsManager} from './settings/settings_manager';
import {ThemeProvider} from '../frontend/theme_provider';
import {OverlayContainer} from '../widgets/overlay_container';
import {QueryPage} from './pages/query_page';
import {HomePage} from './pages/home_page';
import {bigTraceSettingsManager} from './settings/bigtrace_settings_manager';
import {queryState} from './query/query_state';
import {SettingsPage} from './pages/settings_page';
import {Topbar} from './layout/topbar';
import {Sidebar, SidebarMenuItem, SIDEBAR_SECTIONS} from './layout/sidebar';

function getRoot() {
// Works out the root directory where the content should be served from
Expand All @@ -41,10 +50,18 @@ function setupContentSecurityPolicy() {
'default-src': [`'self'`],
'script-src': [`'self'`],
'object-src': ['none'],
'connect-src': [`'self'`],
'connect-src': [
`'self'`,
'https://autopush-brush-googleapis.corp.google.com',
'https://brush-googleapis.corp.google.com',
'http://127.0.0.1:5001',
],
'img-src': [`'self'`, 'data:', 'blob:'],
'style-src': [`'self'`],
'navigate-to': ['https://*.perfetto.dev', 'self'],
'style-src': [
`'self'`,
`'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='`,
`'sha256-yRQRG6LLKMvjvigtzXD1f8VRZSYY7J8fM2ZLfdMaHKg='`,
],
};
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
Expand All @@ -57,7 +74,16 @@ function setupContentSecurityPolicy() {
}

function main() {
// Unregister service workers
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (const registration of registrations) {
registration.unregister();
}
});
}
setupContentSecurityPolicy();
// Settings will be lazy-loaded by the UI components that require them.

// Load the css. The load is asynchronous and the CSS is not ready by the time
// appendChild returns.
Expand All @@ -68,8 +94,10 @@ function main() {
css.href = root + 'perfetto.css';
css.onload = () => cssLoadPromise.resolve();
css.onerror = (err) => cssLoadPromise.reject(err);
const favicon = document.head.querySelector('#favicon') as HTMLLinkElement;
if (favicon) favicon.href = root + 'assets/favicon.png';
const favicon = document.head.querySelector('#favicon');
if (favicon instanceof HTMLLinkElement) {
favicon.href = root + 'assets/favicon.png';
}

document.head.append(css);

Expand All @@ -90,16 +118,134 @@ function main() {
cssLoadPromise.then(() => onCssLoaded());
}

class BigTraceApp implements m.ClassComponent {
private sidebarVisible = true;

oninit() {
bigTraceSettingsManager.loadSettings();
}

view(vnode: m.Vnode) {
const currentRoute = m.route.get();

const items: SidebarMenuItem[] = [
{
section: 'home',
text: 'Home',
href: '#!/',
icon: 'home',
active: currentRoute === '/' || currentRoute === '',
onclick: () => {},
},
{
section: 'query',
text: 'Query Editor',
href: '#!/query',
icon: 'line_style',
active: currentRoute === '/query',
onclick: () => {},
},
{
section: 'settings',
text: 'Settings',
href: '#!/settings',
icon: 'settings',
active: currentRoute === '/settings',
onclick: () => {},
},
];

const currentItem = items.find((item) => item.active);
const title = currentItem
? `${SIDEBAR_SECTIONS[currentItem.section].title} > ${currentItem.text}`
: '';

return m(
'.pf-ui-main',
{
style: {
display: 'flex',
height: '100vh',
overflow: 'hidden',
},
},
[
// Left Sidebar (only render when visible)
this.sidebarVisible &&
m(Sidebar, {
items,
onToggleSidebar: () => {
this.sidebarVisible = !this.sidebarVisible;
},
}),

m(
'.pf-main-content',
{
style: {
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: 'hidden',
},
},
[
m(Topbar, {
sidebarVisible: this.sidebarVisible,
onToggleSidebar: () => {
this.sidebarVisible = !this.sidebarVisible;
},
title: title,
}),
vnode.children,
],
),
],
);
}
}

const BigTraceLayout: m.Component = {
view(vnode) {
const theme = settingsManager.get('theme');
const themeValue = theme ? theme.get() : 'light';
return m(ThemeProvider, {theme: themeValue as 'dark' | 'light'}, [
m(OverlayContainer, {fillHeight: true}, [m(BigTraceApp, vnode.children)]),
]);
},
};

function onCssLoaded() {
// Clear all the contents of the initial page (e.g. the <pre> error message)
// And replace it with the root <main> element which will be used by mithril.
// Clear all the contents of the initial page
document.body.innerHTML = '';

raf.domRedraw = () => {
m.render(document.body, m('div'));
};
m.route.prefix = '#!';
m.route(document.body, '/', {
'/': {
render: () => m(BigTraceLayout, m(HomePage)),
},
'/query': {
onmatch: () => {
const initialQuery = queryState.initialQuery;
queryState.initialQuery = undefined;
return {
view: () =>
m(
BigTraceLayout,
m(QueryPage, {
useBrushBackend: true,
initialQuery,
}),
),
};
},
},
'/settings': {
render: () => m(BigTraceLayout, m(SettingsPage)),
},
});

initLiveReloadIfLocalhost(false);
initLiveReload();
}

main();
142 changes: 142 additions & 0 deletions ui/src/bigtrace/layout/sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (C) 2026 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 m from 'mithril';
import {Icon} from '../../widgets/icon';
import {Button} from '../../widgets/button';
import {getOrCreate} from '../../base/utils';

export const SIDEBAR_SECTIONS = {
home: {
title: 'Home',
summary: '',
defaultCollapsed: false,
},
query: {
title: 'BigTrace',
summary: 'Query and analyze large traces',
defaultCollapsed: false,
},
settings: {
title: 'Settings',
summary: 'Customize your BigTrace experience',
defaultCollapsed: false,
},
} as const;

export type SidebarSections = keyof typeof SIDEBAR_SECTIONS;

export type SidebarMenuItem = {
readonly section: SidebarSections;
readonly text: string;
readonly href: string;
readonly icon: string;
readonly active: boolean;
readonly onclick: () => void;
};

export interface SidebarAttrs {
items: SidebarMenuItem[];
onToggleSidebar: () => void;
}

export class Sidebar implements m.ClassComponent<SidebarAttrs> {
private _sectionExpanded = new Map<string, boolean>();

view({attrs}: m.CVnode<SidebarAttrs>) {
return m(
'nav.pf-sidebar',
{
style: {
width: '200px',
flexShrink: 0,
},
},
[
m(
'header',
{
style: {
padding: '16px',
borderBottom: '1px solid var(--pf-color-border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
},
[
m('h1', {style: {margin: 0, fontSize: '18px'}}, 'BigTrace'),
m(Button, {
icon: 'menu',
onclick: attrs.onToggleSidebar,
}),
],
),
m(
'.pf-sidebar__scroll',
m(
'.pf-sidebar__scroll-container',
Object.keys(SIDEBAR_SECTIONS).map((sectionId) =>
this.renderSection(sectionId as SidebarSections, attrs.items),
),
),
),
],
);
}

private renderSection(sectionId: SidebarSections, items: SidebarMenuItem[]) {
const section = SIDEBAR_SECTIONS[sectionId];
const menuItems = items
.filter((item) => item.section === sectionId)
.map((item) => this.renderItem(item));

if (menuItems.length === 0) return undefined;

const expanded = getOrCreate(
this._sectionExpanded,
sectionId,
() => !section.defaultCollapsed,
);

return m(
`section${expanded ? '.pf-sidebar__section--expanded' : ''}`,
m(
'.pf-sidebar__section-header',
{
onclick: () => {
this._sectionExpanded.set(sectionId, !expanded);
},
},
m('h1', {title: section.title}, section.title),
),
m('.pf-sidebar__section-content', m('ul', menuItems)),
);
}

private renderItem(item: SidebarMenuItem) {
return m(
'li',
m(
'a',
{
class: item.active ? 'active' : '',
onclick: item.onclick,
href: item.href,
},
[m(Icon, {icon: item.icon}), item.text],
),
);
}
}
Loading
Loading