Merge "ui: make ftrace track 10x faster" into main
diff --git a/Android.bp b/Android.bp
index 69c599d..a0b685c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5481,6 +5481,7 @@
         "protos/perfetto/metrics/chrome/dropped_frames.proto",
         "protos/perfetto/metrics/chrome/frame_times.proto",
         "protos/perfetto/metrics/chrome/histogram_hashes.proto",
+        "protos/perfetto/metrics/chrome/histogram_summaries.proto",
         "protos/perfetto/metrics/chrome/long_latency.proto",
         "protos/perfetto/metrics/chrome/media_metric.proto",
         "protos/perfetto/metrics/chrome/performance_mark_hashes.proto",
@@ -6790,6 +6791,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7220,6 +7222,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7312,6 +7315,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.cc",
@@ -7404,6 +7408,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h",
@@ -7492,6 +7497,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7583,6 +7589,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pb.cc",
@@ -7674,6 +7681,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pb.h",
@@ -7762,6 +7770,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7854,6 +7863,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.cc",
@@ -7946,6 +7956,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h",
@@ -13224,6 +13235,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_args_class_names.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_template.sql",
@@ -15152,6 +15164,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -16493,6 +16506,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
diff --git a/BUILD b/BUILD
index d4f1f1b..d4d3aff 100644
--- a/BUILD
+++ b/BUILD
@@ -2541,6 +2541,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_args_class_names.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_template.sql",
@@ -5230,6 +5231,7 @@
         "protos/perfetto/metrics/chrome/dropped_frames.proto",
         "protos/perfetto/metrics/chrome/frame_times.proto",
         "protos/perfetto/metrics/chrome/histogram_hashes.proto",
+        "protos/perfetto/metrics/chrome/histogram_summaries.proto",
         "protos/perfetto/metrics/chrome/long_latency.proto",
         "protos/perfetto/metrics/chrome/media_metric.proto",
         "protos/perfetto/metrics/chrome/performance_mark_hashes.proto",
@@ -5625,6 +5627,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
diff --git a/protos/perfetto/metrics/chrome/BUILD.gn b/protos/perfetto/metrics/chrome/BUILD.gn
index bf47885..c0b0c36 100644
--- a/protos/perfetto/metrics/chrome/BUILD.gn
+++ b/protos/perfetto/metrics/chrome/BUILD.gn
@@ -27,6 +27,7 @@
     "dropped_frames.proto",
     "frame_times.proto",
     "histogram_hashes.proto",
+    "histogram_summaries.proto",
     "long_latency.proto",
     "media_metric.proto",
     "performance_mark_hashes.proto",
diff --git a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
index 596bc7f..ebc0ac5 100644
--- a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
+++ b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
@@ -23,6 +23,7 @@
 import "protos/perfetto/metrics/chrome/dropped_frames.proto";
 import "protos/perfetto/metrics/chrome/frame_times.proto";
 import "protos/perfetto/metrics/chrome/histogram_hashes.proto";
+import "protos/perfetto/metrics/chrome/histogram_summaries.proto";
 import "protos/perfetto/metrics/chrome/long_latency.proto";
 import "protos/perfetto/metrics/chrome/media_metric.proto";
 import "protos/perfetto/metrics/chrome/performance_mark_hashes.proto";
@@ -53,4 +54,5 @@
   optional ChromeUnsymbolizedArgs chrome_unsymbolized_args = 1014;
   optional ChromeArgsClassNames chrome_args_class_names = 1015;
   optional ChromeScrollJankV3 chrome_scroll_jank_v3 = 1017;
+  optional ChromeHistogramSummaries chrome_histogram_summaries = 1018;
 }
diff --git a/protos/perfetto/metrics/chrome/histogram_summaries.proto b/protos/perfetto/metrics/chrome/histogram_summaries.proto
new file mode 100644
index 0000000..57dad94
--- /dev/null
+++ b/protos/perfetto/metrics/chrome/histogram_summaries.proto
@@ -0,0 +1,43 @@
+
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+message HistogramSummary {
+  // The name of the histogram event
+  optional string name = 1;
+  // The avarage value of the histogram event
+  optional int64 mean = 2;
+  // The number of the histogram event in the trace track
+  optional uint32 count = 3;
+  // The sum of value of the histogram event
+  optional int64 sum = 4;
+  // The maximum value of the histogram event
+  optional int64 max = 5;
+  // The 90 percentile value of the histogram event
+  optional int64 p90 = 6;
+  // The 50 percentile (median) value of the histogram event
+  optional int64 p50 = 7;
+}
+
+// The list of the summary of Chrome Histograms in trace track events.
+// This includes the statistic information of each histograms from Chrome.
+message ChromeHistogramSummaries {
+  repeated HistogramSummary histogram_summary = 1;
+}
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 7319dd4..8565125 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -42,6 +42,7 @@
   "fastrpc.proto",
   "fence.proto",
   "filemap.proto",
