Merge changes I8ed3c55e,I021610d7 into main

* changes:
  tp: Migrate stdlib to CREATE PERFETTO INDEX
  tp: Fix validation
diff --git a/infra/luci/PRESUBMIT.py b/infra/luci/PRESUBMIT.py
index c1564ce..d46a2ba 100644
--- a/infra/luci/PRESUBMIT.py
+++ b/infra/luci/PRESUBMIT.py
@@ -18,12 +18,15 @@
 
 
 def CommonChecks(input_api, output_api):
-  recipes_py = input_api.os_path.join(input_api.PresubmitLocalPath(),
-                                      'recipes.py')
+  presubmit_dir = input_api.PresubmitLocalPath()
+  recipes_py = input_api.os_path.join(presubmit_dir, 'recipes.py')
+  recipes_cfg = input_api.os_path.join(
+      presubmit_dir, '..', 'config', 'recipes.cfg'
+  )
   return input_api.RunTests([
       input_api.Command(
           'Run recipe tests',
-          ['python3', recipes_py, 'test', 'run'],
+          ['python3', recipes_py, '--package', recipes_cfg, 'test', 'run'],
           {},
           output_api.PresubmitError,
       )
diff --git a/src/trace_redaction/collect_frame_cookies_unittest.cc b/src/trace_redaction/collect_frame_cookies_unittest.cc
index 2a38160..dc6d8fd 100644
--- a/src/trace_redaction/collect_frame_cookies_unittest.cc
+++ b/src/trace_redaction/collect_frame_cookies_unittest.cc
@@ -139,8 +139,6 @@
   ASSERT_OK(collect_.End(context));
 }
 
-}  // namespace
-
 class FrameCookieTest : public testing::Test {
  protected:
   CollectFrameCookies collect_;
@@ -414,4 +412,6 @@
 
   ASSERT_FALSE(redacted.has_frame_timeline_event());
 }
+
+}  // namespace
 }  // namespace perfetto::trace_redaction
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 567deae..4f50c43 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -31,29 +31,32 @@
 @import "viz_page";
 @import "widgets_page";
 @import "plugins_page";
+
+// Widgets - keep these sorted (they should NOT have any inter-dependencies)
 @import "widgets/anchor";
 @import "widgets/button";
+@import "widgets/callout";
 @import "widgets/checkbox";
 @import "widgets/details_shell";
+@import "widgets/editor";
 @import "widgets/empty_state";
 @import "widgets/error";
 @import "widgets/form";
 @import "widgets/grid_layout";
+@import "widgets/hotkey";
 @import "widgets/menu";
 @import "widgets/multiselect";
 @import "widgets/popup";
 @import "widgets/section";
-@import "widgets/timestamp";
 @import "widgets/select";
 @import "widgets/spinner";
 @import "widgets/switch";
+@import "widgets/tag_input";
 @import "widgets/text_input";
-@import "widgets/tree";
-@import "widgets/virtual_scroll_container";
-@import "widgets/callout";
-@import "widgets/editor";
-@import "widgets/vega_view";
-@import "widgets/hotkey";
 @import "widgets/text_paragraph";
+@import "widgets/timestamp";
+@import "widgets/tree";
 @import "widgets/treetable";
+@import "widgets/vega_view";
+@import "widgets/virtual_scroll_container";
 @import "widgets/virtual_table";
diff --git a/ui/src/assets/widgets/tag_input.scss b/ui/src/assets/widgets/tag_input.scss
new file mode 100644
index 0000000..c91c608
--- /dev/null
+++ b/ui/src/assets/widgets/tag_input.scss
@@ -0,0 +1,67 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@import "theme";
+
+.pf-tag-input {
+  font-family: $pf-font;
+  font-size: inherit;
+  outline: none; // Disable the default outline
+  border: none; // Disable the default border
+  border-bottom: solid 1px $pf-minimal-foreground; // Thin underline
+  background: none;
+  transition: border $pf-anim-timing, box-shadow $pf-anim-timing,
+    background $pf-anim-timing;
+
+  // Round only the top corners to avoid rounding the edges of the underline
+  border-radius: $pf-border-radius $pf-border-radius 0 0;
+
+  input {
+    outline: none;
+    border: none;
+    background: none;
+    font-family: inherit;
+    font-size: inherit;
+  }
+
+  i {
+    cursor: pointer;
+    font-size: smaller;
+    margin-left: 2px;
+  }
+
+  .pf-tag {
+    border-radius: $pf-border-radius;
+    background: $pf-primary-background;
+    margin-right: 2px;
+    color: $pf-primary-foreground;
+    padding-inline: 4px;
+    white-space: nowrap;
+  }
+
+  // The gentle hover effect indicates this component is interactive
+  &:hover {
+    background: $pf-minimal-background-hover;
+  }
+
+  &:focus-within {
+    background: $pf-minimal-background-hover;
+    border-bottom: solid 1px $pf-primary-background;
+
+    // The box-shadow thickens the bottom border, without adding to the height.
+    // This is the same technique used by materializecss:
+    // See https://materializecss.com/text-inputs.html
+    box-shadow: 0 1px 0 $pf-primary-background;
+  }
+}
diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index 98fc400..37a8eac 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -55,6 +55,7 @@
   VirtualTableAttrs,
   VirtualTableRow,
 } from '../widgets/virtual_table';
