--
-- Copyright 2020 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 RUN_METRIC('chrome/chrome_processes.sql');
SELECT RUN_METRIC('chrome/chrome_event_metadata.sql');

-- Priority order for RAIL modes where response has the highest priority and
-- idle has the lowest.
DROP TABLE IF EXISTS rail_modes;
CREATE TABLE rail_modes (
  mode TEXT UNIQUE,
  ordering INT,
  short_name TEXT
);

-- RAIL_MODE_IDLE is used when no frames are visible in the renderer and so this
-- interprets that as background.
-- RAIL_MODE_LOAD is for the time from a navigation until the first meaningful
-- paint (assuming there are no user interactions).
-- RAIL_MODE_RESPONSE is used when the main thread is dealing with a
-- user-interaction (but not for instance for scrolls which may be handled by
-- the compositor).
-- RAIL_MODE_ANIMATION is used when none of the above apply.
-- The enum in chrome is defined in:
-- https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/platform/scheduler/public/rail_mode_observer.h
INSERT INTO rail_modes
VALUES ('RAIL_MODE_IDLE', 0, 'background'),
  ('RAIL_MODE_ANIMATION', 1, "animation"),
  ('RAIL_MODE_LOAD', 2, "load"),
  ('RAIL_MODE_RESPONSE', 3, "response");


-- Find the max ts + dur for every process
DROP TABLE IF EXISTS max_ts_per_process;
CREATE TABLE max_ts_per_process AS
 -- MAX(dur, 0) means unclosed slices just contribute their start time.
SELECT upid,
  MAX(ts + MAX(dur, 0)) AS ts
FROM (
    SELECT upid,
      ts,
      dur
    FROM process_track t
      JOIN slice s
    WHERE s.track_id = t.id
    UNION ALL
    SELECT upid,
      ts,
      dur
    FROM thread_track t
      JOIN thread
      JOIN slice
    WHERE slice.track_id = t.id
      AND thread.utid = t.utid
  )
GROUP BY upid;

-- View containing all Scheduler.RAILMode slices across all Chrome renderer
-- processes.
DROP VIEW IF EXISTS original_rail_mode_slices;
CREATE VIEW original_rail_mode_slices AS
SELECT slice.id,
  slice.ts,
  CASE
    -- Add 1 to the duration to ensure you cannot get a zero-sized RAIL mode
    -- slice, which can throw off the later queries.
    WHEN dur == -1 THEN max_ts_per_process.ts - slice.ts + 1
    ELSE dur
  END AS dur,
  track_id,
  EXTRACT_ARG(
    slice.arg_set_id,
    "chrome_renderer_scheduler_state.rail_mode"
  ) AS rail_mode
FROM max_ts_per_process,
  slice,
  process_track
WHERE slice.name = "Scheduler.RAILMode"
  AND slice.track_id = process_track.id
  AND process_track.upid = max_ts_per_process.upid;

-- Detect if the trace has an unrealistic length (10 minutes) that probably
-- means some trace events have faulty timestamps and which could throw off any
-- metrics that use the trace.
DROP VIEW IF EXISTS trace_has_realistic_length;
CREATE VIEW trace_has_realistic_length AS
SELECT (end_ts - start_ts) < 1e9 * 60 * 10 AS value
FROM trace_bounds;

-- RAIL_MODE_LOAD seems to get stuck which makes it not very useful so remap it
-- to RAIL_MODE_ANIMATION so it doesn't dominate the overall RAIL mode.
DROP VIEW IF EXISTS rail_mode_slices;
CREATE VIEW rail_mode_slices AS
SELECT ts, dur, track_id,
    CASE
      WHEN rail_mode == "RAIL_MODE_LOAD" THEN "RAIL_MODE_ANIMATION"
      ELSE rail_mode
    END AS rail_mode
FROM original_rail_mode_slices;

-- View containing a collapsed view of rail_mode_slices where there is only one
-- RAIL mode active at a given time. The mode is derived using the priority
-- order in rail_modes.
DROP VIEW IF EXISTS overall_rail_mode_slices;
CREATE VIEW overall_rail_mode_slices AS
SELECT s.ts,
  s.end_ts,
  rail_modes.short_name AS rail_mode,
  MAX(rail_modes.ordering)
FROM (
    SELECT ts,
      LEAD(ts, 1, (SELECT MAX(ts + dur) FROM rail_mode_slices)) OVER (
        ORDER BY ts
      ) AS end_ts
    FROM (
        SELECT DISTINCT ts
        FROM rail_mode_slices
      ) start_times
  ) s,
  rail_mode_slices r,
  rail_modes,
  trace_has_realistic_length
WHERE (
    (
      s.ts >= r.ts AND s.ts < r.ts + r.dur
    )
    OR (
      s.end_ts > r.ts AND s.end_ts <= r.ts + r.dur
    )
  )
  AND r.rail_mode == rail_modes.mode
  AND trace_has_realistic_length.value
GROUP BY s.ts;

-- Contains the same data as overall_rail_mode_slices except adjacent slices
-- with the same RAIL mode are combined.
DROP TABLE IF EXISTS combined_overall_rail_slices;
CREATE TABLE combined_overall_rail_slices AS
SELECT ROW_NUMBER() OVER () AS id,
  ts,
  end_ts - ts AS dur,
  rail_mode
FROM (
    SELECT lag(l.end_ts, 1, FIRST) OVER (
        ORDER BY l.ts
      ) AS ts,
      l.end_ts,
      l.rail_mode
    FROM (
        SELECT ts,
          end_ts,
          rail_mode
        FROM overall_rail_mode_slices s
        WHERE NOT EXISTS (
            SELECT NULL
            FROM overall_rail_mode_slices s2
            WHERE s.rail_mode = s2.rail_mode
              AND s.end_ts = s2.ts
          )
      ) AS l,
      (
        SELECT min(ts) AS FIRST
        FROM overall_rail_mode_slices
      )
  );

-- Now we have the RAIL Mode, use other trace events to create a modified RAIL
-- mode that more accurately reflects what the browser/user are doing.

-- First create slices for when there's no animation as indicated by a large gap
-- between vsync events (since it's easier to find gaps than runs of adjacent
-- vsyncs).

-- Mark any large gaps between vsyncs.
-- The value in "not_animating" is always 1. It's just there to be a non-NULL
-- value so the later SPAN_JOIN can find the set-difference.
DROP VIEW IF EXISTS not_animating_slices;
CREATE VIEW not_animating_slices AS
WITH const (vsync_padding, large_gap) AS (
  SELECT
    -- Pad 50ms either side of a vsync
    50000000,
    -- A gap of >200ms between the adjacent vsyncs is treated as a gap in
    -- animation.
    200000000
)
SELECT ts + const.vsync_padding AS ts,
  gap_to_next_vsync - const.vsync_padding * 2 AS dur, 1 AS not_animating
FROM const, (SELECT name,
    ts,
    lead(ts) OVER () - ts AS gap_to_next_vsync,
    dur
  FROM slice
  WHERE name = "VSync")
WHERE gap_to_next_vsync > const.large_gap
UNION
-- Insert a slice between start_ts and the first vsync (or the end of the trace
-- if there are none).
SELECT
  ts,
  dur,
  1
FROM (SELECT start_ts AS ts,
          COALESCE((
              SELECT MIN(ts)
              FROM slice
              WHERE name = "VSync"
            ) - start_ts - const.vsync_padding,
            end_ts - start_ts
          ) AS dur
FROM trace_bounds, const)
WHERE dur > 0
UNION
-- Insert a slice between the last vsync and end_ts
SELECT last_vsync AS ts,
  end_ts - last_vsync AS dur,
  1
FROM (
    SELECT MAX(ts) + const.vsync_padding AS last_vsync
    FROM slice, const
    WHERE name = "VSync"
  ),
  trace_bounds
WHERE last_vsync < end_ts;

-- There are two types of InputLatency:: events:
-- 1) Simple ones that begin at ts and end at ts+dur
-- 2) Paired ones that begin with a "begin" slice and end at an "end" slice.
--
-- Paired events are even trickier because we can't guarantee that the "begin"
-- slice will even be in the trace and because it's possible for multiple begin
-- slices to appear without an intervening end slice.

