mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add option to track widget rebuilds and repaints from the Flutter inspector. (#23534)
This commit is contained in:
parent
b722a744e6
commit
10bcddcc5c
@ -5,6 +5,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import 'object.dart';
|
||||
|
||||
export 'package:flutter/foundation.dart' show debugPrint;
|
||||
|
||||
// Any changes to this file should be reflected in the debugAssertAllRenderVarsUnset()
|
||||
@ -116,6 +118,25 @@ bool debugCheckIntrinsicSizes = false;
|
||||
/// areas are being excessively repainted.
|
||||
bool debugProfilePaintsEnabled = false;
|
||||
|
||||
/// Signature for [debugOnProfilePaint] implementations.
|
||||
typedef ProfilePaintCallback = void Function(RenderObject renderObject);
|
||||
|
||||
/// Callback invoked for every [RenderObject] painted each frame.
|
||||
///
|
||||
/// This callback is only invoked in debug builds.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [debugProfilePaintsEnabled], which does something similar but adds
|
||||
/// [dart:developer.Timeline] events instead of invoking a callback.
|
||||
/// * [debugOnRebuildDirtyWidget], which does something similar for widgets
|
||||
/// being built.
|
||||
/// * [WidgetInspectorService], which uses the [debugOnProfilePaint]
|
||||
/// callback to generate aggregate profile statistics describing what paints
|
||||
/// occurred when the `ext.flutter.inspector.trackRepaintWidgets` service
|
||||
/// extension is enabled.
|
||||
ProfilePaintCallback debugOnProfilePaint;
|
||||
|
||||
/// Setting to true will cause all clipping effects from the layer tree to be
|
||||
/// ignored.
|
||||
///
|
||||
@ -205,7 +226,8 @@ bool debugAssertAllRenderVarsUnset(String reason, { bool debugCheckIntrinsicSize
|
||||
debugPrintMarkNeedsPaintStacks ||
|
||||
debugPrintLayouts ||
|
||||
debugCheckIntrinsicSizes != debugCheckIntrinsicSizesOverride ||
|
||||
debugProfilePaintsEnabled) {
|
||||
debugProfilePaintsEnabled ||
|
||||
debugOnProfilePaint != null) {
|
||||
throw FlutterError(reason);
|
||||
}
|
||||
return true;
|
||||
|
||||
@ -161,6 +161,8 @@ class PaintingContext extends ClipContext {
|
||||
assert(() {
|
||||
if (debugProfilePaintsEnabled)
|
||||
Timeline.startSync('${child.runtimeType}', arguments: timelineWhitelistArguments);
|
||||
if (debugOnProfilePaint != null)
|
||||
debugOnProfilePaint(child);
|
||||
return true;
|
||||
}());
|
||||
|
||||
|
||||
@ -711,6 +711,11 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
|
||||
|
||||
@override
|
||||
Future<void> performReassemble() {
|
||||
assert(() {
|
||||
WidgetInspectorService.instance.performReassemble();
|
||||
return true;
|
||||
}());
|
||||
|
||||
deferFirstFrameReport();
|
||||
if (renderViewElement != null)
|
||||
buildOwner.reassemble(renderViewElement);
|
||||
|
||||
@ -30,6 +30,26 @@ import 'table.dart';
|
||||
/// See also the discussion at [WidgetsBinding.drawFrame].
|
||||
bool debugPrintRebuildDirtyWidgets = false;
|
||||
|
||||
/// Signature for [debugOnRebuildDirtyWidget] implementations.
|
||||
typedef RebuildDirtyWidgetCallback = void Function(Element e, bool builtOnce);
|
||||
|
||||
/// Callback invoked for every dirty widget built each frame.
|
||||
///
|
||||
/// This callback is only invoked in debug builds.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [debugPrintRebuildDirtyWidgets], which does something similar but logs
|
||||
/// to the console instead of invoking a callback.
|
||||
/// * [debugOnProfilePaint], which does something similar for [RenderObject]
|
||||
/// painting.
|
||||
/// * [WidgetInspectorService], which uses the [debugOnRebuildDirtyWidget]
|
||||
/// callback to generate aggregate profile statistics describing which widget
|
||||
/// rebuilds occurred when the
|
||||
/// `ext.flutter.inspector.trackRebuildDirtyWidgets` service extension is
|
||||
/// enabled.
|
||||
RebuildDirtyWidgetCallback debugOnRebuildDirtyWidget;
|
||||
|
||||
/// Log all calls to [BuildOwner.buildScope].
|
||||
///
|
||||
/// Combined with [debugPrintScheduleBuildForStacks], this allows you to track
|
||||
|
||||
@ -3514,6 +3514,9 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
||||
if (!_active || !_dirty)
|
||||
return;
|
||||
assert(() {
|
||||
if (debugOnRebuildDirtyWidget != null) {
|
||||
debugOnRebuildDirtyWidget(this, _debugBuiltOnce);
|
||||
}
|
||||
if (debugPrintRebuildDirtyWidgets) {
|
||||
if (!_debugBuiltOnce) {
|
||||
debugPrint('Building $this');
|
||||
|
||||
@ -32,6 +32,7 @@ import 'package:vector_math/vector_math_64.dart';
|
||||
import 'app.dart';
|
||||
import 'basic.dart';
|
||||
import 'binding.dart';
|
||||
import 'debug.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'icon_data.dart';
|
||||
@ -523,7 +524,7 @@ class _ScreenshotPaintingContext extends PaintingContext {
|
||||
///
|
||||
/// The [debugPaint] argument specifies whether the image should include the
|
||||
/// output of [RenderObject.debugPaint] for [renderObject] with
|
||||
/// [debugPaintSizeEnabled] set to `true`. Debug paint information is not
|
||||
/// [debugPaintSizeEnabled] set to true. Debug paint information is not
|
||||
/// included for the children of [renderObject] so that it is clear precisely
|
||||
/// which object the debug paint information references.
|
||||
///
|
||||
@ -621,7 +622,7 @@ class _DiagnosticsPathNode {
|
||||
|
||||
/// Index of the child that the path continues on.
|
||||
///
|
||||
/// Equal to `null` if the path does not continue.
|
||||
/// Equal to null if the path does not continue.
|
||||
final int childIndex;
|
||||
}
|
||||
|
||||
@ -673,7 +674,7 @@ class _InspectorReferenceData {
|
||||
/// JSON mainly focused on if and how children are included in the JSON.
|
||||
class _SerializeConfig {
|
||||
_SerializeConfig({
|
||||
@required this.groupName,
|
||||
this.groupName,
|
||||
this.summaryTree = false,
|
||||
this.subtreeDepth = 1,
|
||||
this.pathToInclude,
|
||||
@ -693,6 +694,12 @@ class _SerializeConfig {
|
||||
includeProperties = base.includeProperties,
|
||||
expandPropertyValues = base.expandPropertyValues;
|
||||
|
||||
/// Optional object group name used to manage manage lifetimes of object
|
||||
/// references in the returned JSON.
|
||||
///
|
||||
/// A call to `ext.flutter.inspector.disposeGroup` is required before objects
|
||||
/// in the tree are garbage collected unless [groupName] is null in
|
||||
/// which case no object references are included in the JSON payload.
|
||||
final String groupName;
|
||||
|
||||
/// Whether to only include children that would exist in the summary tree.
|
||||
@ -712,6 +719,13 @@ class _SerializeConfig {
|
||||
/// Expand children of properties that have values that are themselves
|
||||
/// Diagnosticable objects.
|
||||
final bool expandPropertyValues;
|
||||
|
||||
/// Whether to include object references to the [DiagnosticsNode] and
|
||||
/// [DiagnosticsNode.value] objects in the JSON payload.
|
||||
///
|
||||
/// If [interactive] is true, a call to `ext.flutter.inspector.disposeGroup`
|
||||
/// is required before objects in the tree will ever be garbage collected.
|
||||
bool get interactive => groupName != null;
|
||||
}
|
||||
|
||||
// Production implementation of [WidgetInspectorService].
|
||||
@ -776,6 +790,9 @@ mixin WidgetInspectorService {
|
||||
|
||||
List<String> _pubRootDirectories;
|
||||
|
||||
bool _trackRebuildDirtyWidgets = false;
|
||||
bool _trackRepaintWidgets = false;
|
||||
|
||||
_RegisterServiceExtensionCallback _registerServiceExtensionCallback;
|
||||
/// Registers a service extension method with the given name (full
|
||||
/// name "ext.flutter.inspector.name").
|
||||
@ -811,9 +828,11 @@ mixin WidgetInspectorService {
|
||||
}
|
||||
|
||||
/// Registers a service extension method with the given name (full
|
||||
/// name "ext.flutter.inspector.name"), which takes a single required argument
|
||||
/// name "ext.flutter.inspector.name"), which takes a single optional argument
|
||||
/// "objectGroup" specifying what group is used to manage lifetimes of
|
||||
/// object references in the returned JSON (see [disposeGroup]).
|
||||
/// If "objectGroup" is omitted, the returned JSON will not include any object
|
||||
/// references to avoid leaking memory.
|
||||
void _registerObjectGroupServiceExtension({
|
||||
@required String name,
|
||||
@required FutureOr<Object> callback(String objectGroup),
|
||||
@ -821,7 +840,6 @@ mixin WidgetInspectorService {
|
||||
registerServiceExtension(
|
||||
name: name,
|
||||
callback: (Map<String, String> parameters) async {
|
||||
assert(parameters.containsKey('objectGroup'));
|
||||
return <String, Object>{'result': await callback(parameters['objectGroup'])};
|
||||
},
|
||||
);
|
||||
@ -930,6 +948,8 @@ mixin WidgetInspectorService {
|
||||
assert(!_debugServiceExtensionsRegistered);
|
||||
assert(() { _debugServiceExtensionsRegistered = true; return true; }());
|
||||
|
||||
SchedulerBinding.instance.addPersistentFrameCallback(_onFrameStart);
|
||||
|
||||
_registerBoolServiceExtension(
|
||||
name: 'show',
|
||||
getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
|
||||
@ -942,6 +962,60 @@ mixin WidgetInspectorService {
|
||||
},
|
||||
);
|
||||
|
||||
if (isWidgetCreationTracked()) {
|
||||
// Service extensions that are only supported if widget creation locations
|
||||
// are tracked.
|
||||
_registerBoolServiceExtension(
|
||||
name: 'trackRebuildDirtyWidgets',
|
||||
getter: () async => _trackRebuildDirtyWidgets,
|
||||
setter: (bool value) async {
|
||||
if (value == _trackRebuildDirtyWidgets) {
|
||||
return null;
|
||||
}
|
||||
_rebuildStats.resetCounts();
|
||||
_trackRebuildDirtyWidgets = value;
|
||||
if (value) {
|
||||
assert(debugOnRebuildDirtyWidget == null);
|
||||
debugOnRebuildDirtyWidget = _onRebuildWidget;
|
||||
// Trigger a rebuild so there are baseline stats for rebuilds
|
||||
// performed by the app.
|
||||
return forceRebuild();
|
||||
} else {
|
||||
debugOnRebuildDirtyWidget = null;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_registerBoolServiceExtension(
|
||||
name: 'trackRepaintWidgets',
|
||||
getter: () async => _trackRepaintWidgets,
|
||||
setter: (bool value) async {
|
||||
if (value == _trackRepaintWidgets) {
|
||||
return;
|
||||
}
|
||||
_repaintStats.resetCounts();
|
||||
_trackRepaintWidgets = value;
|
||||
if (value) {
|
||||
assert(debugOnProfilePaint == null);
|
||||
debugOnProfilePaint = _onPaint;
|
||||
// Trigger an immediate paint so the user has some baseline painting
|
||||
// stats to view.
|
||||
void markTreeNeedsPaint(RenderObject renderObject) {
|
||||
renderObject.markNeedsPaint();
|
||||
renderObject.visitChildren(markTreeNeedsPaint);
|
||||
}
|
||||
final RenderObject root = RendererBinding.instance.renderView;
|
||||
if (root != null) {
|
||||
markTreeNeedsPaint(root);
|
||||
}
|
||||
} else {
|
||||
debugOnProfilePaint = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_registerSignalServiceExtension(
|
||||
name: 'disposeAllGroups',
|
||||
callback: disposeAllGroups,
|
||||
@ -1001,7 +1075,6 @@ mixin WidgetInspectorService {
|
||||
name: 'getRootWidgetSummaryTree',
|
||||
callback: _getRootWidgetSummaryTree,
|
||||
);
|
||||
|
||||
_registerServiceExtensionWithArg(
|
||||
name: 'getDetailsSubtree',
|
||||
callback: _getDetailsSubtree,
|
||||
@ -1052,6 +1125,11 @@ mixin WidgetInspectorService {
|
||||
);
|
||||
}
|
||||
|
||||
void _clearStats() {
|
||||
_rebuildStats.resetCounts();
|
||||
_repaintStats.resetCounts();
|
||||
}
|
||||
|
||||
/// Clear all InspectorService object references.
|
||||
///
|
||||
/// Use this method only for testing to ensure that object references from one
|
||||
@ -1188,7 +1266,7 @@ mixin WidgetInspectorService {
|
||||
/// Set the [WidgetInspector] selection to the object matching the specified
|
||||
/// id if the object is valid object to set as the inspector selection.
|
||||
///
|
||||
/// Returns `true` if the selection was changed.
|
||||
/// Returns true if the selection was changed.
|
||||
///
|
||||
/// The `groupName` parameter is not required by is added to regularize the
|
||||
/// API surface of methods called from the Flutter IntelliJ Plugin.
|
||||
@ -1200,7 +1278,7 @@ mixin WidgetInspectorService {
|
||||
/// Set the [WidgetInspector] selection to the specified `object` if it is
|
||||
/// a valid object to set as the inspector selection.
|
||||
///
|
||||
/// Returns `true` if the selection was changed.
|
||||
/// Returns true if the selection was changed.
|
||||
///
|
||||
/// The `groupName` parameter is not needed but is specified to regularize the
|
||||
/// API surface of methods called from the Flutter IntelliJ Plugin.
|
||||
@ -1219,7 +1297,7 @@ mixin WidgetInspectorService {
|
||||
selection.current = object;
|
||||
}
|
||||
if (selectionChangedCallback != null) {
|
||||
if (WidgetsBinding.instance.schedulerPhase == SchedulerPhase.idle) {
|
||||
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
|
||||
selectionChangedCallback();
|
||||
} else {
|
||||
// It isn't safe to trigger the selection change callback if we are in
|
||||
@ -1310,9 +1388,18 @@ mixin WidgetInspectorService {
|
||||
return null;
|
||||
final Map<String, Object> json = node.toJsonMap();
|
||||
|
||||
json['objectId'] = toId(node, config.groupName);
|
||||
final Object value = node.value;
|
||||
json['valueId'] = toId(value, config.groupName);
|
||||
if (config.interactive) {
|
||||
json['objectId'] = toId(node, config.groupName);
|
||||
json['valueId'] = toId(value, config.groupName);
|
||||
}
|
||||
|
||||
if (value is Element) {
|
||||
if (value is StatefulElement) {
|
||||
json['stateful'] = true;
|
||||
}
|
||||
json['widgetRuntimeType'] = value.widget?.runtimeType.toString();
|
||||
}
|
||||
|
||||
if (config.summaryTree) {
|
||||
json['summaryTree'] = true;
|
||||
@ -1321,6 +1408,7 @@ mixin WidgetInspectorService {
|
||||
final _Location creationLocation = _getCreationLocation(value);
|
||||
bool createdByLocalProject = false;
|
||||
if (creationLocation != null) {
|
||||
json['locationId'] = _toLocationId(creationLocation);
|
||||
json['creationLocation'] = creationLocation.toJsonMap();
|
||||
if (_isLocalCreationLocation(creationLocation)) {
|
||||
createdByLocalProject = true;
|
||||
@ -1384,6 +1472,7 @@ mixin WidgetInspectorService {
|
||||
if (_pubRootDirectories == null || location == null || location.file == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String file = Uri.parse(location.file).path;
|
||||
for (String directory in _pubRootDirectories) {
|
||||
if (file.startsWith(directory)) {
|
||||
@ -1573,6 +1662,7 @@ mixin WidgetInspectorService {
|
||||
/// information needed for the details subtree view.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [getChildrenDetailsSubtree], a method to get children of a node
|
||||
/// in the details subtree.
|
||||
String getDetailsSubtree(String id, String groupName) {
|
||||
@ -1736,13 +1826,248 @@ mixin WidgetInspectorService {
|
||||
/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0
|
||||
/// is required as injecting creation locations requires a
|
||||
/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation).
|
||||
@protected
|
||||
bool isWidgetCreationTracked() {
|
||||
_widgetCreationTracked ??= _WidgetForTypeTests() is _HasCreationLocation;
|
||||
return _widgetCreationTracked;
|
||||
}
|
||||
|
||||
bool _widgetCreationTracked;
|
||||
|
||||
Duration _frameStart;
|
||||
|
||||
void _onFrameStart(Duration timeStamp) {
|
||||
_frameStart = timeStamp;
|
||||
SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd);
|
||||
}
|
||||
|
||||
void _onFrameEnd(Duration timeStamp) {
|
||||
if (_trackRebuildDirtyWidgets) {
|
||||
_postStatsEvent('Flutter.RebuiltWidgets', _rebuildStats);
|
||||
}
|
||||
if (_trackRepaintWidgets) {
|
||||
_postStatsEvent('Flutter.RepaintWidgets', _repaintStats);
|
||||
}
|
||||
}
|
||||
|
||||
void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) {
|
||||
postEvent(eventName, stats.exportToJson(_frameStart));
|
||||
}
|
||||
|
||||
/// All events dispatched by a [WidgetInspectorService] use this method
|
||||
/// instead of calling [developer.postEvent] directly so that tests for
|
||||
/// [WidgetInspectorService] can track which events were dispatched by
|
||||
/// overriding this method.
|
||||
@protected
|
||||
void postEvent(String eventKind, Map<Object, Object> eventData) {
|
||||
developer.postEvent(eventKind, eventData);
|
||||
}
|
||||
|
||||
final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker();
|
||||
final _ElementLocationStatsTracker _repaintStats = _ElementLocationStatsTracker();
|
||||
|
||||
void _onRebuildWidget(Element element, bool builtOnce) {
|
||||
_rebuildStats.add(element);
|
||||
}
|
||||
|
||||
void _onPaint(RenderObject renderObject) {
|
||||
try {
|
||||
final Element element = renderObject.debugCreator?.element;
|
||||
if (element is! RenderObjectElement) {
|
||||
// This branch should not hit as long as all RenderObjects were created
|
||||
// by Widgets. It is possible there might be some render objects
|
||||
// created directly without using the Widget layer so we add this check
|
||||
// to improve robustness.
|
||||
return;
|
||||
}
|
||||
_repaintStats.add(element);
|
||||
|
||||
// Give all ancestor elements credit for repainting as long as they do
|
||||
// not have their own associated RenderObject.
|
||||
element.visitAncestorElements((Element ancestor) {
|
||||
if (ancestor is RenderObjectElement) {
|
||||
// This ancestor has its own RenderObject so we can precisely track
|
||||
// when it repaints.
|
||||
return false;
|
||||
}
|
||||
_repaintStats.add(ancestor);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
catch (exception, stack) {
|
||||
FlutterError.reportError(
|
||||
FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// This method is called by [WidgetBinding.performReassemble] to flush caches
|
||||
/// of obsolete values after a hot reload.
|
||||
///
|
||||
/// Do not call this method directly. Instead, use
|
||||
/// [BindingBase.reassembleApplication].
|
||||
void performReassemble() {
|
||||
_clearStats();
|
||||
}
|
||||
}
|
||||
|
||||
/// Accumulator for a count associated with a specific source location.
|
||||
///
|
||||
/// The accumulator stores whether the source location is [local] and what its
|
||||
/// [id] for efficiency encoding terse JSON payloads describing counts.
|
||||
class _LocationCount {
|
||||
_LocationCount({
|
||||
@required this.location,
|
||||
@required this.id,
|
||||
@required this.local,
|
||||
});
|
||||
|
||||
/// Location id.
|
||||
final int id;
|
||||
|
||||
/// Whether the location is local to the current project.
|
||||
final bool local;
|
||||
|
||||
final _Location location;
|
||||
|
||||
int get count => _count;
|
||||
int _count = 0;
|
||||
|
||||
/// Reset the count.
|
||||
void reset() {
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// Increment the count.
|
||||
void increment() {
|
||||
_count++;
|
||||
}
|
||||
}
|
||||
|
||||
/// A stat tracker that aggregates a performance metric for [Element] objects at
|
||||
/// the granularity of creation locations in source code.
|
||||
///
|
||||
/// This class is optimized to minimize the size of the JSON payloads describing
|
||||
/// the aggregate statistics, for stable memory usage, and low CPU usage at the
|
||||
/// expense of somewhat higher overall memory usage. Stable memory usage is more
|
||||
/// important than peak memory usage to avoid the false impression that the
|
||||
/// user's app is leaking memory each frame.
|
||||
///
|
||||
/// The number of unique widget creation locations tends to be at most in the
|
||||
/// low thousands for regular flutter apps so the peak memory usage for this
|
||||
/// class is not an issue.
|
||||
class _ElementLocationStatsTracker {
|
||||
// All known creation location tracked.
|
||||
//
|
||||
// This could also be stored as a `Map<int, _LocationCount>` but this
|
||||
// representation is more efficient as all location ids from 0 to n are
|
||||
// typically present.
|
||||
//
|
||||
// All logic in this class assumes that if `_stats[i]` is not null
|
||||
// `_stats[i].id` equals `i`.
|
||||
final List<_LocationCount> _stats = <_LocationCount>[];
|
||||
|
||||
/// Locations with a non-zero count.
|
||||
final List<_LocationCount> active = <_LocationCount>[];
|
||||
|
||||
/// Locations that were added since stats were last exported.
|
||||
///
|
||||
/// Only locations local to the current project are included as a performance
|
||||
/// optimization.
|
||||
final List<_LocationCount> newLocations = <_LocationCount>[];
|
||||
|
||||
/// Increments the count associated with the creation location of [element] if
|
||||
/// the creation location is local to the current project.
|
||||
void add(Element element) {
|
||||
final Object widget = element.widget;
|
||||
if (widget is! _HasCreationLocation) {
|
||||
return;
|
||||
}
|
||||
final _HasCreationLocation creationLocationSource = widget;
|
||||
final _Location location = creationLocationSource._location;
|
||||
final int id = _toLocationId(location);
|
||||
|
||||
_LocationCount entry;
|
||||
if (id >= _stats.length || _stats[id] == null) {
|
||||
// After the first frame, almost all creation ids will already be in
|
||||
// _stats so this slow path will rarely be hit.
|
||||
while (id >= _stats.length) {
|
||||
_stats.add(null);
|
||||
}
|
||||
entry = _LocationCount(
|
||||
location: location,
|
||||
id: id,
|
||||
local: WidgetInspectorService.instance._isLocalCreationLocation(location),
|
||||
);
|
||||
if (entry.local) {
|
||||
newLocations.add(entry);
|
||||
}
|
||||
_stats[id] = entry;
|
||||
} else {
|
||||
entry = _stats[id];
|
||||
}
|
||||
|
||||
// We could in the future add an option to track stats for all widgets but
|
||||
// that would significantly increase the size of the events posted using
|
||||
// [developer.postEvent] and current use cases for this feature focus on
|
||||
// helping users find problems with their widgets not the platform
|
||||
// widgets.
|
||||
if (entry.local) {
|
||||
if (entry.count == 0) {
|
||||
active.add(entry);
|
||||
}
|
||||
entry.increment();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all aggregated statistics.
|
||||
void resetCounts() {
|
||||
// We chose to only reset the active counts instead of clearing all data
|
||||
// to reduce the number memory allocations performed after the first frame.
|
||||
// Once an app has warmed up, location stats tracking should not
|
||||
// trigger significant additional memory allocations. Avoiding memory
|
||||
// allocations is important to minimize the impact this class has on cpu
|
||||
// and memory performance of the running app.
|
||||
for (_LocationCount entry in active) {
|
||||
entry.reset();
|
||||
}
|
||||
active.clear();
|
||||
}
|
||||
|
||||
/// Exports the current counts and then resets the stats to prepare to track
|
||||
/// the next frame of data.
|
||||
Map<String, dynamic> exportToJson(Duration startTime) {
|
||||
final List<int> events = List<int>.filled(active.length * 2, 0);
|
||||
int j = 0;
|
||||
for (_LocationCount stat in active) {
|
||||
events[j++] = stat.id;
|
||||
events[j++] = stat.count;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> json = <String, dynamic>{
|
||||
'startTime': startTime.inMicroseconds,
|
||||
'events': events,
|
||||
};
|
||||
|
||||
if (newLocations.isNotEmpty) {
|
||||
// Add all newly used location ids to the JSON.
|
||||
final Map<String, List<int>> locationsJson = <String, List<int>>{};
|
||||
for (_LocationCount entry in newLocations) {
|
||||
final _Location location = entry.location;
|
||||
final List<int> jsonForFile = locationsJson.putIfAbsent(
|
||||
location.file,
|
||||
() => <int>[],
|
||||
);
|
||||
jsonForFile..add(entry.id)..add(location.line)..add(location.column);
|
||||
}
|
||||
json['newLocations'] = locationsJson;
|
||||
}
|
||||
resetCounts();
|
||||
newLocations.clear();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetForTypeTests extends Widget {
|
||||
@ -2460,3 +2785,20 @@ _Location _getCreationLocation(Object object) {
|
||||
final Object candidate = object is Element ? object.widget : object;
|
||||
return candidate is _HasCreationLocation ? candidate._location : null;
|
||||
}
|
||||
|
||||
// _Location objects are always const so we don't need to worry about the GC
|
||||
// issues that are a concern for other object ids tracked by
|
||||
// [WidgetInspectorService].
|
||||
final Map<_Location, int> _locationToId = <_Location, int>{};
|
||||
final List<_Location> _locations = <_Location>[];
|
||||
|
||||
int _toLocationId(_Location location) {
|
||||
int id = _locationToId[location];
|
||||
if (id != null) {
|
||||
return id;
|
||||
}
|
||||
id = _locations.length;
|
||||
_locations.add(location);
|
||||
_locationToId[location] = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
@ -528,12 +528,18 @@ void main() {
|
||||
});
|
||||
|
||||
test('Service extensions - posttest', () async {
|
||||
// See widget_inspector_test.dart for tests of the 15 ext.flutter.inspector
|
||||
// See widget_inspector_test.dart for tests of the ext.flutter.inspector
|
||||
// service extensions included in this count.
|
||||
int widgetInspectorExtensionCount = 15;
|
||||
if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
|
||||
// Some inspector extensions are only exposed if widget creation locations
|
||||
// are tracked.
|
||||
widgetInspectorExtensionCount += 2;
|
||||
}
|
||||
|
||||
// If you add a service extension... TEST IT! :-)
|
||||
// ...then increment this number.
|
||||
expect(binding.extensions.length, 38);
|
||||
expect(binding.extensions.length, 23 + widgetInspectorExtensionCount);
|
||||
|
||||
expect(console, isEmpty);
|
||||
debugPrint = debugPrintThrottled;
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -12,6 +13,103 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
// Start of block of code where widget creation location line numbers and
|
||||
// columns will impact whether tests pass.
|
||||
|
||||
class ClockDemo extends StatefulWidget {
|
||||
@override
|
||||
_ClockDemoState createState() => _ClockDemoState();
|
||||
}
|
||||
|
||||
class _ClockDemoState extends State<ClockDemo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('World Clock'),
|
||||
makeClock('Local', DateTime.now().timeZoneOffset.inHours),
|
||||
makeClock('UTC', 0),
|
||||
makeClock('New York, NY', -4),
|
||||
makeClock('Chicago, IL', -5),
|
||||
makeClock('Denver, CO', -6),
|
||||
makeClock('Los Angeles, CA', -7),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeClock(String label, num utcOffset) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
const Icon(Icons.watch),
|
||||
Text(label),
|
||||
ClockText(utcOffset: utcOffset),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClockText extends StatefulWidget {
|
||||
const ClockText({
|
||||
Key key,
|
||||
this.utcOffset = 0,
|
||||
}) : super(key: key);
|
||||
|
||||
final num utcOffset;
|
||||
|
||||
@override
|
||||
_ClockTextState createState() => _ClockTextState();
|
||||
}
|
||||
|
||||
class _ClockTextState extends State<ClockText> {
|
||||
DateTime currentTime = DateTime.now();
|
||||
|
||||
void updateTime() {
|
||||
setState(() {
|
||||
currentTime = DateTime.now();
|
||||
});
|
||||
}
|
||||
|
||||
void stopClock() {
|
||||
setState(() {
|
||||
currentTime = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (currentTime == null) {
|
||||
return const Text('stopped');
|
||||
}
|
||||
return Text(
|
||||
currentTime
|
||||
.toUtc()
|
||||
.add(Duration(hours: widget.utcOffset))
|
||||
.toIso8601String(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// End of block of code where widget creation location line numbers and
|
||||
// columns will impact whether tests pass.
|
||||
|
||||
class _CreationLocation {
|
||||
const _CreationLocation({
|
||||
@required this.file,
|
||||
@required this.line,
|
||||
@required this.column,
|
||||
@required this.id,
|
||||
});
|
||||
|
||||
final String file;
|
||||
final int line;
|
||||
final int column;
|
||||
final int id;
|
||||
}
|
||||
|
||||
typedef InspectorServiceExtensionCallback = FutureOr<Map<String, Object>> Function(Map<String, String> parameters);
|
||||
|
||||
class RenderRepaintBoundaryWithDebugPaint extends RenderRepaintBoundary {
|
||||
@ -95,6 +193,9 @@ void main() {
|
||||
class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
final Map<String, InspectorServiceExtensionCallback> extensions = <String, InspectorServiceExtensionCallback>{};
|
||||
|
||||
final Map<String, List<Map<Object, Object>>> eventsDispatched =
|
||||
<String, List<Map<Object, Object>>>{};
|
||||
|
||||
@override
|
||||
void registerServiceExtension({
|
||||
@required String name,
|
||||
@ -104,6 +205,15 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
extensions[name] = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
void postEvent(String eventKind, Map<Object, Object> eventData) {
|
||||
getEventsDispatched(eventKind).add(eventData);
|
||||
}
|
||||
|
||||
List<Map<Object, Object>> getEventsDispatched(String eventKind) {
|
||||
return eventsDispatched.putIfAbsent(eventKind, () => <Map<Object, Object>>[]);
|
||||
}
|
||||
|
||||
Future<Object> testExtension(String name, Map<String, String> arguments) async {
|
||||
expect(extensions.containsKey(name), isTrue);
|
||||
// Encode and decode to JSON to match behavior using a real service
|
||||
@ -123,6 +233,11 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
@override
|
||||
Future<void> forceRebuild() async {
|
||||
rebuildCount++;
|
||||
final WidgetsBinding binding = WidgetsBinding.instance;
|
||||
|
||||
if (binding.renderViewElement != null) {
|
||||
binding.buildOwner.reassemble(binding.renderViewElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1301,6 +1416,312 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
|
||||
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
|
||||
|
||||
testWidgets('ext.flutter.inspector.trackRebuildDirtyWidgets', (WidgetTester tester) async {
|
||||
service.rebuildCount = 0;
|
||||
|
||||
await tester.pumpWidget(ClockDemo());
|
||||
|
||||
final Element clockDemoElement = find.byType(ClockDemo).evaluate().first;
|
||||
|
||||
service.setSelection(clockDemoElement, 'my-group');
|
||||
final Map<String, Object> jsonObject = await service.testExtension(
|
||||
'getSelectedWidget',
|
||||
<String, String>{'arg': null, 'objectGroup': 'my-group'});
|
||||
final Map<String, Object> creationLocation =
|
||||
jsonObject['creationLocation'];
|
||||
expect(creationLocation, isNotNull);
|
||||
final String file = creationLocation['file'];
|
||||
expect(file, endsWith('widget_inspector_test.dart'));
|
||||
final List<String> segments = Uri.parse(file).pathSegments;
|
||||
// Strip a couple subdirectories away to generate a plausible pub root
|
||||
// directory.
|
||||
final String pubRootTest =
|
||||
'/' + segments.take(segments.length - 2).join('/');
|
||||
await service.testExtension(
|
||||
'setPubRootDirectories', <String, String>{'arg0': pubRootTest});
|
||||
|
||||
final List<Map<Object, Object>> rebuildEvents =
|
||||
service.getEventsDispatched('Flutter.RebuiltWidgets');
|
||||
expect(rebuildEvents, isEmpty);
|
||||
|
||||
expect(service.rebuildCount, equals(0));
|
||||
expect(
|
||||
await service.testBoolExtension(
|
||||
'trackRebuildDirtyWidgets', <String, String>{'enabled': 'true'}),
|
||||
equals('true'));
|
||||
expect(service.rebuildCount, equals(1));
|
||||
await tester.pump();
|
||||
|
||||
expect(rebuildEvents.length, equals(1));
|
||||
Map<Object, Object> event = rebuildEvents.removeLast();
|
||||
expect(event['startTime'], isInstanceOf<int>());
|
||||
List<int> data = event['events'];
|
||||
expect(data.length, equals(14));
|
||||
final int numDataEntries = data.length ~/ 2;
|
||||
Map<String, List<int>> newLocations = event['newLocations'];
|
||||
expect(newLocations, isNotNull);
|
||||
expect(newLocations.length, equals(1));
|
||||
expect(newLocations.keys.first, equals(file));
|
||||
final List<int> locationsForFile = newLocations[file];
|
||||
expect(locationsForFile.length, equals(21));
|
||||
final int numLocationEntries = locationsForFile.length ~/ 3;
|
||||
expect(numLocationEntries, equals(numDataEntries));
|
||||
|
||||
final Map<int, _CreationLocation> knownLocations =
|
||||
<int, _CreationLocation>{};
|
||||
addToKnownLocationsMap(
|
||||
knownLocations: knownLocations,
|
||||
newLocations: newLocations,
|
||||
);
|
||||
int totalCount = 0;
|
||||
int maxCount = 0;
|
||||
for (int i = 0; i < data.length; i += 2) {
|
||||
final int id = data[i];
|
||||
final int count = data[i + 1];
|
||||
totalCount += count;
|
||||
maxCount = max(maxCount, count);
|
||||
expect(knownLocations.containsKey(id), isTrue);
|
||||
}
|
||||
expect(totalCount, equals(27));
|
||||
// The creation locations that were rebuilt the most were rebuilt 6 times
|
||||
// as there are 6 instances of the ClockText widget.
|
||||
expect(maxCount, equals(6));
|
||||
|
||||
final List<Element> clocks = find.byType(ClockText).evaluate().toList();
|
||||
expect(clocks.length, equals(6));
|
||||
// Update a single clock.
|
||||
StatefulElement clockElement = clocks.first;
|
||||
_ClockTextState state = clockElement.state;
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
await tester.pump();
|
||||
expect(rebuildEvents.length, equals(1));
|
||||
event = rebuildEvents.removeLast();
|
||||
expect(event['startTime'], isInstanceOf<int>());
|
||||
data = event['events'];
|
||||
// No new locations were rebuilt.
|
||||
expect(event.containsKey('newLocations'), isFalse);
|
||||
|
||||
// There were two rebuilds: one for the ClockText element itself and one
|
||||
// for its child.
|
||||
expect(data.length, equals(4));
|
||||
int id = data[0];
|
||||
int count = data[1];
|
||||
_CreationLocation location = knownLocations[id];
|
||||
expect(location.file, equals(file));
|
||||
// ClockText widget.
|
||||
expect(location.line, equals(49));
|
||||
expect(location.column, equals(9));
|
||||
expect(count, equals(1));
|
||||
|
||||
id = data[2];
|
||||
count = data[3];
|
||||
location = knownLocations[id];
|
||||
expect(location.file, equals(file));
|
||||
// Text widget in _ClockTextState build method.
|
||||
expect(location.line, equals(87));
|
||||
expect(location.column, equals(12));
|
||||
expect(count, equals(1));
|
||||
|
||||
// Update 3 of the clocks;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
clockElement = clocks[i];
|
||||
state = clockElement.state;
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
}
|
||||
|
||||
await tester.pump();
|
||||
expect(rebuildEvents.length, equals(1));
|
||||
event = rebuildEvents.removeLast();
|
||||
expect(event['startTime'], isInstanceOf<int>());
|
||||
data = event['events'];
|
||||
// No new locations were rebuilt.
|
||||
expect(event.containsKey('newLocations'), isFalse);
|
||||
|
||||
expect(data.length, equals(4));
|
||||
id = data[0];
|
||||
count = data[1];
|
||||
location = knownLocations[id];
|
||||
expect(location.file, equals(file));
|
||||
// ClockText widget.
|
||||
expect(location.line, equals(49));
|
||||
expect(location.column, equals(9));
|
||||
expect(count, equals(3)); // 3 clock widget instances rebuilt.
|
||||
|
||||
id = data[2];
|
||||
count = data[3];
|
||||
location = knownLocations[id];
|
||||
expect(location.file, equals(file));
|
||||
// Text widget in _ClockTextState build method.
|
||||
expect(location.line, equals(87));
|
||||
expect(location.column, equals(12));
|
||||
expect(count, equals(3)); // 3 clock widget instances rebuilt.
|
||||
|
||||
// Update one clock 3 times.
|
||||
clockElement = clocks.first;
|
||||
state = clockElement.state;
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
|
||||
await tester.pump();
|
||||
expect(rebuildEvents.length, equals(1));
|
||||
event = rebuildEvents.removeLast();
|
||||
expect(event['startTime'], isInstanceOf<int>());
|
||||
data = event['events'];
|
||||
// No new locations were rebuilt.
|
||||
expect(event.containsKey('newLocations'), isFalse);
|
||||
|
||||
expect(data.length, equals(4));
|
||||
id = data[0];
|
||||
count = data[1];
|
||||
// Even though a rebuild was triggered 3 times, only one rebuild actually
|
||||
// occurred.
|
||||
expect(count, equals(1));
|
||||
|
||||
// Trigger a widget creation location that wasn't previously triggered.
|
||||
state.stopClock();
|
||||
await tester.pump();
|
||||
expect(rebuildEvents.length, equals(1));
|
||||
event = rebuildEvents.removeLast();
|
||||
expect(event['startTime'], isInstanceOf<int>());
|
||||
data = event['events'];
|
||||
newLocations = event['newLocations'];
|
||||
|
||||
expect(data.length, equals(4));
|
||||
// The second pair in data is the previously unseen rebuild location.
|
||||
id = data[2];
|
||||
count = data[3];
|
||||
expect(count, equals(1));
|
||||
// Verify the rebuild location is new.
|
||||
expect(knownLocations.containsKey(id), isFalse);
|
||||
addToKnownLocationsMap(
|
||||
knownLocations: knownLocations,
|
||||
newLocations: newLocations,
|
||||
);
|
||||
// Verify the rebuild location was included in the newLocations data.
|
||||
expect(knownLocations.containsKey(id), isTrue);
|
||||
|
||||
// Turn off rebuild counts.
|
||||
expect(
|
||||
await service.testBoolExtension(
|
||||
'trackRebuildDirtyWidgets', <String, String>{'enabled': 'false'}),
|
||||
equals('false'));
|
||||
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
await tester.pump();
|
||||
// Verify that rebuild events are not fired once the extension is disabled.
|
||||
expect(rebuildEvents, isEmpty);
|
||||
},
|
||||
skip: !WidgetInspectorService.instance
|
||||
.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
|
||||
|
||||
testWidgets('ext.flutter.inspector.trackRepaintWidgets', (WidgetTester tester) async {
|
||||
service.rebuildCount = 0;
|
||||
|
||||
await tester.pumpWidget(ClockDemo());
|
||||
|
||||
final Element clockDemoElement = find.byType(ClockDemo).evaluate().first;
|
||||
|
||||
service.setSelection(clockDemoElement, 'my-group');
|
||||
final Map<String, Object> jsonObject = await service.testExtension(
|
||||
'getSelectedWidget',
|
||||
<String, String>{'arg': null, 'objectGroup': 'my-group'});
|
||||
final Map<String, Object> creationLocation =
|
||||
jsonObject['creationLocation'];
|
||||
expect(creationLocation, isNotNull);
|
||||
final String file = creationLocation['file'];
|
||||
expect(file, endsWith('widget_inspector_test.dart'));
|
||||
final List<String> segments = Uri.parse(file).pathSegments;
|
||||
// Strip a couple subdirectories away to generate a plausible pub root
|
||||
// directory.
|
||||
final String pubRootTest =
|
||||
'/' + segments.take(segments.length - 2).join('/');
|
||||
await service.testExtension(
|
||||
'setPubRootDirectories', <String, String>{'arg0': pubRootTest});
|
||||
|
||||
final List<Map<Object, Object>> repaintEvents =
|
||||
service.getEventsDispatched('Flutter.RepaintWidgets');
|
||||
expect(repaintEvents, isEmpty);
|
||||
|
||||
expect(service.rebuildCount, equals(0));
|
||||
expect(
|
||||
await service.testBoolExtension(
|
||||
'trackRepaintWidgets', <String, String>{'enabled': 'true'}),
|
||||
equals('true'));
|
||||
// Unlike trackRebuildDirtyWidgets, trackRepaintWidgets doesn't force a full
|
||||
// rebuild.
|
||||
expect(service.rebuildCount, equals(0));
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(repaintEvents.length, equals(1));
|
||||
Map<Object, Object> event = repaintEvents.removeLast();
|
||||
expect(event['startTime'], isInstanceOf<int>());
|
||||
List<int> data = event['events'];
|
||||
expect(data.length, equals(18));
|
||||
final int numDataEntries = data.length ~/ 2;
|
||||
final Map<String, List<int>> newLocations = event['newLocations'];
|
||||
expect(newLocations, isNotNull);
|
||||
expect(newLocations.length, equals(1));
|
||||
expect(newLocations.keys.first, equals(file));
|
||||
final List<int> locationsForFile = newLocations[file];
|
||||
expect(locationsForFile.length, equals(27));
|
||||
final int numLocationEntries = locationsForFile.length ~/ 3;
|
||||
expect(numLocationEntries, equals(numDataEntries));
|
||||
|
||||
final Map<int, _CreationLocation> knownLocations =
|
||||
<int, _CreationLocation>{};
|
||||
addToKnownLocationsMap(
|
||||
knownLocations: knownLocations,
|
||||
newLocations: newLocations,
|
||||
);
|
||||
int totalCount = 0;
|
||||
int maxCount = 0;
|
||||
for (int i = 0; i < data.length; i += 2) {
|
||||
final int id = data[i];
|
||||
final int count = data[i + 1];
|
||||
totalCount += count;
|
||||
maxCount = max(maxCount, count);
|
||||
expect(knownLocations.containsKey(id), isTrue);
|
||||
}
|
||||
expect(totalCount, equals(34));
|
||||
// The creation locations that were rebuilt the most were rebuilt 6 times
|
||||
// as there are 6 instances of the ClockText widget.
|
||||
expect(maxCount, equals(6));
|
||||
|
||||
final List<Element> clocks = find.byType(ClockText).evaluate().toList();
|
||||
expect(clocks.length, equals(6));
|
||||
// Update a single clock.
|
||||
final StatefulElement clockElement = clocks.first;
|
||||
final _ClockTextState state = clockElement.state;
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
await tester.pump();
|
||||
expect(repaintEvents.length, equals(1));
|
||||
event = repaintEvents.removeLast();
|
||||
expect(event['startTime'], isInstanceOf<int>());
|
||||
data = event['events'];
|
||||
// No new locations were rebuilt.
|
||||
expect(event.containsKey('newLocations'), isFalse);
|
||||
|
||||
// Triggering a a rebuild of one widget in this app causes the whole app
|
||||
// to repaint.
|
||||
expect(data.length, equals(18));
|
||||
|
||||
// TODO(jacobr): add an additional repaint test that uses multiple repaint
|
||||
// boundaries to test more complex repaint conditions.
|
||||
|
||||
// Turn off rebuild counts.
|
||||
expect(
|
||||
await service.testBoolExtension(
|
||||
'trackRepaintWidgets', <String, String>{'enabled': 'false'}),
|
||||
equals('false'));
|
||||
|
||||
state.updateTime(); // Triggers a rebuild.
|
||||
await tester.pump();
|
||||
// Verify that rapint events are not fired once the extension is disabled.
|
||||
expect(repaintEvents, isEmpty);
|
||||
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
|
||||
|
||||
testWidgets('ext.flutter.inspector.show', (WidgetTester tester) async {
|
||||
service.rebuildCount = 0;
|
||||
expect(await service.testBoolExtension('show', <String, String>{'enabled': 'true'}), equals('true'));
|
||||
@ -1824,3 +2245,20 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void addToKnownLocationsMap({
|
||||
@required Map<int, _CreationLocation> knownLocations,
|
||||
@required Map<String, List<int>> newLocations,
|
||||
}) {
|
||||
newLocations.forEach((String file, List<int> entries) {
|
||||
assert(entries.length % 3 == 0);
|
||||
for (int i = 0; i < entries.length; i += 3) {
|
||||
final int id = entries[i];
|
||||
final int line = entries[i + 1];
|
||||
final int column = entries[i + 2];
|
||||
assert(!knownLocations.containsKey(id));
|
||||
knownLocations[id] =
|
||||
_CreationLocation(file: file, line: line, column: column, id: id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user