Skip to content

Fix fireperf RateLimiter replenishment behavior and unit alignment. #2662

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

Merged
merged 5 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -16,6 +16,7 @@

import static com.google.firebase.perf.metrics.resource.ResourceType.NETWORK;
import static com.google.firebase.perf.metrics.resource.ResourceType.TRACE;
import static java.util.concurrent.TimeUnit.SECONDS;

import android.content.Context;
import androidx.annotation.NonNull;
Expand All @@ -25,6 +26,7 @@
import com.google.firebase.perf.metrics.resource.ResourceType;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Constants;
import com.google.firebase.perf.util.Rate;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.util.Utils;
import com.google.firebase.perf.v1.NetworkRequestMetric;
Expand All @@ -34,7 +36,6 @@
import com.google.firebase.perf.v1.TraceMetric;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* Implement the Token Bucket rate limiting algorithm. The token bucket initially holds "capacity"
Expand All @@ -60,10 +61,10 @@ final class RateLimiter {
* Construct a token bucket rate limiter.
*
* @param appContext the application's context.
* @param rate number of token generated per minute.
* @param rate the Rate object representing the number of tokens generated per specified time unit
* @param capacity token bucket capacity
*/
public RateLimiter(@NonNull Context appContext, final double rate, final long capacity) {
public RateLimiter(@NonNull Context appContext, final Rate rate, final long capacity) {
this(rate, capacity, new Clock(), getSamplingBucketId(), ConfigResolver.getInstance());
this.isLogcatEnabled = Utils.isDebugLoggingEnabled(appContext);
}
Expand All @@ -75,7 +76,7 @@ static float getSamplingBucketId() {
}

RateLimiter(
final double rate,
final Rate rate,
final long capacity,
final Clock clock,
float samplingBucketId,
Expand Down Expand Up @@ -211,29 +212,29 @@ boolean getIsDeviceAllowedToSendNetworkEvents() {
static class RateLimiterImpl {

private static final AndroidLogger logger = AndroidLogger.getInstance();
private static final long MICROS_IN_A_SECOND = TimeUnit.SECONDS.toMicros(1);
private static final long MICROS_IN_A_SECOND = SECONDS.toMicros(1);

private final Clock clock;
private final boolean isLogcatEnabled;

// Last time a token is consumed.
private Timer lastTimeTokenConsumed;
private Timer lastTimeTokenReplenished;

// Number of new tokens generated per second.
private double rate;
// The Rate object representing the number of tokens generated per specified time unit
private Rate rate;
// Token bucket capacity, also the initial number of tokens in the bucket.
private long capacity;
// Number of tokens in the bucket.
private long tokenCount;

private double foregroundRate;
private double backgroundRate;
private Rate foregroundRate;
private Rate backgroundRate;

private long foregroundCapacity;
private long backgroundCapacity;

RateLimiterImpl(
final double rate,
final Rate rate,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm noticing further down that there is a separate foreground and background rate, but they seem to be ignoring this initial one and just blindly overwriting it. Do you understand how it works?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the foreground and background rate is set during initilization and read from remote config, but the passed in rate is still in use, until changeRate() is called. changeRate() is called by the TransportManager when an app updates its state (i.e., changes from foreground to background). At least that's my understanding.

Copy link
Contributor

Choose a reason for hiding this comment

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

But shouldn't the passed-in rate be either foreground or background? Why would there be a 3rd rate that is only used to initialize and then overwritten?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps because we don't know whether the app will be in foreground or background when it finishes initializing? I am not sure if there were other reasons that I am not aware of. We could remove the passed in rate and set it to either foreground or background on initialization if that makes more sense (but which one?)

Copy link
Contributor

Choose a reason for hiding this comment

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

@visumickey do you know why this is the way it is and whether it makes sense to keep it or change it?

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems weird. I don't see a need for a third rate. My recommendation would be not to pass the rate as a parameter since we anyways extract the rate from the configResolver.

As a part of the initialization, we can check on the state of the application to get the rate and use that for the token generation. Eg: on iOS, currently we do not pass the rate as a parameter, but use the state of the application to get the rate from config resolver as specified above.

Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds good - I'd recommend leaving it out of this PR though to not continue changing more things in a single PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, I will leave that out of this PR. Although, is there a possibility that the app has not finished initializing yet so it's not in either foreground or background?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that is a possibility. But in those situations, it is fine to default it to the foreground rate.

final long capacity,
final Clock clock,
ConfigResolver configResolver,
Expand All @@ -243,7 +244,7 @@ static class RateLimiterImpl {
this.capacity = capacity;
this.rate = rate;
tokenCount = capacity;
lastTimeTokenConsumed = this.clock.getTime();
lastTimeTokenReplenished = this.clock.getTime();
setRateByReadingRemoteConfigValues(configResolver, type, isLogcatEnabled);
this.isLogcatEnabled = isLogcatEnabled;
}
Expand All @@ -260,11 +261,20 @@ synchronized boolean check(@NonNull PerfMetric metric) {
Timer now = clock.getTime();
long newTokens =
Math.max(
0, (long) (lastTimeTokenConsumed.getDurationMicros(now) * rate / MICROS_IN_A_SECOND));
0,
(long)
(lastTimeTokenReplenished.getDurationMicros(now)
* rate.getTokensPerSeconds()
/ MICROS_IN_A_SECOND));
tokenCount = Math.min(tokenCount + newTokens, capacity);
if (newTokens > 0) {
lastTimeTokenReplenished =
new Timer(
lastTimeTokenReplenished.getMicros()
+ (long) (newTokens * MICROS_IN_A_SECOND / rate.getTokensPerSeconds()));
}
if (tokenCount > 0) {
tokenCount--;
lastTimeTokenConsumed = now;
return true;
}
if (isLogcatEnabled) {
Expand Down Expand Up @@ -297,7 +307,7 @@ private void setRateByReadingRemoteConfigValues(
long fLimitTime = getFlimitSec(configResolver, type);
long fLimitEvents = getFlimitEvents(configResolver, type);

foregroundRate = ((double) fLimitEvents) / fLimitTime;
foregroundRate = new Rate(fLimitEvents, fLimitTime, SECONDS);
foregroundCapacity = fLimitEvents;
if (isLogcatEnabled) {
logger.debug(
Expand All @@ -309,7 +319,7 @@ private void setRateByReadingRemoteConfigValues(
long bLimitTime = getBlimitSec(configResolver, type);
long bLimitEvents = getBlimitEvents(configResolver, type);

backgroundRate = ((double) bLimitEvents) / bLimitTime;
backgroundRate = new Rate(bLimitEvents, bLimitTime, SECONDS);
backgroundCapacity = bLimitEvents;
if (isLogcatEnabled) {
logger.debug(
Expand Down Expand Up @@ -350,7 +360,7 @@ private static long getBlimitEvents(
}

@VisibleForTesting
double getForegroundRate() {
Rate getForegroundRate() {
return foregroundRate;
}

Expand All @@ -360,7 +370,7 @@ long getForegroundCapacity() {
}

@VisibleForTesting
double getBackgroundRate() {
Rate getBackgroundRate() {
return backgroundRate;
}

Expand All @@ -370,12 +380,12 @@ long getBackgroundCapacity() {
}

@VisibleForTesting
double getRate() {
Rate getRate() {
return rate;
}

@VisibleForTesting
void setRate(double newRate) {
void setRate(Rate newRate) {
rate = newRate;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.perf.transport;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;

import android.content.Context;
import android.content.pm.PackageInfo;
Expand All @@ -39,6 +40,7 @@
import com.google.firebase.perf.session.SessionManager;
import com.google.firebase.perf.util.Constants;
import com.google.firebase.perf.util.Constants.CounterNames;
import com.google.firebase.perf.util.Rate;
import com.google.firebase.perf.v1.AndroidApplicationInfo;
import com.google.firebase.perf.v1.ApplicationInfo;
import com.google.firebase.perf.v1.ApplicationProcessState;
Expand Down Expand Up @@ -202,7 +204,9 @@ public void initialize(
private void syncInit() {
appContext = firebaseApp.getApplicationContext();
configResolver = ConfigResolver.getInstance();
rateLimiter = new RateLimiter(appContext, Constants.RATE_PER_MINUTE, Constants.BURST_CAPACITY);
rateLimiter =
new RateLimiter(
appContext, new Rate(Constants.RATE_PER_MINUTE, 1, MINUTES), Constants.BURST_CAPACITY);
appStateMonitor = AppStateMonitor.getInstance();
flgTransport =
new FlgTransport(flgTransportFactoryProvider, configResolver.getAndCacheLogSourceName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.perf.util;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.util.concurrent.TimeUnit;

/** A Rate object representing the number of tokens per total specified time unit. */
public class Rate {

private long numTokensPerTotalTimeUnit;
private long numTimeUnits;
private TimeUnit timeUnit;

/**
* Constructs a Rate object.
*
* <p>For example, a rate of 3 tokens per minute can be represented by new Rate(3, 1, MINUTES) or
* new Rate(3, 60, SECONDS).
*
* @param numTokensPerTotalTimeUnit The number of tokens to be issued within the specified time
* @param numTimeUnits The number of specified time unit
* @param timeUnit The specified time unit
*/
public Rate(long numTokensPerTotalTimeUnit, long numTimeUnits, TimeUnit timeUnit) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious if we can unify the second and third parameter together and be prescriptive about the timeUnit?

Eg. new Rate(3, 60) - Would mean, 3 tokens in 60 seconds.

Any reason to keep the timeUnit as a separate parameter?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is to prevent bugs like the existing one; if you only pass an int or long, it's very easy to get the units wrong (before we were mixing up minutes and seconds). By passing this, it's impossible to get it wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the third parameter is there mainly for readability reasons. Before in this class, there are two conflicting comments about what the rate is: one says it's token per minute and the other one says it's token per second. By explicitly passing in the TimeUnit, we make it very clear that new Rate(3, 60, SECONDS) means 3 tokens per 60 seconds, and not 3 tokens per 60 minutes or vice versa.

Besides, by keeping the third parameter, we can also make testing more readable, by specifying rates of units other than SECONDS. For example, we can specify new Rate(2, 1, MINUTE) for 2 tokens per minute.

Copy link
Contributor

Choose a reason for hiding this comment

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

I definitely see the need for the readability part. My suggestion was to create the readability by calling the time unit to be always in seconds. Eg. (numberOfTokensForTimeLimit, timeLimitInSeconds)

The current is definitely super verbose and helpful. Was just feeling that verbosity can come by qualifying the parameter with better names.

Anyways, not too tied to my thoughts on this. Fine with this change as well.

assert numTimeUnits > 0;
this.numTokensPerTotalTimeUnit = numTokensPerTotalTimeUnit;
this.numTimeUnits = numTimeUnits;
this.timeUnit = timeUnit;
}

/**
* Converts the rate to tokens per second.
*
* @return rate in tokens per second
*/
public double getTokensPerSeconds() {
switch (timeUnit) {
case NANOSECONDS:
return ((double) numTokensPerTotalTimeUnit / numTimeUnits) * SECONDS.toNanos(1);
case MICROSECONDS:
return ((double) numTokensPerTotalTimeUnit / numTimeUnits) * SECONDS.toMicros(1);
case MILLISECONDS:
return ((double) numTokensPerTotalTimeUnit / numTimeUnits) * SECONDS.toMillis(1);
default:
return (double) numTokensPerTotalTimeUnit / timeUnit.toSeconds(numTimeUnits);
}
}
}
Loading