From 311429a3e076b4a618cea5752a2d9afb6180cad6 Mon Sep 17 00:00:00 2001 From: Jeremiah Erinola Date: Tue, 27 Jan 2026 17:47:16 +0000 Subject: [PATCH 1/3] perf: Optimize mask widget rect collection to O(N) Replaced List with Set in _collectMaskWidgetRects to improve performance from O(N^2) to O(N) by avoiding linear scan for contains checks. Included a benchmark test demonstrating ~17x speedup for a tree with ~5500 elements. Co-authored-by: jeremiahseun <53568423+jeremiahseun@users.noreply.github.com> --- .../replay/element_parsers/element_data.dart | 14 ++-- test/reproduction_benchmark.dart | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 test/reproduction_benchmark.dart diff --git a/lib/src/replay/element_parsers/element_data.dart b/lib/src/replay/element_parsers/element_data.dart index 4aeeb1fe..1d0fd023 100644 --- a/lib/src/replay/element_parsers/element_data.dart +++ b/lib/src/replay/element_parsers/element_data.dart @@ -22,9 +22,9 @@ class ElementData { } List extractMaskWidgetRects() { - final rects = []; + final rects = {}; _collectMaskWidgetRects(this, rects); - return rects; + return rects.toList(); } List extractRects({bool isRoot = true}) { @@ -47,14 +47,14 @@ class ElementData { return rects; } - void _collectMaskWidgetRects(ElementData element, List rectList) { - if (!rectList.contains(element.rect)) { + void _collectMaskWidgetRects(ElementData element, Set rectSet) { + if (!rectSet.contains(element.rect)) { if (element.widget is PostHogMaskWidget) { - rectList.add(element.rect); + rectSet.add(element.rect); } else if (element.widget is TextField) { final textField = element.widget as TextField; if (textField.obscureText) { - rectList.add(element.rect); + rectSet.add(element.rect); } } } @@ -62,7 +62,7 @@ class ElementData { final children = element.children; if (children != null && children.isNotEmpty) { for (var child in children) { - _collectMaskWidgetRects(child, rectList); + _collectMaskWidgetRects(child, rectSet); } } } diff --git a/test/reproduction_benchmark.dart b/test/reproduction_benchmark.dart new file mode 100644 index 00000000..5f26e1c1 --- /dev/null +++ b/test/reproduction_benchmark.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:posthog_flutter/src/replay/element_parsers/element_data.dart'; +import 'package:posthog_flutter/src/replay/mask/posthog_mask_widget.dart'; + +void main() { + test('Benchmark extractMaskWidgetRects', () { + // Build a large tree of ElementData + final root = ElementData( + rect: const Rect.fromLTWH(0, 0, 1000, 1000), + type: 'root', + children: [], + ); + + int count = 0; + + // Fan out 4, Depth 6 -> 4^6 = 4096 leaves, total ~5500 nodes. + void addChildren(ElementData parent, int depth) { + if (depth >= 6) return; + + for (int i = 0; i < 4; i++) { + count++; + // Unique rects + final rect = Rect.fromLTWH( + count.toDouble(), + count.toDouble(), + 50, + 50 + ); + + Widget? widget; + // Add about 1/3 of them + if (count % 3 == 0) { + widget = const PostHogMaskWidget(child: SizedBox()); + } else if (count % 3 == 1) { + widget = const TextField(obscureText: true); + } else { + widget = const SizedBox(); + } + + final child = ElementData( + rect: rect, + type: 'child', + widget: widget, + children: [], + ); + + parent.addChildren(child); + addChildren(child, depth + 1); + } + } + + addChildren(root, 0); + print('Created tree with approx $count elements.'); + + final stopwatch = Stopwatch()..start(); + + // Run it multiple times to get a stable measurement + final int iterations = 5; + for (int i = 0; i < iterations; i++) { + root.extractMaskWidgetRects(); + } + + stopwatch.stop(); + print('Time taken for $iterations iterations: ${stopwatch.elapsedMilliseconds} ms'); + print('Average time per iteration: ${stopwatch.elapsedMilliseconds / iterations} ms'); + }); +} From b8d65b1368cde978125512a43b606b7138d5b5dc Mon Sep 17 00:00:00 2001 From: Jeremiah Oluwaseun Erinola Date: Wed, 28 Jan 2026 14:01:41 +0100 Subject: [PATCH 2/3] Deleted test file and updated changelog --- CHANGELOG.md | 4 ++ test/reproduction_benchmark.dart | 68 -------------------------------- 2 files changed, 4 insertions(+), 68 deletions(-) delete mode 100644 test/reproduction_benchmark.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f8cfdccb..a5e0a3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Next +# 5.12.1 + +- perf: Optimize mask widget rect collection to O(N) ([#269](https://github.com/PostHog/posthog-flutter/pull/269)) + # 5.12.0 - feat: flutter error tracking support for web ([#243](https://github.com/PostHog/posthog-flutter/pull/243)) diff --git a/test/reproduction_benchmark.dart b/test/reproduction_benchmark.dart deleted file mode 100644 index 5f26e1c1..00000000 --- a/test/reproduction_benchmark.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:posthog_flutter/src/replay/element_parsers/element_data.dart'; -import 'package:posthog_flutter/src/replay/mask/posthog_mask_widget.dart'; - -void main() { - test('Benchmark extractMaskWidgetRects', () { - // Build a large tree of ElementData - final root = ElementData( - rect: const Rect.fromLTWH(0, 0, 1000, 1000), - type: 'root', - children: [], - ); - - int count = 0; - - // Fan out 4, Depth 6 -> 4^6 = 4096 leaves, total ~5500 nodes. - void addChildren(ElementData parent, int depth) { - if (depth >= 6) return; - - for (int i = 0; i < 4; i++) { - count++; - // Unique rects - final rect = Rect.fromLTWH( - count.toDouble(), - count.toDouble(), - 50, - 50 - ); - - Widget? widget; - // Add about 1/3 of them - if (count % 3 == 0) { - widget = const PostHogMaskWidget(child: SizedBox()); - } else if (count % 3 == 1) { - widget = const TextField(obscureText: true); - } else { - widget = const SizedBox(); - } - - final child = ElementData( - rect: rect, - type: 'child', - widget: widget, - children: [], - ); - - parent.addChildren(child); - addChildren(child, depth + 1); - } - } - - addChildren(root, 0); - print('Created tree with approx $count elements.'); - - final stopwatch = Stopwatch()..start(); - - // Run it multiple times to get a stable measurement - final int iterations = 5; - for (int i = 0; i < iterations; i++) { - root.extractMaskWidgetRects(); - } - - stopwatch.stop(); - print('Time taken for $iterations iterations: ${stopwatch.elapsedMilliseconds} ms'); - print('Average time per iteration: ${stopwatch.elapsedMilliseconds / iterations} ms'); - }); -} From 706883d55e51165d337db68f99b5e2007f67d5f0 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:26:01 +0100 Subject: [PATCH 3/3] Apply suggestion from @marandaneto --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e0a3f6..ebb9f041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,5 @@ ## Next -# 5.12.1 - perf: Optimize mask widget rect collection to O(N) ([#269](https://github.com/PostHog/posthog-flutter/pull/269))