当前位置: 首页 > news >正文

sigwaittest测试超标的调试过程

1,问题描述

硬件环境:飞腾S2500(64核)

OS:kylinOS, linux preempt rt, 4.19.90

测试命令:sigwaittest -p 90 -i 1000 -a 1

测试结果:信号混洗值最大超过了80us,与飞腾其他CPU的设备相比较,增大了很多

2,调试过程

1)日志分析

使用trace-cmd命令来抓取ftrace log:trace-cmd start -e all; sigwaittest -p 90 -i 1000 -a 1 -b 80

得到如下日志:

sigwaitt-13091   1....0  1741.963614: sys_enter:            NR 131 (3321, 3322, a, 20c49ba5e353f7cf, 3b9ac9ff, 44cc0)
sigwaitt-13091   1...10  1741.963615: sys_enter_tgkill:     tgid: 0x00003321, pid: 0x00003322, sig: 0x0000000a
sigwaitt-13091   1d..11  1741.963616: sched_waking:         comm=sigwaittest pid=13090 prio=9 target_cpu=001
sigwaitt-13091   1d..21  1741.963617: sched_wakeup:         sigwaittest:13090 [9] success=1 CPU:001
sigwaitt-13091   1....1  1741.963618: signal_generate:      sig=10 errno=0 code=-6 comm=sigwaittest pid=13090 grp=0 res=0
=====chensong:70us
sigwaitt-13091   1d..21  1741.963689: sched_waking:         comm=sigwaittest pid=13089 prio=120 target_cpu=041
sigwaitt-13091   1d..31  1741.963690: sched_wakeup:         sigwaittest:13089 [120] success=1 CPU:041
=====
sigwaitt-13091   1....0  1741.963691: kfree:                (__audit_syscall_exit+0x1d8) call_site=ffff0000081d26b0 ptr=(nil)
sigwaitt-13091   1....0  1741.963691: sys_exit:             NR 131 = 0
sigwaitt-13091   1...10  1741.963692: sys_exit_tgkill:      0x0

对比正常时候的日志:

sigwaitt-13091   1....0  1741.962569: sys_enter:            NR 131 (3321, 3322, a, 20c49ba5e353f7cf, 3b9ac9ff, 44cc0)
sigwaitt-13091   1...10  1741.962570: sys_enter_tgkill:     tgid: 0x00003321, pid: 0x00003322, sig: 0x0000000a
sigwaitt-13091   1d..11  1741.962571: sched_waking:         comm=sigwaittest pid=13090 prio=9 target_cpu=001
sigwaitt-13091   1d..21  1741.962572: sched_wakeup:         sigwaittest:13090 [9] success=1 CPU:001
sigwaitt-13091   1....1  1741.962573: signal_generate:      sig=10 errno=0 code=-6 comm=sigwaittest pid=13090 grp=0 res=0
sigwaitt-13091   1....0  1741.962574: kfree:                (__audit_syscall_exit+0x1d8) call_site=ffff0000081d26b0 ptr=(nil)
sigwaitt-13091   1....0  1741.962574: sys_exit:             NR 131 = 0
sigwaitt-13091   1...10  1741.962574: sys_exit_tgkill:      0x0
sigwaitt-13091   1....0  1741.962575: sys_enter:            NR 137 (fffdf677e848, fffdf677e6b8, 0, 8, 0, fffdf677f0e0)
sigwaitt-13091   1...10  1741.962575: sys_enter_rt_sigtimedwait: uthese: 0xfffdf677e848, uinfo: 0xfffdf677e6b8, uts: 0x00000000, sigsetsize: 0x00000008
sigwaitt-13091   1d..10  1741.962576: rcu_utilization:      Start context switch
sigwaitt-13091   1d..10  1741.962577: rcu_utilization:      End context switch
sigwaitt-13091   1d..20  1741.962578: sched_switch:         sigwaittest:13091 [9] S ==> sigwaittest:13090 [9]

我们会发现,在signal_generate后,sigwaittest的测试线程又唤醒了一个线程,消耗了70us。

2)过程分析

命令“sigwaittest -p 90 -i 1000 -a 1 -b 80”会创建三个线程,一个主线程main thread,,是一个CFS的普通线程,很大一部分工作是打印测试的结果,一个是sender,是一个实时线程,负责发送信号,还有一个是receiver,负责接收信号,sigwaittest主要就是测试sender发送信号到receiver接收到信号所花费的时间。

3)代码分析

首先看一下发送信号的kill函数,对应内核中系统调用tgkill:

使用function_grath来看一下tgkill的调用过程:trace-cmd start -p function_graph -g sys_tgkill;sigwaittest -p 90 -i 1000 -a 1 -l 100;trace-cmd stop

# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |1)               |  sys_tgkill() {1)               |    do_tkill() {1)               |      __task_pid_nr_ns() {1)   0.770 us    |        __rcu_read_lock();1)   0.500 us    |        __rcu_read_unlock();1)   3.062 us    |      }1)               |      from_kuid_munged() {1)   0.500 us    |        map_id_up();1)   1.563 us    |      }1)               |      do_send_specific() {1)   0.479 us    |        __rcu_read_lock();1)   0.666 us    |        find_task_by_vpid();1)               |        __task_pid_nr_ns() {1)   0.500 us    |          __rcu_read_lock();1)   0.479 us    |          __rcu_read_unlock();1)   2.542 us    |        }1)               |        check_kill_permission() {1)               |          audit_signal_info() {1)               |            auditd_test_task() {1)   0.500 us    |              __rcu_read_lock();1)   0.479 us    |              __rcu_read_unlock();1)   2.604 us    |            }1)               |            audit_signal_info_syscall() {1)   0.500 us    |              __rcu_read_lock();1)   0.500 us    |              __rcu_read_unlock();1)   2.458 us    |            }1)   7.042 us    |          }1)   0.604 us    |          security_task_kill();1)   9.271 us    |        }1)               |        do_send_sig_info() {1)               |          __lock_task_sighand() {1)   0.500 us    |            __rcu_read_lock();1)               |            rt_spin_lock() {1)   0.500 us    |              __rcu_read_lock();1)   0.500 us    |              migrate_disable();1)   2.480 us    |            }1)   0.500 us    |            __rcu_read_unlock();1)   5.459 us    |          }1)               |          send_signal() {1)   0.563 us    |            siginfo_layout();1)   0.479 us    |            __rcu_read_lock();1)   0.500 us    |            __rcu_read_lock();1)   0.500 us    |            __rcu_read_unlock();1)   0.500 us    |            __rcu_read_unlock();1)   0.563 us    |            task_active_pid_ns();1)               |            __task_pid_nr_ns() {1)   0.500 us    |              __rcu_read_lock();1)   0.480 us    |              __rcu_read_unlock();1)   2.479 us    |            }1)               |            __send_signal() {1)   0.583 us    |              prepare_signal();1)               |              __sigqueue_do_alloc() {1)   0.563 us    |                __rcu_read_lock();1)   0.480 us    |                __rcu_read_unlock();1)               |                kmem_cache_alloc() {1)   0.479 us    |                  should_failslab();1)   1.708 us    |                }1)   4.833 us    |              }1)               |              complete_signal() {1)   0.604 us    |                _raw_spin_lock_irqsave();1)   0.500 us    |                _raw_spin_unlock_irqrestore();1)               |                signal_wake_up_state() {1)               |                  wake_up_state() {1)               |                    try_to_wake_up() {1)   0.562 us    |                      _raw_spin_lock_irqsave();1)   0.542 us    |                      _raw_spin_lock();1)   0.541 us    |                      update_rq_clock();1)               |                      ttwu_do_activate() {1)               |                        activate_task() {1)               |                          enqueue_task_rt() {1)               |                            dequeue_rt_stack() {1)   0.521 us    |                              dequeue_top_rt_rq();1)   1.521 us    |                            }1)   0.500 us    |                            update_rt_migration();1)   0.500 us    |                            _raw_spin_lock();1)   0.521 us    |                            enqueue_top_rt_rq();1)   5.604 us    |                          }1)   6.958 us    |                        }1)               |                        ttwu_do_wakeup() {1)               |                          check_preempt_curr() {1)   0.625 us    |                            check_preempt_curr_rt();1)   1.625 us    |                          }1)   0.521 us    |                          task_woken_rt();1)   4.605 us    |                        }1) + 13.084 us   |                      }1)   0.541 us    |                      _raw_spin_unlock_irqrestore();1) + 19.104 us   |                    }1) + 20.146 us   |                  }1) + 21.188 us   |                }1) + 24.542 us   |              }1) + 32.709 us   |            }1) + 42.792 us   |          }1)               |          rt_spin_unlock() {1)   0.563 us    |            migrate_enable();1)   0.521 us    |            __rcu_read_unlock();1)   2.750 us    |          }1) + 53.271 us   |        }1)   0.541 us    |        __rcu_read_unlock();1) + 70.500 us   |      }1) + 77.562 us   |    }1) + 81.333 us   |  }------------------------------------------1) sigwait-31931  => sigwait-31930 ------------------------------------------

简化一下调用过程就是:

tgkill--> do_tkill-->do_send_specific-->do_send_sig_info--> lock_task_sighand //申请锁tsk->sighand->siglocksend_signal //唤醒receiverunlock_task_sighand(p, &flags); //释放tsk->sighand->siglock

在通常情况下,sender调用send_signal唤醒receiver,这个过程就结束了。但在发生错误的日志中,我们发现sender还唤醒了main thread,那么,很可能main thread也在申请tsk->sighand->siglock,这个时候它正在siglock的等待队列中等待,那么,当sender调用unlock_task_sighand的时候,就会去唤醒main thread。

我们再来看看main thread的代码:

26     while (!mustshutdown) {
527         int printed;
528         int errorlines = 0;
529 
530         for (i = 0; i < num_threads; i++)
531             mustshutdown |= receiver[i].shutdown |
532                 sender[i].shutdown;
533 
534         if (receiver[0].samples > oldsamples || mustshutdown) {...//打印结果581         pthread_sigmask(SIG_SETMASK, &sigset, NULL);
582 
583         nanosleep(&maindelay, NULL);
584 
585         sigemptyset(&sigset);
586         pthread_sigmask(SIG_SETMASK, &sigset, NULL);}其中pthread_sigmask(SIG_SETMASK, &sigset, NULL)会进入内核调用:
2870 int sigprocmask(int how, sigset_t *set, sigset_t *oldset)
2871 {
2872     struct task_struct *tsk = current;
2873     sigset_t newset;
2874 
2875     /* Lockless, only current can change ->blocked, never from irq */
2876     if (oldset)
2877         *oldset = tsk->blocked;
2878 
2879     switch (how) {
2880     case SIG_BLOCK:
2881         sigorsets(&newset, &tsk->blocked, set);
2882         break;
2883     case SIG_UNBLOCK:
2884         sigandnsets(&newset, &tsk->blocked, set);
2885         break;
2886     case SIG_SETMASK:
2887         newset = *set;
2888         break;
2889     default:
2890         return -EINVAL;
2891     }
2892 
2893     __set_current_blocked(&newset);
2894     return 0;
2895 }
在函数 __set_current_blocked(&newset)中,也需要申请tsk->sighand->siglock
2846 void __set_current_blocked(const sigset_t *newset)
2847 {
2848     struct task_struct *tsk = current;
2849 
2850     /*
2851      * In case the signal mask hasn't changed, there is nothing we need
2852      * to do. The current->blocked shouldn't be modified by other task.
2853      */
2854     if (sigequalsets(&tsk->blocked, newset))
2855         return;
2856 
2857     spin_lock_irq(&tsk->sighand->siglock);
2858     __set_task_blocked(tsk, newset);
2859     spin_unlock_irq(&tsk->sighand->siglock);
2860 }

所形成的关系大概是这样

sender                                          main threadlock_task_sighandsend_signal(sig, info, p, type)         spin_lock_irq(&tsk->sighand->siglock) //被阻塞unlock_task_sighand(p, &flags)           获取锁,继续调用sigprocmask其他的事情 __set_task_blocked(tsk, newset); spin_unlock_irq(&tsk->sighand->siglock);//释放锁      

本来这个锁的释放,唤醒进程都是很简短的过程,通常都是几微秒,为什么这个设备上会出现70us的问题呢,我们看sender和receiver都运行在CPU1上,而main thread是运行在CPU41上,是不是不在一个numa node上,对远端的内存访问会消耗很长时间?

3, 解决方案:将main thread与sender,receiver放到同一个node上

taskset -c 2 ./sigwaittest -p 90 -i 1000 -a 1 -b 80  //不再重现

如果将main thread强制放在CPU41上呢:

taskset -c 41 ./sigwaittest -p 90 -i 1000 -a 1 -b 80 // 很快重现

相关文章:

sigwaittest测试超标的调试过程

1&#xff0c;问题描述硬件环境&#xff1a;飞腾S2500&#xff08;64核&#xff09;OS&#xff1a;kylinOS, linux preempt rt&#xff0c; 4.19.90测试命令&#xff1a;sigwaittest -p 90 -i 1000 -a 1测试结果&#xff1a;信号混洗值最大超过了80us&#xff0c;与飞腾其他CPU…...

Python进阶-----面对对象4.0(面对对象三大特征之--继承)

目录 前言&#xff1a; Python的继承简介 1.什么是继承 2.继承的好处 3.object类 继承的相关用法 1.继承的定义与法则 2.对继承的重写 3.&#xff08;单继承&#xff09;多层继承 4.多继承 5.多继承重写时调用父类方法 前言&#xff1a; 在讲之前&#xff0c;我想说说中…...

九龙证券|利好政策密集发布,机构扎堆看好的高增长公司曝光

新能源轿车销量和保有量快速增长&#xff0c;带来了充电桩商场的微弱需求。 日前&#xff0c;商务部部长王文涛表明&#xff0c;本年将在落实好方针的一起&#xff0c;活跃出台新方针办法&#xff0c;比方辅导当地展开新能源轿车下乡活动&#xff0c;优化充电等使用环境&#x…...

stm32CubeIDE FMC 驱动LCD(8080)

一&#xff0c;TFT屏硬件接口16位&#xff0c;80并口。二&#xff0c;FMC介绍。FSMC&#xff08;Flexible Static Memory Controller&#xff09;&#xff0c;译为灵活的静态存储控制器。STM32F1 系列芯片使用 FSMC 外设来管理扩展的存储器&#xff0c;它可以用于驱动包括 SRAM…...

Java 数据类型

数据类型用于对数据归类&#xff0c;以便开发者理解和操作。 基本数据类型 Java 确定了每种基本数据类型所占存储空间的大小&#xff0c;不会像其它语言那样随机器硬件架构的变化而变化&#xff0c;这使 Java 程序更具可移植性。 Java 中定义了如下的基本数据类型。 byte …...

Prometheus 监控云Mysql和自建Mysql(多实例)

本文您将了解到 Prometheus如何配置才能监控云Mysql(包括阿里云、腾讯云、华为云)和自建Mysql。 Prometheus 提供了很多种Exporter&#xff0c;用于监控第三方系统指标&#xff0c;如果没有提供也可以根据Exporter规范自定义Exporter。 本文将通过MySQL server exporter 来监控…...

Vue3中的h函数

文章目录简介简单使用参数使用计数器进阶使用函数组件插槽专栏目录请点击 简介 众所周知&#xff0c;vue内部构建的其实是虚拟DOM&#xff0c;而虚拟DOM是由虚拟节点生成的&#xff0c;实质上虚拟节点也就是一个js对象事实上&#xff0c;我们在vue中写的template,最终也是经过…...

阿尔法开发板 IMX6ULL 说明

一. IMX6ULL开发板 IMX6ULL开发板即正点原子的阿尔法(ALPHA)开发板&#xff0c;采用恩智浦芯片&#xff0c;cortex-A7架构的。 二. IM6ULL开发板说明 1. IO说明 对于IMX6ULL芯片&#xff0c;一个IO对应两个寄存器&#xff0c;第一个寄存器负责配置其复用功能&#xff0c;…...

Altium Designer19 #学习笔记# | 基础应用技巧汇总

全文目录一.元件符号库二.元件封装库1.AD09 集成元件库/封装库三.电路原理图1. 巧用查找"相似对象功能"1.1 查找相同元件1.2. 查找相同文本1.3. 查找相同网络 &#xff1a;E - S - C四.PCB原理图【AD PCB模式下的常用快捷键】PCB视图放大/缩小PCB视图左/右移动PCB切换…...

Python 元类编程实现一个简单的 ORM

概述 什么是ORM?    ORM全称“Object Relational Mapping”&#xff0c;即对象-关系映射&#xff0c;就是把关系数据库的一行映射为一个对象&#xff0c;也就是一个类对应一个表&#xff0c;这样&#xff0c;写代码更简单&#xff0c;不用直接操作SQL语句。 现在我们就要实…...

《C++ Primer Plus》第18章:探讨 C++ 新标准(7)

C11 新增的其他功能 C11 增加了很多功能&#xff0c;本书无法全面介绍&#xff1b;另外&#xff0c;本书编写期间&#xff0c;其中很多功能还未得到广泛实现。然而&#xff0c;有些功能有必要简要地介绍一下。 并行编程 当前&#xff0c;为提高计算机性能&#xff0c;增加处…...

Redis学习(二):Redis安装测试

概述 Redis是什么 Redis, Remote Dictionary Server, 即远程字典服务。免费开源的数据库。 由C语言编写&#xff0c;支持网络&#xff0c;可基于内存亦可持久化的日志型、KV数据库&#xff0c;并提供所种语言的API。 Redis能干嘛 用于内存存储&#xff0c;持久化。rdb、ao…...

Vector - CAPL - 简介及数据结构

对于想进入车载行业或者已经在车载行业工作的朋友对于CAPL这个词都会相当的熟悉&#xff0c;都知道他是做车载网络测试脚本的语言&#xff0c;并且跟C有点类似&#xff0c;但是它到底是什么呢&#xff1f;CAPL全称&#xff08;Communication Access Programming Language&#…...

20230304英语学习

What Would Happen if the Moon Disappeared Tomorrow? 如果明天月球消失了会怎样&#xff1f; The closest object to our planet, the Moon, may seem like Earth’s little sibling.Since its birth, the satellite has mostly just hung around, playing gravitational t…...

【基础算法】单链表的OJ练习(3) # 移除链表元素 # 相交链表 #

文章目录前言移除链表元素相交链表写在最后前言 本章的OJ练习也是相对简单的&#xff0c;只要能够理解解题的思路&#xff0c;并且依照这个思路能够快速的写出代码&#xff0c;我相信&#xff0c;你的链表水平已经足够了。 对于OJ练习&#xff08;2&#xff09; : ->传送门…...

【自用】SpringBoot项目通用类整理

文章目录全局Json序列化Controller日志切面全局异常拦截GlobalExceptionHandlerApiResultBusinessExceptionResponseEntityUtil全局返回体包装MP自动填充接口文档配置类自定义Async异步线程池本文主要整理各类项目中通用的配置类、工具类&#xff0c;便于复查自用。 全局Json序…...

动态规划法(总述)多阶段决策最优化问题

动态规划: 研究最优控制问题提出的 该问题有n个输入&#xff0c;问题的解由这n个输入组成&#xff0c;这个子集必须满足事先给定的条件&#xff0c;这些条件称为约束条件&#xff0c;满足约束条件的可行解可能不只有一个为了衡量可行解的优劣&#xff0c;通常以一些函数的形式&…...

MySQL跨服务器数据映射

MySQL跨服务器数据映射环境准备1. 首先是要查看数据库的federated引擎 开启/关闭 状态2. 打开任务管理器&#xff0c;并重启mysql服务3. 再次查看FEDERATED引擎状态&#xff0c;引擎已启动映射实现问题总结在日常的开发中经常进行跨数据库进行查询数据。 同服务器下跨数据库进…...

利用反射实现通过读取配置文件对类进行实例化-课后程序(JAVA基础案例教程-黑马程序员编著-第十二章-课后作业)

【案例12-3】&#xff1a;利用反射实现通过读取配置文件对类进行实例化 【案例介绍】 1.案例描述 现在有一个项目&#xff0c;项目中创建了一个Person类&#xff0c;在Person类中定义了一个sleep()方法。在工程中还定义了一个Student类继承Person类&#xff0c;在Student类中…...

1.2 CSS文本属性

CSS Text(文本)属性&#xff1a; 定义文本外观&#xff0c;颜色&#xff0c;装饰&#xff0c;缩进&#xff0c;行间距来修饰文本 文本样式 文本缩进 text-indent文本水平对齐方式&#xff1a;text-align文本修饰&#xff1a;text-decoration行高 line-height CSS文本颜色属性…...

Hirschmann RS20-0800M4M4SDAE工业以太网交换机

Hirschmann RS20-0800M4M4SDAE 工业以太网交换机产品特点&#xff1a;端口配置&#xff1a;共8个端口&#xff0c;含6个RJ45电口和2个ST光纤接口。端口速率&#xff1a;所有端口均为100Mbps快速以太网。光纤类型&#xff1a;2个光纤端口为多模、ST接头。管理类型&#xff1a;二…...

嘈杂工业场景下的自适应VAD与双码本声纹识别鉴权系统:基于端侧轻量化神经网络与向量量化(VQ)重构

在大型化工车间、能源集控中心以及金融极密隔离库房中&#xff0c;离线声纹识别是物理访问控制和身份安全核验的重要生物特征屏障。然而&#xff0c;在环境本底噪声高达80dB以上的恶劣工业场景下&#xff0c;常规的语音活动检测&#xff08;VAD&#xff09;会频繁误触&#xff…...

荣耀出征官方网站下载正版手游 翅膀养成细节玩法全方位讲解

玩荣耀出征的玩家都清楚&#xff0c;翅膀不仅是角色的颜值象征&#xff0c;更是提升整体战力的核心途径。很多新手玩家只顾着升级、刷装备&#xff0c;完全忽略翅膀养成&#xff0c;导致等级很高但战力始终上不去。还有不少玩家胡乱合成、盲目进阶&#xff0c;浪费了大量稀有翅…...

1901-2022年中国气温变化分析实战:用这份1km栅格数据我们能发现什么?

1901-2022年中国气温变化分析实战&#xff1a;如何从1km栅格数据中挖掘气候演变规律当一份覆盖122年、分辨率精确到1公里的气温栅格数据摆在面前时&#xff0c;我们看到的不仅是数字矩阵&#xff0c;更是一部写在经纬度坐标里的气候变迁史诗。这份由逐月数据聚合生成的逐年气温…...

GEO生成引擎优化:当AI成为信息分发的主角,品牌如何抢占对话窗口?

当用户不再"搜索-浏览"&#xff0c;而是直接"AI提问-获取答案"&#xff0c;传统SEO的逻辑正在被彻底改写。2026年&#xff0c;GEO&#xff08;Generative Engine Optimization&#xff0c;生成式引擎优化&#xff09;已经从概念走向规模化落地。本文从技术…...

CANoe诊断测试没CDD文件怎么办?手把手教你用Fault Memory窗口和CAPL脚本读取解析DTC故障码

CANoe诊断测试无CDD文件的实战解决方案&#xff1a;从Fault Memory到CAPL脚本全解析当CDD文件缺失或定义不清晰时&#xff0c;诊断测试工程师常常陷入困境。本文将深入探讨如何利用Fault Memory窗口的基础功能&#xff0c;并通过CAPL脚本实现更灵活、更强大的故障码读取与解析方…...

在Hermes Agent项目中接入Taotoken作为自定义模型供应商

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 在Hermes Agent项目中接入Taotoken作为自定义模型供应商 基础教程类&#xff0c;针对使用Hermes Agent框架的开发者&#xff0c;详…...

车载诊断系统(OBD)的原理、演进与未来

本文约8,167字&#xff0c;建议收藏阅读 作者 | 北湾南巷 出品 | 汽车电子与软件 引 言 在现代汽车中&#xff0c;越来越多的故障不再表现为明显的机械损坏&#xff0c;而是以“亮灯”“报码”“性能异常”等电子信号的形式出现。发动机为什么亮起故障灯&#xff1f;排放是否达…...

接口测试用例设计:超详细防御体系与分层校验实践

1. 为什么“超详细”三个字在接口测试用例里不是修饰词&#xff0c;而是生死线我带过三支不同行业的测试团队——金融支付、SaaS中台、IoT设备管理平台。每次新人入职第一周&#xff0c;我都会收走他们写的前5条接口测试用例&#xff0c;逐行标红批注。不是因为格式不对&#x…...

告别Selenium?手把手教你用Playwright录制脚本,5分钟搞定Web自动化测试

5分钟极速上手Playwright脚本录制&#xff1a;零代码实现Web自动化测试当产品经理突然丢给你一个刚上线的电商活动页&#xff0c;要求半小时内完成所有核心链路测试时&#xff0c;传统的手写Selenium脚本显然来不及。作为测试工程师&#xff0c;我最近发现微软开源的Playwright…...