代码改变世界

UI自动化脚本并发策略的性能比对

2025-04-30 11:06  第二个卿老师  阅读(72)  评论(0)    收藏  举报

背景

之前对网页有写多用户访问的测试需求,在Selenium的UI自动化中,一直想知道pyhton中使用多线程、线程池、多进程、进程池的性能差异,看到了一篇文章:【Selenium】提高测试&爬虫效率:Selenium与多线程的完美结合,自己准备实验一下看看

方案对比

首先通过业界实践可知

方案 适合场景 特点
多线程 少量并发,轻量测试 简单,GIL限制
多进程 中等并发,真实并行 内存高,启动慢
线程池 控制线程数量 适合小型稳定场景
进程池 控制进程数量 稳定,资源吃紧
分布式 Grid 大量分布式并发 扩展性最强

测试场景

测试的场景是在本地单机上访问某个页面,访问后直接睡眠3s,以屏蔽其他影响因素,然后统计各个并发策略的执行时间,CPU时间,内存增量。测试策略考虑了4个并发方案,也考虑了浏览器中无头与有头模式、多浏览器与多标签页的加载方式

  1. 多浏览器下,有头模式下4个并发方案对比(多线程、线程池、多进程、进程池)
  2. 多浏览器下,无头模式下4个并发方案对比(多线程、线程池、多进程、进程池)
  3. 多标签页下,有头模式下2个并发方案对比(多线程、线程池)
  4. 多标签页下,有头模式下2个并发方案对比(多线程、线程池)

注:Selenium WebDriver本身并非线程安全的设计,多进程或进程池模式下,无法也不建议共享同一个WebDriver driver,所以仅使用多线程或线程池

测试结果

以下是使用ChromeDrivere,并发数为3的执行结果。

  • 多浏览器下,有头模式下4个并发方案对比
    image

  • 多浏览器下,无头模式下4个并发方案对比
    image

  • 多标签页下,有头模式下2个并发方案对比
    image

  • 多标签页下,无头模式下2个并发方案对比
    image

注:报告中,实时用时是程序从开始到结束所经过的真实时间,CPU时间是进程消耗的CPU周期所用总时长,内存增量表示进程所占用的物理RAM(不包括被换出的部分)

测试小结

由于环境差异与测量盲区,也试着多次运行,总体结论如下:

  • 根据实时用时(s) / CPU 时间(s)远大于1,说明Selenium任务主要是IO密集型任务
  • IO密集型任务,多进程比多线程的运行效率提高不了多少,包括进程池与线程池的效率也类似
  • IO密集型任务,线程池比多线程运行效率提高不了多少,但是线程池内存占用要小于多线程
  • 浏览器的无头模式执行效率要高些,有头模式下,多标签与多浏览器的运行效率相差无几

综上所示,基本验证了方案对比的结论,在少量并发下,根据个人喜好使用并发方案,在大量并发下,大家可以试试测试结果(奈何我的本地机太垃圾),我把测试代码贴在后面,然后内存这块目前只对多线程与线程池有效,感兴趣的可以扩展多进程与进程池的,以及分布式Grid模式。

测试代码

# pip install selenium psutil
import sys
import argparse
import time
import os
import psutil
import threading
from multiprocessing import Process
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

URL = "https://www.baidu.com/"

def create_driver(headless=True, remote_url=None):
    opts = Options()
    if headless:
        opts.add_argument('--headless')
        opts.add_argument('--disable-gpu')
        opts.add_argument('--no-sandbox')
    opts.add_argument('--window-size=1920,1080')
    opts.add_argument('--disable-dev-shm-usage')
    if remote_url:
        return webdriver.Remote(command_executor=remote_url, options=opts)
    else:
        return webdriver.Chrome(options=opts)

def visit_site(idx, headless=True, remote_url=None):
    """单个任务:访问页面并等待 2s"""
    driver = create_driver(headless, remote_url)
    try:
        driver.get(URL)
        time.sleep(2)
    finally:
        driver.quit()

# === 新增:多标签页模式共用 driver 和锁 ===
_tab_lock = threading.Lock()

def visit_in_tab(idx, driver):
    """在已有 driver 上打开新标签页并访问"""
    with _tab_lock:
        driver.switch_to.new_window('tab')
        tab_handle = driver.current_window_handle
    try:
        driver.get(URL)
        time.sleep(2)
    finally:
        # 关闭当前标签页,不影响主窗口
        driver.close()
        # 切回原始窗口
        with _tab_lock:
            driver.switch_to.window(driver.window_handles[0])

def run_threads(n, headless, remote_url=None, use_tabs=False):
    if use_tabs:
        # 先创建单个 driver
        driver = create_driver(headless, remote_url)
        threads = []
        for i in range(n):
            t = threading.Thread(target=visit_in_tab, args=(i, driver))
            t.start()
            threads.append(t)
        for t in threads:
            t.join()
        driver.quit()
    else:
        # 原有多线程、每线程独立 driver
        threads = []
        for i in range(n):
            t = threading.Thread(target=visit_site, args=(i, headless, remote_url))
            t.start()
            threads.append(t)
        for t in threads:
            t.join()

def run_processes(n, headless, remote_url=None):
    # 进程模式不支持 tabs,保持不变
    procs = []
    for i in range(n):
        p = Process(target=visit_site, args=(i, headless, remote_url))
        p.start()
        procs.append(p)
    for p in procs:
        p.join()