+  "fs.proto",
   "ftrace.proto",
   "g2d.proto",
   "google_icc_trace.proto",
diff --git a/protos/perfetto/trace/ftrace/fs.proto b/protos/perfetto/trace/ftrace/fs.proto
new file mode 100644
index 0000000..4cc3531
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/fs.proto
@@ -0,0 +1,15 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message DoSysOpenFtraceEvent {
+  optional string filename = 1;
+  optional int32 flags = 2;
+  optional int32 mode = 3;
+}
+message OpenExecFtraceEvent {
+  optional string filename = 1;
+}
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index b363eb0..40e7113 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -42,6 +42,7 @@
 import "protos/perfetto/trace/ftrace/fastrpc.proto";
 import "protos/perfetto/trace/ftrace/fence.proto";
 import "protos/perfetto/trace/ftrace/filemap.proto";
+import "protos/perfetto/trace/ftrace/fs.proto";
 import "protos/perfetto/trace/ftrace/ftrace.proto";
 import "protos/perfetto/trace/ftrace/g2d.proto";
 import "protos/perfetto/trace/ftrace/google_icc_trace.proto";
@@ -683,5 +684,7 @@
     DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
     KprobeEvent kprobe_event = 542;
     ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
+    DoSysOpenFtraceEvent do_sys_open = 544;
+    OpenExecFtraceEvent open_exec = 545;
   }
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 5e07a0c..6104064 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -8805,6 +8805,19 @@
 
 // End of protos/perfetto/trace/ftrace/filemap.proto
 
+// Begin of protos/perfetto/trace/ftrace/fs.proto
+
+message DoSysOpenFtraceEvent {
+  optional string filename = 1;
+  optional int32 flags = 2;
+  optional int32 mode = 3;
+}
+message OpenExecFtraceEvent {
+  optional string filename = 1;
+}
+
+// End of protos/perfetto/trace/ftrace/fs.proto
+
 // Begin of protos/perfetto/trace/ftrace/ftrace.proto
 
 message PrintFtraceEvent {
@@ -11424,6 +11437,8 @@
     DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
     KprobeEvent kprobe_event = 542;
     ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
+    DoSysOpenFtraceEvent do_sys_open = 544;
+    OpenExecFtraceEvent open_exec = 545;
   }
 }
 
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index bfb2a1f..86b4918 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -155,6 +155,8 @@
     StatusResult status = 210;
     // For TPM_REGISTER_SQL_PACKAGE.
     RegisterSqlPackageResult register_sql_package_result = 211;
+    // For TPM_FINALIZE_TRACE_DATA.
+    FinalizeDataResult finalize_data_result = 212;
   }
 
   // Previously: RawQueryArgs for TPM_QUERY_RAW_DEPRECATED
@@ -356,4 +358,8 @@
 
 message RegisterSqlPackageResult {
   optional string error = 1;
-}
\ No newline at end of file
+}
+
+message FinalizeDataResult {
+  optional string error = 1;
+}
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index 976fb9b..40cec35 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py
index d53b0c3..a34ff08 100755
--- a/python/tools/check_ratchet.py
+++ b/python/tools/check_ratchet.py
@@ -36,7 +36,7 @@
 
 from dataclasses import dataclass
 
