Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Nov 20, 2025

📄 10% (0.10x) speedup for SkyvernBrowserPage._input_text in skyvern/library/skyvern_browser_page.py

⏱️ Runtime : 1.93 milliseconds 1.75 milliseconds (best of 29 runs)

📝 Explanation and details

The optimization achieves a 10% runtime improvement by eliminating character-by-character typing overhead in the input_sequentially function. The key change replaces the character iteration loop with bulk text input operations.

What changed:

  • Bulk typing for short text: Instead of iterating through each character with await locator.type(char, ...), short texts (≤ TEXT_PRESS_MAX_LENGTH) are now typed in one call with await locator.type(text, ...).
  • Streamlined long text handling: For longer texts, the remainder portion is also typed in bulk rather than character-by-character.

Why it's faster:
The original approach required 1,926 individual await locator.type() calls (visible in line profiler), consuming 63.8% of the function's runtime. Each await involves async overhead, context switching, and browser communication. The optimized version reduces this to just 1-2 bulk operations, eliminating the loop entirely for most cases.

Performance impact:

  • Line profiler shows the optimized input_sequentially function dropped from 2.16ms to 0.48ms total time
  • The character iteration loop that dominated 63.8% of runtime is completely eliminated for short texts
  • Test results show consistent improvements across all text input scenarios, from basic cases to high-volume concurrent operations

Real-world benefits:
This optimization particularly benefits web automation workflows where text input is frequent, as it reduces the cumulative overhead of multiple small browser interactions into fewer, more efficient bulk operations. The 10% improvement compounds when input_sequentially is called repeatedly during form filling or data entry tasks.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 691 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 85.7%
🌀 Generated Regression Tests and Runtime
import asyncio  # used to run async functions
from typing import Any

import pytest  # used for our unit tests
from skyvern.library.skyvern_browser_page import SkyvernBrowserPage

TEXT_PRESS_MAX_LENGTH = 10

class DummyLocator:
    """A dummy locator to simulate Playwright's Locator for testing."""
    def __init__(self):
        self.filled = None
        self.typed = []
    async def fill(self, text, timeout=None):
        self.filled = text
    async def type(self, char, delay=None, timeout=None):
        self.typed.append(char)

class DummyPage:
    """A dummy Playwright Page for testing."""
    def __init__(self):
        self.locators = {}
        self.url = "http://test.local"
    def locator(self, selector):
        if selector == "raise":
            raise RuntimeError("Locator error")
        if selector not in self.locators:
            self.locators[selector] = DummyLocator()
        return self.locators[selector]

class DummyBrowser:
    """A dummy browser for testing."""
    def __init__(self):
        self.sdk = DummySdk()
        self.client = DummyClient()
        self.browser_session_id = "session"
        self.browser_address = "address"
        self.workflow_run_id = "workflow"

class DummySdk:
    async def ensure_has_server(self):
        return

class DummyClient:
    async def run_sdk_action(self, **kwargs):
        # Simulate a response object with .workflow_run_id and .result
        class Response:
            def __init__(self):
                self.workflow_run_id = "new_workflow"
                self.result = "ai_result"
        return Response()

class DummyAi:
    def __init__(self, browser, page):
        self._browser = browser
        self._page = page
    async def ai_input_text(self, selector, value, intention, data=None, totp_identifier=None, totp_url=None, timeout=None):
        # Simulate AI returning a result
        return f"ai:{value}:{intention}"

class DummyRun:
    def __init__(self, browser, page):
        pass

class settings:
    BROWSER_ACTION_TIMEOUT_MS = 1000

# --- Begin: SdkSkyvernPageAi stub ---

class SdkSkyvernPageAi(DummyAi):
    pass
from skyvern.library.skyvern_browser_page import SkyvernBrowserPage

# --- Begin: Unit Tests ---

# 1. Basic Test Cases

@pytest.mark.asyncio
async def test_input_text_basic_fill_and_type():
    """Test basic input with selector and value, fallback AI."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    result = await skyvern_page._input_text(selector="input", value="hello")
    # Ensure locator was used and typed correctly
    locator = page.locators["input"]

@pytest.mark.asyncio
async def test_input_text_basic_ai_proactive():
    """Test proactive AI input with intention and selector."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    result = await skyvern_page._input_text(selector="input", value="foo", ai="proactive", intention="do something")

