Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@dnfield
Copy link
Contributor

@dnfield dnfield commented Mar 10, 2020

The idea is to eventually apply this to dart:ui and package:flutter/*. This saves about 200kb on the gallery snapshot. An audit of the codebase (and the flutter style guide) point to these methods always being used for debug only info.

@dnfield
Copy link
Contributor Author

dnfield commented Mar 10, 2020

Ah. I should write some tests for this.

Copy link
Contributor

@jonahwilliams jonahwilliams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a script in here somewhere that populates a .packages, maybe that is working?

@dnfield
Copy link
Contributor Author

dnfield commented Mar 10, 2020

Yeah, it's the python script in the folder I believe, which is getting invoked by the build process, which creates the .packages file we need.

@dnfield
Copy link
Contributor Author

dnfield commented Mar 10, 2020

It seems a bit overly complex TBH, it might be good to just replace it with a hard coded .packages file.

Copy link
Member

@zanderso zanderso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


@override
void visitProcedure(Procedure node) {
if (node.name.name == 'toString' && node.enclosingLibrary != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: You can flip the sense of the ifs to lower the nesting depth.

if (node.name.name != 'toString' || node.enclosingLibrary == null) {
  super.visitProcedure(node);
  return;
}
assert(node.enclosingClass != null);
// Turn 'dart:ui' into 'dart:ui', or
// 'package:flutter/src/semantics_event.dart' into 'package:flutter'.
final String packageUri =
  '${node.enclosingLibrary.importUri.scheme}:'
  '${node.enclosingLibrary.importUri.pathSegments.first}';
if (!_packageUris.contains(packageUri)) {
  super.visitProcedure(node);
  return;
}
node.function.replaceWith(
  FunctionNode(
    ReturnStatement(
      SuperMethodInvocation(
        node.name,
        Arguments(<Expression>[]),
      ),
    ),
  ),
);
super.visitProcedure(node);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this slightly differently so that I don't have to do multiple returns but still dropped some nesting.

This also made me realize it's a mistake to assert enclosingClass is null - I should be checking for it instead, in case someone put a toString method at the top of their library. Added a test for that.

@dnfield
Copy link
Contributor Author

dnfield commented Mar 11, 2020

/cc @alexmarkov


final frontend.ProgramTransformer _child;

/// A set of package URIs to apply this transformer too, e.g. 'dart:ui' and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "to" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks, fixing this and the others

}
}

/// Replaces [Object.toString] overides with calls to super for the specified
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "overides" -> "overrides"

/// The [packageUris] must not be null.
ToStringVisitor(this._packageUris) : assert(_packageUris != null);

/// A set of package URIs to apply this transformer too, e.g. 'dart:ui' and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "too" -> "to" ?

@dnfield
Copy link
Contributor Author

dnfield commented Mar 11, 2020

I'm starting to think that this could be improved so that it reads a specific annotation, as suggested in the original issue.

I think we'd still want to avoid completely removing the method, as that gets really messy (we have to update any callsites, which might be hard to find, and it gets a lot harder to figure out what's going on if anything goes wrong).

_packageUris.contains(_importUriToPackage(node.enclosingLibrary.importUri))
) {
node.function.replaceWith(
FunctionNode(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to verify that toString() doesn't have any extra optional parameters. Otherwise AST would be incorrect if there are calls to such toString which pass those optional parameters, but this transformation removed them.

Or maybe we can replace a node.function.body instead of replacing the whole FunctionNode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to replace node.function.body. If that works that sounds better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and updated tests.

void visitProcedure(Procedure node) {
if (
node.name.name == 'toString' &&
node.enclosingClass != null &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also check !node.isStatic && !node.isAbstract

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (
node.name.name == 'toString' &&
node.enclosingClass != null &&
node.enclosingLibrary != null &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check looks unnecessary: if there is an enclosing class, then there should be an enclosing library as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'd be more concerned about somehow getting an NPE below this (where I access enclosingLibrary.importUri) than saving the time on the extra null check.

),
);
}
super.visitProcedure(node);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to go deeper? Not going beyond members might save a little bit of compilation time.
Also consider adding

@OverRide
defaultMember(Member node) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed something here. I'll ping this again when I push up the change that should fix it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I removed the super call this time. I've also added the ability to add an @pragma annotation to a toString method so we let it through.

@dnfield
Copy link
Contributor Author

dnfield commented Mar 11, 2020

I'm starting to suspect we could more safely and clearly attain these results with something like this:

const bool kReleaseMode = true;

mixin ToStringHelper {
  String debugToString() {
    return '$runtimeType';
  }
  
  @override
  String toString() {
    if (kReleaseMode) {
      return super.toString();
    }  
    return debugToString();
  }
}

class MyWidget with ToStringHelper {
  @override
  String debugToString() {
    return '$runtimeType(blah)';
  }
}

void main() {
  var w = MyWidget();
  print(w.toString());
}

We could make such a mixin in dart:ui and either export it to use in the framework or just make DiagnosticableMixin do it in the framework.

This would let someone pretty easily "opt out" if they really needed to.

@dnfield
Copy link
Contributor Author

dnfield commented Mar 11, 2020

It would also let packages outside of the framework safely adopt this pattern.

@dnfield
Copy link
Contributor Author

dnfield commented Mar 11, 2020

The following annotation would now make this transformer ignore a specific toString method:

@pragma('flutter_frontend:keep')
@override
String toString() => 'MyImportantToSTringEvenInReleaseMode';

for (ConstantExpression expression in node.annotations.whereType<ConstantExpression>()) {
final InstanceConstant constant = expression.constant as InstanceConstant;
if (constant.classNode.name == 'pragma' && constant.classNode.enclosingLibrary.importUri.toString() == 'dart:core') {
final StringConstant pragmaName = constant.fieldValues.values.first as StringConstant;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constant.fieldValues.values.first doesn't look reliable as there are multiple fields which may go in an arbitrary order in constant.fieldValues map.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aww phooey. I'll try to fix that up. My hope was that since this should be a stable order map (the contract for Map is that order remains the same), and we know the type has its first parameter as the name, this could be ok. But I'll try to do something better here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


bool _hasKeepAnnotation(Procedure node) {
for (ConstantExpression expression in node.annotations.whereType<ConstantExpression>()) {
final InstanceConstant constant = expression.constant as InstanceConstant;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider testing if expression.constant is InstanceConstant, as there could be other kinds of constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and did it for the cast below as well.

@dnfield dnfield requested review from alexmarkov and zanderso March 16, 2020 16:44
import 'package:kernel/ast.dart';
import 'package:path/path.dart' as path;

import 'package:vm/incremental_compiler.dart';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: alphabetize imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return completer.future;
}

/// A [RecursiveVisitor] that replaces [Object.toString] overrides with
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this code go in a separate file/library?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started doing that but it seemed to be overly complicating things to me - these classes are pretty short right now and the style in the engine and flutter repos tend to be to group related things together rather than splitting files.

That said I don't have a strong opinion about this one either way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably makes more sense to move the transformations to (a) separate library(ies) when the second one gets added. Let's keep an eye out for that, or leave a comment/TODO.

Copy link
Contributor

@alexmarkov alexmarkov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

if (pragmaName is! StringConstant) {
continue;
}
if ((pragmaName as StringConstant).value == 'flutter_frontend:keep') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe call the pragma simply 'flutter:keep' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give an example of where this annotation would go? In what situations is it needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class MyStringBuffer {
  StringBuffer _stringBuffer = StringBuffer();

  // ...

  @override
  @pragma('flutter_frontend:keep')
  String toString() {
    return _stringBuffer.toString();
  }
}

Would prevent the transformer from overwriting the method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there cases where a program would be incorrect, or the transformation would do something wrong if the annotation is not present?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone tried to apply this transfomer to dart:core, it would break their program - all the primitive type classes override toString in meaningful ways, as does StringBuffer.

You shouldn't do that, and we shouldn't make it easy for people to do that. The plan right now is to use this for dart:ui and package:flutter to save binary space, and to test this in a controlled internal environment with programs that have good test coverage in the modes we apply it to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline. Going to add a type to dart:ui called keepToString.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added annotation to dart:ui for this called keepToString, plus documentation there about this feature and how to use the annotation.

continue;
}
final InstanceConstant constant = expression.constant as InstanceConstant;
if (constant.classNode.name == 'keepToString' && constant.classNode.enclosingLibrary.importUri.toString() == 'dart:ui') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class name is _KeepToString, not keepToString.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh. I really need an integration test for this. I'll add one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and added an integration test that would catch this in the future.

Copy link
Member

@zanderso zanderso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm w/ nits

return completer.future;
}

/// A [RecursiveVisitor] that replaces [Object.toString] overrides with
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably makes more sense to move the transformations to (a) separate library(ies) when the second one gets added. Let's keep an eye out for that, or leave a comment/TODO.

/// `toString` bodies with `return super.toString()` during compilation. This
/// significantly reduces release code size, and would make it impossible to
/// implement a meaningful override of `toString` for release mode without
/// disabling the feature and losing the size savings. If package uses this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If package uses --> If a package uses

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done and done.

/// `toString` bodies with `return super.toString()` during compilation. This
/// significantly reduces release code size, and would make it impossible to
/// implement a meaningful override of `toString` for release mode without
/// disabling the feature and losing the size savings. If package uses this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

// @dart = 2.6
part of dart.ui;

/// Annotation used by Flutter's Dart compiler to indicate that an
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the short description reference what happens in release vs. debug?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the flag is implemented, it's not really specific to release or debug.

When we get to a point where we'd turn this on by default in release we should probably mention something. I'll add a TODO to update the docs for this when that happens.

@dnfield dnfield requested a review from alexmarkov March 17, 2020 21:36
Copy link
Contributor

@alexmarkov alexmarkov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still lgtm.

@dnfield dnfield added the waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. label Mar 17, 2020
@dnfield
Copy link
Contributor Author

dnfield commented Mar 17, 2020

Filed flutter/flutter#52770 to track the infra flake, landing this on red to kick flaky infra failures.

@dnfield dnfield merged commit 034f913 into flutter:master Mar 17, 2020
@dnfield dnfield deleted the shake_tostring branch March 17, 2020 22:40
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Mar 18, 2020
@liyuqian liyuqian added perf: app size Performance issues related to app size (binary/code size) severe: performance Relates to speed or footprint issues. labels Mar 19, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

cla: yes perf: app size Performance issues related to app size (binary/code size) severe: performance Relates to speed or footprint issues. waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants