blob: 7c661022d2f4747500bdbdc8b65ea8727390c024 [file] [log] [blame]
// Copyright 2021 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.dynamic;
import static java.lang.Math.min;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
/** A {@link Semaphore} that can be temporarily shrunk. */
@ThreadSafe
public class ShrinkableSemaphore extends Semaphore {
/** How many permits the semaphore has in total. It cannot grow beyond this. */
private final int totalPermits;
/** How many units of load is considered 100% load. */
private final int maxLoad;
/**
* The load factor that determines how we respond to high load. The closer to 0, the more we
* shrink the number of permits under high load.
*/
private final double loadFactor;
/** How many permits the semaphore is currently shrunk by. */
private AtomicInteger permitsPreviouslyShrunkBy = new AtomicInteger(0);
/**
* Create a new ShrinkableSemaphore.
*
* @param permits How many permits to allow maximally.
* @param maxLoad How many units of load is considered 100% load.
* @param loadFactor How much load to allow
*/
public ShrinkableSemaphore(int permits, int maxLoad, double loadFactor) {
super(permits);
this.totalPermits = permits;
this.maxLoad = maxLoad;
this.loadFactor = loadFactor;
}
/**
* Set how much to shrink the number of permits available from this semaphore, relative to the
* total number of permits. The number of total available permits is rounded up and never goes
* below 1. If given a higher {@code shrinkFactor} than previously, the number of permits may
* increase again.
*/
private void setShrinkFactor(double shrinkFactor) {
int shrinkBy = min((int) (totalPermits * shrinkFactor), totalPermits - 1);
int oldShrink = permitsPreviouslyShrunkBy.get();
int reduction = shrinkBy - oldShrink;
if (permitsPreviouslyShrunkBy.compareAndSet(oldShrink, shrinkBy)) {
if (reduction > 0) {
this.reducePermits(reduction);
} else {
this.release(-reduction);
}
}
}
/**
* Calculates the "load factor" for adjusting the number of threads available for local execution.
* When many actions are waiting to be locally executed, we (counter-intuitively) reduce the
* number of threads we allow running locally. Having many actions waiting indicates a large
* build, in which case remote execution/caching will handle the bulk of the actions, and using
* more CPUs for local execution doesn't make much difference. So to make the machine more
* responsive during large builds, we reduce the number of CPUs we allow using locally. This limit
* does not take the number of local-only actions into account, those always need to be run
* anyway.
*
* @param load The current load, relative to the {@code maxLoad} passed as constructor parameter.
* A load of >100% is allowed.
*/
public void updateLoad(int load) {
if (loadFactor > 0 && loadFactor < 1) {
if (load < 0) {
throw new IllegalArgumentException("Cannot have negative load");
}
setShrinkFactor((1 - loadFactor) * (((float) load) / maxLoad));
}
}
}