EMMA Coverage Report (generated Sun Feb 05 10:43:15 CET 2012)
[all classes][de.uka.ipd.sdq.dsexplore.analysis.lqn]

COVERAGE SUMMARY FOR SOURCE FILE [AbstractLQNAnalysis.java]

nameclass, %method, %block, %line, %
AbstractLQNAnalysis.java0%   (0/1)0%   (0/13)0%   (0/687)0%   (0/145)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class AbstractLQNAnalysis0%   (0/1)0%   (0/13)0%   (0/687)0%   (0/145)
<static initializer> 0%   (0/1)0%   (0/6)0%   (0/4)
AbstractLQNAnalysis (): void 0%   (0/1)0%   (0/31)0%   (0/7)
analyse (PCMPhenotype, IProgressMonitor): void 0%   (0/1)0%   (0/40)0%   (0/10)
canEvaluateAspect (EvaluationAspect, Dimension): boolean 0%   (0/1)0%   (0/6)0%   (0/1)
getCriterions (): List 0%   (0/1)0%   (0/11)0%   (0/3)
getQualityAttribute (): DSEConstantsContainer$QualityAttribute 0%   (0/1)0%   (0/4)0%   (0/1)
initialise (DSEWorkflowConfiguration): void 0%   (0/1)0%   (0/23)0%   (0/5)
initialiseCriteria (ILaunchConfiguration, List): void 0%   (0/1)0%   (0/372)0%   (0/73)
isFileNameBelongsToModel (String, LqnModelType): boolean 0%   (0/1)0%   (0/17)0%   (0/3)
launchLQNSolver (PCMPhenotype, IProgressMonitor): void 0%   (0/1)0%   (0/86)0%   (0/20)
retrieveLQNSolverResults (PCMPhenotype, PCMInstance, Criterion): ILQNResult 0%   (0/1)0%   (0/70)0%   (0/14)
retrieveResultsFor (PCMPhenotype, Criterion): IAnalysisResult 0%   (0/1)0%   (0/17)0%   (0/3)
setBlackboard (MDSDBlackboard): void 0%   (0/1)0%   (0/4)0%   (0/2)

