Files
MeowOS-SDK/sdk/CLOCK_ABI.md
T
AskaEth 27a3750e36 SDK: 同步主仓 M15/M16/bug3/MD3 修复 — 时钟 ABI 补全/IME 冷却窗/列表行高派生/形状 token/翻页键
公开 SDK 落后主仓的一批 bugfix(主仓是固件构建源,这些修复已在设备上跑过)。
本次只做「主仓 → 公开仓」单向同步,图像栈方向由主仓侧另一单处理。

时钟 ABI(M16 第二弹,原文件自认「残余未修」):
- meow_ui_time.c 147→345 行:futex 相对超时接管(__timedwait/_cp)、
  timerfd_create/settime/gettime 的 itimerspec 64↔32 双向转换、
  __clock_settime64 越界返 ENOTSUP,另有 4 个纯换算函数
- meow_ui_internal.h:struct meow_time32 / meow_itimerspec32 契约移入内部头
- CLOCK_ABI.md:补接管机制、验证配方与 objdump 证据
- 新增 test_meow_time64.c

IME bug3(成对才有意义):
- meow_ui_ime.c:IME_TAP_COOLDOWN_MS 200 冷却窗 + 打开时二次 drain
- meow_ui_input.c:meow_input_touch_reset() 并在 drain 内调用(复位触摸状态机)
- test_ime_fix.c:新增用例④ stray-tap

列表行高(M15,修用户可见 bug):
- meow_ui_widget.c:listv_item_h() 由主题字体行高派生,不再写死 56
  (两行列表 62 > 56 会让首行标题被裁剪守卫整条跳过)
- include/meow_ui_widgets.h:新增 meow_listview_item_h()
- 新增 test_meow_ui_listview_rowh.c

其余:meow_select.c / meow_ui_dialog.c 统一 MD3 形状 token;
include/meow_ipc.h 补 MSG_KEY_PAGEUP/PAGEDOWN 枚举;
meow_ui_sys.c / meow_cachegov.c 取时统一走 meow_ui_time_ms()。

测试辅助函数标定:tuku/test/test_settings_lock_host.c 的注入前等待 150ms→350ms。
该等待按修复前 IME 标定,新冷却窗下首键 dt_open≈114ms 会被当迟到抬起吞掉,
只录到 "68" 两位。仅改等待时长,断言原样保留。

验证:tuku 四套脚本全绿(run_tests 644 PASS / zip 106 / cbz 296 / webtoon PASS);
12 个源文件与主仓逐字节一致;sdk/build_sdk.sh 34 成员;
tuku/build_tuku.sh 产出静态 MIPS32r2 ELF。
2026-11-06 21:31:54 +08:00

