From 0f69da11d4ba18d85625b84fd7bf7a2f11c5b749 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Wed, 5 Mar 2025 16:58:49 -0500 Subject: [PATCH 1/7] convert android format to sampled --- static/app/types/profiling.d.ts | 5 + static/app/utils/profiling/guards/profile.tsx | 13 +- .../profile/continuousProfile.spec.tsx | 192 +++++++++++++----- .../utils/profiling/profile/importProfile.tsx | 173 ++++++++++++++++ .../app/utils/profiling/profile/testUtils.tsx | 17 ++ static/app/utils/profiling/profile/utils.tsx | 45 ++++ .../app/views/profiling/profilesProvider.tsx | 7 +- 7 files changed, 393 insertions(+), 59 deletions(-) diff --git a/static/app/types/profiling.d.ts b/static/app/types/profiling.d.ts index b678d7545d9868..db4538b53b26b3 100644 --- a/static/app/types/profiling.d.ts +++ b/static/app/types/profiling.d.ts @@ -141,6 +141,11 @@ declare namespace Profiling { profile: ContinuousProfile; } + interface SentryAndroidContinuousProfileChunk extends Omit { + profiles: ReadonlyArray>; + androidClock: string; + } + //////////////// interface RawProfileBase { endValue: number; diff --git a/static/app/utils/profiling/guards/profile.tsx b/static/app/utils/profiling/guards/profile.tsx index 15a430ef3bba44..e4a89f0b70ef51 100644 --- a/static/app/utils/profiling/guards/profile.tsx +++ b/static/app/utils/profiling/guards/profile.tsx @@ -45,8 +45,17 @@ export function isSentryContinuousProfile( export function isSentryContinuousProfileChunk( profile: any -): profile is Profiling.SentryContinousProfileChunk { - return 'chunk_id' in profile; +): profile is + | Profiling.SentryContinousProfileChunk + | Profiling.SentryAndroidContinuousProfileChunk { + // Temporary fix to check for profiler_id in the chunk + return 'chunk_id' in profile || 'profiler_id' in profile; +} + +export function isSentryAndroidContinuousProfileChunk( + profile: any +): profile is Profiling.SentryAndroidContinuousProfileChunk { + return 'androidClock' in profile; } export function isContinuousProfileReference( diff --git a/static/app/utils/profiling/profile/continuousProfile.spec.tsx b/static/app/utils/profiling/profile/continuousProfile.spec.tsx index b26a7b16d9e9a1..41ea60ab3c91d2 100644 --- a/static/app/utils/profiling/profile/continuousProfile.spec.tsx +++ b/static/app/utils/profiling/profile/continuousProfile.spec.tsx @@ -1,73 +1,153 @@ import {ContinuousProfile} from 'sentry/utils/profiling/profile/continuousProfile'; +import { + eventedProfileToSampledProfile, + importAndroidContinuousProfileChunk, +} from 'sentry/utils/profiling/profile/importProfile'; import {createContinuousProfileFrameIndex} from 'sentry/utils/profiling/profile/utils'; -import {makeSentryContinuousProfile, makeTestingBoilerplate} from './testUtils'; +import { + makeSentryAndroidContinuousProfileChunk, + makeSentryContinuousProfile, + makeTestingBoilerplate, +} from './testUtils'; describe('ContinuousProfile', () => { - it('imports the base properties', () => { - const trace = makeSentryContinuousProfile({ - profile: { - samples: [ - {timestamp: Date.now() / 1e3, stack_id: 0, thread_id: '0'}, - // 10ms later - {timestamp: Date.now() / 1e3 + 0.01, stack_id: 1, thread_id: '0'}, - ], - frames: [ - {function: 'foo', in_app: true}, - {function: 'bar', in_app: true}, - ], - stacks: [ - [0, 1], - [0, 1], - ], - }, + describe('sampled profile', () => { + it('imports the base properties', () => { + const trace = makeSentryContinuousProfile({ + profile: { + samples: [ + {timestamp: Date.now() / 1e3, stack_id: 0, thread_id: '0'}, + // 10ms later + {timestamp: Date.now() / 1e3 + 0.01, stack_id: 1, thread_id: '0'}, + ], + frames: [ + {function: 'foo', in_app: true}, + {function: 'bar', in_app: true}, + ], + stacks: [ + [0, 1], + [0, 1], + ], + }, + }); + + const profile = ContinuousProfile.FromProfile( + trace.profile, + createContinuousProfileFrameIndex(trace.profile.frames, 'node'), + {minTimestamp: 0, type: 'flamechart'} + ); + + expect(Math.round(profile.duration)).toBe(10); + expect(profile.startedAt).toBe(1508208080 * 1e3); + expect(profile.endedAt).toBe(1508208080.01 * 1e3); }); - const profile = ContinuousProfile.FromProfile( - trace.profile, - createContinuousProfileFrameIndex(trace.profile.frames, 'node'), - {minTimestamp: 0, type: 'flamechart'} - ); + it('rebuilds the stack', () => { + const trace = makeSentryContinuousProfile({ + profile: { + samples: [ + {timestamp: Date.now() / 1e3, stack_id: 0, thread_id: '0'}, + // 10ms later + {timestamp: Date.now() / 1e3 + 0.01, stack_id: 1, thread_id: '0'}, + ], + frames: [ + {function: 'foo', in_app: true, lineno: 0}, + {function: 'bar', in_app: true, lineno: 1}, + ], + stacks: [ + [0, 1], + [0, 1], + ], + }, + }); + + const {open, close, timings} = makeTestingBoilerplate(); + + const profile = ContinuousProfile.FromProfile( + trace.profile, + createContinuousProfileFrameIndex(trace.profile.frames, 'node'), + {minTimestamp: 0, type: 'flamechart'} + ); + + profile.forEach(open, close); - expect(Math.round(profile.duration)).toBe(10); - expect(profile.startedAt).toBe(1508208080 * 1e3); - expect(profile.endedAt).toBe(1508208080.01 * 1e3); + expect(timings).toEqual([ + ['bar', 'open'], + ['foo', 'open'], + ['foo', 'close'], + ['bar', 'close'], + ]); + }); }); - it('rebuilds the stack', () => { - const trace = makeSentryContinuousProfile({ - profile: { - samples: [ - {timestamp: Date.now() / 1e3, stack_id: 0, thread_id: '0'}, - // 10ms later - {timestamp: Date.now() / 1e3 + 0.01, stack_id: 1, thread_id: '0'}, - ], - frames: [ - {function: 'foo', in_app: true, lineno: 0}, - {function: 'bar', in_app: true, lineno: 1}, + describe('android continuous profile chunk', () => { + it('imports the base properties', () => { + const trace = makeSentryAndroidContinuousProfileChunk({ + shared: { + frames: [ + {name: 'foo', line: 0}, + {name: 'bar', line: 1}, + ], + }, + profiles: [ + { + endValue: 100, + events: [ + {type: 'O', frame: 0, at: 0}, + {type: 'O', frame: 1, at: 1}, + {type: 'C', frame: 1, at: 2}, + {type: 'C', frame: 0, at: 3}, + ], + name: 'main', + startValue: 0, + threadID: 1, + type: 'evented', + unit: 'nanoseconds', + }, ], - stacks: [ - [0, 1], - [0, 1], - ], - }, - }); + }); - const {open, close, timings} = makeTestingBoilerplate(); + const profile = importAndroidContinuousProfileChunk(trace, '123', { + span: undefined, + type: 'flamechart', + frameFilter: undefined, + profileIds: undefined, + }); - const profile = ContinuousProfile.FromProfile( - trace.profile, - createContinuousProfileFrameIndex(trace.profile.frames, 'node'), - {minTimestamp: 0, type: 'flamechart'} - ); + expect(profile.profiles[0]!.duration).toBe(3000); + expect(profile.profiles[0]!.startedAt).toBe(0); + expect(profile.profiles[0]!.endedAt).toBe(3000); + }); - profile.forEach(open, close); + it('assigns stacks', () => { + const trace = makeSentryAndroidContinuousProfileChunk({ + shared: { + frames: [ + {name: 'foo', line: 0}, + {name: 'bar', line: 1}, + ], + }, + profiles: [ + { + endValue: 100, + events: [ + {type: 'O', frame: 0, at: 0}, + {type: 'O', frame: 1, at: 1}, + {type: 'C', frame: 1, at: 2}, + {type: 'C', frame: 0, at: 3}, + ], + name: 'main', + startValue: 0, + threadID: 1, + type: 'evented', + unit: 'nanoseconds', + }, + ], + }); - expect(timings).toEqual([ - ['bar', 'open'], - ['foo', 'open'], - ['foo', 'close'], - ['bar', 'close'], - ]); + const profile = eventedProfileToSampledProfile(trace.profiles); + expect(profile.stacks).toEqual([[0], [0, 1], [0], []]); + }); }); }); diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index 7982568678cc96..343ab0ad3bfcee 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -10,6 +10,7 @@ import { isJSProfile, isSampledProfile, isSchema, + isSentryAndroidContinuousProfileChunk, isSentryContinuousProfile, isSentryContinuousProfileChunk, isSentrySampledProfile, @@ -22,6 +23,7 @@ import type {Profile} from './profile'; import {SampledProfile} from './sampledProfile'; import {SentrySampledProfile} from './sentrySampledProfile'; import { + createAndroidContinuousProfileFrameIndex, createContinuousProfileFrameIndex, createFrameIndex, createSentrySampleProfileFrameIndex, @@ -67,6 +69,15 @@ export function importProfile( try { if (isSentryContinuousProfileChunk(input)) { scope.setTag('profile.type', 'sentry-continuous'); + if (isSentryAndroidContinuousProfileChunk(input)) { + return importAndroidContinuousProfileChunk(input, traceID, { + span, + type, + frameFilter, + activeThreadId, + continuous: true, + }); + } return importSentryContinuousProfileChunk(input, traceID, { span, type, @@ -241,6 +252,168 @@ export function importSchema( }; } +export function eventedProfileToSampledProfile( + input: ReadonlyArray> +): Pick< + Readonly['profile'], + 'samples' | 'stacks' | 'thread_metadata' +> { + const stacks: Profiling.SentrySampledProfile['profile']['stacks'] = []; + const samples: Profiling.SentrySampledProfileChunkSample[] = []; + const thread_metadata: Profiling.SentrySampledProfile['profile']['thread_metadata'] = + {}; + + for (const profile of input) { + let stackId = 0; + const stack: number[] = []; + + thread_metadata[profile.threadID] = { + name: profile.name, + }; + + for (const current of profile.events) { + if (current.type === 'O') { + stack.push(current.frame); + } else if (current.type === 'C') { + const poppedFrame = stack.pop(); + + if (poppedFrame === undefined) { + throw new Error('Stack underflow'); + } + if (poppedFrame !== current.frame) { + throw new Error('Stack mismatch'); + } + } else { + throw new TypeError('Unknown event type, expected O or C, got ' + current.type); + } + + samples.push({ + stack_id: stackId, + thread_id: String(profile.threadID), + timestamp: current.at, + }); + + stacks[stackId] = stack.slice(); + stackId++; + } + + if (stack.length > 0) { + samples.push({ + stack_id: stackId, + thread_id: String(profile.threadID), + timestamp: profile.events[profile.events.length - 1]!.at, + }); + stacks[stackId] = stack.slice(); + stackId++; + } + } + + return { + samples, + stacks, + thread_metadata, + }; +} + +export function importAndroidContinuousProfileChunk( + input: Profiling.SentryAndroidContinuousProfileChunk, + traceID: string, + options: ImportOptions +): ProfileGroup { + const frameIndex = createAndroidContinuousProfileFrameIndex( + input.shared.frames, + input.metadata.platform + ); + + const frames: Profiling.SentrySampledProfileFrame[] = []; + for (const frame of input.shared.frames) { + frames.push({ + in_app: frame.is_application ?? false, + filename: frame.file, + abs_path: frame.path, + module: frame.module, + package: frame.package, + column: frame.columnNumber ?? frame?.col ?? frame?.column, + symbol: frame.symbol, + lineno: frame.lineNumber, + colno: frame.columnNumber, + function: frame.name, + }); + } + + const samplesByThread: Record< + string, + Profiling.SentryContinousProfileChunk['profile']['samples'] + > = {}; + + const convertedProfile = eventedProfileToSampledProfile(input.profiles); + + // @todo(jonas): implement measurements + const minTimestamp = minTimestampInChunk({...convertedProfile, frames}, {}); + + for (const sample of convertedProfile.samples) { + if (!samplesByThread[sample.thread_id]) { + samplesByThread[sample.thread_id] = []; + } + samplesByThread[sample.thread_id]!.push(sample); + } + + for (const key in samplesByThread) { + samplesByThread[key]!.sort((a, b) => a.timestamp - b.timestamp); + } + + const profiles: ContinuousProfile[] = []; + let activeProfileIndex = input.activeProfileIndex ?? 0; + + for (const key in samplesByThread) { + const profile: Profiling.ContinuousProfile = { + ...input, + ...convertedProfile, + frames, + samples: samplesByThread[key]!, + }; + + if (options.activeThreadId && key === options.activeThreadId) { + activeProfileIndex = profiles.length; + } + + profiles.push( + wrapWithSpan( + options.span, + () => + ContinuousProfile.FromProfile(profile, frameIndex, { + minTimestamp, + type: options.type, + frameFilter: options.frameFilter, + }), + { + op: 'profile.import', + description: 'continuous', + } + ) + ); + } + + return { + traceID, + name: '', + type: 'continuous', + transactionID: null, + activeProfileIndex, + profiles, + // @TODO(jonas): implement this + measurements: {}, + // measurements: measurementsFromContinuousMeasurements( + // input.measurements ?? {}, + // minTimestamp + // ), + metadata: { + platform: input.metadata.platform, + projectID: input.metadata.projectID, + }, + }; +} + export function importSentryContinuousProfileChunk( input: Readonly, traceID: string, diff --git a/static/app/utils/profiling/profile/testUtils.tsx b/static/app/utils/profiling/profile/testUtils.tsx index 3a952bd2472fe5..cf4d74fc2f83d8 100644 --- a/static/app/utils/profiling/profile/testUtils.tsx +++ b/static/app/utils/profiling/profile/testUtils.tsx @@ -70,6 +70,23 @@ export function makeSentryContinuousProfile( ) as Profiling.SentryContinousProfileChunk; } +export function makeSentryAndroidContinuousProfileChunk( + profile?: DeepPartial +): Profiling.SentryAndroidContinuousProfileChunk { + return merge( + { + transactionName: 'foo', + metrics: [], + profiles: [], + metadata: {}, + profileID: '1', + projectID: 1, + shared: {}, + }, + profile + ) as Profiling.SentryAndroidContinuousProfileChunk; +} + export const makeSentrySampledProfile = ( profile?: DeepPartial ) => { diff --git a/static/app/utils/profiling/profile/utils.tsx b/static/app/utils/profiling/profile/utils.tsx index ea126d01c6422d..48832223c81d34 100644 --- a/static/app/utils/profiling/profile/utils.tsx +++ b/static/app/utils/profiling/profile/utils.tsx @@ -54,6 +54,51 @@ export function createContinuousProfileFrameIndex( return index; } +export function createAndroidContinuousProfileFrameIndex( + frames: Profiling.SentryAndroidContinuousProfileChunk['shared']['frames'], + platform: 'mobile' | 'node' | 'javascript' | string +): FrameIndex { + const index: FrameIndex = {}; + const insertionCache: Record = {}; + let idx = -1; + + for (let i = 0; i < frames.length; i++) { + const frame = frames[i]!; + const frameKey = `${frame.file ?? ''}:${frame.name ?? 'unknown'}:${String( + frame.lineNumber + )}:${frame.instructionAddr ?? ''}`; + + const existing = insertionCache[frameKey]; + if (existing) { + index[++idx] = existing; + continue; + } + + const f = new Frame( + { + key: i, + is_application: frame.is_application, + file: frame.file, + path: frame.path, + module: frame.module, + package: frame.package, + name: frame.name ?? 'unknown', + line: frame.lineNumber, + column: frame.colno ?? frame?.col ?? frame?.column, + instructionAddr: frame.instructionAddr, + symbol: frame.symbol, + symbolAddr: frame.symbolAddr, + symbolicatorStatus: frame.symbolicatorStatus, + }, + platform + ); + index[++idx] = f; + insertionCache[frameKey] = f; + } + + return index; +} + export function createSentrySampleProfileFrameIndex( frames: Profiling.SentrySampledProfile['profile']['frames'], platform: 'mobile' | 'node' | 'javascript' | string diff --git a/static/app/views/profiling/profilesProvider.tsx b/static/app/views/profiling/profilesProvider.tsx index 7621032e42ab65..5e40370e2a5bc7 100644 --- a/static/app/views/profiling/profilesProvider.tsx +++ b/static/app/views/profiling/profilesProvider.tsx @@ -42,7 +42,12 @@ function fetchContinuousProfileFlamegraph( }, includeAllArgs: true, }) - .then(([data]) => data.chunk); + .then(([data]) => { + // Temporary fix to ensure the profiler_id is set for continuous profiles + const profile = data.chunk; + profile.profiler_id = query.profiler_id; + return profile; + }); } type ProfileProviderValue = RequestState; From a74565028c9570b953ae9d4e7b0e22f31c0921aa Mon Sep 17 00:00:00 2001 From: JonasBa Date: Wed, 5 Mar 2025 18:04:44 -0500 Subject: [PATCH 2/7] omit 0 weighted synthetic samples --- .../utils/profiling/profile/importProfile.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index 343ab0ad3bfcee..eb0a7ffe08bba9 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -271,6 +271,8 @@ export function eventedProfileToSampledProfile( name: profile.name, }; + let currentTimestamp = profile.events[0]!.at; + for (const current of profile.events) { if (current.type === 'O') { stack.push(current.frame); @@ -287,21 +289,24 @@ export function eventedProfileToSampledProfile( throw new TypeError('Unknown event type, expected O or C, got ' + current.type); } - samples.push({ - stack_id: stackId, - thread_id: String(profile.threadID), - timestamp: current.at, - }); + if (current.at !== currentTimestamp) { + samples.push({ + stack_id: stackId, + thread_id: String(profile.threadID), + timestamp: current.at * 1e-9, + }); - stacks[stackId] = stack.slice(); - stackId++; + stacks[stackId] = stack.slice(); + stackId++; + currentTimestamp = current.at; + } } if (stack.length > 0) { samples.push({ stack_id: stackId, thread_id: String(profile.threadID), - timestamp: profile.events[profile.events.length - 1]!.at, + timestamp: profile.events[profile.events.length - 1]!.at * 1e-9, }); stacks[stackId] = stack.slice(); stackId++; @@ -367,7 +372,6 @@ export function importAndroidContinuousProfileChunk( for (const key in samplesByThread) { const profile: Profiling.ContinuousProfile = { - ...input, ...convertedProfile, frames, samples: samplesByThread[key]!, From 8d29eebd5d34abce588a6f2759c56ecadcb8624d Mon Sep 17 00:00:00 2001 From: JonasBa Date: Wed, 5 Mar 2025 19:36:54 -0500 Subject: [PATCH 3/7] fix offset calculation --- .../flamegraph/continuousFlamegraph.tsx | 5 +++++ .../utils/profiling/profile/importProfile.tsx | 16 ++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx index f532f448c4638c..210fd30e52702b 100644 --- a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx +++ b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx @@ -138,6 +138,11 @@ function getProfileOffset( return Rect.Empty(); } + // @todo(jonas): uncomment this when we figure out where to anchor android continuous profiles + if (profile.startedAt - startedAtMs < 0) { + return Rect.Empty(); + } + return new Rect(profile.startedAt - startedAtMs, 0, 0, 0); } diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index eb0a7ffe08bba9..41a13764972090 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -293,7 +293,7 @@ export function eventedProfileToSampledProfile( samples.push({ stack_id: stackId, thread_id: String(profile.threadID), - timestamp: current.at * 1e-9, + timestamp: profile.startValue + current.at * 1e-9, }); stacks[stackId] = stack.slice(); @@ -306,7 +306,8 @@ export function eventedProfileToSampledProfile( samples.push({ stack_id: stackId, thread_id: String(profile.threadID), - timestamp: profile.events[profile.events.length - 1]!.at * 1e-9, + timestamp: + profile.startValue + profile.events[profile.events.length - 1]!.at * 1e-9, }); stacks[stackId] = stack.slice(); stackId++; @@ -363,14 +364,12 @@ export function importAndroidContinuousProfileChunk( samplesByThread[sample.thread_id]!.push(sample); } - for (const key in samplesByThread) { - samplesByThread[key]!.sort((a, b) => a.timestamp - b.timestamp); - } - const profiles: ContinuousProfile[] = []; let activeProfileIndex = input.activeProfileIndex ?? 0; for (const key in samplesByThread) { + samplesByThread[key]!.sort((a, b) => a.timestamp - b.timestamp); + const profile: Profiling.ContinuousProfile = { ...convertedProfile, frames, @@ -442,14 +441,11 @@ export function importSentryContinuousProfileChunk( samplesByThread[sample.thread_id]!.push(sample); } - for (const key in samplesByThread) { - samplesByThread[key]!.sort((a, b) => a.timestamp - b.timestamp); - } - const profiles: ContinuousProfile[] = []; let activeProfileIndex = 0; for (const key in samplesByThread) { + samplesByThread[key]!.sort((a, b) => a.timestamp - b.timestamp); const profile: Profiling.ContinuousProfile = { ...input, ...input.profile, From 735f66b6b9e81308d465c584b3770bf2b17cef38 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 6 Mar 2025 08:43:53 -0500 Subject: [PATCH 4/7] fix rendering of android profiles --- .../flamegraph/continuousFlamegraph.tsx | 5 ----- .../utils/profiling/profile/importProfile.tsx | 21 ++++++++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx index 210fd30e52702b..f532f448c4638c 100644 --- a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx +++ b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx @@ -138,11 +138,6 @@ function getProfileOffset( return Rect.Empty(); } - // @todo(jonas): uncomment this when we figure out where to anchor android continuous profiles - if (profile.startedAt - startedAtMs < 0) { - return Rect.Empty(); - } - return new Rect(profile.startedAt - startedAtMs, 0, 0, 0); } diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index 41a13764972090..5ae6d117666234 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -253,6 +253,7 @@ export function importSchema( } export function eventedProfileToSampledProfile( + profileTimestamp: number, input: ReadonlyArray> ): Pick< Readonly['profile'], @@ -293,10 +294,10 @@ export function eventedProfileToSampledProfile( samples.push({ stack_id: stackId, thread_id: String(profile.threadID), - timestamp: profile.startValue + current.at * 1e-9, + timestamp: profileTimestamp + current.at * 1e-9, }); - stacks[stackId] = stack.slice(); + stacks[stackId] = stack.slice().reverse(); stackId++; currentTimestamp = current.at; } @@ -307,9 +308,9 @@ export function eventedProfileToSampledProfile( stack_id: stackId, thread_id: String(profile.threadID), timestamp: - profile.startValue + profile.events[profile.events.length - 1]!.at * 1e-9, + profileTimestamp + profile.events[profile.events.length - 1]!.at * 1e-9, }); - stacks[stackId] = stack.slice(); + stacks[stackId] = stack.slice().reverse(); stackId++; } } @@ -352,7 +353,17 @@ export function importAndroidContinuousProfileChunk( Profiling.SentryContinousProfileChunk['profile']['samples'] > = {}; - const convertedProfile = eventedProfileToSampledProfile(input.profiles); + if (!input.metadata.timestamp) { + throw new TypeError( + 'No timestamp found in metadata, typestamp is required to render continuous profiles' + ); + } + const profileTimestampInSeconds = new Date(input.metadata.timestamp).getTime() * 1e-3; + + const convertedProfile = eventedProfileToSampledProfile( + profileTimestampInSeconds, + input.profiles + ); // @todo(jonas): implement measurements const minTimestamp = minTimestampInChunk({...convertedProfile, frames}, {}); From ca372475d82833f7970b1a12ff0a47eb55b2ae12 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 6 Mar 2025 12:08:11 -0500 Subject: [PATCH 5/7] profiling: generate flamegraph from continuous profile --- static/app/types/profiling.d.ts | 1 + .../utils/profiling/profile/importProfile.tsx | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/static/app/types/profiling.d.ts b/static/app/types/profiling.d.ts index db4538b53b26b3..3851dffa587c37 100644 --- a/static/app/types/profiling.d.ts +++ b/static/app/types/profiling.d.ts @@ -144,6 +144,7 @@ declare namespace Profiling { interface SentryAndroidContinuousProfileChunk extends Omit { profiles: ReadonlyArray>; androidClock: string; + measurements?: ContinuousMeasurements; } //////////////// diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index 5ae6d117666234..c926db1fb62a70 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -365,8 +365,10 @@ export function importAndroidContinuousProfileChunk( input.profiles ); - // @todo(jonas): implement measurements - const minTimestamp = minTimestampInChunk({...convertedProfile, frames}, {}); + const minTimestamp = minTimestampInChunk( + {...convertedProfile, frames}, + input.measurements ?? {} + ); for (const sample of convertedProfile.samples) { if (!samplesByThread[sample.thread_id]) { @@ -415,12 +417,10 @@ export function importAndroidContinuousProfileChunk( transactionID: null, activeProfileIndex, profiles, - // @TODO(jonas): implement this - measurements: {}, - // measurements: measurementsFromContinuousMeasurements( - // input.measurements ?? {}, - // minTimestamp - // ), + measurements: measurementsFromContinuousMeasurements( + input.measurements ?? {}, + minTimestamp + ), metadata: { platform: input.metadata.platform, projectID: input.metadata.projectID, From d686e969823bf229a4dc82739eef17c073377c84 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 6 Mar 2025 12:29:03 -0500 Subject: [PATCH 6/7] profiling: fix tid selection --- static/app/utils/profiling/profile/importProfile.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index c926db1fb62a70..5e28bfbf946c66 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -380,6 +380,10 @@ export function importAndroidContinuousProfileChunk( const profiles: ContinuousProfile[] = []; let activeProfileIndex = input.activeProfileIndex ?? 0; + if (options.activeThreadId === undefined) { + options.activeThreadId = String(input.profiles[activeProfileIndex]?.threadID); + } + for (const key in samplesByThread) { samplesByThread[key]!.sort((a, b) => a.timestamp - b.timestamp); From 5d65e82ef9dc0fb7b876f877264ef3456ad1fb34 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 6 Mar 2025 12:46:05 -0500 Subject: [PATCH 7/7] profiling: fix sample generaiton --- .../profile/continuousProfile.spec.tsx | 19 +++++++++++++++---- .../utils/profiling/profile/importProfile.tsx | 17 +++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/static/app/utils/profiling/profile/continuousProfile.spec.tsx b/static/app/utils/profiling/profile/continuousProfile.spec.tsx index 41ea60ab3c91d2..25419498a55648 100644 --- a/static/app/utils/profiling/profile/continuousProfile.spec.tsx +++ b/static/app/utils/profiling/profile/continuousProfile.spec.tsx @@ -84,6 +84,9 @@ describe('ContinuousProfile', () => { describe('android continuous profile chunk', () => { it('imports the base properties', () => { const trace = makeSentryAndroidContinuousProfileChunk({ + metadata: { + timestamp: '2021-01-01T00:00:00.000Z', + }, shared: { frames: [ {name: 'foo', line: 0}, @@ -115,13 +118,16 @@ describe('ContinuousProfile', () => { profileIds: undefined, }); - expect(profile.profiles[0]!.duration).toBe(3000); + expect(profile.profiles[0]!.duration).toBe(0); expect(profile.profiles[0]!.startedAt).toBe(0); - expect(profile.profiles[0]!.endedAt).toBe(3000); + expect(profile.profiles[0]!.endedAt).toBe(0); }); it('assigns stacks', () => { const trace = makeSentryAndroidContinuousProfileChunk({ + metadata: { + timestamp: '2021-01-01T00:00:00.000Z', + }, shared: { frames: [ {name: 'foo', line: 0}, @@ -146,8 +152,13 @@ describe('ContinuousProfile', () => { ], }); - const profile = eventedProfileToSampledProfile(trace.profiles); - expect(profile.stacks).toEqual([[0], [0, 1], [0], []]); + const profile = eventedProfileToSampledProfile(0, trace.profiles); + expect(profile.stacks).toEqual([[0], [1, 0], [0], []]); + expect(profile.samples).toHaveLength(4); + expect(profile.samples[0]!.stack_id).toBe(0); + expect(profile.samples[1]!.stack_id).toBe(1); + // We do not deduplicate stacks, so the third sample has a different stack_id + expect(profile.samples[2]!.stack_id).toBe(2); }); }); }); diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index 5e28bfbf946c66..05490480a6e019 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -272,9 +272,19 @@ export function eventedProfileToSampledProfile( name: profile.name, }; - let currentTimestamp = profile.events[0]!.at; + stack.push(profile.events[0]!.frame); + samples.push({ + stack_id: stackId, + thread_id: String(profile.threadID), + timestamp: profileTimestamp + profile.events[0]!.at * 1e-9, + }); + stacks[stackId] = stack.slice().reverse(); + stackId++; + + for (let i = 1; i < profile.events.length; i++) { + const current = profile.events[i]!; + const previous = profile.events[i - 1] ?? current; - for (const current of profile.events) { if (current.type === 'O') { stack.push(current.frame); } else if (current.type === 'C') { @@ -290,7 +300,7 @@ export function eventedProfileToSampledProfile( throw new TypeError('Unknown event type, expected O or C, got ' + current.type); } - if (current.at !== currentTimestamp) { + if (current.at !== previous.at) { samples.push({ stack_id: stackId, thread_id: String(profile.threadID), @@ -299,7 +309,6 @@ export function eventedProfileToSampledProfile( stacks[stackId] = stack.slice().reverse(); stackId++; - currentTimestamp = current.at; } }