Skip to content

Commit a11aeeb

Browse files
committed
[dart2js] Annotation for exception stacks with shorter prefix
Dart's `throw` expression does not correspond exactly to JavaScript's `throw` statement. To implement Dart semantics, and to reduce code size, dart2js uses the functions `wrapException` and `throwExpression` from the dart2js runtime. As a result, these functions appear in the stack trace (one or both). A consequence of this is that a fixed prefix of an error stack is less useful, as potentially interesting frames are forced out of the prefix by these 'noise' frames. This change ensures only one of the helpers on the stack trace. It is also possible to get rid of the helper frame by an annotation. The annotation `@pragma('dart2js:stack-starts-at-throw')` causes the stack to be captured in the current JavaScript function rather than in a helper. The cost is more code at the call site, about 12 bytes per `throw` expression in minified code. The annotation can be placed on a method, class or library, and applies to all `throw` expression in the scope of the annotated element. This change uses the annotation to remove noise frames from type errors and some other errors in the runtime. Change-Id: If15184a5963fb054199177bb4526b32f25e53fe9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/406684 Reviewed-by: Mayank Patke <[email protected]> Reviewed-by: Nate Biggs <[email protected]>
1 parent e038d8a commit a11aeeb

File tree

14 files changed

+139
-54
lines changed

14 files changed

+139
-54
lines changed

