Merge "Remove running duration check and update blocked by IO threshold." into main
diff --git a/examples/shared_lib/example_shlib_data_source.c b/examples/shared_lib/example_shlib_data_source.c
index d8aa1f7..e57ec1a 100644
--- a/examples/shared_lib/example_shlib_data_source.c
+++ b/examples/shared_lib/example_shlib_data_source.c
@@ -24,7 +24,7 @@
 static struct PerfettoDs custom = PERFETTO_DS_INIT();
 
 int main(void) {
-  struct PerfettoProducerInitArgs args = {0};
+  struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
   args.backends = PERFETTO_BACKEND_SYSTEM;
   PerfettoProducerInit(args);
 
diff --git a/include/perfetto/protozero/gen_field_helpers.h b/include/perfetto/protozero/gen_field_helpers.h
index ad3dee8..b2957f1 100644
--- a/include/perfetto/protozero/gen_field_helpers.h
+++ b/include/perfetto/protozero/gen_field_helpers.h
@@ -145,6 +145,17 @@
   HeapBuffered<Message> msg_;
 };
 
+// Wrapper about operator==() which reduces the binary size of generated protos.
+// This is needed because std::string's operator== is inlined aggressively (even
+// when optimizing for size). Having this layer of indirection with removes the
+// overhead.
+template <typename T>
+bool EqualsField(const T& a, const T& b) {
+  return a == b;
+}
+extern template bool EqualsField<std::string>(const std::string&,
+                                              const std::string&);
+
 }  // namespace gen_helpers
 }  // namespace internal
 }  // namespace protozero
diff --git a/include/perfetto/public/producer.h b/include/perfetto/public/producer.h
index 56692c6..8fa1351 100644
--- a/include/perfetto/public/producer.h
+++ b/include/perfetto/public/producer.h
@@ -30,6 +30,10 @@
   PerfettoBackendTypes backends;
 };
 
+// Initializes a PerfettoProducerInitArgs struct.
+#define PERFETTO_PRODUCER_INIT_ARGS_INIT() \
+  { 0 }
+
 // Initializes the global perfetto producer.
 static inline void PerfettoProducerInit(struct PerfettoProducerInitArgs args) {
   if (args.backends & PERFETTO_BACKEND_IN_PROCESS) {
diff --git a/src/protozero/gen_field_helpers.cc b/src/protozero/gen_field_helpers.cc
index 85a41f7..af5949e 100644
--- a/src/protozero/gen_field_helpers.cc
+++ b/src/protozero/gen_field_helpers.cc
@@ -98,6 +98,8 @@
   return msg_.SerializeAsString();
 }
 
+template bool EqualsField<std::string>(const std::string&, const std::string&);
+
 }  // namespace gen_helpers
 }  // namespace internal
 }  // namespace protozero
diff --git a/src/protozero/protoc_plugin/cppgen_plugin.cc b/src/protozero/protoc_plugin/cppgen_plugin.cc
index eadc23a..a33803f 100644
--- a/src/protozero/protoc_plugin/cppgen_plugin.cc
+++ b/src/protozero/protoc_plugin/cppgen_plugin.cc
@@ -702,9 +702,14 @@
   p->Print("bool $n$::operator==(const $n$& other) const {\n", "n", full_name);
   p->Indent();
 
-  p->Print("return unknown_fields_ == other.unknown_fields_");
+  p->Print(
+      "return ::protozero::internal::gen_helpers::EqualsField(unknown_fields_, "
+      "other.unknown_fields_)");
   for (int i = 0; i < msg->field_count(); i++)
-    p->Print("\n && $n$_ == other.$n$_", "n", msg->field(i)->lowercase_name());
+    p->Print(
+        "\n && ::protozero::internal::gen_helpers::EqualsField($n$_, "
+        "other.$n$_)",
+        "n", msg->field(i)->lowercase_name());
   p->Print(";");
   p->Outdent();
   p->Print("\n}\n\n");
