| // 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.exec; | |
| import static com.google.common.base.Preconditions.checkNotNull; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import com.github.luben.zstd.ZstdOutputStream; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.Iterables; | |
| import com.google.devtools.build.lib.actions.AbstractAction; | |
| import com.google.devtools.build.lib.actions.ActionInput; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.actions.CommandLines.ParamFileActionInput; | |
| import com.google.devtools.build.lib.actions.ExecException; | |
| import com.google.devtools.build.lib.actions.InputMetadataProvider; | |
| import com.google.devtools.build.lib.actions.RunfilesTree; | |
| import com.google.devtools.build.lib.actions.Spawn; | |
| import com.google.devtools.build.lib.actions.SpawnResult; | |
| import com.google.devtools.build.lib.actions.Spawns; | |
| import com.google.devtools.build.lib.actions.cache.VirtualActionInput; | |
| import com.google.devtools.build.lib.analysis.SymlinkEntry; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; | |
| import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor; | |
| import com.google.devtools.build.lib.concurrent.ErrorClassifier; | |
| import com.google.devtools.build.lib.concurrent.NamedForkJoinPool; | |
| import com.google.devtools.build.lib.exec.Protos.Digest; | |
| import com.google.devtools.build.lib.exec.Protos.ExecLogEntry; | |
| import com.google.devtools.build.lib.exec.Protos.Platform; | |
| import com.google.devtools.build.lib.profiler.Profiler; | |
| import com.google.devtools.build.lib.profiler.SilentCloseable; | |
| import com.google.devtools.build.lib.remote.options.RemoteOptions; | |
| import com.google.devtools.build.lib.util.io.AsynchronousMessageOutputStream; | |
| import com.google.devtools.build.lib.util.io.MessageOutputStream; | |
| import com.google.devtools.build.lib.vfs.DigestHashFunction; | |
| import com.google.devtools.build.lib.vfs.Dirent; | |
| import com.google.devtools.build.lib.vfs.FileSystem; | |
| import com.google.devtools.build.lib.vfs.IORuntimeException; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.Symlinks; | |
| import com.google.devtools.build.lib.vfs.XattrProvider; | |
| import com.google.errorprone.annotations.CheckReturnValue; | |
| import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; | |
| import java.io.BufferedOutputStream; | |
| import java.io.IOException; | |
| import java.time.Duration; | |
| import java.util.ArrayList; | |
| import java.util.Collection; | |
| import java.util.Comparator; | |
| import java.util.List; | |
| import java.util.SortedMap; | |
| import java.util.concurrent.ForkJoinPool; | |
| import javax.annotation.Nullable; | |
| import javax.annotation.concurrent.GuardedBy; | |
| /** A {@link SpawnLogContext} implementation that produces a log in compact format. */ | |
| public class CompactSpawnLogContext extends SpawnLogContext { | |
| private static final Comparator<ExecLogEntry.File> EXEC_LOG_ENTRY_FILE_COMPARATOR = | |
| Comparator.comparing(ExecLogEntry.File::getPath); | |
| private static final ForkJoinPool VISITOR_POOL = | |
| NamedForkJoinPool.newNamedPool( | |
| "execlog-directory-visitor", Runtime.getRuntime().availableProcessors()); | |
| /** Visitor for use in {@link #visitDirectory}. */ | |
| protected interface DirectoryChildVisitor { | |
| void visit(Path path) throws IOException; | |
| } | |
| private static class DirectoryVisitor extends AbstractQueueVisitor { | |
| private final Path rootDir; | |
| private final DirectoryChildVisitor childVisitor; | |
| private DirectoryVisitor(Path rootDir, DirectoryChildVisitor childVisitor) { | |
| super( | |
| VISITOR_POOL, | |
| ExecutorOwnership.SHARED, | |
| ExceptionHandlingMode.FAIL_FAST, | |
| ErrorClassifier.DEFAULT); | |
| this.rootDir = checkNotNull(rootDir); | |
| this.childVisitor = checkNotNull(childVisitor); | |
| } | |
| private void run() throws IOException, InterruptedException { | |
| execute(() -> visitSubdirectory(rootDir)); | |
| try { | |
| awaitQuiescence(true); | |
| } catch (IORuntimeException e) { | |
| throw e.getCauseIOException(); | |
| } | |
| } | |
| private void visitSubdirectory(Path dir) { | |
| try { | |
| for (Dirent dirent : dir.readdir(Symlinks.FOLLOW)) { | |
| Path child = dir.getChild(dirent.getName()); | |
| if (dirent.getType() == Dirent.Type.DIRECTORY) { | |
| execute(() -> visitSubdirectory(child)); | |
| continue; | |
| } | |
| childVisitor.visit(child); | |
| } | |
| } catch (IOException e) { | |
| throw new IORuntimeException(e); | |
| } | |
| } | |
| } | |
| /** | |
| * Visits a directory hierarchy in parallel. | |
| * | |
| * <p>Calls {@code childVisitor} for every descendant path of {@code rootDir} that isn't itself a | |
| * directory, following symlinks. The visitor may be concurrently called by multiple threads, and | |
| * must synchronize accesses to shared data. | |
| */ | |
| private void visitDirectory(Path rootDir, DirectoryChildVisitor childVisitor) | |
| throws IOException, InterruptedException { | |
| new DirectoryVisitor(rootDir, childVisitor).run(); | |
| } | |
| private interface ExecLogEntrySupplier { | |
| ExecLogEntry.Builder get() throws IOException, InterruptedException; | |
| } | |
| private final PathFragment execRoot; | |
| private final String workspaceName; | |
| private final boolean siblingRepositoryLayout; | |
| @Nullable private final RemoteOptions remoteOptions; | |
| private final DigestHashFunction digestHashFunction; | |
| private final XattrProvider xattrProvider; | |
| // Maps a key identifying an entry into its ID. | |
| // Each key is either a NestedSet.Node or the String path of a file, directory, symlink or | |
| // runfiles tree. | |
| // Only entries that are likely to be referenced by future entries are stored. | |
| // Use a specialized map for minimal memory footprint. | |
| @GuardedBy("this") | |
| private final Object2IntOpenHashMap<Object> entryMap = new Object2IntOpenHashMap<>(); | |
| // The next available entry ID. | |
| @GuardedBy("this") | |
| int nextEntryId = 1; | |
| // Output stream to write to. | |
| private final MessageOutputStream<ExecLogEntry> outputStream; | |
| public CompactSpawnLogContext( | |
| Path outputPath, | |
| PathFragment execRoot, | |
| String workspaceName, | |
| boolean siblingRepositoryLayout, | |
| @Nullable RemoteOptions remoteOptions, | |
| DigestHashFunction digestHashFunction, | |
| XattrProvider xattrProvider) | |
| throws IOException, InterruptedException { | |
| this.execRoot = execRoot; | |
| this.workspaceName = workspaceName; | |
| this.siblingRepositoryLayout = siblingRepositoryLayout; | |
| this.remoteOptions = remoteOptions; | |
| this.digestHashFunction = digestHashFunction; | |
| this.xattrProvider = xattrProvider; | |
| this.outputStream = getOutputStream(outputPath); | |
| logInvocation(); | |
| } | |
| private static MessageOutputStream<ExecLogEntry> getOutputStream(Path path) throws IOException { | |
| // Use an AsynchronousMessageOutputStream so that compression and I/O occur in a separate | |
| // thread. This ensures concurrent writes don't tear and avoids blocking execution. | |
| return new AsynchronousMessageOutputStream<>( | |
| path.toString(), new ZstdOutputStream(new BufferedOutputStream(path.getOutputStream()))); | |
| } | |
| private void logInvocation() throws IOException, InterruptedException { | |
| logEntryWithoutId( | |
| () -> | |
| ExecLogEntry.newBuilder() | |
| .setInvocation( | |
| ExecLogEntry.Invocation.newBuilder() | |
| .setHashFunctionName(digestHashFunction.toString()) | |
| .setWorkspaceRunfilesDirectory(workspaceName) | |
| .setSiblingRepositoryLayout(siblingRepositoryLayout))); | |
| } | |
| @Override | |
| public boolean shouldPublish() { | |
| // The compact log is small enough to be uploaded to a remote store. | |
| return true; | |
| } | |
| @Override | |
| public void logSpawn( | |
| Spawn spawn, | |
| InputMetadataProvider inputMetadataProvider, | |
| SortedMap<PathFragment, ActionInput> inputMap, | |
| FileSystem fileSystem, | |
| Duration timeout, | |
| SpawnResult result) | |
| throws IOException, InterruptedException, ExecException { | |
| try (SilentCloseable c = Profiler.instance().profile("logSpawn")) { | |
| ExecLogEntry.Spawn.Builder builder = ExecLogEntry.Spawn.newBuilder(); | |
| builder.addAllArgs(spawn.getArguments()); | |
| builder.addAllEnvVars(getEnvironmentVariables(spawn)); | |
| Platform platform = getPlatform(spawn, remoteOptions); | |
| if (platform != null) { | |
| builder.setPlatform(platform); | |
| } | |
| builder.setInputSetId(logInputs(spawn, inputMetadataProvider, fileSystem)); | |
| builder.setToolSetId(logTools(spawn, inputMetadataProvider, fileSystem)); | |
| if (spawn.getTargetLabel() != null) { | |
| builder.setTargetLabel(spawn.getTargetLabel().getCanonicalForm()); | |
| } | |
| builder.setMnemonic(spawn.getMnemonic()); | |
| for (ActionInput output : spawn.getOutputFiles()) { | |
| Path path = fileSystem.getPath(execRoot.getRelative(output.getExecPath())); | |
| if (!output.isDirectory() && !output.isSymlink() && path.isFile()) { | |
| builder.addOutputsBuilder().setOutputId(logFile(output, path, inputMetadataProvider)); | |
| } else if (output.isDirectory() && path.isDirectory()) { | |
| builder | |
| .addOutputsBuilder() | |
| .setOutputId(logDirectory(output, path, inputMetadataProvider)); | |
| } else if (output.isSymlink() && path.isSymbolicLink()) { | |
| builder.addOutputsBuilder().setOutputId(logUnresolvedSymlink(output, path)); | |
| } else { | |
| builder.addOutputsBuilder().setInvalidOutputPath(output.getExecPathString()); | |
| } | |
| } | |
| builder.setExitCode(result.exitCode()); | |
| if (result.status() != SpawnResult.Status.SUCCESS) { | |
| builder.setStatus(result.status().toString()); | |
| } | |
| builder.setRunner(result.getRunnerName()); | |
| builder.setCacheHit(result.isCacheHit()); | |
| builder.setRemotable(Spawns.mayBeExecutedRemotely(spawn)); | |
| builder.setCacheable(Spawns.mayBeCached(spawn)); | |
| builder.setRemoteCacheable(Spawns.mayBeCachedRemotely(spawn)); | |
| if (result.getDigest() != null) { | |
| builder.setDigest(result.getDigest().toBuilder().clearHashFunctionName().build()); | |
| } | |
| builder.setTimeoutMillis(timeout.toMillis()); | |
| builder.setMetrics(getSpawnMetricsProto(result)); | |
| logEntryWithoutId(() -> ExecLogEntry.newBuilder().setSpawn(builder)); | |
| } | |
| } | |
| @Override | |
| public void logSymlinkAction(AbstractAction action) throws IOException, InterruptedException { | |
| try (SilentCloseable c = Profiler.instance().profile("logSymlinkAction")) { | |
| ExecLogEntry.SymlinkAction.Builder builder = ExecLogEntry.SymlinkAction.newBuilder(); | |
| Artifact input = action.getPrimaryInput(); | |
| if (input == null) { | |
| // Symlinks to absolute paths are only used by FDO and not worth logging as they can be | |
| // treated just like source files. | |
| return; | |
| } | |
| builder.setInputPath(input.getExecPathString()); | |
| builder.setOutputPath(action.getPrimaryOutput().getExecPathString()); | |
| Label label = action.getOwner().getLabel(); | |
| if (label != null) { | |
| builder.setTargetLabel(label.getCanonicalForm()); | |
| } | |
| builder.setMnemonic(action.getMnemonic()); | |
| logEntryWithoutId(() -> ExecLogEntry.newBuilder().setSymlinkAction(builder)); | |
| } | |
| } | |
| /** | |
| * Logs the inputs. | |
| * | |
| * @return the entry ID of the {@link ExecLogEntry.InputSet} describing the inputs, or 0 if there | |
| * are no inputs. | |
| */ | |
| private int logInputs( | |
| Spawn spawn, InputMetadataProvider inputMetadataProvider, FileSystem fileSystem) | |
| throws IOException, InterruptedException { | |
| // Add filesets as additional direct members of the top-level nested set of inputs. This | |
| // prevents it from being shared, but experimentally, the top-level input nested set for a spawn | |
| // is almost never a transitive member of other nested sets, and not recording its entry ID | |
| // turns out to be a very significant memory optimization. | |
| ImmutableList.Builder<Integer> additionalDirectoryIds = ImmutableList.builder(); | |
| for (Artifact fileset : spawn.getFilesetMappings().keySet()) { | |
| // The fileset symlink tree is always materialized on disk. | |
| additionalDirectoryIds.add( | |
| logDirectory( | |
| fileset, | |
| fileSystem.getPath(execRoot.getRelative(fileset.getExecPath())), | |
| inputMetadataProvider)); | |
| } | |
| return logInputSet( | |
| spawn.getInputFiles(), | |
| additionalDirectoryIds.build(), | |
| inputMetadataProvider, | |
| fileSystem, | |
| /* shared= */ false, | |
| "TestRunner".equals(spawn.getMnemonic())); | |
| } | |
| /** | |
| * Logs the tool inputs. | |
| * | |
| * @return the entry ID of the {@link ExecLogEntry.InputSet} describing the tool inputs, or 0 if | |
| * there are no tool inputs. | |
| */ | |
| private int logTools( | |
| Spawn spawn, InputMetadataProvider inputMetadataProvider, FileSystem fileSystem) | |
| throws IOException, InterruptedException { | |
| return logInputSet( | |
| spawn.getToolFiles(), | |
| ImmutableList.of(), | |
| inputMetadataProvider, | |
| fileSystem, | |
| /* shared= */ true, | |
| "TestRunner".equals(spawn.getMnemonic())); | |
| } | |
| /** | |
| * Logs a nested set. | |
| * | |
| * @param set the nested set | |
| * @param additionalDirectoryIds the entry IDs of additional {@link ExecLogEntry.Directory} | |
| * entries to include as direct members | |
| * @param shared whether this nested set is likely to be a transitive member of other sets | |
| * @param isTestRunnerSpawn whether this nested set is logged for a test runner spawn | |
| * @return the entry ID of the {@link ExecLogEntry.InputSet} describing the nested set, or 0 if | |
| * the nested set is empty. | |
| */ | |
| private int logInputSet( | |
| NestedSet<? extends ActionInput> set, | |
| Collection<Integer> additionalDirectoryIds, | |
| InputMetadataProvider inputMetadataProvider, | |
| FileSystem fileSystem, | |
| boolean shared, | |
| boolean isTestRunnerSpawn) | |
| throws IOException, InterruptedException { | |
| if (set.isEmpty() && additionalDirectoryIds.isEmpty()) { | |
| return 0; | |
| } | |
| return logEntry( | |
| shared ? set.toNode() : null, | |
| () -> { | |
| ExecLogEntry.InputSet.Builder builder = | |
| ExecLogEntry.InputSet.newBuilder().addAllInputIds(additionalDirectoryIds); | |
| for (NestedSet<? extends ActionInput> transitive : set.getNonLeaves()) { | |
| checkState(!transitive.isEmpty()); | |
| builder.addTransitiveSetIds( | |
| logInputSet( | |
| transitive, | |
| /* additionalDirectoryIds= */ ImmutableList.of(), | |
| inputMetadataProvider, | |
| fileSystem, | |
| /* shared= */ true, | |
| isTestRunnerSpawn)); | |
| } | |
| for (ActionInput input : set.getLeaves()) { | |
| if (input instanceof Artifact artifact && artifact.isMiddlemanArtifact()) { | |
| RunfilesTree runfilesTree = | |
| inputMetadataProvider.getRunfilesMetadata(input).getRunfilesTree(); | |
| builder.addInputIds( | |
| logRunfilesTree( | |
| runfilesTree, | |
| inputMetadataProvider, | |
| fileSystem, | |
| // Runfiles of non-test spawns are tool inputs and thus potentially reused | |
| // between spawns. Runfiles of test spawns are reused if the test is attempted | |
| // multiple times in the same build; in this case, the runfiles tree caches | |
| // its mapping. | |
| !isTestRunnerSpawn || runfilesTree.isMappingCached())); | |
| continue; | |
| } | |
| // Filesets are logged separately. | |
| if (input instanceof Artifact artifact && artifact.isFileset()) { | |
| continue; | |
| } | |
| builder.addInputIds(logInput(input, inputMetadataProvider, fileSystem)); | |
| } | |
| return ExecLogEntry.newBuilder().setInputSet(builder); | |
| }); | |
| } | |
| /** | |
| * Logs a nested set of {@link SymlinkEntry}. | |
| * | |
| * @return the entry ID of the {@link ExecLogEntry.SymlinkEntrySet} describing the nested set, or | |
| * 0 if the nested set is empty. | |
| */ | |
| private int logSymlinkEntries( | |
| NestedSet<SymlinkEntry> symlinks, | |
| InputMetadataProvider inputMetadataProvider, | |
| FileSystem fileSystem) | |
| throws IOException, InterruptedException { | |
| if (symlinks.isEmpty()) { | |
| return 0; | |
| } | |
| return logEntry( | |
| symlinks.toNode(), | |
| () -> { | |
| ExecLogEntry.SymlinkEntrySet.Builder builder = ExecLogEntry.SymlinkEntrySet.newBuilder(); | |
| for (NestedSet<SymlinkEntry> transitive : symlinks.getNonLeaves()) { | |
| checkState(!transitive.isEmpty()); | |
| builder.addTransitiveSetIds( | |
| logSymlinkEntries(transitive, inputMetadataProvider, fileSystem)); | |
| } | |
| for (SymlinkEntry input : symlinks.getLeaves()) { | |
| builder.putDirectEntries( | |
| input.getPathString(), | |
| logInput(input.getArtifact(), inputMetadataProvider, fileSystem)); | |
| } | |
| return ExecLogEntry.newBuilder().setSymlinkEntrySet(builder); | |
| }); | |
| } | |
| /** | |
| * Logs a single input that is either a file, a directory or a symlink. | |
| * | |
| * @return the entry ID of the {@link ExecLogEntry} describing the input. | |
| */ | |
| private int logInput( | |
| ActionInput input, InputMetadataProvider inputMetadataProvider, FileSystem fileSystem) | |
| throws IOException, InterruptedException { | |
| Path path = fileSystem.getPath(execRoot.getRelative(input.getExecPath())); | |
| if (isInputDirectory(input, inputMetadataProvider)) { | |
| return logDirectory(input, path, inputMetadataProvider); | |
| } else if (input.isSymlink()) { | |
| return logUnresolvedSymlink(input, path); | |
| } else { | |
| return logFile(input, path, inputMetadataProvider); | |
| } | |
| } | |
| /** | |
| * Logs a file. | |
| * | |
| * @param input the input representing the file. | |
| * @param path the path to the file, which must have already been verified to be of the correct | |
| * type. | |
| * @return the entry ID of the {@link ExecLogEntry.File} describing the file. | |
| */ | |
| private int logFile(ActionInput input, Path path, InputMetadataProvider inputMetadataProvider) | |
| throws IOException, InterruptedException { | |
| checkState(!(input instanceof VirtualActionInput.EmptyActionInput)); | |
| return logEntry( | |
| // A ParamFileActionInput is never shared between spawns. | |
| input instanceof ParamFileActionInput ? null : input.getExecPathString(), | |
| () -> { | |
| ExecLogEntry.File.Builder builder = ExecLogEntry.File.newBuilder(); | |
| builder.setPath(input.getExecPathString()); | |
| Digest digest = | |
| computeDigest( | |
| input, | |
| path, | |
| inputMetadataProvider, | |
| xattrProvider, | |
| digestHashFunction, | |
| /* includeHashFunctionName= */ false); | |
| builder.setDigest(digest); | |
| return ExecLogEntry.newBuilder().setFile(builder); | |
| }); | |
| } | |
| /** | |
| * Logs a directory. | |
| * | |
| * <p>This may be either a source directory, a fileset or an output directory. For runfiles, | |
| * {@link #logRunfilesTree} must be used instead. | |
| * | |
| * @param input the input representing the directory. | |
| * @param root the path to the directory, which must have already been verified to be of the | |
| * correct type. | |
| * @return the entry ID of the {@link ExecLogEntry.Directory} describing the directory. | |
| */ | |
| private int logDirectory( | |
| ActionInput input, Path root, InputMetadataProvider inputMetadataProvider) | |
| throws IOException, InterruptedException { | |
| return logEntry( | |
| input.getExecPathString(), | |
| () -> | |
| ExecLogEntry.newBuilder() | |
| .setDirectory( | |
| ExecLogEntry.Directory.newBuilder() | |
| .setPath(input.getExecPathString()) | |
| .addAllFiles(expandDirectory(root, inputMetadataProvider)))); | |
| } | |
| /** | |
| * Logs a runfiles directory by storing the information in its {@link RunfilesTree}. | |
| * | |
| * <p>Since runfiles trees can be very large and, for tests, are only used by a single spawn, we | |
| * store them in the log as a special entry that references the nested set of artifacts instead of | |
| * as a flat directory. | |
| * | |
| * @param shared whether this runfiles tree is likely to be contained in more than one Spawn's | |
| * inputs | |
| * @return the entry ID of the {@link ExecLogEntry.RunfilesTree} describing the directory. | |
| */ | |
| private int logRunfilesTree( | |
| RunfilesTree runfilesTree, | |
| InputMetadataProvider inputMetadataProvider, | |
| FileSystem fileSystem, | |
| boolean shared) | |
| throws IOException, InterruptedException { | |
| return logEntry( | |
| shared ? runfilesTree.getExecPath().getPathString() : null, | |
| () -> { | |
| Preconditions.checkState(workspaceName.equals(runfilesTree.getWorkspaceName())); | |
| ExecLogEntry.RunfilesTree.Builder builder = | |
| ExecLogEntry.RunfilesTree.newBuilder() | |
| .setPath(runfilesTree.getExecPath().getPathString()) | |
| .setLegacyExternalRunfiles(runfilesTree.isLegacyExternalRunfiles()); | |
| builder.setInputSetId( | |
| logInputSet( | |
| runfilesTree.getArtifactsAtCanonicalLocationsForLogging(), | |
| /* additionalDirectoryIds= */ ImmutableList.of(), | |
| inputMetadataProvider, | |
| fileSystem, | |
| // The runfiles tree itself is shared, but the nested set is unique to the tree as | |
| // it contains the executable. | |
| /* shared= */ false, | |
| // This value only matters for nested sets that may contain runfiles trees, but | |
| // these are never nested. | |
| /* isTestRunnerSpawn= */ false)); | |
| builder.setSymlinksId( | |
| logSymlinkEntries( | |
| runfilesTree.getSymlinksForLogging(), inputMetadataProvider, fileSystem)); | |
| builder.setRootSymlinksId( | |
| logSymlinkEntries( | |
| runfilesTree.getRootSymlinksForLogging(), inputMetadataProvider, fileSystem)); | |
| builder.addAllEmptyFiles( | |
| Iterables.transform( | |
| runfilesTree.getEmptyFilenamesForLogging(), PathFragment::getPathString)); | |
| Artifact repoMappingManifest = runfilesTree.getRepoMappingManifestForLogging(); | |
| if (repoMappingManifest != null) { | |
| builder.setRepoMappingManifest( | |
| ExecLogEntry.File.newBuilder() | |
| .setDigest( | |
| computeDigest( | |
| repoMappingManifest, | |
| repoMappingManifest.getPath(), | |
| inputMetadataProvider, | |
| xattrProvider, | |
| digestHashFunction, | |
| /* includeHashFunctionName= */ false))); | |
| } | |
| return ExecLogEntry.newBuilder().setRunfilesTree(builder); | |
| }); | |
| } | |
| /** | |
| * Expands a directory. | |
| * | |
| * @param root the path to the directory | |
| * @return the list of files transitively contained in the directory | |
| */ | |
| private List<ExecLogEntry.File> expandDirectory( | |
| Path root, InputMetadataProvider inputMetadataProvider) | |
| throws IOException, InterruptedException { | |
| ArrayList<ExecLogEntry.File> files = new ArrayList<>(); | |
| visitDirectory( | |
| root, | |
| (child) -> { | |
| Digest digest = | |
| computeDigest( | |
| /* input= */ null, | |
| child, | |
| inputMetadataProvider, | |
| xattrProvider, | |
| digestHashFunction, | |
| /* includeHashFunctionName= */ false); | |
| ExecLogEntry.File file = | |
| ExecLogEntry.File.newBuilder() | |
| .setPath(child.relativeTo(root).getPathString()) | |
| .setDigest(digest) | |
| .build(); | |
| synchronized (files) { | |
| files.add(file); | |
| } | |
| }); | |
| files.sort(EXEC_LOG_ENTRY_FILE_COMPARATOR); | |
| return files; | |
| } | |
| /** | |
| * Logs an unresolved symlink. | |
| * | |
| * @param input the input representing the unresolved symlink. | |
| * @param path the path to the unresolved symlink, which must have already been verified to be of | |
| * the correct type. | |
| * @return the entry ID of the {@link ExecLogEntry.UnresolvedSymlink} describing the unresolved | |
| * symlink. | |
| */ | |
| private int logUnresolvedSymlink(ActionInput input, Path path) | |
| throws IOException, InterruptedException { | |
| return logEntry( | |
| input.getExecPathString(), | |
| () -> | |
| ExecLogEntry.newBuilder() | |
| .setUnresolvedSymlink( | |
| ExecLogEntry.UnresolvedSymlink.newBuilder() | |
| .setPath(input.getExecPathString()) | |
| .setTargetPath(path.readSymbolicLink().getPathString()))); | |
| } | |
| /** | |
| * Ensures an entry is written to the log without an ID. | |
| * | |
| * @param supplier called to compute the entry; may cause other entries to be logged | |
| */ | |
| private void logEntryWithoutId(ExecLogEntrySupplier supplier) | |
| throws IOException, InterruptedException { | |
| try (SilentCloseable c = Profiler.instance().profile("logEntryWithoutId")) { | |
| logEntryWithoutIdSynchronized(supplier); | |
| } | |
| } | |
| private synchronized void logEntryWithoutIdSynchronized(ExecLogEntrySupplier supplier) | |
| throws IOException, InterruptedException { | |
| try (SilentCloseable c = Profiler.instance().profile("logEntryWithoutId/synchronized")) { | |
| outputStream.write(supplier.get().build()); | |
| } | |
| } | |
| /** | |
| * Ensures an entry is written to the log and returns its assigned ID. | |
| * | |
| * <p>If an entry with the same non-null key was previously added to the log, its recorded ID is | |
| * returned. Otherwise, the entry is computed, assigned an ID, and written to the log. | |
| * | |
| * @param key the key, or null if the ID shouldn't be recorded | |
| * @param supplier called to compute the entry; may cause other entries to be logged | |
| * @return the entry ID | |
| */ | |
| @CheckReturnValue | |
| private int logEntry(@Nullable Object key, ExecLogEntrySupplier supplier) | |
| throws IOException, InterruptedException { | |
| try (SilentCloseable c = Profiler.instance().profile("logEntry")) { | |
| return logEntrySynchronized(key, supplier); | |
| } | |
| } | |
| private synchronized int logEntrySynchronized(@Nullable Object key, ExecLogEntrySupplier supplier) | |
| throws IOException, InterruptedException { | |
| try (SilentCloseable c = Profiler.instance().profile("logEntry/synchronized")) { | |
| if (key == null) { | |
| // No need to check for a previously added entry. | |
| ExecLogEntry.Builder entry = supplier.get(); | |
| int id = nextEntryId++; | |
| outputStream.write(entry.setId(id).build()); | |
| return id; | |
| } | |
| checkState(key instanceof NestedSet.Node || key instanceof String); | |
| // Check for a previously added entry. | |
| int id = entryMap.getOrDefault(key, 0); | |
| if (id != 0) { | |
| return id; | |
| } | |
| // Compute a fresh entry and log it. | |
| // The following order of operations is crucial to ensure that this entry is preceded by any | |
| // entries it references, which in turn ensures the log can be parsed in a single pass. | |
| ExecLogEntry.Builder entry = supplier.get(); | |
| id = nextEntryId++; | |
| entryMap.put(key, id); | |
| outputStream.write(entry.setId(id).build()); | |
| return id; | |
| } | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| outputStream.close(); | |
| } | |
| } |