diff --git a/docs-developer/markers.md b/docs-developer/markers.md index d78e6f508a..bdb19394bc 100644 --- a/docs-developer/markers.md +++ b/docs-developer/markers.md @@ -1,30 +1,354 @@ # Markers -This section is a work-in-progress. Markers are events that happen within the browser. Every marker that is generated is recorded (they are not sample-based), and thus can give a much more accurate picture into what is going on in the browser. Markers consist of either a single point in time, or a start and end time (referred to as tracing markers). They have a name and other data associated with them. Currently the structure of markers is free-form, and they vary in form from implementation to implementation across Gecko. Markers allow for engineers to arbitrarily instrument their own code with useful information for their specific domain. +Markers are events that happen within the browser or application being profiled. Unlike samples (which are taken at regular intervals), every marker that is generated is recorded, providing an accurate picture of specific events. Markers can represent: -## Implementing new markers +- **Instant events**: A single point in time (e.g., a DOM event firing) +- **Interval events**: Events with a start and end time (e.g., a paint operation) -Markers are implemented in [ProfilerMarker.h], [ProfilerMarkerPayload.h] and -[ProfilerMarkerPayload.cpp]. -Markers are added to a profile with the [profiler_add_marker] function. -It is declared in [GeckoProfiler.h]. -Markers without a payload only have a single start time. -Markers with a payload can have a start and end time, as well as additional -information. -The payloads are defined in [ProfilerMarkerPayload.h] and -[ProfilerMarkerPayload.cpp]. +Markers have a name, timing information, and optional structured data (payload) associated with them. They allow engineers to instrument their code with domain-specific information. -[ProfilerMarker.h]: http://searchfox.org/mozilla-central/source/tools/profiler/core/ProfilerMarker.h -[ProfilerMarkerPayload.h]: http://searchfox.org/mozilla-central/source/tools/profiler/public/ProfilerMarkerPayload.h -[ProfilerMarkerPayload.cpp]: http://searchfox.org/mozilla-central/source/tools/profiler/core/ProfilerMarkerPayload.cpp -[GeckoProfiler.h]: http://searchfox.org/mozilla-central/source/tools/profiler/public/GeckoProfiler.h -[profiler_add_marker]: http://searchfox.org/mozilla-central/rev/5e1e8d2f244bd8c210a578ff1f65c3b720efe34e/tools/profiler/public/GeckoProfiler.h#368-378 +## Marker Schemas -## Marker Chart +Marker schemas define how markers are displayed in Firefox Profiler's UI. They specify: +- Which fields from the marker payload to display +- How to format those fields +- Where to show the markers (timeline, marker chart, marker table, etc.) +- Custom labels and descriptions -Coming soon +### Basic Schema Structure -# Marker definitions - WIP +```typescript +MarkerSchema { + // Required fields + name: string // Unique identifier matching the marker name + display: MarkerDisplayLocation[] // Where to show this marker type + fields: MarkerSchemaField[] // Fields to display from marker data + + // Optional display customization + tooltipLabel?: string // Label for tooltips (defaults to name) + tableLabel?: string // Label for marker table (defaults to name) + chartLabel?: string // Label for marker chart (defaults to name) + description?: string // User-facing description + + // Advanced features + graphs?: MarkerGraph[] // Create custom local tracks + colorField?: string // Key of field containing marker color + isStackBased?: boolean // True if markers are well-nested +} +``` + +### Example: Network Marker Schema + +```typescript +{ + name: "Network", + display: ["marker-chart", "marker-table", "timeline-overview"], + tooltipLabel: "Network Request", + tableLabel: "{marker.data.URI}", + chartLabel: "{marker.data.requestMethod} {marker.data.URI}", + fields: [ + { key: "URI", label: "URL", format: "url" }, + { key: "requestMethod", label: "Method", format: "string" }, + { key: "responseStatus", label: "Status", format: "integer" }, + { key: "contentType", label: "Content Type", format: "string" }, + { key: "pri", label: "Priority", format: "integer" }, + { key: "count", label: "Size", format: "bytes" } + ], + description: "Network requests to load resources" +} +``` + +## Marker Schema Fields + +### Display Locations + +The `display` array specifies where markers appear in the UI: + +| Location | Description | +|----------|-------------| +| `marker-chart` | Main marker visualization timeline | +| `marker-table` | Searchable table of all markers | +| `timeline-overview` | Header timeline (requires `thread.showMarkersInTimeline = true` for imported profiles) | +| `timeline-memory` | Memory-specific timeline section | +| `timeline-ipc` | IPC (Inter-Process Communication) timeline | +| `timeline-fileio` | File I/O timeline | +| `stack-chart` | Stack chart view (not yet supported) | + +### Field Definitions + +Each field in the `fields` array describes a property from the marker's data payload: + +```typescript +MarkerSchemaField { + key: string // Property name in marker.data + label?: string // Display label (defaults to key) + format: MarkerFormatType // How to display the value + hidden?: boolean // Hide from tooltip/sidebar (still searchable) +} +``` + +## Format Types + +The `format` field determines how values are displayed and whether they're sanitized: + +### String Formats + +| Format | Description | PII Handling | +|--------|-------------|--------------| +| `string` | Plain string | **No sanitization** - avoid URLs, file paths, sensitive data | +| `sanitized-string` | String with PII protection | Sanitized in public profiles | +| `url` | URL | Sanitized in public profiles | +| `file-path` | File system path | Sanitized in public profiles | +| `unique-string` | Index into string table | Resolved on display | + +### Time Formats + +All time values are stored as milliseconds internally: + +| Format | Description | Example Display | +|--------|-------------|-----------------| +| `duration` | Time duration | "5s", "123ms", "50μs" | +| `time` | Timestamp relative to profile start | "15.5s", "20.5ms" | +| `seconds` | Duration in seconds only | "5s" | +| `milliseconds` | Duration in milliseconds only | "123ms" | +| `microseconds` | Duration in microseconds only | "50μs" | +| `nanoseconds` | Duration in nanoseconds only | "1000ns" | + +### Numeric Formats + +| Format | Description | Example Display | +|--------|-------------|-----------------| +| `integer` | Whole numbers | "1,234,567" | +| `decimal` | Floating-point numbers | "123,456.78" | +| `bytes` | Data size | "5.55 MB", "312.5 KB" | +| `percentage` | Percentage (value 0-1) | "50%" | +| `pid` | Process ID | Formatted process ID | +| `tid` | Thread ID | Formatted thread ID | + +### Flow Formats + +Used to track async operations across threads/processes: + +| Format | Description | +|--------|-------------| +| `flow-id` | Flow identifier (hex string) | +| `terminating-flow-id` | Flow ID that ends the flow chain | + +### Structured Formats + +| Format | Description | +|--------|-------------| +| `list` | Array of values | +| `{ type: 'table', columns: [...] }` | Table with custom columns | + +#### Table Format Example + +```typescript +{ + key: "phases", + label: "GC Phases", + format: { + type: 'table', + columns: [ + { label: "Phase", type: "string" }, + { label: "Time", type: "duration" } + ] + } +} +``` + +## Supported Colors + +Markers and graphs can use these colors: + +- `blue` +- `green` +- `grey` +- `ink` +- `magenta` +- `orange` +- `purple` +- `red` +- `teal` +- `yellow` + +### Using Colors + +**Static color in graph:** +```typescript +graphs: [ + { + key: "cpuUsage", + type: "line", + color: "blue" + } +] +``` + +**Dynamic color per marker:** +```typescript +{ + name: "CustomMarker", + colorField: "markerColor", // Field in marker.data containing color + fields: [ + { key: "markerColor", format: "string", hidden: true } + ] +} +``` + +## Labels with Dynamic Content + +Labels can include dynamic content using template strings with curly braces: + +**Available template variables:** +- `{marker.name}` - The marker's name +- `{marker.data.fieldName}` - Any field from the marker's data payload + +**Label purposes:** +- `tooltipLabel` - First line shown in tooltips and the sidebar (defaults to `marker.name`) +- `tableLabel` - Description column in the marker table (marker.name is already shown as a separate column, so use dynamic data here) +- `chartLabel` - Text displayed inside the marker box in the marker chart + +```typescript +{ + name: "DOMEvent", + tableLabel: "{marker.data.eventType}", // Show the specific event type + chartLabel: "{marker.data.eventType}", // Display event type in the marker box + fields: [ + { key: "eventType", label: "Event Type", format: "string" }, + { key: "latency", label: "Latency", format: "duration" } + ] +} +``` + +**More examples:** +```typescript +// Network request - show URL in table and chart +tableLabel: "{marker.data.URI}" +chartLabel: "{marker.data.requestMethod} {marker.data.URI}" + +// Database query - combine operation and table +tableLabel: "{marker.data.operation} – {marker.data.table}" +tooltipLabel: "Query: {marker.data.operation}" +``` + +## Custom Graphs + +Create custom local tracks for markers with numeric data: + +```typescript +MarkerGraph { + key: string // Field name with numeric data + type: MarkerGraphType // 'bar', 'line', or 'line-filled' + color?: GraphColor // Optional color +} +``` + +**Example:** +```typescript +{ + name: "MemoryAllocation", + display: [], // Markers with graphs can have empty display (shown in their own track) + graphs: [ + { + key: "bytes", + type: "bar", + color: "orange" + } + ], + fields: [ + { key: "bytes", label: "Allocated", format: "bytes" } + ] +} +``` + +When markers have custom graphs, they appear in their own dedicated track and don't need to be shown in other display locations. + +## Stack-Based Markers + +Set `isStackBased: true` for markers that follow call-stack semantics: + +```typescript +{ + name: "FunctionCall", + isStackBased: true, // Markers are well-nested like function calls + display: ["marker-chart"], + fields: [...] +} +``` + +**Requirements for stack-based markers:** +- Instant markers are always well-nested +- Interval markers must not partially overlap (A fully contains B or B fully contains A) + +## Implementing Markers in Gecko + +For Firefox/Gecko development, markers are implemented in C++: + +### Key Files + +- [ProfilerMarker.h] - Marker definitions +- [ProfilerMarkerPayload.h] - Payload types (header) +- [ProfilerMarkerPayload.cpp] - Payload types (implementation) +- [GeckoProfiler.h] - Main profiler API including [profiler_add_marker] + +### Adding a Marker + +```cpp +PROFILER_ADD_MARKER_WITH_PAYLOAD( + "MyMarker", // Marker name + OTHER, // Category + MarkerPayload, // Payload type + (arg1, arg2) // Payload constructor args +); +``` + +[ProfilerMarker.h]: https://searchfox.org/mozilla-central/source/tools/profiler/core/ProfilerMarker.h +[ProfilerMarkerPayload.h]: https://searchfox.org/mozilla-central/source/tools/profiler/public/ProfilerMarkerPayload.h +[ProfilerMarkerPayload.cpp]: https://searchfox.org/mozilla-central/source/tools/profiler/core/ProfilerMarkerPayload.cpp +[GeckoProfiler.h]: https://searchfox.org/mozilla-central/source/tools/profiler/public/GeckoProfiler.h +[profiler_add_marker]: https://searchfox.org/mozilla-central/rev/5e1e8d2f244bd8c210a578ff1f65c3b720efe34e/tools/profiler/public/GeckoProfiler.h#368-378 + +## Best Practices + +1. **Use appropriate formats**: Choose formats that match your data type and provide proper PII protection +2. **Provide descriptions**: Help users understand what the marker represents +3. **Use dynamic labels**: Include key information in `tableLabel` and `chartLabel` +4. **Hide internal fields**: Use `hidden: true` for fields used in labels but not needed in tooltips +5. **Choose display locations**: Only show markers in relevant UI areas +6. **Sanitize PII**: Use `url`, `file-path`, or `sanitized-string` for data that might contain sensitive information + +## Complete Example: Custom Application Marker Schema + +```typescript +{ + name: "DatabaseQuery", + tooltipLabel: "Database Query", + tableLabel: "{marker.data.operation} – {marker.data.table}", + chartLabel: "DB: {marker.data.operation}", + description: "Database queries executed by the application", + display: ["marker-chart", "marker-table", "timeline-overview"], + fields: [ + { key: "operation", label: "Operation", format: "string" }, + { key: "table", label: "Table", format: "string" }, + { key: "rowCount", label: "Rows", format: "integer" }, + { key: "duration", label: "Duration", format: "duration" }, + { key: "query", label: "SQL Query", format: "sanitized-string" }, + { key: "cached", label: "Cached", format: "string" } + ], + graphs: [ + { + key: "rowCount", + type: "bar", + color: "purple" + } + ] +} +``` + +--- + +# Common Marker Types ## Paint markers diff --git a/docs-developer/processed-profile-format.md b/docs-developer/processed-profile-format.md index 0c9c3b101a..1b64bcebb2 100644 --- a/docs-developer/processed-profile-format.md +++ b/docs-developer/processed-profile-format.md @@ -1,7 +1,473 @@ -# Processed profile format +# Processed Profile Format -The Gecko Profiler emits JSON profiles that Firefox Profiler can display. This original Gecko profile format needs to be processed in order to provide a data structure that is optimized for JS analysis. This client-side step provides a convenient mechanism for pre-processesing certain data transformations. The largest transformation is moving the performance data to be from a list of small object for each entry, to a few array tables containing long lists of primitive values. The primitive values do not need to be tracked by the garbage collector. For instance, if each profiler sample had a single object associated per sample with 7 properties per object, then 1000 samples would be one array containing 1000 objects. Transforming that into the processed format that same 1000 samples would be 1 table object, and 7 arrays, each of length 1000. The latter is much easier for the garbage collector to handle when using a reactive programming style. +The Gecko Profiler emits JSON profiles that Firefox Profiler can display. This original Gecko profile format needs to be processed in order to provide a data structure that is optimized for JavaScript analysis and performance. This client-side step provides a convenient mechanism for pre-processing certain data transformations. -## Processed profile documentation +## Why Process the Profile? -The documentation for this format is provided in the TypeScript type definition located at [src/types/profile.ts](../src/types/profile.ts). Eventually the plan is to have the documentation here; now that we've switched to TypeScript we might be able to autogenerate some nice docs. +The largest transformation is moving the performance data from a list of small objects (each entry being an object with multiple properties) to a few array tables containing long lists of primitive values. This structure-of-arrays approach has significant performance benefits: + +**Example transformation:** +- **Before (array-of-objects):** 1000 samples = 1 array containing 1000 objects, each with 7 properties +- **After (structure-of-arrays):** 1000 samples = 1 table object containing 7 arrays, each of length 1000 + +**Benefits:** +- Fewer objects need to be tracked by the garbage collector +- Smaller serialized file + +## Profile Structure Overview + +A processed profile consists of these main components: + +### Top-Level Structure + +```typescript +Profile { + meta: ProfileMeta // Metadata about the recording session + libs: Lib[] // Shared libraries loaded during profiling + pages?: PageList // Browser pages/tabs (for web content) + counters?: RawCounter[] // Performance counters (memory, CPU, etc.) + profilerOverhead?: ProfilerOverhead[] // Profiler's own overhead + shared: RawProfileSharedData // Shared data (strings, sources) + threads: RawThread[] // The actual thread data + profilingLog?: ProfilingLog // Logs from the profiling process + profileGatheringLog?: ProfilingLog // Logs from profile gathering +} +``` + +### Thread Structure + +Each thread contains multiple tables that work together to represent the call stacks and timing data: + +```typescript +RawThread { + // Thread identification + name: string // e.g., "GeckoMain", "Compositor", "DOM Worker" + pid: Pid // e.g., "12345" (stringified process ID) + tid: Tid // e.g., 67890 (thread ID, can be number or string) + processType: ProcessType // e.g., "default", "tab", "gpu", "plugin" + processName?: string // e.g., "Web Content" (human-readable) + isMainThread: boolean // true for the main thread of a process + + // Timing and lifecycle + processStartupTime: Milliseconds // When the process started + processShutdownTime: Milliseconds | null // When process ended (null if still running) + registerTime: Milliseconds // When thread was registered with profiler + unregisterTime: Milliseconds | null // When thread unregistered (null if still active) + pausedRanges: PausedRange[] // Periods when profiling was paused + + // Web content context (for content processes) + 'eTLD+1'?: string // e.g., "mozilla.org" (effective top-level domain) + isPrivateBrowsing?: boolean // true if this thread is for private browsing + userContextId?: number // Container ID (Firefox Multi-Account Containers) + + // Display options + showMarkersInTimeline?: boolean // Whether to show markers in timeline view + isJsTracer?: boolean // true if this is a JS tracing thread + + // Core profiling data tables + samples: RawSamplesTable // When and what was sampled + stackTable: RawStackTable // Call stack tree structure + frameTable: FrameTable // Individual stack frames + funcTable: FuncTable // Function information + resourceTable: ResourceTable // Where code came from (libraries, URLs) + nativeSymbols: NativeSymbolTable // Native code symbols + markers: RawMarkerTable // Arbitrary events and annotations + + // Optional specialized data + jsAllocations?: JsAllocationsTable // JS memory allocations + nativeAllocations?: NativeAllocationsTable // Native memory allocations + jsTracer?: JsTracerTable // Fine-grained JS tracing +} +``` + +**Common thread names:** +- `"GeckoMain"` - Main thread of the browser process +- `"Compositor"` - Graphics compositing thread +- `"Renderer"` - WebRender rendering thread +- `"DOM Worker"` - Web Worker thread +- `"StreamTrans"` - Network stream transport +- `"Socket Thread"` - Network socket operations + +**Process types:** +- `"default"` - Main browser process +- `"tab"` / `"web"` - Web content process +- `"gpu"` - GPU process for graphics +- `"plugin"` - Plugin process (legacy) +- `"extension"` - Extension process + +## Key Tables Explained + +### Samples Table + +The core data structure containing what functions were executing at specific points in time: + +```typescript +RawSamplesTable { + // Timing - either time OR timeDeltas must be present + time?: Milliseconds[] // Absolute timestamp for each sample + timeDeltas?: Milliseconds[] // Time delta since previous sample (alternative to time) + + stack: Array // What stack was active at this sample + + // Weighting + weight: null | number[] // Sample weight (null means all weights are 1) + weightType: 'samples' | 'tracing-ms' | 'bytes' // What the weight represents + + // Responsiveness metrics + responsiveness?: Array // Older metric: event delay (16ms intervals) + eventDelay?: Array // Newer metric: hypothetical input event delay + + // CPU usage + threadCPUDelta?: Array // CPU usage delta between samples + + // For merged profiles + threadId?: Tid[] // Origin thread ID (only in merged threads) + + length: number +} +``` + +### Stack Table + +Stores the tree of stack nodes efficiently. Instead of storing each complete call stack separately, stacks are represented as a tree where each node references its parent (prefix): + +```typescript +RawStackTable { + frame: IndexIntoFrameTable[] // The frame at this stack node + prefix: Array // Parent stack (null for root) + length: number +} +``` + +**How it works:** +- Root stack nodes have `null` as their prefix +- Each non-root stack has the index of its "caller" / "parent" as its prefix +- A complete call stack is obtained by walking from a node to the root +- Shared prefixes are stored only once, saving significant space + +**Example:** +``` +Call stacks: + A -> B -> C + A -> B -> D + A -> E + +Stack table representation: + Index 0: frame=A, prefix=null + Index 1: frame=B, prefix=0 + Index 2: frame=C, prefix=1 + Index 3: frame=D, prefix=1 + Index 4: frame=E, prefix=0 +``` + +### Frame Table + +Contains context information about function execution: + +```typescript +FrameTable { + address: Array
// Memory address (for native code) + inlineDepth: number[] // Depth of inlined functions + category: (IndexIntoCategoryList | null)[] // Frame category + subcategory: (IndexIntoSubcategoryListForCategory | null)[] + func: IndexIntoFuncTable[] // The function being executed + nativeSymbol: (IndexIntoNativeSymbolTable | null)[] + innerWindowID: (InnerWindowID | null)[] // For correlating to pages + line: (number | null)[] // Source line number + column: (number | null)[] // Source column number + length: number +} +``` + +### Function Table + +Groups frames by the function they belong to: + +```typescript +FuncTable { + name: Array // Function name + isJS: Array // Is this JavaScript? + relevantForJS: Array // Show in JS views? + resource: Array // Where code came from + source: Array // Source file (JS only) + lineNumber: Array // Function start line (JS only) + columnNumber: Array // Function start column (JS only) + length: number +} +``` + +### Resource Table + +Describes where code came from (libraries, web hosts, add-ons): + +```typescript +ResourceTable { + lib: Array // Native library + name: Array // Resource name + host: Array // Web host (for JS) + type: ResourceTypeEnum[] // Resource type + length: number +} +``` + +### Markers Table + +Represents arbitrary events that happen during profiling (paints, network requests, user input, etc.): + +```typescript +RawMarkerTable { + data: Array // Structured marker data + name: IndexIntoStringTable[] // Marker name + startTime: Array // When marker started (ms) + endTime: Array // When marker ended (ms) + phase: MarkerPhase[] // Instant, Interval, Start, End + category: IndexIntoCategoryList[] // Marker category + threadId?: Array // Origin thread (for merged threads) + length: number +} +``` + +## String Table + +To save space, all strings are deduplicated into a single `stringArray`. Other tables reference strings by their index: + +```typescript +RawProfileSharedData { + stringArray: string[] // All unique strings in the profile + sources: SourceTable // Source file mappings +} +``` + +## Weight Types + +Profiles can represent different types of data through the weight system: + +- **`samples`**: Traditional sampling profiles (weight of 1 per sample) +- **`tracing-ms`**: Tracing data converted to samples, weighted by duration in milliseconds +- **`bytes`**: Memory allocation profiles, weighted by bytes allocated + +## Sample-Based vs. Tracing Data + +The samples table can represent both traditional sampling and tracing data: + +**Traditional sampling:** Fixed interval sampling (e.g., every 1ms), all samples weighted equally. + +**Tracing as samples:** Convert tracing spans into self-time samples. For example: + +``` +Timeline: 0 1 2 3 4 5 6 7 8 9 10 +Spans: A A A A A A A A A A A + B B D D D D + C C E E E E + +Self-time: A A C C E E E E A A A + +Samples table: + time: [0, 2, 4, 8] + stack: [A, ABC, ADE, A] + weight: [2, 2, 4, 3] +``` + +## Profile Metadata + +The `meta` object contains important context: + +```typescript +ProfileMeta { + interval: Milliseconds // Sample interval (e.g., 1ms) + startTime: Milliseconds // Unix epoch timestamp (ms) when main process started + endTime?: Milliseconds // Unix epoch timestamp (ms) when profile capture ended + profilingStartTime?: Milliseconds // Offset from startTime (ms) when recording began + profilingEndTime?: Milliseconds // Offset from startTime (ms) when recording ended + + // Format versions - see CHANGELOG-formats.md for version history + // When upgrading profiles, implement upgraders in src/profile-logic/process-profile.js + version: number // Gecko profile format version (from browser) + preprocessedProfileVersion: number // Processed format version (this format) + + product: string // "Firefox" or other + processType: number // Raw enum value for main process type + stackwalk: 0 | 1 // 1 if native stack walking enabled, 0 otherwise + debug?: boolean // true for debug builds, false for opt builds + + // Categories define the color and grouping of stack frames (e.g., "JavaScript", "Layout", "Network") + // If absent, a default category list is used. Must include a "grey" default category. + categories?: CategoryList + + // Marker schema defines how to display markers in the UI - see docs-developer/markers.md + markerSchema: MarkerSchema[] + + // Platform-specific clock values for precise time correlation across systems + startTimeAsClockMonotonicNanosecondsSinceBoot?: number // Linux/Android + startTimeAsMachAbsoluteTimeNanoseconds?: number // macOS + startTimeAsQueryPerformanceCounterValue?: number // Windows + + // Platform information + platform?: string // e.g., "Windows", "Macintosh", "X11", "Android" + oscpu?: string // e.g., "Intel Mac OS X" + toolkit?: string // e.g., "gtk", "windows", "cocoa", "android" + abi?: string // e.g., "x86_64-gcc3" (XPCOM ABI) + misc?: string // e.g., "rv:63.0" (browser revision) + + // Hardware information + physicalCPUs?: number // Physical CPU cores + logicalCPUs?: number // Logical CPU cores (includes hyperthreading) + CPUName?: string // e.g., "Intel Core i7-9750H" + mainMemory?: Bytes // Total system RAM in bytes + + // Build information + appBuildID?: string // e.g., "20230615120000" + updateChannel?: string // e.g., "nightly", "release", "beta", "esr", "default" + sourceURL?: string // URL to source code revision + device?: string // Device model (Android only, e.g., "Samsung Galaxy S21") + + // Extensions + extensions?: ExtensionTable // Browser extensions installed during capture + + // Symbolication status + symbolicated?: boolean // true if native symbols already resolved + symbolicationNotSupported?: boolean // true if symbolication impossible (e.g., imported) + + // Profiler configuration + configuration?: ProfilerConfiguration // Settings: threads, features, capacity, etc. + sampleUnits?: SampleUnits // Units for sample table values (per-platform) + + // Visual metrics (browsertime only) + visualMetrics?: VisualMetrics // Page load visual performance metrics + + // Imported profile metadata + importedFrom?: string // Source product/tool name + arguments?: string // Program arguments (for imported profiles) + fileName?: string // File name (for size profiles) + fileSize?: Bytes // Total file size (for size profiles) + + // UI hints for imported profiles + usesOnlyOneStackType?: boolean // Don't show stack type filtering + sourceCodeIsNotOnSearchfox?: boolean // Hide "Look up on Searchfox" option + initialVisibleThreads?: ThreadIndex[] // Pre-select visible threads + initialSelectedThreads?: ThreadIndex[] // Pre-select active thread + keepProfileThreadOrder?: boolean // Don't reorder by importance score + extra?: ExtraProfileInfoSection[] // Additional metadata sections + + // Power profiling + gramsOfCO2ePerKWh?: number // CO2 emissions per kWh (for power tracks) +} +``` + +## Categories and Subcategories + +Categories are used to color-code and filter stack frames: + +```typescript +Category { + name: string // e.g., "JavaScript", "Layout", "Network" + color: CategoryColor // Visual color for the timeline + subcategories: string[] // More specific classifications +} +``` + +Frame categories determine the color and classification of stack nodes in the UI. If a frame has no category, it inherits from its parent in the call stack. + +## Pages and Windows + +For web content, the profile can track which code belongs to which page: + +```typescript +Page { + tabID: TabID // Shared across all pages in a tab's session history + innerWindowID: InnerWindowID // Unique ID for this JS window object (unique key) + url: string // Page URL + embedderInnerWindowID: number // Parent frame's innerWindowID (0 for top-level) + isPrivateBrowsing?: boolean // true if opened in private browsing (Firefox 98+) + favicon?: string | null // Base64-encoded favicon data URI (Firefox 134+, null if unavailable) +} +``` + +Frames have an `innerWindowID` field that correlates them to specific pages. + +## Memory Profiling + +Memory allocations can be profiled and appear as specialized tables: + +### JS Allocations + +```typescript +JsAllocationsTable { + time: Milliseconds[] + className: string[] // Object class name + typeName: string[] // Type (e.g., "JSObject") + coarseType: string[] // Coarse type (e.g., "Object") + weight: Bytes[] // Allocation size + weightType: 'bytes' + inNursery: boolean[] // Nursery vs. tenured + stack: Array + length: number +} +``` + +### Native Allocations + +```typescript +NativeAllocationsTable { + time: Milliseconds[] + weight: Bytes[] // Allocation size (can be negative) + weightType: 'bytes' + stack: Array + memoryAddress?: number[] // Address of allocation + threadId?: number[] // Thread performing allocation + length: number +} +``` + +## Counters + +Track numeric values over time (memory usage, CPU load, custom metrics): + +```typescript +RawCounter { + name: string // Counter name (e.g., "Memory", "CPU Usage") + category: string // Category for grouping + description: string // Human-readable description + color?: GraphColor // Graph color (e.g., "blue", "red", "green") + pid: Pid // Process ID this counter belongs to + mainThreadIndex: ThreadIndex // Main thread of the process + samples: RawCounterSamplesTable { + // Either time OR timeDeltas must be present + time?: Milliseconds[] // Absolute timestamps + timeDeltas?: Milliseconds[] // Time deltas since previous sample + number?: number[] // Change count (pre-v43, now optional) + count: number[] // Counter value at each sample + length: number + } +} +``` + +## TypeScript Type Definitions + +The authoritative documentation for the processed profile format is in the TypeScript type definitions at [src/types/profile.ts](../src/types/profile.ts). The types include detailed inline documentation explaining each field. + +## Working with Profiles + +When working with processed profiles: + +1. **Access strings:** Use indices to look up strings in `profile.shared.stringArray` +2. **Walk stacks:** Follow `prefix` links in the stack table to build complete call stacks +3. **Match frames to functions:** Use `frame.func` to group related frames +4. **Filter by category:** Use frame categories to focus on specific types of code +5. **Correlate to pages:** Use `innerWindowID` to filter by web page/tab +6. **Handle null values:** Many fields are nullable; always check before using + +## Profile Processing Pipeline + +1. **Gecko emits raw profile:** Original JSON from the browser +2. **Process profile:** Transform to structure-of-arrays format +3. **Symbolication:** Resolve addresses to function names (if needed) +4. **Analysis:** Extract derived data (call trees, timelines, etc.) +5. **Visualization:** Render in Firefox Profiler UI + +## Version Compatibility + +- `meta.version`: Gecko profile format version (from browser) +- `meta.preprocessedProfileVersion`: Processed format version (from profiler.firefox.com) + +The profiler includes upgraders to handle older profile versions, ensuring backward compatibility. diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index be047a25b2..c78b6a00c9 100644 --- a/src/app-logic/constants.ts +++ b/src/app-logic/constants.ts @@ -55,6 +55,11 @@ export const TRACK_MARKER_HEIGHT = 25; export const TRACK_MARKER_LINE_WIDTH = 2; export const TRACK_MARKER_DEFAULT_COLOR = 'grey'; +// Height for local tracks when the activity graph is hidden. +// This provides more visual prominence for marker and counter tracks when they're +// the only visualization for a thread. +export const TRACK_LOCAL_HEIGHT_WITHOUT_ACTIVITYGRAPH = 40; + // Height of the blank area in process track. export const TRACK_PROCESS_BLANK_HEIGHT = 30; diff --git a/src/components/timeline/LocalTrack.tsx b/src/components/timeline/LocalTrack.tsx index f43e2b991f..82cc3b6a84 100644 --- a/src/components/timeline/LocalTrack.tsx +++ b/src/components/timeline/LocalTrack.tsx @@ -55,6 +55,7 @@ type StateProps = { readonly isSelected: boolean; readonly isHidden: boolean; readonly titleText: string | undefined; + readonly shouldUseEnlargedHeight: boolean; }; type DispatchProps = { @@ -94,7 +95,7 @@ class LocalTrackComponent extends PureComponent { }; renderTrack() { - const { localTrack, trackName } = this.props; + const { localTrack, trackName, shouldUseEnlargedHeight } = this.props; switch (localTrack.type) { case 'thread': return ( @@ -124,6 +125,7 @@ class LocalTrackComponent extends PureComponent { threadIndex={localTrack.threadIndex} markerSchema={localTrack.markerSchema} markerName={localTrack.markerName} + shouldUseEnlargedHeight={shouldUseEnlargedHeight} /> ); default: @@ -199,6 +201,7 @@ export const TimelineLocalTrack = explicitConnect< // These get assigned based on the track type. let titleText; let isSelected = false; + let shouldUseEnlargedHeight = false; const selectedThreadIndexes = getSelectedThreadIndexes(state); @@ -213,23 +216,28 @@ export const TimelineLocalTrack = explicitConnect< selectedThreadIndexes.has(threadIndex) && selectedTab !== 'network-chart'; titleText = selectors.getThreadProcessDetails(state); + shouldUseEnlargedHeight = selectors.getShouldHideActivityGraph(state); break; } case 'network': { const threadIndex = localTrack.threadIndex; const selectedTab = getSelectedTab(state); + const selectors = getThreadSelectors(threadIndex); isSelected = selectedThreadIndexes.has(threadIndex) && selectedTab === 'network-chart'; + shouldUseEnlargedHeight = selectors.getShouldHideActivityGraph(state); break; } case 'marker': case 'ipc': { const threadIndex = localTrack.threadIndex; const selectedTab = getSelectedTab(state); + const selectors = getThreadSelectors(threadIndex); isSelected = selectedThreadIndexes.has(threadIndex) && selectedTab === 'marker-chart'; + shouldUseEnlargedHeight = selectors.getShouldHideActivityGraph(state); break; } case 'event-delay': { @@ -239,6 +247,7 @@ export const TimelineLocalTrack = explicitConnect< isSelected = selectedThreadIndexes.has(threadIndex); titleText = 'Event Delay of ' + selectors.getThreadProcessDetails(state); + shouldUseEnlargedHeight = selectors.getShouldHideActivityGraph(state); break; } case 'memory': @@ -248,6 +257,9 @@ export const TimelineLocalTrack = explicitConnect< titleText = getCounterSelectors(localTrack.counterIndex).getDescription( state ); + // For counter-based tracks, we don't have a thread directly. + // These are always false for now as they don't have a direct thread association. + shouldUseEnlargedHeight = false; break; } default: @@ -259,6 +271,7 @@ export const TimelineLocalTrack = explicitConnect< titleText, isSelected, isHidden: getHiddenLocalTracks(state, pid).has(trackIndex), + shouldUseEnlargedHeight, }; }, mapDispatchToProps: { diff --git a/src/components/timeline/TrackCustomMarker.tsx b/src/components/timeline/TrackCustomMarker.tsx index ec0b7cedb2..2d0c2c2633 100644 --- a/src/components/timeline/TrackCustomMarker.tsx +++ b/src/components/timeline/TrackCustomMarker.tsx @@ -6,7 +6,10 @@ import * as React from 'react'; import explicitConnect from 'firefox-profiler/utils/connect'; import { getCommittedRange } from 'firefox-profiler/selectors/profile'; import { TrackCustomMarkerGraph } from './TrackCustomMarkerGraph'; -import { TRACK_MARKER_HEIGHT } from 'firefox-profiler/app-logic/constants'; +import { + TRACK_MARKER_HEIGHT, + TRACK_LOCAL_HEIGHT_WITHOUT_ACTIVITYGRAPH, +} from 'firefox-profiler/app-logic/constants'; import type { ThreadIndex, Milliseconds } from 'firefox-profiler/types'; import type { MarkerSchema } from 'firefox-profiler/types/markers'; @@ -20,6 +23,7 @@ type OwnProps = { readonly markerSchema: MarkerSchema; readonly markerName: IndexIntoStringTable; readonly threadIndex: ThreadIndex; + readonly shouldUseEnlargedHeight: boolean; }; type StateProps = { @@ -33,14 +37,18 @@ type Props = ConnectedProps; export class TrackCustomMarkerImpl extends React.PureComponent { override render() { - const { markerSchema, markerName, threadIndex } = this.props; + const { markerSchema, markerName, threadIndex, shouldUseEnlargedHeight } = + this.props; + const height = shouldUseEnlargedHeight + ? TRACK_LOCAL_HEIGHT_WITHOUT_ACTIVITYGRAPH + : TRACK_MARKER_HEIGHT; return (
@@ -48,7 +56,7 @@ export class TrackCustomMarkerImpl extends React.PureComponent { threadIndex={threadIndex} markerSchema={markerSchema} markerName={markerName} - graphHeight={TRACK_MARKER_HEIGHT} + graphHeight={height} />
); diff --git a/src/components/timeline/TrackThread.tsx b/src/components/timeline/TrackThread.tsx index a7d2565a57..c0639d01de 100644 --- a/src/components/timeline/TrackThread.tsx +++ b/src/components/timeline/TrackThread.tsx @@ -94,6 +94,7 @@ type StateProps = { readonly callTreeVisible: boolean; readonly zeroAt: Milliseconds; readonly profileTimelineUnit: string; + readonly shouldHideActivityGraph: boolean; }; type DispatchProps = { @@ -192,8 +193,13 @@ class TimelineTrackThreadImpl extends PureComponent { implementationFilter, zeroAt, profileTimelineUnit, + shouldHideActivityGraph, } = this.props; + console.log( + `[DEBUG TrackThread] threadsKey=${threadsKey}, shouldHideActivityGraph=${shouldHideActivityGraph}, timelineType=${timelineType}, isJsTracer=${filteredThread.isJsTracer}` + ); + const processType = filteredThread.processType; const displayJank = processType !== 'plugin'; const displayMarkers = @@ -225,7 +231,7 @@ class TimelineTrackThreadImpl extends PureComponent { onSelect={this._onMarkerSelect} /> ) : null} - {displayJank ? ( + {displayJank && !shouldHideActivityGraph ? ( { /> ) : null} - {(timelineType === 'category' || timelineType === 'cpu-category') && - !filteredThread.isJsTracer ? ( - <> - - {trackType === 'expanded' ? ( - + - ) : null} - {isExperimentalCPUGraphsEnabled && - rangeFilteredThread.samples.threadCPURatio !== undefined ? ( - - ) : null} - - ) : ( - - )} + {trackType === 'expanded' ? ( + + ) : null} + {isExperimentalCPUGraphsEnabled && + rangeFilteredThread.samples.threadCPURatio !== undefined ? ( + + ) : null} + + ) : ( + + ) + ) : null} = createSelector( + ProfileSelectors.getThreadIndexesThatShouldHideActivityGraph, + (threadIndexesThatShouldHide) => { + // For merged threads, if any of the threads should hide, then hide + if (singleThreadIndex !== null) { + return threadIndexesThatShouldHide.has(singleThreadIndex); + } + // For merged threads, only hide if all threads should hide + for (const threadIndex of threadIndexes) { + if (!threadIndexesThatShouldHide.has(threadIndex)) { + return false; + } + } + return threadIndexes.size > 0; + } + ); + const getRawSamplesTable: Selector = (state) => getRawThread(state).samples; const getSamplesTable: Selector = createSelector( @@ -407,6 +428,7 @@ export function getBasicThreadSelectorsPerThread( getCanShowRetainedMemory, getFunctionsReservedThread, getProcessedEventDelays, + getShouldHideActivityGraph, getCallTreeSummaryStrategy, }; } diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index a0fa0b175d..fd40557b60 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -14,6 +14,7 @@ import { processCounter, getInclusiveSampleIndexRangeForSelection, computeTabToThreadIndexesMap, + hasUsefulSamples, } from '../profile-logic/profile-data'; import type { IPCMarkerCorrelations } from '../profile-logic/marker-data'; import { correlateIPCMarkers } from '../profile-logic/marker-data'; @@ -189,6 +190,89 @@ export const getThreads: Selector = (state) => getProfile(state).threads; export const getThreadNames: Selector = (state) => getProfile(state).threads.map((t) => t.name); + +/** + * Returns a Set of ThreadIndexes for threads that should hide their activity graph. + * This applies to threads that: + * 1. Are the only thread in their process + * 2. Are not the main thread + * 3. Have no useful samples + * 4. Have no CPU usage information + */ +export const getThreadIndexesThatShouldHideActivityGraph: Selector< + Set +> = createSelector( + getThreads, + getRawProfileSharedData, + (threads, shared): Set => { + const result = new Set(); + + // Group threads by PID + const threadsByPid = new Map(); + for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) { + const thread = threads[threadIndex]; + const threadsForPid = threadsByPid.get(thread.pid) || []; + threadsForPid.push(threadIndex); + threadsByPid.set(thread.pid, threadsForPid); + } + + console.log('[DEBUG] Threads by PID:', threadsByPid); + + // Check each thread + for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) { + const thread = threads[threadIndex]; + const threadsInProcess = threadsByPid.get(thread.pid) || []; + + console.log( + `[DEBUG] Thread ${threadIndex} (${thread.name}, PID ${thread.pid}):`, + { + threadsInProcessCount: threadsInProcess.length, + isMainThread: thread.isMainThread, + hasSamples: [ + thread.samples, + thread.jsAllocations, + thread.nativeAllocations, + ].some((table) => hasUsefulSamples(table?.stack, thread, shared)), + hasCPUUsage: thread.samples.threadCPUDelta !== undefined, + } + ); + + // Check condition 1: Only thread in process + if (threadsInProcess.length !== 1) { + continue; + } + + // Check condition 2: Not main thread + if (thread.isMainThread) { + continue; + } + + // Check condition 3: No useful samples + const { samples, jsAllocations, nativeAllocations } = thread; + const hasSamples = [samples, jsAllocations, nativeAllocations].some( + (table) => hasUsefulSamples(table?.stack, thread, shared) + ); + if (hasSamples) { + continue; + } + + // Check condition 4: No CPU usage information + const hasCPUUsage = thread.samples.threadCPUDelta !== undefined; + if (hasCPUUsage) { + continue; + } + + // All conditions met - hide activity graph + console.log( + `[DEBUG] ✓ Thread ${threadIndex} (${thread.name}) SHOULD HIDE activity graph` + ); + result.add(threadIndex); + } + + console.log('[DEBUG] Threads that should hide activity graph:', result); + return result; + } +); export const getLastNonShiftClick: Selector< LastNonShiftClickInformation | null > = (state) => getProfileViewOptions(state).lastNonShiftClick; diff --git a/src/test/components/TrackCustomMarker.test.tsx b/src/test/components/TrackCustomMarker.test.tsx index 608433586d..ae3b82cb64 100644 --- a/src/test/components/TrackCustomMarker.test.tsx +++ b/src/test/components/TrackCustomMarker.test.tsx @@ -113,6 +113,7 @@ function setup( profile.meta.markerSchema[profile.meta.markerSchema.length - 1] } markerName={markerStringIndex} + shouldUseEnlargedHeight={false} /> );