blob: 9332ecb9abad8627de9f5fee927a0fafaa4bfe23 [file] [log] [blame] [view] [edit]
# Perfetto UI Development for AI Agents
Perfetto UI is a Single Page Web Application written in TypeScript using the Mithril framework. It lives in `ui/` and powers ui.perfetto.dev. The UI embeds TraceProcessor via WebAssembly.
## General Principles
- **Don't over-engineer** - Solve the problem at hand, not hypothetical future problems.
- **Prefer simpler approaches** - If there's a simple solution and a complex one, choose simple.
- **Search before creating** - Always search for existing utilities before writing new ones.
- **Be consistent** - Follow the patterns established in the surrounding code.
- **Prefer interfaces with immutable readonly members** - We like immutability, makes the code easier to debug.
## Directory Structure
The UI codebase is organized as follows:
```text
ui/src/
├── base/ # Core utilities (time, color, arrays, logging, disposables)
├── widgets/ # Reusable UI components (Button, Menu, Modal, Popup, etc.)
├── components/ # Higher-level components (aggregation panels, query tables)
├── core/ # Core application logic and managers
├── public/ # Public API surface for plugins
├── plugins/ # Optional third-party/external plugins
├── core_plugins/ # Essential core plugins (cannot be disabled)
├── frontend/ # Main frontend rendering code
├── trace_processor/# Engine communication layer (query results, SQL utilities)
├── test/ # Playwright integration tests
└── assets/ # SCSS stylesheets and static assets
```
When possible (if the API surface allows) feature functionality should be encapsulated in a plugin in src/plugins.
- `core_plugins/` (e.g., `dev.perfetto.CoreCommands`, `dev.perfetto.Notes`) contain essential functionality. They cannot be disabled by users and are always active.
- `plugins/` (e.g., `dev.perfetto.Sched`, `com.android.AndroidStartup`) are optional. Users can enable/disable them via feature flags. These are organized by reverse-DNS naming (e.g., `com.android.*`, `dev.perfetto.*`, `org.chromium.*`).
- This distinction is mostly historical. These days in 90% of cases things can (and should) go only inside plugins/
- Look at /docs/contributing/ui-plugins.md as it has extra useful content for plugin authors.
## Building and Running the UI
To build and serve the UI for development:
```sh
# From the repository root
ui/build # Builds the UI
ui/run-dev-server # Starts the development server with live reload
```
The UI uses:
- **TypeScript** for type safety
- **Mithril** as the UI framework
- **Rollup** for bundling
- **pnpm** for package management
- **ESLint** for linting (based on Google style)
- **Playwright** for integration tests
## Plugin Architecture
Plugins are the primary extension mechanism for the UI. They follow this structure:
```typescript
import {PerfettoPlugin} from '../../public/plugin';
import {Trace} from '../../public/trace';
import {App} from '../../public/app';
export default class MyPlugin implements PerfettoPlugin {
// Unique reverse-DNS identifier
static readonly id = 'com.example.MyPlugin';
// Optional: Human-readable description
static readonly description = 'Does something useful';
// Optional: Declare dependencies on other plugins
static readonly dependencies = [OtherPlugin];
// Called when the plugin is activated (before trace load)
static onActivate(app: App): void {
// Register commands, sidebar items, pages that don't need a trace
}
// Called when a trace is loaded
async onTraceLoad(trace: Trace): Promise<void> {
// Register tracks, tabs, commands that need trace data
// Query the trace processor, add tracks to the workspace
}
}
```
**Plugin Lifecycle:**
1. `onActivate()` - Called when the plugin is enabled, before any trace is loaded. Use for registering global commands, pages, and sidebar items.
2. `onTraceLoad()` - Called when a trace is loaded. Use for registering tracks, tabs, and commands that depend on trace data.
3. `trace.onTraceReady` event - Fired after all plugins have finished `onTraceLoad()`. Use for automations that need all tracks to be available.
**Key APIs available to plugins:**
- `trace.engine` - Run SQL queries against TraceProcessor
- `trace.tracks` - Register and find tracks
- `trace.selection` - Manage selection state
- `trace.commands` - Register commands
- `trace.tabs` - Register tabs in the details panel
- `trace.timeline` - Access timeline state
- `trace.workspace` - Manage the track tree structure
## Mithril Patterns and Best Practices
The UI uses Mithril.js. Follow these patterns:
**Component Structure:**
```typescript
import m from 'mithril';
interface MyComponentAttrs {
readonly value: string;
readonly onChange: (newValue: string) => void;
}
export class MyComponent implements m.ClassComponent<MyComponentAttrs> {
// Local state
private expanded = false;
view({attrs}: m.CVnode<MyComponentAttrs>): m.Children {
return m('.my-component',
m(Button, {label: attrs.value, onclick: () => this.expanded = !this.expanded}),
this.expanded && m('.details', 'Expanded content'),
);
}
}
```
**Mithril Rules:**
- No need to call`m.redraw()` most of the times. We automatically schedules redraws: (1) in Mithril's DOM event handlers; (2) after trace processor queries complete. But NOT after manually registered JS event handlers.
- Use `constructor` for initialization if no DOM access is needed, or `onCreate` if DOM is needed.
- Prefer using the existing widget library (`ui/src/widgets/`) over creating new components.
- Use `readonly` for attrs properties to prevent accidental mutation. We like things to be immutable.
**Conditional Rendering with State Preservation:**
Use the `Gate` component when you need to conditionally show/hide content while preserving component state:
```typescript
import {Gate} from '../base/mithril_utils';
m(Gate, {open: this.isVisible}, m(ExpensiveComponent));
```
### Widget Library
The `ui/src/widgets/` directory contains reusable components. Always check here before creating new UI elements:
- `Button`, `ButtonBar`, `ButtonGroup` - Various button styles
- `PopupMenu`, `Menu`, `MenuItem`, `MenuDivider` - Dropdown menus
- `Popup` - Floating popup containers
- `Modal` - Modal dialogs
- `TextInput`, `Select`, `Checkbox`, `Switch` - Form controls
- `Tree` - Tree view component
- `DataGrid` - Tabular data grid component
- `Tabs` - Tabbed interface
- `Spinner` - Loading indicator
- `EmptyState` - Empty state placeholder
**Using Widgets:**
```typescript
import {Button, ButtonVariant} from '../widgets/button';
import {Popup} from '../widgets/popup';
m(Button, {
label: 'Click me',
icon: 'search',
variant: ButtonVariant.Filled,
onclick: () => { /* handle click */ },
});
```
## TypeScript Code Style
Follow these guidelines for TypeScript code:
- **Avoid `any` as much as you can**: Use `@typescript-eslint/no-explicit-any` rule if you really need it. In most cases it's enough to use `unknown` and type guards instead.
- **Unused variables**: Prefix with underscore (`_unused`) to satisfy `@typescript-eslint/no-unused-vars`.
- **Strict boolean expressions**: Don't use numbers or strings in boolean contexts implicitly.
- **Readonly by default**: Use `readonly` for interface properties and function parameters.
- **Use existing utilities**: Check `ui/src/base/` for utilities before writing your own:
- `time.ts`, `duration.ts` - Time handling
- `logging.ts` - `assertTrue()`, `assertExists()`, `assertFalse()`
- `disposable_stack.ts` - Resource cleanup
- `deferred.ts` - Promise utilities
- `string_utils.ts` - String manipulation
- `array_utils.ts` - Array helpers
## Working with TraceProcessor
Plugins query data using SQL through the TraceProcessor engine:
```typescript
async onTraceLoad(trace: Trace): Promise<void> {
const result = await trace.engine.query(`
SELECT ts, dur, name
FROM slice
WHERE name LIKE '%mySlice%'
LIMIT 100
`);
// Use typed iteration
const iter = result.iter({
ts: LONG, // bigint
dur: LONG, // bigint
name: STR, // string
});
for (; iter.valid(); iter.next()) {
console.log(iter.ts, iter.dur, iter.name);
}
}
```
## Track creation
Rarely you need to create a new Track from scrach.
In most cases you can use higher level components in ui/src/components/tracks/, especially DatasetSliceTrack (examples in /docs/contributing/ui-plugins.md).
Look at those examples first and keep creating a track via trace.tracks.registerTrack as a last-resort.
## CSS/SCSS Conventions
Stylesheets live in `ui/src/assets/` and component-specific `.scss` files alongside components.
- Use the `pf-` prefix for all CSS classes (Perfetto namespace)
- Follow BEM-like naming: `.pf-component`, `.pf-component__element`, `.pf-component--modifier`
- Use CSS custom properties (variables) defined in `theme_provider.scss` for colors
- Support both light and dark themes using semantic color variables
## Common Pitfalls to Avoid
1. **Don't create new widgets without checking existing ones** - The widget library is comprehensive.
2. **Try to use the Trace object as much as possible** - Plumb the Trace object through the hierarchy wherever needed.
## Code Review Pet Peeves and Style Preferences
The following patterns are consistently enforced during code review. Adhering to these will significantly speed up the review process.
### TypeScript/JavaScript Style
**Prefer `undefined` over `null`:**
```typescript
// Bad
function getValue(): string | null { return null; }
// Good
function getValue(): string | undefined { return undefined; }
```
**Use `ReadonlyArray<T>` for arrays that shouldn't be modified:**
```typescript
// Bad
function process(items: string[]): void { ... }
// Good
function process(items: ReadonlyArray<string>): void { ... }
```
**Use `classNames()` utility for building CSS class strings:**
```typescript
import {classNames} from '../base/classnames';
// Bad
const cls = 'pf-row' + (isSelected ? ' pf-row--selected' : '') + (isDisabled ? ' pf-row--disabled' : '');
// Good
const cls = classNames('pf-row', isSelected && 'pf-row--selected', isDisabled && 'pf-row--disabled');
```
**Use `assertUnreachable()` in switch default cases:**
```typescript
import {assertUnreachable} from '../base/logging';
switch (value) {
case 'a': return handleA();
case 'b': return handleB();
default:
assertUnreachable(value); // TypeScript will error if cases aren't exhaustive
}
```
**Variables should be camelCase:**
```typescript
// Bad
const trace_processor_id = 123;
// Good
const traceProcessorId = 123;
```
### CSS/SCSS Style
**Never use inline styles - use stylesheets:**
```typescript
// Bad
m('div', {style: {color: 'red', padding: '10px'}}, 'content')
// Good
m('.pf-my-component', 'content') // with styles in .scss file
```
**All CSS classes must have the `pf-` prefix:**
```scss
// Bad
.my-component { ... }
.row { ... }
// Good
.pf-my-component { ... }
.pf-my-component__row { ... }
```
**Never hard-code colors - use theme variables:**
```scss
// Bad
.pf-my-component {
color: #333;
background: white;
}
// Good
.pf-my-component {
color: var(--pf-color-foreground);
background: var(--pf-color-background);
}
```
### Mithril-Specific Rules
**Don't use `oncreate`/lifecycle hooks for things that can be done in `view()`:**
```typescript
// Bad - splitting code across lifecycle methods hurts readability.
oncreate() {
this.computedValue = inexpensiveComputation();
}
// Good - compute in view. If expensive initialize in the constructor.
view() {
const computedValue = inexpensiveComputation();
return m('div', computedValue);
}
```
### Widget Usage
**Use the `Anchor` widget for links:**
```typescript
import {Anchor} from '../widgets/anchor';
import {Icons} from '../widgets/icons';
// Bad
m('a', {href: 'https://example.com', target: '_blank'}, 'Link')
// Good
m(Anchor, {href: 'https://example.com', icon: Icons.ExternalLink}, 'Link')
```
### Naming Conventions
**Settings/flags should use reverse-DNS format:**
```typescript
// Bad
const settingId = 'trackHeightMinPx';
// Good
const settingId = 'dev.perfetto.TrackHeightMinPx';
```
**Command IDs should be descriptive but omit redundant plugin name:**
```typescript
// Bad (if plugin is com.android.OrganizeNestedTracks)
const commandId = 'com.android.OrganizeNestedTracks#organizeNestedTracks';
// Good
const commandId = 'com.android.OrganizeNestedTracks';
```
**Copyright years should be current when creating new files:**
But don't touch years when editing existing files.
```typescript
// Bad (if current year is 2025)
// Copyright (C) 2024 The Android Open Source Project
// Good
// Copyright (C) 2025 The Android Open Source Project
```
## Testing
**Use Zod for parsing objects of unknown types:**
```typescript
import {z} from 'zod';
// Bad - unsafe type assertion
const config = JSON.parse(data) as MyConfig;
// Good - validated parsing
const ConfigSchema = z.object({
name: z.string(),
value: z.number(),
});
const config = ConfigSchema.parse(JSON.parse(data));
```
### UI Unit Tests
Unit tests are run with:
```sh
$ui/run-unittests
```
TypeScript unit tests follow the pattern `*_unittest.ts` and use Jest.
### UI Integration Tests
Integration tests use Playwright:
```sh
ui/run-integrationtests
```