Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
10bcddcc
Unverified
Commit
10bcddcc
authored
Oct 29, 2018
by
Jacob Richman
Committed by
GitHub
Oct 29, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add option to track widget rebuilds and repaints from the Flutter inspector. (#23534)
parent
b722a744
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
853 additions
and
15 deletions
+853
-15
debug.dart
packages/flutter/lib/src/rendering/debug.dart
+23
-1
object.dart
packages/flutter/lib/src/rendering/object.dart
+2
-0
binding.dart
packages/flutter/lib/src/widgets/binding.dart
+5
-0
debug.dart
packages/flutter/lib/src/widgets/debug.dart
+20
-0
framework.dart
packages/flutter/lib/src/widgets/framework.dart
+3
-0
widget_inspector.dart
packages/flutter/lib/src/widgets/widget_inspector.dart
+354
-12
service_extensions_test.dart
...ages/flutter/test/foundation/service_extensions_test.dart
+8
-2
widget_inspector_test.dart
packages/flutter/test/widgets/widget_inspector_test.dart
+438
-0
No files found.
packages/flutter/lib/src/rendering/debug.dart
View file @
10bcddcc
...
...
@@ -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
;
...
...
packages/flutter/lib/src/rendering/object.dart
View file @
10bcddcc
...
...
@@ -161,6 +161,8 @@ class PaintingContext extends ClipContext {
assert
(()
{
if
(
debugProfilePaintsEnabled
)
Timeline
.
startSync
(
'
${child.runtimeType}
'
,
arguments:
timelineWhitelistArguments
);
if
(
debugOnProfilePaint
!=
null
)
debugOnProfilePaint
(
child
);
return
true
;
}());
...
...
packages/flutter/lib/src/widgets/binding.dart
View file @
10bcddcc
...
...
@@ -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
);
...
...
packages/flutter/lib/src/widgets/debug.dart
View file @
10bcddcc
...
...
@@ -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
...
...
packages/flutter/lib/src/widgets/framework.dart
View file @
10bcddcc
...
...
@@ -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
'
);
...
...
packages/flutter/lib/src/widgets/widget_inspector.dart
View file @
10bcddcc
...
...
@@ -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
(
Widgets
Binding
.
instance
.
schedulerPhase
==
SchedulerPhase
.
idle
)
{
if
(
Scheduler
Binding
.
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
;
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
;
}
packages/flutter/test/foundation/service_extensions_test.dart
View file @
10bcddcc
...
...
@@ -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
;
...
...
packages/flutter/test/widgets/widget_inspector_test.dart
View file @
10bcddcc
...
...
@@ -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
);
}
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment