diff --git a/pkg/compiler/lib/src/io/code_output.dart b/pkg/compiler/lib/src/io/code_output.dart index 4e48c168cec..9ccc4b350b0 100644 --- a/pkg/compiler/lib/src/io/code_output.dart +++ b/pkg/compiler/lib/src/io/code_output.dart @@ -16,7 +16,12 @@ abstract class CodeOutputListener { void onDone(int length); } -/// Interface for a mapping of target offsets to source locations. +/// Interface for a mapping of target offsets to source locations and for +/// tracking inlining frame data. +/// +/// Source-location mapping is used to build standard source-maps files. +/// Inlining frames is used to attach an extension to source-map files to +/// improve deobfuscation of production stack traces. abstract class SourceLocations { /// The name identifying this source mapping. String get name; @@ -24,15 +29,31 @@ abstract class SourceLocations { /// Adds a [sourceLocation] at the specified [targetOffset]. void addSourceLocation(int targetOffset, SourceLocation sourcePosition); + /// Record an inlining call at the [targetOffset]. + /// + /// The inlining call-site was made from [pushLocation] and calls + /// [inlinedMethodName]. + void addPush( + int targetOffset, SourceLocation pushPosition, String inlinedMethodName); + + /// Record a return of an inlining call at the [targetOffset]. + /// + /// [isEmpty] indicates that this return also makes the inlining stack empty. + void addPop(int targetOffset, bool isEmpty); + /// Applies [f] to every target offset and associated source location. void forEachSourceLocation( void f(int targetOffset, SourceLocation sourceLocation)); + + /// Recorded inlining data per target-offset. + Map> get frameMarkers; } class _SourceLocationsImpl implements SourceLocations { final String name; final AbstractCodeOutput codeOutput; Map> markers = >{}; + Map> frameMarkers = >{}; _SourceLocationsImpl(this.name, this.codeOutput); @@ -44,6 +65,21 @@ class _SourceLocationsImpl implements SourceLocations { sourceLocations.add(sourceLocation); } + @override + void addPush(int targetOffset, SourceLocation sourceLocation, + String inlinedMethodName) { + assert(targetOffset <= codeOutput.length); + List frames = frameMarkers[targetOffset] ??= []; + frames.add(new FrameEntry.push(sourceLocation, inlinedMethodName)); + } + + @override + void addPop(int targetOffset, bool isEmpty) { + assert(targetOffset <= codeOutput.length); + List frames = frameMarkers[targetOffset] ??= []; + frames.add(new FrameEntry.pop(isEmpty)); + } + @override void forEachSourceLocation( void f(int targetOffset, SourceLocation sourceLocation)) { @@ -54,17 +90,22 @@ class _SourceLocationsImpl implements SourceLocations { }); } - void _addSourceLocations(_SourceLocationsImpl other) { + void _merge(_SourceLocationsImpl other) { assert(name == other.name); + int length = codeOutput.length; if (other.markers.length > 0) { other.markers .forEach((int targetOffset, List sourceLocations) { - markers - .putIfAbsent( - codeOutput.length + targetOffset, () => []) + (markers[length + targetOffset] ??= []) .addAll(sourceLocations); }); } + + if (other.frameMarkers.length > 0) { + other.frameMarkers.forEach((int targetOffset, List frames) { + (frameMarkers[length + targetOffset] ??= []).addAll(frames); + }); + } } } @@ -117,7 +158,7 @@ abstract class AbstractCodeOutput extends CodeOutput { @override void addBuffer(CodeBuffer other) { other.sourceLocationsMap.forEach((String name, _SourceLocationsImpl other) { - createSourceLocations(name)._addSourceLocations(other); + createSourceLocations(name)._merge(other); }); if (!other.isClosed) { other.close(); @@ -138,8 +179,7 @@ abstract class AbstractCodeOutput extends CodeOutput { @override _SourceLocationsImpl createSourceLocations(String name) { - return sourceLocationsMap.putIfAbsent( - name, () => new _SourceLocationsImpl(name, this)); + return sourceLocationsMap[name] ??= new _SourceLocationsImpl(name, this); } } diff --git a/pkg/compiler/lib/src/io/kernel_source_information.dart b/pkg/compiler/lib/src/io/kernel_source_information.dart index 6a7addfeae5..b70af803fd3 100644 --- a/pkg/compiler/lib/src/io/kernel_source_information.dart +++ b/pkg/compiler/lib/src/io/kernel_source_information.dart @@ -74,9 +74,19 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { final MemberEntity _member; final String _name; + /// Inlining context or null when no inlining has taken place. + /// + /// A new builder is created every time the backend inlines a method. This + /// field contains the location of every call site that has been inlined. The + /// last entry on the list is always a call to [_member]. + final List inliningContext; + KernelSourceInformationBuilder(this._elementMap, this._member) - : this._name = - computeKernelElementNameForSourceMaps(_elementMap, _member); + : _name = computeKernelElementNameForSourceMaps(_elementMap, _member), + inliningContext = null; + + KernelSourceInformationBuilder.withContext( + this._elementMap, this._member, this.inliningContext, this._name); /// Returns the [SourceLocation] for the [offset] within [node] using [name] /// as the name of the source location. @@ -106,8 +116,10 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { SourceInformation _buildFunction( String name, ir.TreeNode node, ir.FunctionNode functionNode) { if (functionNode.fileEndOffset != ir.TreeNode.noOffset) { - return new PositionSourceInformation(_getSourceLocation(name, node), - _getSourceLocation(name, functionNode, functionNode.fileEndOffset)); + return new PositionSourceInformation( + _getSourceLocation(name, node), + _getSourceLocation(name, functionNode, functionNode.fileEndOffset), + this.inliningContext); } return _buildTreeNode(node); } @@ -156,7 +168,9 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { ir.TreeNode node, ir.FunctionNode functionNode) { if (functionNode.fileEndOffset != ir.TreeNode.noOffset) { return new PositionSourceInformation( - _getSourceLocation(_name, functionNode, functionNode.fileEndOffset)); + _getSourceLocation(_name, functionNode, functionNode.fileEndOffset), + null, + this.inliningContext); } return _buildTreeNode(node); } @@ -176,7 +190,7 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { } else { location = _getSourceLocation(_name, node); } - return new PositionSourceInformation(location); + return new PositionSourceInformation(location, null, inliningContext); } /// Creates source information for the body of the current member. @@ -259,12 +273,27 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { SourceInformation _buildTreeNode(ir.TreeNode node, {SourceLocation closingPosition, String name}) { return new PositionSourceInformation( - _getSourceLocation(name ?? _name, node), closingPosition); + _getSourceLocation(name ?? _name, node), + closingPosition, + inliningContext); } @override - SourceInformationBuilder forContext(MemberEntity member) => - new KernelSourceInformationBuilder(_elementMap, member); + SourceInformationBuilder forContext( + MemberEntity member, SourceInformation context) { + List newContext = inliningContext?.toList() ?? []; + if (context != null) { + newContext.add(new FrameContext(context, member.name)); + } else { + // TODO(sigmund): investigate whether we have any more cases where context + // is null. + newContext = inliningContext; + } + + String name = computeKernelElementNameForSourceMaps(_elementMap, _member); + return new KernelSourceInformationBuilder.withContext( + _elementMap, member, newContext, name); + } @override SourceInformation buildSwitchCase(ir.Node node) => null; @@ -371,6 +400,11 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { return _buildTreeNode(node); } + @override + SourceInformation buildAssert(ir.Node node) { + return _buildTreeNode(node); + } + @override SourceInformation buildNew(ir.Node node) { return _buildTreeNode(node); @@ -384,8 +418,8 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { @override SourceInformation buildCall( covariant ir.TreeNode receiver, covariant ir.TreeNode call) { - return new PositionSourceInformation( - _getSourceLocation(_name, receiver), _getSourceLocation(_name, call)); + return new PositionSourceInformation(_getSourceLocation(_name, receiver), + _getSourceLocation(_name, call), inliningContext); } @override @@ -393,6 +427,11 @@ class KernelSourceInformationBuilder implements SourceInformationBuilder { return _buildTreeNode(node); } + @override + SourceInformation buildSet(ir.Node node) { + return _buildTreeNode(node); + } + @override SourceInformation buildLoop(ir.Node node) { return _buildTreeNode(node); @@ -448,4 +487,9 @@ class KernelSourceLocation extends AbstractSourceLocation { KernelSourceLocation(ir.Location location, this.offset, this.sourceName) : sourceUri = location.file, super.fromLocation(location); + + KernelSourceLocation.fromOther(KernelSourceLocation other, this.sourceName) + : sourceUri = other.sourceUri, + offset = other.offset, + super.fromOther(other); } diff --git a/pkg/compiler/lib/src/io/position_information.dart b/pkg/compiler/lib/src/io/position_information.dart index 240ef671675..4fb641abc43 100644 --- a/pkg/compiler/lib/src/io/position_information.dart +++ b/pkg/compiler/lib/src/io/position_information.dart @@ -23,7 +23,10 @@ class PositionSourceInformation extends SourceInformation { @override final SourceLocation innerPosition; - PositionSourceInformation(this.startPosition, [this.innerPosition]); + final List inliningContext; + + PositionSourceInformation( + this.startPosition, this.innerPosition, this.inliningContext); @override List get sourceLocations { @@ -275,14 +278,17 @@ class PositionSourceInformationProcessor extends SourceInformationProcessor { final SourceInformationReader reader; CodePositionMap codePositionMap; List traceListeners; + InliningTraceListener inliningListener; PositionSourceInformationProcessor(SourceMapperProvider provider, this.reader, [Coverage coverage]) { codePositionMap = coverage != null ? new CodePositionCoverage(codePositionRecorder, coverage) : codePositionRecorder; + var sourceMapper = provider.createSourceMapper(id); traceListeners = [ - new PositionTraceListener(provider.createSourceMapper(id), reader) + new PositionTraceListener(sourceMapper, reader), + inliningListener = new InliningTraceListener(sourceMapper, reader), ]; if (coverage != null) { traceListeners.add(new CoverageListener(coverage, reader)); @@ -291,6 +297,7 @@ class PositionSourceInformationProcessor extends SourceInformationProcessor { void process(js.Node node, BufferedCodeOutput code) { new JavaScriptTracer(codePositionMap, reader, traceListeners).apply(node); + inliningListener?.finish(); } @override @@ -368,6 +375,86 @@ abstract class NodeToSourceInformationMixin { } } +/// [TraceListener] that register inlining context-data with a [SourceMapper]. +class InliningTraceListener extends TraceListener + with NodeToSourceInformationMixin { + final SourceMapper sourceMapper; + final SourceInformationReader reader; + final Map> _frames = {}; + + InliningTraceListener(this.sourceMapper, this.reader); + + @override + void onStep(js.Node node, Offset offset, StepKind kind) { + SourceInformation sourceInformation = computeSourceInformation(node); + if (sourceInformation == null) return; + // TODO(sigmund): enable this assertion. + // assert(offset.value != null, "Expected a valid offset: $node $offset"); + if (offset.value == null) return; + + // TODO(sigmund): enable this assertion + //assert(_frames[offset.value] == null, + // "Expect a single entry per offset: $offset $node"); + if (_frames[offset.value] != null) return; + + // During tracing we only collect information per offset because the tracer + // visits nodes in tree order. We'll later sort the data by offset before + // registering the frame data with [SourceMapper]. + if (kind == StepKind.FUN_EXIT) { + _frames[offset.value] = null; + } else { + _frames[offset.value] = sourceInformation.inliningContext; + } + } + + /// Converts the inlining context data collected during tracing into push/pop + /// stack operations that will be emitted with the source-map files. + void finish() { + List lastInliningContext; + for (var offset in _frames.keys.toList()..sort()) { + var newInliningContext = _frames[offset]; + + // Note: this relies on the invariant that, when we built the inlining + // context lists during SSA, we kept lists identical whenever there were + // no inlining changes. + if (lastInliningContext == newInliningContext) continue; + + bool isEmpty = false; + int popCount = 0; + List pushes = const []; + if (newInliningContext == null) { + popCount = lastInliningContext.length; + isEmpty = true; + } else if (lastInliningContext == null) { + pushes = newInliningContext; + } else { + int min = newInliningContext.length; + if (min > lastInliningContext.length) min = lastInliningContext.length; + // Determine the total number of common frames, to produce the minimal + // set of pop and push operations. + int i = 0; + for (i = 0; i < min; i++) { + if (!identical(newInliningContext[i], lastInliningContext[i])) break; + } + isEmpty = i == 0; + popCount = lastInliningContext.length - i; + if (i < newInliningContext.length) { + pushes = newInliningContext.sublist(i); + } + } + lastInliningContext = newInliningContext; + + while (popCount-- > 0) { + sourceMapper.registerPop(offset, isEmpty: popCount == 0 && isEmpty); + } + for (FrameContext push in pushes) { + sourceMapper.registerPush(offset, + getSourceLocation(push.callInformation), push.inlinedMethodName); + } + } + } +} + /// [TraceListener] that register [SourceLocation]s with a [SourceMapper]. class PositionTraceListener extends TraceListener with NodeToSourceInformationMixin { diff --git a/pkg/compiler/lib/src/io/source_information.dart b/pkg/compiler/lib/src/io/source_information.dart index 5520b6de1d2..756340bc381 100644 --- a/pkg/compiler/lib/src/io/source_information.dart +++ b/pkg/compiler/lib/src/io/source_information.dart @@ -29,13 +29,33 @@ abstract class SourceInformation extends JavaScriptNodeSourceInformation { /// The source location associated with the end of the JS node. SourceLocation get endPosition => null; - /// All source locations associated with this source information. + /// A list containing start, inner, and end positions. List get sourceLocations; + /// A list of inlining context locations. + List get inliningContext => null; + /// Return a short textual representation of the source location. String get shortText; } +/// Context information about inlined calls. +/// +/// This is associated with SourceInformation objects to be able to emit +/// precise data about inlining that can then be used by defobuscation tools +/// when reconstructing a source stack from a production stack trace. +class FrameContext { + /// Location of the call that was inlined. + final SourceInformation callInformation; + + /// Name of the method that was inlined. + final String inlinedMethodName; + + FrameContext(this.callInformation, this.inlinedMethodName); + + String toString() => "(FrameContext: $callInformation, $inlinedMethodName)"; +} + /// Strategy for creating, processing and applying [SourceInformation]. class SourceInformationStrategy { const SourceInformationStrategy(); @@ -57,8 +77,11 @@ class SourceInformationStrategy { class SourceInformationBuilder { const SourceInformationBuilder(); - /// Create a [SourceInformationBuilder] for [member]. - SourceInformationBuilder forContext(covariant MemberEntity member) => this; + /// Create a [SourceInformationBuilder] for [member] with additional inlining + /// [context]. + SourceInformationBuilder forContext( + covariant MemberEntity member, SourceInformation context) => + this; /// Generate [SourceInformation] for the declaration of the [member]. SourceInformation buildDeclaration(covariant MemberEntity member) => null; @@ -85,13 +108,13 @@ class SourceInformationBuilder { /// Generate [SourceInformation] for the loop [node]. SourceInformation buildLoop(ir.Node node) => null; - /// Generate [SourceInformation] for a read access like `a.b` where in - /// [receiver] points to the left-most part of the access, `a` in the example, - /// and [property] points to the 'name' of accessed property, `b` in the - /// example. + /// Generate [SourceInformation] for a read access like `a.b`. SourceInformation buildGet(ir.Node node) => null; - /// Generate [SourceInformation] for the read access in [node]. + /// Generate [SourceInformation] for a write access like `a.b = 3`. + SourceInformation buildSet(ir.Node node) => null; + + /// Generate [SourceInformation] for a call in [node]. SourceInformation buildCall(ir.Node receiver, ir.Node call) => null; /// Generate [SourceInformation] for the if statement in [node]. @@ -103,6 +126,9 @@ class SourceInformationBuilder { /// Generate [SourceInformation] for the throw in [node]. SourceInformation buildThrow(ir.Node node) => null; + /// Generate [SourceInformation] for the assert in [node]. + SourceInformation buildAssert(ir.Node node) => null; + /// Generate [SourceInformation] for the assignment in [node]. SourceInformation buildAssignment(ir.Node node) => null; @@ -234,6 +260,9 @@ abstract class AbstractSourceLocation extends SourceLocation { AbstractSourceLocation.fromLocation(this._location) : _sourceFile = null; + AbstractSourceLocation.fromOther(AbstractSourceLocation location) + : this.fromLocation(location._location); + /// The absolute URI of the source file of this source location. Uri get sourceUri => _sourceFile.uri; @@ -338,3 +367,29 @@ class NoSourceLocationMarker extends SourceLocation { String toString() => ''; } + +/// Information tracked about inlined frames. +/// +/// Dart2js adds an extension to source-map files to track where calls are +/// inlined. This information is used to improve the precision of tools that +/// deobfuscate production stack traces. +class FrameEntry { + /// For push operations, the location of the inlining call, otherwise null. + final SourceLocation pushLocation; + + /// For push operations, the inlined method name, otherwise null. + final String inlinedMethodName; + + /// Whether a pop is the last pop that makes the inlining stack empty. + final bool isEmptyPop; + + FrameEntry.push(this.pushLocation, this.inlinedMethodName) + : isEmptyPop = false; + + FrameEntry.pop(this.isEmptyPop) + : pushLocation = null, + inlinedMethodName = null; + + bool get isPush => pushLocation != null; + bool get isPop => pushLocation == null; +} diff --git a/pkg/compiler/lib/src/io/source_map_builder.dart b/pkg/compiler/lib/src/io/source_map_builder.dart index 7e43f60032f..14dec0271e6 100644 --- a/pkg/compiler/lib/src/io/source_map_builder.dart +++ b/pkg/compiler/lib/src/io/source_map_builder.dart @@ -10,7 +10,7 @@ import '../util/uri_extras.dart' show relativize; import '../util/util.dart'; import 'location_provider.dart'; import 'code_output.dart' show SourceLocationsProvider, SourceLocations; -import 'source_information.dart' show SourceLocation; +import 'source_information.dart' show SourceLocation, FrameEntry; class SourceMapBuilder { final String version; @@ -28,13 +28,17 @@ class SourceMapBuilder { final Map minifiedGlobalNames; final Map minifiedInstanceNames; + /// Extension used to deobfuscate inlined stack frames. + final Map> frames; + SourceMapBuilder( this.version, this.sourceMapUri, this.targetFileUri, this.locationProvider, this.minifiedGlobalNames, - this.minifiedInstanceNames); + this.minifiedInstanceNames, + this.frames); void addMapping(int targetOffset, SourceLocation sourceLocation) { entries.add(new SourceMapEntry(sourceLocation, targetOffset)); @@ -84,8 +88,7 @@ class SourceMapBuilder { IndexMap uriMap = new IndexMap(); IndexMap nameMap = new IndexMap(); - lineColumnMap.forEachElement((SourceMapEntry entry) { - SourceLocation sourceLocation = entry.sourceLocation; + void registerLocation(SourceLocation sourceLocation) { if (sourceLocation != null) { if (sourceLocation.sourceUri != null) { uriMap.register(sourceLocation.sourceUri); @@ -94,10 +97,22 @@ class SourceMapBuilder { } } } + } + + lineColumnMap.forEachElement((SourceMapEntry entry) { + registerLocation(entry.sourceLocation); }); minifiedGlobalNames.values.forEach(nameMap.register); minifiedInstanceNames.values.forEach(nameMap.register); + for (List entries in frames.values) { + for (var frame in entries) { + registerLocation(frame.pushLocation); + if (frame.inlinedMethodName != null) { + nameMap.register(frame.inlinedMethodName); + } + } + } StringBuffer mappingsBuffer = new StringBuffer(); writeEntries(lineColumnMap, uriMap, nameMap, mappingsBuffer); @@ -132,7 +147,10 @@ class SourceMapBuilder { buffer.write(',\n'); buffer.write(' "instance": '); writeMinifiedNames(minifiedInstanceNames, nameMap, buffer); - buffer.write('\n }\n }\n}\n'); + buffer.write('\n },\n'); + buffer.write(' "frames": '); + writeFrames(uriMap, nameMap, buffer); + buffer.write('\n }\n}\n'); return buffer.toString(); } @@ -206,6 +224,37 @@ class SourceMapBuilder { buffer.write('}'); } + void writeFrames( + IndexMap uriMap, IndexMap nameMap, StringBuffer buffer) { + bool first = true; + buffer.write('['); + frames.forEach((int offset, List entries) { + if (!first) buffer.write(','); + buffer.write('['); + buffer.write(offset); + for (var entry in entries) { + buffer.write(','); + if (entry.isPush) { + SourceLocation location = entry.pushLocation; + buffer.write('['); + buffer.write(uriMap[location.sourceUri]); + buffer.write(','); + buffer.write(location.line - 1); + buffer.write(','); + buffer.write(location.column - 1); + buffer.write(','); + buffer.write(nameMap[entry.inlinedMethodName]); + buffer.write(']'); + } else { + buffer.write(entry.isEmptyPop ? 0 : -1); + } + } + buffer.write(']'); + first = false; + }); + buffer.write(']'); + } + /// Returns the source map tag to put at the end a .js file in [fileUri] to /// make it point to the source map file in [sourceMapUri]. static String generateSourceMapTag(Uri sourceMapUri, Uri fileUri) { @@ -244,7 +293,8 @@ class SourceMapBuilder { fileUri, locationProvider, minifiedGlobalNames, - minifiedInstanceNames); + minifiedInstanceNames, + sourceLocations.frameMarkers); sourceLocations.forEachSourceLocation(sourceMapBuilder.addMapping); String sourceMap = sourceMapBuilder.build(); String extension = 'js.map'; diff --git a/pkg/compiler/lib/src/js/js_source_mapping.dart b/pkg/compiler/lib/src/js/js_source_mapping.dart index bef8c191a39..13735ca814f 100644 --- a/pkg/compiler/lib/src/js/js_source_mapping.dart +++ b/pkg/compiler/lib/src/js/js_source_mapping.dart @@ -73,6 +73,16 @@ class SourceMapperProviderImpl implements SourceMapperProvider { abstract class SourceMapper { /// Associate [codeOffset] with [sourceLocation] for [node]. void register(Node node, int codeOffset, SourceLocation sourceLocation); + + /// Associate [codeOffset] with an inlining call at [sourceLocation]. + void registerPush( + int codeOffset, SourceLocation sourceLocation, String inlinedMethodName); + + /// Associate [codeOffset] with an inlining return. + /// + /// If [isEmpty] is true, also associate that the inlining stack is empty at + /// [codeOffset]. + void registerPop(int codeOffset, {bool isEmpty: false}); } /// An implementation of [SourceMapper] that stores the information directly @@ -86,6 +96,17 @@ class SourceLocationsMapper implements SourceMapper { void register(Node node, int codeOffset, SourceLocation sourceLocation) { sourceLocations.addSourceLocation(codeOffset, sourceLocation); } + + @override + void registerPush( + int codeOffset, SourceLocation sourceLocation, String inlinedMethodName) { + sourceLocations.addPush(codeOffset, sourceLocation, inlinedMethodName); + } + + @override + void registerPop(int codeOffset, {bool isEmpty: false}) { + sourceLocations.addPop(codeOffset, isEmpty); + } } /// A processor that associates [SourceInformation] with code position of diff --git a/pkg/compiler/lib/src/ssa/builder_kernel.dart b/pkg/compiler/lib/src/ssa/builder_kernel.dart index 4c06c12bab4..1fe982c9504 100644 --- a/pkg/compiler/lib/src/ssa/builder_kernel.dart +++ b/pkg/compiler/lib/src/ssa/builder_kernel.dart @@ -138,7 +138,7 @@ class KernelSsaGraphBuilder extends ir.Visitor ) : this.targetElement = _effectiveTargetElementFor(initialTargetElement), _infoReporter = compiler.dumpInfoTask, _allocatorAnalysis = closedWorld.allocatorAnalysis { - _enterFrame(targetElement); + _enterFrame(targetElement, null); this.loopHandler = new KernelLoopHandler(this); typeBuilder = new KernelTypeBuilder(this, _elementMap, _globalLocalsMap); graph.element = targetElement; @@ -163,7 +163,8 @@ class KernelSsaGraphBuilder extends ir.Visitor return member; } - void _enterFrame(MemberEntity member) { + void _enterFrame( + MemberEntity member, SourceInformation callSourceInformation) { AsyncMarker asyncMarker = AsyncMarker.SYNC; ir.FunctionNode function = getFunctionNode(_elementMap, member); if (function != null) { @@ -176,7 +177,8 @@ class KernelSsaGraphBuilder extends ir.Visitor _globalLocalsMap.getLocalsMap(member), new KernelToTypeInferenceMapImpl(member, globalInferenceResults), _currentFrame != null - ? _currentFrame.sourceInformationBuilder.forContext(member) + ? _currentFrame.sourceInformationBuilder + .forContext(member, callSourceInformation) : _sourceInformationStrategy.createBuilderForContext(member)); } @@ -579,7 +581,9 @@ class KernelSsaGraphBuilder extends ir.Visitor ConstructorEntity inlinedConstructor = _elementMap.getConstructor(body); - inlinedFrom(inlinedConstructor, () { + inlinedFrom( + inlinedConstructor, _sourceInformationBuilder.buildCall(body, body), + () { void handleParameter(ir.VariableDeclaration node) { Local parameter = localsMap.getLocalVariable(node); // If [parameter] is boxed, it will be a field in the box passed as @@ -656,9 +660,10 @@ class KernelSsaGraphBuilder extends ir.Visitor /// Sets context for generating code that is the result of inlining /// [inlinedTarget]. - inlinedFrom(MemberEntity inlinedTarget, f()) { + inlinedFrom(MemberEntity inlinedTarget, + SourceInformation callSourceInformation, f()) { reporter.withCurrentElement(inlinedTarget, () { - _enterFrame(inlinedTarget); + _enterFrame(inlinedTarget, callSourceInformation); var result = f(); _leaveFrame(); return result; @@ -724,7 +729,8 @@ class KernelSsaGraphBuilder extends ir.Visitor // TODO(sra): It would be sufficient to know the context was a field // initializer. if (!_allocatorAnalysis.isInitializedInAllocator(field)) { - inlinedFrom(field, () { + inlinedFrom(field, + _sourceInformationBuilder.buildAssignment(node.initializer), () { node.initializer.accept(this); constructorData.fieldValues[field] = pop(); }); @@ -902,7 +908,9 @@ class KernelSsaGraphBuilder extends ir.Visitor ConstructorEntity element = _elementMap.getConstructor(constructor); ScopeInfo oldScopeInfo = localsHandler.scopeInfo; - inlinedFrom(element, () { + inlinedFrom( + element, _sourceInformationBuilder.buildCall(initializer, initializer), + () { void handleParameter(ir.VariableDeclaration node) { Local parameter = localsMap.getLocalVariable(node); HInstruction argument = arguments[index++]; @@ -1236,7 +1244,8 @@ class KernelSsaGraphBuilder extends ir.Visitor _commonElements.closureConverter, [argument, graph.addConstantInt(arity, closedWorld)], abstractValueDomain.dynamicType, - const []); + const [], + sourceInformation: null); argument = pop(); } inputs.add(argument); @@ -1379,7 +1388,8 @@ class KernelSsaGraphBuilder extends ir.Visitor [prefixConstant, uriConstant], _typeInferenceMap .getReturnTypeOf(_commonElements.checkDeferredIsLoaded), - const []); + const [], + sourceInformation: null); } @override @@ -2104,13 +2114,15 @@ class KernelSsaGraphBuilder extends ir.Visitor @override void visitAssertStatement(ir.AssertStatement node) { if (!options.enableUserAssertions) return; + var sourceInformation = _sourceInformationBuilder.buildAssert(node); if (node.message == null) { node.condition.accept(this); _pushStaticInvocation( _commonElements.assertHelper, [pop()], _typeInferenceMap.getReturnTypeOf(_commonElements.assertHelper), - const []); + const [], + sourceInformation: sourceInformation); pop(); return; } @@ -2122,7 +2134,8 @@ class KernelSsaGraphBuilder extends ir.Visitor _commonElements.assertTest, [pop()], _typeInferenceMap.getReturnTypeOf(_commonElements.assertTest), - const []); + const [], + sourceInformation: sourceInformation); } void fail() { @@ -2131,7 +2144,8 @@ class KernelSsaGraphBuilder extends ir.Visitor _commonElements.assertThrow, [pop()], _typeInferenceMap.getReturnTypeOf(_commonElements.assertThrow), - const []); + const [], + sourceInformation: sourceInformation); pop(); } @@ -2814,7 +2828,8 @@ class KernelSsaGraphBuilder extends ir.Visitor addImplicitInstantiation(type); _pushStaticInvocation( - constructor, inputs, instructionType, const []); + constructor, inputs, instructionType, const [], + sourceInformation: _sourceInformationBuilder.buildNew(node)); removeImplicitInstantiation(type); } @@ -2914,7 +2929,8 @@ class KernelSsaGraphBuilder extends ir.Visitor FunctionEntity setter = _elementMap.getMember(staticTarget); // Invoke the setter _pushStaticInvocation(setter, [value], - _typeInferenceMap.getReturnTypeOf(setter), const []); + _typeInferenceMap.getReturnTypeOf(setter), const [], + sourceInformation: _sourceInformationBuilder.buildSet(node)); pop(); } else { add(new HStaticStore( @@ -3662,8 +3678,9 @@ class KernelSsaGraphBuilder extends ir.Visitor } } - _addTypeArguments(arguments, typeArguments, - _sourceInformationBuilder.buildCall(invocation, invocation)); + SourceInformation sourceInformation = + _sourceInformationBuilder.buildCall(invocation, invocation); + _addTypeArguments(arguments, typeArguments, sourceInformation); HInstruction argumentsInstruction = buildLiteralList(arguments); add(argumentsInstruction); @@ -3697,7 +3714,8 @@ class KernelSsaGraphBuilder extends ir.Visitor typeArgumentCount, ], abstractValueDomain.dynamicType, - const []); + const [], + sourceInformation: sourceInformation); } bool _unexpectedForeignArguments(ir.StaticInvocation invocation, @@ -4472,7 +4490,8 @@ class KernelSsaGraphBuilder extends ir.Visitor graph.addConstantInt(typeArguments.length, closedWorld), ], abstractValueDomain.dynamicType, - typeArguments); + typeArguments, + sourceInformation: sourceInformation); _buildInvokeSuper(Selectors.noSuchMethod_, containingClass, noSuchMethod, [pop()], typeArguments, sourceInformation); @@ -5167,7 +5186,7 @@ class KernelSsaGraphBuilder extends ir.Visitor List compiledArguments = _completeCallArgumentsList( function, selector, providedArguments, currentNode); _enterInlinedMethod(function, compiledArguments, instanceType); - inlinedFrom(function, () { + inlinedFrom(function, sourceInformation, () { if (!isReachable) { _emitReturn(graph.addConstantNull(closedWorld), sourceInformation); } else { @@ -6007,7 +6026,9 @@ class TryCatchFinallyBuilder { [exception], kernelBuilder._typeInferenceMap.getReturnTypeOf( kernelBuilder._commonElements.traceFromException), - const []); + const [], + sourceInformation: + kernelBuilder._sourceInformationBuilder.buildCatch(catchBlock)); HInstruction traceInstruction = kernelBuilder.pop(); Local traceVariable = kernelBuilder.localsMap.getLocalVariable(catchBlock.stackTrace); diff --git a/pkg/sourcemap_testing/lib/src/stacktrace_helper.dart b/pkg/sourcemap_testing/lib/src/stacktrace_helper.dart index f7e33574385..6f968a152f4 100644 --- a/pkg/sourcemap_testing/lib/src/stacktrace_helper.dart +++ b/pkg/sourcemap_testing/lib/src/stacktrace_helper.dart @@ -4,10 +4,12 @@ import 'dart:async'; import 'dart:io'; +import 'dart:convert' show jsonDecode; import 'package:expect/expect.dart'; import 'package:source_maps/source_maps.dart'; import 'package:source_maps/src/utils.dart'; +import 'package:source_span/source_span.dart'; import 'annotated_code_helper.dart'; @@ -105,7 +107,8 @@ Future testStackTrace(Test test, String config, CompileFunc compile, bool useJsMethodNamesOnAbsence: false, String Function(String name) jsNameConverter: identityConverter, Directory forcedTmpDir: null, - int stackTraceLimit: 10}) async { + int stackTraceLimit: 10, + expandDart2jsInliningData: false}) async { Expect.isTrue(test.expectationMap.keys.contains(config), "No expectations found for '$config' in ${test.expectationMap.keys}"); @@ -122,13 +125,14 @@ Future testStackTrace(Test test, String config, CompileFunc compile, sourceMapFile.existsSync(), "Source map not generated for $input"); String sourceMapText = sourceMapFile.readAsStringSync(); SingleMapping sourceMap = parse(sourceMapText); + String jsOutput = new File(output).readAsStringSync(); if (printJs) { print('JavaScript output:'); - print(new File(output).readAsStringSync()); + print(jsOutput); } if (writeJs) { - new File('out.js').writeAsStringSync(new File(output).readAsStringSync()); + new File('out.js').writeAsStringSync(jsOutput); new File('out.js.map').writeAsStringSync(sourceMapText); } print("Running d8 $output"); @@ -161,18 +165,58 @@ Future testStackTrace(Test test, String config, CompileFunc compile, if (targetEntry == null || targetEntry.sourceUrlId == null) { dartStackTrace.add(line); } else { + String fileName; + if (targetEntry.sourceUrlId != null) { + fileName = sourceMap.urls[targetEntry.sourceUrlId]; + } + int targetLine = targetEntry.sourceLine + 1; + int targetColumn = targetEntry.sourceColumn + 1; + + if (expandDart2jsInliningData) { + SourceFile file = new SourceFile.fromString(jsOutput); + int offset = file.getOffset(line.lineNo - 1, line.columnNo - 1); + Map> frames = + _loadInlinedFrameData(sourceMap, sourceMapText); + List indices = frames.keys.toList()..sort(); + int key = binarySearch(indices, (i) => i > offset) - 1; + int depth = 0; + outer: + while (key >= 0) { + for (var frame in frames[indices[key]].reversed) { + if (frame.isEmpty) break outer; + if (frame.isPush) { + if (depth <= 0) { + dartStackTrace.add(new StackTraceLine( + frame.inlinedMethodName + "(inlined)", + fileName, + targetLine, + targetColumn, + isMapped: true)); + fileName = frame.callUri; + targetLine = frame.callLine + 1; + targetColumn = frame.callColumn + 1; + } else { + depth--; + } + } + if (frame.isPop) { + depth++; + } + } + key--; + } + targetEntry = findEnclosingFunction(jsOutput, file, offset, sourceMap); + } + String methodName; if (targetEntry.sourceNameId != null) { methodName = sourceMap.names[targetEntry.sourceNameId]; } else if (useJsMethodNamesOnAbsence) { methodName = jsNameConverter(line.methodName); } - String fileName; - if (targetEntry.sourceUrlId != null) { - fileName = sourceMap.urls[targetEntry.sourceUrlId]; - } - dartStackTrace.add(new StackTraceLine(methodName, fileName, - targetEntry.sourceLine + 1, targetEntry.sourceColumn + 1, + + dartStackTrace.add(new StackTraceLine( + methodName, fileName, targetLine, targetColumn, isMapped: true)); } } @@ -344,8 +388,10 @@ TargetLineEntry _findLine(SingleMapping sourceMap, StackTraceLine stLine) { String filename = stLine.fileName .substring(stLine.fileName.lastIndexOf(new RegExp("[\\\/]")) + 1); if (sourceMap.targetUrl != filename) return null; + return _findLineInternal(sourceMap, stLine.lineNo - 1); +} - int line = stLine.lineNo - 1; +TargetLineEntry _findLineInternal(SingleMapping sourceMap, int line) { int index = binarySearch(sourceMap.lines, (e) => e.line > line); return (index <= 0) ? null : sourceMap.lines[index - 1]; } @@ -384,3 +430,80 @@ class LineException { const LineException(this.methodName, this.fileName); } + +class FrameEntry { + final String callUri; + final int callLine; + final int callColumn; + final String inlinedMethodName; + final bool isEmpty; + FrameEntry.push( + this.callUri, this.callLine, this.callColumn, this.inlinedMethodName) + : isEmpty = false; + FrameEntry.pop(this.isEmpty) + : callUri = null, + callLine = null, + callColumn = null, + inlinedMethodName = null; + + bool get isPush => callUri != null; + bool get isPop => callUri == null; +} + +/// Search backwards in [sources] for a function declaration that includes the +/// [start] offset. +TargetEntry findEnclosingFunction( + String sources, SourceFile file, int start, SingleMapping mapping) { + if (sources == null) return null; + int index = sources.lastIndexOf(': function(', start); + if (index < 0) return null; + index += 2; + var line = file.getLine(index); + var lineEntry = _findLineInternal(mapping, line); + return _findColumn(line, file.getColumn(index), lineEntry); +} + +Map> _loadInlinedFrameData( + SingleMapping mapping, String sourceMapText) { + var json = jsonDecode(sourceMapText); + var frames = >{}; + var extensions = json['x_org_dartlang_dart2js']; + if (extensions == null) return null; + List jsonFrames = extensions['frames']; + if (jsonFrames == null) return null; + + for (List values in jsonFrames) { + if (values.length < 2) { + print("warning: incomplete frame data: $values"); + continue; + } + + int offset = values[0]; + List entries = frames[offset] ??= []; + if (entries.length > 0) { + print("warning: duplicate entries for $offset"); + continue; + } + + for (int i = 1; i < values.length; i++) { + var current = values[i]; + if (current == -1) { + entries.add(new FrameEntry.pop(false)); + } else if (current == 0) { + entries.add(new FrameEntry.pop(true)); + } else { + if (current is List) { + if (current.length == 4) { + entries.add(new FrameEntry.push(mapping.urls[current[0]], + current[1], current[2], mapping.names[current[3]])); + } else { + print("warning: unexpected entry $current"); + } + } else { + print("warning: unexpected entry $current"); + } + } + } + } + return frames; +} diff --git a/tests/compiler/dart2js/sourcemaps/helpers/sourcemap_helper.dart b/tests/compiler/dart2js/sourcemaps/helpers/sourcemap_helper.dart index 5d849c3cb4e..3e52e3ade08 100644 --- a/tests/compiler/dart2js/sourcemaps/helpers/sourcemap_helper.dart +++ b/tests/compiler/dart2js/sourcemaps/helpers/sourcemap_helper.dart @@ -144,6 +144,17 @@ class RecordingSourceMapper implements SourceMapper { nodeToSourceLocationsMap.register(node, codeOffset, sourceLocation); sourceMapper.register(node, codeOffset, sourceLocation); } + + @override + void registerPush( + int codeOffset, SourceLocation sourceLocation, String inlinedMethodName) { + sourceMapper.registerPush(codeOffset, sourceLocation, inlinedMethodName); + } + + @override + void registerPop(int codeOffset, {bool isEmpty: false}) { + sourceMapper.registerPop(codeOffset, isEmpty: isEmpty); + } } /// A wrapper of [SourceInformationProcessor] that records source locations and @@ -448,6 +459,13 @@ class _LocationRecorder implements SourceMapper, LocationMap { .add(sourceLocation); } + @override + void registerPush(int codeOffset, SourceLocation sourceLocation, + String inlinedMethodName) {} + + @override + void registerPop(int codeOffset, {bool isEmpty: false}) {} + Iterable get nodes => _nodeMap.keys; Map> operator [](js.Node node) { diff --git a/tests/compiler/dart2js/sourcemaps/stacktrace/deep_inlining.dart b/tests/compiler/dart2js/sourcemaps/stacktrace/deep_inlining.dart new file mode 100644 index 00000000000..bef5cfaaa9a --- /dev/null +++ b/tests/compiler/dart2js/sourcemaps/stacktrace/deep_inlining.dart @@ -0,0 +1,20 @@ +import 'package:expect/expect.dart'; + +class MyClass {} + +@NoInline() +method3() { + /*4:method3*/ throw new MyClass(); +} + +method2() => /*3:method2*/ method3(); +method4() { + /*2:method4(inlined)*/ method2(); +} + +method1() { + print('hi'); + /*1:method1(inlined)*/ method4(); +} + +main() => /*0:main*/ method1(); diff --git a/tests/compiler/dart2js/sourcemaps/stacktrace_test.dart b/tests/compiler/dart2js/sourcemaps/stacktrace_test.dart index d28f3a24259..deebbc381bc 100644 --- a/tests/compiler/dart2js/sourcemaps/stacktrace_test.dart +++ b/tests/compiler/dart2js/sourcemaps/stacktrace_test.dart @@ -25,6 +25,7 @@ void main(List args) { bool continuing = false; await for (FileSystemEntity entity in dataDir.list()) { String name = entity.uri.pathSegments.last; + if (!name.endsWith('.dart')) continue; if (argResults.rest.isNotEmpty && !argResults.rest.contains(name) && !continuing) { @@ -37,7 +38,8 @@ void main(List args) { await testAnnotatedCode(annotatedCode, verbose: argResults['verbose'], printJs: argResults['print-js'], - writeJs: argResults['write-js']); + writeJs: argResults['write-js'], + inlineData: name.endsWith('_inlining.dart')); if (argResults['continued']) { continuing = true; } @@ -48,18 +50,25 @@ void main(List args) { const String kernelMarker = 'kernel.'; Future testAnnotatedCode(String code, - {bool printJs: false, bool writeJs: false, bool verbose: false}) async { + {bool printJs: false, + bool writeJs: false, + bool verbose: false, + bool inlineData: false}) async { Test test = processTestCode(code, [kernelMarker]); print(test.code); print('---from kernel------------------------------------------------------'); await runTest(test, kernelMarker, - printJs: printJs, writeJs: writeJs, verbose: verbose); + printJs: printJs, + writeJs: writeJs, + verbose: verbose, + inlineData: inlineData); } Future runTest(Test test, String config, {bool printJs: false, bool writeJs: false, bool verbose: false, + bool inlineData: false, List options: const []}) async { List testAfterExceptions = []; if (config == kernelMarker) { @@ -87,7 +96,8 @@ Future runTest(Test test, String config, verbose: verbose, printJs: printJs, writeJs: writeJs, - stackTraceLimit: 100); + stackTraceLimit: 100, + expandDart2jsInliningData: inlineData); } /// Lines allowed before the intended stack trace. Typically from helper