-- Copyright 2025 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 android.suspend; -- Table of parsed wakeup / suspend failure events with suspend backoff. -- -- Certain wakeup events may have multiple causes. When this occurs we -- split those causes into multiple rows in this table with the same ts -- and raw_wakeup values. CREATE PERFETTO TABLE android_wakeups ( -- Timestamp. ts TIMESTAMP, -- Duration for which we blame the wakeup for wakefulness. This is the -- suspend backoff duration if one exists, or the lesser of (5 seconds, -- time to next suspend event). dur DURATION, -- Original wakeup string from the kernel. raw_wakeup STRING, -- Wakeup attribution, as determined on device. May be absent. on_device_attribution STRING, -- One of 'normal' (device woke from sleep), 'abort_pending' (suspend failed -- due to a wakeup that was scheduled by a device during the suspend -- process), 'abort_last_active' (suspend failed, listing the last active -- device) or 'abort_other' (suspend failed for another reason). type STRING, -- Individual wakeup cause. Usually the name of the device that cause the -- wakeup, or the raw message in the 'abort_other' case. item STRING, -- 'good' or 'bad'. 'bad' means failed or short such that suspend backoff -- is triggered. suspend_quality STRING, -- 'new', 'continue' or NULL. Set if suspend backoff is triggered. backoff_state STRING, -- 'short', 'failed' or NULL. Set if suspend backoff is triggered. backoff_reason STRING, -- Number of times suspend backoff has occured, or NULL. Set if suspend -- backoff is triggered. backoff_count LONG, -- Next suspend backoff duration, or NULL. Set if suspend backoff is -- triggered. backoff_millis LONG ) AS WITH wakeup_reason AS ( -- wakeup_reason is recorded ASAP after wakeup; its packet timestamp is reliable. It also contains -- a millisecond timestamp which we can use to associate the wakeup_reason with a later -- wakeup_attribution. SELECT ts, substr(i.name, 0, instr(i.name, ' ')) AS id_timestamp, substr(i.name, instr(i.name, ' ') + 1) AS raw_wakeup FROM track AS t JOIN instant AS i ON t.id = i.track_id WHERE t.name = 'wakeup_reason' ), wakeup_attribution AS ( -- wakeup_attribution is recorded at some later time after wakeup and after batterystat has decided -- how to attribute it. We therefore associate it with the original wakeup via the embedded -- millisecond timestamp. SELECT substr(i.name, 0, instr(i.name, ' ')) AS id_timestamp, substr(i.name, instr(i.name, ' ') + 1) AS on_device_attribution FROM track AS t JOIN instant AS i ON t.id = i.track_id WHERE t.name = 'wakeup_attribution' ), step1 AS ( -- Join reason, attribution, and backoff. reason and attribution contain a timestamp that can be -- used as an ID for joining. backoff does not but we know if it occurs at all it will always -- occur just before the reason. We therefore "join" with a union+lag() to be efficient. SELECT ts, raw_wakeup, on_device_attribution, NULL AS raw_backoff FROM wakeup_reason AS r LEFT OUTER JOIN wakeup_attribution USING (id_timestamp) UNION ALL SELECT ts, NULL AS raw_wakeup, NULL AS on_device_attribution, i.name AS raw_backoff FROM track AS t JOIN instant AS i ON t.id = i.track_id WHERE t.name = 'suspend_backoff' ), step2 AS ( SELECT ts, raw_wakeup, on_device_attribution, lag(raw_backoff) OVER (ORDER BY ts) AS raw_backoff FROM step1 ), step3 AS ( SELECT ts, raw_wakeup, on_device_attribution, str_split(raw_backoff, ' ', 0) AS suspend_quality, str_split(raw_backoff, ' ', 1) AS backoff_state, str_split(raw_backoff, ' ', 2) AS backoff_reason, CAST(str_split(raw_backoff, ' ', 3) AS INTEGER) AS backoff_count, CAST(str_split(raw_backoff, ' ', 4) AS INTEGER) AS backoff_millis, FALSE AS suspend_end FROM step2 WHERE raw_wakeup IS NOT NULL UNION ALL SELECT ts, NULL AS raw_wakeup, NULL AS on_device_attribution, NULL AS suspend_quality, NULL AS backoff_state, NULL AS backoff_reason, NULL AS backoff_count, NULL AS backoff_millis, TRUE AS suspend_end FROM android_suspend_state WHERE power_state = 'suspended' ), -- If the device is in a failure-to-suspend loop it will back off and take -- up to ~1 minute to suspend. We should allow ourselves to apportion that time -- to the wakeup (unless there was another wakeup or suspend following it). -- NB a failure to suspend loop can also manifest as actually suspending and then -- waking up after a few milliseconds so we don't attempt to filter by aborted -- suspends here. step4 AS ( SELECT ts, CASE suspend_quality WHEN 'good' THEN min(lead(ts, 1, ts + 5e9) OVER (ORDER BY ts) - ts, 5e9) WHEN 'bad' THEN backoff_millis * 1000000 ELSE 0 END AS dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, suspend_end FROM step3 ), step5 AS ( SELECT ts, dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis FROM step4 WHERE NOT suspend_end ), -- Each wakeup can contain multiple reasons. We need to parse the wakeup in order to get them out. -- This is made more annoying by the fact it can be in multiple formats: -- If the wakeup represents an aborted attempt at suspending, it will be prefixed with one of a -- couple of variations on "Abort:" and may (in the pending wakeup case) contain multiple reasons, -- separated by spaces -- -- example: "Abort: Device 0001:01:00.0 failed to suspend: error -16" -- example: "Abort: Pending Wakeup Sources: wlan_oob_irq_wake wlan_txfl_wake wlan_rx_wake" -- example: "Abort: Last active Wakeup Source: wlan_oob_irq_wake" -- -- For a normal wakeup there may be multiple reasons separated by ":" and each preceded by a numeric -- IRQ. Essentially the text is a looked-up explanation of the IRQ number. -- -- example: "170 176a0000.mbox:440 dhdpcie_host_wake" -- example: "374 max_fg_irq:440 dhdpcie_host_wake" -- step6 AS ( SELECT ts, dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, CASE WHEN raw_wakeup GLOB 'Abort: Pending Wakeup Sources: *' THEN 'abort_pending' WHEN raw_wakeup GLOB 'Abort: Last active Wakeup Source: *' THEN 'abort_last_active' WHEN raw_wakeup GLOB 'Abort: *' THEN 'abort_other' ELSE 'normal' END AS type, CASE WHEN raw_wakeup GLOB 'Abort: Pending Wakeup Sources: *' THEN substr(raw_wakeup, 32) WHEN raw_wakeup GLOB 'Abort: Last active Wakeup Source: *' THEN substr(raw_wakeup, 35) WHEN raw_wakeup GLOB 'Abort: *' THEN substr(raw_wakeup, 8) ELSE raw_wakeup END AS main, CASE WHEN raw_wakeup GLOB 'Abort: Pending Wakeup Sources: *' THEN ' ' WHEN raw_wakeup GLOB 'Abort: *' THEN 'no delimiter needed' ELSE ':' END AS delimiter FROM step5 ), step7 AS ( SELECT ts, dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, str_split(main, delimiter, 0) AS item_0, str_split(main, delimiter, 1) AS item_1, str_split(main, delimiter, 2) AS item_2, str_split(main, delimiter, 3) AS item_3 FROM step6 ), step8 AS ( SELECT ts, dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_0 AS item FROM step7 UNION ALL SELECT ts, dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_1 AS item FROM step7 WHERE item_1 IS NOT NULL UNION ALL SELECT ts, dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_2 AS item FROM step7 WHERE item_2 IS NOT NULL UNION ALL SELECT ts, dur, raw_wakeup, on_device_attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_3 AS item FROM step7 WHERE item_3 IS NOT NULL ) SELECT ts, cast_int!(dur) AS dur, raw_wakeup, on_device_attribution, type, -- Remove the numeric IRQ, it duplicates the text and is less comprehensible. CASE WHEN type = 'normal' THEN coalesce(str_split(item, ' ', 1), item) ELSE item END AS item, suspend_quality, backoff_state, coalesce(backoff_reason, 'none') AS backoff_reason, backoff_count, backoff_millis FROM step8;