quickwin 1.3.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.en.md +18 -24
  2. package/README.md +17 -23
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20155 -1420
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +25 -82
  10. package/examples/pdf_preview2.ts +27 -84
  11. package/examples/pdf_viewer.js +1424 -1180
  12. package/examples/pdf_viewer.tsx +16 -67
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1418 -1075
  16. package/examples/test_react_lazy.js +1391 -1059
  17. package/examples/test_react_listbox.js +1390 -1058
  18. package/examples/test_root_window.js +1384 -1052
  19. package/examples/test_tab.js +1391 -1059
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +219 -0
  23. package/lib/certs.ts +229 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +88 -7
  27. package/lib/fetch.ts +73 -7
  28. package/lib/ffi/bind.js +441 -0
  29. package/lib/ffi/bind.ts +539 -0
  30. package/lib/ffi/ctype.js +194 -0
  31. package/lib/ffi/ctype.ts +369 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +490 -0
  35. package/lib/ffi/struct.ts +753 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +18 -37
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +6 -19
  43. package/lib/react-qw/components/ListView.js +50 -154
  44. package/lib/react-qw/components/PathPicker.js +28 -88
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +6 -22
  48. package/lib/react-qw/components/Tooltip.js +10 -21
  49. package/lib/react-qw/components/TreeView.js +8 -31
  50. package/lib/react-qw/index.js +5 -3
  51. package/lib/react-qw/index.ts +8 -3
  52. package/lib/react-qw/nmhdr.js +3 -24
  53. package/lib/react-qw/nmhdr.ts +5 -30
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +20 -23
  59. package/lib/text-measure.ts +22 -26
  60. package/lib/websocket.js +44 -4
  61. package/lib/websocket.ts +40 -4
  62. package/lib/windows/advapi32.js +53 -0
  63. package/lib/windows/advapi32.ts +64 -0
  64. package/lib/windows/comctl32.js +18 -0
  65. package/lib/windows/comctl32.ts +19 -0
  66. package/lib/windows/comdlg32.js +8 -0
  67. package/lib/windows/comdlg32.ts +10 -0
  68. package/lib/windows/gdi32.js +392 -0
  69. package/lib/windows/gdi32.ts +420 -0
  70. package/lib/windows/kernel32.js +262 -0
  71. package/lib/windows/kernel32.ts +291 -0
  72. package/lib/windows/msimg32.js +38 -0
  73. package/lib/windows/msimg32.ts +45 -0
  74. package/lib/windows/ntdll.js +9 -0
  75. package/lib/windows/ntdll.ts +11 -0
  76. package/lib/windows/ole32.js +7 -0
  77. package/lib/windows/ole32.ts +9 -0
  78. package/lib/windows/psapi.js +32 -0
  79. package/lib/windows/psapi.ts +37 -0
  80. package/lib/windows/shell32.js +13 -0
  81. package/lib/windows/shell32.ts +16 -0
  82. package/lib/windows/structs.js +519 -0
  83. package/lib/windows/structs.ts +566 -0
  84. package/lib/windows/user32.js +572 -0
  85. package/lib/windows/user32.ts +607 -0
  86. package/lib/windows/version.js +29 -0
  87. package/lib/windows/version.ts +35 -0
  88. package/lib/windows/winspool.js +71 -0
  89. package/lib/windows/winspool.ts +80 -0
  90. package/package.json +1 -1
  91. package/quickwin.d.ts +31 -40
  92. package/quickwin_const.d.ts +137 -0
  93. package/qwin-nowasm-x86.exe +0 -0
  94. package/qwin-nowasm.exe +0 -0
  95. package/qwin-x86.exe +0 -0
  96. package/qwin.exe +0 -0
  97. package/test/bench_ffi_callraw.js +236 -0
  98. package/test/bench_ffi_callraw.ts +236 -0
  99. package/test/bench_ffi_overhead.js +139 -0
  100. package/test/bench_ffi_overhead.ts +141 -0
  101. package/test/bench_sendmessage.js +104 -0
  102. package/test/bench_sendmessage.ts +100 -0
  103. package/test/bench_sendmessage_nonnull.js +187 -0
  104. package/test/bench_sendmessage_nonnull.ts +188 -0
  105. package/test/run.js +19 -4
  106. package/test/run.ts +20 -5
  107. package/test/test_basic_fetch.js +29 -0
  108. package/test/test_basic_fetch.ts +26 -0
  109. package/test/test_ffi.js +27 -45
  110. package/test/test_ffi.ts +35 -54
  111. package/test/test_ffi_abi.js +290 -0
  112. package/test/test_ffi_abi.ts +274 -0
  113. package/test/test_ffi_array.js +116 -0
  114. package/test/test_ffi_array.ts +141 -0
  115. package/test/test_ffi_bind.js +620 -8
  116. package/test/test_ffi_bind.ts +630 -8
  117. package/test/test_ffi_struct.js +632 -88
  118. package/test/test_ffi_struct.ts +666 -91
  119. package/test/test_ffi_struct_layout.js +495 -0
  120. package/test/test_ffi_struct_layout.ts +543 -0
  121. package/test/test_ffi_user32.js +103 -0
  122. package/test/test_ffi_user32.ts +113 -0
  123. package/test/test_ffi_windows.js +101 -0
  124. package/test/test_ffi_windows.ts +113 -0
  125. package/test/test_files.js +102 -0
  126. package/test/test_files.ts +121 -0
  127. package/test/test_gdi_draw.js +681 -0
  128. package/test/test_gdi_draw.ts +725 -0
  129. package/test/test_helper.js +8 -1
  130. package/test/test_helper.ts +9 -1
  131. package/test/test_http_import.js +18 -0
  132. package/test/test_http_import.ts +19 -0
  133. package/test/test_http_server.js +73 -1
  134. package/test/test_http_server.ts +75 -1
  135. package/test/test_ipv6.js +2 -2
  136. package/test/test_ipv6.ts +2 -2
  137. package/test/test_net_fetch.js +74 -11
  138. package/test/test_net_fetch.ts +73 -10
  139. package/test/test_net_websocket.js +1 -1
  140. package/test/test_net_websocket.ts +1 -1
  141. package/test/test_printer.js +137 -0
  142. package/test/test_printer.ts +148 -0
  143. package/test/test_proc.js +141 -0
  144. package/test/test_proc.ts +163 -0
  145. package/test/test_react_complex.js +1401 -1069
  146. package/test/test_react_counter.js +1391 -1059
  147. package/test/test_react_flex.js +1411 -1072
  148. package/test/test_react_hidden.js +1394 -1062
  149. package/test/test_react_listview_link.js +1391 -1059
  150. package/test/test_react_notify.js +18638 -0
  151. package/test/test_react_render.js +1391 -1059
  152. package/test/test_registry.js +74 -0
  153. package/test/test_registry.ts +95 -0
  154. package/test/test_sysinfo.js +122 -0
  155. package/test/test_sysinfo.ts +141 -0
  156. package/test/test_wolfssl_read.js +45 -43
  157. package/test/test_wolfssl_read.ts +42 -40
  158. package/test/test_worker_net.js +35 -17
  159. package/test/test_worker_net.ts +27 -15
  160. package/tsconfig.json +1 -1
  161. package/examples/exec_server.ts +0 -80
  162. package/lib/ffi-bind.js +0 -85
  163. package/lib/ffi-bind.ts +0 -170
  164. package/lib/ffi-struct.js +0 -224
  165. package/lib/ffi-struct.ts +0 -239
  166. package/lib/win-common-structs.js +0 -39
  167. package/lib/win-common-structs.ts +0 -41
@@ -0,0 +1,141 @@
1
+ // 一次性性能 bench:我方 bind() FFI vs node:ffi(Node v26.10) 同机同场景对比(win11 VM)
2
+ // 不入 CI。对照脚本 test/bench_nodeffi.mjs(node 侧同场景)。结论见对话,未落文档。
3
+ // 用法:make js 后 POST /exec {"cmd":"qwin.exe test/bench_ffi_overhead.js"}
4
+ // 场景(跨运行时按 ns/call 归一,每臂独立自校准到 ~400ms/样本):
5
+ // S0 nop0 test/nop.dll 0 参 void —— 纯 JS→C 往返 trampoline(无封送无返回读取)
6
+ // S1 nop1 test/nop.dll 1×i32 —— 单标量封送往返
7
+ // S2 pid kernel32 GetCurrentProcessId 0 参 —— 真实系统 DLL
8
+ // S3 sm user32 SendMessageW WM_NULL 自有窗口 —— 完整链路 + gui C 直调地板参照
9
+ // 方法学沿用 bench_sendmessage_nonnull:预热 1 + 臂序轮转 + median 主指标/min。
10
+ // Date.now() 1ms 量化 → 批量自校准(探针 ≥20ms 才有 ≤5% 步长,目标 ~400ms/样本)。
11
+ // 全部消息发自有窗口(直调 wndproc)。结束必须 DestroyWindow + std.exit。
12
+ import * as std from 'std'
13
+ import * as gui from 'gui'
14
+ import { bind } from '../lib/ffi/bind.js'
15
+
16
+ const WM_NULL = 0x0000
17
+ const ROUNDS = 15
18
+ const TARGET_MS = 400
19
+
20
+ // median(离群稳健)供 calibrate 与 suite 共用
21
+ const med = (t: number[]): number => {
22
+ const s = [...t].sort((x, y) => x - y)
23
+ const m = s.length >> 1
24
+ return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2
25
+ }
26
+
27
+ // 文件日志(cwd=Z:\quickwin,宿主机 _build/bench_ffi_overhead_log.txt 可实时读)——
28
+ // exec 只在进程退出后回包,挂了靠它定位卡点
29
+ const log = std.open('bench_ffi_overhead_log.txt', 'w')
30
+ function step(msg: string): void {
31
+ std.printf('%s\n', msg)
32
+ if (log) { log.puts(msg + '\n'); log.flush() }
33
+ }
34
+
35
+ step('stage: imports ok')
36
+
37
+ const winRaw = gui.CreateWindow('STATIC', 'bench-title', 0, 0, 0, 20, 20, null, null)
38
+ if (!winRaw) throw new Error('CreateWindow(STATIC) failed')
39
+ const win: gui.HWND = winRaw
40
+ step('stage: window created')
41
+
42
+ const fNop0 = bind('test/nop.dll', 'nop0', ' -> void')
43
+ const fNop1 = bind('test/nop.dll', 'nop1', 'i32 -> i32')
44
+ const fPid = bind('kernel32.dll', 'GetCurrentProcessId', ' -> u32')
45
+ const fSm = bind('user32.dll', 'SendMessageW', '<HWND>ptr u32 <>ptr <>ptr -> <>ptr')
46
+
47
+ // 每臂循环体:函数先取到局部 const(与 node 侧同形态——模块槽访问 vs 局部槽访问的
48
+ // 每迭代差在 ns 级,两边都取局部才对齐);调用结果不使用(QuickJS 解释器不消除
49
+ // 副作用调用,与原 bench 同口径)
50
+ const runNop0 = (n: number): number => {
51
+ const f = fNop0
52
+ const t0 = Date.now()
53
+ for (let i = 0; i < n; i++) f()
54
+ return Date.now() - t0
55
+ }
56
+ const runNop1 = (n: number): number => {
57
+ const f = fNop1
58
+ const t0 = Date.now()
59
+ for (let i = 0; i < n; i++) f(1)
60
+ return Date.now() - t0
61
+ }
62
+ const runPid = (n: number): number => {
63
+ const f = fPid
64
+ const t0 = Date.now()
65
+ for (let i = 0; i < n; i++) f()
66
+ return Date.now() - t0
67
+ }
68
+ const runSm = (n: number): number => {
69
+ const f = fSm
70
+ const t0 = Date.now()
71
+ for (let i = 0; i < n; i++) f(win, WM_NULL, 0, 0x1234)
72
+ return Date.now() - t0
73
+ }
74
+ const runGuiSm = (n: number): number => {
75
+ const f = gui.SendMessage
76
+ const t0 = Date.now()
77
+ for (let i = 0; i < n; i++) f(win, WM_NULL, 0, 0x1234)
78
+ return Date.now() - t0
79
+ }
80
+
81
+ type Arm = { label: string; n: number; fn: (n: number) => number }
82
+
83
+ // 自校准:探针逐级放大 n0 直到 dt≥20ms(量化步长 1ms ≤ 5%),再按 TARGET_MS 定批量。
84
+ // 不预设单次调用量级——我方与 gui 地板相差 100×,固定批量必有一侧撞量化墙。
85
+ function calibrate(label: string, fn: (n: number) => number): number {
86
+ let n0 = 10_000
87
+ let dt = 0
88
+ for (;;) {
89
+ dt = fn(n0)
90
+ if (dt >= 20 || n0 >= 10_000_000) break
91
+ n0 *= 10
92
+ }
93
+ const per = dt / n0
94
+ const n = Math.max(10_000, Math.min(10_000_000, Math.round(TARGET_MS / per)))
95
+ step(' calib ' + label + ': probe n=' + String(n0) + ' dt=' + String(dt) +
96
+ 'ms → batch=' + String(n))
97
+ return n
98
+ }
99
+
100
+ function mkArm(label: string, fn: (n: number) => number): Arm {
101
+ return { label, n: calibrate(label, fn), fn }
102
+ }
103
+
104
+ function suite(name: string, arms: Arm[]): void {
105
+ step('suite: ' + name + ' warmup')
106
+ for (const a of arms) a.fn(a.n) // 预热各 1 轮
107
+ const times: number[][] = arms.map(() => [])
108
+ for (let r = 0; r < ROUNDS; r++) {
109
+ // 臂序按 r 轮转起跑,抵消顺序性偏置(温度/调度漂移)
110
+ for (let k = 0; k < arms.length; k++) {
111
+ const i = (r + k) % arms.length
112
+ times[i]!.push(arms[i]!.fn(arms[i]!.n))
113
+ }
114
+ }
115
+ // 每轮原始数据只 dump 前 3 轮(15 轮全打没意义,log 只留诊断线索)
116
+ for (let r = 0; r < Math.min(ROUNDS, 3); r++) {
117
+ step(' round ' + String(r) + ': ' +
118
+ arms.map((a, i) => a.label + '=' + String(times[i]![r]) + 'ms').join(' '))
119
+ }
120
+ const ns = (i: number, ms: number) => (ms * 1e6 / arms[i]!.n).toFixed(1)
121
+ // median 是主指标(离群稳健);min 留作参照
122
+ const meds = times.map(med)
123
+ const mins = times.map(t => Math.min(...t))
124
+ step(' ** median: ' + arms.map((a, i) => a.label + '=' + ns(i, meds[i]!) + 'ns').join(' | '))
125
+ step(' min: ' + arms.map((a, i) => a.label + '=' + ns(i, mins[i]!) + 'ns').join(' | '))
126
+ }
127
+
128
+ step('bench_ffi_overhead (ours): per-arm calib ~' + String(TARGET_MS) + 'ms, ' +
129
+ String(ROUNDS) + 'r (median 主指标), 对照 test/bench_nodeffi.mjs')
130
+ suite('S0 nop0 0参 void 纯往返', [mkArm('ours', runNop0)])
131
+ suite('S1 nop1 1×i32 封送往返', [mkArm('ours', runNop1)])
132
+ suite('S2 GetCurrentProcessId 0参真实API', [mkArm('ours', runPid)])
133
+ suite('S3 SendMessageW WM_NULL 自有窗口 (gui=直调地板)', [
134
+ mkArm('ours', runSm),
135
+ mkArm('gui ', runGuiSm),
136
+ ])
137
+
138
+ gui.DestroyWindow(win)
139
+ step('DONE')
140
+ if (log) log.close()
141
+ std.exit(0)
@@ -0,0 +1,104 @@
1
+ // 一次性性能 bench:gui.SendMessage(C 模块直调) vs FFI bind SendMessageW(libffi trampoline)
2
+ // 不入 CI。用法:make js 后 POST /exec {"cmd":"qwin.exe test/bench_sendmessage.js"}
3
+ // 每场景预热 1 轮 + 5 轮计数(奇偶交叉起跑顺序),取各自最小值降抖动。
4
+ // 注意:全部消息发给自有窗口(直调 wndproc)——跨进程消息走消息经纪,几十 μs/次且
5
+ // IPC 开销掩盖入口链差异,不作测点。结束必须 DestroyWindow + std.exit:留活窗会让
6
+ // qwin 主流程驻留消息循环,exec 永不返回。
7
+ import * as std from 'std';
8
+ import * as gui from 'gui';
9
+ import { SendMessage } from '../lib/windows/user32.js';
10
+ import { WCHAR } from '../lib/ffi/bind.js';
11
+ import { PtrArrayBuffer } from '../lib/ffi/ctype.js';
12
+ const N = 500_000;
13
+ // S2 含 WCHAR.encode 每次分配:500k 时 GC 抖动把单轮拖到 190s+(实测 ~380μs/迭代),
14
+ // 降到 50k——统计上仍远超噪声需求
15
+ const N_S2 = 50_000;
16
+ const ROUNDS = 5;
17
+ const ROUNDS_S2 = 3;
18
+ const WM_SETTEXT = 0x000c;
19
+ const WM_GETTEXT = 0x000d;
20
+ // 文件日志(cwd=Z:\quickwin,宿主机 _build/bench_log.txt 可实时读)——exec 只在进程
21
+ // 退出后回包,挂了靠它定位卡点
22
+ const log = std.open('bench_log.txt', 'w');
23
+ function step(msg) {
24
+ std.printf('%s\n', msg);
25
+ if (log) {
26
+ log.puts(msg + '\n');
27
+ log.flush();
28
+ }
29
+ }
30
+ step('stage: imports ok');
31
+ const winRaw = gui.CreateWindow('STATIC', 'bench-title', 0, 0, 0, 20, 20, null, null);
32
+ if (!winRaw)
33
+ throw new Error('CreateWindow(STATIC) failed');
34
+ const win = winRaw;
35
+ step('stage: window created');
36
+ // 两侧入口各自提为常量(循环内属性查找开销对齐)
37
+ const gSM = gui.SendMessage;
38
+ const fSM = SendMessage;
39
+ const TEXT = 'bench';
40
+ const buf = new PtrArrayBuffer(64);
41
+ // —— S1(WM_NULL 纯数字,最纯入口链开销):实现已随首轮跑数移除,数据见 .agents/SENDMESSAGE_BENCH.md ——
42
+ // —— S2:WM_SETTEXT 字符串 lParam(任务 3 多态点的真实成本形态):
43
+ // gui 侧 C 内编码;FFI 侧调用点 WCHAR.encode 在循环内(迁移后的真实写法)——
44
+ function s2g() {
45
+ const t0 = Date.now();
46
+ for (let i = 0; i < N_S2; i++)
47
+ gSM(win, WM_SETTEXT, 0, TEXT);
48
+ return Date.now() - t0;
49
+ }
50
+ function s2f() {
51
+ const t0 = Date.now();
52
+ for (let i = 0; i < N_S2; i++) {
53
+ const w = WCHAR.encode(TEXT);
54
+ fSM(win, WM_SETTEXT, 0, w.ptr);
55
+ }
56
+ return Date.now() - t0;
57
+ }
58
+ // —— S3:WM_GETTEXT 缓冲出参往返,两侧 JS 实参完全相同(纯入口差 + C 侧拷贝共模)——
59
+ function s3g() {
60
+ const t0 = Date.now();
61
+ for (let i = 0; i < N; i++)
62
+ gSM(win, WM_GETTEXT, 64, buf.ptr);
63
+ return Date.now() - t0;
64
+ }
65
+ function s3f() {
66
+ const t0 = Date.now();
67
+ for (let i = 0; i < N; i++)
68
+ fSM(win, WM_GETTEXT, 64, buf.ptr);
69
+ return Date.now() - t0;
70
+ }
71
+ function suite(name, n, rounds, rg, rf) {
72
+ step('suite: ' + name + ' warmup');
73
+ rg();
74
+ rf(); // 预热各 1 轮
75
+ const gs = [];
76
+ const fs = [];
77
+ for (let r = 0; r < rounds; r++) {
78
+ // 奇偶交叉起跑顺序,抵消顺序性偏置(温度/调度漂移)
79
+ if (r % 2 === 0) {
80
+ gs.push(rg());
81
+ fs.push(rf());
82
+ }
83
+ else {
84
+ fs.push(rf());
85
+ gs.push(rg());
86
+ }
87
+ step(' round ' + String(r) + ': gui=' + String(gs[gs.length - 1]) +
88
+ 'ms ffi=' + String(fs[fs.length - 1]) + 'ms');
89
+ }
90
+ const gmin = Math.min(...gs), fmin = Math.min(...fs);
91
+ const ns = (ms) => (ms * 1e6 / n).toFixed(0);
92
+ step(' ==> gui min=' + String(gmin) + 'ms (' + ns(gmin) + ' ns/call), ffi min=' +
93
+ String(fmin) + 'ms (' + ns(fmin) + ' ns/call), ffi/gui=' + (fmin / gmin).toFixed(2) +
94
+ 'x, delta=' + ns(fmin - gmin) + 'ns');
95
+ }
96
+ step('bench_sendmessage: round2 (S1 done in round1), S2 n=' + String(N_S2) +
97
+ '/' + String(ROUNDS_S2) + 'r, S3 n=' + String(N) + '/' + String(ROUNDS) + 'r (min)');
98
+ suite('S2 WM_SETTEXT 字符串 lParam (FFI 侧含 WCHAR.encode)', N_S2, ROUNDS_S2, s2g, s2f);
99
+ suite('S3 WM_GETTEXT 缓冲出参往返 (两侧实参相同)', N, ROUNDS, s3g, s3f);
100
+ gui.DestroyWindow(win);
101
+ step('DONE');
102
+ if (log)
103
+ log.close();
104
+ std.exit(0);
@@ -0,0 +1,100 @@
1
+ // 一次性性能 bench:gui.SendMessage(C 模块直调) vs FFI bind SendMessageW(libffi trampoline)
2
+ // 不入 CI。用法:make js 后 POST /exec {"cmd":"qwin.exe test/bench_sendmessage.js"}
3
+ // 每场景预热 1 轮 + 5 轮计数(奇偶交叉起跑顺序),取各自最小值降抖动。
4
+ // 注意:全部消息发给自有窗口(直调 wndproc)——跨进程消息走消息经纪,几十 μs/次且
5
+ // IPC 开销掩盖入口链差异,不作测点。结束必须 DestroyWindow + std.exit:留活窗会让
6
+ // qwin 主流程驻留消息循环,exec 永不返回。
7
+ import * as std from 'std'
8
+ import * as gui from 'gui'
9
+ import { SendMessage } from '../lib/windows/user32.js'
10
+ import { WCHAR } from '../lib/ffi/bind.js'
11
+ import { PtrArrayBuffer } from '../lib/ffi/ctype.js'
12
+
13
+ const N = 500_000
14
+ // S2 含 WCHAR.encode 每次分配:500k 时 GC 抖动把单轮拖到 190s+(实测 ~380μs/迭代),
15
+ // 降到 50k——统计上仍远超噪声需求
16
+ const N_S2 = 50_000
17
+ const ROUNDS = 5
18
+ const ROUNDS_S2 = 3
19
+ const WM_SETTEXT = 0x000c
20
+ const WM_GETTEXT = 0x000d
21
+
22
+ // 文件日志(cwd=Z:\quickwin,宿主机 _build/bench_log.txt 可实时读)——exec 只在进程
23
+ // 退出后回包,挂了靠它定位卡点
24
+ const log = std.open('bench_log.txt', 'w')
25
+ function step(msg: string): void {
26
+ std.printf('%s\n', msg)
27
+ if (log) { log.puts(msg + '\n'); log.flush() }
28
+ }
29
+
30
+ step('stage: imports ok')
31
+
32
+ const winRaw = gui.CreateWindow('STATIC', 'bench-title', 0, 0, 0, 20, 20, null, null)
33
+ if (!winRaw) throw new Error('CreateWindow(STATIC) failed')
34
+ const win: gui.HWND = winRaw
35
+ step('stage: window created')
36
+
37
+ // 两侧入口各自提为常量(循环内属性查找开销对齐)
38
+ const gSM = gui.SendMessage
39
+ const fSM = SendMessage
40
+ const TEXT = 'bench'
41
+ const buf = new PtrArrayBuffer<'WCHAR'>(64)
42
+
43
+ // —— S1(WM_NULL 纯数字,最纯入口链开销):实现已随首轮跑数移除,数据见 .agents/SENDMESSAGE_BENCH.md ——
44
+
45
+ // —— S2:WM_SETTEXT 字符串 lParam(任务 3 多态点的真实成本形态):
46
+ // gui 侧 C 内编码;FFI 侧调用点 WCHAR.encode 在循环内(迁移后的真实写法)——
47
+ function s2g(): number {
48
+ const t0 = Date.now()
49
+ for (let i = 0; i < N_S2; i++) gSM(win, WM_SETTEXT, 0, TEXT)
50
+ return Date.now() - t0
51
+ }
52
+ function s2f(): number {
53
+ const t0 = Date.now()
54
+ for (let i = 0; i < N_S2; i++) {
55
+ const w = WCHAR.encode(TEXT)
56
+ fSM(win, WM_SETTEXT, 0, w.ptr)
57
+ }
58
+ return Date.now() - t0
59
+ }
60
+
61
+ // —— S3:WM_GETTEXT 缓冲出参往返,两侧 JS 实参完全相同(纯入口差 + C 侧拷贝共模)——
62
+ function s3g(): number {
63
+ const t0 = Date.now()
64
+ for (let i = 0; i < N; i++) gSM(win, WM_GETTEXT, 64, buf.ptr)
65
+ return Date.now() - t0
66
+ }
67
+ function s3f(): number {
68
+ const t0 = Date.now()
69
+ for (let i = 0; i < N; i++) fSM(win, WM_GETTEXT, 64, buf.ptr)
70
+ return Date.now() - t0
71
+ }
72
+
73
+ function suite(name: string, n: number, rounds: number, rg: () => number, rf: () => number): void {
74
+ step('suite: ' + name + ' warmup')
75
+ rg(); rf() // 预热各 1 轮
76
+ const gs: number[] = []
77
+ const fs: number[] = []
78
+ for (let r = 0; r < rounds; r++) {
79
+ // 奇偶交叉起跑顺序,抵消顺序性偏置(温度/调度漂移)
80
+ if (r % 2 === 0) { gs.push(rg()); fs.push(rf()) }
81
+ else { fs.push(rf()); gs.push(rg()) }
82
+ step(' round ' + String(r) + ': gui=' + String(gs[gs.length - 1]) +
83
+ 'ms ffi=' + String(fs[fs.length - 1]) + 'ms')
84
+ }
85
+ const gmin = Math.min(...gs), fmin = Math.min(...fs)
86
+ const ns = (ms: number) => (ms * 1e6 / n).toFixed(0)
87
+ step(' ==> gui min=' + String(gmin) + 'ms (' + ns(gmin) + ' ns/call), ffi min=' +
88
+ String(fmin) + 'ms (' + ns(fmin) + ' ns/call), ffi/gui=' + (fmin / gmin).toFixed(2) +
89
+ 'x, delta=' + ns(fmin - gmin) + 'ns')
90
+ }
91
+
92
+ step('bench_sendmessage: round2 (S1 done in round1), S2 n=' + String(N_S2) +
93
+ '/' + String(ROUNDS_S2) + 'r, S3 n=' + String(N) + '/' + String(ROUNDS) + 'r (min)')
94
+ suite('S2 WM_SETTEXT 字符串 lParam (FFI 侧含 WCHAR.encode)', N_S2, ROUNDS_S2, s2g, s2f)
95
+ suite('S3 WM_GETTEXT 缓冲出参往返 (两侧实参相同)', N, ROUNDS, s3g, s3f)
96
+
97
+ gui.DestroyWindow(win)
98
+ step('DONE')
99
+ if (log) log.close()
100
+ std.exit(0)
@@ -0,0 +1,187 @@
1
+ // 一次性性能 bench:FFI 签名 '!' 标注的调用时 0 检验成本(同函数多臂对照 + A/A 噪声底)
2
+ // 不入 CI。用法:make js 后 POST /exec {"cmd":"qwin.exe test/bench_sendmessage_nonnull.js"}
3
+ // 臂(同一 proc 各自 bind,唯一变量是 '!'):
4
+ // A1 base '<HWND>ptr u32 <>ptr <>ptr -> <>ptr' 基线
5
+ // A2 ctrl '<HWND>ptr u32 <>ptr <>ptr -> <>ptr' 与 A1 完全同签名——两臂差值 = 机器噪声底
6
+ // B arg! '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr' + 参数位 arg#1 0 检查
7
+ // C ret! '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr!' + 返回位 0 检查(仅非零返回场景)
8
+ // 方法学(前两轮教训):批量缩到 50k/样本 → 臂序每 ~650ms 轮转一次,抵消 QEMU 调度抖动;
9
+ // S1/S3 各臂 30 样本取 median(对 GC/调度离群稳健);A2-A1 是效应检验的参照带——
10
+ // B/A 落在 A2/A 带内即「低于噪声底,不可测」。'!' 边际 = 每标注位 1 次整数 === 比较
11
+ // (endsWith 无条件跑、所有臂共模),理论 ≪ 轮噪声。
12
+ // C 臂标了返回 '!',撞真实 0 即 throw —— 先用 A1 探各场景原始返回值,非零才让 C 进。
13
+ // 全部消息发自有窗口(直调 wndproc)。结束必须 DestroyWindow + std.exit。
14
+ import * as std from 'std';
15
+ import * as gui from 'gui';
16
+ import { bind, WCHAR } from '../lib/ffi/bind.js';
17
+ import { PtrArrayBuffer } from '../lib/ffi/ctype.js';
18
+ const BATCH = 50_000; // 每样本批量(~650ms)——小批量快轮转,去相关调度抖动
19
+ const ROUNDS = 30; // S1/S3 每臂样本数(30 × 3-4 臂 ≈ 60-80s/suite)
20
+ const N_S2 = 50_000;
21
+ const ROUNDS_S2 = 7;
22
+ const WM_NULL = 0x0000;
23
+ const WM_SETTEXT = 0x000c;
24
+ const WM_GETTEXT = 0x000d;
25
+ // median(离群稳健)供 micro 与 suite 共用
26
+ const med = (t) => {
27
+ const s = [...t].sort((x, y) => x - y);
28
+ const m = s.length >> 1;
29
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
30
+ };
31
+ // S0:'!' 边际操作本身(1 次整数 === 比较 + 分支)的解释器成本——FFI 层噪声
32
+ // ±400~900ns/call 盖住了 12.5μs 调用上的 ns 级效应,这里绕开 ffiCall 直接量单检查。
33
+ // flag 必须是模块级可变量:QuickJS 编译期会常量折叠字面量,折叠掉就测不到了。
34
+ let microFlag = true;
35
+ const MICRO_N = 100_000_000;
36
+ function microLoop() {
37
+ const t0 = Date.now();
38
+ let hits = 0;
39
+ for (let i = 0; i < MICRO_N; i++)
40
+ if (microFlag && i === 0)
41
+ hits++;
42
+ const dt = Date.now() - t0;
43
+ if (hits !== (microFlag ? 1 : 0))
44
+ throw new Error('micro sanity failed: hits=' + hits);
45
+ return dt;
46
+ }
47
+ function microSuite() {
48
+ step('suite: S0 micro 0-check interpreter cost (n=' + String(MICRO_N) + ')');
49
+ microFlag = true;
50
+ microLoop();
51
+ microFlag = false;
52
+ microLoop();
53
+ const ts = [];
54
+ const fs = [];
55
+ for (let r = 0; r < 5; r++) {
56
+ // 奇偶交叉起跑顺序(同原 bench 方法)
57
+ if (r % 2 === 0) {
58
+ microFlag = true;
59
+ ts.push(microLoop());
60
+ microFlag = false;
61
+ fs.push(microLoop());
62
+ }
63
+ else {
64
+ microFlag = false;
65
+ fs.push(microLoop());
66
+ microFlag = true;
67
+ ts.push(microLoop());
68
+ }
69
+ step(' round ' + String(r) + ': check=' + String(ts[ts.length - 1]) +
70
+ 'ms base=' + String(fs[fs.length - 1]) + 'ms');
71
+ }
72
+ const ns = (ms) => (ms * 1e6 / MICRO_N).toFixed(2);
73
+ const tmed = med(ts), fmed = med(fs);
74
+ step(' ** median: check=' + ns(tmed) + 'ns/op base=' + ns(fmed) +
75
+ 'ns/op ==> per-check cost=' + ((tmed - fmed) * 1e6 / MICRO_N).toFixed(2) + 'ns');
76
+ }
77
+ // 文件日志(cwd=Z:\quickwin,宿主机 _build/bench_nonnull_log.txt 可实时读)——exec 只在
78
+ // 进程退出后回包,挂了靠它定位卡点
79
+ const log = std.open('bench_nonnull_log.txt', 'w');
80
+ function step(msg) {
81
+ std.printf('%s\n', msg);
82
+ if (log) {
83
+ log.puts(msg + '\n');
84
+ log.flush();
85
+ }
86
+ }
87
+ step('stage: imports ok');
88
+ const winRaw = gui.CreateWindow('STATIC', 'bench-title', 0, 0, 0, 20, 20, null, null);
89
+ if (!winRaw)
90
+ throw new Error('CreateWindow(STATIC) failed');
91
+ const win = winRaw;
92
+ step('stage: window created');
93
+ const SIG = '<HWND>ptr u32 <>ptr <>ptr -> <>ptr';
94
+ const fA1 = bind('user32.dll', 'SendMessageW', SIG);
95
+ const fA2 = bind('user32.dll', 'SendMessageW', SIG); // A/A 对照:同签名二绑 = 噪声底
96
+ const fB = bind('user32.dll', 'SendMessageW', '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr');
97
+ const fC = bind('user32.dll', 'SendMessageW', '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr!');
98
+ const TEXT = 'bench';
99
+ const buf = new PtrArrayBuffer(64);
100
+ // C 臂可行性探针:A1 读各场景原始返回值,撞 0 的场景不进 C('!' 会 throw)。
101
+ // S1 的 WM_NULL 走 DefWindowProc 预期返 0 → C 不进 S1 是预期而非缺测。
102
+ const retS1 = fA1(win, WM_NULL, 0, 0x1234);
103
+ const retS2 = fA1(win, WM_SETTEXT, 0, WCHAR.encode(TEXT).ptr);
104
+ const retS3 = fA1(win, WM_GETTEXT, 64, buf.ptr);
105
+ step('probe ret: S1=' + String(retS1) + ' S2=' + String(retS2) + ' S3=' + String(retS3) +
106
+ ' (C arm only enters nonzero scenes)');
107
+ function run1(f) {
108
+ const t0 = Date.now();
109
+ for (let i = 0; i < BATCH; i++)
110
+ f(win, WM_NULL, 0, 0x1234);
111
+ return Date.now() - t0;
112
+ }
113
+ function run2(f) {
114
+ const t0 = Date.now();
115
+ for (let i = 0; i < N_S2; i++) {
116
+ const w = WCHAR.encode(TEXT);
117
+ f(win, WM_SETTEXT, 0, w.ptr);
118
+ }
119
+ return Date.now() - t0;
120
+ }
121
+ function run3(f) {
122
+ const t0 = Date.now();
123
+ for (let i = 0; i < BATCH; i++)
124
+ f(win, WM_GETTEXT, 64, buf.ptr);
125
+ return Date.now() - t0;
126
+ }
127
+ function suite(name, n, rounds, arms) {
128
+ step('suite: ' + name + ' warmup');
129
+ for (const a of arms)
130
+ a.fn(); // 预热各 1 轮
131
+ const times = arms.map(() => []);
132
+ for (let r = 0; r < rounds; r++) {
133
+ // 臂序按 r 轮转起跑,抵消顺序性偏置(温度/调度漂移)
134
+ for (let k = 0; k < arms.length; k++) {
135
+ const i = (r + k) % arms.length;
136
+ times[i].push(arms[i].fn());
137
+ }
138
+ }
139
+ // 每轮原始数据只 dump 前 3 轮(30 轮全打没意义,log 只留诊断线索)
140
+ for (let r = 0; r < Math.min(rounds, 3); r++) {
141
+ step(' round ' + String(r) + ': ' +
142
+ arms.map((a, i) => a.label + '=' + String(times[i][r]) + 'ms').join(' '));
143
+ }
144
+ const ns = (ms) => (ms * 1e6 / n).toFixed(1);
145
+ // median 是主指标(离群稳健);min 留作与前两轮/原 bench 的可比项
146
+ const meds = times.map(med);
147
+ const mins = times.map(t => Math.min(...t));
148
+ step(' ** median: ' + arms.map((a, i) => a.label + '=' + ns(meds[i]) + 'ns').join(' | '));
149
+ step(' min: ' + arms.map((a, i) => a.label + '=' + ns(mins[i]) + 'ns').join(' | '));
150
+ for (let i = 1; i < arms.length; i++)
151
+ step(' vs A1: ' + arms[i].label + ' delta=' +
152
+ ((meds[i] - meds[0]) * 1e6 / n).toFixed(1) + 'ns/call (median), ' +
153
+ ((mins[i] - mins[0]) * 1e6 / n).toFixed(1) + 'ns/call (min)' +
154
+ (i === 1 ? ' <-- A2-A1 = NOISE FLOOR' : ''));
155
+ }
156
+ const s1Arms = [
157
+ { label: 'A1', fn: () => run1(fA1) },
158
+ { label: 'A2', fn: () => run1(fA2) },
159
+ { label: 'B ', fn: () => run1(fB) },
160
+ ];
161
+ if (retS1 !== 0)
162
+ s1Arms.push({ label: 'C ', fn: () => run1(fC) });
163
+ const s2Arms = [
164
+ { label: 'A1', fn: () => run2(fA1) },
165
+ { label: 'A2', fn: () => run2(fA2) },
166
+ { label: 'B ', fn: () => run2(fB) },
167
+ ];
168
+ if (retS2 !== 0)
169
+ s2Arms.push({ label: 'C ', fn: () => run2(fC) });
170
+ const s3Arms = [
171
+ { label: 'A1', fn: () => run3(fA1) },
172
+ { label: 'A2', fn: () => run3(fA2) },
173
+ { label: 'B ', fn: () => run3(fB) },
174
+ ];
175
+ if (retS3 !== 0)
176
+ s3Arms.push({ label: 'C ', fn: () => run3(fC) });
177
+ step('bench_sendmessage_nonnull: S1/S3 batch=' + String(BATCH) + '×' + String(ROUNDS) +
178
+ 'r, S2 n=' + String(N_S2) + '×' + String(ROUNDS_S2) + 'r (median 主指标)');
179
+ microSuite();
180
+ suite('S1 WM_NULL 纯数字参数 (最灵敏测点)', BATCH, ROUNDS, s1Arms);
181
+ suite('S2 WM_SETTEXT 字符串 lParam (FFI 侧含 WCHAR.encode)', N_S2, ROUNDS_S2, s2Arms);
182
+ suite('S3 WM_GETTEXT 缓冲出参往返', BATCH, ROUNDS, s3Arms);
183
+ gui.DestroyWindow(win);
184
+ step('DONE');
185
+ if (log)
186
+ log.close();
187
+ std.exit(0);