OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library dump_info; | |
6 | |
7 import 'dart:convert' show | |
8 HtmlEscape, | |
9 JsonEncoder, | |
10 StringConversionSink, | |
11 ChunkedConversionSink; | |
12 | |
13 import 'elements/elements.dart'; | |
14 import 'elements/visitor.dart'; | |
15 import 'dart2jslib.dart' show | |
16 Backend, | |
17 CodeBuffer, | |
18 Compiler, | |
19 CompilerTask, | |
20 MessageKind; | |
21 import 'types/types.dart' show TypeMask; | |
22 import 'deferred_load.dart' show OutputUnit; | |
23 import 'js_backend/js_backend.dart' show JavaScriptBackend; | |
24 import 'js/js.dart' as jsAst; | |
25 import 'universe/universe.dart' show Selector; | |
26 import 'util/util.dart' show NO_LOCATION_SPANNABLE; | |
27 | |
28 /// Maps objects to an id. Supports lookups in | |
29 /// both directions. | |
30 class IdMapper<T>{ | |
31 Map<int, T> _idToElement = {}; | |
32 Map<T, int> _elementToId = {}; | |
33 int _idCounter = 0; | |
34 String name; | |
35 | |
36 IdMapper(this.name); | |
37 | |
38 Iterable<T> get elements => _elementToId.keys; | |
39 | |
40 String add(T e) { | |
41 if (_elementToId.containsKey(e)) { | |
42 return name + "/${_elementToId[e]}"; | |
43 } | |
44 | |
45 _idToElement[_idCounter] = e; | |
46 _elementToId[e] = _idCounter; | |
47 _idCounter += 1; | |
48 return name + "/${_idCounter - 1}"; | |
49 } | |
50 } | |
51 | |
52 class GroupedIdMapper { | |
53 // Mappers for specific kinds of elements. | |
54 IdMapper<LibraryElement> _library = new IdMapper('library'); | |
55 IdMapper<TypedefElement> _typedef = new IdMapper('typedef'); | |
56 IdMapper<FieldElement> _field = new IdMapper('field'); | |
57 IdMapper<ClassElement> _class = new IdMapper('class'); | |
58 IdMapper<FunctionElement> _function = new IdMapper('function'); | |
59 IdMapper<OutputUnit> _outputUnit = new IdMapper('outputUnit'); | |
60 | |
61 Iterable<Element> get functions => _function.elements; | |
62 | |
63 // Convert this database of elements into JSON for rendering | |
64 Map<String, dynamic> _toJson(ElementToJsonVisitor elementToJson) { | |
65 Map<String, dynamic> json = {}; | |
66 var m = [_library, _typedef, _field, _class, _function]; | |
67 for (IdMapper mapper in m) { | |
68 Map<String, dynamic> innerMapper = {}; | |
69 mapper._idToElement.forEach((k, v) { | |
70 // All these elements are already cached in the | |
71 // jsonCache, so this is just an access. | |
72 var elementJson = elementToJson.process(v); | |
73 if (elementJson != null) { | |
74 innerMapper["$k"] = elementJson; | |
75 } | |
76 }); | |
77 json[mapper.name] = innerMapper; | |
78 } | |
79 return json; | |
80 } | |
81 } | |
82 | |
83 class ElementToJsonVisitor extends ElementVisitor<Map<String, dynamic>> { | |
84 final GroupedIdMapper mapper = new GroupedIdMapper(); | |
85 final Compiler compiler; | |
86 | |
87 final Map<Element, Map<String, dynamic>> jsonCache = {}; | |
88 | |
89 int programSize; | |
90 String dart2jsVersion; | |
91 | |
92 ElementToJsonVisitor(this.compiler); | |
93 | |
94 void run() { | |
95 Backend backend = compiler.backend; | |
96 if (backend is JavaScriptBackend) { | |
97 // Add up the sizes of all output-buffers. | |
98 programSize = backend.emitter.oldEmitter.outputBuffers.values.fold(0, | |
99 (a, b) => a + b.length); | |
100 } else { | |
101 programSize = compiler.assembledCode.length; | |
102 } | |
103 | |
104 dart2jsVersion = compiler.hasBuildId ? compiler.buildId : null; | |
105 | |
106 for (var library in compiler.libraryLoader.libraries.toList()) { | |
107 library.accept(this); | |
108 } | |
109 } | |
110 | |
111 // If keeping the element is in question (like if a function has a size | |
112 // of zero), only keep it if it holds dependencies to elsewhere. | |
113 bool shouldKeep(Element element) { | |
114 return compiler.dumpInfoTask.selectorsFromElement.containsKey(element) | |
115 || compiler.dumpInfoTask.inlineCount.containsKey(element); | |
116 } | |
117 | |
118 Map<String, dynamic> toJson() { | |
119 return mapper._toJson(this); | |
120 } | |
121 | |
122 // Memoization of the JSON creating process. | |
123 Map<String, dynamic> process(Element element) { | |
124 return jsonCache.putIfAbsent(element, () => element.accept(this)); | |
125 } | |
126 | |
127 // Returns the id of an [element] if it has already been processed. | |
128 // If the element has not been processed, this function does not | |
129 // process it, and simply returns null instead. | |
130 String idOf(Element element) { | |
131 if (jsonCache.containsKey(element) && jsonCache[element] != null) { | |
132 return jsonCache[element]['id']; | |
133 } else { | |
134 return null; | |
135 } | |
136 } | |
137 | |
138 Map<String, dynamic> visitElement(Element element) { | |
139 return null; | |
140 } | |
141 | |
142 Map<String, dynamic> visitConstructorBodyElement(ConstructorBodyElement e) { | |
143 return visitFunctionElement(e.constructor); | |
144 } | |
145 | |
146 Map<String, dynamic> visitLibraryElement(LibraryElement element) { | |
147 var id = mapper._library.add(element); | |
148 List<String> children = <String>[]; | |
149 | |
150 String libname = element.getLibraryName(); | |
151 libname = libname == "" ? "<unnamed>" : libname; | |
152 | |
153 int size = compiler.dumpInfoTask.sizeOf(element); | |
154 | |
155 LibraryElement contentsOfLibrary = element.isPatched | |
156 ? element.patch : element; | |
157 contentsOfLibrary.forEachLocalMember((Element member) { | |
158 Map<String, dynamic> childJson = this.process(member); | |
159 if (childJson == null) return; | |
160 children.add(childJson['id']); | |
161 }); | |
162 | |
163 if (children.length == 0 && !shouldKeep(element)) { | |
164 return null; | |
165 } | |
166 | |
167 return { | |
168 'kind': 'library', | |
169 'name': libname, | |
170 'size': size, | |
171 'id': id, | |
172 'children': children | |
173 }; | |
174 } | |
175 | |
176 Map<String, dynamic> visitTypedefElement(TypedefElement element) { | |
177 String id = mapper._typedef.add(element); | |
178 return element.alias == null | |
179 ? null | |
180 : { | |
181 'id': id, | |
182 'type': element.alias.toString(), | |
183 'kind': 'typedef', | |
184 'name': element.name | |
185 }; | |
186 } | |
187 | |
188 Map<String, dynamic> visitFieldElement(FieldElement element) { | |
189 String id = mapper._field.add(element); | |
190 List<String> children = []; | |
191 StringBuffer emittedCode = compiler.dumpInfoTask.codeOf(element); | |
192 | |
193 TypeMask inferredType = | |
194 compiler.typesTask.getGuaranteedTypeOfElement(element); | |
195 // If a field has an empty inferred type it is never used. | |
196 if (inferredType == null || inferredType.isEmpty || element.isConst) { | |
197 return null; | |
198 } | |
199 | |
200 int size = compiler.dumpInfoTask.sizeOf(element); | |
201 String code; | |
202 | |
203 if (emittedCode != null) { | |
204 size += emittedCode.length; | |
205 code = emittedCode.toString(); | |
206 } | |
207 | |
208 for (Element closure in element.nestedClosures) { | |
209 var childJson = this.process(closure); | |
210 if (childJson != null) { | |
211 children.add(childJson['id']); | |
212 if (childJson.containsKey('size')) { | |
213 size += childJson['size']; | |
214 } | |
215 } | |
216 } | |
217 | |
218 OutputUnit outputUnit = | |
219 compiler.deferredLoadTask.outputUnitForElement(element); | |
220 | |
221 return { | |
222 'id': id, | |
223 'kind': 'field', | |
224 'type': element.type.toString(), | |
225 'inferredType': inferredType.toString(), | |
226 'name': element.name, | |
227 'children': children, | |
228 'size': size, | |
229 'code': code, | |
230 'outputUnit': mapper._outputUnit.add(outputUnit) | |
231 }; | |
232 } | |
233 | |
234 Map<String, dynamic> visitClassElement(ClassElement element) { | |
235 String id = mapper._class.add(element); | |
236 List<String> children = []; | |
237 | |
238 int size = compiler.dumpInfoTask.sizeOf(element); | |
239 JavaScriptBackend backend = compiler.backend; | |
240 | |
241 Map<String, dynamic> modifiers = { 'abstract': element.isAbstract }; | |
242 | |
243 element.forEachLocalMember((Element member) { | |
244 Map<String, dynamic> childJson = this.process(member); | |
245 if (childJson != null) { | |
246 children.add(childJson['id']); | |
247 | |
248 // Closures are placed in the library namespace, but | |
249 // we want to attribute them to a function, and by | |
250 // extension, this class. Process and add the sizes | |
251 // here. | |
252 if (member is MemberElement) { | |
253 for (Element closure in member.nestedClosures) { | |
254 Map<String, dynamic> child = this.process(closure); | |
255 | |
256 // Look for the parent element of this closure which should | |
257 // be a class. If it exists, set the display name to | |
258 // the name of the class + the name of the closure function. | |
259 Element parent = closure.enclosingElement; | |
260 Map<String, dynamic> processedParent = this.process(parent); | |
261 if (processedParent != null) { | |
262 child['name'] = "${processedParent['name']}.${child['name']}"; | |
263 } | |
264 | |
265 if (child != null) { | |
266 size += child['size']; | |
267 } | |
268 } | |
269 } | |
270 } | |
271 }); | |
272 | |
273 // Omit element if it is not needed. | |
274 if (!backend.emitter.neededClasses.contains(element) && | |
275 children.length == 0) { | |
276 return null; | |
277 } | |
278 | |
279 OutputUnit outputUnit = | |
280 compiler.deferredLoadTask.outputUnitForElement(element); | |
281 | |
282 return { | |
283 'name': element.name, | |
284 'size': size, | |
285 'kind': 'class', | |
286 'modifiers': modifiers, | |
287 'children': children, | |
288 'id': id, | |
289 'outputUnit': mapper._outputUnit.add(outputUnit) | |
290 }; | |
291 } | |
292 | |
293 Map<String, dynamic> visitFunctionElement(FunctionElement element) { | |
294 String id = mapper._function.add(element); | |
295 String name = element.name; | |
296 String kind = "function"; | |
297 List<String> children = []; | |
298 List<Map<String, dynamic>> parameters = []; | |
299 String inferredReturnType = null; | |
300 String returnType = null; | |
301 String sideEffects = null; | |
302 String code = ""; | |
303 | |
304 StringBuffer emittedCode = compiler.dumpInfoTask.codeOf(element); | |
305 int size = compiler.dumpInfoTask.sizeOf(element); | |
306 | |
307 Map<String, dynamic> modifiers = { | |
308 'static': element.isStatic, | |
309 'const': element.isConst, | |
310 'factory': element.isFactoryConstructor, | |
311 'external': element.isPatched | |
312 }; | |
313 | |
314 var enclosingElement = element.enclosingElement; | |
315 if (enclosingElement.isField || | |
316 enclosingElement.isFunction || | |
317 element.isClosure || | |
318 enclosingElement.isConstructor) { | |
319 kind = "closure"; | |
320 name = "<unnamed>"; | |
321 } else if (modifiers['static']) { | |
322 kind = 'function'; | |
323 } else if (enclosingElement.isClass) { | |
324 kind = 'method'; | |
325 } | |
326 | |
327 if (element.isConstructor) { | |
328 name == "" | |
329 ? "${element.enclosingElement.name}" | |
330 : "${element.enclosingElement.name}.${element.name}"; | |
331 kind = "constructor"; | |
332 } | |
333 | |
334 if (emittedCode != null) { | |
335 FunctionSignature signature = element.functionSignature; | |
336 returnType = signature.type.returnType.toString(); | |
337 signature.forEachParameter((parameter) { | |
338 parameters.add({ | |
339 'name': parameter.name, | |
340 'type': compiler.typesTask | |
341 .getGuaranteedTypeOfElement(parameter).toString(), | |
342 'declaredType': parameter.node.type.toString() | |
343 }); | |
344 }); | |
345 inferredReturnType = compiler.typesTask | |
346 .getGuaranteedReturnTypeOfElement(element).toString(); | |
347 sideEffects = compiler.world.getSideEffectsOfElement(element).toString(); | |
348 code = emittedCode.toString(); | |
349 } | |
350 | |
351 if (element is MemberElement) { | |
352 MemberElement member = element as MemberElement; | |
353 for (Element closure in member.nestedClosures) { | |
354 Map<String, dynamic> child = this.process(closure); | |
355 if (child != null) { | |
356 child['kind'] = 'closure'; | |
357 children.add(child['id']); | |
358 size += child['size']; | |
359 } | |
360 } | |
361 } | |
362 | |
363 if (size == 0 && !shouldKeep(element)) { | |
364 return null; | |
365 } | |
366 | |
367 int inlinedCount = compiler.dumpInfoTask.inlineCount[element]; | |
368 if (inlinedCount == null) { | |
369 inlinedCount = 0; | |
370 } | |
371 | |
372 OutputUnit outputUnit = | |
373 compiler.deferredLoadTask.outputUnitForElement(element); | |
374 | |
375 return { | |
376 'kind': kind, | |
377 'name': name, | |
378 'id': id, | |
379 'modifiers': modifiers, | |
380 'children': children, | |
381 'size': size, | |
382 'returnType': returnType, | |
383 'inferredReturnType': inferredReturnType, | |
384 'parameters': parameters, | |
385 'sideEffects': sideEffects, | |
386 'inlinedCount': inlinedCount, | |
387 'code': code, | |
388 'type': element.type.toString(), | |
389 'outputUnit': mapper._outputUnit.add(outputUnit) | |
390 }; | |
391 } | |
392 } | |
393 | |
394 class Selection { | |
395 final Element selectedElement; | |
396 final Selector selector; | |
397 Selection(this.selectedElement, this.selector); | |
398 } | |
399 | |
400 class DumpInfoTask extends CompilerTask { | |
401 DumpInfoTask(Compiler compiler) | |
402 : super(compiler); | |
403 | |
404 String name = "Dump Info"; | |
405 | |
406 ElementToJsonVisitor infoCollector; | |
407 | |
408 // A set of javascript AST nodes that we care about the size of. | |
409 // This set is automatically populated when registerElementAst() | |
410 // is called. | |
411 final Set<jsAst.Node> _tracking = new Set<jsAst.Node>(); | |
412 // A mapping from Dart Elements to Javascript AST Nodes. | |
413 final Map<Element, List<jsAst.Node>> _elementToNodes = | |
414 <Element, List<jsAst.Node>>{}; | |
415 // A mapping from Javascript AST Nodes to the size of their | |
416 // pretty-printed contents. | |
417 final Map<jsAst.Node, int> _nodeToSize = <jsAst.Node, int>{}; | |
418 final Map<jsAst.Node, int> _nodeBeforeSize = <jsAst.Node, int>{}; | |
419 final Map<Element, int> _fieldNameToSize = <Element, int>{}; | |
420 | |
421 final Map<Element, Set<Selector>> selectorsFromElement = {}; | |
422 final Map<Element, int> inlineCount = <Element, int>{}; | |
423 // A mapping from an element to a list of elements that are | |
424 // inlined inside of it. | |
425 final Map<Element, List<Element>> inlineMap = <Element, List<Element>>{}; | |
426 | |
427 void registerInlined(Element element, Element inlinedFrom) { | |
428 inlineCount.putIfAbsent(element, () => 0); | |
429 inlineCount[element] += 1; | |
430 inlineMap.putIfAbsent(inlinedFrom, () => new List<Element>()); | |
431 inlineMap[inlinedFrom].add(element); | |
432 } | |
433 | |
434 /** | |
435 * Registers that a function uses a selector in the | |
436 * function body | |
437 */ | |
438 void elementUsesSelector(Element element, Selector selector) { | |
439 if (compiler.dumpInfo) { | |
440 selectorsFromElement | |
441 .putIfAbsent(element, () => new Set<Selector>()) | |
442 .add(selector); | |
443 } | |
444 } | |
445 | |
446 /** | |
447 * Returns an iterable of [Selection]s that are used by | |
448 * [element]. Each [Selection] contains an element that is | |
449 * used and the selector that selected the element. | |
450 */ | |
451 Iterable<Selection> getRetaining(Element element) { | |
452 if (!selectorsFromElement.containsKey(element)) { | |
453 return const <Selection>[]; | |
454 } else { | |
455 return selectorsFromElement[element].expand( | |
456 (selector) { | |
457 return compiler.world.allFunctions.filter(selector).map((element) { | |
458 return new Selection(element, selector); | |
459 }); | |
460 }); | |
461 } | |
462 } | |
463 | |
464 /** | |
465 * A callback that can be called before a jsAst [node] is | |
466 * pretty-printed. The size of the code buffer ([aftersize]) | |
467 * is also passed. | |
468 */ | |
469 void enteringAst(jsAst.Node node, int beforeSize) { | |
470 if (isTracking(node)) { | |
471 _nodeBeforeSize[node] = beforeSize; | |
472 } | |
473 } | |
474 | |
475 /** | |
476 * A callback that can be called after a jsAst [node] is | |
477 * pretty-printed. The size of the code buffer ([aftersize]) | |
478 * is also passed. | |
479 */ | |
480 void exitingAst(jsAst.Node node, int afterSize) { | |
481 if (isTracking(node)) { | |
482 int diff = afterSize - _nodeBeforeSize[node]; | |
483 recordAstSize(node, diff); | |
484 } | |
485 } | |
486 | |
487 // Returns true if we care about tracking the size of | |
488 // this node. | |
489 bool isTracking(jsAst.Node code) { | |
490 if (compiler.dumpInfo) { | |
491 return _tracking.contains(code); | |
492 } else { | |
493 return false; | |
494 } | |
495 } | |
496 | |
497 // Registers that a javascript AST node `code` was produced by the | |
498 // dart Element `element`. | |
499 void registerElementAst(Element element, jsAst.Node code) { | |
500 if (compiler.dumpInfo) { | |
501 _elementToNodes | |
502 .putIfAbsent(element, () => new List<jsAst.Node>()) | |
503 .add(code); | |
504 _tracking.add(code); | |
505 } | |
506 } | |
507 | |
508 // Records the size of a dart AST node after it has been | |
509 // pretty-printed into the output buffer. | |
510 void recordAstSize(jsAst.Node code, int size) { | |
511 if (compiler.dumpInfo) { | |
512 //TODO: should I be incrementing here instead? | |
513 _nodeToSize[code] = size; | |
514 } | |
515 } | |
516 | |
517 // Field names are treated differently by the dart compiler | |
518 // so they must be recorded seperately. | |
519 void recordFieldNameSize(Element element, int size) { | |
520 _fieldNameToSize[element] = size; | |
521 } | |
522 | |
523 // Returns the size of the source code that | |
524 // was generated for an element. If no source | |
525 // code was produced, return 0. | |
526 int sizeOf(Element element) { | |
527 if (_fieldNameToSize.containsKey(element)) { | |
528 return _fieldNameToSize[element]; | |
529 } | |
530 if (_elementToNodes.containsKey(element)) { | |
531 return _elementToNodes[element] | |
532 .map(sizeOfNode) | |
533 .fold(0, (a, b) => a + b); | |
534 } else { | |
535 return 0; | |
536 } | |
537 } | |
538 | |
539 int sizeOfNode(jsAst.Node node) { | |
540 if (_nodeToSize.containsKey(node)) { | |
541 return _nodeToSize[node]; | |
542 } else { | |
543 return 0; | |
544 } | |
545 } | |
546 | |
547 StringBuffer codeOf(Element element) { | |
548 List<jsAst.Node> code = _elementToNodes[element]; | |
549 if (code == null) return null; | |
550 // Concatenate rendered ASTs. | |
551 StringBuffer sb = new StringBuffer(); | |
552 for (jsAst.Node ast in code) { | |
553 sb.writeln(jsAst.prettyPrint(ast, compiler).getText()); | |
554 } | |
555 return sb; | |
556 } | |
557 | |
558 void collectInfo() { | |
559 infoCollector = new ElementToJsonVisitor(compiler)..run(); | |
560 } | |
561 | |
562 void dumpInfo() { | |
563 measure(() { | |
564 if (infoCollector == null) { | |
565 collectInfo(); | |
566 } | |
567 | |
568 StringBuffer jsonBuffer = new StringBuffer(); | |
569 dumpInfoJson(jsonBuffer); | |
570 compiler.outputProvider('', 'info.json') | |
571 ..add(jsonBuffer.toString()) | |
572 ..close(); | |
573 }); | |
574 } | |
575 | |
576 | |
577 void dumpInfoJson(StringSink buffer) { | |
578 JsonEncoder encoder = const JsonEncoder(); | |
579 DateTime startToJsonTime = new DateTime.now(); | |
580 | |
581 Map<String, List<Map<String, String>>> holding = | |
582 <String, List<Map<String, String>>>{}; | |
583 for (Element fn in infoCollector.mapper.functions) { | |
584 Iterable<Selection> pulling = getRetaining(fn); | |
585 // Don't bother recording an empty list of dependencies. | |
586 if (pulling.length > 0) { | |
587 String fnId = infoCollector.idOf(fn); | |
588 // Some dart2js builtin functions are not | |
589 // recorded. Don't register these. | |
590 if (fnId != null) { | |
591 holding[fnId] = pulling | |
592 .map((selection) { | |
593 return <String, String>{ | |
594 "id": infoCollector.idOf(selection.selectedElement), | |
595 "mask": selection.selector.mask.toString() | |
596 }; | |
597 }) | |
598 // Filter non-null ids for the same reason as above. | |
599 .where((a) => a['id'] != null) | |
600 .toList(); | |
601 } | |
602 } | |
603 } | |
604 | |
605 // Track dependencies that come from inlining. | |
606 for (Element element in inlineMap.keys) { | |
607 String keyId = infoCollector.idOf(element); | |
608 if (keyId != null) { | |
609 for (Element held in inlineMap[element]) { | |
610 String valueId = infoCollector.idOf(held); | |
611 if (valueId != null) { | |
612 holding.putIfAbsent(keyId, () => new List<Map<String, String>>()) | |
613 .add(<String, String>{ | |
614 "id": valueId, | |
615 "mask": "inlined" | |
616 }); | |
617 } | |
618 } | |
619 } | |
620 } | |
621 | |
622 List<Map<String, dynamic>> outputUnits = | |
623 new List<Map<String, dynamic>>(); | |
624 | |
625 JavaScriptBackend backend = compiler.backend; | |
626 | |
627 for (OutputUnit outputUnit in | |
628 infoCollector.mapper._outputUnit._elementToId.keys) { | |
629 String id = infoCollector.mapper._outputUnit.add(outputUnit); | |
630 outputUnits.add(<String, dynamic> { | |
631 'id': id, | |
632 'name': outputUnit.name, | |
633 'size': backend.emitter.oldEmitter.outputBuffers[outputUnit].length, | |
634 }); | |
635 } | |
636 | |
637 Map<String, dynamic> outJson = { | |
638 'elements': infoCollector.toJson(), | |
639 'holding': holding, | |
640 'outputUnits': outputUnits, | |
641 'dump_version': 3, | |
642 }; | |
643 | |
644 Duration toJsonDuration = new DateTime.now().difference(startToJsonTime); | |
645 | |
646 Map<String, dynamic> generalProgramInfo = <String, dynamic> { | |
647 'size': infoCollector.programSize, | |
648 'dart2jsVersion': infoCollector.dart2jsVersion, | |
649 'compilationMoment': new DateTime.now().toString(), | |
650 'compilationDuration': compiler.totalCompileTime.elapsed.toString(), | |
651 'toJsonDuration': 0, | |
652 'dumpInfoDuration': this.timing.toString(), | |
653 'noSuchMethodEnabled': compiler.enabledNoSuchMethod | |
654 }; | |
655 | |
656 outJson['program'] = generalProgramInfo; | |
657 | |
658 ChunkedConversionSink<Object> sink = | |
659 encoder.startChunkedConversion( | |
660 new StringConversionSink.fromStringSink(buffer)); | |
661 sink.add(outJson); | |
662 compiler.reportInfo(NO_LOCATION_SPANNABLE, | |
663 const MessageKind( | |
664 "View the dumped .info.json file at " | |
665 "https://dart-lang.github.io/dump-info-visualizer")); | |
666 } | |
667 } | |
OLD | NEW |