Skip to content

Commit

Permalink
Memoizing matcher implementation for FileOpDependency.
Browse files Browse the repository at this point in the history
FileOpMatchMemoizingLookup matches a cache reader's VersionedChanges against
specified FileOpDependency instances.

Unseals FileDependencies. It's subclasses are never actually used by clients
directly. This allows creation of test fakes.

PiperOrigin-RevId: 719295168
Change-Id: Ic2f55885a375d112c6d63b555f9f38396fec20f7
  • Loading branch information
aoeui authored and copybara-github committed Jan 24, 2025
1 parent eb42f91 commit 81add1c
Show file tree
Hide file tree
Showing 7 changed files with 538 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ java_library(
],
)

java_library(
name = "depot_delta_validator",
srcs = [
"FileOpMatchMemoizingLookup.java",
"FileOpMatchResultTypes.java",
"NoMatch.java",
],
deps = [
":file_dependency_deserializer",
":value_or_future_map",
":versioned_changes",
"//src/main/java/com/google/devtools/build/lib/concurrent",
"//src/main/java/com/google/devtools/build/lib/concurrent:settable_future_keyed_value",
"//third_party:guava",
],
)

java_library(
name = "file_dependency_key_support",
srcs = ["FileDependencyKeySupport.java"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,10 @@
* {@link #getDependencyCount} and {@link #getDependency}. If any matches are encountered, the
* associated value is invalidated.
*/
abstract sealed class FileDependencies
// non-sealed for test fakes
abstract non-sealed class FileDependencies
implements FileSystemDependencies.FileOpDependency,
FileDependencyDeserializer.FileDependenciesOrFuture
permits FileDependencies.SingleResolvedPath,
FileDependencies.SingleResolvedPathAndDependency,
FileDependencies.MultiplePaths {

FileDependencyDeserializer.FileDependenciesOrFuture {
/**
* Finds the earliest version where any contained path matches a change in {@code changes}.
*
Expand Down Expand Up @@ -77,7 +74,7 @@ static Builder builder(String firstResolvedPath) {
return new Builder(firstResolvedPath);
}

static class Builder {
static final class Builder {
private final ArrayList<String> paths = new ArrayList<>();
private final ArrayList<FileDependencies> dependencies = new ArrayList<>();

Expand Down Expand Up @@ -119,7 +116,7 @@ FileDependencies build() {

// The implementations here exist to reduce indirection and memory use.

static final class SingleResolvedPath extends FileDependencies {
private static final class SingleResolvedPath extends FileDependencies {
private final String resolvedPath;

private SingleResolvedPath(String resolvedPath) {
Expand Down Expand Up @@ -157,7 +154,7 @@ public String toString() {
}
}

static final class SingleResolvedPathAndDependency extends FileDependencies {
private static final class SingleResolvedPathAndDependency extends FileDependencies {
private final String resolvedPath;
private final FileDependencies dependency;

Expand Down Expand Up @@ -203,7 +200,7 @@ public String toString() {
}
}

static final class MultiplePaths extends FileDependencies {
private static final class MultiplePaths extends FileDependencies {
private final ImmutableList<String> resolvedPaths;
private final ImmutableList<FileDependencies> dependencies;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright 2025 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.skyframe.serialization.analysis;

import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.lang.Math.min;

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.devtools.build.lib.concurrent.QuiescingFuture;
import com.google.devtools.build.lib.skyframe.serialization.analysis.FileOpMatchResultTypes.FileOpMatchResult;
import com.google.devtools.build.lib.skyframe.serialization.analysis.FileOpMatchResultTypes.FileOpMatchResultOrFuture;
import com.google.devtools.build.lib.skyframe.serialization.analysis.FileOpMatchResultTypes.FutureFileOpMatchResult;
import com.google.devtools.build.lib.skyframe.serialization.analysis.FileSystemDependencies.FileOpDependency;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.ConcurrentMap;

/**
* Matches {@link FileOpDependency} instances representing cached value dependencies against {@link
* #changes}, containing file system content changes.
*
* <p>The {@code validityHorizon} (VH) has subtle semantics, but works correctly, even in the
* presence of multiple overlapping nodes at different versions and VH values. See {@link
* DepotDeltaValidator} and {@link VersionedChanges} for more details.
*/
final class FileOpMatchMemoizingLookup
extends AbstractValueOrFutureMap<
FileOpDependency, FileOpMatchResultOrFuture, FileOpMatchResult, FutureFileOpMatchResult> {
private final VersionedChanges changes;

FileOpMatchMemoizingLookup(
VersionedChanges changes, ConcurrentMap<FileOpDependency, FileOpMatchResultOrFuture> map) {
super(map, FutureFileOpMatchResult::new, FutureFileOpMatchResult.class);
this.changes = changes;
}

VersionedChanges changes() {
return changes;
}

FileOpMatchResultOrFuture getValueOrFuture(FileOpDependency key, int validityHorizon) {
FileOpMatchResultOrFuture result = getOrCreateValueForSubclasses(key);
if (result instanceof FutureFileOpMatchResult future && future.tryTakeOwnership()) {
try {
return populateFutureFileOpMatchResult(validityHorizon, future);
} finally {
future.verifyComplete();
}
}
return result;
}

private FileOpMatchResultOrFuture populateFutureFileOpMatchResult(
int validityHorizon, FutureFileOpMatchResult ownedFuture) {
switch (ownedFuture.key()) {
case FileDependencies file:
return aggregateAnyAdditionalFileDependencies(
file.findEarliestMatch(changes, validityHorizon), file, validityHorizon, ownedFuture);
case ListingDependencies listing:
// Matches the listing (files inside the directory changed).
int version = listing.findEarliestMatch(changes, validityHorizon);
// Then matches the directory itself.
FileDependencies realDirectory = listing.realDirectory();
return aggregateAnyAdditionalFileDependencies(
min(version, realDirectory.findEarliestMatch(changes, validityHorizon)),
realDirectory,
validityHorizon,
ownedFuture);
}
}

private FileOpMatchResultOrFuture aggregateAnyAdditionalFileDependencies(
int baseVersion,
FileDependencies file,
int validityHorizon,
FutureFileOpMatchResult ownedFuture) {
if (file.getDependencyCount() == 0) {
return ownedFuture.completeWith(FileOpMatchResult.create(baseVersion));
}
var aggregator = new AggregatingFutureFileOpMatchResult(baseVersion);
for (int i = 0; i < file.getDependencyCount(); i++) {
aggregator.addDependency(getValueOrFuture(file.getDependency(i), validityHorizon));
}
aggregator.notifyAllDependenciesAdded();
return ownedFuture.completeWith(aggregator);
}

private static final class AggregatingFutureFileOpMatchResult
extends QuiescingFuture<FileOpMatchResult> implements FutureCallback<FileOpMatchResult> {
private volatile FileOpMatchResult result;

private AggregatingFutureFileOpMatchResult(int version) {
this.result = FileOpMatchResult.create(version);
}

private void addDependency(FileOpMatchResultOrFuture resultOrFuture) {
switch (resultOrFuture) {
case FileOpMatchResult match:
updateResult(match);
break;
case FutureFileOpMatchResult future:
increment();
Futures.addCallback(future, (FutureCallback<FileOpMatchResult>) this, directExecutor());
break;
}
}

private void notifyAllDependenciesAdded() {
decrement();
}

private void updateResult(FileOpMatchResult newResult) {
FileOpMatchResult snapshot;
do {
snapshot = result;
} while (newResult.version() < snapshot.version()
&& !RESULT_HANDLE.compareAndSet(this, snapshot, newResult));
}

@Override
protected FileOpMatchResult getValue() {
return result;
}

/**
* Implementation of {@link FutureCallback<FileOpMatchResult>}.
*
* @deprecated only for {@link #addDependency} futures callback processing.
*/
@Deprecated
@Override
public void onSuccess(FileOpMatchResult result) {
updateResult(result);
decrement();
}

/**
* Implementation of {@link FutureCallback<FileOpMatchResult>}.
*
* @deprecated only for {@link #addDependency} futures callback processing.
*/
@Deprecated
@Override
public void onFailure(Throwable t) {
notifyException(t);
}

private static final VarHandle RESULT_HANDLE;

static {
try {
RESULT_HANDLE =
MethodHandles.lookup()
.findVarHandle(
AggregatingFutureFileOpMatchResult.class, "result", FileOpMatchResult.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2025 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.skyframe.serialization.analysis;

import static com.google.devtools.build.lib.skyframe.serialization.analysis.NoMatch.NO_MATCH_RESULT;

import com.google.devtools.build.lib.concurrent.SettableFutureKeyedValue;
import com.google.devtools.build.lib.skyframe.serialization.analysis.FileSystemDependencies.FileOpDependency;
import java.util.function.BiConsumer;

/** Container for {@link DeltaDepotValidator#matches(FileOpDependency, int)} result types. */
final class FileOpMatchResultTypes {

/** {@link DeltaDepotValidator#matches(FileOpDependency, int)} result type. */
sealed interface FileOpMatchResultOrFuture permits FileOpMatchResult, FutureFileOpMatchResult {}

/** An immediate result. */
sealed interface FileOpMatchResult extends FileOpMatchResultOrFuture
permits NoMatch, FileOpMatch {
static FileOpMatchResult create(int version) {
return version == VersionedChanges.NO_MATCH ? NO_MATCH_RESULT : new FileOpMatch(version);
}

int version();
}

/** A result signalling a match. */
record FileOpMatch(int version) implements FileOpMatchResult {}

/** A future result. */
static final class FutureFileOpMatchResult
extends SettableFutureKeyedValue<
FileOpMatchResultTypes.FutureFileOpMatchResult, FileOpDependency, FileOpMatchResult>
implements FileOpMatchResultOrFuture {
FutureFileOpMatchResult(
FileOpDependency key, BiConsumer<FileOpDependency, FileOpMatchResult> consumer) {
super(key, consumer);
}
}

private FileOpMatchResultTypes() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2025 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.skyframe.serialization.analysis;

import com.google.devtools.build.lib.skyframe.serialization.analysis.FileOpMatchResultTypes.FileOpMatchResult;

/** The delta didn't match the set of dependencies, meaning a <b>cache hit</b>. */
enum NoMatch implements FileOpMatchResult {
NO_MATCH_RESULT;

@Override
public final int version() {
return VersionedChanges.NO_MATCH;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,17 @@ java_test(
"//third_party:truth",
],
)

java_test(
name = "FileOpMatchMemoizingLookupTest",
srcs = ["FileOpMatchMemoizingLookupTest.java"],
deps = [
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis:depot_delta_validator",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis:file_dependency_deserializer",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis:versioned_changes",
"//third_party:guava",
"//third_party:junit4",
"//third_party:truth",
"@maven//:com_google_testparameterinjector_test_parameter_injector",
],
)
Loading

0 comments on commit 81add1c

Please sign in to comment.