[metrics] Add thread creation spam stats to android_task_names

First added an ANDROID_THREAD_CREATION_SPAM stdlib function to
find stats of rapidly created threads.

To support finding thread names where only some suffix changes,
e.g Thread-123, we aggreate the stats per process per thread prefix.
We currently use a STR_SPLIT workaround to find prefixes by splitting
thread names by some common delimiters. When we have perfetto REGEXP
support, we can replace this with a more robust solution.

Test: tools/diff_test_trace_processor.py out/android/trace_processor_shell
Change-Id: I3862462b17cc8f494a6f2577f05b58d7289e1044
diff --git a/Android.bp b/Android.bp
index 63c30e1..a7bf077 100644
--- a/Android.bp
+++ b/Android.bp
@@ -10508,6 +10508,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/startup/internal_startups_minsdk33.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/statsd.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/thread.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scroll_janks.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/cpu_powerups.sql",
diff --git a/BUILD b/BUILD
index a7e0557..6596748 100644
--- a/BUILD
+++ b/BUILD
@@ -2223,6 +2223,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/statsd.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/thread.sql",
     ],
 )
 
diff --git a/protos/perfetto/metrics/android/task_names.proto b/protos/perfetto/metrics/android/task_names.proto
index 3aed87f..dbf985a 100644
--- a/protos/perfetto/metrics/android/task_names.proto
+++ b/protos/perfetto/metrics/android/task_names.proto
@@ -18,6 +18,12 @@
 
 message AndroidTaskNames {
   message Process {
+    // Identifies processes creating and destroying threads very quickly.
+    message TaskCreationSpam {
+      optional string thread_name_prefix = 1;
+      optional uint32 max_count_per_sec = 2;
+    }
+
     optional int64 pid = 1;
 
     // Process name.
@@ -31,6 +37,12 @@
 
     // Packages matching the process uid.
     repeated string uid_package_name = 5;
+
+    // Tasks created and destroyed within a second.
+    optional TaskCreationSpam short_lived_tasks = 6;
+
+    // Tasks created and not necessarily destroyed.
+    optional TaskCreationSpam long_lived_tasks = 7;
   }
 
   repeated Process process = 1;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 23e9fde..37fd6a5 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -1990,6 +1990,12 @@
 
 message AndroidTaskNames {
   message Process {
+    // Identifies processes creating and destroying threads very quickly.
+    message TaskCreationSpam {
+      optional string thread_name_prefix = 1;
+      optional uint32 max_count_per_sec = 2;
+    }
+
     optional int64 pid = 1;
 
     // Process name.
@@ -2003,6 +2009,12 @@
 
     // Packages matching the process uid.
     repeated string uid_package_name = 5;
+
+    // Tasks created and destroyed within a second.
+    optional TaskCreationSpam short_lived_tasks = 6;
+
+    // Tasks created and not necessarily destroyed.
+    optional TaskCreationSpam long_lived_tasks = 7;
   }
 
   repeated Process process = 1;
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 36ec809..8ed392d 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/trace_processor/metrics/sql/android/android_task_names.sql b/src/trace_processor/metrics/sql/android/android_task_names.sql
index b86186b..82f4e11 100644
--- a/src/trace_processor/metrics/sql/android/android_task_names.sql
+++ b/src/trace_processor/metrics/sql/android/android_task_names.sql
@@ -15,6 +15,7 @@
 --
 
 SELECT RUN_METRIC('android/process_metadata.sql');
+SELECT IMPORT('android.thread');
 
 DROP VIEW IF EXISTS android_task_names_output;
 CREATE VIEW android_task_names_output AS
@@ -43,7 +44,19 @@
       'process_name', p.name,
       'thread_name', threads_by_upid.thread_names,
       'uid', p.uid,
-      'uid_package_name', upid_packages.packages
+      'uid_package_name', upid_packages.packages,
+      'short_lived_tasks', (
+        SELECT AndroidTaskNames_Process_TaskCreationSpam(
+        'thread_name_prefix', s.thread_name_prefix,
+        'max_count_per_sec', s.max_count_per_sec
+        ) FROM ANDROID_THREAD_CREATION_SPAM(1e9, 1e9) s WHERE s.pid = p.pid
+      ),
+      'long_lived_tasks', (
+        SELECT AndroidTaskNames_Process_TaskCreationSpam(
+        'thread_name_prefix', s.thread_name_prefix,
+        'max_count_per_sec', s.max_count_per_sec
+        ) FROM ANDROID_THREAD_CREATION_SPAM(NULL, 1e9) s WHERE s.pid = p.pid
+      )
     )
   )
 )
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
index 24187af..2cb7026 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
@@ -25,5 +25,6 @@
     "process_metadata.sql",
     "slices.sql",
     "statsd.sql",
+    "thread.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/thread.sql b/src/trace_processor/perfetto_sql/stdlib/android/thread.sql
new file mode 100644
index 0000000..ced6b3c
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/thread.sql
@@ -0,0 +1,64 @@
+--
+-- Copyright 2023 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.
+
+SELECT CREATE_FUNCTION(
+    'INTERNAL_THREAD_PREFIX(thread_name STRING)',
+    'STRING',
+    '
+    SELECT STR_SPLIT(STR_SPLIT(STR_SPLIT(STR_SPLIT($thread_name, "-", 0), "[", 0), ":", 0), " ", 0);
+    '
+);
+
+-- Per process stats of threads created in a process
+--
+-- @arg min_thread_dur FLOAT       Minimum duration between creating and destroying a thread before
+-- their the thread creation event is considered. If NULL, considers all thread creations.
+-- @arg sliding_window_dur FLOAT   Sliding window duration for counting the thread creations. Each
+-- window starts at the first thread creation per <process, thread_name_prefix>.
+--
+-- @column process_name            Process name creating threads.
+-- @column thread_name_prefix      String prefix of thread names created.
+-- @column max_count_per_sec       Max number of threads created within a time window.
+SELECT CREATE_VIEW_FUNCTION(
+    'ANDROID_THREAD_CREATION_SPAM(min_thread_dur FLOAT, window_dur FLOAT)',
+    'process_name STRING, pid INT, thread_name_prefix STRING, max_count_per_sec INT',
+    '
+    WITH
+      x AS (
+        SELECT
+          pid,
+          upid,
+          INTERNAL_THREAD_PREFIX(thread.name) AS thread_name_prefix,
+          process.name AS process_name,
+          COUNT(thread.start_ts)
+            OVER (
+              PARTITION BY upid, thread.name
+              ORDER BY thread.start_ts
+              RANGE BETWEEN CURRENT ROW AND CAST($window_dur AS INT64) FOLLOWING
+            ) AS count
+        FROM thread
+        JOIN process
+          USING (upid)
+        WHERE
+          ($min_thread_dur AND (thread.end_ts - thread.start_ts) <= $min_thread_dur)
+          OR $min_thread_dur IS NULL
+      )
+    SELECT process_name, pid, thread_name_prefix, MAX(count) AS max_count_per_sec
+    FROM x
+    GROUP BY upid, thread_name_prefix
+    HAVING max_count_per_sec > 0
+    ORDER BY count DESC;
+    '
+);
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index ba2d198..181d97b 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -477,3 +477,38 @@
         trace=DataPath('android_monitor_contention_trace.atr'),
         query=Metric('android_monitor_contention'),
         out=Path('android_monitor_contention.out'))
+
+  def test_thread_creation_spam(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_monitor_contention_trace.atr'),
+        query="""
+      SELECT IMPORT('android.thread');
+      SELECT * FROM ANDROID_THREAD_CREATION_SPAM(1e9, 1e9);
+      """,
+        out=Csv("""
+      "process_name","pid","thread_name_prefix","max_count_per_sec"
+      "com.android.providers.media.module",3487,"SharedPreferenc",3
+      "com.android.providers.media.module",3487,"MediaCodec",2
+      "/apex/com.android.adbd/bin/adbd",527,"shell",1
+      "media.swcodec",563,"id.hevc.decoder",1
+      "system_server",642,"Thread",1
+      "sh",3474,"sh",1
+      "sh",3476,"sh",1
+      "sh",3478,"sh",1
+      "am",3480,"am",1
+      "cmd",3482,"binder",1
+      "cmd",3482,"cmd",1
+      "com.android.providers.media.module",3487,"CodecLooper",1
+      "sh",3517,"sh",1
+      "sgdisk",3521,"sgdisk",1
+      "blkid",3523,"blkid",1
+      "binder:243_4",3524,"binder",1
+      "fsck_msdos",3525,"fsck",1
+      "binder:243_4",3526,"binder",1
+      "sh",3532,"sh",1
+      "cut",3534,"cut",1
+      "sh",3536,"sh",1
+      "sh",3544,"sh",1
+      "sh",3546,"sh",1
+      "sh",3564,"sh",1
+      """))
diff --git a/test/trace_processor/diff_tests/tables/tests.py b/test/trace_processor/diff_tests/tables/tests.py
index 82fe9c3..b4b441c 100644
--- a/test/trace_processor/diff_tests/tables/tests.py
+++ b/test/trace_processor/diff_tests/tables/tests.py
@@ -175,6 +175,40 @@
             }
           }
         }
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 21963005381
+              pid: 524
+              task_newtask {
+                pid: 1576
+                comm: "adbd"
+                clone_flags: 18874368
+                oom_score_adj: -1000
+              }
+            }
+            event {
+              timestamp: 21973005381
+              pid: 523
+              task_newtask {
+                pid: 1574
+                comm: "sh"
+                clone_flags: 18874368
+                oom_score_adj: -1000
+              }
+            }
+            event {
+              timestamp: 21963008381
+              pid: 1572
+              sched_process_free {
+                comm: "adbd"
+                pid: 1576
+                prio: 120
+              }
+            }
+          }
+        }
         """),
         query=Metric('android_task_names'),
         out=TextProto(r"""
@@ -190,6 +224,28 @@
             uid: 10001
             uid_package_name: "com.google.android.gm"
           }
+          process {
+            pid: 1576
+            process_name: "adbd"
+            thread_name: "adbd"
+            short_lived_tasks {
+              thread_name_prefix: "adbd"
+              max_count_per_sec: 1
+            }
+            long_lived_tasks {
+              thread_name_prefix: "adbd"
+              max_count_per_sec: 1
+            }
+          }
+          process {
+            pid: 1574
+            process_name: "sh"
+            thread_name: "sh"
+            long_lived_tasks {
+              thread_name_prefix: "sh"
+              max_count_per_sec: 1
+            }
+          }
         }
         """))