/* * Copyright 2003-2015 JetBrains s.r.o. * * 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 jetbrains.mps.generator.impl.dependencies; import jetbrains.mps.InternalFlag; import jetbrains.mps.extapi.model.GeneratableSModel; import jetbrains.mps.generator.IncrementalGenerationStrategy; import jetbrains.mps.generator.impl.GenerationFailureException; import jetbrains.mps.generator.impl.GeneratorMappings; import jetbrains.mps.generator.impl.cache.BrokenCacheException; import jetbrains.mps.generator.impl.cache.IntermediateCacheHelper; import jetbrains.mps.generator.impl.cache.MappingsMemento; import jetbrains.mps.generator.impl.cache.TransientModelWithMetainfo; import jetbrains.mps.util.IterableUtil; import jetbrains.mps.util.SNodeOperations; import jetbrains.mps.util.annotation.ToRemove; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Dependencies collector. Created once per model generation. * <p/> * Evgeny Gryaznov, May 11, 2010 */ public class IncrementalDependenciesBuilder implements DependenciesBuilder { private static final Logger LOG = LogManager.getLogger(IncrementalDependenciesBuilder.class); /* generation data */ private Map<String, String> myDependenciesTraces; private final Map<SNode, RootDependenciesBuilder> myRootBuilders = new HashMap<SNode, RootDependenciesBuilder>(); private final String myModelHash; private final String myParametersHash; private final IntermediateCacheHelper myCache; private RootDependenciesBuilder myConditionalsBuilder; private RootDependenciesBuilder[] myAllBuilders; /* next step input -> original */ Map<SNode, SNode> nextStepToOriginalMap; /* current step data */ Map<SNode, SNode> currentToOriginalMap; SModel originalInputModel; SModel currentInputModel; SModel currentOutputModel; private TransientModelWithMetainfo myCachedModel; private int myMajorStep = -1; private int myMinorStep = -1; /* make data */ private Map<String, SNode> myUnchangedSet; private Map<String, SNode> myRequiredSet; public IncrementalDependenciesBuilder(SModel originalInputModel, @Nullable Map<String, String> generationHashes, String parametersHash, IntermediateCacheHelper cache) { this.originalInputModel = currentInputModel = originalInputModel; myParametersHash = parametersHash; myCache = cache; currentOutputModel = null; myModelHash = generationHashes == null ? null : generationHashes.get(GeneratableSModel.FILE); initData(getRoots(originalInputModel), generationHashes); } private void initData(SNode[] roots, Map<String, String> generationHashes) { myConditionalsBuilder = new RootDependenciesBuilder(null, this, generationHashes != null ? generationHashes.get(GeneratableSModel.HEADER) : ""); currentToOriginalMap = new HashMap<SNode, SNode>(roots.length * 3 / 2); myAllBuilders = new RootDependenciesBuilder[roots.length + 1]; int e = 0; myAllBuilders[e++] = myConditionalsBuilder; for (SNode root : roots) { myAllBuilders[e] = new RootDependenciesBuilder(root, this, generationHashes != null ? generationHashes.get(root.getNodeId().toString()) : null); myRootBuilders.put(root, myAllBuilders[e++]); currentToOriginalMap.put(root, root); } } public void traceDependencyOrigins() { if (!InternalFlag.isInternalMode()) return; myDependenciesTraces = new HashMap<String, String>(); } // there's nobody using dependency traces, and as long as incremental story needs complete refactoring, no reason to keep them @ToRemove(version = 3.2) void reportModelAccess(SModel model, SNode root) { if (myDependenciesTraces == null) return; String key = model.getReference().toString() + " in " + (root == null ? "common" : root.getNodeId().toString()); if (myDependenciesTraces.containsKey(key)) return; StringWriter stringWriter = new StringWriter(); final Throwable stackTrace = new Throwable(); stackTrace.printStackTrace(new PrintWriter(stringWriter)); final String v = stringWriter.toString(); myDependenciesTraces.put(key, v); if (myDependenciesTraces.containsKey(null)) { throw new IllegalStateException("Got null key in dep traces after adding key " + key, stackTrace); } if (!v.equals(myDependenciesTraces.get(key))) { throw new IllegalStateException("Mismatch in recorded value for key " + key, stackTrace); } } public void propagateDependencies(Set<SNode> unchangedRoots, Set<SNode> requiredRoots, boolean conditionalsUnchanged, boolean conditionalsRequired, GenerationDependencies saved) { myUnchangedSet = new HashMap<String, SNode>(unchangedRoots.size() + 1); myRequiredSet = new HashMap<String, SNode>(requiredRoots.size() + 1); for (SNode root : unchangedRoots) { propagateDependencies(getRootBuilder(root), saved.getDependenciesFor(root.getNodeId().toString()), false); } for (SNode root : requiredRoots) { propagateDependencies(getRootBuilder(root), saved.getDependenciesFor(root.getNodeId().toString()), true); } if (conditionalsUnchanged || conditionalsRequired) { propagateDependencies(getRootBuilder(null), saved.getDependenciesFor(GeneratableSModel.HEADER), conditionalsRequired); } } private void propagateDependencies(RootDependenciesBuilder builder, GenerationRootDependencies deps, boolean isRequired) { assert deps.getHash().equals(builder.getHash()); builder.loadDependencies(deps); SNode root = builder.getOriginalRoot(); (isRequired ? myRequiredSet : myUnchangedSet).put(root != null ? root.getNodeId().toString() : TransientModelWithMetainfo.CONDITIONALS_ID, root); } private static SNode[] getRoots(SModel model) { Collection<SNode> collection = IterableUtil.asCollection(model.getRootNodes()); return collection.toArray(new SNode[collection.size()]); } @Override public void scriptApplied(SModel newmodel) { Map<SNodeId, SNode> oldidsToOriginal = new HashMap<SNodeId, SNode>(); for (Map.Entry<SNode, SNode> entry : currentToOriginalMap.entrySet()) { oldidsToOriginal.put(entry.getKey().getNodeId(), entry.getValue()); } currentToOriginalMap = new HashMap<SNode, SNode>(); for (SNode root : newmodel.getRootNodes()) { SNodeId id = root.getNodeId(); SNode original = oldidsToOriginal.get(id); if (original == null) { // TODO if original is null -> new root added, warning/error(strict)? LOG.debug("script created a new node"); } currentToOriginalMap.put(root, original); } currentInputModel = newmodel; currentOutputModel = null; } @Override public void registerRoot(SNode outputRoot, SNode inputNode) { // XXX in fact, not sure there's need to keep this map if I can use TracingUtil and userobjects with original input? if (nextStepToOriginalMap == null) { nextStepToOriginalMap = new HashMap<SNode, SNode>(); } if (inputNode == null) { nextStepToOriginalMap.put(outputRoot, null); return; } SNode originalRoot = currentToOriginalMap.get(inputNode.getContainingRoot()); nextStepToOriginalMap.put(outputRoot, originalRoot); } @Override public void rootReplaced(SNode oldOutputRoot, SNode newOutputRoot) { if (nextStepToOriginalMap != null && nextStepToOriginalMap.containsKey(oldOutputRoot)) { SNode original = nextStepToOriginalMap.remove(oldOutputRoot); nextStepToOriginalMap.put(newOutputRoot, original); } } @Override public void updateModel(SModel newInputModel) { if (nextStepToOriginalMap != null) { currentToOriginalMap = nextStepToOriginalMap; nextStepToOriginalMap = null; } else { currentToOriginalMap = new HashMap<SNode, SNode>(); } currentInputModel = newInputModel; currentOutputModel = null; } @Override public void dropModel() { nextStepToOriginalMap = null; currentOutputModel = null; } @Override public void setOutputModel(SModel model, int majorStep, int minorStep) { currentOutputModel = model; myMajorStep = majorStep; myMinorStep = minorStep; myCachedModel = null; } @Override public SNode getOriginalForOutput(SNode outputNode) { if (nextStepToOriginalMap == null) { return null; } return nextStepToOriginalMap.get(outputNode); } @Override public SNode getOriginalForInput(SNode inputNode) { if (currentToOriginalMap == null) { return null; } return currentToOriginalMap.get(inputNode); } @Override public RootDependenciesBuilder getRootBuilder(SNode inputNode) { if (inputNode == null || inputNode.getModel() == null) { return myConditionalsBuilder; } final SNode initial = inputNode; inputNode = inputNode.getContainingRoot(); SNode originalRoot = currentToOriginalMap.get(inputNode); if (originalRoot != null) { return myRootBuilders.get(originalRoot); } else if (currentToOriginalMap.containsKey(inputNode)) { return myConditionalsBuilder; } // shouldn't happen LOG.error("consistency problem in dependencies map", new IllegalStateException()); LOG.error("INPUT: " + SNodeOperations.getDebugText(inputNode)); if (initial != inputNode) { LOG.error("getRootBuilder invoked for: " + SNodeOperations.getDebugText(initial)); } LOG.error("current to original map:"); for (SNode n : currentToOriginalMap.keySet()) { final SNode o = currentToOriginalMap.get(n); LOG.error(String.format("%s --> %s", SNodeOperations.getDebugText(n), o == null ? String.valueOf(o) : SNodeOperations.getDebugText(o))); } return null; } @Override public GenerationDependencies getResult(IncrementalGenerationStrategy incrementalStrategy) { List<GenerationRootDependencies> unchanged = new ArrayList<GenerationRootDependencies>(); List<GenerationRootDependencies> rootDependencies = new ArrayList<GenerationRootDependencies>(myAllBuilders.length); fillRootDependencies(rootDependencies, unchanged); final Map<String, String> externalHashes = getGenerationHashes(rootDependencies, incrementalStrategy); return new GenerationDependencies(rootDependencies, myModelHash, myParametersHash, externalHashes, unchanged.isEmpty() ? Collections.<GenerationRootDependencies>emptyList() : unchanged, myUnchangedSet.size(), myRequiredSet.size(), myDependenciesTraces); } private void fillRootDependencies(List<GenerationRootDependencies> rootDependencies, List<GenerationRootDependencies> unchanged) { for (RootDependenciesBuilder l : myAllBuilders) { GenerationRootDependencies dep; if (l.isUnchanged()) { dep = l.getSavedDependencies(); unchanged.add(dep); } else { dep = GenerationRootDependencies.fromData(l); } rootDependencies.add(dep); } } private Map<String,String> getGenerationHashes(List<GenerationRootDependencies> rootDependencies, IncrementalGenerationStrategy incrementalStrategy) { Map<String, String> externalHashes = new HashMap<String, String>(); final SRepository repo = originalInputModel.getRepository(); for (GenerationRootDependencies dep : rootDependencies) { for (String modelReference : dep.getExternal()) { if (!externalHashes.containsKey(modelReference)) { SModel sm = PersistenceFacade.getInstance().createModelReference(modelReference).resolve(repo); Map<String, String> hashes = incrementalStrategy.getModelHashes(sm, null); String value = hashes != null ? hashes.get(GeneratableSModel.FILE) : null; externalHashes.put(modelReference, value); } } } return externalHashes; } /* working with cache */ private void loadCachedModel() throws BrokenCacheException { TransientModelWithMetainfo model = myCache.retrieve(myMajorStep, myMinorStep, currentOutputModel.getReference()); if (model == null) { throw new BrokenCacheException(currentOutputModel); } myCachedModel = model; } @Override public boolean isStepRequired() { return myCache.isStepCovered(myMajorStep, myMinorStep); } @Override public void reloadRequired(GeneratorMappings mappings) throws GenerationFailureException { if (myRequiredSet.isEmpty()) { return; } loadCachedModel(); List<SNode> toCopy = new ArrayList<SNode>(myRequiredSet.size() * 2 + 16); List<MappingsMemento> toImport = new ArrayList<MappingsMemento>(myRequiredSet.size() * 2); for (SNode root : myCachedModel.getRoots()) { String originalId = myCachedModel.getOriginal(root); if (myRequiredSet.containsKey(originalId)) { SNode originalRoot = myRequiredSet.get(originalId); if (nextStepToOriginalMap == null) { nextStepToOriginalMap = new HashMap<SNode, SNode>(); } nextStepToOriginalMap.put(root, originalRoot); toCopy.add(root); MappingsMemento val = myCachedModel.getMappingsMemento(originalId); if (val != null) { toImport.add(val); } } } for (SNode node : toCopy) { currentOutputModel.addRootNode(node); } for (MappingsMemento val : toImport) { mappings.importPersisted(val, currentInputModel, currentOutputModel); } } @Override public void updateUnchanged(TransientModelWithMetainfo model) throws GenerationFailureException { if (!myCache.hasCache() || myUnchangedSet.isEmpty() || currentOutputModel == null /* do not update after script */) { return; } if (myCachedModel == null) { loadCachedModel(); } for (SNode root : myCachedModel.getRoots()) { String originalId = myCachedModel.getOriginal(root); if (myUnchangedSet.containsKey(originalId)) { model.getRoots().add(root); model.setOriginal(root.getNodeId(), originalId); MappingsMemento mappingsMemento = myCachedModel.getMappingsMemento(originalId); if (mappingsMemento != null) { model.updateMappings(originalId, mappingsMemento); } } } } @Override public TransientModelWithMetainfo create(SModel model, GeneratorMappings mappings) throws GenerationFailureException { TransientModelWithMetainfo rv = TransientModelWithMetainfo.create(model, this); mappings.export(rv, this); return rv; } }