def run_thread_pool(n, headless, remote_url=None, use_tabs=False):
    if use_tabs:
        driver = create_driver(headless, remote_url)
        with ThreadPoolExecutor(max_workers=n) as exe:
            for i in range(n):
                exe.submit(visit_in_tab, i, driver)
        driver.quit()
    else:
        with ThreadPoolExecutor(max_workers=n) as exe:
            for i in range(n):
                exe.submit(visit_site, i, headless, remote_url)

def run_process_pool(n, headless, remote_url=None):
    # 进程池模式不支持 tabs,保持不变
    with ProcessPoolExecutor(max_workers=n) as exe:
        for i in range(n):
            exe.submit(visit_site, i, headless, remote_url)

def run_grid_threads(n, headless, remote_url):
    # Grid 不支持 tabs,保持原有分布式多线程调用
    run_threads(n, headless, remote_url)

def measure(func, *args, **kwargs):
    """测时、测 CPU 时间、测内存增量"""
    p = psutil.Process(os.getpid())
    mem_before = p.memory_info().rss
    cpu_before = sum(p.cpu_times()[:2])
    t0 = time.perf_counter()
    func(*args, **kwargs)
    t1 = time.perf_counter()
    cpu_after = sum(p.cpu_times()[:2])
    mem_after = p.memory_info().rss
    return {
        'elapsed_s': t1 - t0,
        'cpu_s': cpu_after - cpu_before,
        'mem_diff_mb': (mem_after - mem_before) / (1024 * 1024)
    }

def compare_modes(n, headless, use_tabs=False):
    # 根据 tabs 模式选择对比项
    if use_tabs:
        modes = {
            'thread-tabs': lambda: run_threads(n, headless, None, use_tabs=True),
            'threadpool-tabs': lambda: run_thread_pool(n, headless, None, use_tabs=True)
        }
    else:
        modes = {
            'thread': lambda: run_threads(n, headless, None, use_tabs=False),
            'process': lambda: run_processes(n, headless, None),
            'threadpool': lambda: run_thread_pool(n, headless, None, use_tabs=False),
            'processpool': lambda: run_process_pool(n, headless, None)
        }

    results = {}
    for name, fn in modes.items():
        print(f"\n>> 测试模式: {name}")
        res = measure(fn)
        print(f"   用时: {res['elapsed_s']:.2f}s, CPU: {res['cpu_s']:.2f}s, 内存变化: {res['mem_diff_mb']:.1f}MB")
        results[name] = res

    # 打印 Markdown 表格
    print("\n## 并发策略性能对比报告")
    print("| 模式 | 实时用时(s) | CPU 时间(s) | 内存增量(MB) |")
    print("|:----|:----------:|:-----------:|:------------:|")
    for name, r in results.items():
        print(f"| {name} | {r['elapsed_s']:.2f} | {r['cpu_s']:.2f} | {r['mem_diff_mb']:.1f} |")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--mode', choices=['thread','process','threadpool','processpool','grid'], default='thread')
    parser.add_argument('--compare', action='store_true', help="对比各模式性能")
    parser.add_argument('--tasks', type=int, default=3, help="并发任务数")
    parser.add_argument('--headless', action='store_true', help="启用无头模式")
    parser.add_argument('--remote_url', type=str, default="http://localhost:4444/wd/hub", help="Grid Hub 地址")
    parser.add_argument('--tabs', action='store_true', help="启用多标签页模式(仅线程/线程池支持)")  # 新增

    args = parser.parse_args()

    # 参数校验:tabs 模式下禁止 process/ processpool / grid
    if args.tabs and args.mode not in ['thread','threadpool']:
        parser.error("--tabs 模式下仅支持 --mode thread 或 threadpool")

    if args.compare:
        compare_modes(args.tasks, args.headless, use_tabs=args.tabs)
        return

    # 单模式执行
    if args.mode == 'thread':
        run_threads(args.tasks, args.headless, None, use_tabs=args.tabs)
    elif args.mode == 'process':
        run_processes(args.tasks, args.headless, None)
    elif args.mode == 'threadpool':
        run_thread_pool(args.tasks, args.headless, None, use_tabs=args.tabs)
    elif args.mode == 'processpool':
        run_process_pool(args.tasks, args.headless, None)
    elif args.mode == 'grid':
        run_grid_threads(args.tasks, args.headless, args.remote_url)
    else:
        raise ValueError("未知模式")

if __name__ == '__main__':
    # 命令行使用 python concurrent_test.py --compare --tasks 3 运行
    # 右键运行适配,如果没有任何参数,则补充默认值
    if len(sys.argv) == 1:
        # 3个并发,线程池模式
        # sys.argv += [
        #     '--mode', 'threadpool',
        #     '--tasks', '3',
        #     '--headless'
        # ]
        # 3个并发对比,有头模式
        sys.argv += [
            '--tasks', '3',
            '--compare',
        ]
        # 3个并发对比,无头模式
        # sys.argv += [
        #     '--tasks', '3',
        #     '--compare',
        #     # '--headless',
        # ]
        # 3个并发对比,无头模式且使用tab标签模式打开
        # sys.argv += [
        #     '--tasks', '3',
        #     '--compare',
        #     # '--headless',
        #     '--tabs',
        # ]
    main()