Skip to content

Commit 785065f

Browse files
author
Taois
committed
feat: 文件头工具调优
1 parent a559152 commit 785065f

File tree

3 files changed

+299
-7
lines changed

3 files changed

+299
-7
lines changed

scripts/test/test_cctv_header.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ async function runTest() {
126126
const avgTimeWrite = totalTimeWrite / iterations;
127127
console.log(`总耗时 (Write ${iterations}次): ${totalTimeWrite.toFixed(2)} ms`);
128128
console.log(`平均每次写入耗时: ${avgTimeWrite.toFixed(4)} ms`);
129+
130+
// 验证 Buffer 模式是否生效 (检查耗时是否显著低于 String 模式)
131+
// 理论上 Buffer 模式会快很多
132+
if (avgTimeWrite < 1.0) {
133+
console.log('✅ 写入性能极佳,推测已启用 Buffer 优化。');
134+
}
129135

130136
// 10. 移除文件头性能测试
131137
console.log(`\n[10/10] 移除文件头性能测试 (循环 ${iterations} 次)...`);
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
2+
import fs from 'fs/promises';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
5+
import FileHeaderManager from '../../utils/fileHeaderManager.js';
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
10+
const sourceFile = path.resolve(__dirname, '../../spider/js/央视大全[官].js');
11+
const testFile = path.resolve(__dirname, 'test_cctv_header_large_temp.js');
12+
13+
// 模拟 Buffer 模式的 Header 移除
14+
async function removeHeaderBuffer(filePath) {
15+
const handle = await fs.open(filePath, 'r');
16+
try {
17+
const stats = await handle.stat();
18+
const buffer = Buffer.alloc(stats.size);
19+
await handle.read(buffer, 0, stats.size, 0);
20+
21+
// 查找注释块
22+
// /* = 0x2f 0x2a
23+
// */ = 0x2a 0x2f
24+
const start = buffer.indexOf('/*');
25+
if (start === -1) return;
26+
27+
const end = buffer.indexOf('*/', start);
28+
if (end === -1) return;
29+
30+
// 提取注释内容进行检查
31+
const commentBuf = buffer.subarray(start, end + 2);
32+
const commentStr = commentBuf.toString('utf8');
33+
34+
if (!commentStr.includes('@header(')) return;
35+
36+
// 查找 @header
37+
const headerStart = commentStr.indexOf('@header(');
38+
// 这里简化处理:假设我们要移除整个 @header(...)
39+
// 实际需要括号平衡逻辑,这里简单起见,假设只有一层括号或用 regex
40+
// 为了公平对比,我们只测试 "读取 Buffer -> 查找位置 -> 拼接 Buffer -> 写入" 的过程
41+
42+
// 模拟:我们找到了 header 的 start 和 end
43+
// 实际逻辑应该复用 FileHeaderManager.findHeaderBlock
44+
// 但 findHeaderBlock 操作的是 string。
45+
46+
// 关键在于:是否可以只把 commentBlock 转 string,找到 offset,
47+
// 然后在原始 Buffer 上进行 slice 和 concat?
48+
49+
// 假设 headerStartOffset 和 headerEndOffset 是相对于 buffer 的
50+
const headerGlobalStart = start + headerStart;
51+
// 简单模拟 header 长度为 100 字节
52+
const headerGlobalEnd = headerGlobalStart + 100;
53+
54+
const newBuf = Buffer.concat([
55+
buffer.subarray(0, headerGlobalStart),
56+
buffer.subarray(headerGlobalEnd)
57+
]);
58+
59+
await fs.writeFile(filePath, newBuf);
60+
61+
} finally {
62+
await handle.close();
63+
}
64+
}
65+
66+
async function runTest() {
67+
console.log('=== 开始大文件性能对比测试 ===');
68+
69+
// 1. 创建大文件 (500KB)
70+
console.log('正在生成 500KB 测试文件...');
71+
let content = await fs.readFile(sourceFile, 'utf8');
72+
// 重复内容直到达到 500KB
73+
while (Buffer.byteLength(content) < 500 * 1024) {
74+
content += '\n// Padding content to increase file size...\n' + content;
75+
}
76+
// 确保头部有 @header
77+
if (!content.includes('@header(')) {
78+
console.warn('源文件没有 @header,测试可能无效');
79+
}
80+
81+
await fs.writeFile(testFile, content);
82+
const stats = await fs.stat(testFile);
83+
console.log(`测试文件大小: ${(stats.size / 1024).toFixed(2)} KB`);
84+
85+
const iterations = 100; // 大文件操作慢,减少次数
86+
87+
// 2. 测试现有 String 模式 (FileHeaderManager.removeHeader)
88+
console.log(`\n[String Mode] removeHeader 测试 (${iterations} 次)...`);
89+
90+
// 预热
91+
const header = await FileHeaderManager.readHeader(testFile);
92+
93+
const startString = performance.now();
94+
for (let i = 0; i < iterations; i++) {
95+
// removeHeader 目前只返回字符串,不写入
96+
// 为了模拟真实场景,我们需要包含 fs.readFile 和 fs.writeFile
97+
// FileHeaderManager.removeHeader 内部如果不传 path 传 content,则不包含读。
98+
// 如果传 path,则包含读。
99+
// 但它不包含写。
100+
101+
// 模拟完整流程:读 -> 处理 -> 写
102+
const newContent = await FileHeaderManager.removeHeader(testFile); // 包含 readFile
103+
await fs.writeFile(testFile, newContent);
104+
105+
// 恢复文件头以便下一次循环 (为了测试 remove,必须先有)
106+
// 这步不计入时间?不,这很麻烦。
107+
// 我们可以测试 "读取 -> 替换(即使没找到也算处理) -> 写入" 的开销
108+
// 或者只测试 removeHeader 的处理部分。
109+
110+
// 为了简单,我们测试 "Write Header" 吧,因为 writeHeader 包含 读 -> 改 -> 写
111+
await FileHeaderManager.writeHeader(testFile, header, { createBackup: false });
112+
}
113+
const endString = performance.now();
114+
const timeString = endString - startString;
115+
console.log(`String Mode 总耗时: ${timeString.toFixed(2)} ms`);
116+
console.log(`String Mode 平均耗时: ${(timeString / iterations).toFixed(2)} ms`);
117+
118+
119+
// 3. 测试模拟 Buffer 模式
120+
// 由于 FileHeaderManager 没有 Buffer 模式,我们只能模拟 "读 Buffer -> 改 Buffer -> 写 Buffer"
121+
// 对比 "读 String -> 改 String -> 写 String"
122+
123+
console.log(`\n[Buffer Mode (Simulated)] writeHeader 测试 (${iterations} 次)...`);
124+
125+
const startBuffer = performance.now();
126+
for (let i = 0; i < iterations; i++) {
127+
const handle = await fs.open(testFile, 'r');
128+
const bufStats = await handle.stat();
129+
const buffer = Buffer.alloc(bufStats.size);
130+
await handle.read(buffer, 0, bufStats.size, 0);
131+
await handle.close();
132+
133+
// 模拟修改:在 Buffer 中找到 header 位置并替换
134+
// 实际算法:
135+
// 1. 找到 comment block (buffer.indexOf) -> 极快
136+
// 2. 将 comment block 转 string (很短) -> 极快
137+
// 3. 在 comment string 中找 header -> 极快
138+
// 4. 拼接 Buffer -> 极快 (不需要编解码大段代码)
139+
140+
const startComment = buffer.indexOf('/*');
141+
const endComment = buffer.indexOf('*/', startComment);
142+
143+
// 假设我们找到了 offset,进行拼接
144+
// 这里不做真实解析,只做 Buffer 操作模拟开销
145+
const newBuf = Buffer.concat([
146+
buffer.subarray(0, startComment),
147+
Buffer.from('/* Updated Header */'), // 模拟新头
148+
buffer.subarray(endComment + 2)
149+
]);
150+
151+
await fs.writeFile(testFile, newBuf);
152+
153+
// 恢复 (其实上面已经写入了,不需要额外恢复,因为每次都覆盖)
154+
}
155+
const endBuffer = performance.now();
156+
const timeBuffer = endBuffer - startBuffer;
157+
console.log(`Buffer Mode 总耗时: ${timeBuffer.toFixed(2)} ms`);
158+
console.log(`Buffer Mode 平均耗时: ${(timeBuffer / iterations).toFixed(2)} ms`);
159+
160+
const improvement = ((timeString - timeBuffer) / timeString * 100).toFixed(2);
161+
console.log(`\n🚀 预计 Buffer 模式提升: ${improvement}%`);
162+
163+
// 清理
164+
await fs.unlink(testFile);
165+
}
166+
167+
runTest();

utils/fileHeaderManager.js

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,20 +208,26 @@ class FileHeaderManager {
208208
throw new Error('Invalid header object');
209209
}
210210

211-
let content;
212-
try {
213-
content = await fs.readFile(filePath, 'utf8');
214-
} catch (error) {
215-
throw new Error(`Failed to read file: ${error.message}`);
216-
}
217-
218211
const ext = path.extname(filePath);
219212
const config = this.COMMENT_CONFIG[ext];
220213

221214
if (!config) throw new Error(`Unsupported file type: ${ext}`);
222215

223216
const headerStr = `@header(${JSON5.stringify(headerObj, null, 2)})`;
224217

218+
// 尝试使用 Buffer 优化写入 (针对已存在 header 的情况)
219+
const bufferSuccess = await this._replaceHeaderBuffer(filePath, headerStr, config, ext);
220+
if (bufferSuccess) {
221+
return;
222+
}
223+
224+
let content;
225+
try {
226+
content = await fs.readFile(filePath, 'utf8');
227+
} catch (error) {
228+
throw new Error(`Failed to read file: ${error.message}`);
229+
}
230+
225231
// 优化:先尝试只读取头部来匹配正则,避免全量匹配
226232
// 但由于 writeHeader 需要重写整个文件,全量内容是必须的
227233
// 所以这里的优化点主要在于减少不必要的全量正则匹配
@@ -371,6 +377,119 @@ class FileHeaderManager {
371377
}
372378
}
373379

380+
/**
381+
* 使用 Buffer 高效地替换文件头,避免大文件 String 转换开销
382+
* @private
383+
*/
384+
static async _replaceHeaderBuffer(filePath, headerStr, config, ext) {
385+
let handle;
386+
try {
387+
handle = await fs.open(filePath, 'r');
388+
const stats = await handle.stat();
389+
390+
// 如果文件太小,Buffer 优化的开销可能不划算,fallback 到 string 处理
391+
// 或者如果文件太大 (如 > 5MB),我们只读取前 64KB 寻找 header
392+
const MAX_SCAN_SIZE = 64 * 1024; // 64KB
393+
const scanSize = Math.min(stats.size, MAX_SCAN_SIZE);
394+
395+
const buffer = Buffer.alloc(scanSize);
396+
await handle.read(buffer, 0, scanSize, 0);
397+
await handle.close();
398+
handle = null;
399+
400+
// 将 buffer 转为 string 进行正则匹配 (只转前 64KB)
401+
const contentHead = buffer.toString('utf8');
402+
403+
const match = contentHead.match(config.regex);
404+
405+
if (match) {
406+
const [fullComment] = match;
407+
// 找到 headerBlock
408+
const headerBlock = this.findHeaderBlock(fullComment, ext);
409+
410+
if (headerBlock) {
411+
// 计算 headerBlock 在文件中的 byte offset
412+
// 注意:fullComment 是 regex 匹配出来的 string
413+
// 我们需要找到 fullComment 在 buffer 中的 byte offset
414+
415+
// Buffer.indexOf(string) 可以找到 byte offset
416+
// 但 regex 匹配的 fullComment 可能包含 unicode,直接 indexOf string 是安全的
417+
// 前提是 encoding 一致 (utf8)
418+
419+
const commentStartOffset = buffer.indexOf(fullComment);
420+
if (commentStartOffset === -1) {
421+
// Fallback if not found (encoding issues?)
422+
return false;
423+
}
424+
425+
// headerBlock.start 是相对于 fullComment 字符串的 char index
426+
// 我们需要 byte index。这有点麻烦,因为 fullComment 可能包含多字节字符。
427+
// 所以我们需要把 fullComment.substring(0, headerBlock.start) 转为 Buffer 算长度
428+
429+
const preHeaderStr = fullComment.substring(0, headerBlock.start);
430+
const preHeaderLen = Buffer.byteLength(preHeaderStr);
431+
432+
const postHeaderStr = fullComment.substring(headerBlock.end);
433+
434+
// 构造新的 header buffer
435+
const newHeaderBuf = Buffer.from(headerStr);
436+
437+
// 计算替换点
438+
const replaceStart = commentStartOffset + preHeaderLen;
439+
440+
// 原有的 header content byte length
441+
const oldHeaderContentStr = headerBlock.content; // 这里包含了 header(...) 括号内的内容?
442+
// findHeaderBlock 返回的是 {start, end, content}
443+
// start/end 是相对于 fullComment 的 index
444+
// content 是 header(...) 括号内的内容,还是 @header(...)?
445+
// 看 findHeaderBlock 实现:
446+
// content: text.substring(startIndex + startMarker.length, index) -> 只是括号内的内容
447+
// start: startIndex ( "@" 的位置 )
448+
// end: index + 1 ( ")" 后面的位置 )
449+
450+
// 所以 headerBlock.start 指向 "@header" 的 "@"
451+
// headerBlock.end 指向 ")" 后面
452+
453+
// 原始的 header 部分 (string)
454+
const oldHeaderFullStr = fullComment.substring(headerBlock.start, headerBlock.end);
455+
const oldHeaderByteLen = Buffer.byteLength(oldHeaderFullStr);
456+
457+
// 准备写入
458+
// 如果新旧 header 长度一致,可以直接 overwrite (极快)
459+
// 如果不一致,需要重写文件剩余部分
460+
461+
if (newHeaderBuf.length === oldHeaderByteLen) {
462+
// Overwrite inplace
463+
const writeHandle = await fs.open(filePath, 'r+');
464+
await writeHandle.write(newHeaderBuf, 0, newHeaderBuf.length, replaceStart);
465+
await writeHandle.close();
466+
return true;
467+
} else {
468+
// 重写文件
469+
// 读取整个文件为 Buffer 然后 concat
470+
// 避免 string 转换
471+
472+
const fullFileBuf = await fs.readFile(filePath);
473+
const finalBuf = Buffer.concat([
474+
fullFileBuf.subarray(0, replaceStart),
475+
newHeaderBuf,
476+
fullFileBuf.subarray(replaceStart + oldHeaderByteLen)
477+
]);
478+
479+
await fs.writeFile(filePath, finalBuf);
480+
return true;
481+
}
482+
}
483+
}
484+
485+
return false; // Fallback to string mode if no match or complex case
486+
} catch (e) {
487+
if (handle) await handle.close();
488+
return false;
489+
}
490+
}
491+
492+
374493
/**
375494
* 移除头信息区域
376495
* @param {string} input 文件路径或文件内容

0 commit comments

Comments
 (0)