-EXPECTED_ANY_COUNT = 52
+EXPECTED_ANY_COUNT = 51
 EXPECTED_RUN_METRIC_COUNT = 4
 
 ROOT_DIR = os.path.dirname(
diff --git a/src/base/time.cc b/src/base/time.cc
index e799542..ad971af 100644
--- a/src/base/time.cc
+++ b/src/base/time.cc
@@ -188,10 +188,15 @@
 std::string GetTimeFmt(const std::string& fmt) {
   time_t raw_time;
   time(&raw_time);
-  struct tm* local_tm;
-  local_tm = localtime(&raw_time);
+  struct tm local_tm;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  PERFETTO_CHECK(localtime_s(&local_tm, &raw_time) == 0);
+#else
+  tzset();
+  PERFETTO_CHECK(localtime_r(&raw_time, &local_tm) != nullptr);
+#endif
   char buf[128];
-  PERFETTO_CHECK(strftime(buf, 80, fmt.c_str(), local_tm) > 0);
+  PERFETTO_CHECK(strftime(buf, 80, fmt.c_str(), &local_tm) > 0);
   return buf;
 }
 
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index 8c99f49..7eeadfb 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -537,3 +537,5 @@
 sched/sched_wakeup_task_attr
 devfreq/devfreq_frequency
 cpm_trace/param_set_value_cpm
+fs/do_sys_open
+fs/open_exec
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 53085f9..c9d3970 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
 namespace trace_processor {
 namespace {
 
-std::array<FtraceMessageDescriptor, 544> descriptors{{
+std::array<FtraceMessageDescriptor, 546> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -6016,6 +6016,24 @@
             {"timestamp", ProtoSchemaType::kInt64},
         },
     },
+    {
+        "do_sys_open",
+        3,
+        {
+            {},
+            {"filename", ProtoSchemaType::kString},
+            {"flags", ProtoSchemaType::kInt32},
+            {"mode", ProtoSchemaType::kInt32},
+        },
+    },
+    {
+        "open_exec",
+        1,
+        {
+            {},
+            {"filename", ProtoSchemaType::kString},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index 2da16c9..7c3e9f9 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -147,8 +147,10 @@
         TrackTracker::Group::kPower, batt_power_id);
     auto current = evt.current_ua();
     auto voltage = evt.voltage_uv();
-    context_->event_tracker->PushCounter(
-        ts, static_cast<double>(current * voltage / 1000000000), track);
+    // Current is negative when discharging, but we want the power counter to
+    // always be positive, so take the absolute value.
+    auto power = std::abs(static_cast<double>(current * voltage / 1000000000));
+    context_->event_tracker->PushCounter(ts, power, track);
   }
 }
 
diff --git a/src/trace_processor/metrics/sql/chrome/BUILD.gn b/src/trace_processor/metrics/sql/chrome/BUILD.gn
index 30962b8..7d10aeb 100644
--- a/src/trace_processor/metrics/sql/chrome/BUILD.gn
+++ b/src/trace_processor/metrics/sql/chrome/BUILD.gn
@@ -26,6 +26,7 @@
     "chrome_args_class_names.sql",
     "chrome_event_metadata.sql",
     "chrome_histogram_hashes.sql",
+    "chrome_histogram_summaries.sql",
     "chrome_input_to_browser_intervals.sql",
     "chrome_input_to_browser_intervals_base.sql",
     "chrome_input_to_browser_intervals_template.sql",
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql b/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql
new file mode 100644
index 0000000..7946491
--- /dev/null
+++ b/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql
@@ -0,0 +1,49 @@
+--
+-- Copyright 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
+--
+--     https://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.
+--
+
+INCLUDE PERFETTO MODULE chrome.histograms;
+
+DROP VIEW IF EXISTS HistogramSummaryTable;
+CREATE PERFETTO VIEW HistogramSummaryTable AS
+SELECT
+    hist.name AS histname,
+    CAST(AVG(hist.value) AS INTEGER) AS mean_histval,
+    COUNT(*) AS hist_count,
+    CAST(SUM(hist.value) AS INTEGER) AS sum_histval,
+    CAST(MAX(hist.value) AS INTEGER) AS max_histval,
+    CAST(PERCENTILE(hist.value, 90) AS INTEGER) AS p90_histval,
+    CAST(PERCENTILE(hist.value, 50) AS INTEGER) AS p50_histval
+FROM chrome_histograms hist
+GROUP BY hist.name;
+
+DROP VIEW IF EXISTS chrome_histogram_summaries_output;
+CREATE PERFETTO VIEW chrome_histogram_summaries_output AS
+SELECT ChromeHistogramSummaries(
+    'histogram_summary', (
+        SELECT RepeatedField(
+            HistogramSummary(
+                'name', histname,
+                'mean', mean_histval,
+                'count', hist_count,
+                'sum', sum_histval,
+                'max', max_histval,
+                'p90', p90_histval,
+                'p50', p50_histval
+            )
+        )
+        FROM HistogramSummaryTable
+    )
+);
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index 49feef3..d26cdc1 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -212,7 +212,11 @@
     }
     case RpcProto::TPM_FINALIZE_TRACE_DATA: {
       Response resp(tx_seq_id_++, req_type);
-      NotifyEndOfFile();
+      auto* result = resp->set_finalize_data_result();
+      base::Status res = NotifyEndOfFile();
+      if (!res.ok()) {
+        result->set_error(res.message());
+      }
       resp.Send(rpc_response_fn_);
       break;
     }
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 2cd9b20..f93e6e4 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -5045,6 +5045,32 @@
        kUnsetFtraceId,
        98,
        kUnsetSize},
