Skip to content

Commit

Permalink
Open a subset of a timeline
Browse files Browse the repository at this point in the history
Add support for 'startTs' and 'endTs' URL parameters to open a narrow
window of time in Perfetto as though it were the entire trace.

Signed-off-by: Christian W. Damus <[email protected]>
  • Loading branch information
cdamus committed Jan 17, 2024
1 parent a1af4fc commit a39b428
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 8 deletions.
32 changes: 29 additions & 3 deletions ui/src/common/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
QueryResult,
WritableQueryResult,
} from './query_result';
import {TPTime, TPTimeSpan} from './time';
import {TPDuration, TPTime, TPTimeSpan} from './time';

import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
import TraceProcessorRpcStream = perfetto.protos.TraceProcessorRpcStream;
Expand Down Expand Up @@ -89,6 +89,7 @@ export abstract class Engine {
private pendingComputeMetrics = new Array<Deferred<ComputeMetricResult>>();
private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
private _isMetatracingEnabled = false;
private _timelineConstraint?: Span<TPTime, TPDuration>;

constructor(tracker?: LoadingTracker) {
this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
Expand Down Expand Up @@ -412,14 +413,39 @@ export abstract class Engine {
return result.firstRow({cnt: NUM}).cnt;
}

/**
* Obtain a range of timestamps to which the entire timeline is restricted.
*/
get timelineConstraint(): Span<TPTime, TPDuration> | undefined {
return this._timelineConstraint;
}

/**
* Set or clear the range of timestamps to which the entire timeline is restricted.
*/
set timelineConstraint(timeSpan: Span<TPTime, TPDuration> | undefined) {
this._timelineConstraint = timeSpan;
}

/** Clamp a |timeSpan| to my timeline filter, if any. */
protected clampSpan(timeSpan: Span<TPTime, TPDuration>): Span<TPTime, TPDuration> {
const constraint = this.timelineConstraint;
const result = constraint ? timeSpan.intersection(constraint) : timeSpan;
if (!!constraint && result.duration <= 0n) {
console.error(`Invalid timespan constraint resulting in empty timeline. Ignoring the constraint ${constraint}.`);
return timeSpan;
}
return result;
}

async getTraceTimeBounds(): Promise<Span<TPTime>> {
const result = await this.query(
`select start_ts as startTs, end_ts as endTs from trace_bounds`);
const bounds = result.firstRow({
startTs: LONG,
endTs: LONG,
});
return new TPTimeSpan(bounds.startTs, bounds.endTs);
return this.clampSpan(new TPTimeSpan(bounds.startTs, bounds.endTs));
}

async getTracingMetadataTimeBounds(): Promise<Span<TPTime>> {
Expand All @@ -443,7 +469,7 @@ export abstract class Engine {
}
}

return new TPTimeSpan(startBound, endBound);
return this.clampSpan(new TPTimeSpan(startBound, endBound));
}

getProxy(tag: string): EngineProxy {
Expand Down
17 changes: 17 additions & 0 deletions ui/src/common/high_precision_time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,23 @@ export class HighPrecisionTimeSpan implements Span<HighPrecisionTime> {
return !(x.end.lte(this.start) || x.start.gte(this.end));
}

intersection(x: Span<HighPrecisionTime, HighPrecisionTime>): Span<HighPrecisionTime, HighPrecisionTime> {
if (x.start.lte(this.start) && x.end.gte(this.end)) {
// I am the intersection
return this;
}
if (x.start.gte(this.start) && x.end.lte(this.end)) {
// It is the intersection
return x;
}
if (x.end.lt(this.start) || this.end.lt(x.start)) {
// It's an empty intersection. [0, 0] is as good as any other span
return HighPrecisionTimeSpan.ZERO;
}
return new HighPrecisionTimeSpan(HighPrecisionTime.max(this.start, x.start),
HighPrecisionTime.min(x.end, this.end));
}

add(time: HighPrecisionTime): Span<HighPrecisionTime> {
return new HighPrecisionTimeSpan(this.start.add(time), this.end.add(time));
}
Expand Down
13 changes: 13 additions & 0 deletions ui/src/common/high_precision_time_unittest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,17 @@ describe('HighPrecisionTimeSpan', () => {
expect(x.intersects(mkSpan('20', '30'))).toBeFalsy();
expect(x.intersects(mkSpan('5', '25'))).toBeTruthy();
});

it('creates intersection of spans', () => {
const x = mkSpan('10', '20');

expect(x.intersection(mkSpan('0', '10'))).toEqual(mkSpan('10', '10'));
expect(x.intersection(mkSpan('5', '15'))).toEqual(mkSpan('10', '15'));
expect(x.intersection(mkSpan('12', '18'))).toEqual(mkSpan('12', '18'));
expect(x.intersection(mkSpan('15', '25'))).toEqual(mkSpan('15', '20'));
expect(x.intersection(mkSpan('20', '30'))).toEqual(mkSpan('20', '20'));
expect(x.intersection(mkSpan('5', '25'))).toEqual(mkSpan('10', '20'));
expect(x.intersection(mkSpan('2', '8'))).toEqual(mkSpan('0', '0'));
expect(x.intersection(mkSpan('22', '28'))).toEqual(mkSpan('0', '0'));
});
});
31 changes: 30 additions & 1 deletion ui/src/common/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import {assertTrue} from '../base/logging';
import {BigintMath} from '../base/bigint_math';
import {ColumnType} from './query_result';

