| // 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; |
| |
| import 'dart:convert' show JsonEncoder, JsonDecoder; |
| |
| import 'package:compiler/src/js_model/js_strategy.dart'; |
| import 'package:dart2js_info/binary_serialization.dart' as dump_info; |
| import 'package:dart2js_info/info.dart'; |
| import 'package:dart2js_info/json_info_codec.dart'; |
| import 'package:kernel/ast.dart' as ir; |
| import 'package:kernel/core_types.dart' as ir; |
| |
| import '../compiler_api.dart' as api; |
| import 'common.dart'; |
| import 'common/codegen.dart'; |
| import 'common/elements.dart' show JElementEnvironment; |
| import 'common/names.dart'; |
| import 'common/ram_usage.dart'; |
| import 'common/tasks.dart' show CompilerTask, Measurer; |
| import 'constants/values.dart' show ConstantValue; |
| import 'deferred_load/output_unit.dart' show OutputUnit, deferredPartFileName; |
| import 'elements/entities.dart'; |
| import 'elements/entity_utils.dart' as entity_utils; |
| import 'elements/names.dart'; |
| import 'inferrer/abstract_value_domain.dart'; |
| import 'inferrer/types.dart' |
| show GlobalTypeInferenceMemberResult, GlobalTypeInferenceResults; |
| import 'js/js.dart' as js_ast; |
| import 'js_backend/field_analysis.dart'; |
| import 'js_emitter/code_emitter_task.dart'; |
| import 'js_model/elements.dart'; |
| import 'js_model/js_world.dart' show JClosedWorld; |
| import 'options.dart'; |
| import 'serialization/serialization.dart'; |
| import 'universe/world_impact.dart' show WorldImpact; |
| |
| /// Collects data used for the dump info task. |
| /// |
| /// This registry collects data while JS is being emitted and stores it to be |
| /// processed by and used in the dump info stage. Since it holds references to |
| /// AST nodes it should be cleared with [DumpInfoJsAstRegistry.close] as soon |
| /// as the necessary data for it is extracted. |
| /// |
| /// See [DumpInfoProgramData.fromEmitterResults] for how this data is processed. |
| class DumpInfoJsAstRegistry { |
| final bool _disabled; |
| final CompilerOptions options; |
| |
| final Map<Entity, List<CodeSpan>> _entityCode = {}; |
| final Map<ConstantValue, CodeSpan> _constantCode = {}; |
| |
| // Contains impacts that will be used immediately (if codegen is being run |
| // with this compiler execution) or serialized with partial dump info data. |
| // Impacts are not yet transformed by CodegenImpactTransformer. |
| final Map<MemberEntity, CodegenImpact> _impactRegistry = {}; |
| |
| // Contains members whose impacts should be deserialized from codegen results |
| // on subsequent dump info execution. Empty when dump info is being executed |
| // immediately without serialization. |
| final Set<MemberEntity> _serializedImpactMembers = {}; |
| |
| // Temporary structures used to collect data during the visit process with a |
| // low memory footprint. |
| final Map<js_ast.Node, ConstantValue> _constantRegistry = {}; |
| final Map<js_ast.Node, List<Entity>> _entityRegistry = {}; |
| final List<CodeSpan> _stack = []; |
| DataSinkWriter? _dataSinkWriter; |
| int _impactCount = 0; |
| |
| DumpInfoJsAstRegistry(this.options) |
| : _disabled = |
| !options.stage.emitsDumpInfo && |
| !options.stage.shouldWriteDumpInfoData; |
| |
| bool get useBinaryFormat => options.dumpInfoFormat == DumpInfoFormat.binary; |
| |
| void registerEntityAst(Entity? entity, js_ast.Node code) { |
| if (_disabled) return; |
| if (entity != null) { |
| (_entityRegistry[code] ??= []).add(entity); |
| } |
| } |
| |
| void registerConstantAst(ConstantValue constant, js_ast.Node code) { |
| if (_disabled) return; |
| assert( |
| !_constantRegistry.containsValue(constant) || |
| _constantRegistry[code] == constant, |
| ); |
| _constantRegistry[code] = constant; |
| } |
| |
| void registerDataSinkWriter(DataSinkWriter dataSinkWriter) { |
| _dataSinkWriter = dataSinkWriter..startDeferrable(); |
| } |
| |
| void registerImpact( |
| MemberEntity member, |
| CodegenImpact impact, { |
| required bool isGenerated, |
| }) { |
| if (_disabled) return; |
| if (isGenerated || options.stage.emitsDumpInfo) { |
| if (options.stage.shouldWriteDumpInfoData) { |
| // Serialize immediately so that we don't have to hold a reference to |
| // every impact until the end of the phase. |
| _dataSinkWriter!.writeMember(member); |
| impact.writeToDataSink(_dataSinkWriter!); |
| _impactCount++; |
| } else { |
| _impactRegistry[member] = impact; |
| } |
| } else { |
| _serializedImpactMembers.add(member); |
| } |
| } |
| |
| bool get shouldEmitText => !useBinaryFormat; |
| |
| void enterNode(js_ast.Node node, int start) { |
| if (_disabled) return; |
| if (!_entityRegistry.containsKey(node) && |
| !_constantRegistry.containsKey(node)) { |
| return; |
| } |
| final data = useBinaryFormat ? CodeSpan() : _CodeData(); |
| data.start = start; |
| _stack.add(data); |
| } |
| |
| void emit(String string) { |
| if (shouldEmitText) { |
| // Note: historically we emitted the full body of classes and methods, so |
| // instance methods ended up emitted twice. Once we use a different |
| // encoding of dump info, we also plan to remove this duplication. |
| for (var f in _stack) { |
| (f as _CodeData)._text.write(string); |
| } |
| } |
| } |
| |
| void exitNode(js_ast.Node node, int start, int end, int? closing) { |
| if (_disabled) return; |
| final entities = _entityRegistry.remove(node); |
| final constant = _constantRegistry.remove(node); |
| if (entities == null && constant == null) return; |
| final data = _stack.removeLast(); |
| data.end = end; |
| if (entities != null) { |
| for (var e in entities) { |
| (_entityCode[e] ??= []).add(data); |
| } |
| } |
| if (constant != null) { |
| _constantCode[constant] = data; |
| } |
| } |
| |
| void close() { |
| assert(_stack.isEmpty); |
| assert(_entityRegistry.isEmpty); |
| assert(_constantRegistry.isEmpty); |
| _dataSinkWriter?.endDeferrable(); |
| _entityCode.clear(); |
| _constantCode.clear(); |
| _serializedImpactMembers.clear(); |
| _impactRegistry.clear(); |
| } |
| } |
| |
| /// Only includes entity types stored in [DumpInfoProgramData.entityCode]. |
| enum _EntityType { library, cls, member } |
| |
| class DumpInfoProgramData { |
| final int programSize; |
| final Map<OutputUnit, int> outputUnitSizes; |
| /* Map<String, Map<String, String?|List<String>>> */ |
| final Map<String, Map<String, dynamic>> fragmentDeferredMap; |
| final Iterable<ClassEntity> neededClasses; |
| final Iterable<ClassEntity> neededClassTypes; |
| final Map<Entity, List<CodeSpan>> entityCode; |
| final Map<Entity, int> entityCodeSize; |
| final Map<ConstantValue, CodeSpan> constantCode; |
| |
| /// Contains members that are live and whose impacts are serialized in the |
| /// codegen results. This will be empty if dump info is being run without |
| /// serialization. |
| final Set<MemberEntity> serializedImpactMembers; |
| |
| /// If dump info is being without serialziation, this will contain impacts for |
| /// all live members. Otherwise only contains impacts for members that were |
| /// created during the emitter phase and whose impacts are therefore not |
| /// included in codegen results. |
| final Map<MemberEntity, CodegenImpact> registeredImpacts; |
| |
| DumpInfoProgramData._( |
| this.programSize, |
| this.outputUnitSizes, |
| this.fragmentDeferredMap, |
| this.entityCode, |
| this.entityCodeSize, |
| this.constantCode, |
| this.serializedImpactMembers, |
| this.registeredImpacts, { |
| required this.neededClasses, |
| required this.neededClassTypes, |
| }); |
| |
| factory DumpInfoProgramData.fromEmitterResults( |
| CodeEmitterTask emitterTask, |
| DumpInfoJsAstRegistry dumpInfoRegistry, |
| int programSize, |
| ) { |
| final outputUnitSizes = emitterTask.emitter.generatedSizes; |
| |
| var fragmentsToLoad = emitterTask.emitter.finalizedFragmentsToLoad; |
| var fragmentMerger = emitterTask.emitter.fragmentMerger; |
| final fragmentDeferredMap = fragmentMerger.computeDeferredMap( |
| fragmentsToLoad, |
| ); |
| final neededClasses = emitterTask.neededClasses; |
| final neededClassTypes = emitterTask.neededClassTypes; |
| final entityCode = Map.of(dumpInfoRegistry._entityCode); |
| final entityCodeSize = <Entity, int>{}; |
| entityCode.forEach((entity, spans) { |
| entityCodeSize[entity] = spans.fold( |
| 0, |
| (size, span) => size + (span.end! - span.start!), |
| ); |
| }); |
| final constantCode = Map.of(dumpInfoRegistry._constantCode); |
| return DumpInfoProgramData._( |
| programSize, |
| outputUnitSizes, |
| fragmentDeferredMap, |
| entityCode, |
| entityCodeSize, |
| constantCode, |
| Set.from(dumpInfoRegistry._serializedImpactMembers), |
| Map.from(dumpInfoRegistry._impactRegistry), |
| neededClasses: neededClasses, |
| neededClassTypes: neededClassTypes, |
| ); |
| } |
| |
| static Entity _readEntity(DataSourceReader source) { |
| final entityType = source.readEnum(_EntityType.values); |
| switch (entityType) { |
| case _EntityType.library: |
| return source.readLibrary(); |
| case _EntityType.cls: |
| return source.readClass(); |
| case _EntityType.member: |
| return source.readMember(); |
| } |
| } |
| |
| static void _writeEntity(DataSinkWriter sink, Entity entity) { |
| if (entity is LibraryEntity) { |
| sink.writeEnum(_EntityType.library); |
| sink.writeLibrary(entity); |
| } else if (entity is ClassEntity) { |
| sink.writeEnum(_EntityType.cls); |
| sink.writeClass(entity); |
| } else if (entity is MemberEntity) { |
| sink.writeEnum(_EntityType.member); |
| sink.writeMember(entity); |
| } else { |
| throw UnsupportedError('Unsupported dump info entity: $entity'); |
| } |
| } |
| |
| static CodeSpan _readCodeSpan(DataSourceReader source, bool includeText) { |
| final start = source.readInt(); |
| final end = source.readInt(); |
| final savedText = source.readStringOrNull(); |
| final text = includeText ? savedText : null; |
| return CodeSpan(start: start, end: end, text: text); |
| } |
| |
| static void _writeCodeSpan(DataSinkWriter sink, CodeSpan codeSpan) { |
| sink.writeInt(codeSpan.start!); |
| sink.writeInt(codeSpan.end!); |
| sink.writeStringOrNull(codeSpan.text); |
| } |
| |
| factory DumpInfoProgramData.readFromDataSource( |
| DataSourceReader source, { |
| required bool includeCodeText, |
| }) { |
| late int impactCount; |
| final registeredImpactsDeferrable = source.readDeferrable(( |
| DataSourceReader source, |
| ) { |
| final impacts = <MemberEntity, CodegenImpact>{}; |
| for (var i = 0; i < impactCount; i++) { |
| final member = source.readMember(); |
| final impact = CodegenImpact.readFromDataSource(source); |
| impacts[member] = impact; |
| } |
| return impacts; |
| }); |
| impactCount = source.readInt(); |
| final programSize = source.readInt(); |
| final outputUnitSizesLength = source.readInt(); |
| final outputUnitSizes = <OutputUnit, int>{}; |
| for (int i = 0; i < outputUnitSizesLength; i++) { |
| final outputUnit = source.readOutputUnitReference(); |
| final size = source.readInt(); |
| outputUnitSizes[outputUnit] = size; |
| } |
| final fragmentDeferredMap = source.readStringMap( |
| () => source.readStringMap( |
| () => JsonDecoder().convert(source.readString()), |
| ), |
| ); |
| final neededClasses = source.readList(source.readClass); |
| final neededClassTypes = source.readList(source.readClass); |
| final entityCodeLength = source.readInt(); |
| final entityCode = <Entity, List<CodeSpan>>{}; |
| final entityCodeSize = <Entity, int>{}; |
| for (int i = 0; i < entityCodeLength; i++) { |
| final entity = _readEntity(source); |
| final size = source.readInt(); |
| final codeSpans = source.readList( |
| () => _readCodeSpan(source, includeCodeText), |
| ); |
| entityCode[entity] = codeSpans; |
| entityCodeSize[entity] = size; |
| } |
| final constantCode = <ConstantValue, CodeSpan>{}; |
| final costantCodeLength = source.readInt(); |
| for (int i = 0; i < costantCodeLength; i++) { |
| final constant = source.readConstant(); |
| final codeSpan = _readCodeSpan(source, includeCodeText); |
| constantCode[constant] = codeSpan; |
| } |
| final serializedImpactMembers = source.readMembers().toSet(); |
| return DumpInfoProgramData._( |
| programSize, |
| outputUnitSizes, |
| fragmentDeferredMap, |
| entityCode, |
| entityCodeSize, |
| constantCode, |
| serializedImpactMembers, |
| registeredImpactsDeferrable.loaded(), |
| neededClasses: neededClasses, |
| neededClassTypes: neededClassTypes, |
| ); |
| } |
| |
| void writeToDataSink(DataSinkWriter sink, DumpInfoJsAstRegistry registry) { |
| sink.writeInt(registry._impactCount); |
| sink.writeInt(programSize); |
| sink.writeInt(outputUnitSizes.length); |
| outputUnitSizes.forEach((outputUnit, size) { |
| sink.writeOutputUnitReference(outputUnit); |
| sink.writeInt(size); |
| }); |
| sink.writeStringMap(fragmentDeferredMap, (Map<String, dynamic> innerMap) { |
| sink.writeStringMap( |
| innerMap, |
| (value) => sink.writeString(JsonEncoder().convert(value)), |
| ); |
| }); |
| sink.writeList(neededClasses, sink.writeClass); |
| sink.writeList(neededClassTypes, sink.writeClass); |
| assert(entityCode.length == entityCodeSize.length); |
| sink.writeInt(entityCode.length); |
| entityCode.forEach((entity, codeSpans) { |
| final size = entityCodeSize[entity]!; |
| _writeEntity(sink, entity); |
| sink.writeInt(size); |
| sink.writeList( |
| codeSpans, |
| (CodeSpan codeSpan) => _writeCodeSpan(sink, codeSpan), |
| ); |
| }); |
| sink.writeInt(constantCode.length); |
| constantCode.forEach((constant, codeSpan) { |
| sink.writeConstant(constant); |
| _writeCodeSpan(sink, codeSpan); |
| }); |
| sink.writeMembers(serializedImpactMembers); |
| } |
| } |
| |
| class ElementInfoCollector { |
| final CompilerOptions options; |
| final JClosedWorld closedWorld; |
| final GlobalTypeInferenceResults _globalInferenceResults; |
| final DumpInfoTask dumpInfoTask; |
| |
| JElementEnvironment get environment => closedWorld.elementEnvironment; |
| |
| final state = DumpInfoStateData(); |
| |
| ElementInfoCollector( |
| this.options, |
| this.dumpInfoTask, |
| this.closedWorld, |
| this._globalInferenceResults, |
| ); |
| |
| void run() { |
| dumpInfoTask._dumpInfoData.constantCode.forEach((constant, span) { |
| // TODO(sigmund): add dependencies on other constants |
| var info = ConstantInfo( |
| size: span.end! - span.start!, |
| code: [span], |
| outputUnit: _unitInfoForConstant(constant), |
| ); |
| state.constantToInfo[constant] = info; |
| state.info.constants.add(info); |
| }); |
| environment.libraries.forEach(visitLibrary); |
| } |
| |
| /// Whether to emit information about [entity]. |
| /// |
| /// By default we emit information for any entity that contributes to the |
| /// output size. Either because it is a function being emitted or inlined, |
| /// or because it is an entity that holds dependencies to other entities. |
| bool shouldKeep(Entity entity) { |
| return dumpInfoTask.impacts.containsKey(entity) || |
| dumpInfoTask.inlineCount.containsKey(entity); |
| } |
| |
| LibraryInfo? visitLibrary(LibraryEntity lib) { |
| String libname = environment.getLibraryName(lib); |
| if (libname.isEmpty) { |
| libname = '<unnamed>'; |
| } |
| int size = dumpInfoTask.sizeOf(lib); |
| LibraryInfo info = LibraryInfo(libname, lib.canonicalUri, null, size); |
| state.entityToInfo[lib] = info; |
| |
| environment.forEachLibraryMember(lib, (MemberEntity member) { |
| if (member.isFunction || member.isGetter || member.isSetter) { |
| final functionInfo = visitFunction(member as FunctionEntity); |
| if (functionInfo != null) { |
| info.topLevelFunctions.add(functionInfo); |
| functionInfo.parent = info; |
| } |
| } else if (member is FieldEntity) { |
| final fieldInfo = visitField(member); |
| if (fieldInfo != null) { |
| info.topLevelVariables.add(fieldInfo); |
| fieldInfo.parent = info; |
| } |
| } |
| }); |
| |
| environment.forEachClass(lib, (ClassEntity clazz) { |
| final classTypeInfo = visitClassType(clazz); |
| if (classTypeInfo != null) { |
| info.classTypes.add(classTypeInfo); |
| classTypeInfo.parent = info; |
| } |
| |
| final classInfo = visitClass(clazz); |
| if (classInfo != null) { |
| info.classes.add(classInfo); |
| classInfo.parent = info; |
| } |
| }); |
| |
| if (info.isEmpty && !shouldKeep(lib)) return null; |
| state.info.libraries.add(info); |
| return info; |
| } |
| |
| GlobalTypeInferenceMemberResult _resultOfMember(MemberEntity e) => |
| _globalInferenceResults.resultOfMember(e); |
| |
| AbstractValue _resultOfParameter(Local e, MemberEntity? member) => |
| _globalInferenceResults.resultOfParameter(e, member); |
| |
| FieldInfo? visitField(FieldEntity field) { |
| AbstractValue inferredType = _resultOfMember(field).type; |
| // If a field has an empty inferred type it is never used. |
| if (closedWorld.abstractValueDomain |
| .isEmpty(inferredType) |
| .isDefinitelyTrue) { |
| return null; |
| } |
| |
| int size = dumpInfoTask.sizeOf(field); |
| List<CodeSpan> code = dumpInfoTask.codeOf(field); |
| |
| // TODO(het): Why doesn't `size` account for the code size already? |
| size += code.length; |
| |
| FieldInfo info = FieldInfo( |
| name: field.name!, |
| type: '${environment.getFieldType(field)}', |
| inferredType: '$inferredType', |
| code: code, |
| outputUnit: _unitInfoForMember(field), |
| isConst: field.isConst, |
| ); |
| state.entityToInfo[field] = info; |
| final fieldData = closedWorld.fieldAnalysis.getFieldData(field as JField); |
| if (fieldData.initialValue != null) { |
| info.initializer = |
| state.constantToInfo[fieldData.initialValue] as ConstantInfo?; |
| } |
| |
| if (options.experimentCallInstrumentation) { |
| // We use field.hashCode because it is globally unique and it is |
| // available while we are doing codegen. |
| info.coverageId = '${field.hashCode}'; |
| } |
| |
| int closureSize = _addClosureInfo(info, field); |
| info.size = size + closureSize; |
| |
| state.info.fields.add(info); |
| return info; |
| } |
| |
| ClassTypeInfo? visitClassType(ClassEntity clazz) { |
| // Omit class type if it is not needed. |
| ClassTypeInfo classTypeInfo = ClassTypeInfo( |
| name: clazz.name, |
| outputUnit: _unitInfoForClassType(clazz), |
| ); |
| |
| // TODO(joshualitt): Get accurate size information for class types. |
| classTypeInfo.size = 0; |
| |
| bool isNeeded = dumpInfoTask._dumpInfoData.neededClassTypes.contains(clazz); |
| if (!isNeeded) { |
| return null; |
| } |
| |
| state.info.classTypes.add(classTypeInfo); |
| return classTypeInfo; |
| } |
| |
| /// Returns all immediately extended, implemented, or mixed-in types of |
| /// [clazz]. |
| List<ClassEntity> getImmediateSupers(ClassEntity clazz) { |
| final superclass = environment.getSuperClass( |
| clazz, |
| skipUnnamedMixinApplications: true, |
| ); |
| // Ignore 'Object' to reduce overhead. |
| return [ |
| if (superclass != null && |
| superclass != closedWorld.commonElements.objectClass) |
| superclass, |
| ...closedWorld.dartTypes.getInterfaces(clazz).map((i) => i.element), |
| ]; |
| } |
| |
| ClassInfo? visitClass(ClassEntity clazz) { |
| // True if [info] can be safely removed from the output. |
| bool filterClassInfo(ClassInfo info) => |
| !dumpInfoTask._dumpInfoData.neededClasses.contains(clazz) && |
| info.fields.isEmpty && |
| info.functions.isEmpty; |
| |
| ClassInfo? classInfo = state.entityToInfo[clazz] as ClassInfo?; |
| if (classInfo != null) { |
| return filterClassInfo(classInfo) ? null : classInfo; |
| } |
| final supers = <ClassInfo>[]; |
| getImmediateSupers(clazz).forEach((superInterface) { |
| final superclass = environment.lookupClass( |
| superInterface.library, |
| superInterface.name, |
| ); |
| if (superclass == null) return; |
| final classInfo = visitClass(superclass); |
| if (classInfo == null) return; |
| supers.add(classInfo); |
| }); |
| classInfo = ClassInfo( |
| name: clazz.name, |
| isAbstract: clazz.isAbstract, |
| supers: supers, |
| outputUnit: _unitInfoForClass(clazz), |
| ); |
| state.entityToInfo[clazz] = classInfo; |
| |
| int size = dumpInfoTask.sizeOf(clazz); |
| environment.forEachLocalClassMember(clazz, (member) { |
| if (member.isFunction || member.isGetter || member.isSetter) { |
| final functionInfo = visitFunction(member as FunctionEntity); |
| if (functionInfo != null) { |
| classInfo!.functions.add(functionInfo); |
| functionInfo.parent = classInfo; |
| for (var closureInfo in functionInfo.closures) { |
| size += closureInfo.size; |
| } |
| } |
| } else if (member is FieldEntity) { |
| final fieldInfo = visitField(member); |
| if (fieldInfo != null) { |
| classInfo!.fields.add(fieldInfo); |
| fieldInfo.parent = classInfo; |
| for (var closureInfo in fieldInfo.closures) { |
| size += closureInfo.size; |
| } |
| } |
| } else { |
| throw StateError('Class member not a function or field'); |
| } |
| }); |
| environment.forEachConstructor(clazz, (constructor) { |
| final functionInfo = visitFunction(constructor); |
| if (functionInfo != null) { |
| classInfo!.functions.add(functionInfo); |
| functionInfo.parent = classInfo; |
| for (var closureInfo in functionInfo.closures) { |
| size += closureInfo.size; |
| } |
| } |
| }); |
| |
| classInfo.size = size; |
| |
| if (filterClassInfo(classInfo)) { |
| return null; |
| } |
| |
| state.info.classes.add(classInfo); |
| return classInfo; |
| } |
| |
| ClosureInfo? visitClosureClass(ClassEntity element) { |
| ClosureInfo closureInfo = ClosureInfo( |
| name: element.name, |
| outputUnit: _unitInfoForClass(element), |
| size: dumpInfoTask.sizeOf(element), |
| ); |
| state.entityToInfo[element] = closureInfo; |
| |
| final callMethod = closedWorld.elementEnvironment.lookupClassMember( |
| element, |
| Names.call, |
| ); |
| |
| final functionInfo = visitFunction(callMethod as FunctionEntity); |
| if (functionInfo == null) return null; |
| closureInfo.function = functionInfo; |
| functionInfo.parent = closureInfo; |
| |
| state.info.closures.add(closureInfo); |
| return closureInfo; |
| } |
| |
| FunctionInfo? visitFunction(FunctionEntity function) { |
| int size = dumpInfoTask.sizeOf(function); |
| // TODO(sigmund): consider adding a small info to represent unreachable |
| // code here. |
| if (size == 0 && !shouldKeep(function)) return null; |
| |
| // TODO(het): use 'toString' instead of 'text'? It will add '=' for setters |
| String name = function.memberName.text; |
| int? kind; |
| if (function.isStatic || function.isTopLevel) { |
| kind = FunctionInfo.TOP_LEVEL_FUNCTION_KIND; |
| } else if (function.enclosingClass != null) { |
| kind = FunctionInfo.METHOD_FUNCTION_KIND; |
| } |
| |
| if (function is ConstructorEntity) { |
| name = name == "" |
| ? function.enclosingClass.name |
| : "${function.enclosingClass.name}.${function.name}"; |
| kind = FunctionInfo.CONSTRUCTOR_FUNCTION_KIND; |
| } |
| |
| FunctionModifiers modifiers = FunctionModifiers( |
| isStatic: function.isStatic, |
| isConst: function.isConst, |
| isFactory: function is ConstructorEntity |
| ? function.isFactoryConstructor |
| : false, |
| isExternal: function.isExternal, |
| ); |
| List<CodeSpan> code = dumpInfoTask.codeOf(function); |
| |
| List<ParameterInfo> parameters = <ParameterInfo>[]; |
| List<String> inferredParameterTypes = <String>[]; |
| |
| closedWorld.elementEnvironment.forEachParameterAsLocal( |
| _globalInferenceResults.globalLocalsMap, |
| function, |
| (parameter) { |
| inferredParameterTypes.add( |
| '${_resultOfParameter(parameter, function)}', |
| ); |
| }, |
| ); |
| int parameterIndex = 0; |
| closedWorld.elementEnvironment.forEachParameter(function, (type, name, _) { |
| // Synthesized parameters have no name. This can happen on parameters of |
| // setters derived from lowering late fields. |
| parameters.add( |
| ParameterInfo( |
| name ?? '#t$parameterIndex', |
| inferredParameterTypes[parameterIndex++], |
| '$type', |
| ), |
| ); |
| }); |
| |
| final functionType = environment.getFunctionType(function); |
| String returnType = '${functionType.returnType}'; |
| |
| String inferredReturnType = '${_resultOfMember(function).returnType}'; |
| String sideEffects = |
| '${_globalInferenceResults.inferredData.getSideEffectsOfElement(function)}'; |
| |
| int inlinedCount = dumpInfoTask.inlineCount[function] ?? 0; |
| |
| FunctionInfo info = FunctionInfo( |
| name: name, |
| functionKind: kind!, |
| modifiers: modifiers, |
| returnType: returnType, |
| inferredReturnType: inferredReturnType, |
| parameters: parameters, |
| sideEffects: sideEffects, |
| inlinedCount: inlinedCount, |
| code: code, |
| type: functionType.toString(), |
| outputUnit: _unitInfoForMember(function), |
| ); |
| state.entityToInfo[function] = info; |
| |
| int closureSize = _addClosureInfo(info, function); |
| size += closureSize; |
| |
| if (options.experimentCallInstrumentation) { |
| // We use function.hashCode because it is globally unique and it is |
| // available while we are doing codegen. |
| info.coverageId = '${function.hashCode}'; |
| } |
| |
| info.size = size; |
| |
| state.info.functions.add(info); |
| return info; |
| } |
| |
| /// Adds closure information to [info], using all nested closures in [member]. |
| /// |
| /// Returns the total size of the nested closures, to add to the info size. |
| int _addClosureInfo(Info info, MemberEntity member) { |
| assert(info is FunctionInfo || info is FieldInfo); |
| int size = 0; |
| List<ClosureInfo> nestedClosures = <ClosureInfo>[]; |
| environment.forEachNestedClosure(member, (closure) { |
| final closureInfo = visitClosureClass(closure.enclosingClass!); |
| if (closureInfo != null) { |
| closureInfo.parent = info; |
| nestedClosures.add(closureInfo); |
| size += closureInfo.size; |
| } |
| }); |
| if (info is FunctionInfo) info.closures = nestedClosures; |
| if (info is FieldInfo) info.closures = nestedClosures; |
| |
| return size; |
| } |
| |
| OutputUnitInfo _infoFromOutputUnit(OutputUnit outputUnit) { |
| return state.outputToInfo.putIfAbsent(outputUnit, () { |
| // Dump-info currently only works with the full emitter. If another |
| // emitter is used it will fail here. |
| final filename = outputUnit.isMainOutput |
| ? (options.outputUri?.pathSegments.last ?? 'out') |
| : deferredPartFileName(options, outputUnit.name); |
| OutputUnitInfo info = OutputUnitInfo( |
| filename, |
| outputUnit.name, |
| dumpInfoTask._dumpInfoData.outputUnitSizes[outputUnit]!, |
| ); |
| info.imports.addAll( |
| closedWorld.outputUnitData.getImportNames(outputUnit), |
| ); |
| state.info.outputUnits.add(info); |
| return info; |
| }); |
| } |
| |
| OutputUnitInfo _unitInfoForMember(MemberEntity entity) { |
| return _infoFromOutputUnit( |
| closedWorld.outputUnitData.outputUnitForMember(entity), |
| ); |
| } |
| |
| OutputUnitInfo _unitInfoForClass(ClassEntity entity) { |
| return _infoFromOutputUnit( |
| closedWorld.outputUnitData.outputUnitForClass(entity, allowNull: true), |
| ); |
| } |
| |
| OutputUnitInfo _unitInfoForClassType(ClassEntity entity) { |
| return _infoFromOutputUnit( |
| closedWorld.outputUnitData.outputUnitForClassType( |
| entity, |
| allowNull: true, |
| ), |
| ); |
| } |
| |
| OutputUnitInfo _unitInfoForConstant(ConstantValue constant) { |
| final outputUnit = closedWorld.outputUnitData.outputUnitForConstant( |
| constant, |
| ); |
| return _infoFromOutputUnit(outputUnit); |
| } |
| } |
| |
| class KernelInfoCollector { |
| final ir.Component component; |
| final CompilerOptions options; |
| final JClosedWorld closedWorld; |
| final DumpInfoTask dumpInfoTask; |
| final state = DumpInfoStateData(); |
| final ir.CoreTypes coreTypes; |
| |
| JElementEnvironment get environment => closedWorld.elementEnvironment; |
| |
| KernelInfoCollector( |
| this.component, |
| this.options, |
| this.dumpInfoTask, |
| this.closedWorld, |
| ) : coreTypes = ir.CoreTypes(component); |
| |
| void run() { |
| // TODO(markzipan): Add CFE constants to `state.info.constants`. |
| component.libraries.forEach(visitLibrary); |
| } |
| |
| LibraryInfo? visitLibrary(ir.Library lib) { |
| final libEntity = environment.lookupLibrary(lib.importUri); |
| if (libEntity == null) return null; |
| |
| String? libname = lib.name; |
| if (libname == null || libname.isEmpty) { |
| libname = '${lib.importUri}'; |
| } |
| LibraryInfo info = LibraryInfo(libname, lib.importUri, null, 0); |
| |
| for (var member in lib.members) { |
| final memberEntity = environment.lookupLibraryMember( |
| libEntity, |
| member.name.text, |
| ); |
| if (memberEntity == null) continue; |
| final function = member.function; |
| if (function != null) { |
| final functionInfo = visitFunction( |
| function, |
| functionEntity: memberEntity as FunctionEntity, |
| ); |
| if (functionInfo != null) { |
| info.topLevelFunctions.add(functionInfo); |
| functionInfo.parent = info; |
| } |
| } else { |
| final fieldInfo = visitField( |
| member as ir.Field, |
| fieldEntity: memberEntity as FieldEntity, |
| ); |
| if (fieldInfo != null) { |
| info.topLevelVariables.add(fieldInfo); |
| fieldInfo.parent = info; |
| } |
| } |
| } |
| |
| for (var clazz in lib.classes) { |
| final classEntity = environment.lookupClass(libEntity, clazz.name); |
| if (classEntity == null) continue; |
| |
| final classTypeInfo = visitClassType(clazz); |
| if (classTypeInfo != null) { |
| info.classTypes.add(classTypeInfo); |
| classTypeInfo.parent = info; |
| } |
| |
| final classInfo = visitClass(clazz, classEntity: classEntity); |
| if (classInfo != null) { |
| info.classes.add(classInfo); |
| classInfo.parent = info; |
| } |
| } |
| |
| state.info.libraries.add(info); |
| return info; |
| } |
| |
| FieldInfo? visitField(ir.Field field, {required FieldEntity fieldEntity}) { |
| FieldInfo info = FieldInfo.fromKernel( |
| name: field.name.text, |
| type: field.type.toStringInternal(), |
| isConst: field.isConst, |
| ); |
| |
| if (options.experimentCallInstrumentation) { |
| // We use field.hashCode because it is globally unique and it is |
| // available while we are doing codegen. |
| info.coverageId = '${field.hashCode}'; |
| } |
| |
| _addClosureInfo( |
| info, |
| field, |
| libraryEntity: fieldEntity.library, |
| memberEntity: fieldEntity, |
| ); |
| |
| state.info.fields.add(info); |
| return info; |
| } |
| |
| ClassTypeInfo? visitClassType(ir.Class clazz) { |
| ClassTypeInfo classTypeInfo = ClassTypeInfo(name: clazz.name); |
| state.info.classTypes.add(classTypeInfo); |
| return classTypeInfo; |
| } |
| |
| ClassInfo? visitClass(ir.Class clazz, {required ClassEntity classEntity}) { |
| if (state.entityToInfo[classEntity] != null) { |
| return state.entityToInfo[classEntity] as ClassInfo?; |
| } |
| |
| final supers = <ClassInfo>[]; |
| for (var supertype in clazz.supers) { |
| final superclass = supertype.classNode; |
| // Ignore 'Object' to reduce overhead. |
| if (superclass == coreTypes.objectClass) { |
| continue; |
| } |
| final superclassLibrary = environment.lookupLibrary( |
| superclass.enclosingLibrary.importUri, |
| )!; |
| final superclassEntity = environment.lookupClass( |
| superclassLibrary, |
| superclass.name, |
| ); |
| if (superclassEntity == null) continue; |
| final classInfo = visitClass(superclass, classEntity: superclassEntity); |
| if (classInfo != null) supers.add(classInfo); |
| } |
| |
| ClassInfo classInfo = ClassInfo.fromKernel( |
| name: clazz.name, |
| isAbstract: clazz.isAbstract, |
| supers: supers, |
| ); |
| state.entityToInfo[classEntity] = classInfo; |
| |
| for (var member in clazz.members) { |
| final isSetter = member is ir.Procedure && member.isSetter; |
| // clazz.members includes constructors |
| final name = Name( |
| member.name.text, |
| member.name.isPrivate ? member.name.library!.importUri : null, |
| isSetter: isSetter, |
| ); |
| final memberEntity = |
| environment.lookupLocalClassMember(classEntity, name) ?? |
| environment.lookupConstructor(classEntity, member.name.text); |
| if (memberEntity == null) continue; |
| |
| final function = member.function; |
| if (function != null) { |
| // Multiple kernel members can map to single JWorld member |
| // (e.g., when one of a getter/field pair are tree-shaken), |
| // so avoid duplicating the downstream info object. |
| if (memberEntity is FunctionEntity) { |
| final functionInfo = visitFunction( |
| function, |
| functionEntity: memberEntity, |
| ); |
| if (functionInfo != null) { |
| classInfo.functions.add(functionInfo); |
| functionInfo.parent = classInfo; |
| } |
| } |
| } else { |
| final fieldInfo = visitField( |
| member as ir.Field, |
| fieldEntity: memberEntity as FieldEntity, |
| ); |
| if (fieldInfo != null) { |
| classInfo.fields.add(fieldInfo); |
| fieldInfo.parent = classInfo; |
| } |
| } |
| } |
| |
| state.info.classes.add(classInfo); |
| return classInfo; |
| } |
| |
| FunctionInfo? visitFunction( |
| ir.FunctionNode function, { |
| required FunctionEntity functionEntity, |
| LocalFunctionInfo? localFunctionInfo, |
| }) { |
| final parent = function.parent; |
| bool isClosureCallMethod = parent is ir.LocalFunction; |
| String name = isClosureCallMethod ? 'call' : parent!.toStringInternal(); |
| bool isConstructor = parent is ir.Constructor; |
| bool isFactory = parent is ir.Procedure && parent.isFactory; |
| // Kernel `isStatic` refers to static members, constructors, and top-level |
| // members. |
| bool isTopLevel = |
| (parent is ir.Field && parent.isStatic) || |
| (parent is ir.Procedure && parent.isStatic) || |
| (parent is ir.Member && parent.enclosingClass == null); |
| bool isStaticMember = |
| ((parent is ir.Field && parent.isStatic) || |
| (parent is ir.Procedure && parent.isStatic)) && |
| (parent is ir.Member && parent.enclosingClass != null) && |
| !isConstructor && |
| !isFactory; |
| bool isConst = parent is ir.Member && parent.isConst; |
| bool isExternal = parent is ir.Member && parent.isExternal; |
| bool isMethod = |
| isClosureCallMethod || |
| (parent is ir.Member && parent.enclosingClass != null); |
| bool isGetter = parent is ir.Procedure && parent.isGetter; |
| bool isSetter = parent is ir.Procedure && parent.isSetter; |
| late int kind; |
| if (isStaticMember || isTopLevel) { |
| kind = FunctionInfo.TOP_LEVEL_FUNCTION_KIND; |
| } else if (isMethod) { |
| kind = FunctionInfo.METHOD_FUNCTION_KIND; |
| } |
| if (isConstructor || isFactory) { |
| kind = FunctionInfo.CONSTRUCTOR_FUNCTION_KIND; |
| String functionName = function.toStringInternal(); |
| name = functionName.isEmpty ? name : '$name$functionName'; |
| } else { |
| if (parent!.parent is ir.Class && name.contains('.')) { |
| name = name.split('.')[1]; |
| } |
| } |
| if (name.endsWith('.')) name = name.substring(0, name.length - 1); |
| |
| FunctionModifiers modifiers = FunctionModifiers( |
| isStatic: isStaticMember, |
| isConst: isConst, |
| isFactory: isFactory, |
| isExternal: isExternal, |
| isGetter: isGetter, |
| isSetter: isSetter, |
| ); |
| |
| // TODO(markzipan): Determine if it's safe to default to nonNullable here. |
| final nullability = parent is ir.Member |
| ? parent.enclosingLibrary.nonNullable |
| : ir.Nullability.nonNullable; |
| final functionType = function.computeFunctionType(nullability); |
| |
| FunctionInfo info = FunctionInfo.fromKernel( |
| name: name, |
| functionKind: kind, |
| modifiers: modifiers, |
| returnType: function.returnType.toStringInternal(), |
| type: functionType.toStringInternal(), |
| ); |
| |
| final functionParent = function.parent; |
| if (functionParent is ir.Member) { |
| _addClosureInfo( |
| info, |
| functionParent, |
| libraryEntity: functionEntity.library, |
| memberEntity: functionEntity, |
| ); |
| } else { |
| // This branch is only reached when function is a 'call' method. |
| // TODO(markzipan): Ensure call methods never have children. |
| info.closures = []; |
| } |
| |
| if (options.experimentCallInstrumentation) { |
| // We use function.hashCode because it is globally unique and it is |
| // available while we are doing codegen. |
| info.coverageId = '${function.hashCode}'; |
| } |
| |
| state.info.functions.add(info); |
| return info; |
| } |
| |
| /// Adds closure information to [info], using all nested closures in [member]. |
| void _addClosureInfo( |
| Info info, |
| ir.Member member, { |
| required LibraryEntity libraryEntity, |
| required MemberEntity memberEntity, |
| }) { |
| final localFunctionInfoCollector = LocalFunctionInfoCollector(); |
| member.accept(localFunctionInfoCollector); |
| List<ClosureInfo> nestedClosures = <ClosureInfo>[]; |
| localFunctionInfoCollector.localFunctions.forEach((key, value) { |
| late FunctionEntity closureEntity; |
| int closureOrder = value.order; |
| environment.forEachNestedClosure(memberEntity, (closure) { |
| if (closure.enclosingClass!.name == value.name && |
| (closureOrder-- == 0)) { |
| closureEntity = closure; |
| } |
| }); |
| final closureClassEntity = closureEntity.enclosingClass!; |
| final closureInfo = ClosureInfo.fromKernel(name: value.disambiguatedName); |
| |
| final callMethod = closedWorld.elementEnvironment.lookupClassMember( |
| closureClassEntity, |
| Names.call, |
| ); |
| final functionInfo = visitFunction( |
| key.function, |
| functionEntity: callMethod as FunctionEntity, |
| localFunctionInfo: value, |
| ); |
| |
| closureInfo.function = functionInfo!; |
| functionInfo.parent = closureInfo; |
| state.info.closures.add(closureInfo); |
| |
| closureInfo.parent = info; |
| nestedClosures.add(closureInfo); |
| }); |
| if (info is FunctionInfo) info.closures = nestedClosures; |
| if (info is FieldInfo) info.closures = nestedClosures; |
| } |
| } |
| |
| /// Maps JWorld Entity objects to disambiguated names in order to map them |
| /// to/from Kernel. |
| /// |
| /// This is primarily used for naming closure objects, which rely on Entity |
| /// object identity to determine uniqueness. |
| /// |
| /// Note: this relies on the Kernel traversal order to determine order, which |
| /// may change in the future. |
| class EntityDisambiguator { |
| final nameFrequencies = <String, int>{}; |
| final entityNames = <Entity, String>{}; |
| |
| String name(Entity entity) { |
| final disambiguatedName = entityNames[entity]; |
| if (disambiguatedName != null) { |
| return disambiguatedName; |
| } |
| final entityName = entity.name!; |
| nameFrequencies[entityName] = (nameFrequencies[entityName] ?? -1) + 1; |
| final order = nameFrequencies[entityName]!; |
| entityNames[entity] = order == 0 ? entityName : '$entityName%${order - 1}'; |
| |
| return entityNames[entity]!; |
| } |
| } |
| |
| /// Annotates [KernelInfoCollector] with info extracted from closed-world |
| /// analysis. |
| class DumpInfoAnnotator { |
| final KernelInfoCollector kernelInfo; |
| final CompilerOptions options; |
| final JClosedWorld closedWorld; |
| final GlobalTypeInferenceResults _globalInferenceResults; |
| final DumpInfoTask dumpInfoTask; |
| final entityDisambiguator = EntityDisambiguator(); |
| |
| JElementEnvironment get environment => closedWorld.elementEnvironment; |
| |
| DumpInfoAnnotator( |
| this.kernelInfo, |
| this.options, |
| this.dumpInfoTask, |
| this.closedWorld, |
| this._globalInferenceResults, |
| ); |
| |
| void run() { |
| dumpInfoTask._dumpInfoData.constantCode.forEach((constant, span) { |
| // TODO(sigmund): add dependencies on other constants |
| var info = ConstantInfo( |
| size: span.end! - span.start!, |
| code: [span], |
| outputUnit: _unitInfoForConstant(constant), |
| ); |
| kernelInfo.state.constantToInfo[constant] = info; |
| info.treeShakenStatus = TreeShakenStatus.Live; |
| kernelInfo.state.info.constants.add(info); |
| }); |
| environment.libraries.forEach(visitLibrary); |
| } |
| |
| /// Whether to emit information about [entity]. |
| /// |
| /// By default we emit information for any entity that contributes to the |
| /// output size. Either because it is a function being emitted or inlined, |
| /// or because it is an entity that holds dependencies to other entities. |
| bool shouldKeep(Entity entity) { |
| return dumpInfoTask.impacts.containsKey(entity) || |
| dumpInfoTask.inlineCount.containsKey(entity); |
| } |
| |
| LibraryInfo? visitLibrary(LibraryEntity lib) { |
| var kLibraryInfos = kernelInfo.state.info.libraries.where( |
| (i) => '${i.uri}' == '${lib.canonicalUri}', |
| ); |
| assert( |
| kLibraryInfos.length == 1, |
| 'Ambiguous library resolution. ' |
| 'Expected singleton, found $kLibraryInfos', |
| ); |
| var kLibraryInfo = kLibraryInfos.first; |
| kernelInfo.state.entityToInfo[lib] = kLibraryInfo; |
| |
| String libname = environment.getLibraryName(lib); |
| if (libname.isEmpty) { |
| libname = '${lib.canonicalUri}'; |
| } |
| assert(kLibraryInfo.name == libname); |
| kLibraryInfo.size = dumpInfoTask.sizeOf(lib); |
| |
| environment.forEachLibraryMember(lib, (MemberEntity member) { |
| if (member.isFunction || member.isGetter || member.isSetter) { |
| visitFunction(member as FunctionEntity, libname); |
| } else if (member is FieldEntity) { |
| visitField(member, libname); |
| } else { |
| throw StateError('Class member not a function or field'); |
| } |
| }); |
| |
| environment.forEachClass(lib, (ClassEntity clazz) { |
| visitClassType(clazz, libname); |
| visitClass(clazz, libname); |
| }); |
| |
| bool hasLiveFields = [ |
| ...kLibraryInfo.topLevelFunctions, |
| ...kLibraryInfo.topLevelVariables, |
| ...kLibraryInfo.classes, |
| ...kLibraryInfo.classTypes, |
| ].any((i) => i.treeShakenStatus == TreeShakenStatus.Live); |
| if (!hasLiveFields && !shouldKeep(lib)) return null; |
| kLibraryInfo.treeShakenStatus = TreeShakenStatus.Live; |
| return kLibraryInfo; |
| } |
| |
| GlobalTypeInferenceMemberResult _resultOfMember(MemberEntity e) => |
| _globalInferenceResults.resultOfMember(e); |
| |
| AbstractValue _resultOfParameter(Local e, MemberEntity? member) => |
| _globalInferenceResults.resultOfParameter(e, member); |
| |
| // TODO(markzipan): [parentName] is used for disambiguation, but this might |
| // not always be valid. Check and validate later. |
| FieldInfo? visitField(FieldEntity field, String parentName) { |
| final inferredType = _resultOfMember(field).type; |
| // If a field has an empty inferred type it is never used. |
| if (closedWorld.abstractValueDomain |
| .isEmpty(inferredType) |
| .isDefinitelyTrue) { |
| return null; |
| } |
| |
| final kFieldInfos = kernelInfo.state.info.fields |
| .where( |
| (f) => |
| f.name == field.name && |
| fullyResolvedNameForInfo(f.parent) == parentName, |
| ) |
| .toList(); |
| assert( |
| kFieldInfos.length == 1, |
| 'Ambiguous field resolution. ' |
| 'Expected singleton, found $kFieldInfos', |
| ); |
| final kFieldInfo = kFieldInfos.first; |
| kernelInfo.state.entityToInfo[field] = kFieldInfo; |
| |
| int size = dumpInfoTask.sizeOf(field); |
| List<CodeSpan> code = dumpInfoTask.codeOf(field); |
| |
| // TODO(het): Why doesn't `size` account for the code size already? |
| size += code.length; |
| |
| kFieldInfo.outputUnit = _unitInfoForMember(field); |
| kFieldInfo.inferredType = '$inferredType'; |
| kFieldInfo.code = code; |
| kFieldInfo.treeShakenStatus = TreeShakenStatus.Live; |
| |
| FieldAnalysisData fieldData = closedWorld.fieldAnalysis.getFieldData( |
| field as JField, |
| ); |
| if (fieldData.initialValue != null) { |
| kFieldInfo.initializer = |
| kernelInfo.state.constantToInfo[fieldData.initialValue] |
| as ConstantInfo?; |
| } |
| |
| int closureSize = _addClosureInfo(kFieldInfo, field); |
| kFieldInfo.size = size + closureSize; |
| return kFieldInfo; |
| } |
| |
| // TODO(markzipan): [parentName] is used for disambiguation, but this might |
| // not always be valid. Check and validate later. |
| ClassTypeInfo? visitClassType(ClassEntity clazz, String parentName) { |
| var kClassTypeInfos = kernelInfo.state.info.classTypes.where( |
| (i) => i.name == clazz.name && i.parent!.name == parentName, |
| ); |
| assert( |
| kClassTypeInfos.length == 1, |
| 'Ambiguous class type resolution. ' |
| 'Expected singleton, found $kClassTypeInfos', |
| ); |
| var kClassTypeInfo = kClassTypeInfos.first; |
| |
| // TODO(joshualitt): Get accurate size information for class types. |
| kClassTypeInfo.size = 0; |
| |
| // Omit class type if it is not needed. |
| bool isNeeded = dumpInfoTask._dumpInfoData.neededClassTypes.contains(clazz); |
| if (!isNeeded) return null; |
| |
| assert(kClassTypeInfo.name == clazz.name); |
| kClassTypeInfo.outputUnit = _unitInfoForClassType(clazz); |
| kClassTypeInfo.treeShakenStatus = TreeShakenStatus.Live; |
| return kClassTypeInfo; |
| } |
| |
| // TODO(markzipan): [parentName] is used for disambiguation, but this might |
| // not always be valid. Check and validate later. |
| ClassInfo? visitClass(ClassEntity clazz, String parentName) { |
| final kClassInfos = kernelInfo.state.info.classes |
| .where( |
| (i) => |
| i.name == clazz.name && |
| fullyResolvedNameForInfo(i.parent) == parentName, |
| ) |
| .toList(); |
| assert( |
| kClassInfos.length == 1, |
| 'Ambiguous class resolution. ' |
| 'Expected singleton, found $kClassInfos', |
| ); |
| final kClassInfo = kClassInfos.first; |
| kernelInfo.state.entityToInfo[clazz] = kClassInfo; |
| |
| /// Add synthetically injected superclasses like `Interceptor` and |
| /// `LegacyJavaScriptObject`. |
| final syntheticSuperclass = closedWorld.commonElements.getDefaultSuperclass( |
| clazz, |
| closedWorld.nativeData, |
| ); |
| if (syntheticSuperclass != closedWorld.commonElements.objectClass) { |
| final classInfo = kernelInfo.state.entityToInfo[syntheticSuperclass]; |
| if (classInfo != null) { |
| kClassInfo.supers.add(classInfo as ClassInfo); |
| } |
| } |
| |
| int size = dumpInfoTask.sizeOf(clazz); |
| final disambiguatedMemberName = '$parentName/${clazz.name}'; |
| environment.forEachLocalClassMember(clazz, (member) { |
| // Skip certain incongruent locals that during method alias installation. |
| if (member is JMethod && member.enclosingClass!.name != clazz.name) { |
| return; |
| } |
| if (member.isFunction || member.isGetter || member.isSetter) { |
| final functionInfo = visitFunction( |
| member as FunctionEntity, |
| disambiguatedMemberName, |
| ); |
| if (functionInfo != null) { |
| for (var closureInfo in functionInfo.closures) { |
| size += closureInfo.size; |
| } |
| } |
| } else if (member is FieldEntity) { |
| final fieldInfo = visitField(member, disambiguatedMemberName); |
| if (fieldInfo != null) { |
| for (var closureInfo in fieldInfo.closures) { |
| size += closureInfo.size; |
| } |
| } |
| } else { |
| throw StateError('Class member not a function or field'); |
| } |
| }); |
| environment.forEachConstructor(clazz, (constructor) { |
| final functionInfo = visitFunction(constructor, disambiguatedMemberName); |
| if (functionInfo != null) { |
| for (var closureInfo in functionInfo.closures) { |
| size += closureInfo.size; |
| } |
| } |
| }); |
| kClassInfo.size = size; |
| |
| bool hasLiveFields = [ |
| ...kClassInfo.fields, |
| ...kClassInfo.functions, |
| ].any((i) => i.treeShakenStatus == TreeShakenStatus.Live); |
| if (!dumpInfoTask._dumpInfoData.neededClasses.contains(clazz) && |
| !hasLiveFields) { |
| return null; |
| } |
| |
| kClassInfo.outputUnit = _unitInfoForClass(clazz); |
| kClassInfo.treeShakenStatus = TreeShakenStatus.Live; |
| return kClassInfo; |
| } |
| |
| ClosureInfo? visitClosureClass(ClassEntity element) { |
| final disambiguatedElementName = entityDisambiguator.name(element); |
| final kClosureInfos = kernelInfo.state.info.closures |
| .where((info) => info.name == disambiguatedElementName) |
| .toList(); |
| assert( |
| kClosureInfos.length == 1, |
| 'Ambiguous closure resolution. ' |
| 'Expected singleton, found $kClosureInfos', |
| ); |
| final kClosureInfo = kClosureInfos.first; |
| kernelInfo.state.entityToInfo[element] = kClosureInfo; |
| |
| kClosureInfo.outputUnit = _unitInfoForClass(element); |
| kClosureInfo.size = dumpInfoTask.sizeOf(element); |
| |
| final callMethod = closedWorld.elementEnvironment.lookupClassMember( |
| element, |
| Names.call, |
| ); |
| |
| final functionInfo = visitFunction( |
| callMethod as FunctionEntity, |
| disambiguatedElementName, |
| isClosure: true, |
| ); |
| if (functionInfo == null) return null; |
| |
| kClosureInfo.treeShakenStatus = TreeShakenStatus.Live; |
| return kClosureInfo; |
| } |
| |
| // TODO(markzipan): [parentName] is used for disambiguation, but this might |
| // not always be valid. Check and validate later. |
| FunctionInfo? visitFunction( |
| FunctionEntity function, |
| String parentName, { |
| bool isClosure = false, |
| }) { |
| int size = dumpInfoTask.sizeOf(function); |
| if (size == 0 && !shouldKeep(function)) return null; |
| |
| var compareName = function.name; |
| if (function is ConstructorEntity) { |
| compareName = compareName == "" |
| ? function.enclosingClass.name |
| : "${function.enclosingClass.name}.${function.name}"; |
| } |
| |
| // Multiple kernel members can sometimes map to a single JElement. |
| // [isSetter] and [isGetter] are required for disambiguating these cases. |
| final kFunctionInfos = kernelInfo.state.info.functions |
| .where( |
| (i) => |
| i.name == compareName && |
| (isClosure |
| ? i.parent!.name |
| : fullyResolvedNameForInfo(i.parent)) == |
| parentName && |
| !(function.isGetter ^ i.modifiers.isGetter) && |
| !(function.isSetter ^ i.modifiers.isSetter), |
| ) |
| .toList(); |
| assert( |
| kFunctionInfos.length <= 1, |
| 'Ambiguous function resolution. ' |
| 'Expected single or none, found $kFunctionInfos', |
| ); |
| if (kFunctionInfos.isEmpty) return null; |
| final kFunctionInfo = kFunctionInfos.first; |
| kernelInfo.state.entityToInfo[function] = kFunctionInfo; |
| |
| List<CodeSpan> code = dumpInfoTask.codeOf(function); |
| List<ParameterInfo> parameters = <ParameterInfo>[]; |
| List<String> inferredParameterTypes = <String>[]; |
| |
| closedWorld.elementEnvironment.forEachParameterAsLocal( |
| _globalInferenceResults.globalLocalsMap, |
| function, |
| (parameter) { |
| inferredParameterTypes.add( |
| '${_resultOfParameter(parameter, function)}', |
| ); |
| }, |
| ); |
| int parameterIndex = 0; |
| closedWorld.elementEnvironment.forEachParameter(function, (type, name, _) { |
| // Synthesized parameters have no name. This can happen on parameters of |
| // setters derived from lowering late fields. |
| parameters.add( |
| ParameterInfo( |
| name ?? '#t$parameterIndex', |
| inferredParameterTypes[parameterIndex++], |
| '$type', |
| ), |
| ); |
| }); |
| |
| String inferredReturnType = '${_resultOfMember(function).returnType}'; |
| String sideEffects = |
| '${_globalInferenceResults.inferredData.getSideEffectsOfElement(function)}'; |
| int inlinedCount = dumpInfoTask.inlineCount[function] ?? 0; |
| |
| kFunctionInfo.inferredReturnType = inferredReturnType; |
| kFunctionInfo.sideEffects = sideEffects; |
| kFunctionInfo.inlinedCount = inlinedCount; |
| kFunctionInfo.code = code; |
| kFunctionInfo.parameters = parameters; |
| kFunctionInfo.outputUnit = _unitInfoForMember(function); |
| |
| int closureSize = _addClosureInfo(kFunctionInfo, function); |
| kFunctionInfo.size = size + closureSize; |
| |
| kFunctionInfo.treeShakenStatus = TreeShakenStatus.Live; |
| return kFunctionInfo; |
| } |
| |
| /// Adds closure information to [info], using all nested closures in [member]. |
| /// |
| /// Returns the total size of the nested closures, to add to the info size. |
| int _addClosureInfo(BasicInfo info, MemberEntity member) { |
| assert(info is FunctionInfo || info is FieldInfo); |
| int size = 0; |
| environment.forEachNestedClosure(member, (closure) { |
| final closureInfo = visitClosureClass(closure.enclosingClass!); |
| if (closureInfo != null) { |
| closureInfo.treeShakenStatus = TreeShakenStatus.Live; |
| size += closureInfo.size; |
| } |
| }); |
| return size; |
| } |
| |
| OutputUnitInfo _infoFromOutputUnit(OutputUnit outputUnit) { |
| return kernelInfo.state.outputToInfo.putIfAbsent(outputUnit, () { |
| // Dump-info currently only works with the full emitter. If another |
| // emitter is used it will fail here. |
| final filename = outputUnit.isMainOutput |
| ? (options.outputUri?.pathSegments.last ?? 'out') |
| : deferredPartFileName(options, outputUnit.name); |
| OutputUnitInfo info = OutputUnitInfo( |
| filename, |
| outputUnit.name, |
| dumpInfoTask._dumpInfoData.outputUnitSizes[outputUnit]!, |
| ); |
| info.treeShakenStatus = TreeShakenStatus.Live; |
| info.imports.addAll( |
| closedWorld.outputUnitData.getImportNames(outputUnit), |
| ); |
| kernelInfo.state.info.outputUnits.add(info); |
| return info; |
| }); |
| } |
| |
| OutputUnitInfo _unitInfoForMember(MemberEntity entity) { |
| return _infoFromOutputUnit( |
| closedWorld.outputUnitData.outputUnitForMember(entity), |
| ); |
| } |
| |
| OutputUnitInfo _unitInfoForClass(ClassEntity entity) { |
| return _infoFromOutputUnit( |
| closedWorld.outputUnitData.outputUnitForClass(entity, allowNull: true), |
| ); |
| } |
| |
| OutputUnitInfo _unitInfoForClassType(ClassEntity entity) { |
| return _infoFromOutputUnit( |
| closedWorld.outputUnitData.outputUnitForClassType( |
| entity, |
| allowNull: true, |
| ), |
| ); |
| } |
| |
| OutputUnitInfo _unitInfoForConstant(ConstantValue constant) { |
| OutputUnit outputUnit = closedWorld.outputUnitData.outputUnitForConstant( |
| constant, |
| ); |
| return _infoFromOutputUnit(outputUnit); |
| } |
| } |
| |
| class Selection { |
| final Entity selectedEntity; |
| final Object? receiverConstraint; |
| Selection(this.selectedEntity, this.receiverConstraint); |
| } |
| |
| /// Interface used to record information from different parts of the compiler so |
| /// we can emit them in the dump-info task. |
| // TODO(sigmund,het): move more features here. Ideally the dump-info task |
| // shouldn't reach into internals of other parts of the compiler. For example, |
| // we currently reach into the full emitter and as a result we don't support |
| // dump-info when using the startup-emitter (issue #24190). |
| abstract class InfoReporter { |
| void reportInlined(FunctionEntity element, MemberEntity inlinedFrom); |
| } |
| |
| class DumpInfoTask extends CompilerTask implements InfoReporter { |
| final CompilerOptions options; |
| final api.CompilerOutput outputProvider; |
| final DiagnosticReporter reporter; |
| final Measurer measurer; |
| final bool useBinaryFormat; |
| |
| DumpInfoTask(this.options, this.measurer, this.outputProvider, this.reporter) |
| : useBinaryFormat = options.dumpInfoFormat == DumpInfoFormat.binary, |
| super(measurer); |
| |
| @override |
| String get name => "Dump Info"; |
| |
| /// The size of the generated output. |
| late DumpInfoProgramData _dumpInfoData; |
| |
| final Map<Entity, int> inlineCount = <Entity, int>{}; |
| |
| // A mapping from an entity to a list of entities that are |
| // inlined inside of it. |
| final Map<Entity, List<Entity>> inlineMap = <Entity, List<Entity>>{}; |
| |
| final Map<MemberEntity, WorldImpact> impacts = {}; |
| |
| /// Register the size of the generated output. |
| void registerDumpInfoProgramData(DumpInfoProgramData dumpInfoData) { |
| _dumpInfoData = dumpInfoData; |
| } |
| |
| @override |
| void reportInlined(FunctionEntity element, MemberEntity inlinedFrom) { |
| inlineCount.update(element, (i) => i + 1, ifAbsent: () => 1); |
| inlineMap.putIfAbsent(inlinedFrom, () => <Entity>[]); |
| inlineMap[inlinedFrom]!.add(element); |
| } |
| |
| void unregisterImpact(MemberEntity impactSource) { |
| impacts.remove(impactSource); |
| } |
| |
| /// Returns an iterable of [Selection]s that are used by [entity]. Each |
| /// [Selection] contains an entity that is used and the selector that |
| /// selected the entity. |
| Iterable<Selection> getRetaining( |
| MemberEntity entity, |
| JClosedWorld closedWorld, |
| ) { |
| final impact = impacts[entity]; |
| if (impact == null) return const <Selection>[]; |
| |
| var selections = <Selection>[]; |
| impact.forEachDynamicUse((_, dynamicUse) { |
| final mask = dynamicUse.receiverConstraint as AbstractValue?; |
| selections.addAll( |
| closedWorld |
| // TODO(het): Handle `call` on `Closure` through |
| // `world.includesClosureCall`. |
| .locateMembers(dynamicUse.selector, mask) |
| .map((MemberEntity e) => Selection(e, mask)), |
| ); |
| }); |
| impact.forEachStaticUse((_, staticUse) { |
| selections.add(Selection(staticUse.element, null)); |
| }); |
| unregisterImpact(entity); |
| return selections; |
| } |
| |
| /// Returns the size of the source code that was generated for an entity. |
| /// If no source code was produced, return 0. |
| int sizeOf(Entity entity) { |
| return _dumpInfoData.entityCodeSize[entity] ?? 0; |
| } |
| |
| List<CodeSpan> codeOf(MemberEntity entity) { |
| return _dumpInfoData.entityCode[entity] ?? const []; |
| } |
| |
| void _populateImpacts( |
| JClosedWorld closedWorld, |
| CodegenResults codegenResults, |
| JsBackendStrategy backendStrategy, |
| ) { |
| backendStrategy.initialize(closedWorld, codegenResults.codegenInputs); |
| |
| _dumpInfoData.registeredImpacts.forEach((member, impact) { |
| impacts[member] = backendStrategy.transformCodegenImpact(impact); |
| }); |
| for (final member in _dumpInfoData.serializedImpactMembers) { |
| final (:result, :isGenerated) = codegenResults.getCodegenResults(member); |
| assert(!isGenerated, 'Should not be generating impact: $member'); |
| impacts[member] = backendStrategy.transformCodegenImpact(result.impact); |
| } |
| } |
| |
| Future<DumpInfoStateData> dumpInfo( |
| JClosedWorld closedWorld, |
| GlobalTypeInferenceResults globalInferenceResults, |
| CodegenResults codegenResults, |
| JsBackendStrategy backendStrategy, |
| ) async { |
| late DumpInfoStateData dumpInfoState; |
| await measure(() async { |
| _populateImpacts(closedWorld, codegenResults, backendStrategy); |
| |
| ElementInfoCollector elementInfoCollector = ElementInfoCollector( |
| options, |
| this, |
| closedWorld, |
| globalInferenceResults, |
| )..run(); |
| |
| dumpInfoState = await buildDumpInfoData( |
| closedWorld, |
| elementInfoCollector, |
| ); |
| if (useBinaryFormat) { |
| dumpInfoBinary(dumpInfoState.info); |
| } else { |
| dumpInfoJson(dumpInfoState.info); |
| } |
| return; |
| }); |
| return dumpInfoState; |
| } |
| |
| Future<DumpInfoStateData> dumpInfoNew( |
| ir.Component component, |
| JClosedWorld closedWorld, |
| GlobalTypeInferenceResults globalInferenceResults, |
| CodegenResults codegenResults, |
| JsBackendStrategy backendStrategy, |
| ) async { |
| late DumpInfoStateData dumpInfoState; |
| await measure(() async { |
| _populateImpacts(closedWorld, codegenResults, backendStrategy); |
| |
| KernelInfoCollector kernelInfoCollector = KernelInfoCollector( |
| component, |
| options, |
| this, |
| closedWorld, |
| )..run(); |
| |
| DumpInfoAnnotator( |
| kernelInfoCollector, |
| options, |
| this, |
| closedWorld, |
| globalInferenceResults, |
| ).run(); |
| |
| dumpInfoState = await buildDumpInfoDataNew( |
| closedWorld, |
| kernelInfoCollector, |
| ); |
| TreeShakingInfoVisitor().filter(dumpInfoState.info); |
| |
| if (useBinaryFormat) { |
| dumpInfoBinary(dumpInfoState.info); |
| } else { |
| dumpInfoJson(dumpInfoState.info); |
| } |
| }); |
| return dumpInfoState; |
| } |
| |
| void dumpInfoJson(AllInfo data) { |
| JsonEncoder encoder = const JsonEncoder.withIndent(' '); |
| final name = (options.outputUri?.pathSegments.last ?? 'out'); |
| final outputSink = outputProvider.createOutputSink( |
| name, |
| 'info.json', |
| api.OutputType.dumpInfo, |
| ); |
| final sink = encoder.startChunkedConversion( |
| _BufferedStringOutputSink(outputSink), |
| ); |
| sink.add(AllInfoJsonCodec(isBackwardCompatible: true).encode(data)); |
| reporter.reportInfoMessage(noLocationSpannable, MessageKind.generic, { |
| 'text': |
| 'Learn how to process the dumped .info.json file at ' |
| 'https://dart.dev/go/dart2js-info', |
| }); |
| } |
| |
| void dumpInfoBinary(AllInfo data) { |
| final name = "${options.outputUri?.pathSegments.last ?? 'out'}.info.data"; |
| Sink<List<int>> sink = outputProvider.createBinarySink( |
| options.outputUri!.resolve(name), |
| ); |
| dump_info.encode(data, sink); |
| reporter.reportInfoMessage(noLocationSpannable, MessageKind.generic, { |
| 'text': |
| 'Learn how to parse and process the dumped .info.data file at ' |
| 'https://dart.dev/go/dart2js-info', |
| }); |
| } |
| |
| Future<DumpInfoStateData> buildDumpInfoData( |
| JClosedWorld closedWorld, |
| ElementInfoCollector infoCollector, |
| ) async { |
| Stopwatch stopwatch = Stopwatch(); |
| stopwatch.start(); |
| |
| DumpInfoStateData result = infoCollector.state; |
| |
| // Recursively build links to function uses |
| final functionEntities = infoCollector.state.entityToInfo.keys |
| .whereType<FunctionEntity>(); |
| for (final entity in functionEntities) { |
| final info = infoCollector.state.entityToInfo[entity] as FunctionInfo; |
| Iterable<Selection> uses = getRetaining(entity, closedWorld); |
| // Don't bother recording an empty list of dependencies. |
| for (Selection selection in uses) { |
| // Don't register dart2js builtin functions that are not recorded. |
| final useInfo = |
| infoCollector.state.entityToInfo[selection.selectedEntity]; |
| if (useInfo == null) continue; |
| info.uses.add( |
| DependencyInfo(useInfo, selection.receiverConstraint?.toString()), |
| ); |
| } |
| } |
| |
| // Recursively build links to field uses |
| final fieldEntity = infoCollector.state.entityToInfo.keys |
| .whereType<FieldEntity>(); |
| for (final entity in fieldEntity) { |
| final info = infoCollector.state.entityToInfo[entity] as FieldInfo; |
| Iterable<Selection> uses = getRetaining(entity, closedWorld); |
| // Don't bother recording an empty list of dependencies. |
| for (Selection selection in uses) { |
| final useInfo = |
| infoCollector.state.entityToInfo[selection.selectedEntity]; |
| if (useInfo == null) continue; |
| info.uses.add( |
| DependencyInfo(useInfo, selection.receiverConstraint?.toString()), |
| ); |
| } |
| } |
| |
| // Track dependencies that come from inlining. |
| for (Entity entity in inlineMap.keys) { |
| final outerInfo = infoCollector.state.entityToInfo[entity] as CodeInfo?; |
| if (outerInfo == null) continue; |
| for (final inlined in inlineMap[entity]!) { |
| final inlinedInfo = infoCollector.state.entityToInfo[inlined]; |
| if (inlinedInfo == null) continue; |
| outerInfo.uses.add(DependencyInfo(inlinedInfo, 'inlined')); |
| } |
| } |
| |
| result.info.deferredFiles = _dumpInfoData.fragmentDeferredMap; |
| stopwatch.stop(); |
| |
| final ramUsage = |
| (options.omitMemorySummary ? null : await currentHeapCapacityInMb()) ?? |
| 'N/A MB'; |
| |
| result.info.program = ProgramInfo( |
| entrypoint: |
| infoCollector.state.entityToInfo[closedWorld |
| .elementEnvironment |
| .mainFunction] |
| as FunctionInfo, |
| size: _dumpInfoData.programSize, |
| ramUsage: ramUsage, |
| dart2jsVersion: options.hasBuildId ? options.buildId : null, |
| compilationMoment: DateTime.now(), |
| compilationDuration: measurer.elapsedWallClock, |
| toJsonDuration: Duration(milliseconds: stopwatch.elapsedMilliseconds), |
| dumpInfoDuration: Duration(milliseconds: timing), |
| noSuchMethodEnabled: closedWorld.backendUsage.isNoSuchMethodUsed, |
| isRuntimeTypeUsed: closedWorld.backendUsage.isRuntimeTypeUsed, |
| isIsolateInUse: false, |
| isFunctionApplyUsed: closedWorld.backendUsage.isFunctionApplyUsed, |
| minified: options.enableMinification, |
| ); |
| |
| return result; |
| } |
| |
| Future<DumpInfoStateData> buildDumpInfoDataNew( |
| JClosedWorld closedWorld, |
| KernelInfoCollector infoCollector, |
| ) async { |
| Stopwatch stopwatch = Stopwatch(); |
| stopwatch.start(); |
| |
| DumpInfoStateData result = infoCollector.state; |
| |
| // Recursively build links to function uses |
| final functionEntities = infoCollector.state.entityToInfo.keys |
| .whereType<FunctionEntity>(); |
| for (final entity in functionEntities) { |
| final info = infoCollector.state.entityToInfo[entity] as FunctionInfo; |
| Iterable<Selection> uses = getRetaining(entity, closedWorld); |
| // Don't bother recording an empty list of dependencies. |
| for (Selection selection in uses) { |
| // Don't register dart2js builtin functions that are not recorded. |
| final useInfo = |
| infoCollector.state.entityToInfo[selection.selectedEntity]; |
| if (useInfo == null) continue; |
| if (useInfo.treeShakenStatus != TreeShakenStatus.Live) continue; |
| info.uses.add( |
| DependencyInfo(useInfo, selection.receiverConstraint?.toString()), |
| ); |
| } |
| } |
| |
| // Recursively build links to field uses |
| final fieldEntity = infoCollector.state.entityToInfo.keys |
| .whereType<FieldEntity>(); |
| for (final entity in fieldEntity) { |
| final info = infoCollector.state.entityToInfo[entity] as FieldInfo; |
| Iterable<Selection> uses = getRetaining(entity, closedWorld); |
| // Don't bother recording an empty list of dependencies. |
| for (Selection selection in uses) { |
| final useInfo = |
| infoCollector.state.entityToInfo[selection.selectedEntity]; |
| if (useInfo == null) continue; |
| if (useInfo.treeShakenStatus != TreeShakenStatus.Live) continue; |
| info.uses.add( |
| DependencyInfo(useInfo, selection.receiverConstraint?.toString()), |
| ); |
| } |
| } |
| |
| // Track dependencies that come from inlining. |
| for (Entity entity in inlineMap.keys) { |
| final outerInfo = infoCollector.state.entityToInfo[entity] as CodeInfo?; |
| if (outerInfo == null) continue; |
| for (final inlined in inlineMap[entity]!) { |
| final inlinedInfo = infoCollector.state.entityToInfo[inlined]; |
| if (inlinedInfo == null) continue; |
| if (inlinedInfo.treeShakenStatus != TreeShakenStatus.Live) continue; |
| outerInfo.uses.add(DependencyInfo(inlinedInfo, 'inlined')); |
| } |
| } |
| |
| result.info.deferredFiles = _dumpInfoData.fragmentDeferredMap; |
| stopwatch.stop(); |
| |
| final ramUsage = |
| (options.omitMemorySummary ? null : await currentHeapCapacityInMb()) ?? |
| 'N/A MB'; |
| |
| result.info.program = ProgramInfo( |
| entrypoint: |
| infoCollector.state.entityToInfo[closedWorld |
| .elementEnvironment |
| .mainFunction] |
| as FunctionInfo, |
| size: _dumpInfoData.programSize, |
| ramUsage: ramUsage, |
| dart2jsVersion: options.hasBuildId ? options.buildId : null, |
| compilationMoment: DateTime.now(), |
| compilationDuration: measurer.elapsedWallClock, |
| toJsonDuration: Duration(milliseconds: stopwatch.elapsedMilliseconds), |
| dumpInfoDuration: Duration(milliseconds: timing), |
| noSuchMethodEnabled: closedWorld.backendUsage.isNoSuchMethodUsed, |
| isRuntimeTypeUsed: closedWorld.backendUsage.isRuntimeTypeUsed, |
| isIsolateInUse: false, |
| isFunctionApplyUsed: closedWorld.backendUsage.isFunctionApplyUsed, |
| minified: options.enableMinification, |
| ); |
| |
| return result; |
| } |
| } |
| |
| class _BufferedStringOutputSink implements Sink<String> { |
| StringBuffer buffer = StringBuffer(); |
| final Sink<String> outputSink; |
| static const int _maxLength = 1024 * 1024 * 500; |
| |
| _BufferedStringOutputSink(this.outputSink); |
| |
| @override |
| void add(String data) { |
| buffer.write(data); |
| if (buffer.length > _maxLength) { |
| outputSink.add(buffer.toString()); |
| buffer.clear(); |
| } |
| } |
| |
| @override |
| void close() { |
| outputSink.add(buffer.toString()); |
| outputSink.close(); |
| } |
| } |
| |
| /// Helper class to store what dump-info will show for a piece of code. |
| // TODO(sigmund): delete once we no longer emit text by default. |
| class _CodeData extends CodeSpan { |
| final StringBuffer _text = StringBuffer(); |
| |
| @override |
| String get text => '$_text'; |
| int get length => end! - start!; |
| } |
| |
| /// Holds dump-info's mutable state. |
| class DumpInfoStateData { |
| final AllInfo info = AllInfo(); |
| final Map<Entity, Info> entityToInfo = <Entity, Info>{}; |
| final Map<ConstantValue, Info> constantToInfo = <ConstantValue, Info>{}; |
| final Map<OutputUnit, OutputUnitInfo> outputToInfo = {}; |
| |
| DumpInfoStateData(); |
| } |
| |
| class LocalFunctionInfo { |
| final ir.LocalFunction localFunction; |
| final String name; |
| final int order; |
| bool isInvoked = false; |
| |
| LocalFunctionInfo(this.localFunction, this.name, this.order); |
| |
| String get disambiguatedName => order == 0 ? name : '$name%${order - 1}'; |
| } |
| |
| class LocalFunctionInfoCollector extends ir.RecursiveVisitor { |
| final localFunctions = <ir.LocalFunction, LocalFunctionInfo>{}; |
| final localFunctionNameCount = <String, int>{}; |
| |
| LocalFunctionInfo generateLocalFunctionInfo(ir.LocalFunction localFunction) { |
| final name = _computeClosureName(localFunction); |
| localFunctionNameCount[name] = (localFunctionNameCount[name] ?? -1) + 1; |
| return LocalFunctionInfo( |
| localFunction, |
| name, |
| localFunctionNameCount[name]!, |
| ); |
| } |
| |
| @override |
| void visitFunctionExpression(ir.FunctionExpression node) { |
| assert(!localFunctions.containsKey(node)); |
| localFunctions[node] = generateLocalFunctionInfo(node); |
| defaultExpression(node); |
| } |
| |
| @override |
| void visitFunctionDeclaration(ir.FunctionDeclaration node) { |
| assert(!localFunctions.containsKey(node)); |
| localFunctions[node] = generateLocalFunctionInfo(node); |
| defaultStatement(node); |
| } |
| |
| @override |
| void visitLocalFunctionInvocation(ir.LocalFunctionInvocation node) { |
| if (localFunctions[node.localFunction] == null) { |
| visitFunctionDeclaration(node.localFunction); |
| } |
| localFunctions[node.localFunction]!.isInvoked = true; |
| } |
| } |
| |
| // Returns a non-unique name for the given closure element. |
| // |
| // Must be kept logically identical to js_model/element_map_impl.dart. |
| String _computeClosureName(ir.TreeNode treeNode) { |
| String reconstructConstructorName(ir.Member node) { |
| String className = node.enclosingClass!.name; |
| return node.name.text == '' ? className : '$className\$${node.name.text}'; |
| } |
| |
| var parts = <String>[]; |
| // First anonymous is called 'closure', outer ones called '' to give a |
| // compound name where increasing nesting level corresponds to extra |
| // underscores. |
| var anonymous = 'closure'; |
| ir.TreeNode? current = treeNode; |
| while (current != null) { |
| var node = current; |
| if (node is ir.FunctionExpression) { |
| parts.add(anonymous); |
| anonymous = ''; |
| } else if (node is ir.FunctionDeclaration) { |
| final name = node.variable.name; |
| if (name != null && name != "") { |
| parts.add(entity_utils.operatorNameToIdentifier(name)!); |
| } else { |
| parts.add(anonymous); |
| anonymous = ''; |
| } |
| } else if (node is ir.Class) { |
| parts.add(node.name); |
| break; |
| } else if (node is ir.Procedure) { |
| if (node.kind == ir.ProcedureKind.Factory) { |
| parts.add(reconstructConstructorName(node)); |
| } else { |
| parts.add(entity_utils.operatorNameToIdentifier(node.name.text)!); |
| } |
| } else if (node is ir.Constructor) { |
| parts.add(reconstructConstructorName(node)); |
| break; |
| } else if (node is ir.Field) { |
| // Add the field name for closures in field initializers. |
| String name = node.name.text; |
| parts.add(name); |
| } |
| current = current.parent; |
| } |
| return parts.reversed.join('_'); |
| } |
| |
| /// Filters dead code from Dart2JS [Info] trees. |
| class TreeShakingInfoVisitor extends InfoVisitor<void> { |
| List<T> filterDeadInfo<T extends Info>(List<T> infos) { |
| return infos |
| .where((info) => info.treeShakenStatus == TreeShakenStatus.Live) |
| .toList(); |
| } |
| |
| void filter(AllInfo info) { |
| info.program = info.program; |
| info.libraries = filterDeadInfo<LibraryInfo>(info.libraries); |
| info.functions = filterDeadInfo<FunctionInfo>(info.functions); |
| info.typedefs = filterDeadInfo<TypedefInfo>(info.typedefs); |
| info.typedefs = filterDeadInfo<TypedefInfo>(info.typedefs); |
| info.classes = filterDeadInfo<ClassInfo>(info.classes); |
| info.classTypes = filterDeadInfo<ClassTypeInfo>(info.classTypes); |
| info.fields = filterDeadInfo<FieldInfo>(info.fields); |
| info.constants = filterDeadInfo<ConstantInfo>(info.constants); |
| info.closures = filterDeadInfo<ClosureInfo>(info.closures); |
| info.outputUnits = filterDeadInfo<OutputUnitInfo>(info.outputUnits); |
| info.deferredFiles = info.deferredFiles; |
| // TODO(markzipan): 'dependencies' is always empty. Revisit this if/when |
| // this holds meaningful information. |
| info.dependencies = info.dependencies; |
| info.accept(this); |
| } |
| |
| @override |
| void visitAll(AllInfo info) { |
| info.libraries = filterDeadInfo<LibraryInfo>(info.libraries); |
| info.constants = filterDeadInfo<ConstantInfo>(info.constants); |
| |
| info.libraries.forEach(visitLibrary); |
| info.constants.forEach(visitConstant); |
| } |
| |
| @override |
| void visitProgram(ProgramInfo info) {} |
| |
| @override |
| void visitLibrary(LibraryInfo info) { |
| info.topLevelFunctions = filterDeadInfo<FunctionInfo>( |
| info.topLevelFunctions, |
| ); |
| info.topLevelVariables = filterDeadInfo<FieldInfo>(info.topLevelVariables); |
| info.classes = filterDeadInfo<ClassInfo>(info.classes); |
| info.classTypes = filterDeadInfo<ClassTypeInfo>(info.classTypes); |
| info.typedefs = filterDeadInfo<TypedefInfo>(info.typedefs); |
| |
| info.topLevelFunctions.forEach(visitFunction); |
| info.topLevelVariables.forEach(visitField); |
| info.classes.forEach(visitClass); |
| info.classTypes.forEach(visitClassType); |
| info.typedefs.forEach(visitTypedef); |
| } |
| |
| @override |
| void visitClass(ClassInfo info) { |
| info.functions = filterDeadInfo<FunctionInfo>(info.functions); |
| info.fields = filterDeadInfo<FieldInfo>(info.fields); |
| info.supers = filterDeadInfo<ClassInfo>(info.supers); |
| |
| info.functions.forEach(visitFunction); |
| info.fields.forEach(visitField); |
| info.supers.forEach(visitClass); |
| } |
| |
| @override |
| void visitClassType(ClassTypeInfo info) {} |
| |
| @override |
| void visitField(FieldInfo info) { |
| info.closures = filterDeadInfo<ClosureInfo>(info.closures); |
| |
| info.closures.forEach(visitClosure); |
| } |
| |
| @override |
| void visitConstant(ConstantInfo info) {} |
| |
| @override |
| void visitFunction(FunctionInfo info) { |
| info.closures = filterDeadInfo<ClosureInfo>(info.closures); |
| |
| info.closures.forEach(visitClosure); |
| } |
| |
| @override |
| void visitTypedef(TypedefInfo info) {} |
| @override |
| void visitOutput(OutputUnitInfo info) {} |
| @override |
| void visitClosure(ClosureInfo info) { |
| visitFunction(info.function); |
| } |
| } |
| |
| /// Returns a fully resolved name for [info] for disambiguation. |
| String fullyResolvedNameForInfo(Info? info) { |
| if (info == null) return ''; |
| var name = info.name; |
| var currentInfo = info; |
| while (currentInfo.parent != null) { |
| currentInfo = currentInfo.parent!; |
| name = '${currentInfo.name}/$name'; |
| } |
| return name; |
| } |