| // Copyright (C) 2019 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 {fromNs, toNs} from '../../common/time'; |
| import {LIMIT} from '../../common/track_data'; |
| |
| import { |
| TrackController, |
| trackControllerRegistry |
| } from '../../controller/track_controller'; |
| |
| import { |
| Config, |
| Data, |
| THREAD_STATE_TRACK_KIND, |
| } from './common'; |
| |
| class ThreadStateTrackController extends TrackController<Config, Data> { |
| static readonly kind = THREAD_STATE_TRACK_KIND; |
| private setup = false; |
| |
| async onBoundsChange(start: number, end: number, resolution: number): |
| Promise<Data> { |
| const startNs = toNs(start); |
| const endNs = toNs(end); |
| const minNs = Math.round(resolution * 1e9); |
| |
| await this.query( |
| `drop view if exists ${this.tableName('grouped_thread_states')}`); |
| |
| // This query gives all contiguous slices less than minNs the same grouping. |
| await this.query(`create view ${this.tableName('grouped_thread_states')} as |
| select ts, dur, cpu, state, |
| ifnull(sum(value) over (order by ts), 0) as grouping |
| from |
| (select *, |
| (dur >= ${minNs}) or lag(dur >= ${minNs}) over (order by ts) as value |
| from thread_state |
| where utid = ${this.config.utid})`); |
| |
| // Since there are more rows than slices we will output, check the number of |
| // distinct groupings to find the number of slices. |
| const totalSlicesQuery = `select count(distinct(grouping)) |
| from ${this.tableName('grouped_thread_states')} |
| where ts <= ${endNs} and ts + dur >= ${startNs}`; |
| const totalSlices = (await this.engine.queryOneRow(totalSlicesQuery))[0]; |
| |
| // We have ts contraints instead of span joining with the window table |
| // because when selecting a slice we need the real duration (even if it |
| // is outside of the current viewport) |
| // TODO(b/149303809): Return this to using |
| // the window table if possible. |
| const query = `select min(min(ts)) over (partition by grouping) as ts, |
| sum(sum(dur)) over (partition by grouping) as slice_dur, |
| cpu, |
| state, |
| (sum(dur) * 1.0)/(sum(sum(dur)) over (partition by grouping)) as percent, |
| grouping |
| from ${this.tableName('grouped_thread_states')} |
| where ts <= ${endNs} and ts + dur >= ${startNs} |
| group by grouping, state |
| order by grouping |
| limit ${LIMIT}`; |
| |
| const result = await this.query(query); |
| const numRows = +result.numRecords; |
| |
| const summary: Data = { |
| start, |
| end, |
| resolution, |
| length: totalSlices, |
| starts: new Float64Array(totalSlices), |
| ends: new Float64Array(totalSlices), |
| strings: [], |
| state: new Uint16Array(totalSlices), |
| cpu: new Uint8Array(totalSlices), |
| summarisedStateBreakdowns: new Map(), |
| }; |
| |
| const stringIndexes = new Map<string, number>(); |
| function internString(str: string) { |
| let idx = stringIndexes.get(str); |
| if (idx !== undefined) return idx; |
| idx = summary.strings.length; |
| summary.strings.push(str); |
| stringIndexes.set(str, idx); |
| return idx; |
| } |
| |
| let outIndex = 0; |
| for (let row = 0; row < numRows; row++) { |
| const cols = result.columns; |
| const start = +cols[0].longValues![row]; |
| const dur = +cols[1].longValues![row]; |
| const state = cols[3].stringValues![row]; |
| const percent = +(cols[4].doubleValues![row].toFixed(2)); |
| const grouping = +cols[5].longValues![row]; |
| |
| if (percent !== 1) { |
| let breakdownMap = summary.summarisedStateBreakdowns.get(outIndex); |
| if (!breakdownMap) { |
| breakdownMap = new Map(); |
| summary.summarisedStateBreakdowns.set(outIndex, breakdownMap); |
| } |
| |
| const currentPercent = breakdownMap.get(state); |
| if (currentPercent === undefined) { |
| breakdownMap.set(state, percent); |
| } else { |
| breakdownMap.set(state, currentPercent + percent); |
| } |
| } |
| |
| const nextGrouping = |
| row + 1 < numRows ? +cols[5].longValues![row + 1] : -1; |
| // If the next grouping is different then we have reached the end of this |
| // slice. |
| if (grouping !== nextGrouping) { |
| const numStates = summary.summarisedStateBreakdowns.get(outIndex) ? |
| summary.summarisedStateBreakdowns.get(outIndex)!.entries.length : |
| 1; |
| summary.starts[outIndex] = fromNs(start); |
| summary.ends[outIndex] = fromNs(start + dur); |
| summary.state[outIndex] = |
| internString(numStates === 1 ? state : 'Various states'); |
| summary.cpu[outIndex] = +cols[2].doubleValues![row]; |
| outIndex++; |
| } |
| } |
| return summary; |
| } |
| |
| onDestroy(): void { |
| if (this.setup) { |
| this.query(`drop view ${this.tableName('grouped_thread_states')}`); |
| this.setup = false; |
| } |
| } |
| } |
| |
| trackControllerRegistry.register(ThreadStateTrackController); |