| // Copyright (c) 2022, 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. |
| |
| import 'package:collection/collection.dart'; |
| |
| import '_formatting.dart'; |
| import '_primitives.dart'; |
| |
| class ContextKeys { |
| static const startCallstack = 'start'; |
| static const disposalCallstack = 'disposal'; |
| static const retainingPath = 'path'; |
| } |
| |
| enum LeakType { |
| /// Not disposed and garbage collected. |
| notDisposed, |
| |
| /// Disposed and not garbage collected when expected. |
| notGCed, |
| |
| /// Disposed and garbage collected later than expected. |
| gcedLate; |
| |
| static LeakType byName(String name) => LeakType.values.byName(name); |
| } |
| |
| /// Names for json fields. |
| class _JsonFields { |
| static const String type = 'type'; |
| static const String trackedClass = 'tracked'; |
| static const String context = 'context'; |
| static const String code = 'code'; |
| static const String time = 'time'; |
| static const String totals = 'totals'; |
| static const String phase = 'phase'; |
| } |
| |
| abstract class LeakProvider { |
| Future<LeakSummary> leaksSummary(); |
| Future<Leaks> collectLeaks(); |
| Future<void> checkNotGCed(); |
| } |
| |
| /// Statistical information about found leaks. |
| class LeakSummary { |
| LeakSummary(this.totals, {DateTime? time}) { |
| this.time = time ?? DateTime.now(); |
| } |
| |
| factory LeakSummary.fromJson(Map<String, dynamic> json) => LeakSummary( |
| (json[_JsonFields.totals] as Map<String, dynamic>).map( |
| (key, value) => MapEntry( |
| LeakType.byName(key), |
| int.parse(value as String), |
| ), |
| ), |
| time: |
| DateTime.fromMillisecondsSinceEpoch(json[_JsonFields.time] as int), |
| ); |
| |
| final Map<LeakType, int> totals; |
| |
| late final DateTime time; |
| |
| int get total => totals.values.sum; |
| |
| bool get isEmpty => total == 0; |
| |
| String toMessage() { |
| return '${totals.values.sum} memory leak(s): ' |
| 'not disposed: ${totals[LeakType.notDisposed]}, ' |
| 'not GCed: ${totals[LeakType.notGCed]}, ' |
| 'GCed late: ${totals[LeakType.gcedLate]}'; |
| } |
| |
| Map<String, dynamic> toJson() => { |
| _JsonFields.totals: |
| totals.map((key, value) => MapEntry(key.name, value.toString())), |
| _JsonFields.time: time.millisecondsSinceEpoch, |
| }; |
| |
| bool matches(LeakSummary? other) => |
| other != null && |
| const DeepCollectionEquality().equals(totals, other.totals); |
| } |
| |
| /// Detailed information about found leaks. |
| class Leaks { |
| Leaks(this.byType); |
| |
| factory Leaks.fromJson(Map<String, dynamic> json) => Leaks( |
| json.map( |
| (key, value) => MapEntry( |
| LeakType.byName(key), |
| (value as List) |
| .cast<Map<String, dynamic>>() |
| .map(LeakReport.fromJson) |
| .toList(growable: false), |
| ), |
| ), |
| ); |
| |
| final Map<LeakType, List<LeakReport>> byType; |
| |
| List<LeakReport> get notGCed => byType[LeakType.notGCed] ?? []; |
| List<LeakReport> get notDisposed => byType[LeakType.notDisposed] ?? []; |
| List<LeakReport> get gcedLate => byType[LeakType.gcedLate] ?? []; |
| List<LeakReport> get all => byType.values.flattened.toList(); |
| |
| Map<String, dynamic> toJson() => byType.map( |
| (key, value) => |
| MapEntry(key.name, value.map((e) => e.toJson()).toList()), |
| ); |
| |
| int get total => byType.values.map((e) => e.length).sum; |
| |
| String toYaml({required bool phasesAreTests}) { |
| if (total == 0) return ''; |
| final leaks = LeakType.values |
| .map( |
| (e) => LeakReport.iterableToYaml( |
| e.name, |
| byType[e] ?? [], |
| phasesAreTests: phasesAreTests, |
| ), |
| ) |
| .join(); |
| return '$leakTrackerYamlHeader$leaks'; |
| } |
| } |
| |
| /// Leak information, passed from application to DevTools and than extended by |
| /// DevTools after deeper analysis. |
| class LeakReport { |
| LeakReport({ |
| required this.trackedClass, |
| required this.context, |
| required this.code, |
| required this.type, |
| required this.phase, |
| }); |
| |
| factory LeakReport.fromJson(Map<String, dynamic> json) => LeakReport( |
| type: json[_JsonFields.type] as String, |
| context: json[_JsonFields.context] as Map<String, dynamic>? ?? {}, |
| code: json[_JsonFields.code] as int, |
| trackedClass: json[_JsonFields.trackedClass] as String? ?? '', |
| phase: json[_JsonFields.phase] as String?, |
| ); |
| |
| /// Information about the leak that can help in troubleshooting. |
| /// |
| /// Use [ContextKeys] to access predefined keys. |
| final Map<String, dynamic>? context; |
| |
| /// [identityHashCode] of the object. |
| final int code; |
| |
| /// Runtime type of the object. |
| final String type; |
| |
| /// Full name of class, the leak tracking is defined for. |
| /// |
| /// Usually [trackedClass] is expected to be a supertype of [type]. |
| final String trackedClass; |
| |
| final String? phase; |
| |
| // The fields below do not need serialization as they are populated after. |
| String? retainingPath; |
| List<String>? detailedPath; |
| |
| Map<String, dynamic> toJson() => { |
| _JsonFields.type: type, |
| _JsonFields.context: context, |
| _JsonFields.code: code, |
| _JsonFields.trackedClass: trackedClass, |
| }; |
| |
| static String iterableToYaml( |
| String title, |
| Iterable<LeakReport>? leaks, { |
| String indent = '', |
| required bool phasesAreTests, |
| }) { |
| if (leaks == null || leaks.isEmpty) return ''; |
| |
| return '''$title: |
| $indent total: ${leaks.length} |
| $indent objects: |
| ${leaks.map((e) => e.toYaml('$indent ', phasesAreTests: phasesAreTests)).join()} |
| '''; |
| } |
| |
| String toYaml(String indent, {required bool phasesAreTests}) { |
| final result = StringBuffer(); |
| result.writeln('$indent$type:'); |
| if (phase != null) { |
| final fieldName = phasesAreTests ? 'test' : 'phase'; |
| result.writeln('$indent $fieldName: $phase'); |
| } |
| result.writeln('$indent identityHashCode: $code'); |
| final theContext = context; |
| if (theContext != null && theContext.isNotEmpty) { |
| result.writeln('$indent context:'); |
| final contextIndent = '$indent '; |
| result.write( |
| theContext.keys.map((key) { |
| final value = _toMultiLineYamlString( |
| contextToString(theContext[key]), |
| ' $contextIndent', |
| ); |
| return '$contextIndent$key: $value\n'; |
| }).join(), |
| ); |
| } |
| |
| if (detailedPath != null) { |
| result.writeln('$indent retainingPath:'); |
| result.writeln(detailedPath!.map((s) => '$indent - $s').join('\n')); |
| } else if (retainingPath != null) { |
| result.writeln('$indent retainingPath: $retainingPath'); |
| } |
| return result.toString(); |
| } |
| |
| static String _toMultiLineYamlString(String text, String indent) { |
| if (!text.contains('\n')) return text; |
| text = text.replaceAll('\n', '\n$indent').trimRight(); |
| return '>\n$indent$text'; |
| } |
| } |