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)).
* Add functionality to transmit Common Media Client Data (CMCD) data using
query parameters ([#553](https://github.com/androidx/media/issues/553)).
* Fix `ConcurrentModificationException` in `ExperimentalBandwidthMeter`
([#612](https://github.com/androidx/media/issues/612)).
* Transformer:
* Changed `frameRate` and `durationUs` parameters of
`SampleConsumer.queueInputBitmap` to `TimestampIterator`.

View file

@ -17,6 +17,7 @@ package androidx.media3.exoplayer.upstream;
import android.content.Context;
import android.os.Handler;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
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 EventDispatcher eventDispatcher;
private final SlidingPercentile slidingPercentile;
private final Clock clock;
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;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long sampleStartTimeMs;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long sampleBytesTransferred;
private @C.NetworkType int networkType;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long totalElapsedTimeMs;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long totalBytesTransferred;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long bitrateEstimate;
@GuardedBy("this") // Used in TransferListener methods that are called on a background thread.
private long lastReportedBitrateEstimate;
private @C.NetworkType int networkType;
private boolean networkTypeOverrideSet;
private @C.NetworkType int networkTypeOverride;
@ -445,6 +460,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
slidingPercentile.reset();
}
@GuardedBy("this")
private void maybeNotifyBandwidthSample(
int elapsedMs, long bytesTransferred, long bitrateEstimate) {
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.os.Handler;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
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 TimeToFirstByteEstimator timeToFirstByteEstimator;
private final BandwidthEstimator bandwidthEstimator;
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 long initialBitrateEstimate;
private boolean networkTypeOverrideSet;
@ -323,7 +328,7 @@ public final class ExperimentalBandwidthMeter implements BandwidthMeter, Transfe
}
@Override
public long getTimeToFirstByteEstimateUs() {
public synchronized long getTimeToFirstByteEstimateUs() {
return timeToFirstByteEstimator.getTimeToFirstByteEstimateUs();
}
@ -333,19 +338,20 @@ public final class ExperimentalBandwidthMeter implements BandwidthMeter, Transfe
}
@Override
public void addEventListener(Handler eventHandler, EventListener eventListener) {
public synchronized void addEventListener(Handler eventHandler, EventListener eventListener) {
checkNotNull(eventHandler);
checkNotNull(eventListener);
bandwidthEstimator.addEventListener(eventHandler, eventListener);
}
@Override
public void removeEventListener(EventListener eventListener) {
public synchronized void removeEventListener(EventListener eventListener) {
bandwidthEstimator.removeEventListener(eventListener);
}
@Override
public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {
public synchronized void onTransferInitializing(
DataSource source, DataSpec dataSpec, boolean isNetwork) {
if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) {
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.
public final class DefaultBandwidthMeterTest {
private static final int SIMULATED_TRANSFER_COUNT = 100;
private static final String FAST_COUNTRY_ISO = "TW";
private static final String SLOW_COUNTRY_ISO = "PG";
@ -668,7 +667,8 @@ public final class DefaultBandwidthMeterTest {
new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setClock(clock)
.build();
long[] bitrateEstimatesWithNewInstance = simulateTransfers(bandwidthMeter, clock);
long[] bitrateEstimatesWithNewInstance =
simulateTransfers(bandwidthMeter, clock, /* simulatedTransferCount= */ 100);
// Create a new instance and seed with some transfers.
setActiveNetworkInfo(networkInfo2g);
@ -676,11 +676,12 @@ public final class DefaultBandwidthMeterTest {
new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setClock(clock)
.build();
simulateTransfers(bandwidthMeter, clock);
simulateTransfers(bandwidthMeter, clock, /* simulatedTransferCount= */ 100);
// Override the network type to ethernet and simulate transfers again.
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
// estimates generated during simulation to be the same.
@ -697,6 +698,36 @@ public final class DefaultBandwidthMeterTest {
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) {
setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
}
@ -725,12 +756,13 @@ public final class DefaultBandwidthMeterTest {
Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso);
}
private static long[] simulateTransfers(DefaultBandwidthMeter bandwidthMeter, FakeClock clock) {
long[] bitrateEstimates = new long[SIMULATED_TRANSFER_COUNT];
private static long[] simulateTransfers(
DefaultBandwidthMeter bandwidthMeter, FakeClock clock, int simulatedTransferCount) {
long[] bitrateEstimates = new long[simulatedTransferCount];
Random random = new Random(/* seed= */ 0);
DataSource dataSource = new FakeDataSource();
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);
clock.advanceTime(random.nextInt(/* bound= */ 5000));
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.
public final class ExperimentalBandwidthMeterTest {
private static final int SIMULATED_TRANSFER_COUNT = 100;
private static final String FAST_COUNTRY_ISO = "TW";
private static final String SLOW_COUNTRY_ISO = "PG";
@ -666,23 +665,79 @@ public final class ExperimentalBandwidthMeterTest {
setActiveNetworkInfo(networkInfoEthernet);
ExperimentalBandwidthMeter bandwidthMeter =
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.
setActiveNetworkInfo(networkInfo2g);
bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
simulateTransfers(bandwidthMeter);
simulateTransfers(bandwidthMeter, /* simulatedTransferCount= */ 100);
// Override the network type to ethernet and simulate transfers again.
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
// estimates generated during simulation to be the same.
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) {
setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
}
@ -711,12 +766,13 @@ public final class ExperimentalBandwidthMeterTest {
Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso);
}
private static long[] simulateTransfers(ExperimentalBandwidthMeter bandwidthMeter) {
long[] bitrateEstimates = new long[SIMULATED_TRANSFER_COUNT];
private static long[] simulateTransfers(
ExperimentalBandwidthMeter bandwidthMeter, int simulatedTransferCount) {
long[] bitrateEstimates = new long[simulatedTransferCount];
Random random = new Random(/* seed= */ 0);
DataSource dataSource = new FakeDataSource();
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);
ShadowSystemClock.advanceBy(Duration.ofMillis(random.nextInt(50)));
bandwidthMeter.onTransferStart(dataSource, dataSpec, /* isNetwork= */ true);