+import {TagInput} from '../widgets/tag_input';
 
 const DATA_ENGLISH_LETTER_FREQUENCY = {
   table: [
@@ -590,6 +591,32 @@
   rows: [],
 };
 
+function TagInputDemo() {
+  const tags: string[] = ['foo', 'bar', 'baz'];
+  let tagInputValue: string = '';
+
+  return {
+    view: () => {
+      return m(TagInput, {
+        tags,
+        value: tagInputValue,
+        onTagAdd: (tag) => {
+          tags.push(tag);
+          tagInputValue = '';
+          raf.scheduleFullRedraw();
+        },
+        onChange: (value) => {
+          tagInputValue = value;
+        },
+        onTagRemove: (index) => {
+          tags.splice(index);
+          raf.scheduleFullRedraw();
+        },
+      });
+    },
+  };
+}
+
 export const WidgetsPage = createPage({
   view() {
     return m(
@@ -1205,6 +1232,15 @@
           return m(VirtualTable, attrs);
         },
       }),
+      m(WidgetShowcase, {
+        label: 'Tag Input',
+        description: `
+          TagInput displays Tag elements inside an input, followed by an
+          interactive text input. The container is styled to look like a
+          TextInput, but the actual editable element appears after the last tag.
+          Clicking anywhere on the container will focus the text input.`,
+        renderWidget: () => m(TagInputDemo),
+      }),
     );
   },
 });
diff --git a/ui/src/plugins/org.kernel.Wattson/index.ts b/ui/src/plugins/org.kernel.Wattson/index.ts
index dcc8315..a8fb89b 100644
--- a/ui/src/plugins/org.kernel.Wattson/index.ts
+++ b/ui/src/plugins/org.kernel.Wattson/index.ts
@@ -23,7 +23,6 @@
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
-import {NUM} from '../../trace_processor/query_result';
 
 class Wattson implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -36,44 +35,21 @@
       ctx.registerStaticTrack({
         uri: `perfetto.CpuSubsystemEstimate#CPU${cpu}`,
         displayName: `Cpu${cpu} Estimate`,
-        kind: `CpuEstimateTrack`,
+        kind: `CpuSubsystemEstimateTrack`,
         trackFactory: ({trackKey}) =>
           new CpuSubsystemEstimateTrack(ctx.engine, trackKey, queryKey),
         groupName: `Wattson`,
       });
     }
+
     ctx.registerStaticTrack({
-      uri: `perfetto.CpuSubsystemEstimate#Static`,
-      displayName: `Static Estimate`,
-      kind: `CpuEstimateTrack`,
+      uri: `perfetto.CpuSubsystemEstimate#ScuInterconnect`,
+      displayName: `SCU Interconnect Estimate`,
+      kind: `CpuSubsystemEstimateTrack`,
       trackFactory: ({trackKey}) =>
-        new CpuSubsystemEstimateTrack(ctx.engine, trackKey, `static_curve`),
+        new CpuSubsystemEstimateTrack(ctx.engine, trackKey, `scu`),
       groupName: `Wattson`,
     });
-
-    // Cache estimates for remainder of CPU subsystem
-    const L3RowCount = await ctx.engine.query(`
-        SELECT
-          COUNT(*) as numRows
-        FROM _system_state_curves
-        WHERE l3_hit_value is NOT NULL AND l3_hit_value != 0
-    `);
-    const numL3Rows = L3RowCount.firstRow({numRows: NUM}).numRows;
-
-    if (numL3Rows > 0) {
-      const queryKeys: string[] = [`l3_hit_value`, `l3_miss_value`];
-      for (const queryKey of queryKeys) {
-        const keyName = queryKey.replace(`_value`, ``).replace(`l3`, `L3`);
-        ctx.registerStaticTrack({
-          uri: `perfetto.CpuSubsystemEstimate#${keyName}`,
-          displayName: `${keyName} Estimate`,
-          kind: `CacheEstimateTrack`,
-          trackFactory: ({trackKey}) =>
-            new CpuSubsystemEstimateTrack(ctx.engine, trackKey, queryKey),
-          groupName: `Wattson`,
-        });
-      }
-    }
   }
 }
 
