Files
file-transfer-go/MEMORY_OPTIMIZATION.md

393 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 大文件传输内存优化文档 V3
## 问题背景
原有实现在传输大文件时存在内存爆炸风险:
1. **发送端**:预加载整个文件的所有块到内存数组
2. **接收端**:在内存中累积所有接收到的块
3. **合并**:再次复制所有数据生成最终文件
**示例**:传输 1GB 文件,内存峰值可能达到 3GB+(发送端 1GB + 接收端 1GB + 合并时 1GB
## 优化方案
### V3 终极方案:流式写入磁盘
使用 **File System Access API** 直接写入磁盘,彻底解决容量限制:
- ✅ 发送端:按需读取文件块,滑动窗口内仅缓存少量块
- ✅ 接收端:**直接写入磁盘**,不使用 IndexedDB
- ✅ 无文件大小限制:理论上支持任意大小(受限于磁盘空间)
- ✅ 降级方案:不支持的浏览器自动回退到 Blob 下载
### 内存对比
| 场景 | 原实现 | V2 (IndexedDB) | V3 (磁盘流) |
|------|--------|----------------|-------------|
| 传输 1GB 文件 | ~3GB 内存峰值 | ~50MB 内存 | ~20MB 内存 |
| 传输 10GB 文件 | OOM 崩溃 ❌ | ~50MB 内存 | ~20MB 内存 |
| 传输 100GB 文件 | 不可能 ❌ | IndexedDB 满 ⚠️ | ~20MB 内存 ✅ |
| 容量限制 | RAM 大小 | ~几十GB | 磁盘大小 |
## 架构变更
### 1. 流式文件写入层 (`stream-writer.ts`)
#### AutoFileWriter 类
**优先使用 File System Access API**Chrome/Edge 86+
```typescript
const writer = new AutoFileWriter(fileName);
await writer.init(suggestedName); // 用户选择保存位置
// 顺序写入
await writer.writeChunk(arrayBuffer);
// 或指定位置写入(支持乱序接收)
await writer.writeAt(position, arrayBuffer);
// 完成写入
await writer.close(); // 文件已保存到磁盘
```
**自动降级**(不支持的浏览器):
```typescript
// 内部自动使用 Blob + 下载
writer.getMode(); // 'stream' 或 'fallback'
```
#### 浏览器支持
| 浏览器 | 支持情况 | 模式 |
|--------|----------|------|
| Chrome 86+ | ✅ | stream直接写磁盘 |
| Edge 86+ | ✅ | stream |
| Firefox | ⚠️ 计划中 | fallback内存 + 下载) |
| Safari | ❌ | fallback |
### 2. 发送端优化 (`ConnectionTransferProtocol.ts`)
#### 滑动窗口 + 流式读取
```typescript
// 原实现:预加载所有块
const allChunks: ArrayBuffer[] = [];
for (let i = 0; i < total; i++) {
allChunks.push(await readChunk(i)); // ❌ 内存累积
}
// 优化后:按需读取 + 窗口缓存
const chunkCache = new Map<number, ArrayBuffer>();
const readChunk = async (index: number) => {
if (chunkCache.has(index)) return chunkCache.get(index)!;
const data = await file.slice(start, end).arrayBuffer();
// 只缓存窗口内的块
if (index >= ackedCount && index < sentCount + windowSize) {
chunkCache.set(index, data);
}
return data;
};
```
#### 窗口大小配置
```typescript
// WebRTC 模式(网络不稳定,窗口较小)
windowSize: 4 // 同时发送 4 个块
// WebSocket 模式(局域网,窗口可更大)
windowSize: 8 // 同时发送 8 个块
```
### 3. 接收端优化V3
#### 直接写入磁盘
```typescript
// 原实现:内存累积
file.chunks[index] = data; // ❌ 所有块在内存
// V2IndexedDB
await storage.saveChunk(fileId, index, data); // ⚠️ 有容量限制
// V3直接写磁盘
const position = index * chunkSize;
await writer.writeAt(position, data); // ✅ 零内存占用
```
#### 完成处理
```typescript
// 流式模式:文件已保存,直接关闭
await writer.close();
console.log('文件已保存到用户选择的位置');
// 降级模式:触发浏览器下载
await writer.close(); // 自动触发下载对话框
```
## 配置参数
### TransferConfig
```typescript
{
chunkSize: 64 * 1024, // 块大小64KB
windowSize: 4, // 滑动窗口大小
enableAck: true, // 启用 ACK 确认
ackTimeout: 2000, // ACK 超时(毫秒)
maxRetries: 3, // 最大重试次数
}
```
### 内存监控
```typescript
import { TransferMemoryMonitor } from '@/lib/memory-monitor';
const monitor = new TransferMemoryMonitor();
monitor.setWarningThreshold(0.8); // 80% 触发警告
monitor.onWarning((stats) => {
console.warn('内存使用过高:', stats.percentage);
// 可以暂停传输或提示用户
});
monitor.startMonitoring(1000); // 每秒检查
```
## 性能特性
### 内存使用
| 文件大小 | 峰值内存 | 磁盘占用 |
|---------|---------|----------|
| 100MB | ~10MB | 100MB |
| 1GB | ~20MB | 1GB |
| 10GB | ~20MB | 10GB |
| 100GB | ~20MB | 100GB |
> **说明**峰值内存主要来自滑动窗口4-8 个块,每块 64KB+ 系统缓冲区
### 传输速度
- **局域网 WebSocket**100MB/s+
- **WebRTC DataChannel**10-50MB/s
- **磁盘写入**取决于硬盘SSD 500MB/s+, HDD 100MB/s+
### 容量限制
| 方案 | 容量限制 |
|------|----------|
| 原实现(内存) | RAM 大小(~8GB |
| V2IndexedDB | ~几十GB浏览器配额 |
| **V3磁盘流** | **磁盘大小(无实际限制)** |
> **V3 优势**:用户可以传输 100GB+ 的文件,只要硬盘空间足够
## 使用示例
### 发送大文件
```typescript
const protocol = new ConnectionTransferProtocol(connection, {
chunkSize: 64 * 1024,
windowSize: 4,
enableAck: true,
});
// 发送 100GB 文件,内存稳定在 ~20MB
const result = await protocol.sendFile(largeFile, 'file-123');
```
### 接收大文件
```typescript
// V3接收端会自动提示用户选择保存位置
protocol.onFileStart((meta) => {
console.log('准备接收:', meta.name, meta.size);
// 用户会看到文件保存对话框
});
protocol.onFileComplete(({ id, file }) => {
console.log('文件传输完成!');
// 流式模式:文件已保存到用户选择的位置
// 降级模式:浏览器已触发下载
});
protocol.onFileProgress(({ fileName, progress }) => {
console.log(`${fileName}: ${progress.toFixed(1)}%`);
});
```
### 用户体验
**Chrome/Edge流式模式**
1. 开始接收时弹出"保存文件"对话框
2. 用户选择保存位置
3. 文件边接收边写入磁盘
4. 完成后文件直接出现在选择的位置
**Firefox/Safari降级模式**
1. 静默接收(内存中)
2. 接收完成后触发浏览器下载
3. 用户在下载栏看到文件
### 监控内存
```typescript
import { TransferMemoryMonitor } from '@/lib/memory-monitor';
const monitor = new TransferMemoryMonitor();
monitor.onWarning((stats) => {
toast.warning(`内存使用: ${stats.percentage * 100}%`);
});
monitor.startMonitoring();
// 清理
onUnmount(() => {
monitor.stopMonitoring();
});
```
## 最佳实践
### 1. 提前检测浏览器能力
```typescript
import { supportsStreamWrite } from '@/lib/stream-writer';
if (supportsStreamWrite()) {
console.log('✅ 支持流式写入,可传输任意大小文件');
} else {
console.warn('⚠️ 使用降级模式,大文件会占用内存');
// 可以限制文件大小
if (fileSize > 1024 * 1024 * 1024) {
alert('您的浏览器不支持大文件传输,请使用 Chrome 或 Edge');
}
}
```
### 2. 错误处理(用户取消保存)
```typescript
protocol.onFileError(({ fileId, error }) => {
if (error.includes('用户取消')) {
console.log('用户取消了文件保存');
} else {
console.error('传输失败:', error);
}
});
```
### 3. 错误处理
```typescript
protocol.onFileError(({ fileId, error }) => {
console.error('传输失败:', error);
// 清理失败的文件块
getGlobalChunkStorage()
.deleteFile(fileId)
.catch(err => console.error('清理失败:', err));
});
```
### 4. 进度显示
```typescript
protocol.onFileProgress(({ fileName, progress, transferredBytes, totalBytes }) => {
console.log(`${fileName}: ${progress.toFixed(1)}% (${transferredBytes}/${totalBytes})`);
// 更新 UI
setProgress(progress);
});
```
## 兼容性
### 浏览器支持
| 特性 | Chrome | Firefox | Safari | Edge |
|------|--------|---------|--------|------|
| IndexedDB | ✅ 24+ | ✅ 16+ | ✅ 10+ | ✅ 12+ |
| Async Iterator | ✅ 63+ | ✅ 57+ | ✅ 11.1+ | ✅ 79+ |
| File.slice | ✅ 21+ | ✅ 13+ | ✅ 10+ | ✅ 12+ |
### 降级方案
如果浏览器不支持 IndexedDB极少见可以
1. 限制文件大小(如 100MB
2. 显示警告提示用户升级浏览器
3. 使用分段上传到服务器
## 故障排查
### 问题 1用户取消了保存对话框
**症状**:接收失败,提示"初始化失败"
**原因**:用户在 File System Access API 对话框中点了取消
**解决**
```typescript
protocol.onFileError(({ error }) => {
if (error.includes('用户')) {
toast.info('已取消接收文件');
}
});
```
### 问题 2降级模式内存仍然很高
**排查**
1. 检查是否有其他地方缓存了文件
2. 使用 Chrome DevTools Memory Profiler
3. 确认 `chunkCache.clear()` 被调用
**解决**
```typescript
// 强制垃圾回收(仅开发环境)
if (typeof gc !== 'undefined') {
gc();
}
```
### 问题 3传输很慢
**排查**
1. IndexedDB 写入可能较慢(首次)
2. 窗口太小(并行度不够)
**解决**
```typescript
// 增大窗口(局域网环境)
windowSize: 16
// 或禁用 ACK可靠网络
enableAck: false
```
## 后续优化
1. **Web Workers**:将 IndexedDB 操作移到 Worker 避免阻塞主线程
2. **压缩传输**:使用 CompressionStream API 压缩块
3. **增量校验**:使用 xxHash 替代 CRC32 提升性能
4. **断点续传**:基于 IndexedDB 实现传输恢复
## 总结
通过这次优化,文件传输不再受内存限制,理论上可以传输任意大小的文件(受限于 IndexedDB 配额)。
**关键改进**
- ✅ 内存使用从 O(n) 降到 O(1)
- ✅ 支持超大文件10GB+
- ✅ 传输速度不受影响
- ✅ 向后兼容,无需修改 UI 层