pkg/compiler/doc/pragmas.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
| `dart2js:noElision` | Disables an optimization whereby unused fields or unused parameters are removed |
1313
| `dart2js:load-priority` | [Affects deferred library loading](#load-priority) |
1414
| `dart2js:resource-identifier` | [Collects data references to resources](resource_identifiers.md) |
15+
| `dart2js:stack-starts-at-throw` | [Affects stack trace from `throw` expressions](#stack-statrs-at-throw) |
1516
| `weak-tearoff-reference` | [Declaring a static weak reference intrinsic method.](#declaring-a-static-weak-reference-intrinsic-method) |
1617

1718
## Unsafe pragmas for general use
@@ -214,6 +215,21 @@ In the future this annotation might be extended to apply to `late` local
214215
variables, static variables, and top-level variables.
215216

216217

218+
#### Stack starts at throw
219+
220+
In order to generate smaller code, `throw` expressions are usually implemented
221+
by calling a helper method to capture the stack trace and perform the actual
222+
throw. This adds one (or occasionally two) frames to the stack trace showing the
223+
helper functions. It is possible for the stack trace to be captured directly in
224+
the method containing the `throw` expression. This takes a little extra code, so
225+
is opt-in via the following annotation.
226+
227+
```dart
228+
@pragma('dart2js:stack-starts-at-throw')
229+
```
230+
231+
This annotation can be placed on a method, class or library.
232+
217233
### Annotations related to deferred library loading
218234

219235
#### Load priority

pkg/compiler/lib/src/common/elements.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,9 @@ abstract class CommonElements {
898898

899899
FunctionEntity get stringInterpolationHelper => _findHelperFunction('S');
900900

901+
FunctionEntity get initializeExceptionWrapper =>
902+
_findHelperFunction('initializeExceptionWrapper');
903+
901904
FunctionEntity get wrapExceptionHelper =>
902905
_findHelperFunction('wrapException');
903906

pkg/compiler/lib/src/js_backend/annotations.dart

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ enum PragmaAnnotation {
9191
lateCheck('late:check'),
9292

9393
loadLibraryPriority('load-priority', hasOption: true),
94-
resourceIdentifier('resource-identifier');
94+
resourceIdentifier('resource-identifier'),
95+
96+
throwWithoutHelperFrame('stack-starts-at-throw');
9597

9698
final String name;
9799
final bool forFunctionsOnly;
@@ -357,6 +359,11 @@ abstract class AnnotationsData {
357359

358360
/// Determines whether [member] is annotated as a resource identifier.
359361
bool methodIsResourceIdentifier(FunctionEntity member);
362+
363+
/// Is this node in a context requesting that the captured stack in a `throw`
364+
/// expression generates extra code to avoid having a runtime helper on the
365+
/// stack?
366+
bool throwWithoutHelperFrame(ir.TreeNode node);
360367
}
361368

362369
class AnnotationsDataImpl implements AnnotationsData {
@@ -645,6 +652,22 @@ class AnnotationsDataImpl implements AnnotationsData {
645652
}
646653
return false;
647654
}
655+
656+
@override
657+
bool throwWithoutHelperFrame(ir.TreeNode node) {
658+
return _throwWithoutHelperFrame(_findContext(node));
659+
}
660+
661+
bool _throwWithoutHelperFrame(DirectivesContext? context) {
662+
while (context != null) {
663+
EnumSet<PragmaAnnotation>? annotations = context.annotations;
664+
if (annotations.contains(PragmaAnnotation.throwWithoutHelperFrame)) {
665+
return true;
666+
}
667+
context = context.parent;
668+
}
669+
return false;
670+
}
648671
}
649672

650673
class AnnotationsDataBuilder {

pkg/compiler/lib/src/ssa/builder.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2529,7 +2529,14 @@ class KernelSsaGraphBuilder extends ir.VisitorDefault<void>
25292529
final sourceInformation = _sourceInformationBuilder.buildThrow(
25302530
node.expression,
25312531
);
2532-
_closeAndGotoExit(HThrow(pop(), sourceInformation));
2532+
_closeAndGotoExit(
2533+
HThrow(
2534+
pop(),
2535+
sourceInformation,
2536+
withoutHelperFrame: closedWorld.annotationsData
2537+
.throwWithoutHelperFrame(node),
2538+
),
2539+
);
25332540
} else {
25342541
expression.accept(this);
25352542
pop();
@@ -7759,6 +7766,8 @@ class KernelSsaGraphBuilder extends ir.VisitorDefault<void>
77597766
pop(),
77607767
_abstractValueDomain.emptyType,
77617768
sourceInformation,
7769+
withoutHelperFrame: closedWorld.annotationsData
7770+
.throwWithoutHelperFrame(node),
77627771
),
77637772
);
77647773
_isReachable = false;

pkg/compiler/lib/src/ssa/codegen.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2994,13 +2994,24 @@ class SsaCodeGenerator implements HVisitor<void>, HBlockInformationVisitor {
29942994
pushStatement(js.Throw(pop()).withSourceInformation(sourceInformation));
29952995
} else {
29962996
use(node.inputs[0]);
2997-
_pushCallStatic(_commonElements.wrapExceptionHelper, [
2998-
pop(),
2999-
], sourceInformation);
2997+
if (node.withoutHelperFrame) {
2998+
_pushCallStatic(_commonElements.initializeExceptionWrapper, [
2999+
pop(),
3000+
_newErrorObject(sourceInformation),
3001+
], sourceInformation);
3002+
} else {
3003+
_pushCallStatic(_commonElements.wrapExceptionHelper, [
3004+
pop(),
3005+
], sourceInformation);
3006+
}
30003007
pushStatement(js.Throw(pop()).withSourceInformation(sourceInformation));
30013008
}
30023009
}
30033010

3011+
js.Expression _newErrorObject(SourceInformation? sourceInformation) {
3012+
return js.js('new Error()').withSourceInformation(sourceInformation);
3013+
}
3014+
30043015
@override
30053016
void visitAwait(HAwait node) {
30063017
use(node.inputs[0]);
@@ -3185,6 +3196,7 @@ class SsaCodeGenerator implements HVisitor<void>, HBlockInformationVisitor {
31853196
use(node.inputs[0]);
31863197
_pushCallStatic(_commonElements.throwExpressionHelper, [
31873198
pop(),
3199+
if (node.withoutHelperFrame) _newErrorObject(node.sourceInformation),
31883200
], node.sourceInformation);
31893201
}
31903202

pkg/compiler/lib/src/ssa/nodes.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3417,11 +3417,13 @@ class HReturn extends HControlFlow {
34173417
}
34183418

34193419
class HThrowExpression extends HInstruction {
3420+
final bool withoutHelperFrame;
34203421
HThrowExpression(
34213422
super.value,
34223423
super.type,
3423-
SourceInformation? sourceInformation,
3424-
) : super._oneInput() {
3424+
SourceInformation? sourceInformation, {
3425+
this.withoutHelperFrame = false,
3426+
}) : super._oneInput() {
34253427
this.sourceInformation = sourceInformation;
34263428
}
34273429
@override
@@ -3466,10 +3468,12 @@ class HYield extends HInstruction {
34663468

34673469
class HThrow extends HControlFlow {
34683470
final bool isRethrow;
3471+
final bool withoutHelperFrame;
34693472
HThrow(
34703473
HInstruction value,
34713474
SourceInformation? sourceInformation, {
34723475
this.isRethrow = false,
3476+
this.withoutHelperFrame = false,
34733477
}) {
34743478
inputs.add(value);
34753479
this.sourceInformation = sourceInformation;

pkg/compiler/test/impact/data/expressions.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,11 @@ testAsGenericRaw(dynamic o) => o as GenericClass;
358358
testAsGenericDynamic(dynamic o) => o as GenericClass<dynamic, dynamic>;
359359

360360
/*member: testThrow:
361-
static=[throwExpression(1),wrapException(1)],
362-
type=[inst:JSString]*/
361+
static=[
362+
throwExpression(2),
363+
wrapException(1)],
364+
type=[inst:JSString]
365+
*/
363366
testThrow() => throw '';
364367

365368
/*member: testIfNotNull:

pkg/compiler/test/impact/data/statements.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ testTryFinally() {
424424

425425
/*member: testSwitchWithoutFallthrough:
426426
static=[
427-
throwExpression(1),
427+
throwExpression(2),
428428
wrapException(1)],
429429
type=[
430430
inst:JSInt,

pkg/compiler/test/sourcemaps/stacktrace_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ Future runTest(
124124
const List<LineException> beforeExceptions = const [
125125
const LineException('wrapException', 'js_helper.dart'),
126126
const LineException('throwExpression', 'js_helper.dart'),
127+
const LineException('throw_', 'js_helper.dart'),
127128
];
128129

129130
/// Lines allowed after the intended stack trace. Typically from the event

sdk/lib/_internal/js_runtime/lib/async_patch.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import 'dart:_js_helper'
1212
getTraceFromException,
1313
Primitives,
1414
requiresPreamble,
15-
wrapException,
1615
unwrapException;
1716

1817
import 'dart:_foreign_helper'

sdk/lib/_internal/js_runtime/lib/core_patch.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import 'dart:_js_helper'
2323
quoteStringForRegExp,
2424
getTraceFromException,
2525
RuntimeError,
26-
wrapException,
26+
initializeExceptionWrapper,
2727
wrapZoneUnaryCallback,
2828
TrustedGetRuntimeType;
2929

@@ -273,11 +273,11 @@ class Error {
273273
StackTrace? get stackTrace => Primitives.extractStackTrace(this);
274274

275275
@patch
276+
@pragma('dart2js:never-inline')
276277
static Never _throw(Object error, StackTrace stackTrace) {
277-
error = wrapException(error);
278+
error = initializeExceptionWrapper(error, JS('', 'new Error()'));
278279
JS('void', '#.stack = #', error, stackTrace.toString());
279-
JS('', 'throw #', error);
280-
throw "unreachable";
280+
JS<Never>('', 'throw #', error);
281281
}
282282
}
283283

sdk/lib/_internal/js_runtime/lib/js_helper.dart

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,8 @@ class Primitives {
11001100
static void trySetStackTrace(Error error, StackTrace stackTrace) {
11011101
var jsError = JS('', r'#.$thrownJsError', error);
11021102
if (jsError == null) {
1103-
jsError = wrapException(error);
1103+
jsError = JS('', 'new Error()');
1104+
initializeExceptionWrapper(error, jsError);
11041105
JS('', r'#.$thrownJsError = #', error, jsError);
11051106
JS('void', '#.stack = #', jsError, stackTrace.toString());
11061107
}
@@ -1200,18 +1201,23 @@ String checkString(value) {
12001201
return value;
12011202
}
12021203

1203-
/// Wrap the given Dart object as a JS `Error` that can carry a stack trace.
1204+
/// Wrap the given Dart object as a JavaScript `Error` that can carry a stack
1205+
/// trace.
12041206
///
12051207
/// The code in [unwrapException] deals with getting the original Dart
12061208
/// object out of the wrapper again.
12071209
@pragma('dart2js:never-inline')
12081210
wrapException(ex) {
12091211
final wrapper = JS('', 'new Error()');
1210-
return initializeExceptionWrapper(wrapper, ex);
1212+
return initializeExceptionWrapper(ex, wrapper);
12111213
}
12121214

1215+
/// Wrap the given Dart object with the recorded stack trace.
1216+
///
1217+
/// The code in [unwrapException] deals with getting the original Dart
1218+
/// object out of the wrapper again. For convenience, returns [wrapper].
12131219
@pragma('dart2js:never-inline')
1214-
initializeExceptionWrapper(wrapper, ex) {
1220+
initializeExceptionWrapper(ex, wrapper) {
12151221
if (ex == null) ex = TypeError();
12161222
// [unwrapException] looks for the property 'dartException'.
12171223
JS('void', '#.dartException = #', wrapper, ex);
@@ -1240,18 +1246,14 @@ toStringWrapper() {
12401246
return JS('', r'this.dartException').toString();
12411247
}
12421248

1243-
/// This wraps the exception and does the throw. It is possible to call this in
1244-
/// a JS expression context, where the throw statement is not allowed. Helpers
1245-
/// are never inlined, so we don't risk inlining the throw statement into an
1246-
/// expression context.
1249+
/// This wraps the exception and does the throw. Since this is a function, it
1250+
/// is possible to call this in a JavaScript expression context, where the throw
1251+
/// statement is not allowed. The wrapper may be passed in, but usually
1252+
/// [throwExpression] is called with one argument, leaving `wrapper` undefined.
12471253
@pragma('dart2js:never-inline')
1248-
Never throwExpression(ex) {
1249-
// TODO(sra): Manually inline `wrapException` to remove one stack frame.
1250-
JS<Never>('', 'throw #', wrapException(ex));
1251-
}
1252-
1253-
Never throwExpressionWithWrapper(ex, wrapper) {
1254-
JS<Never>('', 'throw #', initializeExceptionWrapper(wrapper, ex));
1254+
Never throwExpression(ex, [wrapper]) {
1255+
wrapper ??= JS('', 'new Error()');
1256+
JS<Never>('', 'throw #', initializeExceptionWrapper(ex, wrapper));
12551257
}
12561258

12571259
throwUnsupportedError(message) {
@@ -1272,8 +1274,7 @@ Never throwUnsupportedOperation(Object o, [Object? operation, Object? verb]) {
12721274
operation ??= 0;
12731275
verb ??= 0;
12741276
final wrapper = JS('', 'Error()');
1275-
throwExpressionWithWrapper(
1276-
_diagnoseUnsupportedOperation(o, operation, verb), wrapper);
1277+
throwExpression(_diagnoseUnsupportedOperation(o, operation, verb), wrapper);
12771278
}
12781279

12791280
@pragma('dart2js:never-inline')

sdk/lib/_internal/js_runtime/lib/late_helper.dart

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,42 @@
55
library _late_helper;
66

77
import 'dart:_internal' show LateError, createSentinel, isSentinel;
8-
import 'dart:_js_helper' show throwExpressionWithWrapper;
98
import 'dart:_foreign_helper' show JS;
109

1110
@pragma('dart2js:never-inline')
11+
@pragma('dart2js:stack-starts-at-throw')
1212
void throwLateFieldNI(String fieldName) {
13-
final wrapper = JS('', 'new Error()');
14-
throwExpressionWithWrapper(LateError.fieldNI(fieldName), wrapper);
13+
throw LateError.fieldNI(fieldName);
1514
}
1615

1716
@pragma('dart2js:never-inline')
17+
@pragma('dart2js:stack-starts-at-throw')
1818
void throwLateFieldAI(String fieldName) {
19-
final wrapper = JS('', 'new Error()');
20-
throwExpressionWithWrapper(LateError.fieldAI(fieldName), wrapper);
19+
throw LateError.fieldAI(fieldName);
2120
}
2221

2322
@pragma('dart2js:never-inline')
23+
@pragma('dart2js:stack-starts-at-throw')
2424
void throwLateFieldADI(String fieldName) {
25-
final wrapper = JS('', 'new Error()');
26-
throwExpressionWithWrapper(LateError.fieldADI(fieldName), wrapper);
25+
throw LateError.fieldADI(fieldName);
2726
}
2827

2928
@pragma('dart2js:never-inline')
29+
@pragma('dart2js:stack-starts-at-throw')
3030
void throwUnnamedLateFieldNI() {
31-
final wrapper = JS('', 'new Error()');
32-
throwExpressionWithWrapper(LateError.fieldNI(''), wrapper);
31+
throw LateError.fieldNI('');
3332
}
3433

3534
@pragma('dart2js:never-inline')
35+
@pragma('dart2js:stack-starts-at-throw')
3636
void throwUnnamedLateFieldAI() {
37-
final wrapper = JS('', 'new Error()');
38-
throwExpressionWithWrapper(LateError.fieldAI(''), wrapper);
37+
throw LateError.fieldAI('');
3938
}
4039

4140
@pragma('dart2js:never-inline')
41+
@pragma('dart2js:stack-starts-at-throw')
4242
void throwUnnamedLateFieldADI() {
43-
final wrapper = JS('', 'new Error()');
44-
throwExpressionWithWrapper(LateError.fieldADI(''), wrapper);
43+
throw LateError.fieldADI('');
4544
}
4645

4746
/// A boxed variable used for lowering uninitialized `late` variables when they

0 commit comments

Comments
 (0)