319 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SDK 时钟 ABI 说明(meow_ui_time_ms 真机故障的根因与修复)
面向对象:维护 SDK / 工具链 / 重链 App 的人。**结论先行**:这不是 SDK 代码写错,
是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾;修复落在 SDK 链上
(`patch_v2/sdk/meow_ui_time.c` + 链接期强定义),改后需重编 `meow_ui.a` 并重链四 App。
## 1. 现象(真机)
M10 抓到的日志首列(`meow_ui_time_ms()`,64 位)可拆成:
```
6607363222717682072 → lo=1135000 hi=1538396632
8754846870717682072 → lo=1135000 hi=2038396632
18138485921460348192 → lo=1140000 hi=4223195352
```
- 低 32 位 = 开机秒数 × 1000,**恒为 1000 的倍数 → 毫秒位丢失**;
- 高 32 位每次调用随机、无单调性。
同进程另一份取时(旧容器编的 core)正常 → 问题只在「zig 0.17 编的那部分」。
## 2. 根因链(三段证据)
### 2.1 zig 头文件:`time_t` 是 64 位,`struct timespec` 16 字节
`<zig lib>/libc/include/mips-linux-musl/bits/alltypes.h`:
```c
#define _REDIR_TIME64 1
#define __USE_TIME_BITS64 1
typedef _Int64 time_t; /* 无条件 64 位 */
struct timespec { time_t tv_sec; ...; long tv_nsec; ... }; /* tv_sec@0(8B)、tv_nsec@8 */
```
`include/generic-musl/time.h` 因此在 `_REDIR_TIME64` 下把 `clock_gettime` 重定向到
`__clock_gettime64`。用该链编译的 C 代码一律按「16 字节、tv_sec 8 字节」取数
(`patch_v2/sdk/meow_ui.a(meow_ui_gfx.o)` 的旧 `meow_ui_time_ms` 反汇编:`lw 0x10/0x14`
取 tv_sec、`lw 0x18` 取 tv_nsec,正是这个布局)。
### 2.2 zig 的 clang 给 mipsel 预定义了 `__ILP32__`
```
$ echo | zig cc --target=mipsel-linux-musleabi -dM -E - | grep __ILP32__
#define __ILP32__ 1
```
(arm-linux-musleabi 同样为 1;x86-linux-musl / aarch64 为 0。容器里的 gcc 对 mips o32
**不定义** `__ILP32__`。)
### 2.3 musl 的 time64 回退分支被这句 `!__ILP32__` 整体编译掉
`<zig lib>/libc/musl/src/time/clock_gettime.c`:
```c
#if defined(SYS_clock_gettime64) && !defined(__ILP32__)
... __syscall(SYS_clock_gettime64, clk, ts); /* 4403 */
... 失败(ENOSYS) 再 __syscall(SYS_clock_gettime, clk, ts32); /* 4263 + 宽度转换 */
#else
r = __syscall(SYS_clock_gettime, clk, ts); /* 直接把 8 字节 time32 写进调用者缓冲 */
#endif
```
这句 `!__ILP32__` 本意只排除 x32(`__ILP32__` 是 x32 的标记),但 clang 把它当成
「32 位数据模型」的通名。于是 zig 的 mipsel libc 走进 `#else`:
`__clock_gettime64` 实际执行 **SYS_clock_gettime(4263)**,只写 8 字节
`{ int32 tv_sec; int32 tv_nsec; }`。
**证据(反汇编,当前 zig 0.17 静态链接的探针)**:
```
0002576c <__clock_gettime64>:
257c8: addiu $2,$zero,0x10a7 ; 4263 = SYS_clock_gettime(time32)
257cc: syscall ; $5 直接是调用者的 16 字节 struct 指针,无 4403 尝试、无转换
```
同一符号在**旧容器产物** `patch_v2/ncm_app/ncm`(已入库,gcc 构建)里是:
```
115748: addiu $2,$zero,0x1133 ; 4403 = SYS_clock_gettime64
11574c: syscall
...
115774: addiu $2,$zero,0x10a7 ; ENOSYS 后回退 4263,写入本地 ts32
...
; 再把 ts32 升成 64 位写回调用者(tv_sec=tv_sec、tv_nsec=tv_nsec)
```
两者对比即根因:**头文件 ABI(64 位 time_t)与 libc 实现 ABI(32 位 time32)不一致**。
### 2.4 数值解释(为什么「低 32 = 秒×1000、高 32 随机」)
libc 把 `{sec, nsec}` 当两个 int32 写进 16 字节缓冲的前 8 字节;SDK 按 64 位读:
```
tv_sec = sec | (nsec << 32) // 低字正确、高字=纳秒
tv_nsec = 缓冲偏移 8 的未初始化值 // 毫秒项≈0
result = tv_sec*1000 + tv_nsec/1e6
= sec*1000 + (nsec*1000 mod 2^32)<<32 + 垃圾/1e6
```
- 低 32 位只剩 `sec*1000`(nsec 项左移 32 位不落到低字)→ 恒为 1000 的倍数;
- 高 32 位 = `nsec*1000 mod 2^32` → 每次随机、不单调;
- `tv_nsec` 来自越界读 → 通常是小值/0(真机上恰好 0,所以没有额外尾数)。
## 3. 修复(`patch_v2/sdk/meow_ui_time.c`)
1. **取数走内核 o32 ABI**:`meow_ui_time_ms()` 用
`syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &{int32 sec; int32 nsec;})`,
再 `meow_time_ms_from_parts(sec, nsec)` 升成 64 位毫秒。完全不依赖 libc 头声明的
`struct timespec` 宽度 —— 即使将来 zig 修好(或换回容器链),内核 time32 入口始终存在。
2. **链接期修 libc 入口**:同一 TU 强定义 `__clock_gettime` / `__clock_gettime64`
(musl 的 `time()`、`gettimeofday()`、`__timedwait_cp()`(pthread 定时等待)内部都调
`__clock_gettime`)。应用链接顺序是 `... meow_ui.a ... -lc`,归档先解析符号,libc 里那份
坏实现(及其 VDSO 前导)不会被拉入 —— 用 `nm` 验证链接产物里没有 `cgt_init/__vdsosym`,
且 `__time64`/`__timedwait_cp` 的 `jal` 指向本 TU 的实现。
3. **睡眠入口一并修正(同源缺陷)**:`__nanosleep_time64` / `__clock_nanosleep(_time64)`
先按 time32 转换再发 `SYS_nanosleep`/`SYS_clock_nanosleep`,`rem` 再扩回 64 位。
修复前 zig libc 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 →
SDK 里 `usleep(16000)`(帧节拍、输入轮询、IME 冷却)全部变成忙等空转。
4. **去掉四处重复的 `now_ms()`**:`meow_ui_gfx.c` / `meow_ui_sys.c` / `meow_ui_input.c` /
`meow_cachegov.c` 统一调 `meow_ui_time_ms()`(cachegov 保留宿主测试注入桩)。
5. 构建脚本 `build_sdk.sh` 的 OBJS 增加 `meow_ui_time`;宿主单测 `test_meow_time.c`
固定换算边界与「单调且前进」;探针 `tools/clock_abi_probe.c` 用于宿主/真机一眼验收。
6. **M16 续修同源入口**(同一 TU,`#if MEOW_TIME32_KERNEL_ABI` 限定 mips o32):
`__timedwait_cp`/`__timedwait`(futex 相对超时)、`__timerfd_settime64`/
`__timerfd_gettime64`/`timerfd_create`、`__clock_settime64`。换算逻辑抽成平台无关的
`meow_time32_{rel,abs}_from_parts()` / itimerspec 转换(`meow_ui_internal.h`),
宿主 `test_meow_time64.c` 直接覆盖。清单、逐项证据与未修项配方见 §6/§7。
非 mips 目标(宿主单测、aarch64 宿主)走普通 `clock_gettime`,行为不变。
## 4. 验证
宿主:
```
cd patch_v2/sdk
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/meow_ui_time.o
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/meow_ui_time.o -o /tmp/test_meow_time
/tmp/test_meow_time # == PASS ==(换算边界 + 单调前进)
```
交叉:
```
bash patch_v2/sdk/build_sdk_host.sh --verify # 成员清单/架构自检 + 0 warning
llvm-objdump -d meow_ui.a(meow_ui_time.o) # meow_ui_time_ms: syscall 4263, lw 0x18 取 nsec、sra 扩展 tv_sec
```
链接期接管(用真实归档链一个探针,检查符号来源):
```
# 不应出现 libc 的 VDSO 前导(cgt_init/__vdsosym)→ libc 的取时实现未被拉入
strings -a clock_abi_probe_sdk | grep -c __vdso # 期望 0
llvm-nm clock_abi_probe_sdk | grep -E 'clock_gettime|nanosleep'
# __time64 / __timedwait_cp 的 jal 应指向本 TU 的 __clock_gettime;睡眠走 SYS_nanosleep(4166)/clock_nanosleep(4265)
```
探针 `tools/clock_abi_probe.c`(宿主 `gcc` 或 mipsel 交叉;加 `-DMEOW_PROBE_WITH_SDK`
并链 `meow_ui.a` 可同时验收 `meow_ui_time_ms`):把 `struct timespec` 预填 0xAA 后取时,
若偏移 8..11 仍是 0xAA 即说明 libc 只写了 8 字节(ABI 不一致),退出码 2。
真机(塔台批准后):日志首列应连续、出现非整千差;`meow_ui_time_ms()` 两次取样差 = 真实毫秒。
本次设备已离线,真机确认**待办**。
## 5. 影响面
- **SDK 内**:`meow_ui_time_ms()` 及其上层 —— 动画 tween(`meow_ui_anim.c`)、滚动/fling
(`meow_ui_scroll.c`/`meow_select.c`)、IME 冷却(`meow_ui_ime.c`)、状态栏动画
(`meow_ui_gfx.c`)、通知 TTL(`meow_ui_notify.c`)、输入层 tick/启动冷却
(`meow_ui_input.c`)、cachegov 回退窗口(`meow_cachegov.c`)、日志首列(`meow_ui_log.c`)。
- **App 侧**:`on_tick` 入参 `now`、`meow_anim_*`、以及任何自行调用 `meow_ui_time_ms()` 的
App(hello/reader/files/ncm…)。
- **产物**:`meow_ui.a` 变更 → 需**重编并重链四 App**(ncm/reader/files/hello);
`time()`/`gettimeofday()` 的修正也在同一份归档里生效(链接期覆盖)。
- **同步等待(M16)**:`pthread_cond_timedwait` / `sem_timedwait` / `pthread_join` 超时 /
`aio_suspend` / 非 PI 的 `pthread_mutex_timedlock`/rwlock timed 的**相对超时按真实剩余
时间**生效(不再「亚秒即超时」)。SDK 侧直接受益者是 `meow_ui_audio.c` 的 5 处条件等待
(:312/:351/:417/:431/:499)。
`timerfd_*`、`clock_settime` 一并修正(当前无调用方)。
- 真机写入红线未触碰;本单只改 SDK 源与文档/测试。
## 6. 同源入口全量清点(M16)
M16 把 zig 0.17 musl 源里所有 `#if defined(SYS_*_time64) && !defined(__ILP32__)` 守卫
逐条编译、反汇编核对(`zig cc --target=mipsel-linux-musleabi`),结论如下。
「回退分支」= `__ILP32__` 被定义时实际编译进去的代码。
### 6.1 受损(回退分支把 time64 结构直传 time32 系统调用)
| musl 源(守卫行) | 入口 | 回退分支实际行为 | 状态 |
| --- | --- | --- | --- |
| `thread/__timedwait.c:14` | futex 相对超时 → `pthread_cond_timedwait` / `sem_timedwait` / `pthread_join` 超时 / `aio_suspend` / 非 PI `pthread_mutex_timedlock` / rwlock timed | `__syscall_cp(SYS_futex=4238, …, to)`,`to` 是 16B time64 结构;内核只读前 8B:`tv_nsec` 落在 `tv_sec` 高半字(恒 0)→ 亚秒等待变 0(立即 ETIMEDOUT)、整秒等待丢纳秒 | **M16 已接管** |
| `linux/timerfd.c:14` | `timerfd_settime` | 32B itimerspec 直传 `SYS_timerfd_settime32=4323`;内核按 16B 读 → `value.tv_sec` 读到调用者的 `interval.tv_nsec`,`value.tv_nsec` 读到填充字节 | **M16 已接管** |
| `linux/timerfd.c:42` | `timerfd_gettime` | 4322 只写 16B,调用者按 32B 解 → 字段串位 | **M16 已接管** |
| `time/clock_settime.c:9` | `clock_settime` | 16B 直传 `SYS_clock_settime32=4262` → nsec 恒 0 | **M16 已接管** |
| `signal/sigtimedwait.c:10` | `sigtimedwait` | 16B 直传 `SYS_rt_sigtimedwait=4197` | 未修(SDK/App 未用,§7.3 配方) |
| `ipc/semtimedop.c:19` | `semtimedop` | 16B 直传 `SYS_ipc=4117`(`IPCOP_semtimedop`) | 未修(未用) |
| `time/timer_settime.c:13` | `timer_settime` | 32B itimerspec 直传 4258 | 未修(未用) |
| `time/timer_gettime.c:11` | `timer_gettime` | 4259 只写 16B | 未修(未用) |
| `stat/utimensat.c:15` | `utimensat`(`futimens` 同源) | `times[2]`(32B)直传 4316 | 未修(仅 ncm 宿主 bench/测试出现) |
| `network/recvmmsg.c:21` | `recvmmsg` | 16B timeout 直传 4335 | 未修(未用) |
| `mq/mq_timedsend.c:10` | `mq_timedsend` | 16B 直传 4273 | 未修(未用) |
| `thread/pthread_mutex_timedlock.c:8` | PI 互斥量的 timedlock 快路径 | 16B 直传 4238(`FUTEX_LOCK_PI`) | 未修(非 PI 走已修的 `__timedwait`;SDK/App 不用 PI 互斥量) |
唯一在用的受害者是 futex:`meow_ui_audio.c`(5 处,:312/:351/:417/:431/:499)、
`meow_audiod`、ncm 播放线程都用
`pthread_cond_timedwait`,修复前「剩 0.4s」会被内核当成 `{0,0}` 立即超时 → 音频环形缓冲
的等待退化成忙轮询。
### 6.2 不受影响(回退分支自己做了宽度转换)——纠正 M11 §6 的猜测
| musl 源 | 证据 |
| --- | --- |
| `select/pselect.c:15` | 回退分支 `syscall_cp(SYS_pselect6=4301, …, (long[]){s, ns}, …)`:o32 的 `long`=4B → 正好 8B time32 |
| `select/ppoll.c:14` | 同上(4302)。反汇编 `__ppoll_time64`:`lw 0x0/0x8` 读 16B 结构 → `sw` 成栈上两字 → `addiu $4,$zero,0x10ce`(4302) |
| `select/select.c:28` | mips 无 `SYS_select`,走 `(long[]){s,ns}` + 4301;同 pselect |
| `linux/clock_adjtime.c:40` | 回退分支保留 `if (sizeof(time_t) > sizeof(long))`,构造 long 版 `struct ktimex` |
| `misc/getrusage.c:9` | 同上宽度分支(`dest = &ru_maxrss - 4*sizeof(long)` + 回填) |
| `linux/wait4.c:11` | 同上宽度分支 |
M11 §6 把 pselect/ppoll 列为「未验证」是保守估计;本次逐条反汇编确认它们**本来就是对的**,
不需要接管(也就没有可修的低风险项)。
## 7. 接管机制与未修项配方(M16)
### 7.1 符号名不是 API 名:`_REDIR_TIME64` 把调用点改了名
zig 的 `alltypes.h` 无条件 `_REDIR_TIME64=1`,头文件里每个受影响 API 都有一行
`__REDIR(x, __x_time64)`(`generic-musl/*.h`)。应用里写 `timerfd_settime(...)`,编译出来
引用的是 `__timerfd_settime64`。所以接管必须定义**重定向后的符号**:
| API | 应用引用的符号 | libc 该 `.o` 还定义(必须一并接管,否则重复定义) |
| --- | --- | --- |
| `timerfd_settime/gettime` | `__timerfd_settime64` / `__timerfd_gettime64` | `timerfd_create` |
| `clock_settime` | `__clock_settime64` | (无) |
| 内部 `__timedwait_cp` | `__timedwait_cp` | `__timedwait`、`__eintr_valid_flag`(弱) |
只定义 `__timedwait_cp` 是**不行**的:libc 的 `__timedwait.o` 会因 `__timedwait` 未解析被
拉入,与本文的重定义撞车。实测:
```
ld.lld: error: duplicate symbol: __timedwait_cp
>>> defined at ovr2.c
>>> defined at __timedwait.c:34 (.../musl/src/thread/__timedwait.c:34)
```
### 7.2 为什么链接期接管有效
- 「hidden」不阻止静态链接接管:lld 按归档顺序解析,`meow_ui_time.o` 先于 `libc.a` 参与,
libc 内部对 `__timedwait_cp` 的引用就绑到本文(实测 `jal 0x…<__timedwait_cp>`)。
- 前提是 `meow_ui_time.o` 被拉入:SDK/App 都引用 `meow_ui_time_ms()`,且 `clock_gettime`
被重定向到 `__clock_gettime64`(本文也定义),所以它总会被拉入。**若将来把本 TU 的
接管符号拆到没人引用的新 TU,接管会静默失效。**
- `__eintr_valid_flag` 给弱定义,libc `signal/sigaction.c` 的强定义优先(同时链 sigaction
实测不冲突)。
- 取消点语义不重写:`__timedwait_cp` 仍调 libc 的 `__syscall_cp`,`ECANCELED`/`EINTR`
处理逐行照抄 musl。
- 结构宽度前提:本 TU 与被接管入口的调用者必须用同一套 zig 头(`time_t` 8B、
`struct timespec` 16B)。当前四 App 与 launcher 都用 `zig-mipsel-cc` 编译
(`$SYSROOT/lib` 只有 `libz.a`,libc 来自 zig 内置 musl),前提成立;若将来换链,
必须整链替换、不能混用。
### 7.3 未修项的配方(同一模式,约 10 行/项)
1. 在 `meow_ui_time.c` 的 mips 分支里定义 `__<api>_time64`(或该 API 的重定向名);
2. 入参展成 `meow_time64_spec` / `meow_time64_itimerspec`,调
`meow_time32_rel_from_parts()`(超时类,饱和)或 `meow_time32_abs_from_parts()`
(绝对时刻类,越界回 `ENOTSUP`);
3. 发 `SYS_<api>_time32`(或 `SYS_<api>` 的 time32 号),输出再反向展开;
4. 同一 `.o` 里 libc 定义的所有符号一起接管(见 7.1 表),否则重复定义;
5. 宿主单测补边界,交叉反汇编确认结构宽度与 syscall 号。
不建议顺手全改:这些 API 当前零调用(全仓 `grep` 只有 ncm 的宿主 bench/测试用 `utimensat`),
每多接管一个就多一份「转换写错 → 静默写坏文件时间/信号等待」的面;优先级应按「谁先被 App
用」来定。根治仍是上游:换容器旧链(gcc 不定义 `__ILP32__`)编 SDK,或修补 zig 内置 musl
的 `!__ILP32__` 守卫(本意只排除 x32)。
## 8. M16 验证
宿主(aarch64 Termux,gcc):
```
cd patch_v2/sdk
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/m.o # 0 warning
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time64.c /tmp/m.o -o /tmp/t && /tmp/t
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/m.o -o /tmp/t0 && /tmp/t0
```
- `test_meow_time64`(新,23 项):相对/绝对换算边界(INT32_MAX±1、INT64_MAX、负值)、
itimerspec 四字段不串位与越界原子性,以及「同一块 16B 内存按内核 o32 只读 8B」的
before/after 模拟(0.4s → `{0,0}` 立即超时;2.3s → 2s 整)。
- `test_meow_time`(M11 回归):换算边界 + 单调前进,仍 PASS。
交叉:
```
bash patch_v2/sdk/build_sdk_host.sh --verify # 76 成员、全 MIPS32r2、0 warning
```
反汇编(本单新增/改写的入口):
| 符号 | syscall 号 | 缓冲宽度证据 |
| --- | --- | --- |
| `__timedwait_cp` | `0x108e` = 4238 `SYS_futex` | 超时在栈上 8B(`addiu $18,$sp,0x20` + `sw $18,0x10($sp)`),不再直传 16B 结构 |
| `__timerfd_settime64` | `0x10e3` = 4323 `timerfd_settime32` | 从调用者 32B 结构读 `0x0/0x4/0x8/0x10/0x14/0x18`,打包成栈上 16B |
| `__timerfd_gettime64` | `0x10e2` = 4322 | 内核只写栈上 16B,再按字段展开到调用者 32B |
| `__clock_settime64` | `0x10a6` = 4262 `clock_settime32` | 8B `{int32 sec, int32 nsec}` |
链接期(真实 `meow_ui.a` + 探针,顺序同 App:`probe.o meow_ui.a … -lc`):
```
llvm-objdump -d probe_sdk | sed -n '/<__pthread_cond_timedwait_time64>:/,/^$/p' | grep jal.__timedwait
# cd930: jal 0x419e8 <__timedwait_cp> ← 绑到 meow_ui.a 里的实现
llvm-nm probe_sdk | grep -c __timedwait_cp # 1(libc 那份未被拉入)
```
真机验证仍待设备(本单未碰真机):`pthread_cond_timedwait` 传 100ms 超时的等待应真的等
约 100ms 再超时,而不是立即返回。