Skip to content

Commit

Permalink
DefaultSyscallCache can export the latest LoadingCache objects fo…
Browse files Browse the repository at this point in the history
…r instrumentation.

The `LatestObjectMetricExporter` supports a pattern for lazily registering
metrics that instrument objects that are effectively singletons: objects for
which there are never more than one instance, but which are destroyed and
recreated over the course of the server's lifetime.

PiperOrigin-RevId: 594053031
Change-Id: Ic616af55b20c3b789ae2037ef4e829954194fae1
  • Loading branch information
michaeledgar authored and copybara-github committed Dec 27, 2023
1 parent 673d4d3 commit 4c4a1a7
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

import static com.google.common.base.MoreObjects.firstNonNull;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.devtools.build.lib.util.LatestObjectMetricExporter;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.FileStatus;
Expand All @@ -39,9 +41,14 @@
* existing data (like the directory listing of a parent) without filesystem access.
*/
public final class DefaultSyscallCache implements SyscallCache {

private final Supplier<LoadingCache<Pair<Path, Symlinks>, Object>> statCacheSupplier;
private final Supplier<LoadingCache<Path, Object>> readdirCacheSupplier;

@Nullable private final LatestObjectMetricExporter<Cache<?, ?>> statCacheMetricExporter;

@Nullable private final LatestObjectMetricExporter<Cache<?, ?>> readdirCacheMetricExporter;

private LoadingCache<Pair<Path, Symlinks>, Object> statCache;

/* Caches the result of readdir(<path>, Symlinks.NOFOLLOW) calls. */
Expand All @@ -51,9 +58,13 @@ public final class DefaultSyscallCache implements SyscallCache {

private DefaultSyscallCache(
Supplier<LoadingCache<Pair<Path, Symlinks>, Object>> statCacheSupplier,
Supplier<LoadingCache<Path, Object>> readdirCacheSupplier) {
Supplier<LoadingCache<Path, Object>> readdirCacheSupplier,
@Nullable LatestObjectMetricExporter<Cache<?, ?>> statCacheMetricExporter,
@Nullable LatestObjectMetricExporter<Cache<?, ?>> readdirCacheMetricExporter) {
this.statCacheSupplier = statCacheSupplier;
this.readdirCacheSupplier = readdirCacheSupplier;
this.statCacheMetricExporter = statCacheMetricExporter;
this.readdirCacheMetricExporter = readdirCacheMetricExporter;
clear();
}

Expand All @@ -67,6 +78,8 @@ public static final class Builder {
private int maxStats = UNSET;
private int maxReaddirs = UNSET;
private int initialCapacity = UNSET;
private LatestObjectMetricExporter<Cache<?, ?>> statCacheMetricExporter = null;
private LatestObjectMetricExporter<Cache<?, ?>> readdirCacheMetricExporter = null;

private Builder() {}

Expand All @@ -91,22 +104,56 @@ public Builder setInitialCapacity(int initialCapacity) {
return this;
}

/**
* Sets the metric exporter for the 'stat' cache.
*
* <p>No metrics are exported by default. If a non-null value is set, the 'stat' cache will
* record access statistics with some overhead.
*/
@CanIgnoreReturnValue
public Builder setStatCacheMetricExporter(
LatestObjectMetricExporter<Cache<?, ?>> statCacheMetricExporter) {
this.statCacheMetricExporter = statCacheMetricExporter;
return this;
}

/**
* Sets the metric exporter for the 'readdir' cache.
*
* <p>No metrics are exported by default. If a non-null value is set, the 'readdir' cache will
* record access statistics with some overhead.
*/
@CanIgnoreReturnValue
public Builder setReaddirCacheMetricExporter(
LatestObjectMetricExporter<Cache<?, ?>> readdirCacheMetricExporter) {
this.readdirCacheMetricExporter = readdirCacheMetricExporter;
return this;
}

public DefaultSyscallCache build() {
Caffeine<Object, Object> statCacheBuilder = Caffeine.newBuilder();
if (maxStats != UNSET) {
statCacheBuilder.maximumSize(maxStats);
}
if (statCacheMetricExporter != null) {
statCacheBuilder.recordStats();
}
Caffeine<Object, Object> readdirCacheBuilder = Caffeine.newBuilder();
if (maxReaddirs != UNSET) {
readdirCacheBuilder.maximumSize(maxReaddirs);
}
if (readdirCacheMetricExporter != null) {
readdirCacheBuilder.recordStats();
}
if (initialCapacity != UNSET) {
statCacheBuilder.initialCapacity(initialCapacity);
readdirCacheBuilder.initialCapacity(initialCapacity);
}
return new DefaultSyscallCache(
() -> statCacheBuilder.build(DefaultSyscallCache::statImpl),
() -> readdirCacheBuilder.build(DefaultSyscallCache::readdirImpl));
() -> readdirCacheBuilder.build(DefaultSyscallCache::readdirImpl),
statCacheMetricExporter,
readdirCacheMetricExporter);
}
}

Expand Down Expand Up @@ -206,6 +253,12 @@ public void clear() {
// Drop not just the memory of the FileStatus objects but the maps themselves.
statCache = statCacheSupplier.get();
readdirCache = readdirCacheSupplier.get();
if (statCacheMetricExporter != null) {
statCacheMetricExporter.setLatestInstance(statCache);
}
if (readdirCacheMetricExporter != null) {
readdirCacheMetricExporter.setLatestInstance(readdirCache);
}
}

// This is used because the cache implementations don't allow null.
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/google/devtools/build/lib/util/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ java_library(
"FileHandlerQuerier.java",
"Fingerprint.java",
"JavaSleeper.java",
"LatestObjectMetricExporter.java",
"LogHandlerQuerier.java",
"LoggingUtil.java",
"LongArrayList.java",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2023 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.util;

import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.function.Supplier;
import javax.annotation.concurrent.GuardedBy;

/**
* Exporter for a callback metric instrumenting a singleton that may not be created, and when
* created, may be discarded and re-created.
*
* <p>Lazily registers a callback-metric with a thread-safe {@link Supplier} of the latest value of
* that reference. Lazily registering the callback metric reduces metric pollution when the
* instrumented codepaths are never executed.
*
* <p>Weak/soft references must be used to allow the instrumented object to be GCed; callbacks must
* expect {@code null} values. Note that in some instrumentation libraries it is impossible to stop
* exporting a given metric.
*
* <p>Simple usage example based on the open-source {@code io.opentelemetry.api.metrics} API:
*
* <pre>
* class FooManager {
* private static final ObservableLongMeasurement fooMetric =
* MyMeterProvider.get().gaugeBuilder("foo").ofLongs().buildObserver();
* private static final ObservableLongMeasurement barMetric =
* MyMeterProvider.get().gaugeBuilder("bar").ofLongs().buildObserver();
* static void updateMetric(FooManager manager) {
* fooMetric.record(manager == null ? 0L : manager.getFoo());
* barMetric.record(manager == null ? 0L : manager.getBar());
* }
* private static final LatestObjectMetricExporter&lt;FooManager&gt; FOO_MANAGER_EXPORTER =
* new LatestObjectMetricExporter&lt;&gt;(
* LatestObjectMetricExporter.Strength.WEAK,
* (supplier) -> MyMeterProvider.get().batchCallback(
* () -> updateMetric(supplier.get()),
* fooMetric,
* barMetric));
*
* // Need some state fields to export.
* \@GuardedBy("this") private Foo foo;
* \@GuardedBy("this") private Bar bar;
* FooManager(Foo foo, Bar bar) {
* // Initialize state fields before exporting the FooManager.
* this.bar = bar;
* this.bar = bar;
* FOO_MANAGER_EXPORTER.setLatestInstance(this);
* }
* // Measurements must be thread-safe.
* synchronized long getFoo() {
* return bar.getFooSize();
* }
* synchronized long getBar() {
* return bar.getBarSize();
* }
* }
* </pre>
*
* @param <T> Type of the <em>latest object</em> being tracked.
*/
@ThreadSafe
public final class LatestObjectMetricExporter<T> {

/**
* Metric-specific callback, run once the first time a {@link LatestObjectMetricExporter} is used.
*/
public interface CallbackRegistration<T> {
/**
* One-time setup method expected to register callback metrics with the instrumentation
* library's metric registry.
*
* <p>Callbacks are expected to use the given {@link Supplier} to get the latest instance (or
* {@code null} if the latest instance has been GCed).
*/
void register(Supplier<T> refSupplier);
}

/** Kind of reference held by the exporter. */
public enum Strength {
/** Creates {@link WeakReference} instances. */
WEAK,
/** Creates {@link SoftReference} instances. */
SOFT;

/** Create a new Reference for the given value, which may be {@code null}. */
<T> Reference<T> makeRef(T value) {
switch (this) {
case WEAK:
return new WeakReference<>(value);
case SOFT:
return new SoftReference<>(value);
}
throw new IllegalStateException("unexpected reference strength: " + name());
}
}

/** The reference strength used for the latest object. */
private final Strength strength;

/**
* Registration callback that will be invoked at most once, the first time {@link
* LatestObjectMetricExporter#setLatestInstance(T)} is called.
*/
private final CallbackRegistration<T> registration;

/** Flag that is set after the callback registration method has been called. */
@GuardedBy("this")
private boolean callbackRegistered = false;

/**
* Reference to the last {@link T object} created by Blaze; as a weak/soft reference, will be null
* if it has been GCed.
*
* <p>We don't use an {@link java.util.concurrent.atomic.AtomicReference} because we don't know
* (other than a finalizer) when to clear the reference to avoid leaking memory.
*/
@GuardedBy("this")
private Reference<T> reference;

/** Create a singleton exporter with the given reference strength and registration callback. */
public LatestObjectMetricExporter(Strength strength, CallbackRegistration<T> registration) {
this.strength = strength;
this.registration = registration;
reference = strength.makeRef(null);
}

/**
* Sets the latest instance of the instrumented singleton (through the Supplier passed to the
* exporter's {@link CallbackRegistration}).
*
* <p>If this is the first time the method has been called, {@code registration#register()} will
* be called after changing {@link #reference}.
*/
public synchronized void setLatestInstance(T value) {
reference = strength.makeRef(value);
if (!callbackRegistered) {
registration.register(
() -> {
synchronized (LatestObjectMetricExporter.this) {
return reference.get();
}
});
callbackRegistered = true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2023 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.util;

import static com.google.common.truth.Truth.assertThat;

import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit test for {@link LatestObjectMetricExporter}. */
@RunWith(JUnit4.class)
public class LatestObjectMetricExporterTest {

@Test
public void weakReferencesAreGarbageCollected() {
// Create an exporter with the given strength and whose registration stores the Supplier<Object>
// in an AtomicReference.
LatestObjectMetricExporter.Strength strength = LatestObjectMetricExporter.Strength.WEAK;
AtomicReference<Supplier<Object>> registeredSupplierRef = new AtomicReference<>(null);
LatestObjectMetricExporter.CallbackRegistration<Object> registration =
registeredSupplierRef::set;
LatestObjectMetricExporter<Object> exporter =
new LatestObjectMetricExporter<>(strength, registration);
assertThat(registeredSupplierRef.get()).isNull();
// Set up three Objects to serve as dummy "latest objects" to pass through the exporter.
Object first = new Object();
Object second = new Object();
Object third = new Object();
// Set the first value, at which point the registration will run and we will get the Supplier
// that tells us the currently exported object.
exporter.setLatestInstance(first);
Supplier<Object> latestObjectSupplier = registeredSupplierRef.get();
assertThat(latestObjectSupplier).isNotNull();
assertThat(latestObjectSupplier.get()).isSameInstanceAs(first);

// Remove only reference to the latest object and run the GC. The supplier should start
// producing null, not `first`.
first = null;
Runtime runtime = Runtime.getRuntime();
runtime.gc();
assertThat(latestObjectSupplier.get()).isNull();

// Remove only reference to the latest object but don't run the GC. The supplier should still
// return `second` until we change the latest inatance to `third`, at which point GC has no
// observable effect..
exporter.setLatestInstance(second);
assertThat(latestObjectSupplier.get()).isSameInstanceAs(second);
second = null;
exporter.setLatestInstance(third);
assertThat(latestObjectSupplier.get()).isSameInstanceAs(third);
runtime.gc();
assertThat(latestObjectSupplier.get()).isSameInstanceAs(third);

// Repeat the first assertion: removing the reference and GCing will cause the Supplier
// to produce null, not `third`.
third = null;
runtime.gc();
assertThat(latestObjectSupplier.get()).isNull();
}
}

0 comments on commit 4c4a1a7

Please sign in to comment.