Skip to content

perf(editor): replace editor rendering pipeline with standalone viewport-windowed editor architecture - #1857

Open
loerei wants to merge 17 commits into
Automattic:trunkfrom
loerei:feature/viewport-windowed-editor
Open

perf(editor): replace editor rendering pipeline with standalone viewport-windowed editor architecture#1857
loerei wants to merge 17 commits into
Automattic:trunkfrom
loerei:feature/viewport-windowed-editor

Conversation

@loerei

@loerei loerei commented Aug 2, 2026

Copy link
Copy Markdown

Summary

This PR replaces the editor rendering pipeline with a standalone viewport-windowed editor architecture while preserving the existing editing model and document format. The primary goal is to reduce layout work during typing in very large notes (>100k characters) without changing user-visible behavior.

It scope-bounds layout measurements and formatting span processing, reducing measured execution duration inside SimplenoteEditText.onDraw() at 100,000+ characters (2,500+ lines) from ~80.5ms down to ~29.8ms, while preserving native touch-drag selection, restoring scroll position persistence, and restoring native Android OverScroller fling inertia scrolling.


Performance Demonstration

The attached video demonstrates typing responsiveness on a document containing 108,440 characters and 2,658 lines:

  • First Half: Modified version using the Viewport-Windowed Single-EditText Engine (~29.8ms measured SimplenoteEditText.onDraw() execution duration).
  • Second Half: Original baseline implementation using the ScrollView-Wrapped Full-Doc Editor (~80.5ms measured SimplenoteEditText.onDraw() execution duration).
SimplenotePerfDiffDemo.mp4

Why

1. Root Cause: Unbounded Layout Measurement

In the legacy architecture, SimplenoteEditText was nested inside a NestedScrollView. Because ScrollView measures child views with MeasureSpec.UNSPECIFIED, Android was forced to measure, layout, and render all 100,000+ characters across the entire document on every single keypress, resulting in a baseline execution duration of ~80.5ms inside SimplenoteEditText.onDraw().

2. User Impact: Main Thread Stalls & Cumulative Frame Skips

While 80.5ms represents a single execution pass inside SimplenoteEditText.onDraw(), rapid typing compounds this delay on the Main UI Thread:

  • VSYNC Frame Skips: On 120Hz (8.3ms budget) or 60Hz (16.6ms budget) displays, an 80.5ms execution inside SimplenoteEditText.onDraw() exceeds the frame deadline, skipping multiple VSYNC render cycles.
  • Queued Event Stalls: When multiple keys are pressed in quick succession during an active layout pass, pending text modification events are processed sequentially on the Main Thread. Each subsequent pass incurs another ~80.5ms execution cost, accumulating input lag before rendering the updated text.
  • String Allocations: Calling full-text toString() on 100,000 characters repeatedly allocates temporary String objects on every keypress.

Implementation Details

1. Layout & View Architecture (fragment_note_editor.xml & SimplenoteEditText.java)

  • Unwrapped NestedScrollView: Replaced root and nested NestedScrollView wrappers with a clean FrameLayout & LinearLayout hierarchy (layout_weight="1"). SimplenoteEditText now manages its own vertical scrollbar (android:scrollbars="vertical").
  • Native Text View: Changed base class of SimplenoteEditText from AppCompatMultiAutoCompleteTextView to native MultiAutoCompleteTextView to avoid AppCompat compatibility layer overhead.
  • Render Performance Stack:
    • Disabled font padding (setIncludeFontPadding(false)) and elegant text height (setElegantTextHeight(false)).
    • Enforced Layout.BREAK_STRATEGY_SIMPLE and Layout.HYPHENATION_FREQUENCY_NONE (Android M+).
    • Preserved standard keyboard suggestions and autocorrect (textAutoCorrect), relying on standard window-level hardware acceleration.

2. Local Cursor Windowing & Incremental Scanning (SimplenoteEditText.java, NoteEditorFragment.java, & AutoBullet.java)

  • Cursor Windowing $O(1)$ Optimization (enoughToFilter): Replaced full-text toString().substring() with a localized subSequence window inspecting max 200 characters around the cursor.
  • Incremental Checklist Scanning: Overloaded processChecklists(int start, int count) to scan and apply checkbox spans only within the paragraph bounds of the active edit window instead of the whole file.
  • Targeted Title Formatting (setTitleSpan): Restricted MetricAffectingSpan search to the title line range ([0, titleEndPosition + 1]).
  • AutoBullet Early Exit: Added instant line-break check editable.charAt(newCursorPosition - 1) != '\n' to bypass toString() allocations on standard typing.

3. Scroll Position Persistence & Native Fling Inertia (SimplenoteMovementMethod.java & SimplenoteEditText.java)

  • Scroll Position Persistence: Attached OnScrollChangeListener directly on SimplenoteEditText to preserve scroll position state across navigation and note opens.
  • Touch Event Delegation: Updated SimplenoteMovementMethod.onTouchEvent() to return super.onTouchEvent(...), restoring drag-selection and cursor placement.
  • Restored Native Fling Behavior: Integrated OverScroller and VelocityTracker in SimplenoteEditText.onTouchEvent(), restoring native Android fling deceleration and preserving standard platform scrolling dynamics upon touch release (ACTION_UP).

Verification & Performance Benchmarks

1. Automated & Compilation Verification

  • Ran .\gradlew assembleDebug $\rightarrow$ BUILD SUCCESSFUL.
  • Ran .\gradlew testDebugUnitTest $\rightarrow$ BUILD SUCCESSFUL (All unit tests pass).
  • Installed and verified on Samsung Galaxy S24 Ultra (SM-S928B).

2. Empirical Performance Metrics (108,440 characters / 2,658 lines)

  • SimplenoteEditText.onDraw() Duration: Measured directly inside SimplenoteEditText.onDraw(), execution duration dropped from ~80.5ms (legacy trunk) down to ~29.8ms at 108,440 characters.
  • Fling Inertia Scrolling: Deceleration matches native Android ScrollView feel.
  • Checklist Toggling: Instant UI update without text watcher callback loops.

Files Changed

Core Editor Engine

  • Simplenote/src/main/java/com/automattic/simplenote/widgets/SimplenoteEditText.java: Implemented hardware render stack, OverScroller fling inertia, local cursor windowing, and incremental checklist processing.
  • Simplenote/src/main/java/com/automattic/simplenote/utils/SimplenoteMovementMethod.java: Delegated touch events back to super.onTouchEvent for touch selection.

Fragment & Utility Optimizations

  • Simplenote/src/main/java/com/automattic/simplenote/NoteEditorFragment.java: Attached scroll listener on SimplenoteEditText to persist scroll position, scoped setTitleSpan to title paragraph, and enabled incremental checklist processing.
  • Simplenote/src/main/java/com/automattic/simplenote/utils/AutoBullet.java: Added early exit check for non-newline typing.

Layout & Build System

  • Simplenote/src/main/res/layout/fragment_note_editor.xml: Removed NestedScrollView wrappers and configured standalone vertical scrollbar layout with textAutoCorrect.
  • Simplenote/build.gradle: Configured debug build string resource value.

@loerei

loerei commented Aug 2, 2026

Copy link
Copy Markdown
Author

Hi team! Long-time user here. 👋

This is my first contribution to the project. I started looking into this after noticing typing lag once my personal diary grew to around 100k characters, which led me to explore a viewport-windowed approach to reduce layout work during editing.

I've verified the changes on my device, ran the existing unit test suite successfully, and included performance measurements and a demo video in the PR description. I'm happy to iterate on the implementation based on your feedback.

@loerei
loerei force-pushed the feature/viewport-windowed-editor branch from 15432dc to 30b91a1 Compare August 4, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant