diff options
Diffstat (limited to 'Tests/hu.bme.mit.inf.dslreasoner.run/src/hu/bme/mit/inf/dslreasoner/run/script/MeasurementScriptRunner.xtend')
-rw-r--r-- | Tests/hu.bme.mit.inf.dslreasoner.run/src/hu/bme/mit/inf/dslreasoner/run/script/MeasurementScriptRunner.xtend | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/Tests/hu.bme.mit.inf.dslreasoner.run/src/hu/bme/mit/inf/dslreasoner/run/script/MeasurementScriptRunner.xtend b/Tests/hu.bme.mit.inf.dslreasoner.run/src/hu/bme/mit/inf/dslreasoner/run/script/MeasurementScriptRunner.xtend new file mode 100644 index 00000000..973c3d13 --- /dev/null +++ b/Tests/hu.bme.mit.inf.dslreasoner.run/src/hu/bme/mit/inf/dslreasoner/run/script/MeasurementScriptRunner.xtend | |||
@@ -0,0 +1,400 @@ | |||
1 | package hu.bme.mit.inf.dslreasoner.run.script | ||
2 | |||
3 | import com.google.gson.Gson | ||
4 | import hu.bme.mit.inf.dlsreasoner.alloy.reasoner.AlloySolver | ||
5 | import hu.bme.mit.inf.dlsreasoner.alloy.reasoner.AlloySolverConfiguration | ||
6 | import hu.bme.mit.inf.dslreasoner.ecore2logic.EClassMapper | ||
7 | import hu.bme.mit.inf.dslreasoner.ecore2logic.Ecore2Logic | ||
8 | import hu.bme.mit.inf.dslreasoner.ecore2logic.Ecore2LogicConfiguration | ||
9 | import hu.bme.mit.inf.dslreasoner.ecore2logic.Ecore2Logic_Trace | ||
10 | import hu.bme.mit.inf.dslreasoner.logic.model.builder.DocumentationLevel | ||
11 | import hu.bme.mit.inf.dslreasoner.logic.model.builder.LogicSolverConfiguration | ||
12 | import hu.bme.mit.inf.dslreasoner.logic.model.builder.TypeScopes | ||
13 | import hu.bme.mit.inf.dslreasoner.logic.model.logiclanguage.DefinedElement | ||
14 | import hu.bme.mit.inf.dslreasoner.logic.model.logiclanguage.IntLiteral | ||
15 | import hu.bme.mit.inf.dslreasoner.logic.model.logiclanguage.RealLiteral | ||
16 | import hu.bme.mit.inf.dslreasoner.logic.model.logiclanguage.StringLiteral | ||
17 | import hu.bme.mit.inf.dslreasoner.logic.model.logiclanguage.Type | ||
18 | import hu.bme.mit.inf.dslreasoner.logic.model.logiclanguage.TypeDefinition | ||
19 | import hu.bme.mit.inf.dslreasoner.logic.model.logicproblem.LogicProblem | ||
20 | import hu.bme.mit.inf.dslreasoner.logic.model.logicresult.IntStatisticEntry | ||
21 | import hu.bme.mit.inf.dslreasoner.logic.model.logicresult.LogicresultFactory | ||
22 | import hu.bme.mit.inf.dslreasoner.logic.model.logicresult.ModelResult | ||
23 | import hu.bme.mit.inf.dslreasoner.logic.model.logicresult.RealStatisticEntry | ||
24 | import hu.bme.mit.inf.dslreasoner.logic.model.logicresult.Statistics | ||
25 | import hu.bme.mit.inf.dslreasoner.logic.model.logicresult.StringStatisticEntry | ||
26 | import hu.bme.mit.inf.dslreasoner.logic2ecore.Logic2Ecore | ||
27 | import hu.bme.mit.inf.dslreasoner.run.EcoreLoader | ||
28 | import hu.bme.mit.inf.dslreasoner.run.FAMLoader | ||
29 | import hu.bme.mit.inf.dslreasoner.run.FileSystemLoader | ||
30 | import hu.bme.mit.inf.dslreasoner.run.MetamodelLoader | ||
31 | import hu.bme.mit.inf.dslreasoner.run.SatelliteLoader | ||
32 | import hu.bme.mit.inf.dslreasoner.run.YakinduLoader | ||
33 | import hu.bme.mit.inf.dslreasoner.util.CollectionsUtil | ||
34 | import hu.bme.mit.inf.dslreasoner.viatra2logic.Viatra2Logic | ||
35 | import hu.bme.mit.inf.dslreasoner.viatra2logic.Viatra2LogicConfiguration | ||
36 | import hu.bme.mit.inf.dslreasoner.viatrasolver.logic2viatra.cardinality.PolyhedralScopePropagatorConstraints | ||
37 | import hu.bme.mit.inf.dslreasoner.viatrasolver.logic2viatra.cardinality.PolyhedralScopePropagatorSolver | ||
38 | import hu.bme.mit.inf.dslreasoner.viatrasolver.logic2viatra.cardinality.ScopePropagatorStrategy | ||
39 | import hu.bme.mit.inf.dslreasoner.viatrasolver.partialinterpretation2logic.InstanceModel2Logic | ||
40 | import hu.bme.mit.inf.dslreasoner.viatrasolver.partialinterpretationlanguage.partial2logicannotations.PartialModelRelation2Assertion | ||
41 | import hu.bme.mit.inf.dslreasoner.viatrasolver.reasoner.ViatraReasoner | ||
42 | import hu.bme.mit.inf.dslreasoner.viatrasolver.reasoner.ViatraReasonerConfiguration | ||
43 | import hu.bme.mit.inf.dslreasoner.workspace.FileSystemWorkspace | ||
44 | import hu.bme.mit.inf.dslreasoner.workspace.ReasonerWorkspace | ||
45 | import java.io.FileReader | ||
46 | import java.util.HashMap | ||
47 | import java.util.HashSet | ||
48 | import java.util.Map | ||
49 | import java.util.Set | ||
50 | import org.eclipse.emf.ecore.EObject | ||
51 | import org.eclipse.emf.ecore.resource.Resource | ||
52 | import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl | ||
53 | import org.eclipse.viatra.query.patternlanguage.emf.EMFPatternLanguageStandaloneSetup | ||
54 | import org.eclipse.viatra.query.runtime.api.ViatraQueryEngineOptions | ||
55 | import org.eclipse.viatra.query.runtime.rete.matcher.ReteBackendFactory | ||
56 | import org.eclipse.xtend.lib.annotations.Data | ||
57 | import hu.bme.mit.inf.dlsreasoner.alloy.reasoner.AlloyBackendSolver | ||
58 | |||
59 | class MeasurementScriptRunner { | ||
60 | static val MODEL_SIZE_GAP = 0 | ||
61 | static val SCOPE_PROPAGATOR_TIMEOUT = 10 | ||
62 | static val USEC_TO_MSEC = 1000000 | ||
63 | |||
64 | static extension val LogicresultFactory = LogicresultFactory.eINSTANCE | ||
65 | |||
66 | val MeasurementScript script | ||
67 | val ReasonerWorkspace inputWorkspace | ||
68 | val ReasonerWorkspace outputWorkspace | ||
69 | val MetamodelLoader metamodelLoader | ||
70 | |||
71 | new(MeasurementScript script) { | ||
72 | this.script = script | ||
73 | inputWorkspace = new FileSystemWorkspace(script.inputPath + "/", "") | ||
74 | outputWorkspace = new FileSystemWorkspace(script.outputPath + | ||
75 | "/", '''«script.domain»_«script.solver»_«script.scope»_«script.scopePropagator ?: "na"»_«script.propagatedConstraints ?: "na"»_«script.polyhedronSolver ?: "na"»_«script.scopeHeuristic ?: "na"»_''') | ||
76 | metamodelLoader = switch (script.domain) { | ||
77 | case fs: new FileSystemLoader(inputWorkspace) | ||
78 | case ecore: new EcoreLoader(inputWorkspace) | ||
79 | case ecoreUnsat: new EcoreLoader(inputWorkspace, false) | ||
80 | case Yakindu: new YakinduLoader(inputWorkspace) => [useSynchronization = false; useComplexStates = true] | ||
81 | case YakinduUnsat: new YakinduLoader(inputWorkspace, false) => [useSynchronization = false; useComplexStates = true] | ||
82 | case FAM: new FAMLoader(inputWorkspace) | ||
83 | case satellite: new SatelliteLoader(inputWorkspace) | ||
84 | case satelliteUnsat: new SatelliteLoader(inputWorkspace, false) | ||
85 | default: throw new IllegalArgumentException("Unsupported domain: " + script.domain) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | def run() { | ||
90 | if (script.sizes.empty) { | ||
91 | return | ||
92 | } | ||
93 | val start = System.currentTimeMillis | ||
94 | val warmupSize = script.sizes.head | ||
95 | for (var int i = 0; i < script.warmupIterations; i++) { | ||
96 | System.err.println('''Warmup «i + 1»/«script.warmupIterations»...''') | ||
97 | runExperiment(warmupSize) | ||
98 | } | ||
99 | val warmupEnd = System.currentTimeMillis | ||
100 | System.err.println('''Warmup completed in «(warmupEnd - start) / 1000» seconds''') | ||
101 | for (size : script.sizes) { | ||
102 | var int failures = 0 | ||
103 | for (var int i = 0; i < script.iterations; i++) { | ||
104 | System.err.println("Running GC...") | ||
105 | runGc() | ||
106 | System.err.println('''Iteration «i + 1»/«script.iterations» of size «size»...''') | ||
107 | val startTime = System.currentTimeMillis | ||
108 | val result = runExperiment(size) | ||
109 | val headerPrefix = '''«script.toCsvHeader»,«size»,«i + 1»,«result.resultName»''' | ||
110 | println('''«headerPrefix»,startTime,«startTime»''') | ||
111 | println('''«headerPrefix»,logic2SolverTransformationTime,«result.statistics.transformationTime»''') | ||
112 | println('''«headerPrefix»,solverTime,«result.statistics.solverTime»''') | ||
113 | for (statistic : result.statistics.entries) { | ||
114 | val valueString = switch (statistic) { | ||
115 | IntStatisticEntry: statistic.value.toString | ||
116 | RealStatisticEntry: statistic.value.toString | ||
117 | StringStatisticEntry: statistic.value.toString | ||
118 | default: statistic.toString | ||
119 | } | ||
120 | println('''«headerPrefix»,«statistic.name»,«valueString»''') | ||
121 | } | ||
122 | if (script.saveModels && result.model !== null) { | ||
123 | outputWorkspace.writeModel(result.model, '''«size»_«i + 1».xmi''') | ||
124 | } | ||
125 | if (result.resultName === "InsuficientResourcesResultImpl") { | ||
126 | failures++ | ||
127 | } | ||
128 | System.out.flush | ||
129 | } | ||
130 | if (failures == script.iterations) { | ||
131 | System.err.println("All measurements failed") | ||
132 | return | ||
133 | } | ||
134 | } | ||
135 | val end = System.currentTimeMillis | ||
136 | System.err.println('''Measurement completed in «(end - start) / 1000» seconds''') | ||
137 | } | ||
138 | |||
139 | private static def void runGc() { | ||
140 | System.gc | ||
141 | Thread.sleep(100) | ||
142 | System.gc | ||
143 | Thread.sleep(100) | ||
144 | System.gc | ||
145 | Thread.sleep(800) | ||
146 | } | ||
147 | |||
148 | private def createViatraConfig() { | ||
149 | val config = new ViatraReasonerConfiguration | ||
150 | config.debugConfiguration.partialInterpretatioVisualiser = null | ||
151 | config.searchSpaceConstraints.additionalGlobalConstraints += metamodelLoader.additionalConstraints | ||
152 | config.scopePropagatorStrategy = switch (script.scopePropagator) { | ||
153 | case none: | ||
154 | ScopePropagatorStrategy.None | ||
155 | case basic: | ||
156 | switch (script.propagatedConstraints) { | ||
157 | case none: | ||
158 | ScopePropagatorStrategy.Basic | ||
159 | case typeHierarchy: | ||
160 | ScopePropagatorStrategy.BasicTypeHierarchy | ||
161 | case relations, | ||
162 | case hints: | ||
163 | throw new IllegalArgumentException( | ||
164 | "Basic scope propagator does not support relational and hint constraints") | ||
165 | default: | ||
166 | throw new IllegalArgumentException("Unknown scope constraints: " + script.propagatedConstraints) | ||
167 | } | ||
168 | case polyhedral: { | ||
169 | val constraints = switch (script.propagatedConstraints) { | ||
170 | case none: | ||
171 | throw new IllegalArgumentException( | ||
172 | "Polyhedral scope propagator needs at least type hierarchy constraints") | ||
173 | case typeHierarchy: | ||
174 | PolyhedralScopePropagatorConstraints.TypeHierarchy | ||
175 | case relations, | ||
176 | case hints: | ||
177 | PolyhedralScopePropagatorConstraints.Relational | ||
178 | default: | ||
179 | throw new IllegalArgumentException("Unknown scope constraints: " + script.propagatedConstraints) | ||
180 | } | ||
181 | val polyhedronSolver = switch (script.polyhedronSolver) { | ||
182 | case Z3Integer: PolyhedralScopePropagatorSolver.Z3Integer | ||
183 | case Z3Real: PolyhedralScopePropagatorSolver.Z3Real | ||
184 | case Cbc: PolyhedralScopePropagatorSolver.Cbc | ||
185 | case Clp: PolyhedralScopePropagatorSolver.Clp | ||
186 | default: throw new IllegalArgumentException("Unknown polyhedron solver: " + script.polyhedronSolver) | ||
187 | } | ||
188 | val updateHeuristic = script.scopeHeuristic != ScopeHeuristic.basic | ||
189 | new ScopePropagatorStrategy.Polyhedral(constraints, polyhedronSolver, updateHeuristic, | ||
190 | SCOPE_PROPAGATOR_TIMEOUT) | ||
191 | } | ||
192 | default: | ||
193 | throw new IllegalArgumentException("Unknown scope propagator: " + script.scopePropagator) | ||
194 | } | ||
195 | config | ||
196 | } | ||
197 | |||
198 | private def createAlloyConfig(AlloyBackendSolver backendSolver) { | ||
199 | val config = new AlloySolverConfiguration | ||
200 | config.solver = backendSolver | ||
201 | config | ||
202 | } | ||
203 | |||
204 | private def createConfig(int modelSize) { | ||
205 | val config = switch (solver : script.solver) { | ||
206 | case ViatraSolver: createViatraConfig() | ||
207 | case AlloySolver: createAlloyConfig(AlloyBackendSolver.SAT4J) | ||
208 | case AlloyMiniSat: createAlloyConfig(AlloyBackendSolver.MiniSatJNI) | ||
209 | default: throw new IllegalArgumentException("Unknown solver: " + solver) | ||
210 | } | ||
211 | config.solutionScope.numberOfRequiredSolutions = 1 | ||
212 | config.runtimeLimit = script.timeout | ||
213 | config.documentationLevel = if(script.saveTemporaryFiles) DocumentationLevel.NORMAL else DocumentationLevel.NONE | ||
214 | config | ||
215 | } | ||
216 | |||
217 | private def runExperiment(int modelSize) { | ||
218 | val config = createConfig(modelSize) | ||
219 | val modelLoadingStart = System.nanoTime | ||
220 | val metamodelDescriptor = metamodelLoader.loadMetamodel | ||
221 | val partialModelDescriptor = metamodelLoader.loadPartialModel | ||
222 | val queryDescriptor = metamodelLoader.loadQueries(metamodelDescriptor) | ||
223 | val modelLoadingTime = System.nanoTime - modelLoadingStart | ||
224 | |||
225 | val domain2LogicTransformationStart = System.nanoTime | ||
226 | val Ecore2Logic ecore2Logic = new Ecore2Logic | ||
227 | val Viatra2Logic viatra2Logic = new Viatra2Logic(ecore2Logic) | ||
228 | val InstanceModel2Logic instanceModel2Logic = new InstanceModel2Logic | ||
229 | var modelGeneration = ecore2Logic.transformMetamodel(metamodelDescriptor, new Ecore2LogicConfiguration()) | ||
230 | var problem = modelGeneration.output | ||
231 | problem = instanceModel2Logic.transform( | ||
232 | modelGeneration, | ||
233 | partialModelDescriptor | ||
234 | ).output | ||
235 | problem = viatra2Logic.transformQueries( | ||
236 | queryDescriptor, | ||
237 | modelGeneration, | ||
238 | new Viatra2LogicConfiguration | ||
239 | ).output | ||
240 | initializeScope(config, modelSize, problem, ecore2Logic, modelGeneration.trace) | ||
241 | if (config instanceof ViatraReasonerConfiguration && script.propagatedConstraints == ScopeConstraints.hints) { | ||
242 | (config as ViatraReasonerConfiguration).hints = metamodelLoader.getHints(ecore2Logic, modelGeneration.trace) | ||
243 | } | ||
244 | val domain2LogicTransformationTime = System.nanoTime - domain2LogicTransformationStart | ||
245 | |||
246 | if (config.documentationLevel != DocumentationLevel.NONE) { | ||
247 | outputWorkspace.writeModel(problem, "initial.logicproblem") | ||
248 | } | ||
249 | |||
250 | val solver = switch (solver : script.solver) { | ||
251 | case ViatraSolver: new ViatraReasoner | ||
252 | case AlloySolver, | ||
253 | case AlloyMiniSat: new AlloySolver | ||
254 | default: throw new IllegalArgumentException("Unknown solver: " + solver) | ||
255 | } | ||
256 | val result = solver.solve(problem, config, outputWorkspace) | ||
257 | val statistics = result.statistics | ||
258 | statistics.entries += createIntStatisticEntry => [ | ||
259 | name = "modelLoadingTime" | ||
260 | value = (modelLoadingTime / USEC_TO_MSEC) as int | ||
261 | ] | ||
262 | statistics.entries += createIntStatisticEntry => [ | ||
263 | name = "domain2LogicTransformationTime" | ||
264 | value = (domain2LogicTransformationTime / USEC_TO_MSEC) as int | ||
265 | ] | ||
266 | var EObject modelResult = null | ||
267 | if (result instanceof ModelResult) { | ||
268 | val intepretations = solver.getInterpretations(result) | ||
269 | if (intepretations.size != 1) { | ||
270 | throw new IllegalStateException("Expected 1 interpretation, got " + intepretations.size) | ||
271 | } | ||
272 | var resultTransformationStart = System.nanoTime | ||
273 | val logic2Ecore = new Logic2Ecore(ecore2Logic) | ||
274 | modelResult = logic2Ecore.transformInterpretation(intepretations.head, modelGeneration.trace) | ||
275 | val resultTransformationTime = System.nanoTime - resultTransformationStart | ||
276 | statistics.entries += createIntStatisticEntry => [ | ||
277 | name = "ecore2LogicTransformationTime" | ||
278 | value = (resultTransformationTime / USEC_TO_MSEC) as int | ||
279 | ] | ||
280 | } | ||
281 | |||
282 | new ExperimentResult(result.class.simpleName, statistics, modelResult) | ||
283 | } | ||
284 | |||
285 | private def initializeScope(LogicSolverConfiguration config, int modelSize, LogicProblem problem, | ||
286 | EClassMapper eClassMapper, Ecore2Logic_Trace trace) { | ||
287 | val knownElements = initializeKnownElements(problem, config.typeScopes) | ||
288 | if (modelSize < 0) { | ||
289 | config.typeScopes.minNewElements = 0 | ||
290 | config.typeScopes.maxNewElements = TypeScopes.Unlimited | ||
291 | } else { | ||
292 | val numberOfKnownElements = knownElements.values.flatten.toSet.size | ||
293 | val newElementCount = modelSize - numberOfKnownElements | ||
294 | switch (script.scope) { | ||
295 | case upperOnly: | ||
296 | config.typeScopes.maxNewElements = newElementCount + MODEL_SIZE_GAP | ||
297 | case exactly: { | ||
298 | config.typeScopes.minNewElements = newElementCount | ||
299 | config.typeScopes.maxNewElements = newElementCount | ||
300 | } | ||
301 | default: { | ||
302 | config.typeScopes.minNewElements = newElementCount | ||
303 | config.typeScopes.maxNewElements = newElementCount + MODEL_SIZE_GAP | ||
304 | } | ||
305 | } | ||
306 | } | ||
307 | switch (scope : script.scope) { | ||
308 | case none, | ||
309 | case exactly: | ||
310 | return | ||
311 | case quantiles, | ||
312 | case unsat, | ||
313 | case upperOnly: { | ||
314 | val quantiles = if (scope == Scope.unsat) { | ||
315 | metamodelLoader.unsatTypeQuantiles | ||
316 | } else { | ||
317 | metamodelLoader.typeQuantiles | ||
318 | } | ||
319 | for (eClassInScope : eClassMapper.allClassesInScope(trace)) { | ||
320 | val quantile = quantiles.get(eClassInScope.name) | ||
321 | if (quantile !== null) { | ||
322 | val type = eClassMapper.TypeofEClass(trace, eClassInScope) | ||
323 | val knownInstances = knownElements.get(type) | ||
324 | val currentCount = if(knownInstances === null) 0 else knownInstances.size | ||
325 | val lowCount = Math.floor(modelSize * quantile.low) as int | ||
326 | val highCount = Math.ceil((modelSize + MODEL_SIZE_GAP) * quantile.high) as int | ||
327 | // println('''«type.name» «lowCount» «highCount»''') | ||
328 | if (script.scope != Scope.upperOnly) { | ||
329 | config.typeScopes.minNewElementsByType.put(type, Math.max(lowCount - currentCount, 0)) | ||
330 | } | ||
331 | config.typeScopes.maxNewElementsByType.put(type, highCount - currentCount) | ||
332 | } | ||
333 | } | ||
334 | } | ||
335 | default: | ||
336 | throw new IllegalArgumentException("Unknown scope: " + script.scope) | ||
337 | } | ||
338 | } | ||
339 | |||
340 | /* | ||
341 | * Copied from hu.bme.mit.inf.dslreasoner.application.execution.ScopeLoader.initialiseknownElements(LogicProblem, TypeScopes) | ||
342 | */ | ||
343 | private static def initializeKnownElements(LogicProblem p, TypeScopes s) { | ||
344 | val Map<Type, Set<DefinedElement>> res = new HashMap | ||
345 | |||
346 | // 1. fill map with every types | ||
347 | for (t : p.types) { | ||
348 | res.put(t, new HashSet) | ||
349 | } | ||
350 | |||
351 | // 2. fill map with every objects | ||
352 | for (definedType : p.types.filter(TypeDefinition)) { | ||
353 | val supertypes = CollectionsUtil.<Type>transitiveClosureStar(definedType)[supertypes] | ||
354 | for (supertype : supertypes) { | ||
355 | for (element : definedType.elements) { | ||
356 | res.get(supertype).add(element) | ||
357 | } | ||
358 | } | ||
359 | } | ||
360 | val partialModelContents = p.annotations.filter(PartialModelRelation2Assertion).map[target].toList.map [ | ||
361 | eAllContents.toIterable | ||
362 | ].flatten.toList | ||
363 | s.knownIntegers += partialModelContents.filter(IntLiteral).map[it.value] | ||
364 | s.knownReals += partialModelContents.filter(RealLiteral).map[it.value] | ||
365 | s.knownStrings += partialModelContents.filter(StringLiteral).map[it.value] | ||
366 | |||
367 | res | ||
368 | } | ||
369 | |||
370 | public static def void main(String[] args) { | ||
371 | if (args.length != 1) { | ||
372 | System.err.println("Missing measurement script name.") | ||
373 | System.exit(-1) | ||
374 | } | ||
375 | EMFPatternLanguageStandaloneSetup.doSetup | ||
376 | ViatraQueryEngineOptions.setSystemDefaultBackends(ReteBackendFactory.INSTANCE, ReteBackendFactory.INSTANCE, | ||
377 | ReteBackendFactory.INSTANCE) | ||
378 | Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("*", new XMIResourceFactoryImpl) | ||
379 | val config = readConfig(args.get(0)) | ||
380 | val runnner = new MeasurementScriptRunner(config) | ||
381 | runnner.run() | ||
382 | } | ||
383 | |||
384 | static def readConfig(String scriptPath) { | ||
385 | val gson = new Gson | ||
386 | val reader = new FileReader(scriptPath) | ||
387 | try { | ||
388 | gson.fromJson(reader, MeasurementScript) | ||
389 | } finally { | ||
390 | reader.close | ||
391 | } | ||
392 | } | ||
393 | |||
394 | @Data | ||
395 | private static class ExperimentResult { | ||
396 | String resultName | ||
397 | Statistics statistics | ||
398 | EObject model | ||
399 | } | ||
400 | } | ||