// Copyright 2022 The Parca Authors // 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 { ProfileDiffSelection, ProfileDiffSelection_Mode, QueryRequest, QueryRequest_Mode, QueryRequest_ReportType, Timestamp, } from '@parca/client'; import {Matcher, NewParser, ProfileType, Query} from '@parca/parser'; import {formatDate, formatDuration} from '@parca/utilities'; export interface ProfileSource { QueryRequest: () => QueryRequest; ProfileType: () => ProfileType; DiffSelection: () => ProfileDiffSelection; toString: (timezone?: string) => string; toKey: () => string; } export interface ProfileSelection { ProfileName: () => string; HistoryParams: () => {[key: string]: any}; ProfileSource: () => ProfileSource; Type: () => string; } export const timeFormat = (timezone?: string): string => { if (timezone !== undefined) { return 'yyyy-MM-dd HH:mm:ss'; } return "yyyy-MM-dd HH:mm:ss '(UTC)'"; }; export function ParamsString(params: {[key: string]: string}): string { return Object.keys(params) .map(function (key) { return `${key}=${params[key]}`; }) .join('&'); } export function SuffixParams(params: {[key: string]: any}, suffix: string): {[key: string]: any} { return Object.fromEntries( Object.entries(params).map(([key, value]) => [`${key}${suffix}`, value]) ); } export function ProfileSelectionFromParams( mergeFrom: string | undefined, mergeTo: string | undefined, selection: string | undefined ): ProfileSelection | null { if ( mergeFrom !== undefined && mergeTo !== undefined && selection !== undefined && selection !== '' ) { const p = NewParser(); p.save(); const {successfulParse} = Query.tryParse(p, selection); if (!successfulParse) { console.log('Failed to parse selected query.'); console.log(selection); return null; } return new MergedProfileSelection(BigInt(mergeFrom), BigInt(mergeTo), Query.parse(selection)); } return null; } export class MergedProfileSelection implements ProfileSelection { mergeFrom: bigint; mergeTo: bigint; query: Query; profileSource: ProfileSource; constructor(mergeFrom: bigint, mergeTo: bigint, query: Query) { this.mergeFrom = mergeFrom; this.mergeTo = mergeTo; this.query = query; this.profileSource = new MergedProfileSource(this.mergeFrom, this.mergeTo, this.query); } ProfileName(): string { return this.query.profileName(); } HistoryParams(): {[key: string]: any} { return { merge_from: this.mergeFrom.toString(), merge_to: this.mergeTo.toString(), selection: this.query.toString(), }; } Type(): string { return 'merge'; } ProfileSource(): ProfileSource { return this.profileSource; } } // Type tag carried on instances so we can identify them reliably even when the // class gets duplicated across bundle chunks (prod builds sometimes end up with // two copies of the same class, making `instanceof` unreliable). export const PROFILE_SOURCE_TYPE_MERGED = 'merged' as const; export const PROFILE_SOURCE_TYPE_DIFF = 'diff' as const; const getProfileSourceType = (source: ProfileSource | null | undefined): string | undefined => { if (source == null) return undefined; const tag = (source as {profileSourceType?: unknown}).profileSourceType; return typeof tag === 'string' ? tag : undefined; }; export const isMergedProfileSource = ( source: ProfileSource | null | undefined ): source is MergedProfileSource => { return getProfileSourceType(source) === PROFILE_SOURCE_TYPE_MERGED; }; export const isProfileDiffSource = ( source: ProfileSource | null | undefined ): source is ProfileDiffSource => { return getProfileSourceType(source) === PROFILE_SOURCE_TYPE_DIFF; }; export class ProfileDiffSource implements ProfileSource { readonly profileSourceType = PROFILE_SOURCE_TYPE_DIFF; a: ProfileSource; b: ProfileSource; profileType: ProfileType; absolute?: boolean; constructor(a: ProfileSource, b: ProfileSource, absolute?: boolean) { this.a = a; this.b = b; this.profileType = a.ProfileType(); this.absolute = absolute; } DiffSelection(): ProfileDiffSelection { throw new Error('Method not implemented.'); } QueryRequest(): QueryRequest { return { options: { oneofKind: 'diff', diff: { a: this.a.DiffSelection(), b: this.b.DiffSelection(), absolute: this.absolute, }, }, reportType: QueryRequest_ReportType.FLAMEGRAPH_ARROW, mode: QueryRequest_Mode.DIFF, filter: [], }; } ProfileType(): ProfileType { return this.profileType; } Describe(): JSX.Element { return ( <>
Browse the comparison
> ); } toString(): string { const aDesc = this.a.toString(); const bDesc = this.b.toString(); if (aDesc === bDesc) { return 'Profile comparison'; } return `${this.a.toString()} compared with ${this.b.toString()}`; } toKey(): string { return `${this.a.toKey()}-${this.b.toKey()}`; } } function nanosToTimestamp(nanos: bigint): Timestamp { const NANOS_PER_SECOND = 1_000_000_000n; const seconds = nanos / NANOS_PER_SECOND; const remainingNanos = nanos % NANOS_PER_SECOND; return { seconds, nanos: Number(remainingNanos), // Safe since remainingNanos < 1e9 }; } export class MergedProfileSource implements ProfileSource { readonly profileSourceType = PROFILE_SOURCE_TYPE_MERGED; mergeFrom: bigint; mergeTo: bigint; query: Query; profileType: ProfileType; constructor(mergeFrom: bigint, mergeTo: bigint, query: Query) { this.mergeFrom = mergeFrom; this.mergeTo = mergeTo; this.query = query; this.profileType = ProfileType.fromString(Query.parse(this.query.toString()).profileName()); } DiffSelection(): ProfileDiffSelection { return { options: { oneofKind: 'merge', merge: { start: nanosToTimestamp(this.mergeFrom), end: nanosToTimestamp(this.mergeTo), query: this.query.toString(), }, }, mode: ProfileDiffSelection_Mode.MERGE, }; } QueryRequest(): QueryRequest { return { options: { oneofKind: 'merge', merge: { start: nanosToTimestamp(this.mergeFrom), end: nanosToTimestamp(this.mergeTo), query: this.query.toString(), }, }, reportType: QueryRequest_ReportType.FLAMEGRAPH_ARROW, mode: QueryRequest_Mode.MERGE, filter: [], }; } ProfileType(): ProfileType { return this.profileType; } stringMatchers(): string[] { return this.query.matchers .filter((m: Matcher) => m.key !== '__name__') .map((m: Matcher) => `${m.key}=${m.value}`); } toString(timezone?: string): string { let queryPart = ''; if (this.query.toString()?.length > 0) { queryPart = ` of query "${this.query.toString()}"`; } let timePart = ''; if (this.mergeFrom !== 0n) { timePart = `over ${formatDuration({ nanos: Number(this.mergeTo - this.mergeFrom), })} from ${formatDate(this.mergeFrom, timeFormat(timezone), timezone)} to ${formatDate( this.mergeTo, timeFormat(timezone), timezone )}`; } return `Merged profiles${queryPart}${timePart}`; } toKey(): string { return `${this.mergeFrom.toString()}-${this.mergeTo.toString()}-${this.query.toString()}`; } }