
从 grpc-go、arrow-rs 和 preact 出发,分别练习看懂一次调用在网线上是几轮帧、一张表在内存里是几段缓冲、一次界面更新先是一棵树,并给出今晚能跑通的最小路径。
:authority 与 Host 都缺的请求直接回 400。13stats.OutHeader 多了一个 Authority 字段,metadata 多了一个 ValueFromOutgoingContext。14grpc-status 与 grpc-message 就结束。5grpc-status 里。5GODEBUG=http2debug=2,它把帧打到 stderr。wrote HEADERS len=98 stream=1
read HEADERS len=14 stream=1 :status 200, content-type: application/grpc
read HEADERS len=24 END_STREAM stream=1 grpc-status=0, grpc-message=""
[stats] OutHeader authority="127.0.0.1:50051"
[stats] OutPayload wire_length=12
[stats] InPayload wire_length=18Ask{name:"world"} 编成 protobuf 是 7 字节,加 5 字节前缀正好 12。响应 18 字节同理,13 字节消息体加前缀。wrote HEADERS len=17 stream=3
read HEADERS len=2 stream=3
read HEADERS len=2 END_STREAM stream=3wire_length 是 13、15、15、15、15。第一条少两个字节,因为 idx 为 0,proto3 不为零值字段编码。grpc-status=3 和一句错误说明,一个 DATA 帧都没有。protoc 得单独装。发布页给的是 zip,解开就能用,本机用的是 36.2。go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
mkdir grpctry && cd grpctry && go mod init example.com/grpctry
go get google.golang.org/grpc@v1.85.0
mkdir demodemo/demo.proto 里只要两个消息、两个方法,其中一个改成流式:syntax = "proto3";
package demo;
option go_package = "example.com/grpctry/demo";
message Ask { string name = 1; int32 n = 2; }
message Reply { string text = 1; int32 idx = 2; }
service Greeter {
rpc Say (Ask) returns (Reply);
rpc Count (Ask) returns (stream Reply);
}protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative demo/demo.proto
go run ./cmd/server &
GODEBUG=http2debug=2 go run ./cmd/clientGODEBUG,数第一次调用出现几个 HEADERS、几个 DATA。本机是三个 HEADERS,DATA 一个都没打印。Ask 里的名字改成 11 个字符,wire_length 应该从 12 变成 18。算对一次,5 字节前缀就记住了。wrote HEADERS 的 len。98 掉到 17 就是 HPACK 在起作用。status.Error(codes.InvalidArgument, ...),再跑一遍。你会看到只有一个 HEADERS 帧,grpc-status=3。:authority。 注册一个 stats.Handler,在 OutHeader 里打印 Authority。这个字段是 1.85.0 才有的,值就是网线上发出去的那个伪头。1go.mod 里写的是 go 1.26.0。6Stop() 现在能打断正在等待的 GracefulStop(),卡住的进程有救。1:authority 与 Host 的请求会被直接拒掉。前面挂着自己写的 HTTP/2 客户端时,得补上这个头。1GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT 调,名字里带 experimental。1http2debug=2 不打 DATA 帧,本机数出来是零条。消息大小只能从 stats 的 WireLength 看,这一点别指望日志。id 是 Int64,一列 name 是十一个字符的字符串。程序建一个 RecordBatch,然后只做观察:rows=1000000 cols=2
arrow memory accounting = 28777496 bytes
row store: vec slots = 32000000 bytes, string heap = 20000000 bytes, total = 52000000 bytes
slice(100,10): offset=0 len=10 pointer delta=800 bytes first_value=100
record batch slice: pointer delta=800 bytes first_id=100
5 values, 2 nulls: values buffer = 40 bytes, null bitmap = 1 bytes
filter every-100th row: 10000 rows out of 1000000 in 456.641µs, output memory = 240220 bytes
parquet write: 24404164 bytes in 86.172526ms, 1 row groups
parquet read: id column only = 4.49705ms, both columns = 20.310449ms
parquet zstd(3) write: 1865768 bytes in 94.840177msVec<(i64, String)> 要 52 MB:每行 32 字节的槽位,再加堆上 11 字节的字符串。列式只要 28.8 MB,少了四成多。filter 留每第 100 行,一百万里留下 10,000 行,456 微秒,输出的 RecordBatch 只占 240,220 字节。id 一列,4.50 毫秒;两列都读,20.31 毫秒。差的那 15 毫秒就是那列字符串的代价。cargo new arrowtry && cd arrowtry
cargo add arrow@60.1 parquet@60.1src/main.rs 先只做一件事:把批次的体积打出来。use std::sync::Arc;
use arrow::array::{Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let ids: Int64Array = (0..1_000_000i64).collect::<Vec<i64>>().into();
let names = StringArray::from_iter_values((0..1_000_000).map(|i| format!("user-{i:07}")));
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("name", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(schema, vec![Arc::new(ids), Arc::new(names)])?;
println!("{}", batch.get_array_memory_size());
Ok(())
}cargo run --releaseInt64Array::values() 看它指向哪儿,再看 offsets() 的那段。一列变长字符串会多出 offset 缓冲,长度是行数加一。ids.slice(100, 10) 之后,把两个指针相减。本机是 800 字节,正好一百个 8 字节,证明没复制。10,000 × (8 + 4 + 11) 对一下,能看出它只留了选中行。ProjectionMask::leaves,只投影 id。本机从 20.31 毫秒掉到 4.50 毫秒。WriterProperties::set_compression(Compression::ZSTD(...)) 再写一遍,对比文件大小。24.4 MB 掉到 1.87 MB。11Symbol.for('pv')。16useState 取数组第 n 格,所以 hook 不能放在条件里。18src 下十三个文件共 2,197 行,最大的一个是 diff/index.js,833 行。li,然后插一行到最前面,把 DOM 的调用次数拦下来数:keyed prepend to 1000: 8.86 ms insertBefore=2 appendChild=0 removeChild=0 text=0
unkeyed prepend to 1000: 4.54 ms insertBefore=2 appendChild=0 removeChild=0 text=1000key,代价却换了地方。有 key 时一次文本写入都没有,无 key 时写了一千次。li,一次是把 li 插到列表头上。npm install,加一个 jsdom 当假浏览器。本机装完 node_modules 29 MB。mkdir preacttry && cd preacttry
npm init -y
npm i preact@11.1.0 jsdomvnode.mjs 里先看一眼 vnode 到底长什么样:import { JSDOM } from 'jsdom';
const dom = new JSDOM('<div id="root"></div>');
globalThis.window = dom.window;
globalThis.document = dom.window.document;
const { h, render } = await import('preact');
console.log(JSON.stringify(h('li', { class: 'x' }, 'hi')));
render(h('li', null, 'hello'), document.getElementById('root'));
console.log(document.getElementById('root').innerHTML);{"type":"li","props":{"class":"x","children":"hi"},"__k":null,"__":null,"__b":0,"__e":null,"__c":null,"__v":1,"__i":-1,"__u":0}
<li>hello</li>type 和 props 之后,preact/src/create-element.js 那 122 行就不难读了。insertBefore、appendChild、removeChild 包一层计数器,再把 CharacterData 的 data setter 也包一层,就能复现上面那两行结果。useState 的组件,打印它的 __c.__H,会看到一个长度 2 的数组。preact.mjs 12,088 字节,gzip 后 5,023;hooks 3,636,gzip 1,594;compat 9,788,gzip 3,985。replaceNode 没了,Component.base 没了,SuspenseList 没了,useEffect 的清理延后到绘制之后。17.mjs。.module.js、CommonJS、UMD 三种包全删了,直接引 preact/dist/preact.min.js 会拿不到文件。17style={{ height: 500 }} 不再自动补 px,这个行为搬进了 preact/compat。不用 compat 的话,渲染结果会变。17useEffect 的清理函数现在延后到浏览器绘制之后,和 React 一致。卸载后立刻断言的测试要先冲一次 effect。17| 想看懂的中间层 | 仓库与本期信号 | 最小条件 | 第一条可观察路径 | 主要限制 |
|---|---|---|---|---|
| 网络上的字节 | grpc-go · v1.85.0 北京时间 10 月 27 日 17:28 | Go 1.26,protoc | GODEBUG=http2debug=2 看三轮帧 | 要写 proto,日志不打 DATA |
| 内存里的字节 | arrow-rs · 60.1.0 北京时间 10 月 28 日 16:41 | Rust 工具链,一次完整编译 | 切一次片,把指针相减 | 编译两分半;改整列才有优势 |
| 界面上的节点 | preact · 11.1.0 北京时间 10 月 27 日 19:48 | Node 22,一次 npm install | 同一列表加不加 key 各测一次 | jsdom 不是真浏览器 |
github.com
github.com
rfc-editor.org
pkg.go.dev
github.com
github.com
github.com
github.com
github.com
arrow.apache.org
parquet.apache.org
github.com
github.com
github.com
github.com
github.com
preactjs.com
github.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.