@@ -92,26 +68,25 @@
 
   protected getDefaultCounterOptions(): CounterOptions {
     const options = super.getDefaultCounterOptions();
+    options.yRangeSharingKey = `CpuSubsystem`;
     options.unit = `mW`;
     return options;
   }
 
   getSqlSource() {
-    const isL3 = this.queryKey.startsWith(`l3`);
-    return isL3
-      ? `
-      select
-        ts,
-        -- scale by 1000 because dividing by ns and LUTs are scaled by 10^6
-        ${this.queryKey} * 1000 / dur as value
-      from _system_state_curves
-    `
-      : `
-      select
-        ts,
-        ${this.queryKey} as value
-      from _system_state_curves
-    `;
+    if (this.queryKey.startsWith(`cpu`)) {
+      return `select ts, ${this.queryKey} as value from _system_state_curves`;
+    } else {
+      return `
+        select
+          ts,
+          -- L3 values are scaled by 1000 because it's divided by ns and L3 LUTs
+          -- are scaled by 10^6. This brings to same units as static_curve (mW)
+          ((IFNULL(l3_hit_value, 0) + IFNULL(l3_miss_value, 0)) * 1000 / dur)
+            + static_curve  as value
+        from _system_state_curves
+      `;
+    }
   }
 }
 
diff --git a/ui/src/widgets/tag_input.ts b/ui/src/widgets/tag_input.ts
new file mode 100644
index 0000000..ed8a5f3
--- /dev/null
+++ b/ui/src/widgets/tag_input.ts
@@ -0,0 +1,141 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {HTMLAttrs} from './common';
+import {Icon} from './icon';
+import {findRef} from '../base/dom_utils';
+
+export interface TagInputAttrs extends HTMLAttrs {
+  value?: string;
+  onChange?: (text: string) => void;
+  tags: string[];
+  onTagAdd: (text: string) => void;
+  onTagRemove: (index: number) => void;
+}
+
+const INPUT_REF = 'input';
+
+/**
+ * TagInput displays Tag elements inside an input, followed by an interactive
+ * text input. The container is styled to look like a TextInput, but the actual
+ * editable element appears after the last tag. Clicking anywhere on the
+ * container will focus the text input.
+ *
+ * To use this widget, the user must provide the tags as a list of strings, and
+ * provide callbacks which are called when the user modifies the list of tags,
+ * either adding a new tag by typing and pressing enter, or removing a tag by
+ * clicking the close button on a tag.
+ *
+ * The text value can be optionally be controlled, which allows access to this
+ * value from outside the widget.
+ *
+ * Uncontrolled example:
+ *
+ * In this example, we only have access to the list of tags from outside.
+ *
+ * ```
+ * const tags = [];
+ *
+ * m(TagInput, {
+ *   tags,
+ *   onTagAdd: (tag) => tags.push(tag),
+ *   onTagRemove: (index) => tags.splice(index),
+ * });
+ * ```
+ *
+ * Controlled example:
+ *
+ * In this example we have complete control over the value in the text field.
+ *
+ * ```
+ * const tags = [];
+ * let value = '';
+ *
+ * m(TagInput, {
+ *   tags,
+ *   onTagAdd: (tag) => {
+ *     tags.push(tag);
+ *     value = ''; // The value is controlled so we must manually clear it here
+ *   },
+ *   onTagRemove: (index) => tags.splice(index),
+ *   value,
+ *   onChange: (x) => value = x,
+ * });
+ * ```
+ *
+ */
+
+export class TagInput implements m.ClassComponent<TagInputAttrs> {
+  view({attrs}: m.CVnode<TagInputAttrs>) {
+    const {value, onChange, tags, onTagAdd, onTagRemove, ...htmlAttrs} = attrs;
+
+    const valueIsControlled = value !== undefined;
+
+    return m(
+      '.pf-tag-input',
+      {
+        onclick: (ev: PointerEvent) => {
+          const target = ev.currentTarget as HTMLElement;
+          const inputElement = findRef(target, INPUT_REF);
+          if (inputElement) {
+            (inputElement as HTMLInputElement).focus();
+          }
+        },
+        ...htmlAttrs,
+      },
+      tags.map((tag, index) => renderTag(tag, () => onTagRemove(index))),
+      m('input', {
+        ref: INPUT_REF,
+        value,
+        onkeydown: (ev: KeyboardEvent) => {
+          if (ev.key === 'Enter') {
+            const el = ev.target as HTMLInputElement;
+            if (el.value.trim() !== '') {
+              onTagAdd(el.value);
+              if (!valueIsControlled) {
+                el.value = '';
+              }
+            }
+          } else if (ev.key === 'Backspace') {
+            const el = ev.target as HTMLInputElement;
+            if (el.value !== '') return;
+            if (tags.length === 0) return;
+
+            const lastTagIndex = tags.length - 1;
+            onTagRemove(lastTagIndex);
+          }
+        },
+        oninput: (ev: InputEvent) => {
+          const el = ev.target as HTMLInputElement;
+          onChange?.(el.value);
+        },
+      }),
+    );
+  }
+}
+
+function renderTag(text: string, onRemove: () => void): m.Children {
+  return m(
+    'span.pf-tag',
+    text,
+    m(Icon, {
+      icon: 'close',
+      onclick: () => {
+        onRemove();
+      },
+    }),
+  );
+}