我试图使用fs.createWriteStream
扫描驱动器目录(递归地遍历所有路径)并将所有路径写到文件中(以便找到它们),以保持较低的内存使用率,但是不起作用,在扫描期间内存使用量达到2GB。
我期望fs.createWriteStream
始终自动处理内存/磁盘使用情况,以使内存使用率保持在最低水平,并产生反压。
const fs = require('fs')
const walkdir = require('walkdir')
let dir = 'C:/'
let options = {
"max_depth": 0,
"track_inodes": true,
"return_object": false,
"no_return": true,
}
const wstream = fs.createWriteStream("C:/Users/USERNAME/Desktop/paths.txt")
let walker = walkdir(dir, options)
walker.on('path', (path) => {
wstream.write(path + '\n')
})
walker.on('end', (path) => {
wstream.end()
})
是因为我没有使用.pipe()
吗?我尝试创建一个new Stream.Readable({read{}})
,然后在.on('path'
发射器内部,用readable.push(path)
将路径推入其中,但这并没有真正起作用。
更新:
方法2:
我尝试了在答案drain
中提出的建议,但并没有太大帮助,它确实将内存使用量减少到500mb(对于流来说仍然太多了),但是却显着降低了代码的速度(从秒到分钟)
方法3:
我也尝试使用readdirp
,它使用更少的内存(〜400mb)并且速度更快,但是我不知道如何暂停它并在其中使用drain
方法来减少内存使用量进一步:
const readdirp = require('readdirp')
let dir = 'C:/'
const wstream = fs.createWriteStream("C:/Users/USERNAME/Desktop/paths.txt")
readdirp(dir, {alwaysStat: false, type: 'files_directories'})
.on('data', (entry) => {
wstream.write(`${entry.fullPath}\n`)
})
方法4:
我还尝试使用自定义的递归walker进行此操作,尽管它只使用了30mb的内存,这是我想要的,但是它比readdirp
方法要慢10倍,并且{ {1}},这是不可取的:
synchronous
答案 0 :(得分:5)
初步观察:您试图使用多种方法来获得所需的结果。比较您使用的方法时,一个复杂之处是它们并没有全部完成相同的工作。如果您在仅包含常规文件的文件树上运行测试,而该树不包含安装点,则可以可能比较这些方法,但是当您开始添加安装点,符号链接等时,您会可能仅由于一种方法排除了另一种方法包含的文件这一事实,可能会获得不同的内存和时间统计信息。
我最初尝试使用readdirp
的解决方案,但不幸的是,但是该库对我来说似乎是个问题。在这里在我的系统上运行它,结果不一致。一次运行将输出10Mb的数据,另一次运行具有相同的输入参数将输出22Mb,然后得到另一个数字,依此类推。我查看了一下代码,发现它does not respect的返回值为{{1 }}:
push
根据the documentation,_push(entry) {
if (this.readable) {
this.push(entry);
}
}
方法可能返回一个push
值,在这种情况下,false
流应该停止产生数据并等待直到Readable
再次打电话。 _read
完全忽略了规范的那部分。 至关重要的是要注意readdirp
的返回值,以便正确处理背压。在该代码中还有其他一些问题值得关注。
因此,我放弃了这一点,而是进行了概念验证,展示了如何做到这一点。关键部分是:
当push
方法返回push
时,必须停止向流中添加数据。相反,我们记录我们的位置,然后停下来。
我们仅在调用false
时重新开始。
如果取消注释打印_read
和console.log
的{{1}}语句。您会在控制台上看到它们的顺序打印。我们开始,产生数据,直到Node告诉我们停止,然后停止,直到Node告诉我们再次开始,依此类推。
START
当我在这里使用STOP
进行首次尝试时,我得到以下统计信息:
使用上面显示的代码时:
我用于测试的文件树产生了792 MB的文件列表
答案 1 :(得分:2)
您可以利用WritableStream.write()
的返回值:它实质上表明您是否应该继续读取。 WritableStream
具有一个内部属性,该属性存储阈值,在此阈值之后,操作系统应处理缓冲区。清空缓冲区后,将触发drain
事件,即您可以安全地调用WritableStream.write()
,而不必担心过多地填充缓冲区(这意味着RAM)。幸运的是,walkdir
使您可以控制流程:您可以发出pause
(暂停步行。在恢复之前不会再发出任何事件)和resume
(恢复步行)事件walkdir对象,相应地暂停和恢复流上的写入过程。试试这个:
let is_emitter_paused = false;
wstream.on('drain', (evt) => {
if (is_emitter_paused) {
walkdir.resume();
}
});
walkdir.on('path', function(path, stat) {
is_emitter_paused = !wstream.write(path + '\n');
if (is_emitter_paused) {
walkdir.pause();
}
});
答案 2 :(得分:1)
这是@Louis的回答启发的实现。我认为这样做要容易一些,而在我的最少测试中,它的表现大致相同。
const fs = require('fs');
const path = require('path');
const stream = require('stream');
class Walker extends stream.Readable {
constructor(root = process.cwd(), maxDepth = Infinity) {
super();
// Dirs to process
this._dirs = [{ path: root, depth: 0 }];
// Max traversal depth
this._maxDepth = maxDepth;
// Files to flush
this._files = [];
}
_drain() {
while (this._files.length > 0) {
const file = this._files.pop();
if (file.isFile() || file.isDirectory() || file.isSymbolicLink()) {
const filePath = path.join(this._dir.path, file.name);
if (file.isDirectory() && this._maxDepth > this._dir.depth) {
// Add directory to be walked at a later time
this._dirs.push({ path: filePath, depth: this._dir.depth + 1 });
}
if (!this.push(`${filePath}\n`)) {
// Hault walking
return false;
}
}
}
if (this._dirs.length === 0) {
// Walking complete
this.push(null);
return false;
}
// Continue walking
return true;
}
async _step() {
try {
this._dir = this._dirs.pop();
this._files = await fs.promises.readdir(this._dir.path, { withFileTypes: true });
} catch (e) {
this.emit('error', e); // Uh oh...
}
}
async _walk() {
this.walking = true;
while (this._drain()) {
await this._step();
}
this.walking = false;
}
_read() {
if (!this.walking) {
this._walk();
}
}
}
stream.pipeline(new Walker('some/dir/path', 5),
fs.createWriteStream('output.txt'),
(err) => console.log('ended with', err));