// TODO(hjd): Combine with timeToCode.
Expand Down Expand Up @@ -133,6 +134,8 @@ export function tpTimeFromSql(value: ColumnType): TPTime {
return value;
} else if (typeof value === 'number') {
return tpTimeFromNanos(value);
} else if (typeof value === 'string') {
return BigInt(value);
} else if (value === null) {
return 0n;
} else {
Expand Down Expand Up @@ -162,7 +165,8 @@ export interface Span<Unit, Duration = Unit> {
get duration(): Duration;
get midpoint(): Unit;
contains(span: Unit|Span<Unit, Duration>): boolean;
intersects(x: Span<Unit>): boolean;
intersects(x: Span<Unit, Duration>): boolean;
intersection(x: Span<Unit, Duration>): Span<Unit, Duration>;
equals(span: Span<Unit, Duration>): boolean;
add(offset: Duration): Span<Unit, Duration>;
pad(padding: Duration): Span<Unit, Duration>;
Expand All @@ -180,6 +184,10 @@ export class TPTimeSpan implements Span<TPTime, TPDuration> {
this.end = end;
}

static get ZERO(): TPTimeSpan {
return new TPTimeSpan(0n, 0n);
}

get duration(): TPDuration {
return this.end - this.start;
}
Expand All @@ -200,6 +208,23 @@ export class TPTimeSpan implements Span<TPTime, TPDuration> {
return !(x.end <= this.start || x.start >= this.end);
}

intersection(x: Span<TPTime, TPDuration>): Span<TPTime, TPDuration> {
if (x.start <= this.start && x.end >= this.end) {
// I am the intersection
return this;
}
if (x.start > this.start && x.end < this.end) {
// It is the intersection
return x;
}
if (x.end < this.start || this.end < x.start) {
// It's an empty intersection. [0, 0] is as good as any other span
return TPTimeSpan.ZERO;
}
return new TPTimeSpan(BigintMath.max(this.start, x.start),
BigintMath.min(x.end, this.end));
}

equals(span: Span<TPTime, TPDuration>): boolean {
return this.start === span.start && this.end === span.end;
}
Expand All @@ -211,4 +236,8 @@ export class TPTimeSpan implements Span<TPTime, TPDuration> {
pad(padding: TPDuration): Span<TPTime, TPDuration> {
return new TPTimeSpan(this.start - padding, this.end + padding);
}

toString(): string {
return `TPTimeSpan(${this.start}, ${this.end})`;
}
}
24 changes: 23 additions & 1 deletion ui/src/common/time_unittest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {timeToCode, TPTime, TPTimeSpan} from './time';
import {timeToCode, tpTimeFromSql, TPTime, TPTimeSpan} from './time';

test('seconds to code', () => {
expect(timeToCode(3)).toEqual('3s');
Expand All @@ -29,6 +29,15 @@ test('seconds to code', () => {
expect(timeToCode(0)).toEqual('0s');
});

test('time from SQL', () => {
expect(tpTimeFromSql(3_000_000_000n)).toEqual(3_000_000_000n);
expect(tpTimeFromSql(3_000)).toEqual(3_000n);
expect(tpTimeFromSql('3000000000')).toEqual(3_000_000_000n);
expect(tpTimeFromSql(null)).toEqual(0n);
expect(() => tpTimeFromSql('3000000000ns')).toThrow();
expect(() => tpTimeFromSql(new Uint8Array([0b10110010, 0b11010000, 0b01011110, 0b00000000]))).toThrow();
});

function mkSpan(start: TPTime, end: TPTime) {
return new TPTimeSpan(start, end);
}
Expand Down Expand Up @@ -83,6 +92,19 @@ describe('TPTimeSpan', () => {
expect(x.intersects(mkSpan(5n, 25n))).toBeTruthy();
});

it('creates intersection', () => {
const x = mkSpan(10n, 20n);

expect(x.intersection(mkSpan(0n, 10n))).toEqual(mkSpan(10n, 10n));
expect(x.intersection(mkSpan(5n, 15n))).toEqual(mkSpan(10n, 15n));
expect(x.intersection(mkSpan(12n, 18n))).toEqual(mkSpan(12n, 18n));
expect(x.intersection(mkSpan(15n, 25n))).toEqual(mkSpan(15n, 20n));
expect(x.intersection(mkSpan(20n, 30n))).toEqual(mkSpan(20n, 20n));
expect(x.intersection(mkSpan(5n, 25n))).toEqual(mkSpan(10n, 20n));
expect(x.intersection(mkSpan(2n, 8n))).toEqual(mkSpan(0n, 0n));
expect(x.intersection(mkSpan(22n, 28n))).toEqual(mkSpan(0n, 0n));
});

it('can add', () => {
const x = mkSpan(10n, 20n);
expect(x.add(5n)).toEqual(mkSpan(15n, 25n));
Expand Down
2 changes: 2 additions & 0 deletions ui/src/controller/trace_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ export class TraceController extends Controller<States> {
Actions.setEngineFailed({mode: 'HTTP_RPC', failure: `${err}`}));
throw err;
};
engine.timelineConstraint = globals.timelineSubsetRange;
globals.httpRpcEngineCustomizer?.(engine);
} else {
console.log('Opening trace using built-in WASM engine');
Expand All @@ -377,6 +378,7 @@ export class TraceController extends Controller<States> {
ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(),
analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(),
});
engine.timelineConstraint = globals.timelineSubsetRange;
}
this.engine = engine;

Expand Down
15 changes: 15 additions & 0 deletions ui/src/frontend/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ class Globals {
private _promptToLoadFromTraceProcessorShell = true;
private _trackFilteringEnabled = false;
private _engineReadyObservers: ((engine: EngineConfig) => void)[] = [];
private _timelineSubsetRange?: Span<TPTime, TPDuration> = undefined;


// Init from session storage since correct value may be required very early on
Expand Down Expand Up @@ -738,6 +739,20 @@ class Globals {
this._engineReadyObservers.push(observer);
}

/**
* Obtain a range of timestamps to which the entire timeline is restricted.
*/
get timelineSubsetRange(): Span<TPTime, TPDuration> | undefined {
return this._timelineSubsetRange;
}

/**
* Set or clear the range of timestamps to which the entire timeline is restricted.
*/
set timelineSubsetRange(timeSpan: Span<TPTime, TPDuration> | undefined) {
this._timelineSubsetRange = timeSpan;
}

makeSelection(action: DeferredAction<{}>, tabToOpen = 'current_selection') {
// A new selection should cancel the current search selection.
globals.dispatch(Actions.setSearchIndex({index: -1}));
Expand Down
16 changes: 13 additions & 3 deletions ui/src/frontend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {TraceInfoPage} from './trace_info_page';
import {maybeOpenTraceFromRoute} from './trace_url_handler';
import {ViewerPage} from './viewer_page';
import {WidgetsPage} from './widgets_page';
import {TPTimeSpan, tpTimeFromSql} from '../common/time';

export {ViewerPage} from './viewer_page';
export {globals} from './globals';
Expand Down Expand Up @@ -145,9 +146,18 @@ function setExtensionAvailability(available: boolean) {
}

function initGlobalsFromQueryString() {
const queryString = window.location.search;
globals.embeddedMode = queryString.includes('mode=embedded');
globals.hideSidebar = queryString.includes('hideSidebar=true');
const queryString = new URLSearchParams(window.location.search);
globals.embeddedMode = queryString.get('mode') === 'embedded';
globals.hideSidebar = queryString.get('hideSidebar') === 'true';
const startTs = queryString.get('startTs');
const endTs = queryString.get('endTs');
if (startTs && endTs) {
try {
globals.timelineSubsetRange = new TPTimeSpan(tpTimeFromSql(startTs), tpTimeFromSql(endTs));
} catch (error) {
console.error('Invalid timeline subset range.', error);
}
}
}

function defaultContentSecurityPolicy(): string {
Expand Down

0 comments on commit a39b428

Please sign in to comment.