| // Copyright 2014 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.analysis.config; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.Ordering; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; |
| import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext; |
| import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec; |
| import com.google.devtools.build.lib.skyframe.serialization.SerializationContext; |
| import com.google.devtools.build.lib.skyframe.serialization.SerializationException; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; |
| import com.google.devtools.build.lib.skyframe.trimming.ConfigurationComparer; |
| import com.google.devtools.build.lib.util.Fingerprint; |
| import com.google.devtools.build.lib.util.OrderedSetMultimap; |
| import com.google.devtools.common.options.InvocationPolicyEnforcer; |
| import com.google.devtools.common.options.OptionDefinition; |
| import com.google.devtools.common.options.OptionsBase; |
| import com.google.devtools.common.options.OptionsParser; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import com.google.devtools.common.options.OptionsProvider; |
| import com.google.protobuf.ByteString; |
| import com.google.protobuf.CodedInputStream; |
| import com.google.protobuf.CodedOutputStream; |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.logging.Logger; |
| import java.util.stream.Collectors; |
| import javax.annotation.Nullable; |
| |
| /** Stores the command-line options from a set of configuration fragments. */ |
| // TODO(janakr): If overhead of FragmentOptions class names is too high, add constructor that just |
| // takes fragments and gets names from them. |
| @AutoCodec |
| public final class BuildOptions implements Cloneable, Serializable { |
| private static final Comparator<Class<? extends FragmentOptions>> |
| lexicalFragmentOptionsComparator = Comparator.comparing(Class::getName); |
| private static final Comparator<Label> skylarkOptionsComparator = Ordering.natural(); |
| private static final Logger logger = Logger.getLogger(BuildOptions.class.getName()); |
| |
| public static Map<Label, Object> labelizeStarlarkOptions(Map<String, Object> starlarkOptions) { |
| return starlarkOptions.entrySet().stream() |
| .collect( |
| Collectors.toMap(e -> Label.parseAbsoluteUnchecked(e.getKey()), Map.Entry::getValue)); |
| } |
| |
| /** |
| * Creates a BuildOptions object with all options set to their default values, processed by the |
| * given {@code invocationPolicy}. |
| */ |
| static BuildOptions createDefaults( |
| Iterable<Class<? extends FragmentOptions>> options, InvocationPolicy invocationPolicy) { |
| return of(options, createDefaultParser(options, invocationPolicy)); |
| } |
| |
| private static OptionsParser createDefaultParser( |
| Iterable<Class<? extends FragmentOptions>> options, InvocationPolicy invocationPolicy) { |
| OptionsParser optionsParser = OptionsParser.newOptionsParser(options); |
| try { |
| new InvocationPolicyEnforcer(invocationPolicy).enforce(optionsParser); |
| } catch (OptionsParsingException e) { |
| throw new IllegalStateException(e); |
| } |
| return optionsParser; |
| } |
| |
| /** Creates a new BuildOptions instance for host. */ |
| public BuildOptions createHostOptions() { |
| Builder builder = builder(); |
| for (FragmentOptions options : fragmentOptionsMap.values()) { |
| builder.addFragmentOptions(options.getHost()); |
| } |
| return builder.addStarlarkOptions(skylarkOptionsMap).build(); |
| } |
| |
| /** |
| * Returns an equivalent instance to this one with only options from the given {@link |
| * FragmentOptions} classes. |
| */ |
| public BuildOptions trim(Set<Class<? extends FragmentOptions>> optionsClasses) { |
| Builder builder = builder(); |
| for (FragmentOptions options : fragmentOptionsMap.values()) { |
| if (optionsClasses.contains(options.getClass()) |
| // TODO(bazel-team): make this non-hacky while not requiring BuildConfiguration access |
| // to BuildOptions. |
| || options.toString().contains("BuildConfiguration$Options")) { |
| builder.addFragmentOptions(options); |
| } |
| } |
| return builder.addStarlarkOptions(skylarkOptionsMap).build(); |
| } |
| |
| /** |
| * Creates a BuildOptions class by taking the option values from an options provider (eg. an |
| * OptionsParser). |
| */ |
| public static BuildOptions of( |
| Iterable<Class<? extends FragmentOptions>> optionsList, OptionsProvider provider) { |
| Builder builder = builder(); |
| for (Class<? extends FragmentOptions> optionsClass : optionsList) { |
| builder.addFragmentOptions(provider.getOptions(optionsClass)); |
| } |
| return builder |
| .addStarlarkOptions(labelizeStarlarkOptions(provider.getStarlarkOptions())) |
| .build(); |
| } |
| |
| /** |
| * Creates a BuildOptions class by taking the option values from command-line arguments. Returns a |
| * BuildOptions class that only has native options. |
| */ |
| @VisibleForTesting |
| public static BuildOptions of(List<Class<? extends FragmentOptions>> optionsList, String... args) |
| throws OptionsParsingException { |
| Builder builder = builder(); |
| OptionsParser parser = |
| OptionsParser.newOptionsParser( |
| ImmutableList.<Class<? extends OptionsBase>>copyOf(optionsList)); |
| parser.parse(args); |
| for (Class<? extends FragmentOptions> optionsClass : optionsList) { |
| builder.addFragmentOptions(parser.getOptions(optionsClass)); |
| } |
| return builder.build(); |
| } |
| |
| /* |
| * Returns a BuildOptions class that only has skylark options. |
| */ |
| @VisibleForTesting |
| public static BuildOptions of(Map<Label, Object> skylarkOptions) { |
| return builder().addStarlarkOptions(skylarkOptions).build(); |
| } |
| |
| /** Returns the actual instance of a FragmentOptions class. */ |
| public <T extends FragmentOptions> T get(Class<T> optionsClass) { |
| FragmentOptions options = fragmentOptionsMap.get(optionsClass); |
| Preconditions.checkNotNull(options, "fragment options unavailable: " + optionsClass.getName()); |
| return optionsClass.cast(options); |
| } |
| |
| /** Returns true if these options contain the given {@link FragmentOptions}. */ |
| public boolean contains(Class<? extends FragmentOptions> optionsClass) { |
| return fragmentOptionsMap.containsKey(optionsClass); |
| } |
| |
| // It would be very convenient to use a Multimap here, but we cannot do that because we need to |
| // support defaults labels that have zero elements. |
| ImmutableMap<String, ImmutableSet<Label>> getDefaultsLabels() { |
| Map<String, Set<Label>> collector = new TreeMap<>(); |
| for (FragmentOptions fragment : fragmentOptionsMap.values()) { |
| for (Map.Entry<String, Set<Label>> entry : fragment.getDefaultsLabels().entrySet()) { |
| if (!collector.containsKey(entry.getKey())) { |
| collector.put(entry.getKey(), new TreeSet<Label>()); |
| } |
| collector.get(entry.getKey()).addAll(entry.getValue()); |
| } |
| } |
| |
| ImmutableMap.Builder<String, ImmutableSet<Label>> result = new ImmutableMap.Builder<>(); |
| for (Map.Entry<String, Set<Label>> entry : collector.entrySet()) { |
| result.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue())); |
| } |
| |
| return result.build(); |
| } |
| |
| /** The cache key for the options collection. Recomputes cache key every time it's called. */ |
| public String computeCacheKey() { |
| StringBuilder keyBuilder = new StringBuilder(); |
| for (FragmentOptions options : fragmentOptionsMap.values()) { |
| keyBuilder.append(options.cacheKey()); |
| } |
| keyBuilder.append( |
| OptionsBase.mapToCacheKey( |
| skylarkOptionsMap.entrySet().stream() |
| .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue)))); |
| return keyBuilder.toString(); |
| } |
| |
| public String computeChecksum() { |
| return Fingerprint.getHexDigest(computeCacheKey()); |
| } |
| |
| /** String representation of build options. */ |
| @Override |
| public String toString() { |
| StringBuilder stringBuilder = new StringBuilder(); |
| for (FragmentOptions options : fragmentOptionsMap.values()) { |
| stringBuilder.append(options); |
| } |
| return stringBuilder.toString(); |
| } |
| |
| /** Returns the options contained in this collection. */ |
| public Collection<FragmentOptions> getNativeOptions() { |
| return fragmentOptionsMap.values(); |
| } |
| |
| public ImmutableMap<Label, Object> getStarlarkOptions() { |
| return skylarkOptionsMap; |
| } |
| |
| /** |
| * Creates a copy of the BuildOptions object that contains copies of the FragmentOptions and |
| * skylark options. |
| */ |
| @Override |
| public BuildOptions clone() { |
| ImmutableMap.Builder<Class<? extends FragmentOptions>, FragmentOptions> nativeOptionsBuilder = |
| ImmutableMap.builder(); |
| for (Map.Entry<Class<? extends FragmentOptions>, FragmentOptions> entry : |
| fragmentOptionsMap.entrySet()) { |
| nativeOptionsBuilder.put(entry.getKey(), entry.getValue().clone()); |
| } |
| return new BuildOptions(nativeOptionsBuilder.build(), ImmutableMap.copyOf(skylarkOptionsMap)); |
| } |
| |
| /** |
| * Lazily initialize {@link #fingerprint} and {@link #hashCode}. Keeps computation off critical |
| * path of build, while still avoiding expensive computation for equality and hash code each time. |
| * |
| * <p>We check for nullity of {@link #fingerprint} to see if this method has already been called. |
| * Using {@link #hashCode} after this method is called is safe because it is set here before |
| * {@link #fingerprint} is set, so if {@link #fingerprint} is non-null then {@link #hashCode} is |
| * definitely set. |
| */ |
| private void maybeInitializeFingerprintAndHashCode() { |
| if (fingerprint != null) { |
| return; |
| } |
| synchronized (this) { |
| if (fingerprint != null) { |
| return; |
| } |
| Fingerprint fingerprint = new Fingerprint(); |
| for (Map.Entry<Class<? extends FragmentOptions>, FragmentOptions> entry : |
| fragmentOptionsMap.entrySet()) { |
| fingerprint.addString(entry.getKey().getName()); |
| fingerprint.addString(entry.getValue().cacheKey()); |
| } |
| for (Map.Entry<Label, Object> entry : skylarkOptionsMap.entrySet()) { |
| fingerprint.addString(entry.getKey().toString()); |
| fingerprint.addString(entry.getValue().toString()); |
| } |
| byte[] computedFingerprint = fingerprint.digestAndReset(); |
| hashCode = Arrays.hashCode(computedFingerprint); |
| this.fingerprint = computedFingerprint; |
| } |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (this == other) { |
| return true; |
| } else if (!(other instanceof BuildOptions)) { |
| return false; |
| } else { |
| maybeInitializeFingerprintAndHashCode(); |
| BuildOptions otherOptions = (BuildOptions) other; |
| otherOptions.maybeInitializeFingerprintAndHashCode(); |
| return Arrays.equals(this.fingerprint, otherOptions.fingerprint); |
| } |
| } |
| |
| @Override |
| public int hashCode() { |
| maybeInitializeFingerprintAndHashCode(); |
| return hashCode; |
| } |
| |
| // Lazily initialized. |
| @Nullable private volatile byte[] fingerprint; |
| private volatile int hashCode; |
| |
| /** Maps options class definitions to FragmentOptions objects. */ |
| private final ImmutableMap<Class<? extends FragmentOptions>, FragmentOptions> fragmentOptionsMap; |
| /** Maps skylark options names to skylark options values. */ |
| private final ImmutableMap<Label, Object> skylarkOptionsMap; |
| |
| @AutoCodec.VisibleForSerialization |
| BuildOptions( |
| ImmutableMap<Class<? extends FragmentOptions>, FragmentOptions> fragmentOptionsMap, |
| ImmutableMap<Label, Object> skylarkOptionsMap) { |
| this.fragmentOptionsMap = fragmentOptionsMap; |
| this.skylarkOptionsMap = skylarkOptionsMap; |
| } |
| |
| public BuildOptions applyDiff(OptionsDiffForReconstruction optionsDiff) { |
| if (optionsDiff.isEmpty()) { |
| return this; |
| } |
| maybeInitializeFingerprintAndHashCode(); |
| if (!Arrays.equals(fingerprint, optionsDiff.baseFingerprint)) { |
| throw new IllegalArgumentException("Can not reconstruct BuildOptions with a different base."); |
| } |
| Builder builder = builder(); |
| for (FragmentOptions options : fragmentOptionsMap.values()) { |
| FragmentOptions newOptions = optionsDiff.transformOptions(options); |
| if (newOptions != null) { |
| builder.addFragmentOptions(newOptions); |
| } |
| } |
| for (FragmentOptions extraSecondFragment : optionsDiff.extraSecondFragments) { |
| builder.addFragmentOptions(extraSecondFragment); |
| } |
| |
| Map<Label, Object> skylarkOptions = new HashMap<>(); |
| for (Map.Entry<Label, Object> buildSettingAndValue : skylarkOptionsMap.entrySet()) { |
| Label buildSetting = buildSettingAndValue.getKey(); |
| if (optionsDiff.extraFirstStarlarkOptions.contains(buildSetting)) { |
| continue; |
| } else if (optionsDiff.differingStarlarkOptions.containsKey(buildSetting)) { |
| skylarkOptions.put(buildSetting, optionsDiff.differingStarlarkOptions.get(buildSetting)); |
| } else { |
| skylarkOptions.put(buildSetting, skylarkOptionsMap.get(buildSetting)); |
| } |
| } |
| skylarkOptions.putAll(optionsDiff.extraSecondStarlarkOptions); |
| builder.addStarlarkOptions(skylarkOptions); |
| return builder.build(); |
| } |
| |
| /** Creates a builder object for BuildOptions */ |
| public static Builder builder() { |
| return new Builder(); |
| } |
| |
| /** Creates a builder operating on a clone of this BuildOptions. */ |
| public Builder toBuilder() { |
| return builder().merge(clone()); |
| } |
| |
| /** Builder class for BuildOptions. */ |
| public static class Builder { |
| /** |
| * Merges the given BuildOptions into this builder, overriding any previous instances of |
| * Starlark options or FragmentOptions subclasses found in the new BuildOptions. |
| */ |
| public Builder merge(BuildOptions options) { |
| for (FragmentOptions fragment : options.getNativeOptions()) { |
| this.addFragmentOptions(fragment); |
| } |
| this.addStarlarkOptions(options.getStarlarkOptions()); |
| return this; |
| } |
| |
| /** |
| * Adds a new FragmentOptions instance to the builder. Overrides previous instances of the exact |
| * same subclass of FragmentOptions. |
| */ |
| public <T extends FragmentOptions> Builder addFragmentOptions(T options) { |
| fragmentOptions.put(options.getClass(), options); |
| return this; |
| } |
| |
| /** |
| * Adds multiple Starlark options to the builder. Overrides previous instances of the same key. |
| */ |
| public Builder addStarlarkOptions(Map<Label, Object> options) { |
| starlarkOptions.putAll(options); |
| return this; |
| } |
| |
| /** Adds a Starlark option to the builder. Overrides previous instances of the same key. */ |
| public Builder addStarlarkOption(Label key, Object value) { |
| starlarkOptions.put(key, value); |
| return this; |
| } |
| |
| /** Removes the value for the Starlark option with the given key. */ |
| public Builder removeStarlarkOption(Label key) { |
| starlarkOptions.remove(key); |
| return this; |
| } |
| |
| public BuildOptions build() { |
| return new BuildOptions( |
| ImmutableSortedMap.copyOf(fragmentOptions, lexicalFragmentOptionsComparator), |
| ImmutableSortedMap.copyOf(starlarkOptions, skylarkOptionsComparator)); |
| } |
| |
| private final Map<Class<? extends FragmentOptions>, FragmentOptions> fragmentOptions; |
| private final Map<Label, Object> starlarkOptions; |
| |
| private Builder() { |
| fragmentOptions = new HashMap<>(); |
| starlarkOptions = new HashMap<>(); |
| } |
| } |
| |
| /** Returns the difference between two BuildOptions in a new {@link BuildOptions.OptionsDiff}. */ |
| public static OptionsDiff diff(@Nullable BuildOptions first, @Nullable BuildOptions second) { |
| return diff(new OptionsDiff(), first, second); |
| } |
| |
| /** |
| * Returns the difference between two BuildOptions in a pre-existing {@link |
| * BuildOptions.OptionsDiff}. |
| * |
| * <p>In a single pass through this method, the method can only compare a single "first" {@link |
| * BuildOptions} and single "second" BuildOptions; but an OptionsDiff instance can store the diff |
| * between a single "first" BuildOptions and multiple "second" BuildOptions. Being able to |
| * maintain a single OptionsDiff over multiple calls to diff is useful for, for example, |
| * aggregating the difference between a single BuildOptions and the results of applying a {@link |
| * com.google.devtools.build.lib.analysis.config.transitions.SplitTransition}) to it. |
| */ |
| public static OptionsDiff diff( |
| OptionsDiff diff, @Nullable BuildOptions first, @Nullable BuildOptions second) { |
| if (diff.hasStarlarkOptions) { |
| throw new IllegalStateException( |
| "OptionsDiff cannot handle multiple 'second' BuildOptions with skylark options " |
| + "and is trying to diff against a second BuildOptions with skylark options."); |
| } |
| if (first == null || second == null) { |
| throw new IllegalArgumentException("Cannot diff null BuildOptions"); |
| } |
| if (first.equals(second)) { |
| return diff; |
| } |
| // Check and report if either class has been trimmed of an options class that exists in the |
| // other. |
| ImmutableSet<Class<? extends FragmentOptions>> firstOptionClasses = |
| first.getNativeOptions().stream() |
| .map(FragmentOptions::getClass) |
| .collect(ImmutableSet.toImmutableSet()); |
| ImmutableSet<Class<? extends FragmentOptions>> secondOptionClasses = |
| second.getNativeOptions().stream() |
| .map(FragmentOptions::getClass) |
| .collect(ImmutableSet.toImmutableSet()); |
| Sets.difference(firstOptionClasses, secondOptionClasses).forEach(diff::addExtraFirstFragment); |
| Sets.difference(secondOptionClasses, firstOptionClasses).stream() |
| .map(second::get) |
| .forEach(diff::addExtraSecondFragment); |
| // For fragments in common, report differences. |
| for (Class<? extends FragmentOptions> clazz : |
| Sets.intersection(firstOptionClasses, secondOptionClasses)) { |
| if (!first.get(clazz).equals(second.get(clazz))) { |
| ImmutableList<OptionDefinition> definitions = OptionsParser.getOptionDefinitions(clazz); |
| Map<String, Object> firstClazzOptions = first.get(clazz).asMap(); |
| Map<String, Object> secondClazzOptions = second.get(clazz).asMap(); |
| for (OptionDefinition definition : definitions) { |
| String name = definition.getOptionName(); |
| Object firstValue = firstClazzOptions.get(name); |
| Object secondValue = secondClazzOptions.get(name); |
| if (!Objects.equals(firstValue, secondValue)) { |
| diff.addDiff(clazz, definition, firstValue, secondValue); |
| } |
| } |
| } |
| } |
| |
| // Compare skylark options for the two classes |
| Map<Label, Object> skylarkFirst = first.getStarlarkOptions(); |
| Map<Label, Object> skylarkSecond = second.getStarlarkOptions(); |
| diff.setHasStarlarkOptions(!skylarkFirst.isEmpty() || !skylarkSecond.isEmpty()); |
| for (Label buildSetting : Sets.union(skylarkFirst.keySet(), skylarkSecond.keySet())) { |
| if (skylarkFirst.get(buildSetting) == null) { |
| diff.addExtraSecondStarlarkOption(buildSetting, skylarkSecond.get(buildSetting)); |
| } else if (skylarkSecond.get(buildSetting) == null) { |
| diff.addExtraFirstStarlarkOption(buildSetting); |
| } else if (!skylarkFirst.get(buildSetting).equals(skylarkSecond.get(buildSetting))) { |
| diff.putStarlarkDiff( |
| buildSetting, skylarkFirst.get(buildSetting), skylarkSecond.get(buildSetting)); |
| } |
| } |
| return diff; |
| } |
| |
| /** |
| * Returns a {@link OptionsDiffForReconstruction} object that can be applied to {@code first} via |
| * {@link #applyDiff} to get a {@link BuildOptions} object equal to {@code second}. |
| */ |
| public static OptionsDiffForReconstruction diffForReconstruction( |
| BuildOptions first, BuildOptions second) { |
| OptionsDiff diff = diff(first, second); |
| if (diff.areSame()) { |
| return OptionsDiffForReconstruction.getEmpty(first.fingerprint, second.computeChecksum()); |
| } |
| LinkedHashMap<Class<? extends FragmentOptions>, Map<String, Object>> differingOptions = |
| new LinkedHashMap<>(diff.differingOptions.keySet().size()); |
| for (Class<? extends FragmentOptions> clazz : |
| diff.differingOptions.keySet().stream() |
| .sorted(lexicalFragmentOptionsComparator) |
| .collect(Collectors.toList())) { |
| Collection<OptionDefinition> fields = diff.differingOptions.get(clazz); |
| LinkedHashMap<String, Object> valueMap = new LinkedHashMap<>(fields.size()); |
| for (OptionDefinition optionDefinition : |
| fields.stream() |
| .sorted(Comparator.comparing(o -> o.getField().getName())) |
| .collect(Collectors.toList())) { |
| Object secondValue; |
| try { |
| secondValue = Iterables.getOnlyElement(diff.second.get(optionDefinition)); |
| } catch (IllegalArgumentException e) { |
| // TODO(janakr): Currently this exception should never be thrown since diff is never |
| // constructed using the diff method that takes in a preexisting OptionsDiff. If this |
| // changes, add a test verifying this error catching works properly. |
| throw new IllegalStateException( |
| "OptionsDiffForReconstruction can only handle a single first BuildOptions and a " |
| + "single second BuildOptions and has encountered multiple second BuildOptions", |
| e); |
| } |
| valueMap.put(optionDefinition.getField().getName(), secondValue); |
| } |
| differingOptions.put(clazz, valueMap); |
| } |
| first.maybeInitializeFingerprintAndHashCode(); |
| return new OptionsDiffForReconstruction( |
| differingOptions, |
| diff.extraFirstFragments.stream() |
| .sorted(lexicalFragmentOptionsComparator) |
| .collect(ImmutableSet.toImmutableSet()), |
| ImmutableList.sortedCopyOf( |
| Comparator.comparing(o -> o.getClass().getName()), diff.extraSecondFragments), |
| first.fingerprint, |
| second.computeChecksum(), |
| diff.skylarkSecond, |
| diff.extraStarlarkOptionsFirst, |
| diff.extraStarlarkOptionsSecond); |
| } |
| |
| /** |
| * A diff class for BuildOptions. Fields are meant to be populated and returned by {@link |
| * BuildOptions#diff} |
| */ |
| public static class OptionsDiff { |
| private final Multimap<Class<? extends FragmentOptions>, OptionDefinition> differingOptions = |
| ArrayListMultimap.create(); |
| // The keyset for the {@link first} and {@link second} maps are identical and indicate which |
| // specific options differ between the first and second built options. |
| private final Map<OptionDefinition, Object> first = new LinkedHashMap<>(); |
| // Since this class can be used to track the result of transitions, {@link second} is a multimap |
| // to be able to handle [@link SplitTransition}s. |
| private final Multimap<OptionDefinition, Object> second = OrderedSetMultimap.create(); |
| // List of "extra" fragments for each BuildOption aka fragments that were trimmed off one |
| // BuildOption but not the other. |
| private final Set<Class<? extends FragmentOptions>> extraFirstFragments = new HashSet<>(); |
| private final Set<FragmentOptions> extraSecondFragments = new HashSet<>(); |
| |
| private final Map<Label, Object> skylarkFirst = new LinkedHashMap<>(); |
| // TODO(b/112041323): This should also be multimap but we don't diff multiple times with |
| // skylark options anywhere yet so add that feature when necessary. |
| private final Map<Label, Object> skylarkSecond = new LinkedHashMap<>(); |
| |
| private final List<Label> extraStarlarkOptionsFirst = new ArrayList<>(); |
| private final Map<Label, Object> extraStarlarkOptionsSecond = new HashMap<>(); |
| |
| private boolean hasStarlarkOptions = false; |
| |
| @VisibleForTesting |
| Set<Class<? extends FragmentOptions>> getExtraFirstFragmentClassesForTesting() { |
| return extraFirstFragments; |
| } |
| |
| @VisibleForTesting |
| Set<FragmentOptions> getExtraSecondFragmentsForTesting() { |
| return extraSecondFragments; |
| } |
| |
| public Map<OptionDefinition, Object> getFirst() { |
| return first; |
| } |
| |
| public Multimap<OptionDefinition, Object> getSecond() { |
| return second; |
| } |
| |
| private void addDiff( |
| Class<? extends FragmentOptions> fragmentOptionsClass, |
| OptionDefinition option, |
| Object firstValue, |
| Object secondValue) { |
| differingOptions.put(fragmentOptionsClass, option); |
| first.put(option, firstValue); |
| second.put(option, secondValue); |
| } |
| |
| private void addExtraFirstFragment(Class<? extends FragmentOptions> options) { |
| extraFirstFragments.add(options); |
| } |
| |
| private void addExtraSecondFragment(FragmentOptions options) { |
| extraSecondFragments.add(options); |
| } |
| |
| private void putStarlarkDiff(Label buildSetting, Object firstValue, Object secondValue) { |
| skylarkFirst.put(buildSetting, firstValue); |
| skylarkSecond.put(buildSetting, secondValue); |
| } |
| |
| private void addExtraFirstStarlarkOption(Label buildSetting) { |
| extraStarlarkOptionsFirst.add(buildSetting); |
| } |
| |
| private void addExtraSecondStarlarkOption(Label buildSetting, Object value) { |
| extraStarlarkOptionsSecond.put(buildSetting, value); |
| } |
| |
| private void setHasStarlarkOptions(boolean hasStarlarkOptions) { |
| this.hasStarlarkOptions = hasStarlarkOptions; |
| } |
| |
| @VisibleForTesting |
| Map<Label, Object> getStarlarkFirstForTesting() { |
| return skylarkFirst; |
| } |
| |
| @VisibleForTesting |
| Map<Label, Object> getStarlarkSecondForTesting() { |
| return skylarkSecond; |
| } |
| |
| @VisibleForTesting |
| List<Label> getExtraStarlarkOptionsFirstForTesting() { |
| return extraStarlarkOptionsFirst; |
| } |
| |
| @VisibleForTesting |
| Map<Label, Object> getExtraStarlarkOptionsSecondForTesting() { |
| return extraStarlarkOptionsSecond; |
| } |
| |
| /** |
| * Note: it's not enough for first and second to be empty, with trimming, they must also contain |
| * the same options classes. |
| */ |
| boolean areSame() { |
| return first.isEmpty() |
| && second.isEmpty() |
| && extraSecondFragments.isEmpty() |
| && extraFirstFragments.isEmpty() |
| && differingOptions.isEmpty() |
| && skylarkFirst.isEmpty() |
| && skylarkSecond.isEmpty() |
| && extraStarlarkOptionsFirst.isEmpty() |
| && extraStarlarkOptionsSecond.isEmpty(); |
| } |
| |
| public String prettyPrint() { |
| StringBuilder toReturn = new StringBuilder(); |
| for (String diff : getPrettyPrintList()) { |
| toReturn.append(diff).append(System.lineSeparator()); |
| } |
| return toReturn.toString(); |
| } |
| |
| public List<String> getPrettyPrintList() { |
| List<String> toReturn = new ArrayList<>(); |
| first.forEach( |
| (option, value) -> |
| toReturn.add(option.getOptionName() + ":" + value + " -> " + second.get(option))); |
| skylarkFirst.forEach( |
| (option, value) -> toReturn.add(option + ":" + value + skylarkSecond.get(option))); |
| return toReturn; |
| } |
| } |
| |
| /** |
| * An object that encapsulates the data needed to transform one {@link BuildOptions} object into |
| * another: the full fragments of the second one, the fragment classes of the first that should be |
| * omitted, and the values of any fields that should be changed. |
| */ |
| public static final class OptionsDiffForReconstruction { |
| private final Map<Class<? extends FragmentOptions>, Map<String, Object>> differingOptions; |
| private final ImmutableSet<Class<? extends FragmentOptions>> extraFirstFragmentClasses; |
| private final ImmutableList<FragmentOptions> extraSecondFragments; |
| private final byte[] baseFingerprint; |
| private final String checksum; |
| |
| private final Map<Label, Object> differingStarlarkOptions; |
| private final List<Label> extraFirstStarlarkOptions; |
| private final Map<Label, Object> extraSecondStarlarkOptions; |
| |
| @VisibleForTesting |
| public OptionsDiffForReconstruction( |
| Map<Class<? extends FragmentOptions>, Map<String, Object>> differingOptions, |
| ImmutableSet<Class<? extends FragmentOptions>> extraFirstFragmentClasses, |
| ImmutableList<FragmentOptions> extraSecondFragments, |
| byte[] baseFingerprint, |
| String checksum, |
| Map<Label, Object> differingStarlarkOptions, |
| List<Label> extraFirstStarlarkOptions, |
| Map<Label, Object> extraSecondStarlarkOptions) { |
| this.differingOptions = differingOptions; |
| this.extraFirstFragmentClasses = extraFirstFragmentClasses; |
| this.extraSecondFragments = extraSecondFragments; |
| this.baseFingerprint = baseFingerprint; |
| this.checksum = checksum; |
| this.differingStarlarkOptions = differingStarlarkOptions; |
| this.extraFirstStarlarkOptions = extraFirstStarlarkOptions; |
| this.extraSecondStarlarkOptions = extraSecondStarlarkOptions; |
| } |
| |
| private static OptionsDiffForReconstruction getEmpty(byte[] baseFingerprint, String checksum) { |
| return new OptionsDiffForReconstruction( |
| ImmutableMap.of(), |
| ImmutableSet.of(), |
| ImmutableList.of(), |
| baseFingerprint, |
| checksum, |
| ImmutableMap.of(), |
| ImmutableList.of(), |
| ImmutableMap.of()); |
| } |
| |
| @Nullable |
| @VisibleForTesting |
| FragmentOptions transformOptions(FragmentOptions input) { |
| Class<? extends FragmentOptions> clazz = input.getClass(); |
| if (extraFirstFragmentClasses.contains(clazz)) { |
| return null; |
| } |
| Map<String, Object> changedOptions = differingOptions.get(clazz); |
| if (changedOptions == null || changedOptions.isEmpty()) { |
| return input; |
| } |
| FragmentOptions newOptions = input.clone(); |
| for (Map.Entry<String, Object> entry : changedOptions.entrySet()) { |
| try { |
| clazz.getField(entry.getKey()).set(newOptions, entry.getValue()); |
| } catch (IllegalAccessException | NoSuchFieldException e) { |
| throw new IllegalStateException("Couldn't set " + entry + " for " + newOptions, e); |
| } |
| } |
| return newOptions; |
| } |
| |
| public String getChecksum() { |
| return checksum; |
| } |
| |
| private boolean isEmpty() { |
| return differingOptions.isEmpty() |
| && extraFirstFragmentClasses.isEmpty() |
| && extraSecondFragments.isEmpty() |
| && differingStarlarkOptions.isEmpty() |
| && extraFirstStarlarkOptions.isEmpty() |
| && extraSecondStarlarkOptions.isEmpty(); |
| } |
| |
| /** |
| * Compares the fragment sets in the options described by two diffs with the same base. |
| * |
| * @see ConfigurationComparer |
| */ |
| public static ConfigurationComparer.Result compareFragments( |
| OptionsDiffForReconstruction left, OptionsDiffForReconstruction right) { |
| Preconditions.checkArgument( |
| Arrays.equals(left.baseFingerprint, right.baseFingerprint), |
| "Can't compare diffs with different bases: %s and %s", |
| left, |
| right); |
| // This code effectively looks up each piece of data (native fragment or Starlark option) in |
| // this table (numbers reference comments in the code below): |
| // â–¼left rightâ–¶ (none) extraSecond extraFirst differing |
| // (none) equal right only (#4) left only (#4) different (#1) |
| // extraSecond left only (#4) compare (#3) (impossible) (impossible) |
| // extraFirst right only (#4) (impossible) equal right only (#4) |
| // differing different (#1) (impossible) left only (#4) compare (#2) |
| |
| // Any difference in shared data is grounds to return DIFFERENT, which happens if: |
| // 1a. any starlark option was changed by one diff, but is neither changed nor removed by |
| // the other |
| if (left.hasChangeToStarlarkOptionUnchangedIn(right) |
| || right.hasChangeToStarlarkOptionUnchangedIn(left)) { |
| return ConfigurationComparer.Result.DIFFERENT; |
| } |
| // 1b. any native fragment was changed by one diff, but is neither changed nor removed by |
| // the other |
| if (left.hasChangeToNativeFragmentUnchangedIn(right) |
| || right.hasChangeToNativeFragmentUnchangedIn(left)) { |
| return ConfigurationComparer.Result.DIFFERENT; |
| } |
| // 2a. any starlark option was changed by both diffs, but to different values |
| if (!commonKeysHaveEqualValues( |
| left.differingStarlarkOptions, right.differingStarlarkOptions)) { |
| return ConfigurationComparer.Result.DIFFERENT; |
| } |
| // 2b. any native fragment was changed by both diffs, but to different values |
| if (!commonKeysHaveEqualValues(left.differingOptions, right.differingOptions)) { |
| return ConfigurationComparer.Result.DIFFERENT; |
| } |
| // 3a. any starlark option was added by both diffs, but with different values |
| if (!commonKeysHaveEqualValues( |
| left.extraSecondStarlarkOptions, right.extraSecondStarlarkOptions)) { |
| return ConfigurationComparer.Result.DIFFERENT; |
| } |
| // 3b. any native fragment was added by both diffs, but with different values |
| if (!commonKeysHaveEqualValues( |
| left.getExtraSecondFragmentsByClass(), right.getExtraSecondFragmentsByClass())) { |
| return ConfigurationComparer.Result.DIFFERENT; |
| } |
| |
| // At this point DIFFERENT is definitely not the result, so depending on which side(s) have |
| // extra data, we can decide which of the remaining choices to return. (#4) |
| boolean leftHasExtraData = left.hasExtraNativeFragmentsOrStarlarkOptionsNotIn(right); |
| boolean rightHasExtraData = right.hasExtraNativeFragmentsOrStarlarkOptionsNotIn(left); |
| |
| if (leftHasExtraData && rightHasExtraData) { |
| // If both have data that the other does not, all-shared-fragments-are-equal is all |
| // that can be said. |
| return ConfigurationComparer.Result.ALL_SHARED_FRAGMENTS_EQUAL; |
| } else if (leftHasExtraData) { |
| // If only the left instance has extra data, left is a superset of right. |
| return ConfigurationComparer.Result.SUPERSET; |
| } else if (rightHasExtraData) { |
| // If only the right instance has extra data, left is a subset of right. |
| return ConfigurationComparer.Result.SUBSET; |
| } else { |
| // If there is no extra data, the two options described by these diffs are equal. |
| return ConfigurationComparer.Result.EQUAL; |
| } |
| } |
| |
| private boolean hasChangeToStarlarkOptionUnchangedIn(OptionsDiffForReconstruction that) { |
| Set<Label> starlarkOptionsChangedOrRemovedInThat = |
| Sets.union( |
| that.differingStarlarkOptions.keySet(), |
| ImmutableSet.copyOf(that.extraFirstStarlarkOptions)); |
| return !starlarkOptionsChangedOrRemovedInThat.containsAll( |
| this.differingStarlarkOptions.keySet()); |
| } |
| |
| private boolean hasChangeToNativeFragmentUnchangedIn(OptionsDiffForReconstruction that) { |
| Set<Class<? extends FragmentOptions>> nativeFragmentsChangedOrRemovedInThat = |
| Sets.union(that.differingOptions.keySet(), that.extraFirstFragmentClasses); |
| return !nativeFragmentsChangedOrRemovedInThat.containsAll(this.differingOptions.keySet()); |
| } |
| |
| private Map<Class<? extends FragmentOptions>, FragmentOptions> |
| getExtraSecondFragmentsByClass() { |
| ImmutableMap.Builder<Class<? extends FragmentOptions>, FragmentOptions> builder = |
| new ImmutableMap.Builder<>(); |
| for (FragmentOptions options : extraSecondFragments) { |
| builder.put(options.getClass(), options); |
| } |
| return builder.build(); |
| } |
| |
| private static <K> boolean commonKeysHaveEqualValues(Map<K, ?> left, Map<K, ?> right) { |
| Set<K> commonKeys = Sets.intersection(left.keySet(), right.keySet()); |
| for (K commonKey : commonKeys) { |
| if (!Objects.equals(left.get(commonKey), right.get(commonKey))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private boolean hasExtraNativeFragmentsOrStarlarkOptionsNotIn( |
| OptionsDiffForReconstruction that) { |
| // extra fragments/options can be... |
| // starlark options added by this diff, but not that one |
| if (!that.extraSecondStarlarkOptions |
| .keySet() |
| .containsAll(this.extraSecondStarlarkOptions.keySet())) { |
| return true; |
| } |
| // native fragments added by this diff, but not that one |
| if (!that.getExtraSecondFragmentsByClass() |
| .keySet() |
| .containsAll(this.getExtraSecondFragmentsByClass().keySet())) { |
| return true; |
| } |
| // starlark options removed by that diff, but not this one |
| if (!this.extraFirstStarlarkOptions.containsAll(that.extraFirstStarlarkOptions)) { |
| return true; |
| } |
| // native fragments removed by that diff, but not this one |
| if (!this.extraFirstFragmentClasses.containsAll(that.extraFirstFragmentClasses)) { |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| OptionsDiffForReconstruction that = (OptionsDiffForReconstruction) o; |
| return Arrays.equals(this.baseFingerprint, that.baseFingerprint) |
| && this.checksum.equals(that.checksum); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("differingOptions", differingOptions) |
| .add("extraFirstFragmentClasses", extraFirstFragmentClasses) |
| .add("extraSecondFragments", extraSecondFragments) |
| .add("differingStarlarkOptions", differingStarlarkOptions) |
| .add("extraFirstStarlarkOptions", extraFirstStarlarkOptions) |
| .add("extraSecondStarlarkOptions", extraSecondStarlarkOptions) |
| .toString(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return 31 * Arrays.hashCode(baseFingerprint) + checksum.hashCode(); |
| } |
| |
| private static class Codec implements ObjectCodec<OptionsDiffForReconstruction> { |
| |
| @Override |
| public Class<OptionsDiffForReconstruction> getEncodedClass() { |
| return OptionsDiffForReconstruction.class; |
| } |
| |
| @Override |
| public void serialize( |
| SerializationContext context, |
| OptionsDiffForReconstruction diff, |
| CodedOutputStream codedOut) |
| throws SerializationException, IOException { |
| OptionsDiffCache cache = context.getDependency(OptionsDiffCache.class); |
| ByteString bytes = cache.getBytesFromOptionsDiff(diff); |
| if (bytes == null) { |
| context = context.getNewNonMemoizingContext(); |
| ByteString.Output byteStringOut = ByteString.newOutput(); |
| CodedOutputStream bytesOut = CodedOutputStream.newInstance(byteStringOut); |
| context.serialize(diff.differingOptions, bytesOut); |
| context.serialize(diff.extraFirstFragmentClasses, bytesOut); |
| context.serialize(diff.extraSecondFragments, bytesOut); |
| bytesOut.writeByteArrayNoTag(diff.baseFingerprint); |
| context.serialize(diff.checksum, bytesOut); |
| context.serialize(diff.differingStarlarkOptions, bytesOut); |
| context.serialize(diff.extraFirstStarlarkOptions, bytesOut); |
| context.serialize(diff.extraSecondStarlarkOptions, bytesOut); |
| bytesOut.flush(); |
| byteStringOut.flush(); |
| int optionsDiffSize = byteStringOut.size(); |
| bytes = byteStringOut.toByteString(); |
| cache.putBytesFromOptionsDiff(diff, bytes); |
| logger.info( |
| "Serialized OptionsDiffForReconstruction " |
| + diff.toString() |
| + ". Diff took " |
| + optionsDiffSize |
| + " bytes."); |
| } |
| codedOut.writeBytesNoTag(bytes); |
| } |
| |
| @Override |
| public OptionsDiffForReconstruction deserialize( |
| DeserializationContext context, CodedInputStream codedIn) |
| throws SerializationException, IOException { |
| OptionsDiffCache cache = context.getDependency(OptionsDiffCache.class); |
| ByteString bytes = codedIn.readBytes(); |
| OptionsDiffForReconstruction diff = cache.getOptionsDiffFromBytes(bytes); |
| if (diff == null) { |
| CodedInputStream codedInput = bytes.newCodedInput(); |
| context = context.getNewNonMemoizingContext(); |
| Map<Class<? extends FragmentOptions>, Map<String, Object>> differingOptions = |
| context.deserialize(codedInput); |
| ImmutableSet<Class<? extends FragmentOptions>> extraFirstFragmentClasses = |
| context.deserialize(codedInput); |
| ImmutableList<FragmentOptions> extraSecondFragments = context.deserialize(codedInput); |
| byte[] baseFingerprint = codedInput.readByteArray(); |
| String checksum = context.deserialize(codedInput); |
| Map<Label, Object> differingStarlarkOptions = context.deserialize(codedInput); |
| List<Label> extraFirstStarlarkOptions = context.deserialize(codedInput); |
| Map<Label, Object> extraSecondStarlarkOptions = context.deserialize(codedInput); |
| diff = |
| new OptionsDiffForReconstruction( |
| differingOptions, |
| extraFirstFragmentClasses, |
| extraSecondFragments, |
| baseFingerprint, |
| checksum, |
| differingStarlarkOptions, |
| extraFirstStarlarkOptions, |
| extraSecondStarlarkOptions); |
| cache.putBytesFromOptionsDiff(diff, bytes); |
| } |
| return diff; |
| } |
| } |
| } |
| |
| /** |
| * Injected cache for {@code Codec}, so that we don't have to repeatedly serialize the same |
| * object. We still incur the over-the-wire cost of the bytes, but we don't use CPU to repeatedly |
| * compute it. |
| * |
| * <p>We provide the cache as an injected dependency so that different serializers' caches are |
| * isolated. |
| * |
| * <p>Used when configured targets are serialized: the more compact {@link |
| * FingerprintingKDiffToByteStringCache} cache below cannot be easily used because a configured |
| * target may have an edge to a configured target in a different configuration, and with only the |
| * checksum there would be no way to compute that configuration (although it is very likely in the |
| * graph already). |
| */ |
| public interface OptionsDiffCache { |
| ByteString getBytesFromOptionsDiff(OptionsDiffForReconstruction diff); |
| |
| void putBytesFromOptionsDiff(OptionsDiffForReconstruction diff, ByteString bytes); |
| |
| OptionsDiffForReconstruction getOptionsDiffFromBytes(ByteString bytes); |
| } |
| |
| /** |
| * Implementation of the {@link OptionsDiffCache} that acts as a {@code BiMap} utilizing two |
| * {@code ConcurrentHashMaps}. |
| */ |
| public static class DiffToByteCache implements OptionsDiffCache { |
| // We expect there to be very few elements so keeping the reverse map as well as the forward |
| // map should be very cheap. |
| private static final ConcurrentHashMap<OptionsDiffForReconstruction, ByteString> |
| diffToByteStringMap = new ConcurrentHashMap<>(); |
| private static final ConcurrentHashMap<ByteString, OptionsDiffForReconstruction> |
| byteStringToDiffMap = new ConcurrentHashMap<>(); |
| |
| @VisibleForTesting |
| public DiffToByteCache() {} |
| |
| @Override |
| public ByteString getBytesFromOptionsDiff(OptionsDiffForReconstruction diff) { |
| return diffToByteStringMap.get(diff); |
| } |
| |
| @Override |
| public void putBytesFromOptionsDiff(OptionsDiffForReconstruction diff, ByteString bytes) { |
| // We need to insert data into map that will be used for deserialization first in case there |
| // is a race between two threads. If we populated the diffToByteStringMap first, another |
| // thread could get the result above and race ahead, but then get a cache miss on |
| // deserialization. |
| byteStringToDiffMap.put(bytes, diff); |
| diffToByteStringMap.put(diff, bytes); |
| } |
| |
| @Override |
| public OptionsDiffForReconstruction getOptionsDiffFromBytes(ByteString bytes) { |
| return byteStringToDiffMap.get(bytes); |
| } |
| } |
| |
| /** |
| * {@link BuildOptions.OptionsDiffForReconstruction} serialization cache that relies on only |
| * serializing the checksum string instead of the full OptionsDiffForReconstruction. |
| * |
| * <p>This requires that every {@link BuildOptions.OptionsDiffForReconstruction} instance |
| * encountered is serialized <i>before</i> it is ever deserialized. When not serializing |
| * configured targets, this holds because every configuration present in the build is looked up in |
| * the graph using a {@code BuildConfigurationValue.Key}, which contains its {@link |
| * BuildOptions.OptionsDiffForReconstruction}. This requires that {@code BuildConfigurationValue} |
| * instances must always be serialized. |
| */ |
| public static class FingerprintingKDiffToByteStringCache |
| implements BuildOptions.OptionsDiffCache { |
| private static final ConcurrentHashMap<OptionsDiffForReconstruction, ByteString> |
| diffToByteStringCache = new ConcurrentHashMap<>(); |
| private static final ConcurrentHashMap<ByteString, OptionsDiffForReconstruction> |
| byteStringToDiffMap = new ConcurrentHashMap<>(); |
| |
| @Override |
| public ByteString getBytesFromOptionsDiff(OptionsDiffForReconstruction diff) { |
| ByteString checksumString = diffToByteStringCache.get(diff); |
| if (checksumString != null) { |
| // Fast path to avoid ByteString creation churn and unnecessary map insertions. |
| return checksumString; |
| } |
| checksumString = ByteString.copyFromUtf8(diff.getChecksum()); |
| // We need to insert data into map that will be used for deserialization first in case there |
| // is a race between two threads. If we populated the diffToByteStringCache first, another |
| // thread could get checksumString above during serialization and race ahead, but then get a |
| // cache miss on deserialization. |
| byteStringToDiffMap.put(checksumString, diff); |
| diffToByteStringCache.put(diff, checksumString); |
| return checksumString; |
| } |
| |
| @Override |
| public void putBytesFromOptionsDiff(OptionsDiffForReconstruction diff, ByteString bytes) { |
| throw new UnsupportedOperationException( |
| "diff " |
| + diff |
| + " should have not been serialized: " |
| + diffToByteStringCache |
| + ", " |
| + byteStringToDiffMap); |
| } |
| |
| @Override |
| public OptionsDiffForReconstruction getOptionsDiffFromBytes(ByteString bytes) { |
| return Preconditions.checkNotNull( |
| byteStringToDiffMap.get(bytes), |
| "Missing bytes %s: %s %s", |
| bytes, |
| diffToByteStringCache, |
| byteStringToDiffMap); |
| } |
| } |
| } |