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

Add collectors and background service for measurements #2239

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import io.sentry.SendFireAndForgetEnvelopeSender;
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.android.core.internal.measurement.battery.BatteryLevelMeasurementCollectorFactory;
import io.sentry.android.core.internal.measurement.cpu.CpuMeasurementCollectorFactory;
import io.sentry.android.core.internal.measurement.memory.MemoryMeasurementCollectorFactory;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.util.Objects;
Expand Down Expand Up @@ -153,6 +156,15 @@ static void init(
options.setTransportGate(new AndroidTransportGate(context, options.getLogger()));
options.setTransactionProfiler(
new AndroidTransactionProfiler(context, options, buildInfoProvider));

options.addMetricsCollectorFactory(new CpuMeasurementCollectorFactory());
options.addMetricsCollectorFactory(new MemoryMeasurementCollectorFactory(context));
options.addMetricsCollectorFactory(new BatteryLevelMeasurementCollectorFactory(context));
// options.addMetricsCollectorFactory(new FpsMeasurementCollectorFactory(context));

options
.getMeasurementBackgroundService()
.registerBackgroundCollectors(options.getMeasurementCollectorFactories());
}

private static void installDefaultIntegrations(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.sentry.android.core.internal.measurement.battery;

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.measurement.MeasurementBackgroundCollector;
import io.sentry.measurement.MeasurementBackgroundServiceType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class BatteryLevelBackgroundMeasurementCollector
implements MeasurementBackgroundCollector {

private final @NotNull SentryOptions options;
private final @NotNull Context applicationContext;

public BatteryLevelBackgroundMeasurementCollector(
@NotNull SentryOptions options, @NotNull Context applicationContext) {
this.options = options;
this.applicationContext = applicationContext;
}

@Override
public @NotNull MeasurementBackgroundServiceType getMeasurementType() {
return MeasurementBackgroundServiceType.BATTERY;
}

@Override
public @Nullable Object collect() {
// TODO 0.4 - 20 ms
return getBatteryLevel();
}

private @Nullable Float getBatteryLevel() {
@Nullable Intent batteryIntent = getBatteryIntent();
if (batteryIntent != null) {
return getBatteryLevel(batteryIntent);
}
return null;
}

private @Nullable Intent getBatteryIntent() {
return applicationContext.registerReceiver(
null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
}

private @Nullable Float getBatteryLevel(final @NotNull Intent batteryIntent) {
try {
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);

if (level == -1 || scale == -1) {
return null;
}

float percentMultiplier = 100.0f;

return ((float) level / (float) scale) * percentMultiplier;
} catch (Throwable e) {
options.getLogger().log(SentryLevel.ERROR, "Error getting device battery level.", e);
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.sentry.android.core.internal.measurement.battery;

import io.sentry.ITransaction;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.measurement.BackgroundAwareMeasurementCollector;
import io.sentry.measurement.MeasurementBackgroundService;
import io.sentry.measurement.MeasurementBackgroundServiceType;
import io.sentry.measurement.MeasurementContext;
import io.sentry.protocol.MeasurementValue;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class BatteryLevelMeasurementCollector extends BackgroundAwareMeasurementCollector {

private final @NotNull SentryOptions options;
private List<MeasurementBackgroundServiceType> listenToTypes;

public BatteryLevelMeasurementCollector(
@NotNull SentryOptions options, @NotNull MeasurementBackgroundService backgroundService) {
super(backgroundService);
this.options = options;
this.listenToTypes = Arrays.asList(MeasurementBackgroundServiceType.BATTERY);
}

@Override
protected List<MeasurementBackgroundServiceType> listenToTypes() {
return listenToTypes;
}

@Override
public void onTransactionStartedInternal(@NotNull ITransaction transaction) {
// nothing to do
}

@Override
public @Nullable Map<String, MeasurementValue> onTransactionFinishedInternal(
@NotNull ITransaction transaction, @NotNull MeasurementContext context) {
@NotNull Map<String, MeasurementValue> results = new HashMap<>();

List<Object> batteryLevels =
backgroundService.getFrom(
MeasurementBackgroundServiceType.BATTERY,
startDate,
backgroundService.getPollingInterval());
if (batteryLevels.size() >= 2) {
float batteryLevelAtStart = (float) batteryLevels.get(0);
float batteryLevelAtEnd = (float) batteryLevels.get(batteryLevels.size() - 1);
float batteryLevelDelta = batteryLevelAtEnd - batteryLevelAtStart;
results.put("battery_drain", new MeasurementValue(batteryLevelDelta));
@Nullable Double transactionDuration = context.getDuration();
if (transactionDuration != null) {
@NotNull Double batteryDrainPerSecond = batteryLevelDelta / transactionDuration;
results.put(
"battery_drain_per_second", new MeasurementValue(batteryDrainPerSecond.floatValue()));
}
} else {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Did not get enough battery level background measurement values to calculate something.");
}
return results;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.sentry.android.core.internal.measurement.battery;

import android.content.Context;
import io.sentry.SentryOptions;
import io.sentry.measurement.MeasurementBackgroundCollector;
import io.sentry.measurement.MeasurementBackgroundService;
import io.sentry.measurement.MeasurementCollector;
import io.sentry.measurement.MeasurementCollectorFactory;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class BatteryLevelMeasurementCollectorFactory implements MeasurementCollectorFactory {

private final Context applicationContext;

public BatteryLevelMeasurementCollectorFactory(@NotNull Context applicationContext) {
this.applicationContext = applicationContext;
}

@Override
public @NotNull MeasurementCollector create(
@NotNull SentryOptions options, @NotNull MeasurementBackgroundService backgroundService) {
return new BatteryLevelMeasurementCollector(options, backgroundService);
}

@Override
public @Nullable MeasurementBackgroundCollector createBackgroundCollector(
@NotNull SentryOptions options) {
return new BatteryLevelBackgroundMeasurementCollector(options, applicationContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.sentry.android.core.internal.measurement.cpu;

import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.measurement.MeasurementBackgroundCollector;
import io.sentry.measurement.MeasurementBackgroundServiceType;
import java.io.IOException;
import java.io.RandomAccessFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class CpuBackgroundMeasurementCollector implements MeasurementBackgroundCollector {

private final SentryOptions options;

public CpuBackgroundMeasurementCollector(@NotNull SentryOptions options) {
this.options = options;
}

@Override
public @NotNull MeasurementBackgroundServiceType getMeasurementType() {
return MeasurementBackgroundServiceType.CPU;
}

@Override
public @Nullable Object collect() {
try {
// TODO 1-5 ms
String stat = readProcSelfStat();
return parseClockTicks(stat);
} catch (IOException e) {
options
.getLogger()
.log(SentryLevel.ERROR, "Unable to collect CPU measurement in background", e);
return null;
}
}

private String readProcSelfStat() throws IOException {
try (RandomAccessFile reader = new RandomAccessFile("/proc/self/stat", "r")) {
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
content.append('\n');
}
return content.toString();
}
}

private @Nullable Long parseClockTicks(String statString) {
String[] split = statString.split(" ", -1);
return parseClockTicks(split);
}

private @Nullable Long parseClockTicks(String[] split) {
// TODO make safe
String clockTicksUserModeString = split[13];
String clockTicksKernelModeString = split[14];
String clockTicksUserModeChildrenString = split[15];
String clockTicksKernelModeChildrenString = split[16];
Long clockTicksUserMode = Long.valueOf(clockTicksUserModeString);
Long clockTicksKernelMode = Long.valueOf(clockTicksKernelModeString);
Long clockTicksUserModeChildren = Long.valueOf(clockTicksUserModeChildrenString);
Long clockTicksKernelModeChildren = Long.valueOf(clockTicksKernelModeChildrenString);
Long clockTicks =
clockTicksUserMode
+ clockTicksKernelMode
+ clockTicksUserModeChildren
+ clockTicksKernelModeChildren;
return clockTicks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package io.sentry.android.core.internal.measurement.cpu;

import android.os.Build;
import android.os.Process;
import android.os.SystemClock;
import android.system.Os;
import android.system.OsConstants;
import io.sentry.ITransaction;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.measurement.BackgroundAwareMeasurementCollector;
import io.sentry.measurement.MeasurementBackgroundService;
import io.sentry.measurement.MeasurementBackgroundServiceType;
import io.sentry.measurement.MeasurementContext;
import io.sentry.protocol.MeasurementValue;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class CpuMeasurementCollector extends BackgroundAwareMeasurementCollector {

private final SentryOptions options;
private List<MeasurementBackgroundServiceType> listenToTypes;
private @Nullable Long startRealtimeNanos;
private @Nullable Long startElapsedTimeMs;

public CpuMeasurementCollector(
@NotNull SentryOptions options, @NotNull MeasurementBackgroundService backgroundService) {
super(backgroundService);
this.options = options;
this.listenToTypes = Arrays.asList(MeasurementBackgroundServiceType.CPU);
}

@Override
protected List<MeasurementBackgroundServiceType> listenToTypes() {
return listenToTypes;
}

@Override
protected void onTransactionStartedInternal(@NotNull ITransaction transaction) {
// TODO 8 - 23 μs
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
startRealtimeNanos = SystemClock.elapsedRealtimeNanos();
startElapsedTimeMs = Process.getElapsedCpuTime();
}
}

@Override
protected @Nullable Map<String, MeasurementValue> onTransactionFinishedInternal(
@NotNull ITransaction transaction, @NotNull MeasurementContext context) {
HashMap<String, MeasurementValue> results = new HashMap<>();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
&& startRealtimeNanos != null
&& startElapsedTimeMs != null) {
Long diffRealtimeNanos = SystemClock.elapsedRealtimeNanos() - startRealtimeNanos;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like our best bet as it's very fast and can be done synchronously. Probably also less risk of being taken away than reading /proc/self/stat

NOTE: The difference for Process.getElapsedCpuTime() might be higher than for SystemClock.elapsedRealtimeNanos() if multiple cores are under load.

Long diffElapsedTimeMs = Process.getElapsedCpuTime() - startElapsedTimeMs;
results.put("cpu_realtime_diff_ns", new MeasurementValue(diffRealtimeNanos));
results.put("cpu_elapsed_time_diff_ms", new MeasurementValue(diffElapsedTimeMs));
Double ratio = diffElapsedTimeMs.doubleValue() * 1000000.0 / diffRealtimeNanos.doubleValue();
results.put("cpu_time_ratio", new MeasurementValue(ratio.floatValue()));
}

List<Object> values =
backgroundService.getFrom(
MeasurementBackgroundServiceType.CPU,
startDate,
backgroundService.getPollingInterval());

options.getLogger().log(SentryLevel.INFO, "CPU mc got %d values from bg", values.size());

if (values.size() >= 2) {
long ticksAtStart = (long) values.get(0);
long ticksAtEnd = (long) values.get(values.size() - 1);
long ticksDelta = ticksAtEnd - ticksAtStart;

results.put("cpu_ticks", new MeasurementValue(ticksDelta));

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// TODO 6 - 9 μs
long clockTickHz = Os.sysconf(OsConstants._SC_CLK_TCK);
Double cpuTimeSeconds = Double.valueOf(ticksDelta) / Double.valueOf(clockTickHz);
float cpuTimeMs = Double.valueOf(cpuTimeSeconds * 1000.0).floatValue();
results.put("cpu_time_ms", new MeasurementValue(cpuTimeMs));

@Nullable Double transactionDuration = context.getDuration();
if (transactionDuration != null) {
results.put(
"transaction_duration_ms",
new MeasurementValue(Double.valueOf(transactionDuration * 1000.0).floatValue()));
Double factor = cpuTimeSeconds / transactionDuration;
// TODO name
results.put("cpu_busy_factor", new MeasurementValue(factor.floatValue()));

Double altFactor =
(Double.valueOf(ticksDelta) * 1000000.0) / (transactionDuration * 1000 * 1000000);
results.put("cpu_busy_factor_alt", new MeasurementValue(altFactor.floatValue()));
}
}
} else {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Did not get enough CPU background measurement values to calculate something.");
}

return results;
}
}
Loading