com.google.idea.blaze.base.sync.aspects.BlazeIdeInterfaceAspectsImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.google.idea.blaze.base.sync.aspects.BlazeIdeInterfaceAspectsImpl.java

Source

/*
 * Copyright 2016 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.idea.blaze.base.sync.aspects;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.idea.blaze.base.async.FutureUtil;
import com.google.idea.blaze.base.async.executor.BlazeExecutor;
import com.google.idea.blaze.base.async.process.ExternalTask;
import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
import com.google.idea.blaze.base.command.BlazeCommand;
import com.google.idea.blaze.base.command.BlazeCommandName;
import com.google.idea.blaze.base.command.BlazeFlags;
import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
import com.google.idea.blaze.base.filecache.FileDiffer;
import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
import com.google.idea.blaze.base.ideinfo.TargetKey;
import com.google.idea.blaze.base.ideinfo.TargetMap;
import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
import com.google.idea.blaze.base.metrics.Action;
import com.google.idea.blaze.base.model.BlazeVersionData;
import com.google.idea.blaze.base.model.SyncState;
import com.google.idea.blaze.base.model.primitives.TargetExpression;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.prefetch.PrefetchService;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.Result;
import com.google.idea.blaze.base.scope.Scope;
import com.google.idea.blaze.base.scope.ScopedFunction;
import com.google.idea.blaze.base.scope.output.PerformanceWarning;
import com.google.idea.blaze.base.scope.output.PrintOutput;
import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
import com.google.idea.blaze.base.scope.scopes.TimingScope;
import com.google.idea.blaze.base.settings.Blaze;
import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategy;
import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProvider;
import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;
import java.util.zip.GZIPInputStream;
import javax.annotation.Nullable;

/** Implementation of BlazeIdeInterface based on aspects. */
public class BlazeIdeInterfaceAspectsImpl implements BlazeIdeInterface {

    private static final Logger LOG = Logger.getInstance(BlazeIdeInterfaceAspectsImpl.class);

    static class State implements Serializable {
        private static final long serialVersionUID = 14L;
        TargetMap targetMap;
        ImmutableMap<File, Long> fileState = null;
        Map<File, TargetKey> fileToTargetMapKey = Maps.newHashMap();
        WorkspaceLanguageSettings workspaceLanguageSettings;
        String aspectStrategyName;
    }

    @Override
    public IdeResult updateTargetMap(Project project, BlazeContext context, WorkspaceRoot workspaceRoot,
            ProjectViewSet projectViewSet, BlazeVersionData blazeVersionData, List<TargetExpression> targets,
            WorkspaceLanguageSettings workspaceLanguageSettings, ArtifactLocationDecoder artifactLocationDecoder,
            SyncState.Builder syncStateBuilder, @Nullable SyncState previousSyncState, boolean mergeWithOldState) {
        State prevState = previousSyncState != null ? previousSyncState.get(State.class) : null;

        // If the language filter has changed, redo everything from scratch
        if (prevState != null && !prevState.workspaceLanguageSettings.equals(workspaceLanguageSettings)) {
            prevState = null;
        }

        // If the aspect strategy has changed, redo everything from scratch
        final AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);
        if (prevState != null && !Objects.equal(prevState.aspectStrategyName, aspectStrategy.getName())) {
            prevState = null;
        }

        IdeInfoResult ideInfoResult = getIdeInfo(project, context, workspaceRoot, projectViewSet, targets,
                aspectStrategy);
        if (ideInfoResult.buildResult == BuildResult.FATAL_ERROR) {
            return new IdeResult(prevState != null ? prevState.targetMap : null, BuildResult.FATAL_ERROR);
        }
        // If there was a partial error, make a best-effort attempt to sync. Retain
        // any old state that we have in an attempt not to lose too much code.
        if (ideInfoResult.buildResult == BuildResult.BUILD_ERROR) {
            mergeWithOldState = true;
        }

        List<File> fileList = ideInfoResult.files;
        List<File> updatedFiles = Lists.newArrayList();
        List<File> removedFiles = Lists.newArrayList();
        ImmutableMap<File, Long> fileState = FileDiffer.updateFiles(prevState != null ? prevState.fileState : null,
                fileList, updatedFiles, removedFiles);
        if (fileState == null) {
            return new IdeResult(prevState != null ? prevState.targetMap : null, BuildResult.FATAL_ERROR);
        }

        context.output(PrintOutput.log(String.format("Total rules: %d, new/changed: %d, removed: %d",
                fileList.size(), updatedFiles.size(), removedFiles.size())));

        ListenableFuture<?> prefetchFuture = PrefetchService.getInstance().prefetchFiles(project, updatedFiles);
        if (!FutureUtil.waitForFuture(context, prefetchFuture).timed("FetchAspectOutput")
                .withProgressMessage("Reading IDE info result...").run().success()) {
            return new IdeResult(prevState != null ? prevState.targetMap : null, BuildResult.FATAL_ERROR);
        }