@pytest.mark.asyncio
async def test_input_text_basic_fallback_ai_with_intention():
    """Test fallback AI triggers AI input when intention is provided."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    result = await skyvern_page._input_text(selector="input", value="bar", intention="do this")

@pytest.mark.asyncio
async def test_input_text_basic_no_selector_with_intention():
    """Test fallback AI when selector is None but intention is provided."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    result = await skyvern_page._input_text(selector=None, value="baz", intention="intention")

@pytest.mark.asyncio
async def test_input_text_basic_no_selector_no_intention():
    """Test fallback AI when selector and intention are None (should return value)."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    result = await skyvern_page._input_text(selector=None, value="baz")

# 2. Edge Test Cases

@pytest.mark.asyncio
async def test_input_text_edge_long_text_triggers_fill_and_type():
    """Test input_sequentially triggers fill for long text."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    long_text = "a" * (TEXT_PRESS_MAX_LENGTH + 5)
    result = await skyvern_page._input_text(selector="input", value=long_text)
    locator = page.locators["input"]

@pytest.mark.asyncio
async def test_input_text_edge_selector_raises_exception():
    """Test that an exception in locator is propagated."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    with pytest.raises(RuntimeError) as excinfo:
        await skyvern_page._input_text(selector="raise", value="fail")

@pytest.mark.asyncio
async def test_input_text_edge_no_selector_proactive_ai_requires_intention():
    """Test proactive AI with no selector but intention provided."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    result = await skyvern_page._input_text(selector=None, value="hello", ai="proactive", intention="intent")

@pytest.mark.asyncio
async def test_input_text_edge_no_selector_no_ai_raises():
    """Test that missing selector and no intention raises ValueError."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    with pytest.raises(ValueError) as excinfo:
        await skyvern_page._input_text(selector=None, value="fail", ai="notfallback")

@pytest.mark.asyncio
async def test_input_text_edge_empty_string_value():
    """Test input with empty string value."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    result = await skyvern_page._input_text(selector="input", value="")
    locator = page.locators["input"]

@pytest.mark.asyncio

async def test_input_text_large_concurrent_calls():
    """Test multiple concurrent _input_text calls for scalability."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    selectors = [f"input{i}" for i in range(20)]
    tasks = [
        skyvern_page._input_text(selector=sel, value=f"val{idx}")
        for idx, sel in enumerate(selectors)
    ]
    results = await asyncio.gather(*tasks)
    # Each locator should have typed the correct value
    for i, sel in enumerate(selectors):
        locator = page.locators[sel]

@pytest.mark.asyncio
async def test_input_text_large_concurrent_ai_calls():
    """Test multiple concurrent AI input calls."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    tasks = [
        skyvern_page._input_text(selector="input", value=f"v{i}", ai="proactive", intention=f"intent{i}")
        for i in range(20)
    ]
    results = await asyncio.gather(*tasks)

# 4. Throughput Test Cases

@pytest.mark.asyncio
async def test_input_text_throughput_small_load():
    """Throughput: Test small load of _input_text calls."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    results = await asyncio.gather(
        *(skyvern_page._input_text(selector="input", value=f"load{i}") for i in range(5))
    )
    locator = page.locators["input"]

@pytest.mark.asyncio
async def test_input_text_throughput_medium_load():
    """Throughput: Test medium load of _input_text calls."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    results = await asyncio.gather(
        *(skyvern_page._input_text(selector=f"input{i}", value=f"medium{i}") for i in range(50))
    )
    for i in range(50):
        locator = page.locators[f"input{i}"]

@pytest.mark.asyncio
async def test_input_text_throughput_high_volume():
    """Throughput: Test high volume of _input_text calls."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    N = 100
    results = await asyncio.gather(
        *(skyvern_page._input_text(selector=f"input{i}", value=f"hv{i}") for i in range(N))
    )
    for i in range(N):
        locator = page.locators[f"input{i}"]

@pytest.mark.asyncio
async def test_input_text_throughput_ai_high_volume():
    """Throughput: Test high volume of AI input calls."""
    browser = DummyBrowser()
    page = DummyPage()
    skyvern_page = SkyvernBrowserPage(browser, page)
    N = 100
    tasks = [
        skyvern_page._input_text(selector="input", value=f"ai{i}", ai="proactive", intention=f"intent{i}")
        for i in range(N)
    ]
    results = await asyncio.gather(*tasks)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
import asyncio  # used to run async functions
# SkyvernBrowserPage._input_text implementation (EXACTLY as provided)
from typing import Any

import pytest  # used for our unit tests
from skyvern.library.skyvern_browser_page import SkyvernBrowserPage

# Mocks for Playwright Page, Locator, and SdkSkyvernPageAi
class MockLocator:
    def __init__(self):
        self.filled = []
        self.typed = []
    async def fill(self, text, timeout=None):
        self.filled.append((text, timeout))
    async def type(self, char, delay=None, timeout=None):
        self.typed.append((char, delay, timeout))

class MockPage:
    def __init__(self):
        self.locators = {}
    def locator(self, selector):
        # Return a new locator for each selector
        locator = MockLocator()
        self.locators[selector] = locator
        return locator
    @property
    def url(self):
        return "http://test-url"

class MockSdkSkyvernPageAi:
    def __init__(self, browser, page):
        self.calls = []
    async def ai_input_text(self, selector, value, intention, data=None, totp_identifier=None, totp_url=None, timeout=None):
        self.calls.append((selector, value, intention, data, totp_identifier, totp_url, timeout))
        # Simulate returning a string based on intention
        return f"ai:{intention}:{value}"

class MockBrowser:
    def __init__(self):
        self.sdk = self
        self.client = self
        self.browser_session_id = "bsid"
        self.browser_address = "baddr"
        self.workflow_run_id = "wrid"
    async def ensure_has_server(self):
        return
    async def run_sdk_action(self, url, action, browser_session_id=None, browser_address=None, workflow_run_id=None):
        class Response:
            workflow_run_id = "new_wrid"
            result = f"ai_action:{action.value}"
        return Response()

# Mock settings
class settings:
    BROWSER_ACTION_TIMEOUT_MS = 1000
from skyvern.library.skyvern_browser_page import SkyvernBrowserPage

# ----------------------- UNIT TESTS -----------------------

# 1. Basic Test Cases

@pytest.mark.asyncio
async def test_input_text_basic_fill_and_type():
    """Test basic input with selector and value, no AI, fallback mode."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    result = await sky_page._input_text(selector="#input", value="hello")
    locator = page.locators["#input"]

@pytest.mark.asyncio
async def test_input_text_basic_fill_and_type_long_text():
    """Test input with long value triggers fill then type for last chars."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    text = "abcdefghij"  # 10 chars, TEXT_PRESS_MAX_LENGTH=5
    result = await sky_page._input_text(selector="#input", value=text)
    locator = page.locators["#input"]

@pytest.mark.asyncio
async def test_input_text_basic_ai_input_text_fallback():
    """Test fallback to AI input when intention is provided."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    result = await sky_page._input_text(selector="#input", value="foo", intention="test-intent")

@pytest.mark.asyncio
async def test_input_text_basic_ai_input_text_proactive():
    """Test proactive AI input when ai='proactive' and intention is provided."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    result = await sky_page._input_text(selector="#input", value="foo", ai="proactive", intention="test-intent")

@pytest.mark.asyncio
async def test_input_text_basic_return_value_when_no_selector_and_no_intention():
    """Test returns value if no selector and no intention."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    result = await sky_page._input_text(selector=None, value="bar")

# 2. Edge Test Cases

@pytest.mark.asyncio
async def test_input_text_edge_no_selector_raises():
    """Test raises ValueError if selector is required but not provided (ai != fallback/proactive)."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    # ai is not fallback or proactive, so selector required
    with pytest.raises(ValueError):
        await sky_page._input_text(selector=None, value="bar", ai="other")

@pytest.mark.asyncio
async def test_input_text_edge_locator_exception_fallback_to_ai():
    """Test fallback to AI input if locator raises exception and intention is provided."""
    class BadPage(MockPage):
        def locator(self, selector):
            raise RuntimeError("bad locator")
    page = BadPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    result = await sky_page._input_text(selector="#bad", value="baz", intention="intent")

@pytest.mark.asyncio
async def test_input_text_edge_locator_exception_raises_if_no_intention():
    """Test raises original exception if locator fails and no intention."""
    class BadPage(MockPage):
        def locator(self, selector):
            raise RuntimeError("bad locator")
    page = BadPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    with pytest.raises(RuntimeError):
        await sky_page._input_text(selector="#bad", value="baz")

@pytest.mark.asyncio
async def test_input_text_edge_empty_string_value():
    """Test inputting empty string value."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    result = await sky_page._input_text(selector="#input", value="")
    locator = page.locators["#input"]

@pytest.mark.asyncio

async def test_input_text_large_scale_concurrent_calls():
    """Test multiple concurrent _input_text calls with different selectors and values."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    selectors = [f"#input{i}" for i in range(10)]
    values = [chr(65+i)*6 for i in range(10)]  # Each value is 6 repeated chars
    coros = [sky_page._input_text(sel, val) for sel, val in zip(selectors, values)]
    results = await asyncio.gather(*coros)
    # Each locator should have filled first char and typed last 5 chars
    for i, sel in enumerate(selectors):
        locator = page.locators[sel]

@pytest.mark.asyncio
async def test_input_text_large_scale_concurrent_ai_calls():
    """Test multiple concurrent _input_text calls using AI input."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    coros = [sky_page._input_text("#input", f"val{i}", ai="proactive", intention=f"intent{i}") for i in range(10)]
    results = await asyncio.gather(*coros)

# 4. Throughput Test Cases

@pytest.mark.asyncio
async def test_input_text_throughput_small_load():
    """Throughput: Test _input_text performance under small load (10 calls)."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    coros = [sky_page._input_text(f"#input{i}", f"data{i}") for i in range(10)]
    results = await asyncio.gather(*coros)

@pytest.mark.asyncio
async def test_input_text_throughput_medium_load():
    """Throughput: Test _input_text performance under medium load (50 calls)."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    coros = [sky_page._input_text(f"#input{i}", f"data{i}") for i in range(50)]
    results = await asyncio.gather(*coros)

@pytest.mark.asyncio
async def test_input_text_throughput_high_volume():
    """Throughput: Test _input_text performance under high volume (100 calls)."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    coros = [sky_page._input_text(f"#input{i}", f"data{i}") for i in range(100)]
    results = await asyncio.gather(*coros)

@pytest.mark.asyncio
async def test_input_text_throughput_ai_high_volume():
    """Throughput: Test AI input performance under high volume (100 calls)."""
    page = MockPage()
    browser = MockBrowser()
    sky_page = SkyvernBrowserPage(browser, page)
    coros = [
        sky_page._input_text(
            f"#input{i}", f"data{i}", ai="proactive", intention=f"intent{i}"
        )
        for i in range(100)
    ]
    results = await asyncio.gather(*coros)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-SkyvernBrowserPage._input_text-mi6t7h6l and push.

Codeflash Static Badge

The optimization achieves a **10% runtime improvement** by eliminating character-by-character typing overhead in the `input_sequentially` function. The key change replaces the character iteration loop with bulk text input operations.

**What changed:**
- **Bulk typing for short text**: Instead of iterating through each character with `await locator.type(char, ...)`, short texts (≤ TEXT_PRESS_MAX_LENGTH) are now typed in one call with `await locator.type(text, ...)`.
- **Streamlined long text handling**: For longer texts, the remainder portion is also typed in bulk rather than character-by-character.

**Why it's faster:**
The original approach required **1,926 individual `await locator.type()` calls** (visible in line profiler), consuming 63.8% of the function's runtime. Each await involves async overhead, context switching, and browser communication. The optimized version reduces this to just **1-2 bulk operations**, eliminating the loop entirely for most cases.

**Performance impact:**
- Line profiler shows the optimized `input_sequentially` function dropped from 2.16ms to 0.48ms total time
- The character iteration loop that dominated 63.8% of runtime is completely eliminated for short texts
- Test results show consistent improvements across all text input scenarios, from basic cases to high-volume concurrent operations

**Real-world benefits:**
This optimization particularly benefits web automation workflows where text input is frequent, as it reduces the cumulative overhead of multiple small browser interactions into fewer, more efficient bulk operations. The 10% improvement compounds when `input_sequentially` is called repeatedly during form filling or data entry tasks.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 November 20, 2025 02:25
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant