1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {assertTrue} from '../../base/logging';
16import {RawQueryResult} from '../../common/protos';
17import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
18import {fromNs, toNs} from '../../common/time';
19import {
20  TrackController,
21  trackControllerRegistry
22} from '../../controller/track_controller';
23
24import {
25  Config,
26  CPU_FREQ_TRACK_KIND,
27  Data,
28} from './common';
29
30class CpuFreqTrackController extends TrackController<Config, Data> {
31  static readonly kind = CPU_FREQ_TRACK_KIND;
32
33  private maxDurNs = 0;
34  private maxTsEndNs = 0;
35  private maximumValueSeen = 0;
36  private cachedBucketNs = Number.MAX_SAFE_INTEGER;
37
38  async onSetup() {
39    await this.createFreqIdleViews();
40
41    this.maximumValueSeen = await this.queryMaxFrequency();
42    this.maxDurNs = await this.queryMaxSourceDur();
43
44    const result = await this.query(`
45      select max(ts), dur, count(1)
46      from ${this.tableName('freq_idle')}
47    `);
48    this.maxTsEndNs =
49        result.columns[0].longValues![0] + result.columns[1].longValues![0];
50
51    const rowCount = result.columns[2].longValues![0];
52    const bucketNs = this.cachedBucketSizeNs(rowCount);
53    if (bucketNs === undefined) {
54      return;
55    }
56    await this.query(`
57      create table ${this.tableName('freq_idle_cached')} as
58      select
59        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
60        min(freq_value) as min_freq,
61        max(freq_value) as max_freq,
62        value_at_max_ts(ts, freq_value) as last_freq,
63        value_at_max_ts(ts, idle_value) as last_idle_value
64      from ${this.tableName('freq_idle')}
65      group by cached_tsq
66      order by cached_tsq
67    `);
68    this.cachedBucketNs = bucketNs;
69  }
70
71  async onBoundsChange(start: number, end: number, resolution: number):
72      Promise<Data> {
73    // The resolution should always be a power of two for the logic of this
74    // function to make sense.
75    const resolutionNs = toNs(resolution);
76    assertTrue(Math.log2(resolutionNs) % 1 === 0);
77
78    const startNs = toNs(start);
79    const endNs = toNs(end);
80
81    // ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
82    // be an even number, so we can snap in the middle.
83    const bucketNs =
84        Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
85
86    const freqResult = await this.queryData(startNs, endNs, bucketNs);
87
88    const numRows = slowlyCountRows(freqResult);
89    const data: Data = {
90      start,
91      end,
92      resolution,
93      length: numRows,
94      maximumValue: this.maximumValue(),
95      maxTsEnd: this.maxTsEndNs,
96      timestamps: new Float64Array(numRows),
97      minFreqKHz: new Uint32Array(numRows),
98      maxFreqKHz: new Uint32Array(numRows),
99      lastFreqKHz: new Uint32Array(numRows),
100      lastIdleValues: new Int8Array(numRows),
101    };
102
103    const it = iter(
104        {
105          'tsq': NUM,
106          'minFreq': NUM,
107          'maxFreq': NUM,
108          'lastFreq': NUM,
109          'lastIdleValue': NUM,
110        },
111        freqResult);
112    for (let i = 0; it.valid(); ++i, it.next()) {
113      data.timestamps[i] = fromNs(it.row.tsq);
114      data.minFreqKHz[i] = it.row.minFreq;
115      data.maxFreqKHz[i] = it.row.maxFreq;
116      data.lastFreqKHz[i] = it.row.lastFreq;
117      data.lastIdleValues[i] = it.row.lastIdleValue;
118    }
119    return data;
120  }
121
122  private async queryData(startNs: number, endNs: number, bucketNs: number):
123      Promise<RawQueryResult> {
124    const isCached = this.cachedBucketNs <= bucketNs;
125
126    if (isCached) {
127      return this.query(`
128        select
129          cached_tsq / ${bucketNs} * ${bucketNs} as tsq,
130          min(min_freq) as minFreq,
131          max(max_freq) as maxFreq,
132          value_at_max_ts(cached_tsq, last_freq) as lastFreq,
133          value_at_max_ts(cached_tsq, last_idle_value) as lastIdleValue
134        from ${this.tableName('freq_idle_cached')}
135        where
136          cached_tsq >= ${startNs - this.maxDurNs} and
137          cached_tsq <= ${endNs}
138        group by tsq
139        order by tsq
140      `);
141    }
142
143    const minTsFreq = await this.query(`
144      select ifnull(max(ts), 0) from ${this.tableName('freq')}
145      where ts < ${startNs}
146    `);
147    let minTs = minTsFreq.columns[0].longValues![0];
148    if (this.config.idleTrackId !== undefined) {
149      const minTsIdle = await this.query(`
150        select ifnull(max(ts), 0) from ${this.tableName('idle')}
151        where ts < ${startNs}
152      `);
153      minTs = Math.min(minTsIdle.columns[0].longValues![0], minTs);
154    }
155    const geqConstraint = this.config.idleTrackId === undefined ?
156        `ts >= ${minTs}` :
157        `source_geq(ts, ${minTs})`;
158    return this.query(`
159      select
160        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
161        min(freq_value) as minFreq,
162        max(freq_value) as maxFreq,
163        value_at_max_ts(ts, freq_value) as lastFreq,
164        value_at_max_ts(ts, idle_value) as lastIdleValue
165      from ${this.tableName('freq_idle')}
166      where
167        ${geqConstraint} and
168        ts <= ${endNs}
169      group by tsq
170      order by tsq
171    `);
172  }
173
174  private async queryMaxFrequency(): Promise<number> {
175    const result = await this.query(`
176      select max(freq_value)
177      from ${this.tableName('freq')}
178    `);
179    return result.columns[0].doubleValues![0];
180  }
181
182  private async queryMaxSourceDur(): Promise<number> {
183    const maxDurFreqResult =
184        await this.query(`select max(dur) from ${this.tableName('freq')}`);
185    const maxFreqDurNs = maxDurFreqResult.columns[0].longValues![0];
186    if (this.config.idleTrackId === undefined) {
187      return maxFreqDurNs;
188    }
189
190    const maxDurIdleResult =
191        await this.query(`select max(dur) from ${this.tableName('idle')}`);
192    return Math.max(maxFreqDurNs, maxDurIdleResult.columns[0].longValues![0]);
193  }
194
195  private async createFreqIdleViews() {
196    await this.query(`create view ${this.tableName('freq')} as
197      select
198        ts,
199        dur,
200        value as freq_value
201      from experimental_counter_dur c
202      where track_id = ${this.config.freqTrackId};
203    `);
204
205    if (this.config.idleTrackId === undefined) {
206      await this.query(`create view ${this.tableName('freq_idle')} as
207        select
208          ts,
209          dur,
210          -1 as idle_value,
211          freq_value
212        from ${this.tableName('freq')};
213      `);
214      return;
215    }
216
217    await this.query(`
218      create view ${this.tableName('idle')} as
219      select
220        ts,
221        dur,
222        iif(value = 4294967295, -1, cast(value as int)) as idle_value
223      from experimental_counter_dur c
224      where track_id = ${this.config.idleTrackId};
225    `);
226
227    await this.query(`
228      create virtual table ${this.tableName('freq_idle')}
229      using span_join(${this.tableName('freq')}, ${this.tableName('idle')});
230    `);
231  }
232
233  private maximumValue() {
234    return Math.max(this.config.maximumValue || 0, this.maximumValueSeen);
235  }
236}
237
238
239trackControllerRegistry.register(CpuFreqTrackController);
240