Skip to content

[Feature]: TesseractLastSeen — persistent per-text lastSeen cache (×25 to ×150 speedup, mirrors #353) #423

Description

@julienmerconsulting

Type Surface Measured speedup Mirrors Design

Problem / motivation

Two weeks ago I shipped #353 — persistent lastSeen cache for image find via a PNG ancillary chunk. Idea was honest: the second time you look for the same image, you already paid the bill once; the second lookup should not pay it again. Measured cold-start speedup on repeated find(): ~×6.5.

Then I ran a notepad session with the word "Espagne" on screen and called findText("Espagne") eight times in a row. Each call took roughly six seconds. Eight calls, forty-eight seconds. The text never moved between calls of the same test step, and Tesseract was scanning the entire 1920×1080 screen every single time, just to confirm something it had already located one second earlier.

The obvious question came next: why do image lookups have a lastSeen cache but text lookups don't? The data shape is the same — a bounding box, a screen resolution stamp. The fallback semantics are the same — if the cached ROI no longer contains the target, scan the full region and update. The only thing #353 has that this cannot reuse is the PNG chunk as a carrier: text has no PNG file to attach to.

Full design story and measured numbers live in the design discussion linked in the badges — Discussion #400. This issue tracks the implementation.

Proposed solution

A new class org.sikuli.support.TesseractLastSeen — persistent per-text last-seen cache, JSON sidecar catalog, mirroring the image mechanism from #353.

Storage: tesseract-lastseen.json in the JVM working directory (user.dir). Colocated with the user's test project so it can be committed, shared across machines, and survives across runs. Deliberately out of ~/.oculix/: it is a project asset, not a per-user setting.

Key: (text, search-region context) pair. The same text sought in different sub-regions (notepad.findText("Save") vs chrome.findText("Save")) gets independent slots and never overwrites the other.

Flow:

  • On first lookup: full region scan (unchanged behaviour), bounding box of the returned Match is persisted under the (text, context) key.
  • On subsequent lookups for the same pair: only the cached box is rescanned — typically <50 ms vs >1000 ms for full screen.
  • If the cached region no longer contains the text (UI shifted, element moved): caller falls back to a full scan and the stale entry is invalidated.

Public API — three static methods on TesseractLastSeen:

Region getRegion(Region searchRegion, String text);          // returns cached ROI or null
void   put(Region searchRegion, String text, Match match);   // persist bounding box
void   invalidate(Region searchRegion, String text);         // drop stale entry

Injection site: single funnel in Region.doFind — the private method through which every text-finding API converges (findText, existsText, waitText, hasText, click(text), doubleClick(text), rightClick(text), hover(text), find(text), dragDrop(text, ...)). One modification at the funnel = full coverage across all public entry points, zero duplication.

Safety:

  • All reads/writes guarded by a single intra-process lock.
  • Writes are atomic (write tmp + ATOMIC_MOVE) so a crash mid-persistence never leaves corrupt JSON.
  • Any IO/parse failure is swallowed and the OCR call proceeds as if no cache existed — the cache layer must never break the user's automation.
  • Screen-resolution-aware: entries captured on 1920×1080 are ignored when the current screen is a different size, so cached coordinates never point off-screen on a different setup.
  • Zero padding on purpose: cached box is exact, any UI shift >0 px triggers a fallback. Deterministic beats permissive.

Alternatives considered

  • In-memory cache only — dies with the JVM, does not persist across runs. Repeated CI runs still pay the full scan every time. The whole point of [Feature]: Persistent lastSeen via PNG ancillary chunk — ~×6.5 find speedup, zero config #353 was persistence; the text version needs the same property.
  • Reuse the PNG chunk trick from [Feature]: Persistent lastSeen via PNG ancillary chunk — ~×6.5 find speedup, zero config #353 — text has no PNG file to attach to. Text lookups target strings, not image files on disk.
  • Store in ~/.oculix/ as a per-user setting — the cache is inherently tied to a specific project (its screens, its UI, its resolutions). Colocating it in user.dir lets teams commit it with the test project, which turns a per-user optimization into a team-shared one.
  • Padded cached box for tolerance to small UI shifts — considered and rejected. Padding introduces ambiguity ("did the cache match or did the fallback?"). Determinism first: exact box, any shift triggers a full rescan and updates.
  • TTL-based invalidation — considered for future iteration but not in v1. The current invalidation (miss → full rescan → update) is sufficient for the reported use case. TTL can be added as an option later without breaking the wire format (FORMAT_VERSION field is reserved).

Where would this live?

  • API (Screen / Region / Pattern / Match)
  • PaddleOCR / Tesseract

API impact

Yes — new public API (backward-compatible). Adds org.sikuli.support.TesseractLastSeen as a utility class with three static methods. No existing method signature changes. The injection at Region.doFind is transparent: callers of findText, click(text), etc. see faster repeated lookups, nothing else.

Use case / impact

  • Anyone running OCR-heavy scripts on the same UI repeatedly — CI test suites, kiosk automation, screen readers, RPA workflows that revisit the same fields many times per run.
  • Measured cases in the PoC:
    • Notepad "Espagne" ×8 lookups: 48 s → <400 ms after warm-up (×120).
    • Full-screen findText on a typical desktop: >1000 ms cold → <50 ms warm (~×25 low bound).
  • Ships alongside [Feature]: Persistent lastSeen via PNG ancillary chunk — ~×6.5 find speedup, zero config #353 so image and text lookups benefit from the same persistence philosophy, no user-visible discrepancy between the two.

Additional context

Full design walkthrough, PoC measurements, and open design questions live in the design discussion linked in the badges — Discussion #400. Raimund's feedback there already covers the future dimensions (per-project scope, config surface via OCR.options, oculix-text-cache.json as future filename, cache-all-occurrences as future option). This issue tracks the v1 landing.

Format version is a first-class field in the JSON schema so future evolutions (TTL, multi-occurrence, padded boxes as opt-in) can be added without breaking existing catalogs.

🦎

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    No fields configured for Feature.

    Projects

    Status
    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions