From e06aa5b3710b3c180e583d86edc4c281bcfd664e Mon Sep 17 00:00:00 2001 From: Alspb <73047043+Alspb@users.noreply.github.com> Date: Tue, 2 Jul 2024 07:09:52 +0000 Subject: [PATCH] Perf: Performance optimization (#1964) * Node.length, Node.offset caching added * Unnecessary Document.from removed, method arguments changed accordingly * root.toDelta check replaced with assertion --- lib/src/models/documents/document.dart | 6 +--- lib/src/models/documents/nodes/container.dart | 22 +++++++++--- lib/src/models/documents/nodes/leaf.dart | 35 ++++++++++++++---- lib/src/models/documents/nodes/line.dart | 9 ++--- lib/src/models/documents/nodes/node.dart | 36 ++++++++++++++----- lib/src/models/rules/delete.dart | 17 ++++----- lib/src/models/rules/format.dart | 15 ++++---- lib/src/models/rules/insert.dart | 36 +++++++++---------- lib/src/models/rules/rule.dart | 7 ++-- 9 files changed, 118 insertions(+), 65 deletions(-) diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index b46e6707..95036ca1 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -354,10 +354,7 @@ class Document { } catch (e) { throw StateError('_delta compose failed'); } - - if (_delta != _root.toDelta()) { - throw StateError('Compose failed'); - } + assert(_delta == _root.toDelta(), 'Compose failed'); final change = DocChange(originalDelta, delta, changeSource); documentChangeObserver.add(change); history.handleDocChange(change); @@ -442,7 +439,6 @@ class Document { doc.toString(), 'Document Delta cannot be empty.'); } - // print(doc.last.data.runtimeType); assert((doc.last.data as String).endsWith('\n')); var offset = 0; diff --git a/lib/src/models/documents/nodes/container.dart b/lib/src/models/documents/nodes/container.dart index bc52adb6..7b5bb551 100644 --- a/lib/src/models/documents/nodes/container.dart +++ b/lib/src/models/documents/nodes/container.dart @@ -47,11 +47,14 @@ abstract base class QuillContainer extends Node { /// Always returns fresh instance. T get defaultChild; + int? _length; + /// Adds [node] to the end of this container children list. void add(T node) { assert(node?.parent == null); node?.parent = this; _children.add(node as Node); + clearLengthCache(); } /// Adds [node] to the beginning of this container children list. @@ -59,6 +62,7 @@ abstract base class QuillContainer extends Node { assert(node?.parent == null); node?.parent = this; _children.addFirst(node as Node); + clearLengthCache(); } /// Removes [node] from this container. @@ -66,6 +70,7 @@ abstract base class QuillContainer extends Node { assert(node?.parent == this); node?.parent = null; _children.remove(node as Node); + clearLengthCache(); } /// Moves children of this node to [newParent]. @@ -118,11 +123,20 @@ abstract base class QuillContainer extends Node { .map((e) => e.toPlainText(embedBuilders, unknownEmbedBuilder)) .join(); - /// Content length of this node's children. - /// - /// To get number of children in this node use [childCount]. @override - int get length => _children.fold(0, (cur, node) => cur + node.length); + int get length { + _length ??= _children.fold(0, (cur, node) => (cur ?? 0) + node.length); + return _length!; + } + + @override + void clearLengthCache() { + _length = null; + clearOffsetCache(); + if (parent != null) { + parent!.clearLengthCache(); + } + } @override void insert(int index, Object data, Style? style) { diff --git a/lib/src/models/documents/nodes/leaf.dart b/lib/src/models/documents/nodes/leaf.dart index 676abdc4..43a5e1ff 100644 --- a/lib/src/models/documents/nodes/leaf.dart +++ b/lib/src/models/documents/nodes/leaf.dart @@ -24,18 +24,39 @@ abstract base class Leaf extends Node { /// Contents of this node, either a String if this is a [QuillText] or an /// [Embed] if this is an [BlockEmbed]. Object get value => _value; + + set value(Object v) { + _value = v; + _length = null; + clearOffsetCache(); + } + Object _value; @override Line? get parent => super.parent as Line?; + int? _length; + @override int get length { + if (_length != null) { + return _length!; + } if (_value is String) { - return (_value as String).length; + _length = (_value as String).length; + } else { + // return 1 for embedded object + _length = 1; + } + return _length!; + } + + @override + void clearLengthCache() { + if (parent != null) { + parent!.clearLengthCache(); } - // return 1 for embedded object - return 1; } @override @@ -47,6 +68,7 @@ abstract base class Leaf extends Node { @override void insert(int index, Object data, Style? style) { + final length = this.length; assert(index >= 0 && index <= length); final node = Leaf(data); if (index < length) { @@ -75,6 +97,7 @@ abstract base class Leaf extends Node { @override void delete(int index, int? len) { + final length = this.length; assert(index < length); final local = math.min(length - index, len!); @@ -117,7 +140,7 @@ abstract base class Leaf extends Node { // Merging it with previous node if style is the same. final prev = node.previous; if (!node.isFirst && prev is QuillText && prev.style == node.style) { - prev._value = prev.value + node.value; + prev.value = prev.value + node.value; node.unlink(); node = prev; } @@ -125,7 +148,7 @@ abstract base class Leaf extends Node { // Merging it with next node if style is the same. final next = node.next; if (!node.isLast && next is QuillText && next.style == node.style) { - node._value = node.value + next.value; + node.value = node.value + next.value; next.unlink(); } } @@ -152,7 +175,7 @@ abstract base class Leaf extends Node { assert(this is QuillText); final text = _value as String; - _value = text.substring(0, index); + value = text.substring(0, index); final split = Leaf(text.substring(index))..applyStyle(style); insertAfter(split); return split; diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index 65e893f7..ee467d4e 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -127,11 +127,11 @@ base class Line extends QuillContainer { if (style == null) { return; } - final thisLength = length; + final length = this.length; - final local = math.min(thisLength - index, len!); + final local = math.min(length - index, len!); // If index is at newline character then this is a line/block style update. - final isLineFormat = (index + local == thisLength) && local == 1; + final isLineFormat = (index + local == length) && local == 1; if (isLineFormat) { assert( @@ -145,7 +145,7 @@ base class Line extends QuillContainer { assert(style.values.every((attr) => attr.scope == AttributeScope.inline || attr.scope == AttributeScope.ignore)); - assert(index + local != thisLength); + assert(index + local != length); super.retain(index, local, style); } @@ -158,6 +158,7 @@ base class Line extends QuillContainer { @override void delete(int index, int? len) { + final length = this.length; final local = math.min(length - index, len!); final isLFDeleted = index + local == length; // Line feed if (isLFDeleted) { diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 0ef7c10a..0287dc4d 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -47,24 +47,41 @@ abstract base class Node extends LinkedListEntry { /// Length of this node in characters. int get length; + void clearLengthCache(); + Node clone() => newInstance()..applyStyle(style); + int? _offset; + /// Offset in characters of this node relative to [parent] node. /// /// To get offset of this node in the document see [documentOffset]. int get offset { - var offset = 0; + if (_offset != null) { + return _offset!; + } if (list == null || isFirst) { - return offset; + return 0; + } + var offset = 0; + for (final node in list!) { + if (node == this) { + break; + } + offset += node.length; } - var cur = this; - do { - cur = cur.previous!; - offset += cur.length; - } while (!cur.isFirst); - return offset; + _offset = offset; + return _offset!; + } + + void clearOffsetCache() { + _offset = null; + final next = this.next; + if (next != null) { + next.clearOffsetCache(); + } } /// Offset in characters of this node in the document. @@ -100,6 +117,7 @@ abstract base class Node extends LinkedListEntry { assert(entry.parent == null && parent != null); entry.parent = parent; super.insertBefore(entry); + clearLengthCache(); } @override @@ -107,11 +125,13 @@ abstract base class Node extends LinkedListEntry { assert(entry.parent == null && parent != null); entry.parent = parent; super.insertAfter(entry); + clearLengthCache(); } @override void unlink() { assert(parent != null); + clearLengthCache(); parent = null; super.unlink(); } diff --git a/lib/src/models/rules/delete.dart b/lib/src/models/rules/delete.dart index 78b94f6b..2f36f76d 100644 --- a/lib/src/models/rules/delete.dart +++ b/lib/src/models/rules/delete.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart' show immutable; import '../../../quill_delta.dart'; +import '../../models/documents/document.dart'; import '../documents/attribute.dart'; import '../documents/nodes/embeddable.dart'; import 'rule.dart'; @@ -26,9 +27,9 @@ class EnsureLastLineBreakDeleteRule extends DeleteRule { const EnsureLastLineBreakDeleteRule(); @override - Delta? applyRule(Delta document, int index, + Delta? applyRule(Document document, int index, {int? len, Object? data, Attribute? attribute}) { - final itr = DeltaIterator(document)..skip(index + len!); + final itr = DeltaIterator(document.toDelta())..skip(index + len!); return Delta() ..retain(index) @@ -43,9 +44,9 @@ class CatchAllDeleteRule extends DeleteRule { const CatchAllDeleteRule(); @override - Delta applyRule(Delta document, int index, + Delta applyRule(Document document, int index, {int? len, Object? data, Attribute? attribute}) { - final itr = DeltaIterator(document)..skip(index + len!); + final itr = DeltaIterator(document.toDelta())..skip(index + len!); return Delta() ..retain(index) @@ -64,9 +65,9 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { const PreserveLineStyleOnMergeRule(); @override - Delta? applyRule(Delta document, int index, + Delta? applyRule(Document document, int index, {int? len, Object? data, Attribute? attribute}) { - final itr = DeltaIterator(document)..skip(index); + final itr = DeltaIterator(document.toDelta())..skip(index); var op = itr.next(1); if (op.data != '\n') { return null; @@ -121,9 +122,9 @@ class EnsureEmbedLineRule extends DeleteRule { const EnsureEmbedLineRule(); @override - Delta? applyRule(Delta document, int index, + Delta? applyRule(Document document, int index, {int? len, Object? data, Attribute? attribute}) { - final itr = DeltaIterator(document); + final itr = DeltaIterator(document.toDelta()); var op = itr.skip(index); final opAfter = itr.skip(index + 1); diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index 7f731e7a..f1e1b804 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart' show immutable; import '../../../quill_delta.dart'; +import '../../models/documents/document.dart'; import '../documents/attribute.dart'; import 'rule.dart'; @@ -28,7 +29,7 @@ class ResolveLineFormatRule extends FormatRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -41,7 +42,7 @@ class ResolveLineFormatRule extends FormatRule { // Apply line styles to all newline characters within range of this // retain operation. var result = Delta()..retain(index); - final itr = DeltaIterator(document)..skip(index); + final itr = DeltaIterator(document.toDelta())..skip(index); Operation op; for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { op = itr.next(len - cur); @@ -119,7 +120,7 @@ class FormatLinkAtCaretPositionRule extends FormatRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -130,7 +131,7 @@ class FormatLinkAtCaretPositionRule extends FormatRule { } final delta = Delta(); - final itr = DeltaIterator(document); + final itr = DeltaIterator(document.toDelta()); final before = itr.skip(index), after = itr.next(); int? beg = index, retain = 0; if (before != null && before.hasAttribute(attribute.key)) { @@ -159,7 +160,7 @@ class ResolveInlineFormatRule extends FormatRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -170,7 +171,7 @@ class ResolveInlineFormatRule extends FormatRule { } final delta = Delta()..retain(index); - final itr = DeltaIterator(document)..skip(index); + final itr = DeltaIterator(document.toDelta())..skip(index); Operation op; for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { @@ -205,7 +206,7 @@ class ResolveImageFormatRule extends FormatRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 873b1cc4..6b2a05b6 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -33,7 +33,7 @@ class PreserveLineStyleOnSplitRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -42,8 +42,7 @@ class PreserveLineStyleOnSplitRule extends InsertRule { if (data is! String || data != '\n') { return null; } - - final itr = DeltaIterator(document); + final itr = DeltaIterator(document.toDelta()); final before = itr.skip(index); if (before == null) { return null; @@ -84,7 +83,7 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -94,8 +93,7 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { // Only interested in text containing at least one newline character. return null; } - - final itr = DeltaIterator(document)..skip(index); + final itr = DeltaIterator(document.toDelta())..skip(index); // Look for the next newline. final nextNewLine = _getNextNewLine(itr); @@ -171,7 +169,7 @@ class AutoExitBlockRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -181,7 +179,7 @@ class AutoExitBlockRule extends InsertRule { return null; } - final itr = DeltaIterator(document); + final itr = DeltaIterator(document.toDelta()); final prev = itr.skip(index), cur = itr.next(); final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader(); // We are not in a block, ignore. @@ -241,7 +239,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -251,7 +249,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule { return null; } - final itr = DeltaIterator(document)..skip(index); + final itr = DeltaIterator(document.toDelta())..skip(index); final cur = itr.next(); if (cur.data is! String || !(cur.data as String).startsWith('\n')) { return null; @@ -278,7 +276,7 @@ class InsertEmbedsRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -295,7 +293,7 @@ class InsertEmbedsRule extends InsertRule { } final delta = Delta()..retain(index + (len ?? 0)); - final itr = DeltaIterator(document); + final itr = DeltaIterator(document.toDelta()); final prev = itr.skip(index), cur = itr.next(); final textBefore = prev?.data is String ? prev!.data as String? : ''; @@ -386,7 +384,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -397,7 +395,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { if (data is! String) return null; // Get current text. - final entireText = Document.fromDelta(document).toPlainText(); + final entireText = document.toPlainText(); // Get word before insertion. final leftWordPart = entireText @@ -502,7 +500,7 @@ class AutoFormatLinksRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -512,7 +510,7 @@ class AutoFormatLinksRule extends InsertRule { return null; } - final itr = DeltaIterator(document); + final itr = DeltaIterator(document.toDelta()); final prev = itr.skip(index); if (prev == null || prev.data is! String) { return null; @@ -548,7 +546,7 @@ class PreserveInlineStylesRule extends InsertRule { @override Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -558,7 +556,7 @@ class PreserveInlineStylesRule extends InsertRule { return null; } - final itr = DeltaIterator(document); + final itr = DeltaIterator(document.toDelta()); var prev = itr.skip(len == 0 ? index : index + 1); if (prev == null || prev.data is! String) return null; @@ -609,7 +607,7 @@ class CatchAllInsertRule extends InsertRule { @override Delta applyRule( - Delta document, + Document document, int index, { int? len, Object? data, diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart index d7132b3e..fd319bb5 100644 --- a/lib/src/models/rules/rule.dart +++ b/lib/src/models/rules/rule.dart @@ -14,7 +14,7 @@ abstract class Rule { const Rule(); Delta? apply( - Delta document, + Document document, int index, { int? len, Object? data, @@ -35,7 +35,7 @@ abstract class Rule { /// Applies heuristic rule to an operation on a [document] and returns /// resulting [Delta]. Delta? applyRule( - Delta document, + Document document, int index, { int? len, Object? data, @@ -85,13 +85,12 @@ class Rules { Object? data, Attribute? attribute, }) { - final delta = document.toDelta(); for (final rule in _customRules + _rules) { if (rule.type != ruleType) { continue; } try { - final result = rule.apply(delta, index, + final result = rule.apply(document, index, len: len, data: data, attribute: attribute); if (result != null) { return result..trim();