-- Table of begin and end events along with the increment/decrement to be
-- applied to the appropriate counter (one for each type of paired event). Final
-- column dur_multiplier is used to find the timestamp to mark the event at in
-- the equation event_ts = ts + dur * dur_multiplier. End events have
-- dur_multiplier of 1, which makes their ts the end of the slice rather than
-- the start.
DROP TABLE IF EXISTS input_latency_begin_end_names;
CREATE TABLE input_latency_begin_end_names
(
  full_name TEXT UNIQUE,
  prefix TEXT,
  scroll_increment INT,
  pinch_increment INT,
  touch_increment INT,
  fling_increment INT,
  pointer_increment INT,
  dur_multiplier INT
);

INSERT
  OR IGNORE INTO input_latency_begin_end_names
VALUES
  ("InputLatency::GestureScrollBegin",
     "InputLatency::GestureScroll", 1, 0, 0, 0, 0, 0),
  ("InputLatency::GestureScrollEnd",
     "InputLatency::GestureScroll", -1, 0, 0, 0, 0, 1),
  ("InputLatency::GesturePinchBegin",
     "InputLatency::GesturePinch", 0, 1, 0, 0, 0, 0),
  ("InputLatency::GesturePinchEnd",
     "InputLatency::GesturePinch", 0, -1, 0, 0, 0, 1),
  ("InputLatency::TouchStart",
     "InputLatency::Touch", 0, 0, 1, 0, 0, 0),
  ("InputLatency::TouchEnd",
     "InputLatency::Touch", 0, 0, -1, 0, 0, 1),
  ("InputLatency::GestureFlingStart",
     "InputLatency::GestureFling", 0, 0, 0, 1, 0, 0),
  ("InputLatency::GestureFlingCancel",
     "InputLatency::GestureFling", 0, 0, 0, -1, 0, 1),
  ("InputLatency::PointerDown",
     "InputLatency::Pointer", 0, 0, 0, 0, 1, 0),
  ("InputLatency::PointerUp",
     "InputLatency::Pointer", 0, 0, 0, 0, -1, 1),
  ("InputLatency::PointerCancel",
     "InputLatency::Pointer", 0, 0, 0, 0, -1, 1);

-- Find all the slices that have split "begin" and "end" slices and maintain a
-- running total for each type, where >0 means that type of input event is
-- ongoing.
DROP VIEW IF EXISTS input_begin_end_slices;
CREATE VIEW input_begin_end_slices AS
SELECT prefix,
  -- Mark the change at the start of "start" slices and the end of "end" slices.
  ts + dur * dur_multiplier AS ts,
  scroll_increment,
  pinch_increment,
  touch_increment,
  fling_increment,
  pointer_increment
FROM slice
JOIN input_latency_begin_end_names ON name = full_name
ORDER BY ts;

-- Combine all the paired input events to get an indication of when any paired
-- input event is ongoing.
DROP VIEW IF EXISTS unified_input_pair_increments;
CREATE VIEW unified_input_pair_increments AS
SELECT ts,
  scroll_increment +
  pinch_increment +
  touch_increment +
  fling_increment +
  pointer_increment AS increment
FROM input_begin_end_slices;

-- It's possible there's an end slice without a start slice (as it occurred
-- before the trace started) which would result in (starts - ends) going
-- negative at some point. So find an offset that shifts up all counts so the
-- lowest values becomes zero. It's possible this could still do the wrong thing
-- if there were start AND end slices that are outside the trace bounds, in
-- which case it should count as covering the entire trace, but it's impossible
-- to compensate for that without augmenting the trace events themselves.
DROP VIEW IF EXISTS initial_paired_increment;
CREATE VIEW initial_paired_increment AS
SELECT ts,
  MIN(0, MIN(scroll_total)) +
  MIN(0, MIN(pinch_total))  +
  MIN(0, MIN(touch_total))  +
  MIN(0, MIN(fling_total))  +
  MIN(0, MIN(pointer_total)) AS offset
