Conversation
|
另外感觉TM返回的headers的是不是排序了?(这个我觉得没必要也跟随了) |
有注意到。但感觉 TM 不是故意用了 sort.
对。如果加 sort 会拖慢 |
| @@ -131,10 +131,14 @@ export class FetchXHR { | |||
| getAllResponseHeaders(): string { | |||
| let ret: string | undefined = this.cache[""]; | |||
There was a problem hiding this comment.
有 key 的是单一 header (getResponseHeader). 空 key 是 all (getAllResponseHeaders)
只是用了同一个 cache 来处理
当然不好看的话也可以拆开做多一个 variable
9aed140 to
47e9359
Compare
Co-Authored-By: wangyizhi <yz@ggnb.top>
47e9359 to
5296f49
Compare
src/pkg/utils/utils.ts
Outdated
| export const nativeResponseHeadersTreatment = (headersString: string) => { | ||
| if (!headersString) return ""; | ||
| const len = headersString.length; | ||
| let out = ""; | ||
| let start = len; // start position = nil | ||
| let separator = ""; | ||
| for (let i = 0; i <= len; i++) { | ||
| const char = headersString.charCodeAt(i) || 10; | ||
| if (char === 10 || char === 13) { | ||
| if (i > start) { | ||
| const seg = headersString.substring(start, i); // "key: value" | ||
| const j = seg.indexOf(":"); | ||
| if (j > 0) { | ||
| let k = j + 1; | ||
| if (seg.charCodeAt(k) === 32) k++; | ||
| if (k < seg.length) { | ||
| const headerName = seg.substring(0, j); // "key" | ||
| const headerValue = seg.substring(k); // "value" | ||
| out += `${separator}${headerName}:${headerValue}`; | ||
| separator = "\r\n"; | ||
| } | ||
| } | ||
| } | ||
| start = len; // start position = nil | ||
| } else { | ||
| if (start === len) { | ||
| start = i; // set start position | ||
| } | ||
| } | ||
| } | ||
| return out; | ||
| }; |
There was a problem hiding this comment.
真心希望使用可读性更好的方式去实现,虽然大概明白是个什么逻辑,也处理了很多异常的情况,当然不是说不好,我觉得很厉害,但是阅读起来好困难
我们拿到的header应该是浏览器已经处理过一遍的header,而且http协议应该也不允许那种异常的header,按照http协议来说,结尾一定是 \r\n
https://developer.mozilla.org/zh-CN/docs/Glossary/HTTP_header
看你吧
// TM Xhr Header 兼容处理,原生xhr \r\n 在尾,但TM的GMXhr没有;同时除去冒号后面的空白
export const nativeResponseHeadersTreatment = (headersString: string) => {
if (!headersString) return "";
let out = "";
let separator = "";
headersString.split(/\r?\n/).forEach((line) => {
const j = line.indexOf(":");
if (j > 0) {
const headerName = line.substring(0, j); // "key"
let headerValue = line.substring(j + 1); // "value"
let k = 0;
// 删除开头空白
for (; k < headerValue.length; k += 1) {
if (headerValue[k] !== " ") {
break;
}
}
headerValue = headerValue.substring(k);
out += `${separator}${headerName}:${headerValue}`;
separator = "\r\n";
}
});
return out;
};There was a problem hiding this comment.
headersString.split(/\r?\n/)
不想用 regex. 效能考虑
There was a problem hiding this comment.
做了个基准测试,好像性能并不好。。。。。:
✓ src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试 8784ms
name hz min max mean p75 p99 p995 p999 rme samples
· 简单响应头 2,731,094.40 0.0002 5.7630 0.0004 0.0003 0.0004 0.0006 0.0010 ±4.16% 1365548
· 包含多余空格的响应头 1,931,481.53 0.0004 0.4018 0.0005 0.0005 0.0006 0.0011 0.0015 ±0.50% 965741
· 复杂真实场景响应头 468,745.09 0.0020 0.2766 0.0021 0.0021 0.0025 0.0028 0.0062 ±0.26% 234373
· 混合分隔符响应头 1,495,232.50 0.0005 0.9980 0.0007 0.0006 0.0015 0.0015 0.0016 ±0.84% 747617
· 空字符串 24,717,513.26 0.0000 0.9977 0.0000 0.0000 0.0000 0.0000 0.0001 ±0.51% 12358757
· 长响应头 (100个header) 54,875.94 0.0171 0.5177 0.0182 0.0175 0.0199 0.0340 0.4157 ±1.03% 27438
BENCH Summary
空字符串 - src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试
9.05x faster than 简单响应头
12.80x faster than 包含多余空格的响应头
16.53x faster than 混合分隔符响应头
52.73x faster than 复杂真实场景响应头
450.43x faster than 长响应头 (100个header)
✓ src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试 8989ms
name hz min max mean p75 p99 p995 p999 rme samples
· 简单响应头 3,731,787.16 0.0001 0.6287 0.0003 0.0003 0.0004 0.0007 0.0010 ±1.05% 1865894
· 包含多余空格的响应头 2,648,493.16 0.0002 8.9453 0.0004 0.0003 0.0004 0.0006 0.0010 ±4.20% 1324247
· 复杂真实场景响应头 793,283.50 0.0011 0.5739 0.0013 0.0012 0.0017 0.0019 0.0034 ±0.64% 396642
· 混合分隔符响应头 2,360,763.12 0.0003 3.7784 0.0004 0.0004 0.0005 0.0010 0.0011 ±1.77% 1180382
· 空字符串 23,601,426.73 0.0000 0.2814 0.0000 0.0000 0.0001 0.0001 0.0001 ±0.30% 11800714
· 长响应头 (100个header) 102,675.98 0.0087 0.8815 0.0097 0.0090 0.0103 0.0197 0.3522 ±1.40% 51338
BENCH Summary
空字符串 - src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试
6.32x faster than 简单响应头
8.91x faster than 包含多余空格的响应头
10.00x faster than 混合分隔符响应头
29.75x faster than 复杂真实场景响应头
229.86x faster than 长响应头 (100个header)
import { bench, describe } from "vitest";
import { nativeResponseHeadersTreatment } from "./utils";
describe("nativeResponseHeadersTreatment 基准测试", () => {
// 测试用例1: 简单的响应头
const simpleHeaders = "content-type: application/json\r\nserver: nginx";
// 测试用例2: 包含多余空格的响应头
const headersWithSpaces = "content-type: application/json\r\nserver: nginx\r\ncache-control: no-cache";
// 测试用例3: 复杂的响应头(真实场景)
const complexHeaders = `content-type: text/html; charset=utf-8\r\n
server: Apache/2.4.41 (Ubuntu)\r\n
set-cookie: sessionid=abc123; Path=/; HttpOnly\r\n
cache-control: no-store, no-cache, must-revalidate\r\n
expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n
pragma: no-cache\r\n
x-frame-options: SAMEORIGIN\r\n
x-content-type-options: nosniff\r\n
content-security-policy: default-src 'self'\r\n
access-control-allow-origin: *`;
// 测试用例4: 包含多种分隔符的响应头(混合 \r\n 和 \n)
const mixedHeaders = "content-type: application/json\nserver: nginx\r\ncache-control: no-cache\ncontent-length: 1234";
// 测试用例5: 空字符串
const emptyHeaders = "";
// 测试用例6: 很长的响应头(性能压力测试)
const longHeaders = Array(100)
.fill(0)
.map((_, i) => `x-custom-header-${i}: value-${i}`)
.join("\r\n");
bench("简单响应头", () => {
nativeResponseHeadersTreatment(simpleHeaders);
});
bench("包含多余空格的响应头", () => {
nativeResponseHeadersTreatment(headersWithSpaces);
});
bench("复杂真实场景响应头", () => {
nativeResponseHeadersTreatment(complexHeaders);
});
bench("混合分隔符响应头", () => {
nativeResponseHeadersTreatment(mixedHeaders);
});
bench("空字符串", () => {
nativeResponseHeadersTreatment(emptyHeaders);
});
bench("长响应头 (100个header)", () => {
nativeResponseHeadersTreatment(longHeaders);
});
});There was a problem hiding this comment.
呀。測試方法有問題
對同一個 string 做 regex, V8內部會cache 起來
bench(...) 會跑同一個N次
第一次是真時間,第2~N次都是假的
let atomicId = 0;
bench("简单响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头", () => {
nativeResponseHeadersTreatment(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)", () => {
nativeResponseHeadersTreatment(`${++atomicId}${longHeaders}`);
});我估計上面是 regex 下面是我的版本
你會看到 上面的 max 很大
There was a problem hiding this comment.
你的写法和思路更像是在弄 C/C++ ,不过说实话,哪怕是性能更好,我也宁愿牺牲掉这么一点微不足道的性能换取可读性,可读性差加大了其他人的阅读门槛,类似的问题我提了好多次了,后续我可能要重新思考这些为了性能妥协的内容了 😣
当然也不是说性能问题不重要,只是不用在这种很细小的问题上去钻,一般都是在量级上来了后才会体现出问题来,而且这个量级几乎不会达到。
就我来说,一般只会考虑实际情况可能会达到一定量级的地方,才会特意的去做性能优化
There was a problem hiding this comment.
import { bench, describe } from "vitest";
// TM Xhr Header 兼容处理,原生xhr \r\n 在尾,但TM的GMXhr没有;同时除去冒号后面的空白
export const nativeResponseHeadersTreatment1 = (hs: string) => {
let start = 0;
let out = "";
const len = hs.length;
let separator = "";
while (start < len) {
const i = hs.indexOf(":", start); // 冒号的位置
let j = hs.indexOf("\n", start); // 换行符的位置
if (j < 0) j = len; // 尾行
if (i < j) {
const key = hs.substring(start, i).trim(); // i 为 -1 的话 key 也会是空值
if (key) {
out += `${separator}${key}:${hs.substring(i + 1, j).trim()}`;
separator = "\r\n";
}
}
start = j + 1; // 移到下一行
}
return out;
};
export const nativeResponseHeadersTreatment2 = (headersString: string) => {
if (!headersString) return "";
let out = "";
let separator = "";
headersString.split(/\r?\n/).forEach((line) => {
const j = line.indexOf(":");
if (j > 0) {
const headerName = line.substring(0, j); // "key"
let headerValue = line.substring(j + 1).trim(); // "value"
let k = 0;
// 删除开头空白
for (; k < headerValue.length; k += 1) {
if (headerValue[k] !== " ") {
break;
}
}
headerValue = headerValue.substring(k);
out += `${separator}${headerName}:${headerValue}`;
separator = "\r\n";
}
});
return out;
};
describe("nativeResponseHeadersTreatment 基准测试", () => {
// 测试用例1: 简单的响应头
const simpleHeaders = "content-type: application/json\r\nserver: nginx";
// 测试用例2: 包含多余空格的响应头
const headersWithSpaces = "content-type: application/json\r\nserver: nginx\r\ncache-control: no-cache";
// 测试用例3: 复杂的响应头(真实场景)
const complexHeaders = `content-type: text/html; charset=utf-8\r\n
server: Apache/2.4.41 (Ubuntu)\r\n
set-cookie: sessionid=abc123; Path=/; HttpOnly\r\n
cache-control: no-store, no-cache, must-revalidate\r\n
expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n
pragma: no-cache\r\n
x-frame-options: SAMEORIGIN\r\n
x-content-type-options: nosniff\r\n
content-security-policy: default-src 'self'\r\n
access-control-allow-origin: *`;
// 测试用例4: 包含多种分隔符的响应头(混合 \r\n 和 \n)
const mixedHeaders = "content-type: application/json\nserver: nginx\r\ncache-control: no-cache\ncontent-length: 1234";
// 测试用例5: 空字符串
// const emptyHeaders = "";
// 测试用例6: 很长的响应头(性能压力测试)
const longHeaders = Array(100)
.fill(0)
.map((_, i) => `x-custom-header-${i}: value-${i}`)
.join("\r\n");
let atomicId = 10_000_000;
bench("简单响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${longHeaders}`);
});
bench("简单响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${longHeaders}`);
});
});
There was a problem hiding this comment.
如果不处理 \r 的话,我也修改了并通过了单元测试,某些情况会好一点点,差距极小,但是这点差距有什么意义呢,你为什么如此的排斥语法糖,JS本来就是一个解释型的语言,他不像编译型的语言,一些操作可以直接访问内存会更快,一些语法糖V8有内部的优化,你以为的一些优化可能并没有效果,反而会加重解释器的负担
我可能说服不了你,但是我不想再因为这些舍弃可读性和架构整洁了,除了你、我可能还会有其他人来阅读,水平不够或者不了解的人只会两眼一黑,为了读懂这代码所消耗的能量已经够SC所有用户跑上几十年了:
✓ src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试 9305ms
name hz min max mean p75 p99 p995 p999 rme samples
· 简单响应头-1 3,101,845.11 0.0001 8.3151 0.0003 0.0002 0.0005 0.0006 0.0050 ±5.48% 1550923
· 包含多余空格的响应头-1 3,232,974.52 0.0002 1.2363 0.0003 0.0003 0.0003 0.0007 0.0010 ±2.81% 1616488
· 复杂真实场景响应头-1 865,962.22 0.0008 2.8180 0.0012 0.0010 0.0016 0.0024 0.0175 ±3.14% 433564
· 混合分隔符响应头-1 2,533,276.00 0.0002 3.1258 0.0004 0.0003 0.0004 0.0008 0.0012 ±3.56% 1266688
· 长响应头 (100个header)-1 137,587.81 0.0064 0.5487 0.0073 0.0068 0.0083 0.0169 0.2648 ±1.18% 68794
· 简单响应头-2 2,703,374.50 0.0002 2.3180 0.0004 0.0003 0.0004 0.0008 0.0012 ±3.77% 1351688
· 包含多余空格的响应头-2 2,371,600.25 0.0003 1.9080 0.0004 0.0004 0.0005 0.0008 0.0011 ±2.96% 1185801
· 复杂真实场景响应头-2 914,793.00 0.0008 0.8549 0.0011 0.0010 0.0014 0.0016 0.0045 ±2.37% 457397
· 混合分隔符响应头-2 2,450,759.48 0.0003 0.6513 0.0004 0.0004 0.0005 0.0005 0.0011 ±0.30% 1225380
· 长响应头 (100个header)-2 140,328.80 0.0062 0.6886 0.0071 0.0066 0.0083 0.0157 0.2791 ±1.28% 70165
BENCH Summary
包含多余空格的响应头-1 - src/pkg/utils/utils.bench.ts > nativeResponseHeadersTreatment 基准测试
1.04x faster than 简单响应头-1
1.20x faster than 简单响应头-2
1.28x faster than 混合分隔符响应头-1
1.32x faster than 混合分隔符响应头-2
1.36x faster than 包含多余空格的响应头-2
3.53x faster than 复杂真实场景响应头-2
3.73x faster than 复杂真实场景响应头-1
23.04x faster than 长响应头 (100个header)-2
23.50x faster than 长响应头 (100个header)-1
import { bench, describe } from "vitest";
// TM Xhr Header 兼容处理,原生xhr \r\n 在尾,但TM的GMXhr没有;同时除去冒号后面的空白
export const nativeResponseHeadersTreatment1 = (hs: string) => {
let start = 0;
let out = "";
const len = hs.length;
let separator = "";
while (start < len) {
const i = hs.indexOf(":", start); // 冒号的位置
let j = hs.indexOf("\n", start); // 换行符的位置
if (j < 0) j = len; // 尾行
if (i < j) {
const key = hs.substring(start, i).trim(); // i 为 -1 的话 key 也会是空值
if (key) {
out += `${separator}${key}:${hs.substring(i + 1, j).trim()}`;
separator = "\r\n";
}
}
start = j + 1; // 移到下一行
}
return out;
};
export const nativeResponseHeadersTreatment2 = (headersString: string) => {
if (!headersString) return "";
let out = "";
headersString.split("\n").forEach((line) => {
const j = line.indexOf(":");
if (j > 0) {
const headerName = line.substring(0, j); // "key"
const headerValue = line.substring(j + 1).trim(); // "value"
out += `${headerName}:${headerValue}\r\n`;
}
});
return out.substring(0, out.length - 2); // 去掉最后的 \r\n
};
describe("nativeResponseHeadersTreatment 基准测试", () => {
// 测试用例1: 简单的响应头
const simpleHeaders = "content-type: application/json\r\nserver: nginx";
// 测试用例2: 包含多余空格的响应头
const headersWithSpaces = "content-type: application/json\r\nserver: nginx\r\ncache-control: no-cache";
// 测试用例3: 复杂的响应头(真实场景)
const complexHeaders = `content-type: text/html; charset=utf-8\r\n
server: Apache/2.4.41 (Ubuntu)\r\n
set-cookie: sessionid=abc123; Path=/; HttpOnly\r\n
cache-control: no-store, no-cache, must-revalidate\r\n
expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n
pragma: no-cache\r\n
x-frame-options: SAMEORIGIN\r\n
x-content-type-options: nosniff\r\n
content-security-policy: default-src 'self'\r\n
access-control-allow-origin: *`;
// 测试用例4: 包含多种分隔符的响应头(混合 \r\n 和 \n)
const mixedHeaders = "content-type: application/json\nserver: nginx\r\ncache-control: no-cache\ncontent-length: 1234";
// 测试用例5: 空字符串
// const emptyHeaders = "";
// 测试用例6: 很长的响应头(性能压力测试)
const longHeaders = Array(100)
.fill(0)
.map((_, i) => `x-custom-header-${i}: value-${i}`)
.join("\r\n");
let atomicId = 10_000_000;
bench("简单响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-1", () => {
nativeResponseHeadersTreatment1(`${++atomicId}${longHeaders}`);
});
bench("简单响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${simpleHeaders}`);
});
bench("包含多余空格的响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${headersWithSpaces}`);
});
bench("复杂真实场景响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${complexHeaders}`);
});
bench("混合分隔符响应头-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${mixedHeaders}`);
});
bench("长响应头 (100个header)-2", () => {
nativeResponseHeadersTreatment2(`${++atomicId}${longHeaders}`);
});
});There was a problem hiding this comment.
修改后的 nativeResponseHeadersTreatment1 已经是很好读的吧
就是简单的一行一行处理
你要用 nativeResponseHeadersTreatment2 我也没意见
你觉得 headersString.split("\n").forEach((line) => { ... } 是好一点阅读也可以呀
我没所谓
操作多,日后不预期大改动,PR的写法都尽量平衡阅读和效能
这个东西写了之后就永远不用改动。
也不是几页纸的长度
这20行写的是很一般的程序
里面又没有什么复杂 algorithm
我觉得是观点角度问题啦。我认为 7a2dbe6 这个修改已经是相关简化
不过你不欣赏对字串用这种看似C代码的写法,也是一种看法
像一个没写开 React 的人,看到 React 都会完全看不懂一样
每个人的看法不一样吧
在我看起来,现在只用了 indexOf, while 等东西,是一个简单合理的写法
你不喜欢也行呀,改成你的 split("\n") 吧
当然这不是重大的效能影响
但 12 行 跟 20 行 的代码,我看起来不懂的人还是不懂,懂的人还是懂
There was a problem hiding this comment.
相比之前确实好一些,但是依旧没有 nativeResponseHeadersTreatment2 的直接,这里的主要目的还是希望你不要那么排斥语法糖,性能并没有那么差
那我修改为 nativeResponseHeadersTreatment2 了
| } | ||
| return out; | ||
| }); | ||
| return out.substring(0, out.length - 2); // 去掉最后的 \r\n |
There was a problem hiding this comment.
注:这段可以用 ${separator}${headerName}:${headerValue} 这样来避免最后呼叫 substring
不过作者认为这些是过度优化那就算了
* responseHeaders: `TM兼容: 使用 \r\n 及不包含空白` Co-Authored-By: wangyizhi <yz@ggnb.top> * 中文 * 代码调整 * added streamReader error * 代码调整 * 代码调整 * 加注释 * nativeResponseHeadersTreatment -> normalizeResponseHeaders * Update bg_gm_xhr.ts * update * 代码调整 * 代码调整 * 单元测试 * 修改 normalizeResponseHeaders 实现 --------- Co-Authored-By: wangyizhi <yz@ggnb.top>
很多腳本也有用
response.responseHeaders.split("\r\n")https://github.com/search?q=+responseHeaders.split%28%22%5Cr%5Cn%22%29&type=code