Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change payload for Continuous Profiling v8 (p2) #3711

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
import io.sentry.ILogger;
import io.sentry.IScopes;
import io.sentry.ISentryExecutorService;
import io.sentry.ProfileChunk;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.protocol.SentryId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import org.jetbrains.annotations.ApiStatus;
Expand All @@ -33,6 +38,9 @@ public class AndroidContinuousProfiler implements IContinuousProfiler {
private boolean isRunning = false;
private @Nullable IScopes scopes;
private @Nullable Future<?> closeFuture;
private final @NotNull List<ProfileChunk.Builder> payloadBuilders = new ArrayList<>();
private @NotNull SentryId profilerId = SentryId.EMPTY_ID;
private @NotNull SentryId chunkId = SentryId.EMPTY_ID;

public AndroidContinuousProfiler(
final @NotNull BuildInfoProvider buildInfoProvider,
Expand Down Expand Up @@ -81,8 +89,7 @@ private void init() {
(int) SECONDS.toMicros(1) / profilingTracesHz,
frameMetricsCollector,
null,
logger,
buildInfoProvider);
logger);
}

public synchronized void setScopes(final @NotNull IScopes scopes) {
Expand All @@ -106,8 +113,17 @@ public synchronized void start() {
if (startData == null) {
return;
}

isRunning = true;

if (profilerId == SentryId.EMPTY_ID) {
profilerId = new SentryId();
}

if (chunkId == SentryId.EMPTY_ID) {
chunkId = new SentryId();
}

try {
closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS);
} catch (RejectedExecutionException e) {
Expand Down Expand Up @@ -143,17 +159,35 @@ private synchronized void stop(final boolean restartProfiler) {

// check if profiler end successfully
if (endData == null) {
// A problem occurred. Profile chunk is not captured. Let's reset ids.
chunkId = SentryId.EMPTY_ID;
profilerId = SentryId.EMPTY_ID;
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
return;
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
}

// The scopes can be null if the profiler is started before the SDK is initialized (app start
// profiling), meaning there's no scopes to send the chunks. In that case, we store the data
// in a list and send it when the next chunk is finished.
synchronized (payloadBuilders) {
payloadBuilders.add(
new ProfileChunk.Builder(
profilerId, chunkId, endData.measurementsMap, endData.traceFile));
}

isRunning = false;
// A chunk is finished. Next chunk will have a different id.
chunkId = SentryId.EMPTY_ID;

// todo schedule capture profile chunk envelope
if (scopes != null) {
sendChunks(scopes, scopes.getOptions());
}

if (restartProfiler) {
logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one.");
start();
} else {
// When the profiler is stopped manually, we have to reset its id
profilerId = SentryId.EMPTY_ID;
logger.log(SentryLevel.DEBUG, "Profile chunk finished.");
}
}
Expand All @@ -162,6 +196,28 @@ public synchronized void close() {
stop();
}

private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
try {
options
.getExecutorService()
.submit(
() -> {
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
final ArrayList<ProfileChunk> payloads = new ArrayList<>(payloadBuilders.size());
romtsn marked this conversation as resolved.
Show resolved Hide resolved
synchronized (payloadBuilders) {
for (ProfileChunk.Builder builder : payloadBuilders) {
payloads.add(builder.build(options));
}
payloadBuilders.clear();
}
for (ProfileChunk payload : payloads) {
scopes.captureProfileChunk(payload);
romtsn marked this conversation as resolved.
Show resolved Hide resolved
}
});
} catch (Throwable e) {
options.getLogger().log(SentryLevel.DEBUG, "Failed to send profile chunks.", e);
}
}

@Override
public boolean isRunning() {
return isRunning;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.sentry.IPerformanceSnapshotCollector;
import io.sentry.PerformanceCollectionData;
import io.sentry.SentryLevel;
import io.sentry.SentryNanotimeDate;
import io.sentry.util.FileUtils;
import io.sentry.util.Objects;
import java.io.File;
Expand Down Expand Up @@ -73,7 +74,7 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti

CpuCollectionData cpuData =
new CpuCollectionData(
System.currentTimeMillis(), (cpuUsagePercentage / (double) numCores) * 100.0);
(cpuUsagePercentage / (double) numCores) * 100.0, new SentryNanotimeDate());

performanceCollectionData.addCpuData(cpuData);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.sentry.IPerformanceSnapshotCollector;
import io.sentry.MemoryCollectionData;
import io.sentry.PerformanceCollectionData;
import io.sentry.SentryNanotimeDate;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

Expand All @@ -15,10 +16,10 @@ public void setup() {}

@Override
public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) {
long now = System.currentTimeMillis();
long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
long usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize();
MemoryCollectionData memoryData = new MemoryCollectionData(now, usedMemory, usedNativeMemory);
MemoryCollectionData memoryData =
new MemoryCollectionData(usedMemory, usedNativeMemory, new SentryNanotimeDate());
performanceCollectionData.addMemoryData(memoryData);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import io.sentry.ISentryLifecycleToken;
import io.sentry.MemoryCollectionData;
import io.sentry.PerformanceCollectionData;
import io.sentry.SentryDate;
import io.sentry.SentryLevel;
import io.sentry.SentryNanotimeDate;
import io.sentry.SentryUUID;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.profilemeasurements.ProfileMeasurement;
Expand Down Expand Up @@ -154,6 +156,7 @@ public void onFrameMetricCollected(
// profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(),
// but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp
// relative to profileStartNanos
final SentryDate timestamp = new SentryNanotimeDate();
final long frameTimestampRelativeNanos =
frameEndNanos
- System.nanoTime()
Expand All @@ -167,15 +170,18 @@ public void onFrameMetricCollected(
}
if (isFrozen) {
frozenFrameRenderMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
new ProfileMeasurementValue(
frameTimestampRelativeNanos, durationNanos, timestamp));
} else if (isSlow) {
slowFrameRenderMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
new ProfileMeasurementValue(
frameTimestampRelativeNanos, durationNanos, timestamp));
}
if (refreshRate != lastRefreshRate) {
lastRefreshRate = refreshRate;
screenFrameRateMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate));
new ProfileMeasurementValue(
frameTimestampRelativeNanos, refreshRate, timestamp));
}
}
});
Expand Down Expand Up @@ -318,20 +324,23 @@ private void putPerformanceCollectionDataInMeasurements(
if (cpuData != null) {
cpuUsageMeasurements.add(
new ProfileMeasurementValue(
TimeUnit.MILLISECONDS.toNanos(cpuData.getTimestampMillis()) + timestampDiff,
cpuData.getCpuUsagePercentage()));
cpuData.getTimestamp().nanoTimestamp() + timestampDiff,
cpuData.getCpuUsagePercentage(),
cpuData.getTimestamp()));
}
if (memoryData != null && memoryData.getUsedHeapMemory() > -1) {
memoryUsageMeasurements.add(
new ProfileMeasurementValue(
TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff,
memoryData.getUsedHeapMemory()));
memoryData.getTimestamp().nanoTimestamp() + timestampDiff,
memoryData.getUsedHeapMemory(),
memoryData.getTimestamp()));
}
if (memoryData != null && memoryData.getUsedNativeMemory() > -1) {
nativeMemoryUsageMeasurements.add(
new ProfileMeasurementValue(
TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff,
memoryData.getUsedNativeMemory()));
memoryData.getTimestamp().nanoTimestamp() + timestampDiff,
memoryData.getUsedNativeMemory(),
memoryData.getTimestamp()));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import android.content.Context
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.IHub
import io.sentry.ILogger
import io.sentry.IScopes
import io.sentry.ISentryExecutorService
import io.sentry.SentryLevel
import io.sentry.SentryTracer
Expand Down Expand Up @@ -45,7 +45,7 @@ class AndroidContinuousProfilerTest {
}
val mockLogger = mock<ILogger>()

val hub: IHub = mock()
val scopes: IScopes = mock()
val frameMetricsCollector: SentryFrameMetricsCollector = mock()

lateinit var transaction1: SentryTracer
Expand All @@ -61,10 +61,10 @@ class AndroidContinuousProfilerTest {

fun getSut(buildInfoProvider: BuildInfoProvider = buildInfo, optionConfig: ((options: SentryAndroidOptions) -> Unit) = {}): AndroidContinuousProfiler {
optionConfig(options)
whenever(hub.options).thenReturn(options)
transaction1 = SentryTracer(TransactionContext("", ""), hub)
transaction2 = SentryTracer(TransactionContext("", ""), hub)
transaction3 = SentryTracer(TransactionContext("", ""), hub)
whenever(scopes.options).thenReturn(options)
transaction1 = SentryTracer(TransactionContext("", ""), scopes)
transaction2 = SentryTracer(TransactionContext("", ""), scopes)
transaction3 = SentryTracer(TransactionContext("", ""), scopes)
return AndroidContinuousProfiler(
buildInfoProvider,
frameMetricsCollector,
Expand All @@ -73,7 +73,7 @@ class AndroidContinuousProfilerTest {
options.isProfilingEnabled,
options.profilingTracesHz,
options.executorService
)
).also { it.setScopes(scopes) }
}
}

Expand Down Expand Up @@ -316,4 +316,38 @@ class AndroidContinuousProfilerTest {
verify(fixture.mockLogger, times(2)).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one."))
assertTrue(profiler.isRunning)
}

@Test
fun `profiler sends chunk on each restart`() {
val executorService = DeferredExecutorService()
val profiler = fixture.getSut {
it.executorService = executorService
}
profiler.start()
assertTrue(profiler.isRunning)
// We run the executor service to trigger the profiler restart (chunk finish)
executorService.runAll()
verify(fixture.scopes, never()).captureProfileChunk(any())
// Now the executor is used to send the chunk
executorService.runAll()
verify(fixture.scopes).captureProfileChunk(any())
}

@Test
fun `profiler sends another chunk on stop`() {
val executorService = DeferredExecutorService()
val profiler = fixture.getSut {
it.executorService = executorService
}
profiler.start()
assertTrue(profiler.isRunning)
// We run the executor service to trigger the profiler restart (chunk finish)
executorService.runAll()
verify(fixture.scopes, never()).captureProfileChunk(any())
// We stop the profiler, which should send an additional chunk
profiler.stop()
// Now the executor is used to send the chunk
executorService.runAll()
verify(fixture.scopes, times(2)).captureProfileChunk(any())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ class AndroidCpuCollectorTest {
val cpuData = data.cpuData
assertNotNull(cpuData)
assertNotEquals(0.0, cpuData.cpuUsagePercentage)
assertNotEquals(0, cpuData.timestampMillis)
assertNotEquals(0, cpuData.timestamp.nanoTimestamp())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ class AndroidMemoryCollectorTest {
assertNotEquals(-1, memoryData.usedNativeMemory)
assertEquals(usedNativeMemory, memoryData.usedNativeMemory)
assertEquals(usedMemory, memoryData.usedHeapMemory)
assertNotEquals(0, memoryData.timestampMillis)
assertNotEquals(0, memoryData.timestamp.nanoTimestamp())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.sentry.ILogger
import io.sentry.ISentryExecutorService
import io.sentry.MemoryCollectionData
import io.sentry.PerformanceCollectionData
import io.sentry.SentryDate
import io.sentry.SentryExecutorService
import io.sentry.SentryLevel
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector
Expand Down Expand Up @@ -258,12 +259,14 @@ class AndroidProfilerTest {
val profiler = fixture.getSut()
val performanceCollectionData = ArrayList<PerformanceCollectionData>()
var singleData = PerformanceCollectionData()
singleData.addMemoryData(MemoryCollectionData(1, 2, 3))
singleData.addCpuData(CpuCollectionData(1, 1.4))
val t1 = mock<SentryDate>()
val t2 = mock<SentryDate>()
singleData.addMemoryData(MemoryCollectionData(2, 3, t1))
singleData.addCpuData(CpuCollectionData(1.4, t1))
performanceCollectionData.add(singleData)

singleData = PerformanceCollectionData()
singleData.addMemoryData(MemoryCollectionData(2, 3, 4))
singleData.addMemoryData(MemoryCollectionData(3, 4, t2))
performanceCollectionData.add(singleData)

profiler.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,12 @@ class AndroidTransactionProfilerTest {
val profiler = fixture.getSut(context)
val performanceCollectionData = ArrayList<PerformanceCollectionData>()
var singleData = PerformanceCollectionData()
singleData.addMemoryData(MemoryCollectionData(1, 2, 3))
singleData.addCpuData(CpuCollectionData(1, 1.4))
singleData.addMemoryData(MemoryCollectionData(2, 3, mock()))
singleData.addCpuData(CpuCollectionData(1.4, mock()))
performanceCollectionData.add(singleData)

singleData = PerformanceCollectionData()
singleData.addMemoryData(MemoryCollectionData(2, 3, 4))
singleData.addMemoryData(MemoryCollectionData(3, 4, mock()))
performanceCollectionData.add(singleData)

profiler.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.sentry.CheckIn
import io.sentry.Hint
import io.sentry.IScope
import io.sentry.ISentryClient
import io.sentry.ProfileChunk
import io.sentry.ProfilingTraceData
import io.sentry.Sentry
import io.sentry.SentryEnvelope
Expand Down Expand Up @@ -176,6 +177,10 @@ class SessionTrackingIntegrationTest {
TODO("Not yet implemented")
}

override fun captureProfileChunk(profileChunk: ProfileChunk, scope: IScope?): SentryId {
TODO("Not yet implemented")
}

override fun captureCheckIn(checkIn: CheckIn, scope: IScope?, hint: Hint?): SentryId {
TODO("Not yet implemented")
}
Expand Down
Loading
Loading