1package de.uka.ipd.sdq.dsexplore.analysis.lqn;
2 
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.Iterator;
6import java.util.LinkedList;
7import java.util.List;
8import java.util.Map;
9 
10import org.apache.log4j.Logger;
11import org.eclipse.core.runtime.CoreException;
12import org.eclipse.core.runtime.IProgressMonitor;
13import org.eclipse.debug.core.ILaunchConfiguration;
14import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
15import org.eclipse.debug.core.ILaunchManager;
16import org.opt4j.core.Criterion;
17import org.opt4j.core.InfeasibilityConstraint;
18import org.opt4j.core.Objective;
19import org.opt4j.core.SatisfactionConstraint;
20 
21import LqnCore.LqnModelType;
22import de.uka.ipd.sdq.dsexplore.analysis.AnalysisFailedException;
23import de.uka.ipd.sdq.dsexplore.analysis.IAnalysis;
24import de.uka.ipd.sdq.dsexplore.analysis.IAnalysisResult;
25import de.uka.ipd.sdq.dsexplore.analysis.PCMPhenotype;
26import de.uka.ipd.sdq.dsexplore.launch.DSEWorkflowConfiguration;
27import de.uka.ipd.sdq.dsexplore.launch.DSEConstantsContainer.QualityAttribute;
28import de.uka.ipd.sdq.dsexplore.qml.contract.QMLContract.EvaluationAspect;
29import de.uka.ipd.sdq.dsexplore.qml.contracttype.QMLContractType.Dimension;
30import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.EvaluationAspectWithContext;
31import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.UsageScenarioBasedObjective;
32import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.builder.EntryLevelSystemCallInfeasibilityConstraintBuilder;
33import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.builder.EntryLevelSystemCallObjectiveBuilder;
34import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.builder.EntryLevelSystemCallSatisfactionConstraintBuilder;
35import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.builder.UsageScenarioBasedInfeasibilityConstraintBuilder;
36import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.builder.UsageScenarioBasedObjectiveBuilder;
37import de.uka.ipd.sdq.dsexplore.qml.pcm.datastructures.builder.UsageScenarioBasedSatisfactionConstraintBuilder;
38import de.uka.ipd.sdq.dsexplore.qml.pcm.reader.PCMDeclarationsReader;
39import de.uka.ipd.sdq.dsexplore.qml.profile.QMLProfile.EntryLevelSystemCallRequirement;
40import de.uka.ipd.sdq.dsexplore.qml.profile.QMLProfile.UsageScenarioRequirement;
41import de.uka.ipd.sdq.pcm.usagemodel.EntryLevelSystemCall;
42import de.uka.ipd.sdq.pcm.usagemodel.UsageModel;
43import de.uka.ipd.sdq.pcm.usagemodel.UsageScenario;
44import de.uka.ipd.sdq.pcmsolver.RunPCMAnalysisJob;
45import de.uka.ipd.sdq.pcmsolver.models.PCMInstance;
46import de.uka.ipd.sdq.pcmsolver.runconfig.MessageStrings;
47import de.uka.ipd.sdq.pcmsolver.runconfig.PCMSolverConfigurationBasedConfigBuilder;
48import de.uka.ipd.sdq.pcmsolver.runconfig.PCMSolverWorkflowRunConfiguration;
49import de.uka.ipd.sdq.pcmsolver.transformations.SolverStrategy;
50import de.uka.ipd.sdq.pcmsolver.transformations.pcm2lqn.LqnXmlHandler;
51import de.uka.ipd.sdq.pcmsolver.transformations.pcm2lqn.Pcm2LqnStrategy;
52import de.uka.ipd.sdq.workflow.exceptions.JobFailedException;
53import de.uka.ipd.sdq.workflow.exceptions.UserCanceledException;
54import de.uka.ipd.sdq.workflow.launchconfig.AbstractWorkflowConfigurationBuilder;
55import de.uka.ipd.sdq.workflow.mdsd.blackboard.MDSDBlackboard;
56import de.uka.ipd.sdq.workflow.pcm.blackboard.PCMResourceSetPartition;
57import de.uka.ipd.sdq.workflow.pcm.configurations.PCMWorkflowConfigurationBuilder;
58import de.uka.ipd.sdq.workflow.pcm.jobs.LoadPCMModelsIntoBlackboardJob;
59 
60public abstract class AbstractLQNAnalysis implements IAnalysis {
61 
62        /** Logger for log4j. */
63        protected static Logger logger = 
64                Logger.getLogger("de.uka.ipd.sdq.dsexplore.analysis.lqn.LQNSolverAnalysis");
65        
66        
67        /**
68         * Store the launch parameters so that we do not have to pass them all the
69         * time.
70         */
71        private ILaunchConfiguration config;
72        
73        protected int iteration = -1;
74 
75        private MDSDBlackboard blackboard;
76 
77        protected LQNQualityAttributeDeclaration lQNQualityAttribute = new LQNQualityAttributeDeclaration();
78        
79        //Criteria handling
80        private List<Criterion> criteriaList = new ArrayList<Criterion>();
81        protected Map<Criterion, EvaluationAspectWithContext> criterionToAspect = new HashMap<Criterion, EvaluationAspectWithContext>(); //This is needed to determine, what THE result is (Mean,  Variance, ...)
82        
83 
84        private Map<Long, String> previousResultFileName = new HashMap<Long, String>();
85        
86        // cache for LQN models so that they do not have to be loaded so often. Limited capacity (below) to not store too many models. 
87        private LinkedList<LqnModelType> recentModels = new LinkedList<LqnModelType>();
88        private static int RECENT_MODEL_CAPACITY = 10;
89        
90        /**
91         * {@inheritDoc}
92         * @throws UserCanceledException 
93         */
94        public void analyse(PCMPhenotype pheno, IProgressMonitor monitor)
95                        throws AnalysisFailedException, CoreException, UserCanceledException {
96                
97                ILaunchConfigurationWorkingCopy wcopy = this.config.getWorkingCopy();
98                wcopy.setAttribute(MessageStrings.SOLVER,
99                                this.getSolverMessageString());
100                this.config = wcopy.doSave();
101                
102                iteration++;
103                
104                PCMInstance pcm = new PCMInstance((PCMResourceSetPartition)this.blackboard.getPartition(LoadPCMModelsIntoBlackboardJob.PCM_MODELS_PARTITION_ID));
105                
106                try {
107                        launchLQNSolver(pheno, monitor);
108                        //IAnalysisResult result = retrieveLQNSolverResults(pcm);
109                        //return result;
110                } catch (RuntimeException e){
111                        handleException(e, pcm);
112                }
113                
114                
115        }
116        
117        /**
118         * FIXME: Make this method independent of the blackboard state.  
119         */
120        public IAnalysisResult retrieveResultsFor(PCMPhenotype pheno, Criterion criterion) throws AnalysisFailedException{
121                PCMInstance pcm = new PCMInstance((PCMResourceSetPartition)this.blackboard.getPartition(LoadPCMModelsIntoBlackboardJob.PCM_MODELS_PARTITION_ID));
122                IAnalysisResult result = retrieveLQNSolverResults(pheno, pcm,criterion);
123                return result;
124        }
125        
126        /**
127         * try to handle the exception or throw it back. 
128         * @param e
129         * @param pcm 
130         * @return
131         */
132        protected abstract IAnalysisResult handleException(RuntimeException e, PCMInstance pcm);
133 
134        protected abstract String getSolverMessageString();
135 
136        ILQNResult retrieveLQNSolverResults(PCMPhenotype pheno, PCMInstance pcm, Criterion criterion) throws AnalysisFailedException {
137                
138                String xmlFileName = this.previousResultFileName.get(pheno.getNumericID());
139                
140                // check recent models if the one is in there
141                LqnModelType model =  null;
142                for (LqnModelType recentModel : this.recentModels) {
143                        if (isFileNameBelongsToModel(xmlFileName, recentModel)){
144                                model = recentModel;
145                        }
146                }
147                
148                // if not, read in from file. 
149                if (model == null){
150                        // Read XML output file generated by LQNSolver
151                        model =  LqnXmlHandler.loadModelFromXMI(xmlFileName);
152                }
153                                
154                if (model == null){
155                        throw new AnalysisFailedException("LQN model "+xmlFileName+" could not be loaded. See previous logging entries for details.");
156                }
157                
158                // delete the oldest model from the recent model list; and add the current one. 
159                if (this.recentModels.size() >= RECENT_MODEL_CAPACITY){
160                        this.recentModels.pollLast();
161                }
162                
163                this.recentModels.push(model);
164                
165                ILQNResult result = retrieveResult(pcm, model, criterion);
166                return result;        
167                
168        }
169 
170        private boolean isFileNameBelongsToModel(String xmlFileName,
171                        LqnModelType recentModel) {
172                String modelString = recentModel.eResource().getURI().toString();
173                modelString = modelString.substring(modelString.lastIndexOf("/")+1);
174                return xmlFileName.contains(modelString);
175        }
176 
177        protected abstract ILQNResult retrieveResult(PCMInstance pcm,
178                        LqnModelType model, Criterion criterion) throws AnalysisFailedException;
179 
180        /**
181         * Launches the LQN Solver.
182         * @param monitor 
183         * 
184         * @param pcmInstance the instance of PCM
185         * @return 
186         * @throws AnalysisFailedException 
187         * @throws CoreException 
188         * @throws UserCanceledException 
189         */
190        private void  launchLQNSolver(PCMPhenotype pheno, IProgressMonitor monitor)
191                        throws AnalysisFailedException, CoreException, UserCanceledException {
192        
193                if (monitor == null){
194                        throw new AnalysisFailedException(this.getClass().getName()+" was not correctly initialised.");
195                }
196 
197 
198                PCMSolverWorkflowRunConfiguration solverConfiguration = new PCMSolverWorkflowRunConfiguration();
199                AbstractWorkflowConfigurationBuilder builder;
200 
201                builder = new PCMWorkflowConfigurationBuilder(this.config, ILaunchManager.RUN_MODE);
202                builder.fillConfiguration(solverConfiguration);
203 
204                builder = new PCMSolverConfigurationBasedConfigBuilder(this.config,
205                                ILaunchManager.RUN_MODE);
206                builder.fillConfiguration(solverConfiguration);
207                solverConfiguration.setInteractive(false);
208                
209                
210                // Create a new Analysis job
211                RunPCMAnalysisJob solverJob = new RunPCMAnalysisJob(solverConfiguration);
212 
213                solverJob.setBlackboard(blackboard);
214                SolverStrategy strategy = solverJob.getStrategy();
215                if (strategy instanceof Pcm2LqnStrategy){
216                        this.previousResultFileName.put(pheno.getNumericID(), ((Pcm2LqnStrategy)strategy).getFilenameResultXML());
217                }
218 
219                try {
220                        
221                        //TODO catch exceptions due to convergence problems and handle them nicely. For example, set the response time to MAXINT or similar.
222                        
223                        //execute the job
224                        solverJob.execute(monitor);
225                        
226                        logger.debug("Finished PCMSolver LQN analysis");
227                        
228                } catch (JobFailedException e) {  
229                        logger.error(e.getMessage());
230                        throw new AnalysisFailedException(e);
231                } 
232                
233                
234        }
235 
236        /**
237         * {@inheritDoc}
238         * @throws CoreException 
239         * @see de.uka.ipd.sdq.dsexplore.analysis.IAnalysis#initialise(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
240         */
241        @Override
242        public void initialise(DSEWorkflowConfiguration configuration) throws CoreException {
243                this.config = configuration.getRawConfiguration();
244                
245                PCMInstance pcmInstance = new PCMInstance((PCMResourceSetPartition)this.blackboard.getPartition(LoadPCMModelsIntoBlackboardJob.PCM_MODELS_PARTITION_ID));
246                List<UsageScenario> scenarios = pcmInstance.getUsageModel().getUsageScenario_UsageModel();
247                
248                initialiseCriteria(this.config, scenarios);
249 
250//                this.objectives = new ArrayList<Objective>(scenarios.size());
251//                for (UsageScenario usageScenario : scenarios) {
252//                        //FIXME: hardcoded usage scenario selection
253//                        String scenName = usageScenario.getEntityName();
254//                        if (!scenName.contains("AlarmRetrieve") 
255//                                        && !scenName.contains("Wrapper")
256//                                        && !scenName.contains("HistoryRetrieve")){
257//                                objectives.add(new UsageScenarioBasedObjective(this.getQualityAttribute(), Objective.Sign.MIN, usageScenario));
258//                        }
259//                }
260 
261 
262        }
263        
264        private void initialiseCriteria(ILaunchConfiguration configuration, List<UsageScenario> scenarios) throws CoreException{
265                PCMInstance pcm = new PCMInstance((PCMResourceSetPartition)this.blackboard.getPartition(LoadPCMModelsIntoBlackboardJob.PCM_MODELS_PARTITION_ID));
266                UsageModel usageModel = pcm.getUsageModel();
267                
268                PCMDeclarationsReader reader = new PCMDeclarationsReader( 
269                                configuration.getAttribute("qmlDefinitionFile", ""));
270                
271                List<Dimension> dimensions = this.lQNQualityAttribute.getDimensions();
272                
273                List<EvaluationAspectWithContext> responseTimeAspect = new ArrayList<EvaluationAspectWithContext>(6);
274                for (Dimension dimension : dimensions) {
275                        responseTimeAspect.addAll(reader.getDimensionConstraintContextsForUsageModel(usageModel, dimension.getId()));
276                        responseTimeAspect.addAll(reader.getDimensionObjectiveContextsForUsageModel(usageModel, dimension.getId()));
277                }
278                
279                //Check constraint aspects and create Constraint-Objects for every Aspect
280                for (Iterator<EvaluationAspectWithContext> iterator = responseTimeAspect.iterator(); iterator.hasNext();) {
281                        EvaluationAspectWithContext aspectContext = iterator
282                                        .next();
283                        //handle possible aspects here
284                        if (canEvaluateAspect(aspectContext.getEvaluationAspect(), aspectContext.getDimension())) {
285 
286                                if(aspectContext.getRequirement() instanceof UsageScenarioRequirement) {  
287 
288                                        if(((UsageScenarioRequirement)aspectContext.getRequirement()).getUsageScenario() == null) {
289                                                //The criterion refers to EVERY US since none is explicitly specified
290                                                for (Iterator<UsageScenario> iterator2 = scenarios.iterator(); iterator2.hasNext();) {
291                                                        UsageScenario usageScenario = (UsageScenario) iterator2
292                                                        .next();
293 
294                                                        //FIXME: hardcoded usage scenario selection. Delete after a while
295                                                        String scenName = usageScenario.getEntityName();
296                                                        if (scenName.contains("AlarmRetrieve") 
297                                                                        || scenName.contains("Wrapper")
298                                                                        || scenName.contains("HistoryRetrieve")){
299                                                                // continue;
300                                                                logger.warn("ABB usage scenarios encountered that used to be ignored, but this special case has been removed again. Check whether this is ok.");
301                                                        }
302 
303                                                        if(aspectContext.getCriterion() instanceof de.uka.ipd.sdq.dsexplore.qml.contract.QMLContract.Constraint) {
304                                                                UsageScenarioBasedInfeasibilityConstraintBuilder builder = new UsageScenarioBasedInfeasibilityConstraintBuilder(usageScenario);
305                                                                InfeasibilityConstraint c = 
306                                                                        reader.translateEvalAspectToInfeasibilityConstraint(aspectContext, builder);
307 
308                                                                criteriaList.add(c);
309                                                                criterionToAspect.put(c, aspectContext);
310                                                        } else {
311                                                                //instanceof Objective
312                                                                UsageScenarioBasedObjectiveBuilder objectiveBuilder = new UsageScenarioBasedObjectiveBuilder(usageScenario); 
313                                                                Objective o = reader.translateEvalAspectToObjective(this.getQualityAttribute().getName(), aspectContext, objectiveBuilder);
314                                                                criteriaList.add(o);
315                                                                criterionToAspect.put(o, aspectContext); 
316 
317                                                                UsageScenarioBasedSatisfactionConstraintBuilder builder = new UsageScenarioBasedSatisfactionConstraintBuilder(usageScenario);
318                                                                SatisfactionConstraint c = 
319                                                                        reader.translateEvalAspectToSatisfactionConstraint(aspectContext, o, builder); 
320                                                                criteriaList.add(c);
321                                                                criterionToAspect.put(c, aspectContext);
322                                                        }
323                                                }
324                                        } else {
325                                                if(aspectContext.getCriterion() instanceof de.uka.ipd.sdq.dsexplore.qml.contract.QMLContract.Constraint) {
326                                                        UsageScenarioBasedInfeasibilityConstraintBuilder builder = new UsageScenarioBasedInfeasibilityConstraintBuilder(((UsageScenarioRequirement)aspectContext.getRequirement()).getUsageScenario());
327                                                        
328                                                        InfeasibilityConstraint c = 
329                                                                reader.translateEvalAspectToInfeasibilityConstraint(aspectContext, builder);
330                                                        criteriaList.add(c);
331                                                        criterionToAspect.put(c, aspectContext);
332                                                } else {
333                                                        //instanceof Objective
334                                                        UsageScenarioBasedObjectiveBuilder objectiveBuilder = new UsageScenarioBasedObjectiveBuilder(((UsageScenarioRequirement)aspectContext.getRequirement()).getUsageScenario());
335                                                        Objective o = reader.translateEvalAspectToObjective(this.getQualityAttribute().getName(), aspectContext, objectiveBuilder);
336                                                        criteriaList.add(o);
337                                                        criterionToAspect.put(o, aspectContext);
338 
339                                                        UsageScenarioBasedSatisfactionConstraintBuilder builder = new UsageScenarioBasedSatisfactionConstraintBuilder(((UsageScenarioRequirement)aspectContext.getRequirement()).getUsageScenario());
340                                                        
341                                                        SatisfactionConstraint c = 
342                                                                reader.translateEvalAspectToSatisfactionConstraint(aspectContext, o, builder);
343                                                        criteriaList.add(c);
344                                                        criterionToAspect.put(c, aspectContext);
345                                                }
346                                        }
347 
348                                } else if (aspectContext.getRequirement() instanceof EntryLevelSystemCallRequirement) {
349                                        if(aspectContext.getCriterion() instanceof de.uka.ipd.sdq.dsexplore.qml.contract.QMLContract.Constraint) {
350                                                EntryLevelSystemCallInfeasibilityConstraintBuilder builder = new EntryLevelSystemCallInfeasibilityConstraintBuilder(((EntryLevelSystemCallRequirement)aspectContext.getRequirement()).getEntryLevelSystemCall());
351                                                InfeasibilityConstraint c = 
352                                                        reader.translateEvalAspectToInfeasibilityConstraint(aspectContext, builder);
353                                                criteriaList.add(c);
354                                                criterionToAspect.put(c, aspectContext);
355                                        } else {
356                                                //instanceof Objective
357                                                EntryLevelSystemCall entryLevelSystemCall = ((EntryLevelSystemCallRequirement)aspectContext.getRequirement()).getEntryLevelSystemCall();
358                                                EntryLevelSystemCallObjectiveBuilder builder = new EntryLevelSystemCallObjectiveBuilder(entryLevelSystemCall);
359                                                
360                                                Objective o = reader.translateEvalAspectToObjective(this.getQualityAttribute().getName(), aspectContext, builder);
361                                                criteriaList.add(o);
362                                                criterionToAspect.put(o, aspectContext);
363 
364                                                EntryLevelSystemCallSatisfactionConstraintBuilder satisBuilder = new EntryLevelSystemCallSatisfactionConstraintBuilder(entryLevelSystemCall);
365                                                SatisfactionConstraint c = 
366                                                        reader.translateEvalAspectToSatisfactionConstraint(aspectContext, o, satisBuilder);
367                                                criteriaList.add(c);
368                                                criterionToAspect.put(c, aspectContext);
369                                        }
370 
371                                } else {
372                                        throw new RuntimeException("Unsupported Requirement!");
373                                }
374                        } else {
375                                //XXX: This should never be the case if the optimization is started with the LaunchConfig the aspect is checked there as well
376                                throw new RuntimeException("Evaluation aspect not supported("+aspectContext.getEvaluationAspect()+")!");
377                        }
378                }
379        }
380        
381        private boolean canEvaluateAspect(EvaluationAspect aspect, Dimension dimension){
382                return lQNQualityAttribute.canEvaluateAspectForDimension(aspect, dimension);
383        }
384        
385        //MOVED to PCMDeclarationsReader
386//        public UsageScenarioBasedObjective translateEvalAspectToObjective(EvaluationAspectWithContext aspect, UsageScenario usageScenario){
387//                //Make sure, the aspect IS an objective
388//                try {
389//                        if(aspect.getDimension().getType().getRelationSemantics().getRelSem() == EnumRelationSemantics.DECREASING) {
390//                                return new UsageScenarioBasedObjective(this.getQualityAttribute(), Objective.Sign.MIN, usageScenario);
391//                        } else {
392//                                //INCREASING
393//                                return new UsageScenarioBasedObjective(this.getQualityAttribute(), Objective.Sign.MAX, usageScenario);
394//                        }
395//                } catch (CoreException e) {
396//                        e.printStackTrace();
397//                        throw new RuntimeException("Could not get response time quality attribute!");
398//                }
399//        }
400        
401        public QualityAttribute getQualityAttribute() throws CoreException {
402                //return DSEConstantsContainer.MEAN_RESPONSE_TIME_QUALITY;
403                return lQNQualityAttribute.getQualityAttribute();
404        }
405 
406        public abstract boolean hasStatisticResults() throws CoreException;
407        
408        @Override
409        public List<Criterion> getCriterions() throws CoreException {
410                List<Criterion> list = new ArrayList<Criterion>();
411                list.addAll(this.criteriaList);
412                return list;
413        }
414        
415        @Override
416        public void setBlackboard(MDSDBlackboard blackboard){
417                this.blackboard = blackboard;
418        }
419 
420}

[all classes][de.uka.ipd.sdq.dsexplore.analysis.lqn]
EMMA 2.0.9414 (unsupported private build) (C) Vladimir Roubtsov