FROM (
    SELECT ts,
      SUM(scroll_increment) OVER(ROWS UNBOUNDED PRECEDING) AS scroll_total,
      SUM(pinch_increment)  OVER(ROWS UNBOUNDED PRECEDING) AS pinch_total,
      SUM(touch_increment)  OVER(ROWS UNBOUNDED PRECEDING) AS touch_total,
      SUM(fling_increment)  OVER(ROWS UNBOUNDED PRECEDING) AS fling_total,
      SUM(pointer_increment)  OVER(ROWS UNBOUNDED PRECEDING) AS pointer_total
    FROM input_begin_end_slices
  );

-- Now find all the simple input slices that fully enclose the input they're
-- marking (i.e. not the start or end of a pair).
DROP VIEW IF EXISTS simple_input_slices;
CREATE VIEW simple_input_slices AS
SELECT id,
  name,
  ts,
  dur
FROM slice s
WHERE name GLOB "InputLatency::*"
  AND NOT EXISTS (
    SELECT 1
    FROM slice
      JOIN input_latency_begin_end_names
    WHERE s.name == full_name
  );

-- Turn the simple input slices into +1s and -1s at the start and end of each
-- slice.
DROP VIEW IF EXISTS simple_input_increments;
CREATE VIEW simple_input_increments AS
SELECT ts,
  1 AS increment
FROM simple_input_slices
UNION ALL
SELECT ts + dur,
  -1
FROM simple_input_slices
ORDER BY ts;

-- Combine simple and paired inputs into one, summing all the increments at a
-- given ts.
DROP VIEW IF EXISTS all_input_increments;
CREATE VIEW all_input_increments AS
SELECT ts,
  SUM(increment) AS increment
FROM (
    SELECT *
    FROM simple_input_increments
    UNION ALL
    SELECT *
    FROM unified_input_pair_increments
    ORDER BY ts
  )
GROUP BY ts;

-- Now calculate the cumulative sum of the increments as each ts, giving the
-- total number of outstanding input events at a given time.
DROP VIEW IF EXISTS all_input_totals;
CREATE VIEW all_input_totals AS
SELECT ts,
  SUM(increment) OVER(ROWS UNBOUNDED PRECEDING) > 0 AS input_total
FROM all_input_increments;

-- Now find the transitions from and to 0 and use that to create slices where
-- input events were occurring. The input_active column always contains 1, but
-- is there so that the SPAN_JOIN_LEFT can put NULL in it for RAIL Mode slices
-- that do not have corresponding input events.
DROP VIEW IF EXISTS all_input_slices;
CREATE VIEW all_input_slices AS
SELECT ts,
  dur,
  input_active
FROM (
    SELECT ts,
      lead(ts, 1, end_ts) OVER() - ts AS dur,
      input_active
    FROM trace_bounds,
      (
        SELECT ts,
          input_total > 0 AS input_active
        FROM (
            SELECT ts,
              input_total,
              lag(input_total) OVER() AS prev_input_total
            FROM all_input_totals
          )
        WHERE (input_total > 0 <> prev_input_total > 0)
          OR prev_input_total IS NULL
      )
  )
WHERE input_active > 0;

-- Since the scheduler defaults to animation when none of the other RAIL modes
-- apply, animation overestimates the amount of time that actual animation is
-- occurring.
-- So instead we try to divide up animation in other buckets based on other
-- trace events.
DROP VIEW IF EXISTS rail_mode_animation_slices;
CREATE VIEW rail_mode_animation_slices AS
SELECT * FROM combined_overall_rail_slices WHERE rail_mode = "animation";

-- Left-join rail mode animation slices with all_input_slices to find all
-- "animation" slices that should actually be labelled "response".
DROP TABLE IF EXISTS rail_mode_join_inputs;
CREATE VIRTUAL TABLE rail_mode_join_inputs
USING SPAN_LEFT_JOIN(rail_mode_animation_slices, all_input_slices);

-- Left-join rail mode animation slices with not_animating_slices which is
-- based on the gaps between vsync events.
DROP TABLE IF EXISTS rail_mode_join_inputs_join_animation;
CREATE VIRTUAL TABLE rail_mode_join_inputs_join_animation
USING SPAN_LEFT_JOIN(rail_mode_join_inputs, not_animating_slices);

DROP VIEW IF EXISTS has_modified_rail_slices;
CREATE VIEW has_modified_rail_slices AS
SELECT (
    SELECT value
    FROM chrome_event_metadata
    WHERE name == "os-name"
  ) == "Android" AS value;

-- Mapping to allow CamelCased names to be produced from the modified rail
-- modes.
DROP TABLE IF EXISTS modified_rail_mode_prettier;
CREATE TABLE modified_rail_mode_prettier (
  orig_name TEXT UNIQUE,
  pretty_name TEXT
);
INSERT INTO modified_rail_mode_prettier
VALUES ("background", "Background"),
  ("foreground_idle", "ForegroundIdle"),
  ("animation", "Animation"),
  ("load", "Load"),
  ("response", "Response");

-- When the RAIL mode is animation, use input/vsync data to conditionally change
-- the mode to response or foreground_idle.
DROP VIEW IF EXISTS unmerged_modified_rail_slices;
CREATE VIEW unmerged_modified_rail_slices AS
SELECT ROW_NUMBER() OVER () AS id,
  ts,
  dur,
  mode
FROM (
    SELECT ts,
      dur,
      CASE
        WHEN input_active IS NOT NULL THEN "response"
        WHEN not_animating IS NULL THEN "animation"
        ELSE "foreground_idle"
      END AS mode
    FROM rail_mode_join_inputs_join_animation
    UNION
    SELECT ts,
      dur,
      rail_mode AS mode
    FROM combined_overall_rail_slices
    WHERE rail_mode <> "animation"
  )
  -- Since VSync events are only emitted on Android (and the concept of a
  -- unified RAIL mode only makes sense if there's just a single Chrome window),
  -- don't output anything on other platforms. This will result in all the power
  -- and cpu time tables being empty rather than containing bogus results.
WHERE (
    SELECT value
    FROM has_modified_rail_slices
  );

-- The previous query creating unmerged_modified_rail_slices, can create
-- adjacent slices with the same mode. This merges them together as well as
-- adding a unique id to each slice. Rather than directly merging slices
-- together, this instead looks for all the transitions and uses this to
-- reconstruct the slices that should occur between them.
DROP TABLE IF EXISTS modified_rail_slices;
CREATE TABLE modified_rail_slices AS
WITH const (end_ts) AS (SELECT ts + dur
              FROM unmerged_modified_rail_slices
              ORDER BY ts DESC
              LIMIT 1)
SELECT ROW_NUMBER() OVER () AS id, lag(next_ts) OVER() AS ts,
  ts + dur - lag(next_ts) OVER() AS dur,
  mode AS mode
FROM (
    -- For each row in the original table, create a new row with the information
    -- from the following row, since you can't use lag/lead in WHERE clause.
    --
    -- Transition row at the beginning. "mode" is invalid, so a transition will
    -- always be recorded.
    SELECT *
    FROM (SELECT
          0 AS ts,
          ts AS dur,
          "" AS mode,
          ts AS next_ts,
          dur AS next_dur,
          mode AS next_mode
        FROM unmerged_modified_rail_slices
        LIMIT 1
      )
    UNION ALL
    SELECT ts,
      dur,
      mode,
      lead(ts, 1, end_ts) OVER() AS next_ts,
      lead(dur) OVER() AS next_dur,
      lead(mode) OVER() AS next_mode
    FROM unmerged_modified_rail_slices, const
    UNION ALL
    -- Transition row at the end. "next_mode" is invalid, so a transition will
    -- always be recorded.
    SELECT *
    FROM (SELECT
          ts + dur AS ts,
          0 AS dur,
          mode,
          ts + dur AS next_ts,
          0,
          "" AS next_mode
        FROM unmerged_modified_rail_slices
        ORDER BY ts DESC
        LIMIT 1
      )
  )
WHERE mode <> next_mode
-- Retrieve all but the first row.
LIMIT -1 OFFSET 1;
