Skip to content

Commit

Permalink
Perf: Performance optimization (#1964)
Browse files Browse the repository at this point in the history
* Node.length, Node.offset caching added

* Unnecessary Document.from removed, method arguments changed accordingly

* root.toDelta check replaced with assertion
  • Loading branch information
Alspb authored Jul 2, 2024
1 parent 40214bd commit e06aa5b
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 65 deletions.
6 changes: 1 addition & 5 deletions lib/src/models/documents/document.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 18 additions & 4 deletions lib/src/models/documents/nodes/container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,30 @@ abstract base class QuillContainer<T extends Node?> 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.
void addFirst(T node) {
assert(node?.parent == null);
node?.parent = this;
_children.addFirst(node as Node);
clearLengthCache();
}

/// Removes [node] from this container.
void remove(T node) {
assert(node?.parent == this);
node?.parent = null;
_children.remove(node as Node);
clearLengthCache();
}

/// Moves children of this node to [newParent].
Expand Down Expand Up @@ -118,11 +123,20 @@ abstract base class QuillContainer<T extends Node?> 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) {
Expand Down
35 changes: 29 additions & 6 deletions lib/src/models/documents/nodes/leaf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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!);
Expand Down Expand Up @@ -117,15 +140,15 @@ 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;
}

// 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();
}
}
Expand All @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions lib/src/models/documents/nodes/line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ base class Line extends QuillContainer<Leaf?> {
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(
Expand All @@ -145,7 +145,7 @@ base class Line extends QuillContainer<Leaf?> {
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);
}

Expand All @@ -158,6 +158,7 @@ base class Line extends QuillContainer<Leaf?> {

@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) {
Expand Down
36 changes: 28 additions & 8 deletions lib/src/models/documents/nodes/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,41 @@ abstract base class Node extends LinkedListEntry<Node> {
/// 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.
Expand Down Expand Up @@ -100,18 +117,21 @@ abstract base class Node extends LinkedListEntry<Node> {
assert(entry.parent == null && parent != null);
entry.parent = parent;
super.insertBefore(entry);
clearLengthCache();
}

@override
void insertAfter(Node entry) {
assert(entry.parent == null && parent != null);
entry.parent = parent;
super.insertAfter(entry);
clearLengthCache();
}

@override
void unlink() {
assert(parent != null);
clearLengthCache();
parent = null;
super.unlink();
}
Expand Down
17 changes: 9 additions & 8 deletions lib/src/models/rules/delete.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 8 additions & 7 deletions lib/src/models/rules/format.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -28,7 +29,7 @@ class ResolveLineFormatRule extends FormatRule {

@override
Delta? applyRule(
Delta document,
Document document,
int index, {
int? len,
Object? data,
Expand All @@ -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);
Expand Down Expand Up @@ -119,7 +120,7 @@ class FormatLinkAtCaretPositionRule extends FormatRule {

@override
Delta? applyRule(
Delta document,
Document document,
int index, {
int? len,
Object? data,
Expand All @@ -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)) {
Expand Down Expand Up @@ -159,7 +160,7 @@ class ResolveInlineFormatRule extends FormatRule {

@override
Delta? applyRule(
Delta document,
Document document,
int index, {
int? len,
Object? data,
Expand All @@ -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!) {
Expand Down Expand Up @@ -205,7 +206,7 @@ class ResolveImageFormatRule extends FormatRule {

@override
Delta? applyRule(
Delta document,
Document document,
int index, {
int? len,
Object? data,
Expand Down
Loading

0 comments on commit e06aa5b

Please sign in to comment.