From a7e752d570ce7f3575162846230ef73226e8e906 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Wed, 22 Jun 2022 23:25:46 +1000 Subject: [PATCH 1/8] fix: Cache generated spans across rebuilds --- flutter_highlight/lib/flutter_highlight.dart | 63 ++++++++++++++------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 9afbc47..775d1f8 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart'; import 'package:highlight/highlight.dart' show highlight, Node; /// Highlight Flutter Widget -class HighlightView extends StatelessWidget { +class HighlightView extends StatefulWidget { /// The original code to be highlighted final String source; @@ -36,6 +36,22 @@ class HighlightView extends StatelessWidget { int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 }) : source = input.replaceAll('\t', ' ' * tabSize); + static const _rootKey = 'root'; + static const _defaultFontColor = Color(0xff000000); + static const _defaultBackgroundColor = Color(0xffffffff); + + // TODO: dart:io is not available at web platform currently + // See: https://github.com/flutter/flutter/issues/39998 + // So we just use monospace here for now + static const _defaultFontFamily = 'monospace'; + + @override + State createState() => _HighlightViewState(); +} + +class _HighlightViewState extends State { + late List _spans; + List _convert(List nodes) { List spans = []; var currentSpans = spans; @@ -45,10 +61,11 @@ class HighlightView extends StatelessWidget { if (node.value != null) { currentSpans.add(node.className == null ? TextSpan(text: node.value) - : TextSpan(text: node.value, style: theme[node.className!])); + : TextSpan(text: node.value, style: widget.theme[node.className!])); } else if (node.children != null) { List tmp = []; - currentSpans.add(TextSpan(children: tmp, style: theme[node.className!])); + currentSpans + .add(TextSpan(children: tmp, style: widget.theme[node.className!])); stack.add(currentSpans); currentSpans = tmp; @@ -68,32 +85,44 @@ class HighlightView extends StatelessWidget { return spans; } - static const _rootKey = 'root'; - static const _defaultFontColor = Color(0xff000000); - static const _defaultBackgroundColor = Color(0xffffffff); + void _generateSpans() => _spans = _convert( + highlight.parse(widget.source, language: widget.language).nodes!); - // TODO: dart:io is not available at web platform currently - // See: https://github.com/flutter/flutter/issues/39998 - // So we just use monospace here for now - static const _defaultFontFamily = 'monospace'; + @override + void initState() { + super.initState(); + _generateSpans(); + } + + @override + void didUpdateWidget(HighlightView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.source != oldWidget.source || + widget.language != oldWidget.language || + widget.theme != oldWidget.theme) { + _generateSpans(); + } + } @override Widget build(BuildContext context) { var _textStyle = TextStyle( - fontFamily: _defaultFontFamily, - color: theme[_rootKey]?.color ?? _defaultFontColor, + fontFamily: HighlightView._defaultFontFamily, + color: widget.theme[HighlightView._rootKey]?.color ?? + HighlightView._defaultFontColor, ); - if (textStyle != null) { - _textStyle = _textStyle.merge(textStyle); + if (widget.textStyle != null) { + _textStyle = _textStyle.merge(widget.textStyle); } return Container( - color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor, - padding: padding, + color: widget.theme[HighlightView._rootKey]?.backgroundColor ?? + HighlightView._defaultBackgroundColor, + padding: widget.padding, child: RichText( text: TextSpan( style: _textStyle, - children: _convert(highlight.parse(source, language: language).nodes!), + children: _spans, ), ), ); From 9ba389f462eab2f5ac8b8e3af59bb5f29a296ab3 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Wed, 22 Jun 2022 23:32:17 +1000 Subject: [PATCH 2/8] fix: Don't regenerate nodes on a theme change --- flutter_highlight/lib/flutter_highlight.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 775d1f8..fd843ed 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -50,6 +50,7 @@ class HighlightView extends StatefulWidget { } class _HighlightViewState extends State { + late List _nodes; late List _spans; List _convert(List nodes) { @@ -85,12 +86,15 @@ class _HighlightViewState extends State { return spans; } - void _generateSpans() => _spans = _convert( - highlight.parse(widget.source, language: widget.language).nodes!); + void _parse() => + _nodes = highlight.parse(widget.source, language: widget.language).nodes!; + + void _generateSpans() => _spans = _convert(_nodes); @override void initState() { super.initState(); + _parse(); _generateSpans(); } @@ -98,8 +102,10 @@ class _HighlightViewState extends State { void didUpdateWidget(HighlightView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.source != oldWidget.source || - widget.language != oldWidget.language || - widget.theme != oldWidget.theme) { + widget.language != oldWidget.language) { + _parse(); + _generateSpans(); + } else if (widget.theme != oldWidget.theme) { _generateSpans(); } } From b2714cb584ff93ae00f17059c46cbec4d7c17102 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Thu, 23 Jun 2022 01:39:07 +1000 Subject: [PATCH 3/8] feat: Add Flutter background processing functionality --- flutter_highlight/README.md | 8 + flutter_highlight/example/lib/main.dart | 20 ++- flutter_highlight/lib/flutter_highlight.dart | 59 +++++-- .../lib/flutter_highlight_background.dart | 152 ++++++++++++++++++ 4 files changed, 216 insertions(+), 23 deletions(-) create mode 100644 flutter_highlight/lib/flutter_highlight_background.dart diff --git a/flutter_highlight/README.md b/flutter_highlight/README.md index 4c3f34f..0af7a85 100644 --- a/flutter_highlight/README.md +++ b/flutter_highlight/README.md @@ -44,6 +44,14 @@ class MyWidget extends StatelessWidget { } ``` +### Background processing + +Processing large amounts of text can be slow. To perform text processing in the background, add a +`HighlightBackgroundEnvironment` above any `HighlightView`s in your widget tree. + +A background isolate will be automatically started in `HighlightBackgroundEnvironment.initState`, and stopped in +`HighlightBackgroundEnvironment.dispose`. + ## References - [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages) diff --git a/flutter_highlight/example/lib/main.dart b/flutter_highlight/example/lib/main.dart index 74ada48..86edb9e 100644 --- a/flutter_highlight/example/lib/main.dart +++ b/flutter_highlight/example/lib/main.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_highlight/flutter_highlight.dart'; +import 'package:flutter_highlight/flutter_highlight_background.dart'; import 'package:flutter_highlight/theme_map.dart'; import 'package:url_launcher/url_launcher.dart'; + import 'example_map.dart'; void main() => runApp(MyApp()); @@ -95,14 +97,16 @@ class _MyHomePageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - HighlightView( - exampleMap[language], - language: language, - theme: themeMap[theme], - padding: EdgeInsets.all(12), - textStyle: TextStyle( - fontFamily: - 'SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace'), + HighlightBackgroundEnvironment( + child: HighlightView( + exampleMap[language], + language: language, + theme: themeMap[theme], + padding: EdgeInsets.all(12), + textStyle: TextStyle( + fontFamily: + 'SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace'), + ), ) ], ), diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index fd843ed..2dd0ee2 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_highlight/flutter_highlight_background.dart'; import 'package:highlight/highlight.dart' show highlight, Node; /// Highlight Flutter Widget @@ -27,6 +27,12 @@ class HighlightView extends StatefulWidget { /// Specify text styles such as font family and font size final TextStyle? textStyle; + /// Progress indicator + /// + /// A widget that is displayed while the [source] is being processed. + /// This may only be used if a [HighlightBackgroundEnvironment] is available. + Widget? progressIndicator; + HighlightView( String input, { this.language, @@ -34,6 +40,7 @@ class HighlightView extends StatefulWidget { this.padding, this.textStyle, int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 + this.progressIndicator, }) : source = input.replaceAll('\t', ' ' * tabSize); static const _rootKey = 'root'; @@ -50,8 +57,8 @@ class HighlightView extends StatefulWidget { } class _HighlightViewState extends State { - late List _nodes; - late List _spans; + late Future> _nodesFuture; + late Future> _spansFuture; List _convert(List nodes) { List spans = []; @@ -86,15 +93,19 @@ class _HighlightViewState extends State { return spans; } - void _parse() => - _nodes = highlight.parse(widget.source, language: widget.language).nodes!; + void _parse(HighlightBackgroundProvider? backgroundProvider) => _nodesFuture = + backgroundProvider?.parse(widget.source, language: widget.language) ?? + Future.value( + highlight.parse(widget.source, language: widget.language).nodes, + ); - void _generateSpans() => _spans = _convert(_nodes); + void _generateSpans() => _spansFuture = _nodesFuture.then(_convert); @override - void initState() { - super.initState(); - _parse(); + void didChangeDependencies() { + super.didChangeDependencies(); + final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); + _parse(backgroundProvider); _generateSpans(); } @@ -103,7 +114,8 @@ class _HighlightViewState extends State { super.didUpdateWidget(oldWidget); if (widget.source != oldWidget.source || widget.language != oldWidget.language) { - _parse(); + final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); + _parse(backgroundProvider); _generateSpans(); } else if (widget.theme != oldWidget.theme) { _generateSpans(); @@ -125,11 +137,28 @@ class _HighlightViewState extends State { color: widget.theme[HighlightView._rootKey]?.backgroundColor ?? HighlightView._defaultBackgroundColor, padding: widget.padding, - child: RichText( - text: TextSpan( - style: _textStyle, - children: _spans, - ), + child: FutureBuilder>( + future: _spansFuture, + builder: (context, snapshot) { + if (!snapshot.hasData) { + final progressIndicator = widget.progressIndicator; + if (progressIndicator == null) { + return const SizedBox.shrink(); + } else { + assert( + HighlightBackgroundProvider.maybeOf(context) != null, + 'Cannot display a progress indicator unless a HighlightBackgroundEnvironment is available!', + ); + return progressIndicator; + } + } + return RichText( + text: TextSpan( + style: _textStyle, + children: snapshot.requireData, + ), + ); + }, ), ); } diff --git a/flutter_highlight/lib/flutter_highlight_background.dart b/flutter_highlight/lib/flutter_highlight_background.dart new file mode 100644 index 0000000..7fbdb56 --- /dev/null +++ b/flutter_highlight/lib/flutter_highlight_background.dart @@ -0,0 +1,152 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'package:flutter/widgets.dart'; +import 'package:highlight/highlight.dart' show highlight, Node; + +/// A widget that provides a background [Isolate] to do expensive highlighting +/// work in. +/// +/// The [HighlightView] will detect and use the background environment +/// automatically. +/// It can also be used manually through the [HighlightBackgroundProvider] +/// [InheritedWidget]. +class HighlightBackgroundEnvironment extends StatefulWidget { + final Widget child; + + const HighlightBackgroundEnvironment({ + Key? key, + required this.child, + }) : super(key: key); + + @override + State createState() => + _HighlightBackgroundEnvironmentState(); +} + +class _HighlightBackgroundEnvironmentState + extends State { + late final Completer _sendPortCompleter; + late final StreamController<_ParseResponse> _parseResultStreamController; + + @override + void initState() { + super.initState(); + _sendPortCompleter = Completer(); + _parseResultStreamController = StreamController.broadcast(); + final receivePort = ReceivePort(); + receivePort.listen((message) { + if (message is _ParseResponse) { + _parseResultStreamController.add(message); + } else if (message is _IsolateStartedResponse) { + _sendPortCompleter.complete(message.sendPort); + } else if (message is _IsolateEndedResponse) { + print('Ended'); + receivePort.close(); + } + }); + Isolate.spawn(_isolateEntrypoint, receivePort.sendPort); + } + + @override + void dispose() { + super.dispose(); + _sendPortCompleter.future + .then((sendPort) => sendPort.send(_IsolateEndRequest())); + } + + Future> parse(String source, {String? language}) { + final identifier = Capability(); + _sendPortCompleter.future.then((sendPort) => + sendPort.send(_ParseRequest(identifier, source, language: language))); + return _parseResultStreamController.stream + .firstWhere((message) => identical(message.identifier, identifier)) + .then((message) => message.nodes); + } + + @override + Widget build(BuildContext context) { + return HighlightBackgroundProvider._( + environmentIdentifier: this, + parse: parse, + child: widget.child, + ); + } +} + +class HighlightBackgroundProvider extends InheritedWidget { + final Object environmentIdentifier; + final Future> Function(String source, {String? language}) parse; + + HighlightBackgroundProvider._({ + Key? key, + required this.environmentIdentifier, + required this.parse, + required Widget child, + }) : super( + key: key, + child: child, + ); + + @override + bool updateShouldNotify(HighlightBackgroundProvider oldWidget) => + !identical(environmentIdentifier, oldWidget.environmentIdentifier); + + static HighlightBackgroundProvider of(BuildContext context) { + final backgroundProvider = maybeOf(context); + assert(backgroundProvider != null, + 'No HighlightBackgroundProvider found in context'); + return backgroundProvider!; + } + + static HighlightBackgroundProvider? maybeOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType(); +} + +void _isolateEntrypoint(SendPort sendPort) { + final receivePort = ReceivePort(); + receivePort.listen((message) { + if (message is _ParseRequest) { + final nodes = + highlight.parse(message.source, language: message.language).nodes!; + sendPort.send(_ParseResponse(message.identifier, nodes)); + } else if (message is _IsolateEndRequest) { + receivePort.close(); + sendPort.send(const _IsolateEndedResponse()); + } + }); + sendPort.send(_IsolateStartedResponse(receivePort.sendPort)); +} + +abstract class _IsolateRequest {} + +class _IsolateEndRequest implements _IsolateRequest { + const _IsolateEndRequest(); +} + +class _ParseRequest implements _IsolateRequest { + final Capability identifier; + final String source; + final String? language; + + const _ParseRequest(this.identifier, this.source, {this.language}); +} + +abstract class _IsolateResponse {} + +class _IsolateStartedResponse implements _IsolateResponse { + final SendPort sendPort; + + const _IsolateStartedResponse(this.sendPort); +} + +class _IsolateEndedResponse implements _IsolateResponse { + const _IsolateEndedResponse(); +} + +class _ParseResponse implements _IsolateResponse { + final Capability identifier; + final List nodes; + + const _ParseResponse(this.identifier, this.nodes); +} From c404d61ffab35e158636cfb1eea08ba2c498bfd3 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Thu, 23 Jun 2022 01:51:59 +1000 Subject: [PATCH 4/8] fix: Make HighlightView.progressIndicator final --- flutter_highlight/lib/flutter_highlight.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 2dd0ee2..3eccd03 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -31,7 +31,7 @@ class HighlightView extends StatefulWidget { /// /// A widget that is displayed while the [source] is being processed. /// This may only be used if a [HighlightBackgroundEnvironment] is available. - Widget? progressIndicator; + final Widget? progressIndicator; HighlightView( String input, { From 85236d18515127279eefcfb8509d449598001c02 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Thu, 23 Jun 2022 14:38:19 +1000 Subject: [PATCH 5/8] fix: Remove print statement upon isolate termination --- flutter_highlight/lib/flutter_highlight_background.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter_highlight/lib/flutter_highlight_background.dart b/flutter_highlight/lib/flutter_highlight_background.dart index 7fbdb56..535937c 100644 --- a/flutter_highlight/lib/flutter_highlight_background.dart +++ b/flutter_highlight/lib/flutter_highlight_background.dart @@ -41,7 +41,6 @@ class _HighlightBackgroundEnvironmentState } else if (message is _IsolateStartedResponse) { _sendPortCompleter.complete(message.sendPort); } else if (message is _IsolateEndedResponse) { - print('Ended'); receivePort.close(); } }); From c93fa11bee43958a3b79703acc950632af07afc1 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Thu, 23 Jun 2022 14:01:08 +1000 Subject: [PATCH 6/8] refactor: Move HighlightView convert function into static render function --- flutter_highlight/lib/flutter_highlight.dart | 30 ++++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 3eccd03..193c127 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -54,13 +54,13 @@ class HighlightView extends StatefulWidget { @override State createState() => _HighlightViewState(); -} - -class _HighlightViewState extends State { - late Future> _nodesFuture; - late Future> _spansFuture; - List _convert(List nodes) { + /// Renders a list of [nodes] into a list of [TextSpan]s using the given + /// [theme]. + static List render( + List nodes, + Map theme, + ) { List spans = []; var currentSpans = spans; List> stack = []; @@ -69,11 +69,11 @@ class _HighlightViewState extends State { if (node.value != null) { currentSpans.add(node.className == null ? TextSpan(text: node.value) - : TextSpan(text: node.value, style: widget.theme[node.className!])); + : TextSpan(text: node.value, style: theme[node.className!])); } else if (node.children != null) { List tmp = []; currentSpans - .add(TextSpan(children: tmp, style: widget.theme[node.className!])); + .add(TextSpan(children: tmp, style: theme[node.className!])); stack.add(currentSpans); currentSpans = tmp; @@ -92,6 +92,11 @@ class _HighlightViewState extends State { return spans; } +} + +class _HighlightViewState extends State { + late Future> _nodesFuture; + late Future> _spansFuture; void _parse(HighlightBackgroundProvider? backgroundProvider) => _nodesFuture = backgroundProvider?.parse(widget.source, language: widget.language) ?? @@ -99,14 +104,15 @@ class _HighlightViewState extends State { highlight.parse(widget.source, language: widget.language).nodes, ); - void _generateSpans() => _spansFuture = _nodesFuture.then(_convert); + void _render() => _spansFuture = + _nodesFuture.then((nodes) => HighlightView.render(nodes, widget.theme)); @override void didChangeDependencies() { super.didChangeDependencies(); final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); _parse(backgroundProvider); - _generateSpans(); + _render(); } @override @@ -116,9 +122,9 @@ class _HighlightViewState extends State { widget.language != oldWidget.language) { final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); _parse(backgroundProvider); - _generateSpans(); + _render(); } else if (widget.theme != oldWidget.theme) { - _generateSpans(); + _render(); } } From 99b9c94b9c4b2fb22c9e8aca9947aba198db3333 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Thu, 23 Jun 2022 14:40:49 +1000 Subject: [PATCH 7/8] feat: Add Flutter background rendering functionality --- flutter_highlight/lib/flutter_highlight.dart | 16 ++- .../lib/flutter_highlight_background.dart | 106 +++++++++++++----- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 193c127..f187b93 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/widgets.dart'; import 'package:flutter_highlight/flutter_highlight_background.dart'; import 'package:highlight/highlight.dart' show highlight, Node; @@ -104,15 +106,18 @@ class _HighlightViewState extends State { highlight.parse(widget.source, language: widget.language).nodes, ); - void _render() => _spansFuture = - _nodesFuture.then((nodes) => HighlightView.render(nodes, widget.theme)); + void _render(HighlightBackgroundProvider? backgroundProvider) => + _spansFuture = _nodesFuture.then((nodes) => + (backgroundProvider?.render(nodes, widget.theme) ?? + HighlightView.render(nodes, widget.theme)) + as FutureOr>); @override void didChangeDependencies() { super.didChangeDependencies(); final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); _parse(backgroundProvider); - _render(); + _render(backgroundProvider); } @override @@ -122,9 +127,10 @@ class _HighlightViewState extends State { widget.language != oldWidget.language) { final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); _parse(backgroundProvider); - _render(); + _render(backgroundProvider); } else if (widget.theme != oldWidget.theme) { - _render(); + final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); + _render(backgroundProvider); } } diff --git a/flutter_highlight/lib/flutter_highlight_background.dart b/flutter_highlight/lib/flutter_highlight_background.dart index 535937c..10e7d46 100644 --- a/flutter_highlight/lib/flutter_highlight_background.dart +++ b/flutter_highlight/lib/flutter_highlight_background.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:isolate'; import 'package:flutter/widgets.dart'; +import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:highlight/highlight.dart' show highlight, Node; /// A widget that provides a background [Isolate] to do expensive highlighting @@ -27,20 +28,21 @@ class HighlightBackgroundEnvironment extends StatefulWidget { class _HighlightBackgroundEnvironmentState extends State { late final Completer _sendPortCompleter; - late final StreamController<_ParseResponse> _parseResultStreamController; + late final StreamController<_IdentifiableIsolateMessage> + _resultStreamController; @override void initState() { super.initState(); _sendPortCompleter = Completer(); - _parseResultStreamController = StreamController.broadcast(); + _resultStreamController = StreamController.broadcast(); final receivePort = ReceivePort(); - receivePort.listen((message) { - if (message is _ParseResponse) { - _parseResultStreamController.add(message); - } else if (message is _IsolateStartedResponse) { - _sendPortCompleter.complete(message.sendPort); - } else if (message is _IsolateEndedResponse) { + receivePort.listen((response) { + if (response is _IdentifiableIsolateResponse) { + _resultStreamController.add(response); + } else if (response is _IsolateStartedResponse) { + _sendPortCompleter.complete(response.sendPort); + } else if (response is _IsolateEndedResponse) { receivePort.close(); } }); @@ -54,20 +56,32 @@ class _HighlightBackgroundEnvironmentState .then((sendPort) => sendPort.send(_IsolateEndRequest())); } - Future> parse(String source, {String? language}) { - final identifier = Capability(); - _sendPortCompleter.future.then((sendPort) => - sendPort.send(_ParseRequest(identifier, source, language: language))); - return _parseResultStreamController.stream - .firstWhere((message) => identical(message.identifier, identifier)) - .then((message) => message.nodes); + Future _performTask( + _IdentifiableIsolateRequest request) { + _sendPortCompleter.future.then((sendPort) => sendPort.send(request)); + return _resultStreamController.stream + .firstWhere( + (message) => identical(message.identifier, request.identifier)) + .then((response) => response as T); } + Future> _parse(String source, {String? language}) => + _performTask(_ParseRequest(source, language: language)) + .then((response) => response.nodes); + + Future> _render( + List nodes, + Map theme, + ) => + _performTask(_RenderRequest(nodes, theme)) + .then((response) => response.spans); + @override Widget build(BuildContext context) { return HighlightBackgroundProvider._( environmentIdentifier: this, - parse: parse, + parse: _parse, + render: _render, child: widget.child, ); } @@ -76,11 +90,16 @@ class _HighlightBackgroundEnvironmentState class HighlightBackgroundProvider extends InheritedWidget { final Object environmentIdentifier; final Future> Function(String source, {String? language}) parse; + final Future> Function( + List nodes, + Map theme, + ) render; HighlightBackgroundProvider._({ Key? key, required this.environmentIdentifier, required this.parse, + required this.render, required Widget child, }) : super( key: key, @@ -104,12 +123,15 @@ class HighlightBackgroundProvider extends InheritedWidget { void _isolateEntrypoint(SendPort sendPort) { final receivePort = ReceivePort(); - receivePort.listen((message) { - if (message is _ParseRequest) { + receivePort.listen((request) { + if (request is _ParseRequest) { final nodes = - highlight.parse(message.source, language: message.language).nodes!; - sendPort.send(_ParseResponse(message.identifier, nodes)); - } else if (message is _IsolateEndRequest) { + highlight.parse(request.source, language: request.language).nodes!; + sendPort.send(_ParseResponse(request, nodes)); + } else if (request is _RenderRequest) { + final spans = HighlightView.render(request.nodes, request.theme); + sendPort.send(_RenderResponse(request, spans)); + } else if (request is _IsolateEndRequest) { receivePort.close(); sendPort.send(const _IsolateEndedResponse()); } @@ -117,22 +139,43 @@ void _isolateEntrypoint(SendPort sendPort) { sendPort.send(_IsolateStartedResponse(receivePort.sendPort)); } +abstract class _IdentifiableIsolateMessage { + Capability get identifier; +} + abstract class _IsolateRequest {} +abstract class _IdentifiableIsolateRequest< + T extends _IdentifiableIsolateResponse> + implements _IsolateRequest, _IdentifiableIsolateMessage {} + class _IsolateEndRequest implements _IsolateRequest { const _IsolateEndRequest(); } -class _ParseRequest implements _IsolateRequest { - final Capability identifier; +class _ParseRequest implements _IdentifiableIsolateRequest<_ParseResponse> { + @override + final Capability identifier = Capability(); final String source; final String? language; - const _ParseRequest(this.identifier, this.source, {this.language}); + _ParseRequest(this.source, {this.language}); +} + +class _RenderRequest implements _IdentifiableIsolateRequest<_RenderResponse> { + @override + final Capability identifier = Capability(); + final List nodes; + final Map theme; + + _RenderRequest(this.nodes, this.theme); } abstract class _IsolateResponse {} +abstract class _IdentifiableIsolateResponse + implements _IsolateResponse, _IdentifiableIsolateMessage {} + class _IsolateStartedResponse implements _IsolateResponse { final SendPort sendPort; @@ -143,9 +186,20 @@ class _IsolateEndedResponse implements _IsolateResponse { const _IsolateEndedResponse(); } -class _ParseResponse implements _IsolateResponse { +class _ParseResponse implements _IdentifiableIsolateResponse { + @override final Capability identifier; final List nodes; - const _ParseResponse(this.identifier, this.nodes); + _ParseResponse(_ParseRequest request, this.nodes) + : identifier = request.identifier; +} + +class _RenderResponse implements _IdentifiableIsolateResponse { + @override + final Capability identifier; + final List spans; + + _RenderResponse(_RenderRequest request, this.spans) + : identifier = request.identifier; } From ed43677e801891564385e3a1e561df49aa33fa24 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Thu, 23 Jun 2022 15:35:07 +1000 Subject: [PATCH 8/8] perf: Minimise message sending between the Flutter parse and render steps --- flutter_highlight/lib/flutter_highlight.dart | 21 ++- .../lib/flutter_highlight_background.dart | 125 ++++++++++++++++-- 2 files changed, 131 insertions(+), 15 deletions(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index f187b93..7c2327d 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -112,12 +112,26 @@ class _HighlightViewState extends State { HighlightView.render(nodes, widget.theme)) as FutureOr>); + void _parseAndRender(HighlightBackgroundProvider? backgroundProvider) { + if (backgroundProvider == null) { + _parse(null); + _render(null); + } else { + final resultFuture = backgroundProvider.parseAndRender( + widget.source, + widget.theme, + language: widget.language, + ); + _nodesFuture = resultFuture.then((result) => result.nodes); + _spansFuture = resultFuture.then((result) => result.spans); + } + } + @override void didChangeDependencies() { super.didChangeDependencies(); final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); - _parse(backgroundProvider); - _render(backgroundProvider); + _parseAndRender(backgroundProvider); } @override @@ -126,8 +140,7 @@ class _HighlightViewState extends State { if (widget.source != oldWidget.source || widget.language != oldWidget.language) { final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); - _parse(backgroundProvider); - _render(backgroundProvider); + _parseAndRender(backgroundProvider); } else if (widget.theme != oldWidget.theme) { final backgroundProvider = HighlightBackgroundProvider.maybeOf(context); _render(backgroundProvider); diff --git a/flutter_highlight/lib/flutter_highlight_background.dart b/flutter_highlight/lib/flutter_highlight_background.dart index 10e7d46..b79572a 100644 --- a/flutter_highlight/lib/flutter_highlight_background.dart +++ b/flutter_highlight/lib/flutter_highlight_background.dart @@ -5,6 +5,15 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:highlight/highlight.dart' show highlight, Node; +/// A result of a combined parse and render operation done in a +/// [HighlightBackgroundEnvironment]. +class HighlightBackgroundResult { + final List nodes; + final List spans; + + const HighlightBackgroundResult(this.nodes, this.spans); +} + /// A widget that provides a background [Isolate] to do expensive highlighting /// work in. /// @@ -76,12 +85,22 @@ class _HighlightBackgroundEnvironmentState _performTask(_RenderRequest(nodes, theme)) .then((response) => response.spans); + Future _parseAndRender( + String source, + Map theme, { + String? language, + }) => + _performTask(_ParseAndRenderRequest(source, theme, language: language)) + .then((response) => + HighlightBackgroundResult(response.nodes, response.spans)); + @override Widget build(BuildContext context) { return HighlightBackgroundProvider._( environmentIdentifier: this, parse: _parse, render: _render, + parseAndRender: _parseAndRender, child: widget.child, ); } @@ -94,12 +113,18 @@ class HighlightBackgroundProvider extends InheritedWidget { List nodes, Map theme, ) render; + final Future Function( + String source, + Map theme, { + String? language, + }) parseAndRender; HighlightBackgroundProvider._({ Key? key, required this.environmentIdentifier, required this.parse, required this.render, + required this.parseAndRender, required Widget child, }) : super( key: key, @@ -124,13 +149,24 @@ class HighlightBackgroundProvider extends InheritedWidget { void _isolateEntrypoint(SendPort sendPort) { final receivePort = ReceivePort(); receivePort.listen((request) { - if (request is _ParseRequest) { - final nodes = - highlight.parse(request.source, language: request.language).nodes!; - sendPort.send(_ParseResponse(request, nodes)); - } else if (request is _RenderRequest) { - final spans = HighlightView.render(request.nodes, request.theme); - sendPort.send(_RenderResponse(request, spans)); + if (request is _ParsingRequest || request is _RenderingRequest) { + late final List nodes; + if (request is _ParsingRequest) { + nodes = + highlight.parse(request.source, language: request.language).nodes!; + } else if (request is _RenderRequest) { + nodes = request.nodes; + } + if (request is _ParseRequest) { + sendPort.send(_ParseResponse(request, nodes)); + } else if (request is _RenderingRequest) { + final spans = HighlightView.render(nodes, request.theme); + if (request is _RenderRequest) { + sendPort.send(_RenderResponse(request, spans)); + } else if (request is _ParseAndRenderRequest) { + sendPort.send(_ParseAndRenderResponse(request, nodes, spans)); + } + } } else if (request is _IsolateEndRequest) { receivePort.close(); sendPort.send(const _IsolateEndedResponse()); @@ -153,24 +189,62 @@ class _IsolateEndRequest implements _IsolateRequest { const _IsolateEndRequest(); } -class _ParseRequest implements _IdentifiableIsolateRequest<_ParseResponse> { +abstract class _ParsingRequest + implements _IdentifiableIsolateRequest { + String get source; + + String? get language; +} + +class _ParseRequest implements _ParsingRequest<_ParseResponse> { @override final Capability identifier = Capability(); + + @override final String source; + + @override final String? language; _ParseRequest(this.source, {this.language}); } -class _RenderRequest implements _IdentifiableIsolateRequest<_RenderResponse> { +abstract class _RenderingRequest + implements _IdentifiableIsolateRequest { + Map get theme; +} + +class _RenderRequest implements _RenderingRequest { @override final Capability identifier = Capability(); + final List nodes; + + @override final Map theme; _RenderRequest(this.nodes, this.theme); } +class _ParseAndRenderRequest + implements + _ParsingRequest<_ParseAndRenderResponse>, + _RenderingRequest<_ParseAndRenderResponse> { + @override + final Capability identifier = Capability(); + + @override + final String source; + + @override + final String? language; + + @override + final Map theme; + + _ParseAndRenderRequest(this.source, this.theme, {this.language}); +} + abstract class _IsolateResponse {} abstract class _IdentifiableIsolateResponse @@ -186,20 +260,49 @@ class _IsolateEndedResponse implements _IsolateResponse { const _IsolateEndedResponse(); } -class _ParseResponse implements _IdentifiableIsolateResponse { +abstract class _ParsingResponse implements _IdentifiableIsolateResponse { + List get nodes; +} + +class _ParseResponse implements _ParsingResponse { @override final Capability identifier; + + @override final List nodes; _ParseResponse(_ParseRequest request, this.nodes) : identifier = request.identifier; } -class _RenderResponse implements _IdentifiableIsolateResponse { +abstract class _RenderingResponse implements _IdentifiableIsolateResponse { + List get spans; +} + +class _RenderResponse implements _RenderingResponse { @override final Capability identifier; + + @override final List spans; _RenderResponse(_RenderRequest request, this.spans) : identifier = request.identifier; } + +class _ParseAndRenderResponse implements _ParsingResponse, _RenderingResponse { + @override + final Capability identifier; + + @override + final List nodes; + + @override + final List spans; + + _ParseAndRenderResponse( + _ParseAndRenderRequest request, + this.nodes, + this.spans, + ) : identifier = request.identifier; +}