        State state = updateState(context, prevState, fileState, workspaceLanguageSettings, artifactLocationDecoder,
                aspectStrategy, updatedFiles, removedFiles, mergeWithOldState);
        if (state == null) {
            return new IdeResult(prevState != null ? prevState.targetMap : null, BuildResult.FATAL_ERROR);
        }
        syncStateBuilder.put(State.class, state);
        return new IdeResult(state.targetMap, ideInfoResult.buildResult);
    }

    private static class IdeInfoResult {
        private final List<File> files;
        private final BuildResult buildResult;

        IdeInfoResult(List<File> files, BuildResult buildResult) {
            this.files = files;
            this.buildResult = buildResult;
        }
    }

    private static IdeInfoResult getIdeInfo(Project project, BlazeContext parentContext,
            WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet, List<TargetExpression> targets,
            AspectStrategy aspectStrategy) {
        return Scope.push(parentContext, context -> {
            context.push(new TimingScope(String.format("Execute%sCommand", Blaze.buildSystemName(project))));

            List<File> result = Lists.newArrayList();

            BuildSystem buildSystem = Blaze.getBuildSystem(project);
            BlazeCommand.Builder blazeCommandBuilder = BlazeCommand.builder(buildSystem, BlazeCommandName.BUILD);
            blazeCommandBuilder.addTargets(targets);
            blazeCommandBuilder.addBlazeFlags(BlazeFlags.KEEP_GOING);
            blazeCommandBuilder.addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS)
                    .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));

            aspectStrategy.modifyIdeInfoCommand(blazeCommandBuilder);

            String fileExtension = aspectStrategy.getAspectOutputFileExtension();
            String gzFileExtension = fileExtension + ".gz";
            Predicate<String> fileFilter = fileName -> fileName.endsWith(fileExtension)
                    || fileName.endsWith(gzFileExtension);

            int retVal = ExternalTask.builder(workspaceRoot).addBlazeCommand(blazeCommandBuilder.build())
                    .context(context)
                    .stderr(LineProcessingOutputStream.of(
                            new ExperimentalShowArtifactsLineProcessor(result, fileFilter),
                            new IssueOutputLineProcessor(project, context, workspaceRoot)))
                    .build().run(new LoggedTimingScope(project, Action.BLAZE_BUILD));

            BuildResult buildResult = BuildResult.fromExitCode(retVal);
            return new IdeInfoResult(result, buildResult);
        });
    }

    private static class TargetFilePair {
        private final File file;
        private final TargetIdeInfo target;

        TargetFilePair(File file, TargetIdeInfo target) {
            this.file = file;
            this.target = target;
        }
    }

    @Nullable
    static State updateState(BlazeContext parentContext, @Nullable State prevState,
            ImmutableMap<File, Long> fileState, WorkspaceLanguageSettings workspaceLanguageSettings,
            ArtifactLocationDecoder artifactLocationDecoder, AspectStrategy aspectStrategy, List<File> newFiles,
            List<File> removedFiles, boolean mergeWithOldState) {
        Result<State> result = Scope.push(parentContext, (ScopedFunction<Result<State>>) context -> {
            context.push(new TimingScope("UpdateTargetMap"));

            // If we're not removing we have to merge the old state
            // into the new one or we'll miss file removes next time
            ImmutableMap<File, Long> nextFileState = fileState;
            if (mergeWithOldState && prevState != null) {
                ImmutableMap.Builder<File, Long> fileStateBuilder = ImmutableMap.<File, Long>builder()
                        .putAll(fileState);
                for (Map.Entry<File, Long> entry : prevState.fileState.entrySet()) {
                    if (!fileState.containsKey(entry.getKey())) {
                        fileStateBuilder.put(entry);
                    }
                }
                nextFileState = fileStateBuilder.build();
            }

            State state = new State();
            state.fileState = nextFileState;
            state.workspaceLanguageSettings = workspaceLanguageSettings;
            state.aspectStrategyName = aspectStrategy.getName();

            Map<TargetKey, TargetIdeInfo> targetMap = Maps.newHashMap();
            Map<TargetKey, TargetIdeInfo> updatedTargets = Maps.newHashMap();
            if (prevState != null) {
                targetMap.putAll(prevState.targetMap.map());
                state.fileToTargetMapKey.putAll(prevState.fileToTargetMapKey);
            }

            // Update removed unless we're merging with the old state
            if (!mergeWithOldState) {
                for (File removedFile : removedFiles) {
                    TargetKey key = state.fileToTargetMapKey.remove(removedFile);
                    if (key != null) {
                        targetMap.remove(key);
                    }
                }
            }

            AtomicLong totalSizeLoaded = new AtomicLong(0);

            ListeningExecutorService executor = BlazeExecutor.getInstance().getExecutor();

            // Read protos from any new files
            List<ListenableFuture<TargetFilePair>> futures = Lists.newArrayList();
            for (File file : newFiles) {
                futures.add(executor.submit(() -> {
                    totalSizeLoaded.addAndGet(file.length());
                    try (InputStream inputStream = getAspectInputStream(file)) {
                        IntellijIdeInfo.TargetIdeInfo ruleProto = aspectStrategy.readAspectFile(inputStream);
                        TargetIdeInfo target = IdeInfoFromProtobuf.makeTargetIdeInfo(workspaceLanguageSettings,
                                ruleProto);
                        return new TargetFilePair(file, target);
                    }
                }));
            }

            // Update state with result from proto files
            int duplicateTargetLabels = 0;
            try {
                for (TargetFilePair targetFilePairs : Futures.allAsList(futures).get()) {
                    if (targetFilePairs.target != null) {
                        File file = targetFilePairs.file;
                        TargetKey key = targetFilePairs.target.key;
                        TargetIdeInfo previousTarget = updatedTargets.putIfAbsent(key, targetFilePairs.target);
                        if (previousTarget == null) {
                            state.fileToTargetMapKey.put(file, key);
                        } else {
                            duplicateTargetLabels++;
                        }
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return Result.error(null);
            } catch (ExecutionException e) {
                return Result.error(e);
            }
            targetMap.putAll(updatedTargets);

            context.output(PrintOutput.log(String.format("Loaded %d aspect files, total size %dkB", newFiles.size(),
                    totalSizeLoaded.get() / 1024)));
            if (duplicateTargetLabels > 0) {
                context.output(new PerformanceWarning(String.format(
                        "There were %d duplicate rules. "
                                + "You may be including multiple configurations in your build. "
                                + "Your IDE sync is slowed down by ~%d%%.",
                        duplicateTargetLabels, (100 * duplicateTargetLabels / targetMap.size()))));
            }

            state.targetMap = new TargetMap(ImmutableMap.copyOf(targetMap));
            return Result.of(state);
        });

        if (result.error != null) {
            LOG.error(result.error);
            return null;
        }
        return result.result;
    }

    private static InputStream getAspectInputStream(File file) throws IOException {
        InputStream inputStream = new FileInputStream(file);
        if (file.getName().endsWith(".gz")) {
            inputStream = new GZIPInputStream(inputStream);
        }
        return inputStream;
    }

    @Override
    public BuildResult resolveIdeArtifacts(Project project, BlazeContext context, WorkspaceRoot workspaceRoot,
            ProjectViewSet projectViewSet, BlazeVersionData blazeVersionData, List<TargetExpression> targets) {
        return resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, blazeVersionData, targets,
                false);
    }

    @Override
    public BuildResult compileIdeArtifacts(Project project, BlazeContext context, WorkspaceRoot workspaceRoot,
            ProjectViewSet projectViewSet, BlazeVersionData blazeVersionData, List<TargetExpression> targets) {
        boolean ideCompile = hasIdeCompileOutputGroup(blazeVersionData);
        return resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, blazeVersionData, targets,
                ideCompile);
    }

    private static boolean hasIdeCompileOutputGroup(BlazeVersionData blazeVersionData) {
        return blazeVersionData.bazelIsAtLeastVersion(0, 4, 3);
    }

    private static BuildResult resolveIdeArtifacts(Project project, BlazeContext context,
            WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet, BlazeVersionData blazeVersionData,
            List<TargetExpression> targets, boolean useIdeCompileOutputGroup) {
        AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);

        BlazeCommand.Builder blazeCommandBuilder = BlazeCommand
                .builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD).addTargets(targets).addBlazeFlags()
                .addBlazeFlags(BlazeFlags.KEEP_GOING).addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));

        if (useIdeCompileOutputGroup) {
            aspectStrategy.modifyIdeCompileCommand(blazeCommandBuilder);
        } else {
            aspectStrategy.modifyIdeResolveCommand(blazeCommandBuilder);
        }

        BlazeCommand blazeCommand = blazeCommandBuilder.build();

        int retVal = ExternalTask.builder(workspaceRoot).addBlazeCommand(blazeCommand).context(context)
                .stderr(LineProcessingOutputStream
                        .of(new IssueOutputLineProcessor(project, context, workspaceRoot)))
                .build().run(new LoggedTimingScope(project, Action.BLAZE_BUILD));

        return BuildResult.fromExitCode(retVal);
    }

    private static AspectStrategy getAspectStrategy(Project project, BlazeVersionData blazeVersionData) {
        for (AspectStrategyProvider provider : AspectStrategyProvider.EP_NAME.getExtensions()) {
            AspectStrategy aspectStrategy = provider.getAspectStrategy(project, blazeVersionData);
            if (aspectStrategy != null) {
                return aspectStrategy;
            }
        }
        // Should never get here
        throw new IllegalStateException("No aspect strategy found.");
    }
}