将数据写入文件时,fs.createWriteStream不使用背压,从而导致较高的内存使用量

时间:2019-05-08 11:42:41

标签: javascript node.js

问题

我试图使用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

3 个答案:

答案 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的返回值,以便正确处理背压。在该代码中还有其他一些问题值得关注。

因此,我放弃了这一点,而是进行了概念验证,展示了如何做到这一点。关键部分是:

  1. push方法返回push时,必须停止向流中添加数据。相反,我们记录我们的位置,然后停下来。

  2. 我们仅在调用false时重新开始。

如果取消注释打印_readconsole.log的{​​{1}}语句。您会在控制台上看到它们的顺序打印。我们开始,产生数据,直到Node告诉我们停止,然后停止,直到Node告诉我们再次开始,依此类推。

START

当我在这里使用STOP进行首次尝试时,我得到以下统计信息:

  • 经过的时间(挂钟):59秒
  • 最大居民集大小:2.90 GB

使用上面显示的代码时:

  • 经过的时间(挂钟):35秒
  • 最大居民集大小:0.1 GB

我用于测试的文件树产生了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));