Add missing synchronization in ExperimentalBandwidthMeter

Issue: androidx/media#612
PiperOrigin-RevId: 563690229
This commit is contained in:
tonihei 2023-09-08 02:24:53 -07:00 committed by Copybara-Service
parent f2b1d0cfa3
commit dd2a373636
5 changed files with 134 additions and 22 deletions

View file

@ -14,6 +14,8 @@
([#8699](https://github.com/google/ExoPlayer/issues/8699)). ([#8699](https://github.com/google/ExoPlayer/issues/8699)).
* Add functionality to transmit Common Media Client Data (CMCD) data using * Add functionality to transmit Common Media Client Data (CMCD) data using
query parameters ([#553](https://github.com/androidx/media/issues/553)). query parameters ([#553](https://github.com/androidx/media/issues/553)).
* Fix `ConcurrentModificationException` in `ExperimentalBandwidthMeter`
([#612](https://github.com/androidx/media/issues/612)).
* Transformer: * Transformer:
* Changed `frameRate` and `durationUs` parameters of * Changed `frameRate` and `durationUs` parameters of
`SampleConsumer.queueInputBitmap` to `TimestampIterator`. `SampleConsumer.queueInputBitmap` to `TimestampIterator`.

View file

@ -17,6 +17,7 @@ package androidx.media3.exoplayer.upstream;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
@ -285,20 +286,34 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
private final ImmutableMap<Integer, Long> initialBitrateEstimates; private final ImmutableMap<Integer, Long> initialBitrateEstimates;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final SlidingPercentile slidingPercentile;
private final Clock clock; private final Clock clock;
private final boolean resetOnNetworkTypeChange; private final boolean resetOnNetworkTypeChange;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private final SlidingPercentile slidingPercentile;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private int streamCount; private int streamCount;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long sampleStartTimeMs; private long sampleStartTimeMs;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long sampleBytesTransferred; private long sampleBytesTransferred;
private @C.NetworkType int networkType; @GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long totalElapsedTimeMs; private long totalElapsedTimeMs;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long totalBytesTransferred; private long totalBytesTransferred;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long bitrateEstimate; private long bitrateEstimate;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long lastReportedBitrateEstimate; private long lastReportedBitrateEstimate;
private @C.NetworkType int networkType;
private boolean networkTypeOverrideSet; private boolean networkTypeOverrideSet;
private @C.NetworkType int networkTypeOverride; private @C.NetworkType int networkTypeOverride;
@ -445,6 +460,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
slidingPercentile.reset(); slidingPercentile.reset();
} }
@GuardedBy("this")
private void maybeNotifyBandwidthSample( private void maybeNotifyBandwidthSample(
int elapsedMs, long bytesTransferred, long bitrateEstimate) { int elapsedMs, long bytesTransferred, long bitrateEstimate) {
if (elapsedMs == 0 && bytesTransferred == 0 && bitrateEstimate == lastReportedBitrateEstimate) { if (elapsedMs == 0 && bytesTransferred == 0 && bitrateEstimate == lastReportedBitrateEstimate) {

View file

@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.NetworkTypeObserver; import androidx.media3.common.util.NetworkTypeObserver;
@ -275,10 +276,14 @@ public final class ExperimentalBandwidthMeter implements BandwidthMeter, Transfe
} }
private final ImmutableMap<Integer, Long> initialBitrateEstimates; private final ImmutableMap<Integer, Long> initialBitrateEstimates;
private final TimeToFirstByteEstimator timeToFirstByteEstimator;
private final BandwidthEstimator bandwidthEstimator;
private final boolean resetOnNetworkTypeChange; private final boolean resetOnNetworkTypeChange;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private final TimeToFirstByteEstimator timeToFirstByteEstimator;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private final BandwidthEstimator bandwidthEstimator;
private @C.NetworkType int networkType; private @C.NetworkType int networkType;
private long initialBitrateEstimate; private long initialBitrateEstimate;
private boolean networkTypeOverrideSet; private boolean networkTypeOverrideSet;
@ -323,7 +328,7 @@ public final class ExperimentalBandwidthMeter implements BandwidthMeter, Transfe
} }
@Override @Override
public long getTimeToFirstByteEstimateUs() { public synchronized long getTimeToFirstByteEstimateUs() {
return timeToFirstByteEstimator.getTimeToFirstByteEstimateUs(); return timeToFirstByteEstimator.getTimeToFirstByteEstimateUs();
} }
@ -333,19 +338,20 @@ public final class ExperimentalBandwidthMeter implements BandwidthMeter, Transfe
} }
@Override @Override
public void addEventListener(Handler eventHandler, EventListener eventListener) { public synchronized void addEventListener(Handler eventHandler, EventListener eventListener) {
checkNotNull(eventHandler); checkNotNull(eventHandler);
checkNotNull(eventListener); checkNotNull(eventListener);
bandwidthEstimator.addEventListener(eventHandler, eventListener); bandwidthEstimator.addEventListener(eventHandler, eventListener);
} }
@Override @Override
public void removeEventListener(EventListener eventListener) { public synchronized void removeEventListener(EventListener eventListener) {
bandwidthEstimator.removeEventListener(eventListener); bandwidthEstimator.removeEventListener(eventListener);
} }
@Override @Override
public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) { public synchronized void onTransferInitializing(
DataSource source, DataSpec dataSpec, boolean isNetwork) {
if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) { if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) {
return; return;
} }

View file

@ -52,7 +52,6 @@ import org.robolectric.shadows.ShadowTelephonyManager;
@Config(sdk = Config.ALL_SDKS) // Test all SDKs because network detection logic changed over time. @Config(sdk = Config.ALL_SDKS) // Test all SDKs because network detection logic changed over time.
public final class DefaultBandwidthMeterTest { public final class DefaultBandwidthMeterTest {
private static final int SIMULATED_TRANSFER_COUNT = 100;
private static final String FAST_COUNTRY_ISO = "TW"; private static final String FAST_COUNTRY_ISO = "TW";
private static final String SLOW_COUNTRY_ISO = "PG"; private static final String SLOW_COUNTRY_ISO = "PG";
@ -668,7 +667,8 @@ public final class DefaultBandwidthMeterTest {
new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()) new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setClock(clock) .setClock(clock)
.build(); .build();
long[] bitrateEstimatesWithNewInstance = simulateTransfers(bandwidthMeter, clock); long[] bitrateEstimatesWithNewInstance =
simulateTransfers(bandwidthMeter, clock, /* simulatedTransferCount= */ 100);
// Create a new instance and seed with some transfers. // Create a new instance and seed with some transfers.
setActiveNetworkInfo(networkInfo2g); setActiveNetworkInfo(networkInfo2g);
@ -676,11 +676,12 @@ public final class DefaultBandwidthMeterTest {
new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()) new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setClock(clock) .setClock(clock)
.build(); .build();
simulateTransfers(bandwidthMeter, clock); simulateTransfers(bandwidthMeter, clock, /* simulatedTransferCount= */ 100);
// Override the network type to ethernet and simulate transfers again. // Override the network type to ethernet and simulate transfers again.
bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_ETHERNET); bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_ETHERNET);
long[] bitrateEstimatesAfterReset = simulateTransfers(bandwidthMeter, clock); long[] bitrateEstimatesAfterReset =
simulateTransfers(bandwidthMeter, clock, /* simulatedTransferCount= */ 100);
// If overriding the network type fully reset the bandwidth meter, we expect the bitrate // If overriding the network type fully reset the bandwidth meter, we expect the bitrate
// estimates generated during simulation to be the same. // estimates generated during simulation to be the same.
@ -697,6 +698,36 @@ public final class DefaultBandwidthMeterTest {
assertThat(initialEstimate).isLessThan(50_000_000L); assertThat(initialEstimate).isLessThan(50_000_000L);
} }
@Test
public void getBitrateEstimate_withSimultaneousTransferEvents_receivesUpdatedValues() {
FakeClock clock = new FakeClock(/* initialTimeMs= */ 0);
DefaultBandwidthMeter bandwidthMeter =
new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setClock(clock)
.build();
Thread thread =
new Thread("backgroundTransfers") {
@Override
public void run() {
simulateTransfers(bandwidthMeter, clock, /* simulatedTransferCount= */ 10000);
}
};
thread.start();
long currentBitrateEstimate = bandwidthMeter.getBitrateEstimate();
boolean bitrateEstimateUpdated = false;
while (thread.isAlive()) {
long newBitrateEstimate = bandwidthMeter.getBitrateEstimate();
if (newBitrateEstimate != currentBitrateEstimate) {
currentBitrateEstimate = newBitrateEstimate;
bitrateEstimateUpdated = true;
}
}
assertThat(bitrateEstimateUpdated).isTrue();
}
private void setActiveNetworkInfo(NetworkInfo networkInfo) { private void setActiveNetworkInfo(NetworkInfo networkInfo) {
setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE); setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
} }
@ -725,12 +756,13 @@ public final class DefaultBandwidthMeterTest {
Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso); Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso);
} }
private static long[] simulateTransfers(DefaultBandwidthMeter bandwidthMeter, FakeClock clock) { private static long[] simulateTransfers(
long[] bitrateEstimates = new long[SIMULATED_TRANSFER_COUNT]; DefaultBandwidthMeter bandwidthMeter, FakeClock clock, int simulatedTransferCount) {
long[] bitrateEstimates = new long[simulatedTransferCount];
Random random = new Random(/* seed= */ 0); Random random = new Random(/* seed= */ 0);
DataSource dataSource = new FakeDataSource(); DataSource dataSource = new FakeDataSource();
DataSpec dataSpec = new DataSpec(Uri.parse("https://test.com")); DataSpec dataSpec = new DataSpec(Uri.parse("https://test.com"));
for (int i = 0; i < SIMULATED_TRANSFER_COUNT; i++) { for (int i = 0; i < simulatedTransferCount; i++) {
bandwidthMeter.onTransferStart(dataSource, dataSpec, /* isNetwork= */ true); bandwidthMeter.onTransferStart(dataSource, dataSpec, /* isNetwork= */ true);
clock.advanceTime(random.nextInt(/* bound= */ 5000)); clock.advanceTime(random.nextInt(/* bound= */ 5000));
bandwidthMeter.onBytesTransferred( bandwidthMeter.onBytesTransferred(

View file

@ -53,7 +53,6 @@ import org.robolectric.shadows.ShadowTelephonyManager;
@Config(sdk = Config.ALL_SDKS) // Test all SDKs because network detection logic changed over time. @Config(sdk = Config.ALL_SDKS) // Test all SDKs because network detection logic changed over time.
public final class ExperimentalBandwidthMeterTest { public final class ExperimentalBandwidthMeterTest {
private static final int SIMULATED_TRANSFER_COUNT = 100;
private static final String FAST_COUNTRY_ISO = "TW"; private static final String FAST_COUNTRY_ISO = "TW";
private static final String SLOW_COUNTRY_ISO = "PG"; private static final String SLOW_COUNTRY_ISO = "PG";
@ -666,23 +665,79 @@ public final class ExperimentalBandwidthMeterTest {
setActiveNetworkInfo(networkInfoEthernet); setActiveNetworkInfo(networkInfoEthernet);
ExperimentalBandwidthMeter bandwidthMeter = ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long[] bitrateEstimatesWithNewInstance = simulateTransfers(bandwidthMeter); long[] bitrateEstimatesWithNewInstance =
simulateTransfers(bandwidthMeter, /* simulatedTransferCount= */ 100);
// Create a new instance and seed with some transfers. // Create a new instance and seed with some transfers.
setActiveNetworkInfo(networkInfo2g); setActiveNetworkInfo(networkInfo2g);
bandwidthMeter = bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
simulateTransfers(bandwidthMeter); simulateTransfers(bandwidthMeter, /* simulatedTransferCount= */ 100);
// Override the network type to ethernet and simulate transfers again. // Override the network type to ethernet and simulate transfers again.
bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_ETHERNET); bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_ETHERNET);
long[] bitrateEstimatesAfterReset = simulateTransfers(bandwidthMeter); long[] bitrateEstimatesAfterReset =
simulateTransfers(bandwidthMeter, /* simulatedTransferCount= */ 100);
// If overriding the network type fully reset the bandwidth meter, we expect the bitrate // If overriding the network type fully reset the bandwidth meter, we expect the bitrate
// estimates generated during simulation to be the same. // estimates generated during simulation to be the same.
assertThat(bitrateEstimatesAfterReset).isEqualTo(bitrateEstimatesWithNewInstance); assertThat(bitrateEstimatesAfterReset).isEqualTo(bitrateEstimatesWithNewInstance);
} }
@Test
public void getTimeToFirstByteEstimateUs_withSimultaneousTransferEvents_receivesUpdatedValues() {
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
Thread thread =
new Thread("backgroundTransfers") {
@Override
public void run() {
simulateTransfers(bandwidthMeter, /* simulatedTransferCount= */ 10000);
}
};
thread.start();
long currentTimeToFirstByteEstimateUs = bandwidthMeter.getTimeToFirstByteEstimateUs();
boolean timeToFirstByteEstimateUpdated = false;
while (thread.isAlive()) {
long newTimeToFirstByteEstimateUs = bandwidthMeter.getTimeToFirstByteEstimateUs();
if (newTimeToFirstByteEstimateUs != currentTimeToFirstByteEstimateUs) {
currentTimeToFirstByteEstimateUs = newTimeToFirstByteEstimateUs;
timeToFirstByteEstimateUpdated = true;
}
}
assertThat(timeToFirstByteEstimateUpdated).isTrue();
}
@Test
public void getBitrateEstimate_withSimultaneousTransferEvents_receivesUpdatedValues() {
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
Thread thread =
new Thread("backgroundTransfers") {
@Override
public void run() {
simulateTransfers(bandwidthMeter, /* simulatedTransferCount= */ 10000);
}
};
thread.start();
long currentBitrateEstimate = bandwidthMeter.getBitrateEstimate();
boolean bitrateEstimateUpdated = false;
while (thread.isAlive()) {
long newBitrateEstimate = bandwidthMeter.getBitrateEstimate();
if (newBitrateEstimate != currentBitrateEstimate) {
currentBitrateEstimate = newBitrateEstimate;
bitrateEstimateUpdated = true;
}
}
assertThat(bitrateEstimateUpdated).isTrue();
}
private void setActiveNetworkInfo(NetworkInfo networkInfo) { private void setActiveNetworkInfo(NetworkInfo networkInfo) {
setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE); setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
} }
@ -711,12 +766,13 @@ public final class ExperimentalBandwidthMeterTest {
Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso); Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso);
} }
private static long[] simulateTransfers(ExperimentalBandwidthMeter bandwidthMeter) { private static long[] simulateTransfers(
long[] bitrateEstimates = new long[SIMULATED_TRANSFER_COUNT]; ExperimentalBandwidthMeter bandwidthMeter, int simulatedTransferCount) {
long[] bitrateEstimates = new long[simulatedTransferCount];
Random random = new Random(/* seed= */ 0); Random random = new Random(/* seed= */ 0);
DataSource dataSource = new FakeDataSource(); DataSource dataSource = new FakeDataSource();
DataSpec dataSpec = new DataSpec(Uri.parse("https://test.com")); DataSpec dataSpec = new DataSpec(Uri.parse("https://test.com"));
for (int i = 0; i < SIMULATED_TRANSFER_COUNT; i++) { for (int i = 0; i < simulatedTransferCount; i++) {
bandwidthMeter.onTransferInitializing(dataSource, dataSpec, /* isNetwork= */ true); bandwidthMeter.onTransferInitializing(dataSource, dataSpec, /* isNetwork= */ true);
ShadowSystemClock.advanceBy(Duration.ofMillis(random.nextInt(50))); ShadowSystemClock.advanceBy(Duration.ofMillis(random.nextInt(50)));
bandwidthMeter.onTransferStart(dataSource, dataSpec, /* isNetwork= */ true); bandwidthMeter.onTransferStart(dataSource, dataSpec, /* isNetwork= */ true);