-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
.write(Buffer) support #174
Changes from 4 commits
9152450
1ed75f0
99b0470
a694b87
ad82abc
7b1079f
5764633
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ const path = require('path') | |
const sleep = require('atomic-sleep') | ||
|
||
const BUSY_WRITE_TIMEOUT = 100 | ||
const kEmptyBuffer = Buffer.allocUnsafe(0) | ||
|
||
// 16 KB. Don't write more than docker buffer size. | ||
// https://github.com/moby/moby/blob/513ec73831269947d38a644c278ce3cac36783b2/daemon/logger/copier.go#L13 | ||
|
@@ -56,7 +57,11 @@ function openFile (file, sonic) { | |
|
||
// start | ||
if (!sonic._writing && sonic._len > sonic.minLength && !sonic.destroyed) { | ||
actualWrite(sonic) | ||
if (sonic.contentMode === 'buffer') { | ||
actualWriteBuffer(sonic) | ||
} else { | ||
actualWrite(sonic) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we assign it as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I was thinking of using |
||
} | ||
} | ||
|
||
|
@@ -87,15 +92,15 @@ function SonicBoom (opts) { | |
return new SonicBoom(opts) | ||
} | ||
|
||
let { fd, dest, minLength, maxLength, maxWrite, sync, append = true, mode, mkdir, retryEAGAIN, fsync } = opts || {} | ||
let { fd, dest, minLength, maxLength, maxWrite, sync, append = true, mkdir, retryEAGAIN, fsync, contentMode, mode } = opts || {} | ||
|
||
fd = fd || dest | ||
|
||
this._bufs = [] | ||
this._len = 0 | ||
this.fd = -1 | ||
this._bufs = [] | ||
this._lens = [] | ||
this._writing = false | ||
this._writingBuf = '' | ||
this._ending = false | ||
this._reopening = false | ||
this._asyncDrainScheduled = false | ||
|
@@ -106,12 +111,36 @@ function SonicBoom (opts) { | |
this.maxLength = maxLength || 0 | ||
this.maxWrite = maxWrite || MAX_WRITE | ||
this.sync = sync || false | ||
this.writable = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. forgot to mention, this is so that node.js stream util compose function works properly with this |
||
this._fsync = fsync || false | ||
this.append = append || false | ||
this.mode = mode | ||
this.retryEAGAIN = retryEAGAIN || (() => true) | ||
this.mkdir = mkdir || false | ||
|
||
let fsWriteSync | ||
let fsWrite | ||
let _actualWrite | ||
if (contentMode === 'buffer') { | ||
this.contentMode = 'buffer' | ||
this._writingBuf = kEmptyBuffer | ||
this.write = writeBuffer | ||
this.flush = flushBuffer | ||
this.flushSync = flushBufferSync | ||
fsWriteSync = () => fs.writeSync(this.fd, this._writingBuf) | ||
fsWrite = () => fs.write(this.fd, this._writingBuf, this.release) | ||
_actualWrite = actualWriteBuffer | ||
} else { | ||
this.contentMode = 'utf8' | ||
this._writingBuf = '' | ||
this.write = write | ||
this.flush = flush | ||
this.flushSync = flushSync | ||
fsWriteSync = () => fs.writeSync(this.fd, this._writingBuf, 'utf8') | ||
fsWrite = () => fs.write(this.fd, this._writingBuf, 'utf8', this.release) | ||
_actualWrite = actualWrite | ||
} | ||
|
||
if (typeof fd === 'number') { | ||
this.fd = fd | ||
process.nextTick(() => this.emit('ready')) | ||
|
@@ -140,9 +169,7 @@ function SonicBoom (opts) { | |
} | ||
} else { | ||
// Let's give the destination some time to process the chunk. | ||
setTimeout(() => { | ||
fs.write(this.fd, this._writingBuf, 'utf8', this.release) | ||
}, BUSY_WRITE_TIMEOUT) | ||
setTimeout(fsWrite, BUSY_WRITE_TIMEOUT) | ||
} | ||
} else { | ||
this._writing = false | ||
|
@@ -171,16 +198,16 @@ function SonicBoom (opts) { | |
|
||
if (this._writingBuf.length) { | ||
if (!this.sync) { | ||
fs.write(this.fd, this._writingBuf, 'utf8', this.release) | ||
fsWrite() | ||
return | ||
} | ||
|
||
try { | ||
do { | ||
const n = fs.writeSync(this.fd, this._writingBuf, 'utf8') | ||
const n = fsWriteSync() | ||
this._len -= n | ||
this._writingBuf = this._writingBuf.slice(n) | ||
} while (this._writingBuf) | ||
} while (this._writingBuf.length) | ||
} catch (err) { | ||
this.release(err) | ||
return | ||
|
@@ -197,10 +224,10 @@ function SonicBoom (opts) { | |
this._reopening = false | ||
this.reopen() | ||
} else if (len > this.minLength) { | ||
actualWrite(this) | ||
_actualWrite(this) | ||
} else if (this._ending) { | ||
if (len > 0) { | ||
actualWrite(this) | ||
_actualWrite(this) | ||
} else { | ||
this._writing = false | ||
actualClose(this) | ||
|
@@ -234,7 +261,19 @@ function emitDrain (sonic) { | |
|
||
inherits(SonicBoom, EventEmitter) | ||
|
||
SonicBoom.prototype.write = function (data) { | ||
function mergeBuf (bufs, len) { | ||
if (bufs.length === 0) { | ||
return kEmptyBuffer | ||
} | ||
|
||
if (bufs.length === 1) { | ||
return bufs[0] | ||
} | ||
|
||
return Buffer.concat(bufs, len) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the source of the slowdown. We should not merge them, but rather keep them as a list. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've tried having a list here, but tests that expect single flush instead of multiples break. Will work on it further now that there are 2 separate content modes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All in all Buffer.concat seem to be cheaper than multiple fs.write as long as it doesn't have to allocate memory outside of the Buffer.poolSize. Will investigate further how that could be avoided There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tried fs.writev to avoid concat, but that made it slower than writeStream |
||
} | ||
|
||
function write (data) { | ||
if (this.destroyed) { | ||
throw new Error('SonicBoom destroyed') | ||
} | ||
|
@@ -265,7 +304,41 @@ SonicBoom.prototype.write = function (data) { | |
return this._len < this._hwm | ||
} | ||
|
||
SonicBoom.prototype.flush = function () { | ||
function writeBuffer (data) { | ||
if (this.destroyed) { | ||
throw new Error('SonicBoom destroyed') | ||
} | ||
|
||
const len = this._len + data.length | ||
const bufs = this._bufs | ||
const lens = this._lens | ||
|
||
if (this.maxLength && len > this.maxLength) { | ||
this.emit('drop', data) | ||
return this._len < this._hwm | ||
} | ||
|
||
if ( | ||
bufs.length === 0 || | ||
lens[lens.length - 1] + data.length > this.maxWrite | ||
) { | ||
bufs.push([data]) | ||
lens.push(data.length) | ||
} else { | ||
bufs[bufs.length - 1].push(data) | ||
lens[lens.length - 1] += data.length | ||
} | ||
|
||
this._len = len | ||
|
||
if (!this._writing && this._len >= this.minLength) { | ||
actualWriteBuffer(this) | ||
} | ||
|
||
return this._len < this._hwm | ||
} | ||
|
||
function flush () { | ||
if (this.destroyed) { | ||
throw new Error('SonicBoom destroyed') | ||
} | ||
|
@@ -281,6 +354,23 @@ SonicBoom.prototype.flush = function () { | |
actualWrite(this) | ||
} | ||
|
||
function flushBuffer () { | ||
if (this.destroyed) { | ||
throw new Error('SonicBoom destroyed') | ||
} | ||
|
||
if (this._writing || this.minLength <= 0) { | ||
return | ||
} | ||
|
||
if (this._bufs.length === 0) { | ||
this._bufs.push([]) | ||
this._lens.push(0) | ||
} | ||
|
||
actualWriteBuffer(this) | ||
} | ||
|
||
SonicBoom.prototype.reopen = function (file) { | ||
if (this.destroyed) { | ||
throw new Error('SonicBoom destroyed') | ||
|
@@ -344,13 +434,17 @@ SonicBoom.prototype.end = function () { | |
} | ||
|
||
if (this._len > 0 && this.fd >= 0) { | ||
actualWrite(this) | ||
if (this.contentMode === 'buffer') { | ||
actualWriteBuffer(this) | ||
} else { | ||
actualWrite(this) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, can't we call |
||
} else { | ||
actualClose(this) | ||
} | ||
} | ||
|
||
SonicBoom.prototype.flushSync = function () { | ||
function flushSync () { | ||
if (this.destroyed) { | ||
throw new Error('SonicBoom destroyed') | ||
} | ||
|
@@ -365,7 +459,7 @@ SonicBoom.prototype.flushSync = function () { | |
} | ||
|
||
let buf = '' | ||
while (this._bufs.length || buf.length) { | ||
while (this._bufs.length || buf) { | ||
if (buf.length <= 0) { | ||
buf = this._bufs[0] | ||
} | ||
|
@@ -387,6 +481,44 @@ SonicBoom.prototype.flushSync = function () { | |
} | ||
} | ||
|
||
function flushBufferSync () { | ||
if (this.destroyed) { | ||
throw new Error('SonicBoom destroyed') | ||
} | ||
|
||
if (this.fd < 0) { | ||
throw new Error('sonic boom is not ready yet') | ||
} | ||
|
||
if (!this._writing && this._writingBuf.length > 0) { | ||
this._bufs.unshift([this._writingBuf]) | ||
this._writingBuf = kEmptyBuffer | ||
} | ||
|
||
let buf = kEmptyBuffer | ||
while (this._bufs.length || buf.length) { | ||
if (buf.length <= 0) { | ||
buf = mergeBuf(this._bufs[0], this._lens[0]) | ||
} | ||
try { | ||
const n = fs.writeSync(this.fd, buf) | ||
buf = buf.subarray(n) | ||
this._len = Math.max(this._len - n, 0) | ||
if (buf.length <= 0) { | ||
this._bufs.shift() | ||
this._lens.shift() | ||
} | ||
} catch (err) { | ||
const shouldRetry = err.code === 'EAGAIN' || err.code === 'EBUSY' | ||
if (shouldRetry && !this.retryEAGAIN(err, buf.length, this._len - buf.length)) { | ||
throw err | ||
} | ||
|
||
sleep(BUSY_WRITE_TIMEOUT) | ||
} | ||
} | ||
} | ||
|
||
SonicBoom.prototype.destroy = function () { | ||
if (this.destroyed) { | ||
return | ||
|
@@ -411,6 +543,23 @@ function actualWrite (sonic) { | |
} | ||
} | ||
|
||
function actualWriteBuffer (sonic) { | ||
const release = sonic.release | ||
sonic._writing = true | ||
sonic._writingBuf = sonic._writingBuf.length ? sonic._writingBuf : mergeBuf(sonic._bufs.shift(), sonic._lens.shift()) | ||
|
||
if (sonic.sync) { | ||
try { | ||
const written = fs.writeSync(sonic.fd, sonic._writingBuf) | ||
release(null, written) | ||
} catch (err) { | ||
release(err) | ||
} | ||
} else { | ||
fs.write(sonic.fd, sonic._writingBuf, release) | ||
} | ||
} | ||
|
||
function actualClose (sonic) { | ||
if (sonic.fd === -1) { | ||
sonic.once('ready', actualClose.bind(null, sonic)) | ||
|
@@ -419,6 +568,7 @@ function actualClose (sonic) { | |
|
||
sonic.destroyed = true | ||
sonic._bufs = [] | ||
sonic._lens = [] | ||
|
||
if (sonic.fd !== 1 && sonic.fd !== 2) { | ||
fs.close(sonic.fd, done) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if
contentMode
as an enum instead of a string would make more sense. Either way, this should be included intypes/index.d.ts
and should be documented.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great idea!