+      {"do_sys_open",
+       "fs",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "filename", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "flags", 2, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "mode", 3, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       544,
+       kUnsetSize},
+      {"open_exec",
+       "fs",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "filename", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       545,
+       kUnsetSize},
       {"print",
        "ftrace",
        {
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format b/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format
new file mode 100644
index 0000000..d6bac92
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format
@@ -0,0 +1,13 @@
+name: do_sys_open
+ID: 685
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:__data_loc char[] filename;	offset:8;	size:4;	signed:1;
+	field:int flags;	offset:12;	size:4;	signed:1;
+	field:int mode;	offset:16;	size:4;	signed:1;
+
+print fmt: ""%s" %x %o", __get_str(filename), REC->flags, REC->mode
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format b/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format
new file mode 100644
index 0000000..9f6fe3d
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format
@@ -0,0 +1,11 @@
+name: open_exec
+ID: 686
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:__data_loc char[] filename;	offset:8;	size:4;	signed:1;
+
+print fmt: ""%s"", __get_str(filename)
diff --git a/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py b/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
index 046d1c4..ccdae4f 100644
--- a/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
+++ b/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
@@ -147,7 +147,7 @@
         packet {
           timestamp: 4000000
           battery {
-            current_ua: 510000
+            current_ua: -510000
             voltage_uv: 12000000
           }
         }
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index 7662755..c1045e2 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -214,8 +214,8 @@
     return this.appCtx.currentTrace?.forPlugin(this.pluginId);
   }
 
-  scheduleFullRedraw(): void {
-    raf.scheduleFullRedraw();
+  scheduleFullRedraw(force?: 'force'): void {
+    raf.scheduleFullRedraw(force);
   }
 
   get httpRpc() {
diff --git a/ui/src/core/command_manager.ts b/ui/src/core/command_manager.ts
index fdf6ee6..ad0f482 100644
--- a/ui/src/core/command_manager.ts
+++ b/ui/src/core/command_manager.ts
@@ -15,6 +15,7 @@
 import {FuzzyFinder, FuzzySegment} from '../base/fuzzy';
 import {Registry} from '../base/registry';
 import {Command, CommandManager} from '../public/command';
+import {raf} from './raf_scheduler';
 
 export interface CommandWithMatchInfo extends Command {
   segments: FuzzySegment[];
@@ -39,10 +40,11 @@
     return this.registry.register(cmd);
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any {
+  runCommand(id: string, ...args: unknown[]): unknown {
     const cmd = this.registry.get(id);
-    return cmd.callback(...args);
+    const res = cmd.callback(...args);
+    Promise.resolve(res).finally(() => raf.scheduleFullRedraw('force'));
+    return res;
   }
 
   // Returns a list of commands that match the search term, along with a list
diff --git a/ui/src/core/load_trace.ts b/ui/src/core/load_trace.ts
index c39245f..41196c7 100644
--- a/ui/src/core/load_trace.ts
+++ b/ui/src/core/load_trace.ts
@@ -147,7 +147,7 @@
       ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
     });
   }
-  engine.onResponseReceived = () => raf.scheduleFullRedraw();
+  engine.onResponseReceived = () => raf.scheduleFullRedraw('force');
 
   if (isMetatracingEnabled()) {
     engine.enableMetatrace(assertExists(getEnabledMetatracingCategories()));
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index b23379f..2935963 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -13,10 +13,19 @@
 // limitations under the License.
 
 import {PerfStats} from './perf_stats';
+import m from 'mithril';
+import {featureFlags} from './feature_flags';
 
 export type AnimationCallback = (lastFrameMs: number) => void;
 export type RedrawCallback = () => void;
 
+export const AUTOREDRAW_FLAG = featureFlags.register({
+  id: 'mithrilAutoredraw',
+  name: 'Enable Mithril autoredraw',
+  description: 'Turns calls to schedulefullRedraw() a no-op',
+  defaultValue: false,
+});
+
 // This class orchestrates all RAFs in the UI. It ensures that there is only
 // one animation frame handler overall and that callbacks are called in
 // predictable order. There are two types of callbacks here:
@@ -34,12 +43,12 @@
 
   // These happen at the end of full (DOM) animation frames.
   private postRedrawCallbacks = new Array<RedrawCallback>();
-  private syncDomRedrawFn: () => void = () => {};
   private hasScheduledNextFrame = false;
   private requestedFullRedraw = false;
   private isRedrawing = false;
   private _shutdown = false;
   private recordPerfStats = false;
+  private mounts = new Map<Element, m.ComponentTypes>();
 
   readonly perfStats = {
     rafActions: new PerfStats(),
@@ -49,16 +58,23 @@
     domRedraw: new PerfStats(),
   };
 
-  // Called by frontend/index.ts. syncDomRedrawFn is a function that invokes
-  // m.render() of the root UiMain component.
-  initialize(syncDomRedrawFn: () => void) {
-    this.syncDomRedrawFn = syncDomRedrawFn;
+  constructor() {
+    // Patch m.redraw() to our RAF full redraw.
+    const origSync = m.redraw.sync;
+    const redrawFn = () => this.scheduleFullRedraw('force');
+    redrawFn.sync = origSync;
+    m.redraw = redrawFn;
+
+    m.mount = this.mount.bind(this);
   }
 
   // Schedule re-rendering of virtual DOM and canvas.
   // If a callback is passed it will be executed after the DOM redraw has
   // completed.
-  scheduleFullRedraw(cb?: RedrawCallback) {
+  scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) {
+    // If we are using autoredraw mode, make this function a no-op unless
+    // 'force' is passed.
+    if (AUTOREDRAW_FLAG.get() && force !== 'force') return;
     this.requestedFullRedraw = true;
     cb && this.postRedrawCallbacks.push(cb);
     this.maybeScheduleAnimationFrame(true);
@@ -88,6 +104,16 @@
     };
   }
 
+  mount(element: Element, component: m.ComponentTypes | null): void {
+    const mounts = this.mounts;
+    if (component === null) {
+      mounts.delete(element);
+    } else {
+      mounts.set(element, component);
+    }
+    this.syncDomRedrawMountEntry(element, component);
+  }
+
   shutdown() {
     this._shutdown = true;
   }
@@ -103,12 +129,37 @@
 
   private syncDomRedraw() {
     const redrawStart = performance.now();
-    this.syncDomRedrawFn();
+
+    for (const [element, component] of this.mounts.entries()) {
+      this.syncDomRedrawMountEntry(element, component);
+    }
+
     if (this.recordPerfStats) {
       this.perfStats.domRedraw.addValue(performance.now() - redrawStart);
     }
   }
 
+  private syncDomRedrawMountEntry(
+    element: Element,
+    component: m.ComponentTypes | null,
+  ) {
+    // Mithril's render() function takes a third argument which tells us if a
+    // further redraw is needed (e.g. due to managed event handler). This allows
+    // us to implement auto-redraw. The redraw argument is documented in the
+    // official Mithril docs but is just not part of the @types/mithril package.
+    const mithrilRender = m.render as (
+      el: Element,
+      vnodes: m.Children,
+      redraw?: () => void,
+    ) => void;
+
+    mithrilRender(
+      element,
+      component !== null ? m(component) : null,
+      AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined,
+    );
+  }
+
   private syncCanvasRedraw() {
     const redrawStart = performance.now();
     if (this.isRedrawing) return;
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 99d4157..3e27b07 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -242,7 +242,7 @@
       this.uploadStatus = '';
       const uploader = new GcsUploader(this.traceData, {
         onProgress: () => {
-          raf.scheduleFullRedraw();
+          raf.scheduleFullRedraw('force');
           this.uploadStatus = uploader.getEtaString();
           if (uploader.state === 'UPLOADED') {
             this.traceState = 'UPLOADED';
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index 819f271..322748e 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -54,7 +54,7 @@
     nativeKeyboardLayoutMap()
       .then((keyMap: KeyboardLayoutMap) => {
         this.keyMap = keyMap;
-        AppImpl.instance.scheduleFullRedraw();
+        AppImpl.instance.scheduleFullRedraw('force');
       })
       .catch((e) => {
         if (
@@ -69,7 +69,7 @@
           // The alternative would be to show key mappings for all keyboard
           // layouts which is not feasible.
           this.keyMap = new EnglishQwertyKeyboardLayoutMap();
-          AppImpl.instance.scheduleFullRedraw();
+          AppImpl.instance.scheduleFullRedraw('force');
         } else {
           // Something unexpected happened. Either the browser doesn't conform
           // to the keyboard API spec, or the keyboard API spec has changed!
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 4c87e4d..600f08b 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -63,7 +63,7 @@
 });
 
 function routeChange(route: Route) {
-  raf.scheduleFullRedraw(() => {
+  raf.scheduleFullRedraw('force', () => {
     if (route.fragment) {
       // This needs to happen after the next redraw call. It's not enough
       // to use setTimeout(..., 0); since that may occur before the
@@ -148,7 +148,7 @@
   });
 
   // Wire up raf for widgets.
-  setScheduleFullRedraw(() => raf.scheduleFullRedraw());
+  setScheduleFullRedraw((force?: 'force') => raf.scheduleFullRedraw(force));
 
   // Load the css. The load is asynchronous and the CSS is not ready by the time
   // appendChild returns.
@@ -225,12 +225,8 @@
   const router = new Router();
   router.onRouteChanged = routeChange;
 
-  raf.initialize(() =>
-    m.render(
-      document.body,
-      m(UiMain, pages.renderPageForCurrentRoute(AppImpl.instance.trace)),
-    ),
-  );
+  // Mount the main mithril component. This also forces a sync render pass.
+  raf.mount(document.body, UiMain);
 
   if (
     (location.origin.startsWith('http://localhost:') ||
@@ -269,12 +265,6 @@
     routeChange(route);
   });
 
-  // Force one initial render to get everything in place
-  m.render(
-    document.body,
-    m(UiMain, AppImpl.instance.pages.renderPageForCurrentRoute(undefined)),
-  );
-
   // Initialize plugins, now that we are ready to go.
   const pluginManager = AppImpl.instance.plugins;
   CORE_PLUGINS.forEach((p) => pluginManager.registerPlugin(p));
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index 5f1e29a..c94ee3f 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -328,7 +328,13 @@
     document.removeEventListener('mousedown', this.onMouseDown);
   }
 
+  // This is defined as an arrow function to have a single handler that can be
+  // added/remove while keeping `this` bound.
   private onMouseDown = (e: Event) => {
+    // We need to schedule a redraw manually as this event handler was added
+    // manually to the DOM and doesn't use Mithril's auto-redraw system.
+    raf.scheduleFullRedraw('force');
+
     // Don't close if the click was within ourselves or our popup.
     if (e.target instanceof Node) {
       if (this.popupElement && this.popupElement.contains(e.target)) {
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 8ece2d3..9c03eb7 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -352,7 +352,9 @@
 }
 
 export class Sidebar implements m.ClassComponent<OptionalTraceImplAttrs> {
-  private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
+  private _redrawWhileAnimating = new Animation(() =>
+    raf.scheduleFullRedraw('force'),
+  );
   private _asyncJobPending = new Set<string>();
   private _sectionExpanded = new Map<string, boolean>();
 
@@ -523,7 +525,7 @@
       raf.scheduleFullRedraw();
       res.finally(() => {
         this._asyncJobPending.delete(itemId);
-        raf.scheduleFullRedraw();
+        raf.scheduleFullRedraw('force');
       });
     };
   }
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 0662ef8..04aee7e 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -163,7 +163,7 @@
         /* onDrag */ (_x, y) => {
           const deltaYSinceDragStart = dragStartY - y;
           this.resizableHeight = heightWhenDragStarted + deltaYSinceDragStart;
-          raf.scheduleFullRedraw();
+          raf.scheduleFullRedraw('force');
         },
         /* onDragStarted */ (_x, y) => {
           this.resizableHeight = this.height;
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index c67f5b7..b8d8dda 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -52,9 +52,9 @@
 // This wrapper creates a new instance of UiMainPerTrace for each new trace
 // loaded (including the case of no trace at the beginning).
 export class UiMain implements m.ClassComponent {
-  view({children}: m.CVnode) {
+  view() {
     const currentTraceId = AppImpl.instance.trace?.engine.engineId ?? '';
-    return [m(UiMainPerTrace, {key: currentTraceId}, children)];
+    return [m(UiMainPerTrace, {key: currentTraceId})];
   }
 }
 
@@ -629,12 +629,13 @@
     this.maybeFocusOmnibar();
   }
 
-  view({children}: m.Vnode): m.Children {
+  view(): m.Children {
+    const app = AppImpl.instance;
     const hotkeys: HotkeyConfig[] = [];
-    for (const {id, defaultHotkey} of AppImpl.instance.commands.commands) {
+    for (const {id, defaultHotkey} of app.commands.commands) {
       if (defaultHotkey) {
         hotkeys.push({
-          callback: () => AppImpl.instance.commands.runCommand(id),
+          callback: () => app.commands.runCommand(id),
           hotkey: defaultHotkey,
         });
       }
@@ -650,10 +651,10 @@
           omnibox: this.renderOmnibox(),
           trace: this.trace,
         }),
-        children,
+        app.pages.renderPageForCurrentRoute(app.trace),
         m(CookieConsent),
         maybeRenderFullscreenModalDialog(),
-        AppImpl.instance.perfDebugging.renderPerfStats(),
+        app.perfDebugging.renderPerfStats(),
       ),
     );
   }
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index aef7a76..37206a3 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -272,6 +272,7 @@
   private onmessage(msg: MessageEvent) {
     if (this._ctx === undefined) return; // Trace unloaded
     if (!('perfettoSync' in msg.data)) return;
+    this._ctx.scheduleFullRedraw('force');
     const msgData = msg.data as SyncMessage;
     const sync = msgData.perfettoSync;
     switch (sync.cmd) {
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index 327a179..a5ff6d7 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -685,6 +685,7 @@
             icon: arg(icon, 'send'),
             rightIcon: arg(rightIcon, 'arrow_forward'),
             label: arg(label, 'Button', ''),
+            onclick: () => alert('button pressed'),
             ...rest,
           }),
         initialOpts: {
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
index 50def57..0c8321b 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -54,7 +54,7 @@
 
   // TODO(primiano): this should be needed in extremely rare cases. We should
   // probably switch to mithril auto-redraw at some point.
-  scheduleFullRedraw(): void;
+  scheduleFullRedraw(force?: 'force'): void;
 
   /**
    * Navigate to a new page.
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index ccb8a03..58a3705 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -208,7 +208,7 @@
     let isFinalResponse = true;
 
     switch (rpc.response) {
-      case TPM.TPM_APPEND_TRACE_DATA:
+      case TPM.TPM_APPEND_TRACE_DATA: {
         const appendResult = assertExists(rpc.appendResult);
         const pendingPromise = assertExists(this.pendingParses.shift());
         if (exists(appendResult.error) && appendResult.error.length > 0) {
@@ -217,9 +217,17 @@
           pendingPromise.resolve();
         }
         break;
-      case TPM.TPM_FINALIZE_TRACE_DATA:
-        assertExists(this.pendingEOFs.shift()).resolve();
+      }
+      case TPM.TPM_FINALIZE_TRACE_DATA: {
+        const finalizeResult = assertExists(rpc.finalizeDataResult);
+        const pendingPromise = assertExists(this.pendingEOFs.shift());
+        if (exists(finalizeResult.error) && finalizeResult.error.length > 0) {
+          pendingPromise.reject(finalizeResult.error);
+        } else {
+          pendingPromise.resolve();
+        }
         break;
+      }
       case TPM.TPM_RESET_TRACE_PROCESSOR:
         assertExists(this.pendingResetTraceProcessors.shift()).resolve();
         break;
diff --git a/ui/src/widgets/editor.ts b/ui/src/widgets/editor.ts
index 58ea153..1187be0 100644
--- a/ui/src/widgets/editor.ts
+++ b/ui/src/widgets/editor.ts
@@ -21,6 +21,7 @@
 import {assertExists} from '../base/logging';
 import {DragGestureHandler} from '../base/drag_gesture_handler';
 import {DisposableStack} from '../base/disposable_stack';
+import {scheduleFullRedraw} from './raf';
 
 export interface EditorAttrs {
   // Initial state for the editor.
@@ -64,6 +65,7 @@
             text = selectedText;
           }
           onExecute(text);
+          scheduleFullRedraw('force');
           return true;
         },
       });
@@ -75,6 +77,7 @@
         view.update([tr]);
         const text = view.state.doc.toString();
         onUpdate(text);
+        scheduleFullRedraw('force');
       };
     }
 
diff --git a/ui/src/widgets/hotkey_context.ts b/ui/src/widgets/hotkey_context.ts
index f4d702a..767683e 100644
--- a/ui/src/widgets/hotkey_context.ts
+++ b/ui/src/widgets/hotkey_context.ts
@@ -14,6 +14,7 @@
 
 import m from 'mithril';
 import {checkHotkey, Hotkey} from '../base/hotkeys';
+import {scheduleFullRedraw} from './raf';
 
 export interface HotkeyConfig {
   hotkey: Hotkey;
@@ -58,6 +59,7 @@
         if (checkHotkey(hotkey, e)) {
           e.preventDefault();
           callback();
+          scheduleFullRedraw('force');
         }
       });
     }
diff --git a/ui/src/widgets/modal.ts b/ui/src/widgets/modal.ts
index e32f329..c07e6fe 100644
--- a/ui/src/widgets/modal.ts
+++ b/ui/src/widgets/modal.ts
@@ -14,8 +14,8 @@
 
 import m from 'mithril';
 import {defer} from '../base/deferred';
-import {scheduleFullRedraw} from './raf';
 import {Icon} from './icon';
+import {scheduleFullRedraw} from './raf';
 
 // This module deals with modal dialogs. Unlike most components, here we want to
 // render the DOM elements outside of the corresponding vdom tree. For instance
@@ -79,7 +79,10 @@
 export class Modal implements m.ClassComponent<ModalAttrs> {
   onbeforeremove(vnode: m.VnodeDOM<ModalAttrs>) {
     const removePromise = defer<void>();
-    vnode.dom.addEventListener('animationend', () => removePromise.resolve());
+    vnode.dom.addEventListener('animationend', () => {
+      scheduleFullRedraw('force');
+      removePromise.resolve();
+    });
     vnode.dom.classList.add('modal-fadeout');
 
     // Retuning `removePromise` will cause Mithril to defer the actual component
@@ -94,7 +97,6 @@
       // in turn will: (1) call the user's original attrs.onClose; (2) resolve
       // the promise returned by showModal().
       vnode.attrs.onClose();
-      scheduleFullRedraw();
     }
   }
 
@@ -223,7 +225,7 @@
     },
   };
   currentModal = attrs;
-  scheduleFullRedraw();
+  redrawModal();
   return returnedClosePromise;
 }
 
@@ -232,7 +234,7 @@
 // evident why a redraw is requested.
 export function redrawModal() {
   if (currentModal !== undefined) {
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 }
 
@@ -251,7 +253,7 @@
     return;
   }
   currentModal = undefined;
-  scheduleFullRedraw();
+  scheduleFullRedraw('force');
 }
 
 export function getCurrentModalKey(): string | undefined {
diff --git a/ui/src/widgets/popup.ts b/ui/src/widgets/popup.ts
index ed16695..ac8b563 100644
--- a/ui/src/widgets/popup.ts
+++ b/ui/src/widgets/popup.ts
@@ -352,13 +352,13 @@
     if (this.isOpen) {
       this.isOpen = false;
       this.onChange(this.isOpen);
-      scheduleFullRedraw();
+      scheduleFullRedraw('force');
     }
   }
 
   private togglePopup() {
     this.isOpen = !this.isOpen;
     this.onChange(this.isOpen);
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 }
diff --git a/ui/src/widgets/portal.ts b/ui/src/widgets/portal.ts
index 734fee6..91c3608 100644
--- a/ui/src/widgets/portal.ts
+++ b/ui/src/widgets/portal.ts
@@ -46,13 +46,22 @@
 export class Portal implements m.ClassComponent<PortalAttrs> {
   private portalElement?: HTMLElement;
   private containerElement?: Element;
+  private contentComponent: m.Component;
+
+  constructor({children}: m.CVnode<PortalAttrs>) {
+    // Create a temporary component that we can mount in oncreate, and unmount
+    // in onremove, but inject the new portal content (children) into it each
+    // render cycle. This is initialized here rather than in oncreate to avoid
+    // having to make it optional or use assertExists().
+    this.contentComponent = {view: () => children};
+  }
 
   view() {
     // Dummy element renders nothing but permits DOM access in lifecycle hooks.
     return m('span', {style: {display: 'none'}});
   }
 
-  oncreate({attrs, children, dom}: m.VnodeDOM<PortalAttrs, this>) {
+  oncreate({attrs, dom}: m.CVnodeDOM<PortalAttrs>) {
     const {
       onContentMount = () => {},
       onBeforeContentMount = (): MountOptions => ({}),
@@ -65,16 +74,21 @@
     container.appendChild(this.portalElement);
     this.applyPortalProps(attrs);
 
-    m.render(this.portalElement, children);
+    m.mount(this.portalElement, this.contentComponent);
 
     onContentMount(this.portalElement);
   }
 
-  onupdate({attrs, children}: m.VnodeDOM<PortalAttrs, this>) {
+  onbeforeupdate({children}: m.CVnode<PortalAttrs>) {
+    // Update the mounted content's view function to return the latest portal
+    // content passed in via children, without changing the component itself.
+    this.contentComponent.view = () => children;
+  }
+
+  onupdate({attrs}: m.CVnodeDOM<PortalAttrs>) {
     const {onContentUpdate = () => {}} = attrs;
     if (this.portalElement) {
       this.applyPortalProps(attrs);
-      m.render(this.portalElement, children);
       onContentUpdate(this.portalElement);
     }
   }
@@ -86,14 +100,14 @@
     }
   }
 
-  onremove({attrs}: m.VnodeDOM<PortalAttrs, this>) {
+  onremove({attrs}: m.CVnodeDOM<PortalAttrs>) {
     const {onContentUnmount = () => {}} = attrs;
     const container = this.containerElement ?? document.body;
     if (this.portalElement) {
       if (container.contains(this.portalElement)) {
         onContentUnmount(this.portalElement);
         // Rendering null ensures previous vnodes are removed properly.
-        m.render(this.portalElement, null);
+        m.mount(this.portalElement, null);
         container.removeChild(this.portalElement);
       }
     }
diff --git a/ui/src/widgets/raf.ts b/ui/src/widgets/raf.ts
index 20afb61..dc0d3ab 100644
--- a/ui/src/widgets/raf.ts
+++ b/ui/src/widgets/raf.ts
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-let FULL_REDRAW_FUNCTION = () => {};
+let FULL_REDRAW_FUNCTION = (_force?: 'force') => {};
 
 export function setScheduleFullRedraw(func: () => void) {
   FULL_REDRAW_FUNCTION = func;
 }
 
-export function scheduleFullRedraw() {
-  FULL_REDRAW_FUNCTION();
+export function scheduleFullRedraw(force?: 'force') {
+  FULL_REDRAW_FUNCTION(force);
 }
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 7cbf533..1a8cb43 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -228,7 +228,7 @@
     }
     this._status = Status.Done;
     this.pending = undefined;
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 
   private handleError(pending: Promise<vega.View>, err: unknown) {
@@ -242,7 +242,7 @@
   private setError(err: unknown) {
     this._status = Status.Error;
     this._error = getErrorMessage(err);
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 
   [Symbol.dispose]() {
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
index 475c360..6172c94 100644
--- a/ui/src/widgets/virtual_scroll_helper.ts
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -14,6 +14,7 @@
 
 import {DisposableStack} from '../base/disposable_stack';
 import {Bounds2D, Rect2D} from '../base/geom';
+import {scheduleFullRedraw} from './raf';
 
 export interface VirtualScrollHelperOpts {
   overdrawPx: number;
@@ -46,6 +47,7 @@
       this._data.forEach((data) =>
         recalculatePuckRect(sliderElement, containerElement, data),
       );
+      scheduleFullRedraw('force');
     };
 
     containerElement.addEventListener('scroll', recalculateRects, {