Skip to content
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

Direct IO #133

Merged
merged 23 commits into from
Apr 7, 2020
Merged

Direct IO #133

merged 23 commits into from
Apr 7, 2020

Conversation

zvin
Copy link
Contributor

@zvin zvin commented Mar 27, 2020

Major change

Only use aligned buffers to write to block devices.
It means all read streams read into aligned buffers when the target is a block device.
When we don't control the read stream, use a transform that will copy its buffers to aligned buffers.
Each read stream uses a pool of 1 MiB buffers to minimize allocations.

This should improve speed and reduce cpu usage.

@zvin zvin requested review from Page- and thundron March 27, 2020 18:28
@zvin zvin changed the title Directio Direct IO Mar 27, 2020
package.json Outdated Show resolved Hide resolved
lib/source-destination/file.ts Show resolved Hide resolved
lib/fs.ts Outdated Show resolved Hide resolved
lib/lockable-buffer.ts Outdated Show resolved Hide resolved
lib/lockable-buffer.ts Outdated Show resolved Hide resolved
lib/aligned-lockable-buffer.ts Outdated Show resolved Hide resolved
lib/block-transform-stream.ts Outdated Show resolved Hide resolved
lib/block-transform-stream.ts Outdated Show resolved Hide resolved
lib/block-transform-stream.ts Show resolved Hide resolved
lib/source-destination/multi-destination.ts Outdated Show resolved Hide resolved
this.bytesWritten += chunk.buffer.length;
const unlock = isAlignedLockableBuffer(chunk.buffer)
? await chunk.buffer.rlock()
: noop;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also make this

Suggested change
: noop;
: undefined;

and use unlock?.() below if you want

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

this.push({ position: chunk.position, buffer });
this.bytesWritten += chunk.buffer.length;
this.position += chunk.buffer.length;
callback();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callback isn't guaranteed to be called if await buffer.lock() fails - is that a case that should be handled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it should be handled, updated

Comment on lines 113 to 115
if (this.state.hasher !== undefined) {
this.state.hasher.update(buffer);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be

Suggested change
if (this.state.hasher !== undefined) {
this.state.hasher.update(buffer);
}
this.state.hasher?.update(buffer);

if you want

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

this.alignedReadableState !== undefined
? this.alignedReadableState.getCurrentBuffer().slice(0, length)
: Buffer.allocUnsafe(length);
const unlock = isAlignedLockableBuffer(buffer) ? await buffer.lock() : noop;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be

Suggested change
const unlock = isAlignedLockableBuffer(buffer) ? await buffer.lock() : noop;
const unlock = isAlignedLockableBuffer(buffer) ? await buffer.lock() : undefined;

and unlock?.()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

public async createSparseWriteStream(): Promise<SparseWritable> {
return await this.createStream('createSparseWriteStream');
public async createSparseWriteStream(
...args: any[]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can be properly typed using

Suggested change
...args: any[]
...args: Parameters<SourceDestination['createSparseWriteStream']>

public async createWriteStream(): Promise<NodeJS.WritableStream> {
return await this.createStream('createWriteStream');
public async createWriteStream(
...args: any[]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can be properly typed using

Suggested change
...args: any[]
...args: Parameters<SourceDestination['createWriteStream']>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, thanks

lib/source-destination/dmg.ts Show resolved Hide resolved
public _flush(callback: (error?: Error) => void) {
this.writeBuffers(true);
callback();
public async _flush(callback: (error?: Error) => void): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns an immediately-resolved promise, is that intended or should this be

Suggested change
public async _flush(callback: (error?: Error) => void): Promise<void> {
public _flush(callback: (error?: Error) => void): void {

or have an await?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, fixed

chunk: Buffer,
_encoding: string,
callback: (error?: Error) => void,
) {
): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also an immediately-resolved promise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

const { bytesRead } = await this.tryRead(buffer);
if (bytesRead === 0) {
unlock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in a finally block?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, added

@@ -55,7 +55,7 @@ export interface SparseWritable extends NodeJS.WritableStream {
_write(
chunk: SparseStreamChunk,
encoding: string,
callback: (err?: Error | void) => void,
callback: (err?: Error | void | null) => void,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be

Suggested change
callback: (err?: Error | void | null) => void,
callback: (err?: Error | null) => void,

as void doesn't really make sense as a parameter value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

lib/utils.ts Outdated
@@ -84,7 +84,7 @@ export function difference<T>(setA: Set<T>, setB: Set<T>): Set<T> {

export async function asCallback<T>(
promise: Promise<T>,
callback: (error: Error | void, value?: T) => void,
callback: (error: Error | void | null, value?: T) => void,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another place to drop void

Suggested change
callback: (error: Error | void | null, value?: T) => void,
callback: (error: Error | null, value?: T) => void,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropped

tests/zip.spec.ts Outdated Show resolved Hide resolved
tsconfig.json Outdated Show resolved Hide resolved
Change-type: major
lib/utils.ts Outdated
const unlock = await chunk.buffer.rlock();
const data = Buffer.allocUnsafe(chunk.buffer.length);
chunk.buffer.copy(data);
unlock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in a try/finally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, added

lib/utils.ts Outdated
stream.on('data', chunks.push.bind(chunks));
stream.on('data', async (chunk: SparseStreamChunk) => {
if (isAlignedLockableBuffer(chunk.buffer)) {
const unlock = await chunk.buffer.rlock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this lock fails (can it fail?) then the chunk will be silently lost

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this function is only used in tests, fixed it anyway.

lib/utils.ts Outdated
Comment on lines 30 to 33
const unlock = await chunk.rlock();
data = Buffer.allocUnsafe(chunk.length);
chunk.copy(data);
unlock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concerns here about try/catch/finally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Comment on lines 256 to 259
private async createStream(
methodName: 'createWriteStream' | 'createSparseWriteStream',
...args: any[]
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want 100% typing you can do:

Suggested change
private async createStream(
methodName: 'createWriteStream' | 'createSparseWriteStream',
...args: any[]
) {
private async createStream(
methodName: 'createWriteStream',
...args: Parameters<SourceDestination['createWriteStream']>
) {
private async createStream(
methodName: 'createSparseWriteStream',
...args: Parameters<SourceDestination['createSparseWriteStream']>
) {
private async createStream(
methodName: 'createWriteStream' | 'createSparseWriteStream',
...args: Parameters<SourceDestination['createWriteStream']> | Parameters<SourceDestination['createSparseWriteStream']>
) {

although I'm also OK with skipping it as it's a private method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, thanks

let block = Buffer.concat(this._buffers);
private async writeBuffers(flush = false): Promise<void> {
if (flush || this.inputBytes >= this.chunkSize) {
// TODO optimize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw node 12.16.2 is slated to include buffer.concat performance improvements: nodejs/node#32313 / nodejs/node#31522

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can probably completely avoid Buffer.concat, but that'll make it more complex to read.

this.push(null);
return;
const unlock = await buffer.lock();
let bytesRead: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typescript should be able to infer the type if you want:

Suggested change
let bytesRead: number;
let bytesRead;

but I'm not fussed either way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

zvin added 2 commits April 7, 2020 16:22
Change-type: patch
Change-type: patch
@zvin zvin merged commit 9960a67 into master Apr 7, 2020
@zvin zvin deleted the directio branch April 7, 2020 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants