blob: 2e6bc8a2e660b52ab99098e3aeb88dc7410f714a [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size 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 {AsyncLimiter} from '../base/async_limiter';
import {AsyncDisposableStack} from '../base/disposable_stack';
import {Size2D} from '../base/geom';
import {Duration, Time, TimeSpan, duration, time} from '../base/time';
import {TimeScale} from '../base/time_scale';
import {calculateResolution} from '../common/resolution';
import {TraceImpl} from '../core/trace_impl';
import {LONG, NUM} from '../trace_processor/query_result';
import {escapeSearchQuery} from '../trace_processor/query_utils';
import {createVirtualTable} from '../trace_processor/sql_utils';
interface SearchSummary {
tsStarts: BigInt64Array;
tsEnds: BigInt64Array;
count: Uint8Array;
}
/**
* This component is drawn on top of the timeline and creates small yellow
* rectangles that highlight the time span of search results (similarly to what
* Chrome does on the scrollbar when you Ctrl+F and type a search term).
* It reacts to changes in SearchManager and queries the quantized ranges of the
* search results.
*/
export class SearchOverviewTrack implements AsyncDisposable {
private readonly trash = new AsyncDisposableStack();
private readonly trace: TraceImpl;
private readonly limiter = new AsyncLimiter();
private initialized = false;
private previousResolution: duration | undefined;
private previousSpan: TimeSpan | undefined;
private previousSearchGeneration = 0;
private searchSummary: SearchSummary | undefined;
constructor(trace: TraceImpl) {
this.trace = trace;
}
render(ctx: CanvasRenderingContext2D, size: Size2D) {
this.maybeUpdate(size);
this.renderSearchOverview(ctx, size);
}
private async initialize() {
const engine = this.trace.engine;
this.trash.use(
await createVirtualTable(engine, 'search_summary_window', 'window'),
);
this.trash.use(
await createVirtualTable(
engine,
'search_summary_sched_span',
'span_join(sched PARTITIONED cpu, search_summary_window)',
),
);
this.trash.use(
await createVirtualTable(
engine,
'search_summary_slice_span',
'span_join(slice PARTITIONED track_id, search_summary_window)',
),
);
}
private async update(
search: string,
start: time,
end: time,
resolution: duration,
): Promise<SearchSummary> {
if (!this.initialized) {
this.initialized = true;
await this.initialize();
}
const searchLiteral = escapeSearchQuery(search);
const resolutionScalingFactor = 10n;
const quantum = resolution * resolutionScalingFactor;
start = Time.quantFloor(start, quantum);
const windowDur = Duration.max(Time.diff(end, start), 1n);
const engine = this.trace.engine;
await engine.query(`update search_summary_window set
window_start=${start},
window_dur=${windowDur},
quantum=${quantum}
where rowid = 0;`);
const utidRes = await engine.query(`select utid from thread join process
using(upid) where thread.name glob ${searchLiteral}
or process.name glob ${searchLiteral}`);
const utids = [];
for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
utids.push(it.utid);
}
const res = await engine.query(`
select
(quantum_ts * ${quantum} + ${start}) as tsStart,
((quantum_ts+1) * ${quantum} + ${start}) as tsEnd,
min(count(*), 255) as count
from (
select
quantum_ts
from search_summary_sched_span
where utid in (${utids.join(',')})
union all
select
quantum_ts
from search_summary_slice_span
where name glob ${searchLiteral}
)
group by quantum_ts
order by quantum_ts;`);
const numRows = res.numRows();
const summary: SearchSummary = {
tsStarts: new BigInt64Array(numRows),
tsEnds: new BigInt64Array(numRows),
count: new Uint8Array(numRows),
};
const it = res.iter({tsStart: LONG, tsEnd: LONG, count: NUM});
for (let row = 0; it.valid(); it.next(), ++row) {
summary.tsStarts[row] = it.tsStart;
summary.tsEnds[row] = it.tsEnd;
summary.count[row] = it.count;
}
return summary;
}
private maybeUpdate(size: Size2D) {
const searchManager = this.trace.search;
const timeline = this.trace.timeline;
if (!searchManager.hasResults) {
return;
}
const newSpan = timeline.visibleWindow;
const newSearchGeneration = searchManager.searchGeneration;
const newResolution = calculateResolution(newSpan, size.width);
const newTimeSpan = newSpan.toTimeSpan();
if (
this.previousSpan?.containsSpan(newTimeSpan.start, newTimeSpan.end) &&
this.previousResolution === newResolution &&
this.previousSearchGeneration === newSearchGeneration
) {
return;
}
// TODO(hjd): We should restrict this to the start of the trace but
// that is not easily available here.
// N.B. Timestamps can be negative.
const {start, end} = newTimeSpan.pad(newTimeSpan.duration);
this.previousSpan = new TimeSpan(start, end);
this.previousResolution = newResolution;
this.previousSearchGeneration = newSearchGeneration;
const search = searchManager.searchText;
if (search === '') {
this.searchSummary = {
tsStarts: new BigInt64Array(0),
tsEnds: new BigInt64Array(0),
count: new Uint8Array(0),
};
return;
}
this.limiter.schedule(async () => {
const summary = await this.update(
searchManager.searchText,
start,
end,
newResolution,
);
this.searchSummary = summary;
});
}
private renderSearchOverview(
ctx: CanvasRenderingContext2D,
size: Size2D,
): void {
const visibleWindow = this.trace.timeline.visibleWindow;
const timescale = new TimeScale(visibleWindow, {
left: 0,
right: size.width,
});
if (!this.searchSummary) return;
for (let i = 0; i < this.searchSummary.tsStarts.length; i++) {
const tStart = Time.fromRaw(this.searchSummary.tsStarts[i]);
const tEnd = Time.fromRaw(this.searchSummary.tsEnds[i]);
if (!visibleWindow.overlaps(tStart, tEnd)) {
continue;
}
const rectStart = Math.max(timescale.timeToPx(tStart), 0);
const rectEnd = timescale.timeToPx(tEnd);
ctx.fillStyle = '#ffe263';
ctx.fillRect(
Math.floor(rectStart),
0,
Math.ceil(rectEnd - rectStart),
size.height,
);
}
const results = this.trace.search.searchResults;
if (results === undefined) {
return;
}
const index = this.trace.search.resultIndex;
if (index !== -1 && index < results.tses.length) {
const start = results.tses[index];
if (start !== -1n) {
const triangleStart = Math.max(
timescale.timeToPx(Time.fromRaw(start)),
0,
);
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.moveTo(triangleStart, size.height);
ctx.lineTo(triangleStart - 3, 0);
ctx.lineTo(triangleStart + 3, 0);
ctx.lineTo(triangleStart, size.height);
ctx.fill();
ctx.closePath();
}
}
ctx.restore();
}
async [Symbol.asyncDispose](): Promise<void> {
return await this.trash.asyncDispose();
}
}