| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library dump_info; |
| |
| import 'dart:convert' show |
| HtmlEscape, |
| JsonEncoder, |
| StringConversionSink, |
| ChunkedConversionSink; |
| |
| import 'elements/elements.dart'; |
| import 'elements/visitor.dart'; |
| import 'dart2jslib.dart' show |
| Backend, |
| CodeBuffer, |
| Compiler, |
| CompilerTask, |
| MessageKind; |
| import 'types/types.dart' show TypeMask; |
| import 'deferred_load.dart' show OutputUnit; |
| import 'js_backend/js_backend.dart' show JavaScriptBackend; |
| import 'js/js.dart' as jsAst; |
| import 'universe/universe.dart' show Selector; |
| import 'util/util.dart' show NO_LOCATION_SPANNABLE; |
| |
| /// Maps objects to an id. Supports lookups in |
| /// both directions. |
| class IdMapper<T>{ |
| Map<int, T> _idToElement = {}; |
| Map<T, int> _elementToId = {}; |
| int _idCounter = 0; |
| String name; |
| |
| IdMapper(this.name); |
| |
| Iterable<T> get elements => _elementToId.keys; |
| |
| String add(T e) { |
| if (_elementToId.containsKey(e)) { |
| return name + "/${_elementToId[e]}"; |
| } |
| |
| _idToElement[_idCounter] = e; |
| _elementToId[e] = _idCounter; |
| _idCounter += 1; |
| return name + "/${_idCounter - 1}"; |
| } |
| } |
| |
| class GroupedIdMapper { |
| // Mappers for specific kinds of elements. |
| IdMapper<LibraryElement> _library = new IdMapper('library'); |
| IdMapper<TypedefElement> _typedef = new IdMapper('typedef'); |
| IdMapper<FieldElement> _field = new IdMapper('field'); |
| IdMapper<ClassElement> _class = new IdMapper('class'); |
| IdMapper<FunctionElement> _function = new IdMapper('function'); |
| IdMapper<OutputUnit> _outputUnit = new IdMapper('outputUnit'); |
| |
| Iterable<Element> get functions => _function.elements; |
| |
| // Convert this database of elements into JSON for rendering |
| Map<String, dynamic> _toJson(ElementToJsonVisitor elementToJson) { |
| Map<String, dynamic> json = {}; |
| var m = [_library, _typedef, _field, _class, _function]; |
| for (IdMapper mapper in m) { |
| Map<String, dynamic> innerMapper = {}; |
| mapper._idToElement.forEach((k, v) { |
| // All these elements are already cached in the |
| // jsonCache, so this is just an access. |
| var elementJson = elementToJson.process(v); |
| if (elementJson != null) { |
| innerMapper["$k"] = elementJson; |
| } |
| }); |
| json[mapper.name] = innerMapper; |
| } |
| return json; |
| } |
| } |
| |
| class ElementToJsonVisitor extends ElementVisitor<Map<String, dynamic>> { |
| final GroupedIdMapper mapper = new GroupedIdMapper(); |
| final Compiler compiler; |
| |
| final Map<Element, Map<String, dynamic>> jsonCache = {}; |
| |
| String dart2jsVersion; |
| |
| ElementToJsonVisitor(this.compiler); |
| |
| void run() { |
| Backend backend = compiler.backend; |
| |
| dart2jsVersion = compiler.hasBuildId ? compiler.buildId : null; |
| |
| for (var library in compiler.libraryLoader.libraries.toList()) { |
| library.accept(this); |
| } |
| } |
| |
| // If keeping the element is in question (like if a function has a size |
| // of zero), only keep it if it holds dependencies to elsewhere. |
| bool shouldKeep(Element element) { |
| return compiler.dumpInfoTask.selectorsFromElement.containsKey(element) |
| || compiler.dumpInfoTask.inlineCount.containsKey(element); |
| } |
| |
| Map<String, dynamic> toJson() { |
| return mapper._toJson(this); |
| } |
| |
| // Memoization of the JSON creating process. |
| Map<String, dynamic> process(Element element) { |
| return jsonCache.putIfAbsent(element, () => element.accept(this)); |
| } |
| |
| // Returns the id of an [element] if it has already been processed. |
| // If the element has not been processed, this function does not |
| // process it, and simply returns null instead. |
| String idOf(Element element) { |
| if (jsonCache.containsKey(element) && jsonCache[element] != null) { |
| return jsonCache[element]['id']; |
| } else { |
| return null; |
| } |
| } |
| |
| Map<String, dynamic> visitElement(Element element) { |
| return null; |
| } |
| |
| Map<String, dynamic> visitConstructorBodyElement(ConstructorBodyElement e) { |
| return visitFunctionElement(e.constructor); |
| } |
| |
| Map<String, dynamic> visitLibraryElement(LibraryElement element) { |
| var id = mapper._library.add(element); |
| List<String> children = <String>[]; |
| |
| String libname = element.getLibraryName(); |
| libname = libname == "" ? "<unnamed>" : libname; |
| |
| int size = compiler.dumpInfoTask.sizeOf(element); |
| |
| LibraryElement contentsOfLibrary = element.isPatched |
| ? element.patch : element; |
| contentsOfLibrary.forEachLocalMember((Element member) { |
| Map<String, dynamic> childJson = this.process(member); |
| if (childJson == null) return; |
| children.add(childJson['id']); |
| }); |
| |
| if (children.length == 0 && !shouldKeep(element)) { |
| return null; |
| } |
| |
| return { |
| 'kind': 'library', |
| 'name': libname, |
| 'size': size, |
| 'id': id, |
| 'children': children |
| }; |
| } |
| |
| Map<String, dynamic> visitTypedefElement(TypedefElement element) { |
| String id = mapper._typedef.add(element); |
| return element.alias == null |
| ? null |
| : { |
| 'id': id, |
| 'type': element.alias.toString(), |
| 'kind': 'typedef', |
| 'name': element.name |
| }; |
| } |
| |
| Map<String, dynamic> visitFieldElement(FieldElement element) { |
| String id = mapper._field.add(element); |
| List<String> children = []; |
| StringBuffer emittedCode = compiler.dumpInfoTask.codeOf(element); |
| |
| TypeMask inferredType = |
| compiler.typesTask.getGuaranteedTypeOfElement(element); |
| // If a field has an empty inferred type it is never used. |
| if (inferredType == null || inferredType.isEmpty || element.isConst) { |
| return null; |
| } |
| |
| int size = compiler.dumpInfoTask.sizeOf(element); |
| String code; |
| |
| if (emittedCode != null) { |
| size += emittedCode.length; |
| code = emittedCode.toString(); |
| } |
| |
| for (Element closure in element.nestedClosures) { |
| var childJson = this.process(closure); |
| if (childJson != null) { |
| children.add(childJson['id']); |
| if (childJson.containsKey('size')) { |
| size += childJson['size']; |
| } |
| } |
| } |
| |
| OutputUnit outputUnit = |
| compiler.deferredLoadTask.outputUnitForElement(element); |
| |
| return { |
| 'id': id, |
| 'kind': 'field', |
| 'type': element.type.toString(), |
| 'inferredType': inferredType.toString(), |
| 'name': element.name, |
| 'children': children, |
| 'size': size, |
| 'code': code, |
| 'outputUnit': mapper._outputUnit.add(outputUnit) |
| }; |
| } |
| |
| Map<String, dynamic> visitClassElement(ClassElement element) { |
| String id = mapper._class.add(element); |
| List<String> children = []; |
| |
| int size = compiler.dumpInfoTask.sizeOf(element); |
| JavaScriptBackend backend = compiler.backend; |
| |
| Map<String, dynamic> modifiers = { 'abstract': element.isAbstract }; |
| |
| element.forEachLocalMember((Element member) { |
| Map<String, dynamic> childJson = this.process(member); |
| if (childJson != null) { |
| children.add(childJson['id']); |
| |
| // Closures are placed in the library namespace, but |
| // we want to attribute them to a function, and by |
| // extension, this class. Process and add the sizes |
| // here. |
| if (member is MemberElement) { |
| for (Element closure in member.nestedClosures) { |
| Map<String, dynamic> child = this.process(closure); |
| |
| // Look for the parent element of this closure which should |
| // be a class. If it exists, set the display name to |
| // the name of the class + the name of the closure function. |
| Element parent = closure.enclosingElement; |
| Map<String, dynamic> processedParent = this.process(parent); |
| if (processedParent != null) { |
| child['name'] = "${processedParent['name']}.${child['name']}"; |
| } |
| |
| if (child != null) { |
| size += child['size']; |
| } |
| } |
| } |
| } |
| }); |
| |
| // Omit element if it is not needed. |
| if (!backend.emitter.neededClasses.contains(element) && |
| children.length == 0) { |
| return null; |
| } |
| |
| OutputUnit outputUnit = |
| compiler.deferredLoadTask.outputUnitForElement(element); |
| |
| return { |
| 'name': element.name, |
| 'size': size, |
| 'kind': 'class', |
| 'modifiers': modifiers, |
| 'children': children, |
| 'id': id, |
| 'outputUnit': mapper._outputUnit.add(outputUnit) |
| }; |
| } |
| |
| Map<String, dynamic> visitFunctionElement(FunctionElement element) { |
| String id = mapper._function.add(element); |
| String name = element.name; |
| String kind = "function"; |
| List<String> children = []; |
| List<Map<String, dynamic>> parameters = []; |
| String inferredReturnType = null; |
| String returnType = null; |
| String sideEffects = null; |
| String code = ""; |
| |
| StringBuffer emittedCode = compiler.dumpInfoTask.codeOf(element); |
| int size = compiler.dumpInfoTask.sizeOf(element); |
| |
| Map<String, dynamic> modifiers = { |
| 'static': element.isStatic, |
| 'const': element.isConst, |
| 'factory': element.isFactoryConstructor, |
| 'external': element.isPatched |
| }; |
| |
| var enclosingElement = element.enclosingElement; |
| if (enclosingElement.isField || |
| enclosingElement.isFunction || |
| element.isClosure || |
| enclosingElement.isConstructor) { |
| kind = "closure"; |
| name = "<unnamed>"; |
| } else if (modifiers['static']) { |
| kind = 'function'; |
| } else if (enclosingElement.isClass) { |
| kind = 'method'; |
| } |
| |
| if (element.isConstructor) { |
| name == "" |
| ? "${element.enclosingElement.name}" |
| : "${element.enclosingElement.name}.${element.name}"; |
| kind = "constructor"; |
| } |
| |
| if (emittedCode != null) { |
| FunctionSignature signature = element.functionSignature; |
| returnType = signature.type.returnType.toString(); |
| signature.forEachParameter((parameter) { |
| parameters.add({ |
| 'name': parameter.name, |
| 'type': compiler.typesTask |
| .getGuaranteedTypeOfElement(parameter).toString(), |
| 'declaredType': parameter.node.type.toString() |
| }); |
| }); |
| inferredReturnType = compiler.typesTask |
| .getGuaranteedReturnTypeOfElement(element).toString(); |
| sideEffects = compiler.world.getSideEffectsOfElement(element).toString(); |
| code = emittedCode.toString(); |
| } |
| |
| if (element is MemberElement) { |
| MemberElement member = element as MemberElement; |
| for (Element closure in member.nestedClosures) { |
| Map<String, dynamic> child = this.process(closure); |
| if (child != null) { |
| child['kind'] = 'closure'; |
| children.add(child['id']); |
| size += child['size']; |
| } |
| } |
| } |
| |
| if (size == 0 && !shouldKeep(element)) { |
| return null; |
| } |
| |
| int inlinedCount = compiler.dumpInfoTask.inlineCount[element]; |
| if (inlinedCount == null) { |
| inlinedCount = 0; |
| } |
| |
| OutputUnit outputUnit = |
| compiler.deferredLoadTask.outputUnitForElement(element); |
| |
| return { |
| 'kind': kind, |
| 'name': name, |
| 'id': id, |
| 'modifiers': modifiers, |
| 'children': children, |
| 'size': size, |
| 'returnType': returnType, |
| 'inferredReturnType': inferredReturnType, |
| 'parameters': parameters, |
| 'sideEffects': sideEffects, |
| 'inlinedCount': inlinedCount, |
| 'code': code, |
| 'type': element.type.toString(), |
| 'outputUnit': mapper._outputUnit.add(outputUnit) |
| }; |
| } |
| } |
| |
| class Selection { |
| final Element selectedElement; |
| final Selector selector; |
| Selection(this.selectedElement, this.selector); |
| } |
| |
| class DumpInfoTask extends CompilerTask { |
| DumpInfoTask(Compiler compiler) |
| : super(compiler); |
| |
| String name = "Dump Info"; |
| |
| ElementToJsonVisitor infoCollector; |
| |
| /// The size of the generated output. |
| int _programSize; |
| |
| // A set of javascript AST nodes that we care about the size of. |
| // This set is automatically populated when registerElementAst() |
| // is called. |
| final Set<jsAst.Node> _tracking = new Set<jsAst.Node>(); |
| // A mapping from Dart Elements to Javascript AST Nodes. |
| final Map<Element, List<jsAst.Node>> _elementToNodes = |
| <Element, List<jsAst.Node>>{}; |
| // A mapping from Javascript AST Nodes to the size of their |
| // pretty-printed contents. |
| final Map<jsAst.Node, int> _nodeToSize = <jsAst.Node, int>{}; |
| final Map<jsAst.Node, int> _nodeBeforeSize = <jsAst.Node, int>{}; |
| final Map<Element, int> _fieldNameToSize = <Element, int>{}; |
| |
| final Map<Element, Set<Selector>> selectorsFromElement = {}; |
| final Map<Element, int> inlineCount = <Element, int>{}; |
| // A mapping from an element to a list of elements that are |
| // inlined inside of it. |
| final Map<Element, List<Element>> inlineMap = <Element, List<Element>>{}; |
| |
| /// Register the size of the generated output. |
| void reportSize(int programSize) { |
| _programSize = programSize; |
| } |
| |
| void registerInlined(Element element, Element inlinedFrom) { |
| inlineCount.putIfAbsent(element, () => 0); |
| inlineCount[element] += 1; |
| inlineMap.putIfAbsent(inlinedFrom, () => new List<Element>()); |
| inlineMap[inlinedFrom].add(element); |
| } |
| |
| /** |
| * Registers that a function uses a selector in the |
| * function body |
| */ |
| void elementUsesSelector(Element element, Selector selector) { |
| if (compiler.dumpInfo) { |
| selectorsFromElement |
| .putIfAbsent(element, () => new Set<Selector>()) |
| .add(selector); |
| } |
| } |
| |
| /** |
| * Returns an iterable of [Selection]s that are used by |
| * [element]. Each [Selection] contains an element that is |
| * used and the selector that selected the element. |
| */ |
| Iterable<Selection> getRetaining(Element element) { |
| if (!selectorsFromElement.containsKey(element)) { |
| return const <Selection>[]; |
| } else { |
| return selectorsFromElement[element].expand( |
| (selector) { |
| return compiler.world.allFunctions.filter(selector).map((element) { |
| return new Selection(element, selector); |
| }); |
| }); |
| } |
| } |
| |
| /** |
| * A callback that can be called before a jsAst [node] is |
| * pretty-printed. The size of the code buffer ([aftersize]) |
| * is also passed. |
| */ |
| void enteringAst(jsAst.Node node, int beforeSize) { |
| if (isTracking(node)) { |
| _nodeBeforeSize[node] = beforeSize; |
| } |
| } |
| |
| /** |
| * A callback that can be called after a jsAst [node] is |
| * pretty-printed. The size of the code buffer ([aftersize]) |
| * is also passed. |
| */ |
| void exitingAst(jsAst.Node node, int afterSize) { |
| if (isTracking(node)) { |
| int diff = afterSize - _nodeBeforeSize[node]; |
| recordAstSize(node, diff); |
| } |
| } |
| |
| // Returns true if we care about tracking the size of |
| // this node. |
| bool isTracking(jsAst.Node code) { |
| if (compiler.dumpInfo) { |
| return _tracking.contains(code); |
| } else { |
| return false; |
| } |
| } |
| |
| // Registers that a javascript AST node `code` was produced by the |
| // dart Element `element`. |
| void registerElementAst(Element element, jsAst.Node code) { |
| if (compiler.dumpInfo) { |
| _elementToNodes |
| .putIfAbsent(element, () => new List<jsAst.Node>()) |
| .add(code); |
| _tracking.add(code); |
| } |
| } |
| |
| // Records the size of a dart AST node after it has been |
| // pretty-printed into the output buffer. |
| void recordAstSize(jsAst.Node code, int size) { |
| if (compiler.dumpInfo) { |
| //TODO: should I be incrementing here instead? |
| _nodeToSize[code] = size; |
| } |
| } |
| |
| // Field names are treated differently by the dart compiler |
| // so they must be recorded seperately. |
| void recordFieldNameSize(Element element, int size) { |
| _fieldNameToSize[element] = size; |
| } |
| |
| // Returns the size of the source code that |
| // was generated for an element. If no source |
| // code was produced, return 0. |
| int sizeOf(Element element) { |
| if (_fieldNameToSize.containsKey(element)) { |
| return _fieldNameToSize[element]; |
| } |
| if (_elementToNodes.containsKey(element)) { |
| return _elementToNodes[element] |
| .map(sizeOfNode) |
| .fold(0, (a, b) => a + b); |
| } else { |
| return 0; |
| } |
| } |
| |
| int sizeOfNode(jsAst.Node node) { |
| if (_nodeToSize.containsKey(node)) { |
| return _nodeToSize[node]; |
| } else { |
| return 0; |
| } |
| } |
| |
| StringBuffer codeOf(Element element) { |
| List<jsAst.Node> code = _elementToNodes[element]; |
| if (code == null) return null; |
| // Concatenate rendered ASTs. |
| StringBuffer sb = new StringBuffer(); |
| for (jsAst.Node ast in code) { |
| sb.writeln(jsAst.prettyPrint(ast, compiler).getText()); |
| } |
| return sb; |
| } |
| |
| void collectInfo() { |
| infoCollector = new ElementToJsonVisitor(compiler)..run(); |
| } |
| |
| void dumpInfo() { |
| measure(() { |
| if (infoCollector == null) { |
| collectInfo(); |
| } |
| |
| StringBuffer jsonBuffer = new StringBuffer(); |
| dumpInfoJson(jsonBuffer); |
| compiler.outputProvider('', 'info.json') |
| ..add(jsonBuffer.toString()) |
| ..close(); |
| }); |
| } |
| |
| |
| void dumpInfoJson(StringSink buffer) { |
| JsonEncoder encoder = const JsonEncoder(); |
| DateTime startToJsonTime = new DateTime.now(); |
| |
| Map<String, List<Map<String, String>>> holding = |
| <String, List<Map<String, String>>>{}; |
| for (Element fn in infoCollector.mapper.functions) { |
| Iterable<Selection> pulling = getRetaining(fn); |
| // Don't bother recording an empty list of dependencies. |
| if (pulling.length > 0) { |
| String fnId = infoCollector.idOf(fn); |
| // Some dart2js builtin functions are not |
| // recorded. Don't register these. |
| if (fnId != null) { |
| holding[fnId] = pulling |
| .map((selection) { |
| return <String, String>{ |
| "id": infoCollector.idOf(selection.selectedElement), |
| "mask": selection.selector.mask.toString() |
| }; |
| }) |
| // Filter non-null ids for the same reason as above. |
| .where((a) => a['id'] != null) |
| .toList(); |
| } |
| } |
| } |
| |
| // Track dependencies that come from inlining. |
| for (Element element in inlineMap.keys) { |
| String keyId = infoCollector.idOf(element); |
| if (keyId != null) { |
| for (Element held in inlineMap[element]) { |
| String valueId = infoCollector.idOf(held); |
| if (valueId != null) { |
| holding.putIfAbsent(keyId, () => new List<Map<String, String>>()) |
| .add(<String, String>{ |
| "id": valueId, |
| "mask": "inlined" |
| }); |
| } |
| } |
| } |
| } |
| |
| List<Map<String, dynamic>> outputUnits = |
| new List<Map<String, dynamic>>(); |
| |
| JavaScriptBackend backend = compiler.backend; |
| |
| for (OutputUnit outputUnit in |
| infoCollector.mapper._outputUnit._elementToId.keys) { |
| String id = infoCollector.mapper._outputUnit.add(outputUnit); |
| outputUnits.add(<String, dynamic> { |
| 'id': id, |
| 'name': outputUnit.name, |
| 'size': backend.emitter.oldEmitter.outputBuffers[outputUnit].length, |
| }); |
| } |
| |
| Map<String, dynamic> outJson = { |
| 'elements': infoCollector.toJson(), |
| 'holding': holding, |
| 'outputUnits': outputUnits, |
| 'dump_version': 3, |
| 'deferredFiles': compiler.deferredLoadTask.computeDeferredMap(), |
| // This increases when new information is added to the map, but the viewer |
| // still is compatible. |
| 'dump_minor_version': '1' |
| }; |
| |
| Duration toJsonDuration = new DateTime.now().difference(startToJsonTime); |
| |
| Map<String, dynamic> generalProgramInfo = <String, dynamic> { |
| 'size': _programSize, |
| 'dart2jsVersion': infoCollector.dart2jsVersion, |
| 'compilationMoment': new DateTime.now().toString(), |
| 'compilationDuration': compiler.totalCompileTime.elapsed.toString(), |
| 'toJsonDuration': 0, |
| 'dumpInfoDuration': this.timing.toString(), |
| 'noSuchMethodEnabled': compiler.enabledNoSuchMethod |
| }; |
| |
| outJson['program'] = generalProgramInfo; |
| |
| ChunkedConversionSink<Object> sink = |
| encoder.startChunkedConversion( |
| new StringConversionSink.fromStringSink(buffer)); |
| sink.add(outJson); |
| compiler.reportInfo(NO_LOCATION_SPANNABLE, |
| const MessageKind( |
| "View the dumped .info.json file at " |
| "https://dart-lang.github.io/dump-info-visualizer")); |
| } |
| } |