js// 获取每个字符的字节数
const str = "你好,世界!";
const bufferStr = new TextEncoder().encode(str);
console.log(bufferStr / str.length);
jsconst uint8 = new Uint8Array(10);
unit8[0] = 255;
unit8.set([1, 2, 3], 1);
console.log(uint8); // Uint8Array(10) [255, 1, 2, 3, 0, 0, 0, 0, 0, 0]
// 当填写的数据超出了一个字节。则不会被添加到 Uint8Array 中(在不同场景下表现不一致。有可能也会直接报错。)
uint8[0] = 256;
console.log(uint8[0]); // 0
jsconst uint32 = new Uint32Array(10);
uint32[0] = 4294967295;
console.log(uint32); // Uint32Array(10) [4294967295, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// 当填写的数据超出了一个字节。则不会被添加到 Uint32Array 中(在不同场景下表现不一致。有可能也会直接报错。)
uint32[0] = 4294967296;
console.log(uint32[0]); // 0
jsconst sab = new SharedArrayBuffer(1024);
const uint8 = new Uint8Array(sab);
console.log(uint8.length); // 1024
jsconst sab = new SharedArrayBuffer(1024);
const uint8_1 = new Uint8Array(sab);
const uint8_2 = new Uint8Array(sab);
uint8_1[0] = 42;
console.log(uint8_2[0]); // 42
js// 错误的示例
const str = "你好,世界!";
const uint8 = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
uint8[i] = str.charCodeAt(i);
}
console.log(uint8); // Uint8Array(6) [20320, 22909, 65292, 19990, 30028, 65281]
js// 转换
const str = "你好,世界!";
const coderStr = new TextEncoder().encode(str);
// 放入 Uint8Array 中
const arrayBuffer = new Uint8Array(coderStr.byteLength);
arrayBuffer.set(coderStr);
// 输出中文
console.log(new TextDecoder().decode(arrayBuffer)); // 你好,世界!
jsconst encoder = new TextEncoder();
console.log(encoder.encode('中')); // Uint8Array(3) [ 228, 184, 173 ]
console.log(encoder.encode('中国')); // Uint8Array(6) [ 228, 184, 173, 229, 155, 189 ]
// 读者如果读到这里,可以回顾下最初前面提到的,一个中文三个字节
console.log(encoder.encode('一')); // Uint8Array(3) [ 228, 184, 128 ]
jsconst sab = new SharedArrayBuffer(1024);
// 1. 用 Int32Array 操作前 4 个字节 (存 ID)
const idView = new Int32Array(sab, 0, 1);
idView[0] = 999527;
// 2. 用 Uint8Array 操作剩下的 1020 个字节 (存数据)
const dataView = new Uint8Array(sab, 4);
dataView.set([0xff, 0xd8, 0xff]);
jsconst sab = new SharedArrayBuffer(1024);
// 1. 用 Int32Array 操作前 4 个字节 (存 ID)
const idView = new Int32Array(sab, 0, 1);
// sab:SharedArrayBuffer
// 0:表示从 SharedArrayBuffer 的第 0 个字节开始创建视图
// 1:表示视图包含 1 个元素(即 4 个字节,因为 Int32Array 的每个元素占用 4 个字节)
// 2. 用 Uint8Array 操作剩下的 1020 个字节 (存数据)
const dataView = new Uint8Array(sab, 4);
dataView.set([0xff, 0xd8, 0xff]);
// sab:SharedArrayBuffer
// 4:表示从 SharedArrayBuffer 的第 4 个字节开始创建视图, 因为 Int32Array 一个格子占4个字节。
// undefined:表示后面的所有空间,都是自身的。
常规模式下,我们在主进程中,可以直接使用IPC进行通讯。 不排除业务中需要把数据量较大的内容,传输给渲染进程进行渲染。此时,采用 TypedArray 和 MessageChannel 则是非常合适的。
jsconst id = xxx;
const { port1, port2 } = new MessageChannelMain();
HelperWindow.helperWindow.webContents.postMessage(SendToRender.messageHistoryDetail, null, [port2]);
// 把 thread_id 转换为 buffer
const encoder = new TextEncoder();
const idBuffer = encoder.encode(id);
// 计算所需的字节长度
const totalBytes =
rows.reduce((pre, next) => {
return pre + 4 + idBuffer.byteLength + 4 + next.value.byteLength;
}, 0) + 4;
// 分配物理内存
// uint8 只能处理最大为 255 的数值。所以,补全前缀长度的大整数,必须使用 dataview, 因为 dataview 支持所有的基础数值类型(uint8, uint32, float8,32...)
const shareBuffer = new ArrayBuffer(totalBytes);
const view = new DataView(shareBuffer);
const uint8View = new Uint8Array(shareBuffer);
view.setUint32(0, rows.length, true); // 把数据的总条数添加到最前面
let offset = 4; // (unit32 占用4个字节、uint8占1个字节,考虑前四位的数据可能特别长),故前四位采用uint32写入了rows的长度,因此offset从4开始
// 下方的示例中,是如下类似的数据: [{ id: xxx, value: <Buffer> }]
for (const line of rows) {
view.setUint32(offset, idBuffer.byteLength, true);
offset += 4;
uint8View.set(idBuffer, offset);
offset += idBuffer.byteLength;
// value 本身即是buffer
view.setUint32(offset, line.value.byteLength, true);
offset += 4;
uint8View.set(line.value, offset);
offset += line.value.byteLength;
}
port1.start();
// 坑点:buffer的零拷贝,必须是在相同的进程下,才可以实现。因此,即使使用了buffef,在messageChannel通信时,也会在短时间内,内存中同样也会存储两份数据。
// 好的一点是,message Channel 本身是点对点的通信,资源耗费低。其次:在进程通信时,如果传递的是json,那么会走 struct clone,也就是json的key value 结构化克隆。是比较耗费资源的。
// buffer的好处是,在结构化克隆时,直接传递内存地址,不在需要 struct clone。因此会比较快。
port1.postMessage({ id, buffer: shareBuffer });
port1.close(); // 这里可以直接放心的close,port1.postMessage 本身是同步的把数据放入 ipc 管道。
// 渲染进程的解析(采用webworker,不阻塞主线程)
import { AnswerType } from "@/type/answer";
self.onmessage = (e: MessageEvent<{ buffer: ArrayBuffer }>) => {
const { buffer } = e.data;
if (!buffer || !buffer.byteLength) return self.postMessage({ value: [] });
const dataView = new DataView(buffer);
const decoder = new TextDecoder();
// 获取本次的消息total
const total = dataView.getUint32(0, true); // 前四个字节表示当前的total
const values = [];
let offset = 4;
for (let i = 0; i < total; i++) {
const len = dataView.getInt32(offset, true);
offset += 4;
const tBuffer = new Uint8Array(buffer, offset, len);
const tid = decoder.decode(tBuffer); // 还原 tid 数值
offset += len;
const vLen = dataView.getUint32(offset, true);
offset += 4;
const vBuffer = new Uint8Array(buffer, offset, vLen);
let v = JSON.parse(decoder.decode(vBuffer)); // 还原 value 数据
offset += vLen;
const isArrayHistory = Array.isArray(v);
if (isArrayHistory && v.length > 1) continue;
if (isArrayHistory) v = v[0];
values.push({
id: tid,
xxx: xxx,
xxx: {
value: v["kwargs"].content.at(-1).text,
type: AnswerType.none,
},
});
}
self.postMessage({ value: values });
};
以上的知识点,在流式响应与内存占用优化是非常重要的。