From 70835d3d5f030deadf606e1c79c70a60883e50c9 Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:04:05 +0800 Subject: [PATCH] perf(wasm): skip payload text during WAT framing Fixes #5760 --- experiments/analysis-efficiency/README.md | 4 + .../wasm-string-framing.json | 561 ++++++++++++++++++ .../wasm-string-framing.md | 107 ++++ .../wasm-string-framing.mjs | 184 ++++++ src/wasm/WabtInterface.ts | 6 +- src/wasm/WabtString.test.ts | 56 ++ src/wasm/WabtString.ts | 28 +- src/wasm/WabtWat.ts | 20 +- 8 files changed, 944 insertions(+), 22 deletions(-) create mode 100644 experiments/analysis-efficiency/wasm-string-framing.json create mode 100644 experiments/analysis-efficiency/wasm-string-framing.md create mode 100644 experiments/analysis-efficiency/wasm-string-framing.mjs create mode 100644 src/wasm/WabtString.test.ts diff --git a/experiments/analysis-efficiency/README.md b/experiments/analysis-efficiency/README.md index 2f2f049544..b19989706f 100644 --- a/experiments/analysis-efficiency/README.md +++ b/experiments/analysis-efficiency/README.md @@ -1,5 +1,9 @@ # Analysis efficiency measurements +The [WAT framing checkpoint](wasm-string-framing.md) measures structural scanning +on real SQLite and VS Code components, with unchanged Evidence and token costs, +small whole-task timing gains and no memory improvement. + The [native inventory view checkpoint](native-inventory-views.md) compares complete delivery with summaries and selected rows on real `true` and jq inventories, including oversized-response recovery, portable old Evidence, diff --git a/experiments/analysis-efficiency/wasm-string-framing.json b/experiments/analysis-efficiency/wasm-string-framing.json new file mode 100644 index 0000000000..d27ced7efa --- /dev/null +++ b/experiments/analysis-efficiency/wasm-string-framing.json @@ -0,0 +1,561 @@ +{ + "schema_version": 1, + "issue": 5760, + "baseline_source": "d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f", + "candidate_code_source": "9776d78f6c799cba8d8cf5c9b3139a86449eaebd", + "integration_base": "335c042d166b2c8b2c0600935cf77607ae11edbc", + "toolchain": { + "node": "24.18.0", + "npm": "11.16.0", + "wabt": "1.0.42", + "tokenizer": "0.12.0", + "encoding": "cl100k_base" + }, + "catalog_sha256": "4586abd317171cbb307295cc687c24f7f45e603959538643325895b546a38242", + "catalog_tools": 212, + "package_lock_sha256": "699a14749e3825c62430faeb18224190fee1c9347760494e38e334e180bea6ec", + "runtime_mcp_sha256": { + "baseline": "fa6831f05d97b6b28642b709b5497e351d2ee632dd1abc0d2f87a648057afe87", + "candidate": "e4af82d847fe3d9816f4f7ae1f584ef129ba151bc5a6f95080a7519fd08be2cf" + }, + "sources_sha256": { + "WabtString.ts": "398f3cd98c087740a553768d10c21912d28f33e13bdab589eec760da4ed0b9fd", + "WabtWat.ts": "70fb9acfbe747be4fa5d8be235012a88f94485bc0baf0fbba1fe22f18b87f44b", + "WabtInterface.ts": "1a63c834a46268bbaf97e6eb39a99b3ccd3f2893527749d65ee6f5026bc2782a" + }, + "native_tools_sha256": { + "wasm-validate": "6b722ed0df68204cfc7a8effdf1fc2ddeac2d184bd211cdf7fdfa5f9d49d3eb5", + "wasm-objdump": "dad264def1a3b7755859c5bf48227f01259a95fa82b7c38358b12b9e70b27a41", + "wasm2wat": "4322d652f650c1ee117b549c9722f42fc93eb4676ff046d60ba033b5639697a5" + }, + "artifacts": { + "small": { + "sha256": "f399acbd52258f950f24dd5ca4a82674f1f26cea60e7be7c5daaade568768fda", + "bytes": 80, + "wat_sha256": "918ecd8a14daf20b58707d0879efda8b38df37f219279688b3aa3a812ae5646a", + "wat_bytes": 270, + "answer_normalized_sha256": "8eb8aedcf17bac4a98d34f5a004b5541f671350f94918575e2fc4eee9fd2db29", + "parent_evidence_id": "ev_88b35d9f926672b9e42fc4e1514090eaf2f99bb918439f1270dd53f7fe684e74" + }, + "sqlite": { + "sha256": "2ee8f3dab694532afc8840e07703127287662d08b74e6ff50491ce63f00d5752", + "bytes": 868907, + "wat_sha256": "7b1f8cd4fcbf310f4203ce64e15b4eb068ecdf29838551137245efecb7283f81", + "wat_bytes": 15382128, + "answer_normalized_sha256": "330b84e01541f70a964c826754da3e5f49a1bb1bd535ae1d1f00fb44a1157ff6", + "parent_evidence_id": "ev_899ecb126cd0a283169641085fa10e46460504bf8bc8eba69580fa6d641e431e" + }, + "css": { + "sha256": "8a23977fe271357cce6f254ef88c9bebf3854602d8046605aef6a45c02135c59", + "bytes": 128668, + "wat_sha256": "c53273cb7da0867ac52490d083c7952fcc538b3848c6d0d1f2e2621e2ded23c0", + "wat_bytes": 13599679, + "answer_normalized_sha256": "3d301feaa48a646c4d9f2a026353544519cc4694c9840b1c764d81ec9ba7fb65", + "parent_evidence_id": "ev_27108db83ae4d0a9289c23dace9c0e9e6d573f72f1e982b490c7f45394ba702c" + } + }, + "samples": [ + { + "case": "small", + "lane": "baseline", + "round": 1, + "wall_seconds": 12.417, + "cpu_seconds": 12.114979, + "memory_peak_bytes": 823410688, + "transport_proxy_tokens": 894936, + "fresh_inspect_ms": 1781.0228269999998, + "repeat_inspect_ms": 1793.3802260000011, + "reply_bytes": 36180, + "raw_guard_log_sha256": "f4ab763c21c4dd22f07ab978690966fd1efc6961216e41e195316e7330c269d6" + }, + { + "case": "small", + "lane": "candidate", + "round": 1, + "wall_seconds": 10.191, + "cpu_seconds": 9.868799, + "memory_peak_bytes": 829722624, + "transport_proxy_tokens": 894936, + "fresh_inspect_ms": 1298.6234909999998, + "repeat_inspect_ms": 1917.7533390000008, + "reply_bytes": 36182, + "raw_guard_log_sha256": "5cfd93018a2cabbe5b9fa4b9062fbed8e6da638ad5672fd566f5cf76182fecd5" + }, + { + "case": "sqlite", + "lane": "baseline", + "round": 1, + "wall_seconds": 14.317, + "cpu_seconds": 13.845753, + "memory_peak_bytes": 1032982528, + "transport_proxy_tokens": 895044, + "fresh_inspect_ms": 3405.006531, + "repeat_inspect_ms": 3345.840587999999, + "reply_bytes": 37248, + "raw_guard_log_sha256": "46b1b2e9f02e41be7a7cbae8a311b799ab52148a150ec5c0c683e0745c44ad03" + }, + { + "case": "sqlite", + "lane": "candidate", + "round": 1, + "wall_seconds": 13.311, + "cpu_seconds": 12.956772, + "memory_peak_bytes": 1029627904, + "transport_proxy_tokens": 895044, + "fresh_inspect_ms": 2838.048675, + "repeat_inspect_ms": 3233.226218, + "reply_bytes": 37250, + "raw_guard_log_sha256": "8933144b3f6fd100de72b1c1f22a791b7c4bef74c355bb9211be4c9e9383fe4b" + }, + { + "case": "css", + "lane": "baseline", + "round": 1, + "wall_seconds": 13.211, + "cpu_seconds": 12.79919, + "memory_peak_bytes": 1099599872, + "transport_proxy_tokens": 895430, + "fresh_inspect_ms": 2823.246057, + "repeat_inspect_ms": 3302.541784000001, + "reply_bytes": 37426, + "raw_guard_log_sha256": "9f4b7dfcd97faf8c85463a4c17f0612b6c37b7e29843f5e52b43df27aaae0ac6" + }, + { + "case": "css", + "lane": "candidate", + "round": 1, + "wall_seconds": 12.912, + "cpu_seconds": 12.46982, + "memory_peak_bytes": 1110261760, + "transport_proxy_tokens": 895430, + "fresh_inspect_ms": 2839.796418, + "repeat_inspect_ms": 3002.9903570000006, + "reply_bytes": 37428, + "raw_guard_log_sha256": "dc2ed426cd1191ca779a4e736bceb3d0001a5b8344dbba01544ecd91a6bb73b6" + }, + { + "case": "small", + "lane": "candidate", + "round": 2, + "wall_seconds": 10.295, + "cpu_seconds": 9.986392, + "memory_peak_bytes": 807264256, + "transport_proxy_tokens": 894936, + "fresh_inspect_ms": 1687.949067, + "repeat_inspect_ms": 1907.0628659999993, + "reply_bytes": 36182, + "raw_guard_log_sha256": "376199e896af92b6351d2dde7c765b5aa4b1feac0e9664da934e24732eaa22ec" + }, + { + "case": "small", + "lane": "baseline", + "round": 2, + "wall_seconds": 10.192, + "cpu_seconds": 9.86319, + "memory_peak_bytes": 826806272, + "transport_proxy_tokens": 894936, + "fresh_inspect_ms": 1621.7653269999996, + "repeat_inspect_ms": 1937.1286520000012, + "reply_bytes": 36180, + "raw_guard_log_sha256": "2d40d7309203eefcb7cbb5a5e81640e728839eb30f29cad017584738ea50616f" + }, + { + "case": "sqlite", + "lane": "candidate", + "round": 2, + "wall_seconds": 13.52, + "cpu_seconds": 13.133355, + "memory_peak_bytes": 1211944960, + "transport_proxy_tokens": 895044, + "fresh_inspect_ms": 3167.13338, + "repeat_inspect_ms": 3425.1366179999986, + "reply_bytes": 37250, + "raw_guard_log_sha256": "a962200a36a2530b044548b475b28d7234f73b875ffbc3fe16ef4b36e61ae49a" + }, + { + "case": "sqlite", + "lane": "baseline", + "round": 2, + "wall_seconds": 13.515, + "cpu_seconds": 13.170183, + "memory_peak_bytes": 1017561088, + "transport_proxy_tokens": 895044, + "fresh_inspect_ms": 3224.859665, + "repeat_inspect_ms": 3230.52117, + "reply_bytes": 37248, + "raw_guard_log_sha256": "0452dae8c26a023bd6220bca03dfd7a58425ef579f8e7964f04b4cb944723f84" + }, + { + "case": "css", + "lane": "candidate", + "round": 2, + "wall_seconds": 12.112, + "cpu_seconds": 11.727334, + "memory_peak_bytes": 1186643968, + "transport_proxy_tokens": 895430, + "fresh_inspect_ms": 2662.82945, + "repeat_inspect_ms": 2489.199032999999, + "reply_bytes": 37428, + "raw_guard_log_sha256": "9d27bfa9720f4291324aa6fc47e44ffdee8b6fbc63ea29a2c82d297d0ba1f0c6" + }, + { + "case": "css", + "lane": "baseline", + "round": 2, + "wall_seconds": 12.409, + "cpu_seconds": 12.038676, + "memory_peak_bytes": 1079783424, + "transport_proxy_tokens": 895430, + "fresh_inspect_ms": 2793.9973870000003, + "repeat_inspect_ms": 2541.021634999999, + "reply_bytes": 37426, + "raw_guard_log_sha256": "ddc43e4517a2ed634d5a969104b9051ac4e654485d3552f19d4c141a5834b205" + }, + { + "case": "small", + "lane": "baseline", + "round": 3, + "wall_seconds": 9.797, + "cpu_seconds": 9.449418, + "memory_peak_bytes": 822263808, + "transport_proxy_tokens": 894936, + "fresh_inspect_ms": 1612.1414539999996, + "repeat_inspect_ms": 1833.922163000001, + "reply_bytes": 36180, + "raw_guard_log_sha256": "92aa46fe57caeae60cb3105b2ac370a8f8463afed4a7f666f46be885cad0ca4d" + }, + { + "case": "small", + "lane": "candidate", + "round": 3, + "wall_seconds": 10.192, + "cpu_seconds": 9.868713, + "memory_peak_bytes": 829575168, + "transport_proxy_tokens": 894936, + "fresh_inspect_ms": 1629.0064629999997, + "repeat_inspect_ms": 1962.5034050000004, + "reply_bytes": 36182, + "raw_guard_log_sha256": "0451dacd244ced0ac56ceb0f73b39e1b86fa1517a0ec88391a3ab37d45b9bd20" + }, + { + "case": "sqlite", + "lane": "baseline", + "round": 3, + "wall_seconds": 13.714, + "cpu_seconds": 13.319143, + "memory_peak_bytes": 1196531712, + "transport_proxy_tokens": 895044, + "fresh_inspect_ms": 3228.7134460000007, + "repeat_inspect_ms": 3397.271211000001, + "reply_bytes": 37248, + "raw_guard_log_sha256": "d3f7e1e572a4c3251ca479a3c018d2f024b80103e443a8daeb125bd896165d2a" + }, + { + "case": "sqlite", + "lane": "candidate", + "round": 3, + "wall_seconds": 12.709, + "cpu_seconds": 12.349818, + "memory_peak_bytes": 1205280768, + "transport_proxy_tokens": 895044, + "fresh_inspect_ms": 3071.7333470000003, + "repeat_inspect_ms": 3024.3819120000007, + "reply_bytes": 37250, + "raw_guard_log_sha256": "57b405e4a4e8a9acbb4d9e4bb4663d931ad7ffec52a2b6a2969a7362287db892" + }, + { + "case": "css", + "lane": "baseline", + "round": 3, + "wall_seconds": 12.315, + "cpu_seconds": 11.921752, + "memory_peak_bytes": 1081831424, + "transport_proxy_tokens": 895430, + "fresh_inspect_ms": 2798.227023, + "repeat_inspect_ms": 2563.7297770000005, + "reply_bytes": 37426, + "raw_guard_log_sha256": "2f6fca011f6718111b455d8104121ff6880131f58e0a82df069efef9445f5c07" + }, + { + "case": "css", + "lane": "candidate", + "round": 3, + "wall_seconds": 11.901, + "cpu_seconds": 11.54646, + "memory_peak_bytes": 1123659776, + "transport_proxy_tokens": 895430, + "fresh_inspect_ms": 2600.1125399999996, + "repeat_inspect_ms": 2398.082381, + "reply_bytes": 37428, + "raw_guard_log_sha256": "180726dbb90af48c442109f75ccc50bf243ebcc8392908f9322eb91100a5f179" + } + ], + "parser_samples": [ + { + "case": "small.decoded", + "lane": "baseline", + "round": 1, + "elapsed_ms": [ + 2.9900399999999934, 1.9184900000000056, 0.3164599999999922, + 0.622399999999999, 0.6818110000000104, 1.7410099999999886 + ], + "result_sha256": "beb5c7e4e8da08e3c104753b768c8ca40559d517dba80c483253991721b39c7f", + "wall_seconds": 0.422, + "cpu_seconds": 0.169929, + "memory_peak_bytes": 28078080 + }, + { + "case": "small.decoded", + "lane": "candidate", + "round": 1, + "elapsed_ms": [ + 3.8333299999999895, 1.5887500000000045, 0.4467700000000008, + 0.3875499999999903, 0.3238100000000088, 0.21590999999999383 + ], + "result_sha256": "beb5c7e4e8da08e3c104753b768c8ca40559d517dba80c483253991721b39c7f", + "wall_seconds": 0.323, + "cpu_seconds": 0.154998, + "memory_peak_bytes": 27508736 + }, + { + "case": "sqlite", + "lane": "baseline", + "round": 1, + "elapsed_ms": [ + 248.049268, 224.29203599999994, 176.49019599999997, 155.392424, + 162.08057399999996, 172.70000600000003 + ], + "result_sha256": "74eb7108ddd459dfa7bfb7a1fe653c8862eb43acd89d91ae033e7ac9158587ce", + "wall_seconds": 1.532, + "cpu_seconds": 1.326985, + "memory_peak_bytes": 71315456 + }, + { + "case": "sqlite", + "lane": "candidate", + "round": 1, + "elapsed_ms": [ + 103.56140300000001, 103.344674, 56.359200999999985, 43.188242, + 44.79720200000003, 44.11639200000002 + ], + "result_sha256": "74eb7108ddd459dfa7bfb7a1fe653c8862eb43acd89d91ae033e7ac9158587ce", + "wall_seconds": 0.827, + "cpu_seconds": 0.598442, + "memory_peak_bytes": 71557120 + }, + { + "case": "tree-sitter-css", + "lane": "baseline", + "round": 1, + "elapsed_ms": [ + 133.568665, 122.39741400000003, 110.63244400000002, 111.27637299999998, + 111.761034, 111.34842400000002 + ], + "result_sha256": "b59e5bdc32939c215443da6f3ce48e61fe2bfd86ff75b7a85d3c10a93b6fae7e", + "wall_seconds": 1.127, + "cpu_seconds": 0.882723, + "memory_peak_bytes": 55656448 + }, + { + "case": "tree-sitter-css", + "lane": "candidate", + "round": 1, + "elapsed_ms": [ + 15.454230999999993, 15.1781, 17.94840099999999, 15.499510999999984, + 15.028279999999995, 2.9674699999999916 + ], + "result_sha256": "b59e5bdc32939c215443da6f3ce48e61fe2bfd86ff75b7a85d3c10a93b6fae7e", + "wall_seconds": 0.424, + "cpu_seconds": 0.255557, + "memory_peak_bytes": 54427648 + }, + { + "case": "small.decoded", + "lane": "candidate", + "round": 2, + "elapsed_ms": [ + 2.492229000000009, 1.6617090000000019, 0.4688189999999963, + 0.40376000000000545, 0.18850899999999626, 0.19684000000000879 + ], + "result_sha256": "beb5c7e4e8da08e3c104753b768c8ca40559d517dba80c483253991721b39c7f", + "wall_seconds": 0.324, + "cpu_seconds": 0.14932, + "memory_peak_bytes": 27750400 + }, + { + "case": "small.decoded", + "lane": "baseline", + "round": 2, + "elapsed_ms": [ + 3.0103999999999758, 2.4378910000000076, 0.30259000000000924, + 0.5248499999999865, 0.6185799999999801, 1.3529399999999896 + ], + "result_sha256": "beb5c7e4e8da08e3c104753b768c8ca40559d517dba80c483253991721b39c7f", + "wall_seconds": 0.428, + "cpu_seconds": 0.219561, + "memory_peak_bytes": 30007296 + }, + { + "case": "sqlite", + "lane": "candidate", + "round": 2, + "elapsed_ms": [ + 150.102906, 136.73758599999996, 85.33841300000006, 42.61153200000001, + 45.540752, 43.07188099999996 + ], + "result_sha256": "74eb7108ddd459dfa7bfb7a1fe653c8862eb43acd89d91ae033e7ac9158587ce", + "wall_seconds": 0.959, + "cpu_seconds": 0.738843, + "memory_peak_bytes": 68399104 + }, + { + "case": "sqlite", + "lane": "baseline", + "round": 2, + "elapsed_ms": [ + 236.412329, 219.61998900000003, 163.37286700000004, 152.177506, + 153.764226, 153.57296699999984 + ], + "result_sha256": "74eb7108ddd459dfa7bfb7a1fe653c8862eb43acd89d91ae033e7ac9158587ce", + "wall_seconds": 1.532, + "cpu_seconds": 1.317278, + "memory_peak_bytes": 70189056 + }, + { + "case": "tree-sitter-css", + "lane": "candidate", + "round": 2, + "elapsed_ms": [ + 15.074011000000013, 22.501490000000018, 23.890242, 9.095690999999988, + 25.85492100000002, 16.470331000000016 + ], + "result_sha256": "b59e5bdc32939c215443da6f3ce48e61fe2bfd86ff75b7a85d3c10a93b6fae7e", + "wall_seconds": 0.524, + "cpu_seconds": 0.297941, + "memory_peak_bytes": 53497856 + }, + { + "case": "tree-sitter-css", + "lane": "baseline", + "round": 2, + "elapsed_ms": [ + 135.69754600000002, 128.91107599999998, 115.180316, 115.23454600000002, + 117.18869600000005, 103.74374499999999 + ], + "result_sha256": "b59e5bdc32939c215443da6f3ce48e61fe2bfd86ff75b7a85d3c10a93b6fae7e", + "wall_seconds": 1.13, + "cpu_seconds": 0.916205, + "memory_peak_bytes": 57880576 + }, + { + "case": "small.decoded", + "lane": "baseline", + "round": 3, + "elapsed_ms": [ + 2.7721499999999963, 2.0805500000000023, 0.2946500000000043, + 0.4885200000000083, 0.37317999999999074, 1.052209999999988 + ], + "result_sha256": "beb5c7e4e8da08e3c104753b768c8ca40559d517dba80c483253991721b39c7f", + "wall_seconds": 0.321, + "cpu_seconds": 0.161694, + "memory_peak_bytes": 28037120 + }, + { + "case": "small.decoded", + "lane": "candidate", + "round": 3, + "elapsed_ms": [ + 2.4845909999999947, 1.5559899999999942, 1.9349599999999896, + 0.6082500000000124, 0.23478000000000065, 0.22989100000000917 + ], + "result_sha256": "beb5c7e4e8da08e3c104753b768c8ca40559d517dba80c483253991721b39c7f", + "wall_seconds": 0.321, + "cpu_seconds": 0.154815, + "memory_peak_bytes": 27426816 + }, + { + "case": "sqlite", + "lane": "baseline", + "round": 3, + "elapsed_ms": [ + 219.92179099999998, 204.47974999999997, 159.57980799999996, + 152.3218579999999, 150.58105799999998, 149.3406970000001 + ], + "result_sha256": "74eb7108ddd459dfa7bfb7a1fe653c8862eb43acd89d91ae033e7ac9158587ce", + "wall_seconds": 1.428, + "cpu_seconds": 1.22534, + "memory_peak_bytes": 71987200 + }, + { + "case": "sqlite", + "lane": "candidate", + "round": 3, + "elapsed_ms": [ + 103.150995, 103.714395, 58.592934000000014, 47.94820199999998, + 47.04763299999996, 47.239012 + ], + "result_sha256": "74eb7108ddd459dfa7bfb7a1fe653c8862eb43acd89d91ae033e7ac9158587ce", + "wall_seconds": 0.828, + "cpu_seconds": 0.60423, + "memory_peak_bytes": 72187904 + }, + { + "case": "tree-sitter-css", + "lane": "baseline", + "round": 3, + "elapsed_ms": [ + 126.407417, 121.22317700000002, 109.654766, 108.04800599999999, + 109.459836, 100.50628600000005 + ], + "result_sha256": "b59e5bdc32939c215443da6f3ce48e61fe2bfd86ff75b7a85d3c10a93b6fae7e", + "wall_seconds": 1.029, + "cpu_seconds": 0.856826, + "memory_peak_bytes": 58101760 + }, + { + "case": "tree-sitter-css", + "lane": "candidate", + "round": 3, + "elapsed_ms": [ + 14.889070999999987, 15.635390999999998, 17.013621, 15.000391000000008, + 17.56236100000001, 2.893010000000004 + ], + "result_sha256": "b59e5bdc32939c215443da6f3ce48e61fe2bfd86ff75b7a85d3c10a93b6fae7e", + "wall_seconds": 0.422, + "cpu_seconds": 0.269288, + "memory_peak_bytes": 57966592 + } + ], + "limits": { + "cpu_affinity": [15], + "cpu_quota_percent": 100, + "hard_memory_bytes": 4294967296, + "early_stop_bytes": 3221225472, + "swap_bytes": 0, + "task_ceiling": 192 + }, + "limitations": [ + "Fresh server sessions; OS file caches warm/unflushed. Parser samples use one fresh and five repeated in-memory parses per process.", + "SDK transport proxy counts one complete catalog plus five named request/reply envelopes. Initialization/framing and actual model usage are unmeasured. No token saving is claimed.", + "Memory is kernel process-family memory.peak including file cache; final counters after transient scope removal remain unknown.", + "First string-only 18-task development cohort, one invalid-source-kind harness attempt, and the cache-hit build with unverified payload cleanup are preserved outside Git and excluded from final benefit claims.", + "Independent native WebAssembly.Module import/export oracle compiles without instantiating or executing target code. It does not prove every typed signature or unknown future proposal." + ], + "resource_qualification": { + "completed_mcp_tasks": 18, + "completed_parser_processes": 18, + "all_payload_exit_codes_zero": true, + "all_payload_cleanup_verified": true, + "all_payload_counters_verified": true, + "all_memory_high_hard_oom_and_task_limit_events_zero": true, + "final_whole_scope_counters": "unknown" + }, + "call_sequence": [ + "inspect_wasm_artifact(summary):fresh", + "inspect_analysis_view(exports,first4)", + "inspect_wasm_artifact(summary):repeat", + "inspect_analysis_view(exports,first4)", + "export_evidence_bundle" + ], + "transport_proxy_scope": "One complete SDK catalog and all five named request/reply envelopes, compact UTF-8 JSON encoded with cl100k_base; not actual model input." +} diff --git a/experiments/analysis-efficiency/wasm-string-framing.md b/experiments/analysis-efficiency/wasm-string-framing.md new file mode 100644 index 0000000000..e4d3d4522e --- /dev/null +++ b/experiments/analysis-efficiency/wasm-string-framing.md @@ -0,0 +1,107 @@ +# WAT framing costs on shipped WASM + +[Issue #5760](https://github.com/morluto/rea/issues/5760) follows the current +WASM summary workflow; no new tool, selector, cache or default is needed. +The [curated measurements](wasm-string-framing.json) pin source, runtime, +artifact, producer and answer fingerprints, including every final sample. + +## Owner and preserved behavior + +WABT's WAT framing scanned instruction text character by character. Interface +tokenization also decoded quoted bytes merely to obtain an end position. +`WabtString` now owns a shared quoted-string boundary operation, preserving +escaped-quote/backslash parity. The top-level framer searches for structural +ASCII delimiters and skips string payloads. Name and custom-payload consumers +still decode their bytes; unsupported typed signatures remain unknown. +Nested/line comments, exact source forms and full retained WAT remain available. +Data forms were already excluded from interface parsing; this does not add a +new data filter or reinterpret raw WASM outside WABT. + +Baseline source is `d0067ba34a39`; candidate code is +`9776d78f6c799cba8d8cf5c9b3139a86449eaebd`, based on latest main +`335c042d166b` (the intervening main change is testing documentation). +Linux x64, Node 24.18.0/npm 11.16.0, native WABT 1.0.42. Three alternating +baseline/candidate, candidate/baseline, baseline/candidate pairs ran serially. +Each task starts a fresh production stdio MCP server, discovers all 212 tools, +inspects the selected artifact twice with existing summary delivery, reads the +first four exact exports after each inspection, exports complete Evidence and +closes. OS file caches are warm/unflushed; no cold-disk claim is made. + +The public SQLite Wasm 3.53.4-build1 and VS Code tree-sitter-wasm 0.3.1 CSS +components match the pinned [corpus](corpus.json) archive and member digests. +An 80-byte source-assembled module supplies the small control, with an escaped +import name. Native `WebAssembly.Module` import/export metadata independently +checks all returned names/kinds without instantiating or executing target code. +A separate native `wasm2wat --enable-all` output checks exact full WAT and its +SHA-256. Every before/after original Evidence ID, normalized report digest, +source byte digest, selected export and catalog digest agrees. This does not +independently prove every typed signature or future WASM proposal. + +## Completed-task costs + +Medians of three complete tasks per side; kernel process-family memory includes +file cache. Wall time includes startup, discovery, all five investigation RPCs, +client validation, independent checks, export and transport cleanup. + +| Artifact | Wall seconds, before → after | CPU seconds, before → after | Peak MiB, before → after | +| ------------- | ---------------------------: | --------------------------: | -----------------------: | +| Small control | 10.192 → 10.192 | 9.863 → 9.869 | 785.3 → 791.1 | +| SQLite | 13.714 → 13.311 | 13.319 → 12.957 | 985.1 → 1149.4 | +| CSS grammar | 12.409 → 12.112 | 12.039 → 11.727 | 1031.7 → 1071.6 | + +Whole-task median wall reductions are about 2.9%/2.4% for SQLite/CSS; small-task +time is unchanged. Three pairs are descriptive observations, not statistical +or cross-provider guarantees. Memory does not improve: SQLite's median is +higher, with overlapping 970–1141 versus 982–1156 MiB ranges. No heap-retention +cause or memory-saving claim is established. + +Separate fresh-process parser cohorts use one first parse and five repeated +in-memory parses, checking the complete parsed-result digest after each parse. +Median first/warm parse milliseconds: SQLite 236.41/153.76 → 103.56/45.54; +CSS 133.57/111.35 → 15.07/15.64; small 2.99/0.62 → 2.49/0.40. These phase +improvements do not include native WABT production or complete task costs. + +The unchanged catalog and all five named request/reply envelopes cost median +894,936 / 895,044 / 895,430 `cl100k_base` tokens for small/SQLite/CSS in both +implementations. This is a single serialized SDK transport proxy using +tiktoken 0.12.0; actual model input/usage, repeated model namespaces and protocol +initialization/framing are unmeasured. No token saving is claimed. + +All 18 final MCP tasks and 18 parser processes completed under one CPU, +4 GiB hard/3 GiB early-stop memory, no swap and 192 tasks, with payload-exit +counters and cleanup verified and no limit/OOM events. Final whole-scope counters +after transient scope removal remain unknown. Raw binaries, exports, +transcripts and logs stay outside Git. The earlier string-only 18-task +cohort, an invalid source-kind harness attempt and a cache-hit build with +unverified payload cleanup are retained and excluded from final benefit claims. + +## Reproduction + +Build/freeze each runtime with the pinned toolchain; keep the selected artifact, +WABT directory and independently produced WAT paths identical across lanes: + +```sh +python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \ + node experiments/analysis-efficiency/wasm-string-framing.mjs \ + --runtime /absolute/runtime --artifact /absolute/input.wasm \ + --wat /absolute/native-output.wat --tools /absolute/wabt/bin \ + --output /absolute/new-output-directory +``` + +For parser-only cohorts, import `dist/wasm/WabtWat.js` from each frozen runtime, +load the same native WAT once, and call `parseWabtWat(text)` six times. Measure +each call with `performance.now()`; outside that timer, hash +`JSON.stringify(result)` and require the same digest for every call and lane. +Keep each artifact in a separate fresh guarded process. + +Source verification passed 76 tests across nine files, including malformed +strings, slash parity, comments, invalid names, capacity/partial facts, +cancellation, cleanup retry and shared application behavior. `npm run check` +passed typecheck, lint, formatting, Knip, metadata and architecture guards. +Four compiled CLI/MCP/acceptance files passed another 14 tests. The native WABT +artifact verifier passed 51 CLI/MCP cases; the native interface verifier passed +nine cases, including all admitted kinds, escaped/BOM names, padded LEB128, +signature changes, duplicate imports and unsupported reference semantics. +The build used a separate 6 GiB hard/5 GiB early-stop guard; the full check used +8/7 GiB, peaking at 5.37 GiB without resource events. These are build/check +budgets; runtime tasks retain the smaller bounds above. diff --git a/experiments/analysis-efficiency/wasm-string-framing.mjs b/experiments/analysis-efficiency/wasm-string-framing.mjs new file mode 100644 index 0000000000..67cf0c634e --- /dev/null +++ b/experiments/analysis-efficiency/wasm-string-framing.mjs @@ -0,0 +1,184 @@ +/** Compare unchanged public WASM tasks; raw inputs, exports and transcripts stay outside Git. */ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { createWriteStream } from "node:fs"; +import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { tmpdir } from "node:os"; +import { finished } from "node:stream/promises"; +import { parseArgs } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { createRetainedQueryCaller } from "./retained-query-calls.mjs"; +import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs"; + +const { values } = parseArgs({ + options: Object.fromEntries( + ["runtime", "artifact", "wat", "tools", "output"].map((name) => [ + name, + { type: "string" }, + ]), + ), +}); +for (const name of ["runtime", "artifact", "wat", "tools", "output"]) + assert.ok( + values[name] && values[name] === resolve(values[name]), + `${name} must be absolute`, + ); +const hash = (value) => createHash("sha256").update(value).digest("hex"); +const bytes = await readFile(values.artifact); +const nativeWat = await readFile(values.wat, "utf8"); +// Native compilation validates the independent name inventory; no instance or target code runs. +const module = new WebAssembly.Module(bytes); +const nativeImports = WebAssembly.Module.imports(module); +const nativeExports = WebAssembly.Module.exports(module); +await mkdir(values.output, { mode: 0o700 }); +const account = await mkdtemp(join(tmpdir(), "rea-wat-framing-")); +const client = new Client({ name: "wasm-string-framing", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [join(values.runtime, "scripts/rea.mjs"), "mcp"], + env: { + HOME: account, + PATH: process.env.PATH, + NODE_OPTIONS: "--max-old-space-size=2048", + UV_THREADPOOL_SIZE: "1", + REA_LOG_LEVEL: "silent", + REA_WABT_BIN_DIRECTORY: values.tools, + TMPDIR: account, + }, + stderr: "pipe", +}); +const serverLog = createWriteStream(join(values.output, "server.log")); +const serverLogFinished = finished(serverLog); +serverLogFinished.catch(() => {}); +transport.stderr?.pipe(serverLog); +const report = { + artifact_sha256: hash(bytes), + artifact_bytes: bytes.length, + calls: [], + answers: [], +}; +const transcript = []; +const call = createRetainedQueryCaller(client, (receipt, event) => { + report.calls.push(receipt); + transcript.push(event); +}); +const observe = async (name, input, phase) => { + const reply = await call(name, input, phase); + assert.notEqual(reply.isError, true, JSON.stringify(reply)); + assert.ok(reply.structuredContent); + return reply.structuredContent; +}; +const started = performance.now(); +try { + await client.connect(transport); + const catalog = await client.listTools(); + assert.equal(catalog.nextCursor, undefined); + await writeFile(join(values.output, "catalog.json"), JSON.stringify(catalog)); + report.catalog_sha256 = hash(JSON.stringify(catalog)); + report.catalog_tools = catalog.tools.length; + const definitions = new Map( + catalog.tools.map((definition) => [definition.name, definition]), + ); + const originalCall = client.callTool.bind(client); + client.callTool = (input, options) => + originalCall(input, { + ...options, + toolDefinition: definitions.get(input.name), + }); + const observations = []; + for (const phase of ["fresh", "repeat"]) { + const summary = await observe( + "inspect_wasm_artifact", + { path: values.artifact, detail: "summary" }, + phase, + ); + const parentId = summary.normalized_result.parent_evidence_id; + const exports = await observe( + "inspect_analysis_view", + { + source: { kind: "retained-evidence", evidence_id: parentId }, + view: { kind: "wasm", facet: "exports", offset: 0, limit: 4 }, + }, + `${phase}:exports`, + ); + assert.equal(exports.normalized_result.parent_evidence_id, parentId); + observations.push({ summary, exports, parentId }); + } + await observe( + "export_evidence_bundle", + { path: join(values.output, "evidence.json") }, + "export", + ); + const bundle = JSON.parse( + await readFile(join(values.output, "evidence.json"), "utf8"), + ); + const kind = (value) => (value === "func" ? "function" : value); + for (const { summary, exports, parentId } of observations) { + const parent = bundle.records.find( + (record) => record.evidence_id === parentId, + ); + assert.ok(parent); + assert.equal(parent.subject.digest.sha256, report.artifact_sha256); + const facts = parent.normalized_result; + assert.equal(facts.validation, "valid"); + assert.equal(facts.wat.text, nativeWat); + assert.equal(facts.wat.sha256, hash(nativeWat)); + assert.equal(facts.interface.state, "complete"); + assert.deepEqual( + facts.interface.entries + .filter((entry) => entry.direction === "import") + .map((entry) => ({ + module: entry.module, + name: entry.name, + kind: kind(entry.kind), + })), + nativeImports, + ); + assert.deepEqual( + facts.interface.entries + .filter((entry) => entry.direction === "export") + .map((entry) => ({ name: entry.name, kind: kind(entry.kind) })), + nativeExports, + ); + assert.deepEqual(exports.normalized_result.item, facts.exports.slice(0, 4)); + assert.equal( + summary.normalized_result.summary.available_counts.imports, + nativeImports.length, + ); + assert.equal( + summary.normalized_result.summary.available_counts.exports, + nativeExports.length, + ); + assert.equal(facts.runtime_execution, "not-performed"); + assert.equal(facts.network_fetch, "not-performed"); + report.answers.push({ + parent_evidence_id: parentId, + normalized_sha256: hash(JSON.stringify(facts)), + exports: exports.normalized_result.item, + }); + } + assert.deepEqual(report.answers[0], report.answers[1]); + assert.equal(hash(await readFile(values.artifact)), report.artifact_sha256); + report.status = "passed"; +} catch (cause) { + report.status = "failed"; + report.failure = String(cause?.stack ?? cause); + process.exitCode = 1; +} finally { + report.elapsed_ms_before_cleanup = performance.now() - started; + if ( + !(await finalizeRetainedQueryReceipt({ + report, + client, + transport, + account, + serverLog, + serverLogFinished, + output: values.output, + transcript, + })) + ) + process.exitCode = 1; +} diff --git a/src/wasm/WabtInterface.ts b/src/wasm/WabtInterface.ts index 65e6fc0fa4..df47208995 100644 --- a/src/wasm/WabtInterface.ts +++ b/src/wasm/WabtInterface.ts @@ -7,7 +7,7 @@ import type { WasmInterfaceSignature, WasmInterfaceValueType, } from "../domain/wasm/wasmInterface.js"; -import { watString } from "./WabtString.js"; +import { watString, watStringEnd } from "./WabtString.js"; /** Split direct WAT children only; function instruction bodies are never materialized. */ const parts = (form: string): string[] => { @@ -40,12 +40,12 @@ const parts = (form: string): string[] => { } const start = i; if (form[i] === '"') { - i = watString(form, i + 1).end; + i = watStringEnd(form, i + 1); } else if (form[i] === "(") { let depth = 1; i++; while (i < form.length && depth > 0) { - if (form[i] === '"') i = watString(form, i + 1).end; + if (form[i] === '"') i = watStringEnd(form, i + 1); else { if (form[i] === "(") depth++; else if (form[i] === ")") depth--; diff --git a/src/wasm/WabtString.test.ts b/src/wasm/WabtString.test.ts new file mode 100644 index 0000000000..bdbaf40afe --- /dev/null +++ b/src/wasm/WabtString.test.ts @@ -0,0 +1,56 @@ +import { expect, it } from "vitest"; +import { parseWabtWat } from "./WabtWat.js"; +import { watString, watStringEnd } from "./WabtString.js"; + +it("frames escaped quotes and trailing backslashes without consuming the next form", () => { + const source = String.raw`prefix "a\"b\\" (export "next" (func 0))`; + const end = source.indexOf(" (export"); + expect(watStringEnd(source, 8)).toBe(end); + expect(watString(source, 8)).toEqual({ + bytes: Buffer.from('a"b\\'), + end, + }); + expect(watStringEnd('""', 1)).toBe(2); + for (const count of [1, 2, 3, 4, 5]) { + for (const tail of ['"tail', ""]) { + const value = "\\".repeat(count) + tail; + const encoded = JSON.stringify(value); + const source = encoded + ' (export "next" (func 0))'; + expect(watStringEnd(source, 1)).toBe(encoded.length); + expect(watString(source, 1).bytes).toEqual(Buffer.from(value)); + } + } +}); + +it("rejects missing terminators, including a dangling escape or escaped last quote", () => { + for (const source of ['"', '"payload', '"payload\\', '"payload\\"']) { + expect(() => watStringEnd(source, 1)).toThrow("Incomplete WABT string"); + expect(() => watString(source, 1)).toThrow("Incomplete WABT string"); + expect(() => parseWabtWat(`(module (import ${source}`)).toThrow(); + } +}); + +it("preserves quoted WAT punctuation, nested comments, escaped names and unknown signatures", () => { + const result = parseWabtWat(String.raw`(module + (; outer " (; inner ;) ;) + (import "env" "a\"b\\(;);;\00" (func (param i32))) + (memory 1 (future-proposal "ignored\ff")) + (export "memory" (memory 0)) + (data (i32.const 0) "(; );) ;; \" \\"))`); + expect(result.interface.entries).toMatchObject([ + { + direction: "import", + name: 'a"b\\(;);;\0', + signature: { kind: "func", params: ["i32"] }, + }, + { + direction: "export", + name: "memory", + signature: null, + issue: expect.any(String), + }, + ]); + expect(() => parseWabtWat('(module (import "env" "\\q" (func)))')).toThrow( + "Unsupported WABT name escape", + ); +}); diff --git a/src/wasm/WabtString.ts b/src/wasm/WabtString.ts index 79615b193d..715e4cb565 100644 --- a/src/wasm/WabtString.ts +++ b/src/wasm/WabtString.ts @@ -1,13 +1,21 @@ -/** Decode a producer WAT string to frame objdump's raw display text. */ -export const watString = (form: string, start: number) => { - let end = start; - while (end < form.length && form[end] !== '"') { - if (form[end] === "\\") end++; - end++; +/** Locate a WAT string's exclusive end without decoding or copying its payload. */ +export const watStringEnd = (form: string, start: number): number => { + for ( + let end = form.indexOf('"', start); + end !== -1; + end = form.indexOf('"', end + 1) + ) { + let escapes = end; + while (escapes > start && form[escapes - 1] === "\\") escapes--; + if ((end - escapes) % 2 === 0) return end + 1; } - if (end === form.length) - throw new Error("Incomplete WABT custom-section name."); - const name = form.slice(start, end); + throw new Error("Incomplete WABT string."); +}; + +/** Decode a producer WAT string only where its bytes are interpreted. */ +export const watString = (form: string, start: number) => { + const end = watStringEnd(form, start); + const name = form.slice(start, end - 1); // Escapes shrink; literal UTF-8 has the same byte extent as its source. const bytes = Buffer.alloc(Buffer.byteLength(name, "utf8")); let length = 0; @@ -42,5 +50,5 @@ export const watString = (form: string, start: number) => { index += 2; } } - return { bytes: bytes.subarray(0, length), end: end + 1 }; + return { bytes: bytes.subarray(0, length), end }; }; diff --git a/src/wasm/WabtWat.ts b/src/wasm/WabtWat.ts index 01b9f16bc5..669c04c8e5 100644 --- a/src/wasm/WabtWat.ts +++ b/src/wasm/WabtWat.ts @@ -1,5 +1,5 @@ import { parseWabtInterface } from "./WabtInterface.js"; -import { watString } from "./WabtString.js"; +import { watString, watStringEnd } from "./WabtString.js"; /** Read only module-name metadata from WABT's preserved name-section payload. */ const moduleNames = (bytes: Uint8Array): string[] => { @@ -48,22 +48,24 @@ export const parseWabtWat = (text: string) => { const debugModuleNames: string[] = []; let depth = 0; let start = 0; - let quoted = false; let commentDepth = 0; let lineComment = false; let closed = false; + const boundaries = /[()";]/gu; for (let index = 0; index < text.length; index++) { + // Instructions and whitespace inside a form cannot change its framing. + if (depth > 0 && !lineComment && commentDepth === 0) { + boundaries.lastIndex = index; + const boundary = boundaries.exec(text); + if (boundary === null) break; + index = boundary.index; + } const char = text[index]; const next = text[index + 1]; if (lineComment) { if (char === "\n") lineComment = false; continue; } - if (quoted) { - if (char === "\\") index++; - else if (char === '"') quoted = false; - continue; - } if (commentDepth > 0) { if (char === "(" && next === ";") { commentDepth++; @@ -87,7 +89,7 @@ export const parseWabtWat = (text: string) => { if (char === '"') { if (depth === 0) throw new Error("Unexpected string outside WABT WAT module."); - quoted = true; + index = watStringEnd(text, index + 1) - 1; continue; } if (char === "(") { @@ -133,7 +135,7 @@ export const parseWabtWat = (text: string) => { } else if (depth === 0 && char !== undefined && !/\s/u.test(char)) throw new Error("Unexpected text outside WABT WAT module."); } - if (depth !== 0 || quoted || commentDepth !== 0) + if (depth !== 0 || commentDepth !== 0) throw new Error("Incomplete WABT WAT output."); return { imports,