From fbcabb7d11bcef7aea9099c2df42472009150370 Mon Sep 17 00:00:00 2001 From: Anna Mayzner Date: Fri, 28 Nov 2025 17:53:12 +0000 Subject: [PATCH 1/3] exp: Show tables as frequent and look in the columns description --- .../generators/sql_processing/stdlib_tags.py | 5 +- .../query_builder/help.scss | 4 +- .../query_builder/table_list.ts | 82 +++++++++++++------ 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/python/generators/sql_processing/stdlib_tags.py b/python/generators/sql_processing/stdlib_tags.py index a31efaa8b8..89761858e9 100644 --- a/python/generators/sql_processing/stdlib_tags.py +++ b/python/generators/sql_processing/stdlib_tags.py @@ -451,8 +451,8 @@ def _validate_tags(): # Table importance levels for documentation. # Importance levels help users discover the most relevant tables for their use case. # Levels: -# 'high': Most commonly used, fundamental tables for trace analysis -# 'mid': Important for specific use cases, moderately common +# 'high': Very frequent - Most commonly used, fundamental tables for trace analysis +# 'mid': Frequent - Important for specific use cases, moderately common # 'low': Specialized or advanced tables, less frequently needed # None/absent: Normal importance (default) TABLE_IMPORTANCE = { @@ -481,6 +481,7 @@ def _validate_tags(): 'android_anrs': 'mid', # Application Not Responding events and diagnostics 'android_battery_charge': 'mid', # Battery charge level tracking over time 'android_charging_states': 'mid', # Device charging state transitions + 'android_process_metadata': 'mid', # Process metadata and information # LOW IMPORTANCE - Raw/specialized tables, less frequently needed 'slices': diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/help.scss b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/help.scss index 6e1617eaf0..eaf985c371 100644 --- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/help.scss +++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/help.scss @@ -80,11 +80,11 @@ } .pf-importance-high { - background-color: #d32f2f; // Red - Important + background-color: #d32f2f; // Red - Very frequent } .pf-importance-mid { - background-color: var(--pf-color-primary); // Primary blue - Recommended + background-color: var(--pf-color-primary); // Primary blue - Frequent } .pf-importance-low { diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts index 576830bc48..35a9798b09 100644 --- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts +++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts @@ -40,9 +40,9 @@ interface TableWithModule { function getImportanceLabel(importance: 'high' | 'mid' | 'low'): string { switch (importance) { case 'high': - return 'Important'; + return 'Very frequent'; case 'mid': - return 'Recommended'; + return 'Frequent'; case 'low': return 'Low'; } @@ -195,32 +195,64 @@ export class TableList implements m.ClassComponent { module.tables.map((table) => ({table, moduleName: module.includeKey})), ); - const finder = new FuzzyFinder(allTables, (item) => item.table.name); - const fuzzyResults = finder.find(attrs.searchQuery); + // Search by table name first + const titleFinder = new FuzzyFinder(allTables, (item) => item.table.name); + const titleResults = titleFinder.find(attrs.searchQuery); - // Group fuzzy results by importance level to ensure: - // - High importance tables always appear first - // - Low importance tables always appear last - // - Within each level, fuzzy finder's natural sorting applies - const highImportance = fuzzyResults.filter( - (r) => r.item.table.importance === 'high', - ); - const midImportance = fuzzyResults.filter( - (r) => r.item.table.importance === 'mid', - ); - const normalImportance = fuzzyResults.filter( - (r) => r.item.table.importance === undefined, - ); - const lowImportance = fuzzyResults.filter( - (r) => r.item.table.importance === 'low', + // Create a set of table names that matched by title to avoid duplicates + const titleMatchedNames = new Set( + titleResults.map((r) => r.item.table.name), ); - const sortedFuzzyResults = [ - ...highImportance, - ...midImportance, - ...normalImportance, - ...lowImportance, - ]; + // Search by column names for tables that didn't match by title + const columnResults: Array<{ + item: TableWithModule; + segments: FuzzySegment[]; + }> = []; + + if (attrs.searchQuery.trim() !== '') { + for (const tableWithModule of allTables) { + // Skip tables that already matched by title + if (titleMatchedNames.has(tableWithModule.table.name)) { + continue; + } + + // Search in column names + const columnNames = tableWithModule.table.columns + .map((col) => col.name) + .join(' '); + const columnFinder = new FuzzyFinder([columnNames], (name) => name); + const matches = columnFinder.find(attrs.searchQuery); + + if (matches.length > 0) { + // Use the table name segments (non-highlighted) for display + columnResults.push({ + item: tableWithModule, + segments: [{matching: false, value: tableWithModule.table.name}], + }); + } + } + } + + // Helper function to group results by importance + const groupByImportance = ( + results: Array<{item: TableWithModule; segments: FuzzySegment[]}>, + ) => { + const high = results.filter((r) => r.item.table.importance === 'high'); + const mid = results.filter((r) => r.item.table.importance === 'mid'); + const normal = results.filter( + (r) => r.item.table.importance === undefined, + ); + const low = results.filter((r) => r.item.table.importance === 'low'); + return [...high, ...mid, ...normal, ...low]; + }; + + // Combine results: title matches first (sorted by importance), + // then column matches (sorted by importance) + const sortedTitleResults = groupByImportance(titleResults); + const sortedColumnResults = groupByImportance(columnResults); + + const sortedFuzzyResults = [...sortedTitleResults, ...sortedColumnResults]; const tableCards = sortedFuzzyResults.map(({item, segments}) => m(TableCard, { From f6956e0b662071f18e3fb1540fd42be4f88032fd Mon Sep 17 00:00:00 2001 From: Anna Mayzner Date: Sat, 29 Nov 2025 16:22:47 +0000 Subject: [PATCH 2/3] better ordering --- .../query_builder/table_list.scss | 9 + .../query_builder/table_list.ts | 267 ++++++++++++++---- 2 files changed, 218 insertions(+), 58 deletions(-) diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss index f74b8fa388..ced96ef847 100644 --- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss +++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss @@ -38,3 +38,12 @@ } } } + +.pf-table-card-header { + .pf-match-type-chip { + background-color: var(--surface-variant); + color: var(--on-surface-variant); + font-size: 11px; + opacity: 0.8; + } +} diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts index 35a9798b09..cc91652c55 100644 --- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts +++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts @@ -36,6 +36,13 @@ interface TableWithModule { moduleName: string; } +// Type of match when searching for tables +type MatchType = + | 'table-name' + | 'column-name' + | 'table-description' + | 'column-description'; + // Helper function to get the display label for importance levels. function getImportanceLabel(importance: 'high' | 'mid' | 'low'): string { switch (importance) { @@ -83,6 +90,20 @@ class SearchBar } } +// Helper function to get the display label for match types. +function getMatchTypeLabel(matchType: MatchType): string | undefined { + switch (matchType) { + case 'table-name': + return undefined; // No label needed for table name matches + case 'column-name': + return 'from column name'; + case 'table-description': + return 'from table description'; + case 'column-description': + return 'from column description'; + } +} + // Renders a single table card in the list. // This component displays the table name, its module, and description. // It also highlights the parts of the name that match the search query. @@ -91,6 +112,7 @@ class TableCard m.ClassComponent<{ tableWithModule: TableWithModule; segments: FuzzySegment[]; + matchType: MatchType; onTableClick: (tableName: string) => void; }> { @@ -99,9 +121,10 @@ class TableCard }: m.CVnode<{ tableWithModule: TableWithModule; segments: FuzzySegment[]; + matchType: MatchType; onTableClick: (tableName: string) => void; }>) { - const {tableWithModule, segments, onTableClick} = attrs; + const {tableWithModule, segments, matchType, onTableClick} = attrs; const {table, moduleName} = tableWithModule; const renderedName = segments.map((segment) => @@ -109,6 +132,7 @@ class TableCard ); const packageName = moduleName.split('.')[0]; + const matchTypeLabel = getMatchTypeLabel(matchType); return m( Card, @@ -121,6 +145,12 @@ class TableCard m( '.pf-table-card-header', m('.table-name', renderedName), + matchTypeLabel && + m(Chip, { + label: matchTypeLabel, + compact: true, + className: classNames('pf-match-type-chip'), + }), table.importance && m(Chip, { label: getImportanceLabel(table.importance), @@ -167,76 +197,182 @@ export class TableList implements m.ClassComponent { ); } - // Compute which tags should be disabled - // A tag should be disabled if selecting it (in addition to current tags) would result in 0 tables - const disabledTags = new Set(); - if (this.selectedTags.size > 0) { - for (const tag of allTags) { - if (!this.selectedTags.has(tag)) { - // Check if adding this tag to selected tags would result in any modules - const wouldHaveModules = allModules.some((module) => { - // Module must have all currently selected tags AND this tag - const hasAllSelectedTags = Array.from(this.selectedTags).every( - (selectedTag) => module.tags.includes(selectedTag), - ); - const hasThisTag = module.tags.includes(tag); - const hasTables = module.tables.length > 0; - return hasAllSelectedTags && hasThisTag && hasTables; - }); + // Helper function to search tables by query (used for both display and tag filtering) + // Returns results in priority order: table name, column name, table description, column description + const searchTables = ( + tables: TableWithModule[], + query: string, + ): Array<{ + item: TableWithModule; + segments: FuzzySegment[]; + matchType: MatchType; + }> => { + if (query.trim() === '') { + return tables.map((table) => ({ + item: table, + segments: [{matching: false, value: table.table.name}], + matchType: 'table-name' as MatchType, + })); + } - if (!wouldHaveModules) { - disabledTags.add(tag); - } + // Track which tables have been matched to avoid duplicates + const matchedTableNames = new Set(); + + // 1. Search by table name (highest priority) + const tableFinder = new FuzzyFinder(tables, (item) => item.table.name); + const tableNameResults = tableFinder.find(query).map((result) => { + matchedTableNames.add(result.item.table.name); + return { + ...result, + matchType: 'table-name' as MatchType, + }; + }); + + // 2. Search by column names (second priority) - exact match + const columnNameResults: Array<{ + item: TableWithModule; + segments: FuzzySegment[]; + matchType: MatchType; + }> = []; + + const lowerQuery = query.toLowerCase(); + for (const tableWithModule of tables) { + if (matchedTableNames.has(tableWithModule.table.name)) { + continue; + } + + const hasMatch = tableWithModule.table.columns.some((col) => + col.name.toLowerCase().includes(lowerQuery), + ); + + if (hasMatch) { + matchedTableNames.add(tableWithModule.table.name); + columnNameResults.push({ + item: tableWithModule, + segments: [{matching: false, value: tableWithModule.table.name}], + matchType: 'column-name', + }); } } - } - const allTables: TableWithModule[] = filteredModules.flatMap((module) => - module.tables.map((table) => ({table, moduleName: module.includeKey})), - ); + // 3. Search by table description (third priority) - exact match + const tableDescriptionResults: Array<{ + item: TableWithModule; + segments: FuzzySegment[]; + matchType: MatchType; + }> = []; - // Search by table name first - const titleFinder = new FuzzyFinder(allTables, (item) => item.table.name); - const titleResults = titleFinder.find(attrs.searchQuery); + for (const tableWithModule of tables) { + if (matchedTableNames.has(tableWithModule.table.name)) { + continue; + } - // Create a set of table names that matched by title to avoid duplicates - const titleMatchedNames = new Set( - titleResults.map((r) => r.item.table.name), - ); + if ( + tableWithModule.table.description && + tableWithModule.table.description.toLowerCase().includes(lowerQuery) + ) { + matchedTableNames.add(tableWithModule.table.name); + tableDescriptionResults.push({ + item: tableWithModule, + segments: [{matching: false, value: tableWithModule.table.name}], + matchType: 'table-description', + }); + } + } - // Search by column names for tables that didn't match by title - const columnResults: Array<{ - item: TableWithModule; - segments: FuzzySegment[]; - }> = []; + // 4. Search by column descriptions (lowest priority) - exact match + const columnDescriptionResults: Array<{ + item: TableWithModule; + segments: FuzzySegment[]; + matchType: MatchType; + }> = []; - if (attrs.searchQuery.trim() !== '') { - for (const tableWithModule of allTables) { - // Skip tables that already matched by title - if (titleMatchedNames.has(tableWithModule.table.name)) { + for (const tableWithModule of tables) { + if (matchedTableNames.has(tableWithModule.table.name)) { continue; } - // Search in column names - const columnNames = tableWithModule.table.columns - .map((col) => col.name) - .join(' '); - const columnFinder = new FuzzyFinder([columnNames], (name) => name); - const matches = columnFinder.find(attrs.searchQuery); + const hasMatch = tableWithModule.table.columns.some( + (col) => + col.description && + col.description.toLowerCase().includes(lowerQuery), + ); - if (matches.length > 0) { - // Use the table name segments (non-highlighted) for display - columnResults.push({ + if (hasMatch) { + matchedTableNames.add(tableWithModule.table.name); + columnDescriptionResults.push({ item: tableWithModule, segments: [{matching: false, value: tableWithModule.table.name}], + matchType: 'column-description', }); } } + + return [ + ...tableNameResults, + ...columnNameResults, + ...tableDescriptionResults, + ...columnDescriptionResults, + ]; + }; + + const allTables: TableWithModule[] = filteredModules.flatMap((module) => + module.tables.map((table) => ({table, moduleName: module.includeKey})), + ); + + // Compute which tags should be disabled + // A tag should be disabled if selecting it (in addition to current tags) + // would result in 0 tables OR if combined with the search query would result in 0 results + const disabledTags = new Set(); + for (const tag of allTags) { + if (!this.selectedTags.has(tag)) { + // Compute what modules would exist if this tag was selected + const testSelectedTags = new Set(this.selectedTags); + testSelectedTags.add(tag); + + const modulesWithThisTag = allModules.filter((module) => + Array.from(testSelectedTags).every((selectedTag) => + module.tags.includes(selectedTag), + ), + ); + + const tablesWithThisTag: TableWithModule[] = modulesWithThisTag.flatMap( + (module) => + module.tables.map((table) => ({ + table, + moduleName: module.includeKey, + })), + ); + + // Check if there would be any tables + if (tablesWithThisTag.length === 0) { + disabledTags.add(tag); + continue; + } + + // If there's a search query, check if any tables would match the search + if (attrs.searchQuery.trim() !== '') { + const searchResults = searchTables( + tablesWithThisTag, + attrs.searchQuery, + ); + if (searchResults.length === 0) { + disabledTags.add(tag); + } + } + } } - // Helper function to group results by importance - const groupByImportance = ( - results: Array<{item: TableWithModule; segments: FuzzySegment[]}>, + // Perform the actual search for display + const searchResults = searchTables(allTables, attrs.searchQuery); + + // Helper function to sort by importance within a group + const sortByImportance = ( + results: Array<{ + item: TableWithModule; + segments: FuzzySegment[]; + matchType: MatchType; + }>, ) => { const high = results.filter((r) => r.item.table.importance === 'high'); const mid = results.filter((r) => r.item.table.importance === 'mid'); @@ -247,17 +383,32 @@ export class TableList implements m.ClassComponent { return [...high, ...mid, ...normal, ...low]; }; - // Combine results: title matches first (sorted by importance), - // then column matches (sorted by importance) - const sortedTitleResults = groupByImportance(titleResults); - const sortedColumnResults = groupByImportance(columnResults); + // Group by match type (already ordered by searchTables), then sort by importance within each group + const tableNameResults = searchResults.filter( + (r) => r.matchType === 'table-name', + ); + const columnNameResults = searchResults.filter( + (r) => r.matchType === 'column-name', + ); + const tableDescResults = searchResults.filter( + (r) => r.matchType === 'table-description', + ); + const columnDescResults = searchResults.filter( + (r) => r.matchType === 'column-description', + ); - const sortedFuzzyResults = [...sortedTitleResults, ...sortedColumnResults]; + const sortedFuzzyResults = [ + ...sortByImportance(tableNameResults), + ...sortByImportance(columnNameResults), + ...sortByImportance(tableDescResults), + ...sortByImportance(columnDescResults), + ]; - const tableCards = sortedFuzzyResults.map(({item, segments}) => + const tableCards = sortedFuzzyResults.map(({item, segments, matchType}) => m(TableCard, { tableWithModule: item, segments, + matchType, onTableClick: attrs.onTableClick, }), ); From b8c519467ef33aa5b3c08610df40ee27aca4d63e Mon Sep 17 00:00:00 2001 From: Anna Mayzner Date: Sat, 29 Nov 2025 16:33:58 +0000 Subject: [PATCH 3/3] md support --- .../query_builder/table_list.scss | 66 +++++++++++++++++++ .../query_builder/table_list.ts | 7 +- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss index ced96ef847..b4833fac1d 100644 --- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss +++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.scss @@ -47,3 +47,69 @@ opacity: 0.8; } } + +.table-description { + // Inline code styling + code { + background-color: var(--surface-variant); + padding: 2px 6px; + border-radius: 3px; + font-family: "Roboto Mono", monospace; + font-size: 0.9em; + color: var(--on-surface); + } + + // Links + a { + color: var(--primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + // Bold text + strong { + font-weight: 500; + } + + // Italic text + em { + font-style: italic; + } + + // Paragraphs + p { + margin: 0.5em 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + // Lists + ul, + ol { + margin: 0.5em 0; + padding-left: 1.5em; + } + + // Code blocks + pre { + background-color: var(--surface-variant); + padding: 8px 12px; + border-radius: 4px; + overflow-x: auto; + margin: 0.5em 0; + + code { + background-color: transparent; + padding: 0; + } + } +} diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts index cc91652c55..4329b99398 100644 --- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts +++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/table_list.ts @@ -20,6 +20,10 @@ import {EmptyState} from '../../../widgets/empty_state'; import {Chip} from '../../../widgets/chip'; import {classNames} from '../../../base/classnames'; import {Intent} from '../../../widgets/common'; +import markdownit from 'markdown-it'; + +// Create a markdown renderer instance +const md = markdownit(); // Attributes for the main TableList component. export interface TableListAttrs { @@ -162,7 +166,8 @@ class TableCard }), ), packageName === 'prelude' ? null : m('.table-module', moduleName), - table.description && m('.table-description', table.description), + table.description && + m('.table-description', m.trust(md.render(table.description))), ), ); }