diff --git a/src/shared_lib/test/api_integrationtest.cc b/src/shared_lib/test/api_integrationtest.cc
index 35b1c19..03735fa 100644
--- a/src/shared_lib/test/api_integrationtest.cc
+++ b/src/shared_lib/test/api_integrationtest.cc
@@ -190,7 +190,7 @@
 class SharedLibDataSourceTest : public testing::Test {
  protected:
   void SetUp() override {
-    struct PerfettoProducerInitArgs args = {0};
+    struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
     args.backends = PERFETTO_BACKEND_IN_PROCESS;
     PerfettoProducerInit(args);
     PerfettoDsRegister(&data_source_1, kDataSourceName1,
diff --git a/src/shared_lib/test/benchmark.cc b/src/shared_lib/test/benchmark.cc
index 4861a31..5b2a70f 100644
--- a/src/shared_lib/test/benchmark.cc
+++ b/src/shared_lib/test/benchmark.cc
@@ -42,7 +42,7 @@
 constexpr char kDataSourceName[] = "com.example.custom_data_source";
 
 bool Initialize() {
-  struct PerfettoProducerInitArgs args = {0};
+  struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
   args.backends = PERFETTO_BACKEND_IN_PROCESS;
   PerfettoProducerInit(args);
   PerfettoDsRegister(&custom, kDataSourceName, PerfettoDsParamsDefault());
diff --git a/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql b/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
index b954447..3a9d203 100644
--- a/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
+++ b/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
@@ -68,7 +68,15 @@
   AND slice.name not GLOB '*resynced*'
   AND slice.dur > 0
   AND (vsync >= begin_vsync OR begin_vsync is NULL)
-  AND (vsync <= end_vsync OR end_vsync is NULL);
+  AND (vsync <= end_vsync OR end_vsync is NULL)
+  -- In some malformed traces we see nested doFrame slices.
+  -- If that is the case, we ignore all parent doFrames and only keep the one
+  -- the lowest in the hierarchy.
+  AND NOT EXISTS (
+    SELECT 1 FROM descendant_slice(slice.id) child
+    WHERE child.name GLOB 'Choreographer#doFrame*'
+    AND child.name NOT GLOB '*resynced*'
+  );
 
 
 -- Store render thread DrawFrames by matching in the vsync IDs extracted from
diff --git a/ui/src/assets/topbar.scss b/ui/src/assets/topbar.scss
index d40b345..faea9e4 100644
--- a/ui/src/assets/topbar.scss
+++ b/ui/src/assets/topbar.scss
@@ -255,26 +255,38 @@
   }
 }
 
-ul.pf-omnibox-dropdown {
+.pf-omnibox-dropdown {
   margin: 2px;
   max-height: 300px;
   overflow-y: auto;
-  padding-left: 0;
-}
 
-li.pf-omnibox-option {
-  font-family: $pf-font;
-  cursor: pointer;
-  list-style-type: none;
-  padding: 4px 8px;
-  border-radius: $pf-border-radius;
-  align-items: center;
-  font-weight: lighter;
-  &:hover {
-    background-color: $pf-minimal-background-hover;
+  ul {
+    padding-left: 0;
   }
-  &.pf-highlighted {
-    background-color: $pf-primary-background;
-    color: white;
+
+  .pf-omnibox-section-header {
+    font-family: $pf-font;
+    font-size: smaller;
+    margin: 4px 0;
+    border-bottom: solid 1px $pf-colour-thin-border;
+    position: sticky;
+    top: 0;
+  }
+
+  li {
+    font-family: $pf-font;
+    cursor: pointer;
+    list-style-type: none;
+    padding: 4px 8px;
+    border-radius: $pf-border-radius;
+    align-items: center;
+    font-weight: lighter;
+    &:hover {
+      background-color: $pf-minimal-background-hover;
+    }
+    &.pf-highlighted {
+      background-color: $pf-primary-background;
+      color: white;
+    }
   }
 }
diff --git a/ui/src/assets/widgets/form.scss b/ui/src/assets/widgets/form.scss
index 6aadc68..13b0e77 100644
--- a/ui/src/assets/widgets/form.scss
+++ b/ui/src/assets/widgets/form.scss
@@ -38,6 +38,8 @@
 // Add some spacing around forms when placed inside a menu
 .pf-menu {
   .pf-form {
-    margin: 4px;
+    // Compensate for the uneven |4px 0| padding in pf-menu, resulting in an
+    // even 4px padding all around the form.
+    margin: 0 4px;
   }
 }
diff --git a/ui/src/assets/widgets/menu.scss b/ui/src/assets/widgets/menu.scss
index 83cb9c4..523ca9f 100644
--- a/ui/src/assets/widgets/menu.scss
+++ b/ui/src/assets/widgets/menu.scss
@@ -18,6 +18,7 @@
   display: flex;
   flex-direction: column;
   align-items: stretch;
+  margin: 4px 0;
 
   .pf-menu-item {
     font-family: $pf-font;
diff --git a/ui/src/assets/widgets/popup.scss b/ui/src/assets/widgets/popup.scss
index 0b9e666..5b3c0bd 100644
--- a/ui/src/assets/widgets/popup.scss
+++ b/ui/src/assets/widgets/popup.scss
@@ -24,7 +24,7 @@
   border: solid 1px $pf-colour-thin-border;
   border-radius: $pf-border-radius;
   box-shadow: 2px 2px 16px rgba(0, 0, 0, 0.2);
-  padding: 4px;
+
   .pf-popup-content {
     // Ensures all content is rendered above the arrow
     position: relative;
diff --git a/ui/src/common/viewer.ts b/ui/src/common/viewer.ts
index 49f1203..9169214 100644
--- a/ui/src/common/viewer.ts
+++ b/ui/src/common/viewer.ts
@@ -22,13 +22,13 @@
   sidebar = {
     hide: () => {
       globals.dispatch(Actions.setSidebar({
-        visible: true,
+        visible: false,
       }));
     },
     show:
         () => {
           globals.dispatch(Actions.setSidebar({
-            visible: false,
+            visible: true,
           }));
         },
     isVisible: () => globals.state.sidebarVisible,
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index c3ad90e..15c01f2 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -20,8 +20,10 @@
 import {assertExists} from '../base/logging';
 import {undoCommonChatAppReplacements} from '../base/string_utils';
 import {Actions} from '../common/actions';
+import {CommandWithMatchInfo} from '../common/commands';
 import {setTimestampFormat, TimestampFormat} from '../common/time';
 import {raf} from '../core/raf_scheduler';
+import {Command} from '../public';
 
 import {addTab} from './bottom_tab';
 import {onClickCopy} from './clipboard';
@@ -90,6 +92,7 @@
   private pendingPrompt?: Prompt;
   static readonly OMNIBOX_INPUT_REF = 'omnibox';
   private omniboxInputEl?: HTMLInputElement;
+  private recentCommands: string[] = [];
 
   private enterCommandMode(): void {
     this.omniboxMode = OmniboxMode.Command;
@@ -280,6 +283,38 @@
       name: 'Search',
       callback: () => this.enterSearchMode(true),
     },
+    {
+      id: 'perfetto.binder.system_server.Incoming',
+      name: 'Run query: system_server incoming binder graph',
+      callback: () => runQueryInNewTab(
+          `SELECT IMPORT('android.binder');
+           SELECT * FROM android_binder_incoming_graph((SELECT utid FROM thread WHERE name = 'system_server'))`,
+          'system_server incoming binder graph'),
+    },
+    {
+      id: 'perfetto.binder.system_server.Outgoing',
+      name: 'Run query: system_server outgoing binder graph',
+      callback: () => runQueryInNewTab(
+          `SELECT IMPORT('android.binder');
+           SELECT * FROM android_binder_outgoing_graph((SELECT utid FROM thread WHERE name = 'system_server'))`,
+          'system_server outgoing binder graph'),
+    },
+    {
+      id: 'perfetto.monitor_contention.system_server',
+      name: 'Run query: system_server monitor_contention graph',
+      callback: () => runQueryInNewTab(
+          `SELECT IMPORT('android.monitor_contention');
+           SELECT * FROM android_monitor_contention_graph((SELECT upid FROM process WHERE name = 'system_server'))`,
+          'system_server monitor_contention graph'),
+    },
+    {
+      id: 'perfetto.binder.all',
+      name: 'Run query: all process binder graph',
+      callback: () => runQueryInNewTab(
+          `SELECT IMPORT('android.binder');
+           SELECT * FROM android_binder_graph(-1000, 1000, -1000, 1000)`,
+          'all process binder graph'),
+    },
   ];
 
   commands() {
@@ -351,7 +386,7 @@
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'prompt-mode',
       closeOnOutsideClick: true,
-      options,
+      options: options && [{options}],
       selectedOptionIndex: this.omniboxSelectionIndex,
       onSelectedOptionChanged: (index) => {
         this.omniboxSelectionIndex = index;
@@ -359,6 +394,7 @@
       },
       onInput: (value) => {
         this.omniboxText = value;
+        this.omniboxSelectionIndex = 0;
         raf.scheduleFullRedraw();
       },
       onSubmit: (value, _alt) => {
@@ -372,20 +408,42 @@
 
   renderCommandOmnibox(): m.Children {
     const cmdMgr = globals.commandManager;
+
+    // Fuzzy-filter commands by the filter string.
     const filteredCmds = cmdMgr.fuzzyFilterCommands(this.omniboxText);
-    const options: OmniboxOption[] = filteredCmds.map(({segments, id}) => {
+
+    // Sort commands alphabetically
+    const sortedFilteredCommands =
+        filteredCmds.sort((a, b) => a.name.localeCompare(b.name));
+
+    // Look up recent comands
+    const recents = this.findRecentCommands(filteredCmds);
+
+    const cmdToOpt = ({segments, id}: CommandWithMatchInfo): OmniboxOption => {
       return {
         key: id,
         displayName: segments,
       };
-    });
+    };
+
+    const recentOptions = recents.map(cmdToOpt);
+    const allOptions = sortedFilteredCommands.map(cmdToOpt);
+
+    const optionCategories = recentOptions.length > 0 ?
+        [
+          {name: 'Recent Commands', options: recentOptions},
+          {name: 'All Commands', options: allOptions},
+        ] :
+        [
+          {name: 'All Commands', options: allOptions},
+        ];
 
     return m(Omnibox, {
       value: this.omniboxText,
-      placeholder: 'Start typing a command...',
+      placeholder: 'Filter commands...',
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'command-mode',
-      options,
+      options: optionCategories,
       closeOnSubmit: true,
       closeOnOutsideClick: true,
       selectedOptionIndex: this.omniboxSelectionIndex,
@@ -395,6 +453,7 @@
       },
       onInput: (value) => {
         this.omniboxText = value;
+        this.omniboxSelectionIndex = 0;
         raf.scheduleFullRedraw();
       },
       onClose: () => {
@@ -405,11 +464,30 @@
         raf.scheduleFullRedraw();
       },
       onSubmit: (key: string) => {
+        this.addRecentCommand(key);
         cmdMgr.runCommand(key);
       },
     });
   }
 
+  // Locates all recent commands within a given list of commands.
+  private findRecentCommands<T extends Command>(cmds: T[]): T[] {
+    const recents: T[] = [];
+    for (const recentCmdId of this.recentCommands) {
+      const cmd = cmds.find(({id}) => id === recentCmdId);
+      if (cmd) {
+        recents.push(cmd);
+      }
+    }
+    return recents;
+  }
+
+  private addRecentCommand(id: string): void {
+    this.recentCommands = this.recentCommands.filter((x) => x !== id);
+    this.recentCommands.unshift(id);
+    this.recentCommands.length = Math.min(this.recentCommands.length, 6);
+  }
+
   renderQueryOmnibox(): m.Children {
     const ph = 'e.g. select * from sched left join thread using(utid) limit 10';
     return m(Omnibox, {
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 2bf320f..b70eb25 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -65,10 +65,6 @@
   return slice.process?.pid;
 }
 
-function getUpidFromSlice(slice: SliceDetails): number|undefined {
-  return slice.process?.upid;
-}
-
 function getProcessNameFromSlice(slice: SliceDetails): string|undefined {
   return slice.process?.name;
 }
@@ -77,10 +73,6 @@
   return slice.name !== undefined;
 }
 
-function hasId(slice: SliceDetails): boolean {
-  return slice.id !== undefined;
-}
-
 function hasTid(slice: SliceDetails): boolean {
   return getTidFromSlice(slice) !== undefined;
 }
@@ -95,7 +87,7 @@
 
 const ITEMS: ContextMenuItem[] = [
   {
-    name: 'Average duration',
+    name: 'Average duration of slice name',
     shouldDisplay: (slice: SliceDetails) => hasName(slice),
     run: (slice: SliceDetails) => runQueryInNewTab(
         `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
@@ -103,19 +95,7 @@
         ),
   },
   {
-    name: 'Binder by TXN',
-    shouldDisplay: () => true,
-    run: () => runQueryInNewTab(
-        `SELECT IMPORT('android.binder');
-
-         SELECT *
-         FROM android_sync_binder_metrics_by_txn
-         ORDER BY client_dur DESC`,
-        'Binder by TXN',
-        ),
-  },
-  {
-    name: 'Binder call names',
+    name: 'Binder call names on thread',
     shouldDisplay: (slice) =>
         hasProcessName(slice) && hasTid(slice) && hasPid(slice),
     run: (slice: SliceDetails) => {
@@ -139,18 +119,6 @@
                   ));
     },
   },
-  {
-    name: 'Lock graph',
-    shouldDisplay: (slice: SliceDetails) => hasId(slice),
-    run: (slice: SliceDetails) => runQueryInNewTab(
-        `
-         SELECT IMPORT('android.monitor_contention');
-
-         SELECT * FROM android_monitor_contention_graph(${getUpidFromSlice(slice)});
-        `,
-        'Lock graph',
-        ),
-  },
 ];
 
 function getSliceContextMenuItems(slice: SliceDetails) {
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index 4013910..f2d7279 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -41,7 +41,7 @@
   view({attrs}: m.Vnode<OmniboxOptionRowAttrs>): void|m.Children {
     const {displayName, highlighted, ...htmlAttrs} = attrs;
     return m(
-        'li.pf-omnibox-option',
+        'li',
         {
           class: classNames(highlighted && 'pf-highlighted'),
           ...htmlAttrs,
@@ -70,10 +70,24 @@
   }
 }
 
+// Omnibox option.
 export interface OmniboxOption {
-  // The value to place into the omnibox.
+  // The value to place into the omnibox. This is what's returned in onSubmit.
   key: string;
-  displayName: FuzzySegment[];
+
+  // Display name provided as a string or a list of fuzzy segments to enable
+  // fuzzy match highlighting.
+  displayName: FuzzySegment[]|string;
+}
+
+// A category of omnibox options.
+export interface OmniboxOptionsCategory {
+  // Optional name of the category. If omitted, the divider for this category
+  // shall be omitted.
+  name?: string;
+
+  // List of options in this category.
+  options: OmniboxOption[];
 }
 
 export interface OmniboxAttrs {
@@ -95,7 +109,10 @@
   // Dropdown items to show. If none are supplied, the omnibox runs in free text
   // mode, where anyt text can be input. Otherwise, onSubmit will always be
   // called with one of the options.
-  options?: OmniboxOption[];
+  // Options are provided in groups called categories. If the category has a
+  // name the name will be listed at the top of the group rendered with a little
+  // divider as well.
+  options?: OmniboxOptionsCategory[];
 
   // Called when the user expresses the intent to "execute" the thing.
   onSubmit?: (value: string, mod: boolean, shift: boolean) => void;
@@ -144,10 +161,12 @@
       selectedOptionIndex = 0,
     } = attrs;
 
+    const flatOptions = options?.flatMap(({options}) => options);
+
     return m(
         Popup,
         {
-          className: 'pf-popup-padded',
+          className: 'pf-omnibox-dropdown',
           onPopupMount: (dom: HTMLElement) => this.popupElement = dom,
           onPopupUnMount: (_dom: HTMLElement) => this.popupElement = undefined,
           isOpen: exists(options),
@@ -174,7 +193,7 @@
                     this.close(attrs);
                   }
 
-                  if (options) {
+                  if (flatOptions) {
                     if (e.key === 'ArrowUp') {
                       e.preventDefault();
                       this.highlightPreviousOption(attrs);
@@ -184,7 +203,7 @@
                     } else if (e.key === 'Enter') {
                       e.preventDefault();
 
-                      const option = options[selectedOptionIndex];
+                      const option = flatOptions[selectedOptionIndex];
                       if (option) {
                         closeOnSubmit && this.close(attrs);
 
@@ -223,23 +242,28 @@
 
     if (!options) return null;
 
+    let index = 0;
+
     if (options.length === 0) {
       return m(EmptyState, {header: 'No matching options...'});
     } else {
-      return m(
-          'ul.pf-omnibox-dropdown',
-          options.map(({displayName, key}, index) => {
-            return m(OmniboxOptionRow, {
-              key,
-              displayName: displayName,
-              highlighted: index === selectedOptionIndex,
-              onclick: () => {
-                closeOnSubmit && onClose();
-                onSubmit(key, false, false);
-              },
-            });
-          }),
-      );
+      return options.map(({name, options}) => {
+        return m(
+            'ul.pf-omnibox-dropdown',
+            name && m('.pf-omnibox-section-header', name),
+            options.map(({displayName, key}) => {
+              return m(OmniboxOptionRow, {
+                key,
+                displayName: displayName,
+                highlighted: index++ === selectedOptionIndex,
+                onclick: () => {
+                  closeOnSubmit && onClose();
+                  onSubmit(key, false, false);
+                },
+              });
+            }),
+        );
+      });
     }
   }
 
@@ -304,7 +328,7 @@
       options = [],
     } = attrs;
 
-    const max = options.length - 1;
+    const max = options.reduce((sum, {options}) => sum + options.length, 0) - 1;
     onSelectedOptionChanged(Math.min(max, selectedOptionIndex + 1));
   }
 }