Skip to content

Opening large text files causes 100% CPU hang in difflib.SequenceMatcher (yunicode.py) #401

Description

@ctcjab

Problem

Opening a large text file (e.g. a 176MB log file) in JupyterLab with jupyter_server_ydoc installed causes the Jupyter server process to peg at 100% CPU indefinitely, making the server completely unresponsive. The server must be killed to recover, and the problem recurs on restart if the file tab is restored.

Root cause

YUnicode.set() in yunicode.py calls difflib.SequenceMatcher.ratio() to decide whether to perform a granular update or a full reload:

matcher = SequenceMatcher(a=before_bytes, b=after_bytes)

if (
    matcher.real_quick_ratio() >= SIMILARITY_THREESHOLD
    and matcher.ratio() >= SIMILARITY_THREESHOLD
):

SequenceMatcher.ratio() internally calls get_matching_blocks()find_longest_match(), which is O(n * m) in the lengths of the two sequences. For a 176MB file this means diffing ~167M × 184M bytes, which takes hours or longer and blocks the main thread (with the GIL held) for the entire duration.

The real_quick_ratio() check is fast but doesn't help when the file is being loaded for the first time (old value is empty → ratio is 0.0 → but the file being opened as a new ydoc room goes through rooms.py:initializeybasedoc.py:asetyunicode.py:set where the old value is already populated from ystore).

Stack trace

Captured with py-spy dump --pid <PID>:

Thread 7 (active+gil): "MainThread"
    find_longest_match (difflib.py:385)
        ahi: 167472901
        bhi: 184489591
    get_matching_blocks (difflib.py:454)
    ratio (difflib.py:619)
    set (jupyter_ydoc/yunicode.py:85)
    aset (jupyter_ydoc/ybasedoc.py:203)
    initialize (jupyter_server_ydoc/rooms.py:164)
    open (jupyter_server_ydoc/handlers.py:278)

Impact

  • Server becomes completely unresponsive (all tabs, all kernels, all terminals)
  • The .jupyter_ystore.db grows to match the file size (~177MB in our case)
  • Restarting the server doesn't help if the browser restores the tab — the problem immediately recurs
  • Recovery requires removing the file (or renaming it so the tab can't find it), deleting .jupyter_ystore.db, and restarting

Suggested fix

Add a file size threshold before attempting difflib-based granular updates. For files above a certain size (e.g. 5MB or 10MB), skip the SequenceMatcher entirely and fall back to the full-reload path which is already implemented in the else branch:

MAX_DIFF_SIZE = 5 * 1024 * 1024  # 5 MB

if len(before_bytes) > MAX_DIFF_SIZE or len(after_bytes) > MAX_DIFF_SIZE:
    self._ysource.clear()
    if value:
        self._ysource += value
else:
    matcher = SequenceMatcher(a=before_bytes, b=after_bytes)
    ...

Environment

  • jupyter_ydoc 3.1.1
  • jupyter_server_ydoc 2.3.0
  • Python 3.13.12
  • JupyterLab 4.5.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions