blob: c3ccb29e763a70e4cb976bd4d89dee81c309811b [file] [log] [blame] [edit]
// 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;
}