Merge pull request #233 from vibe-d/concurrent_pipe
Add a concurrent pipe() mode
This commit is contained in:
commit
65b921cc65
|
@ -1,3 +1,11 @@
|
||||||
|
1.11.0 - 2020-10-24
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Added a concurrent mode to `pipe()` using `PipeMode.concurrent` to improve throughput in I/O limited situations - [pull #233][issue233]
|
||||||
|
|
||||||
|
[issue233]: https://github.com/vibe-d/vibe-core/issues/233
|
||||||
|
|
||||||
|
|
||||||
1.10.3 - 2020-10-15
|
1.10.3 - 2020-10-15
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|
|
@ -1102,7 +1102,7 @@ void setTaskCreationCallback(TaskCreationCallback func)
|
||||||
/**
|
/**
|
||||||
A version string representing the current vibe.d core version
|
A version string representing the current vibe.d core version
|
||||||
*/
|
*/
|
||||||
enum vibeVersionString = "1.10.3";
|
enum vibeVersionString = "1.11.0";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -242,7 +242,7 @@ void copyFile(NativePath from, NativePath to, bool overwrite = false)
|
||||||
auto dst = openFile(to, FileMode.createTrunc);
|
auto dst = openFile(to, FileMode.createTrunc);
|
||||||
scope(exit) dst.close();
|
scope(exit) dst.close();
|
||||||
dst.truncate(src.size);
|
dst.truncate(src.size);
|
||||||
dst.write(src);
|
src.pipe(dst, PipeMode.concurrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: also retain creation time on windows
|
// TODO: also retain creation time on windows
|
||||||
|
@ -651,7 +651,7 @@ struct FileStream {
|
||||||
void write(InputStream)(InputStream stream, ulong nbytes = ulong.max)
|
void write(InputStream)(InputStream stream, ulong nbytes = ulong.max)
|
||||||
if (isInputStream!InputStream)
|
if (isInputStream!InputStream)
|
||||||
{
|
{
|
||||||
writeDefault(this, stream, nbytes);
|
pipe(stream, this, nbytes, PipeMode.concurrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
void flush()
|
void flush()
|
||||||
|
@ -670,38 +670,6 @@ struct FileStream {
|
||||||
mixin validateRandomAccessStream!FileStream;
|
mixin validateRandomAccessStream!FileStream;
|
||||||
|
|
||||||
|
|
||||||
private void writeDefault(OutputStream, InputStream)(ref OutputStream dst, InputStream stream, ulong nbytes = ulong.max)
|
|
||||||
if (isOutputStream!OutputStream && isInputStream!InputStream)
|
|
||||||
{
|
|
||||||
import vibe.internal.allocator : theAllocator, make, dispose;
|
|
||||||
import std.algorithm.comparison : min;
|
|
||||||
|
|
||||||
static struct Buffer { ubyte[64*1024] bytes = void; }
|
|
||||||
auto bufferobj = () @trusted { return theAllocator.make!Buffer(); } ();
|
|
||||||
scope (exit) () @trusted { theAllocator.dispose(bufferobj); } ();
|
|
||||||
auto buffer = bufferobj.bytes[];
|
|
||||||
|
|
||||||
//logTrace("default write %d bytes, empty=%s", nbytes, stream.empty);
|
|
||||||
if (nbytes == ulong.max) {
|
|
||||||
while (!stream.empty) {
|
|
||||||
size_t chunk = min(stream.leastSize, buffer.length);
|
|
||||||
assert(chunk > 0, "leastSize returned zero for non-empty stream.");
|
|
||||||
//logTrace("read pipe chunk %d", chunk);
|
|
||||||
stream.read(buffer[0 .. chunk]);
|
|
||||||
dst.write(buffer[0 .. chunk]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
while (nbytes > 0) {
|
|
||||||
size_t chunk = min(nbytes, buffer.length);
|
|
||||||
//logTrace("read pipe chunk %d", chunk);
|
|
||||||
stream.read(buffer[0 .. chunk]);
|
|
||||||
dst.write(buffer[0 .. chunk]);
|
|
||||||
nbytes -= chunk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Interface for directory watcher implementations.
|
Interface for directory watcher implementations.
|
||||||
|
|
||||||
|
|
|
@ -35,17 +35,23 @@ public import eventcore.driver : IOMode;
|
||||||
The actual number of bytes written is returned. If `nbytes` is given
|
The actual number of bytes written is returned. If `nbytes` is given
|
||||||
and not equal to `ulong.max`, íts value will be returned.
|
and not equal to `ulong.max`, íts value will be returned.
|
||||||
*/
|
*/
|
||||||
ulong pipe(InputStream, OutputStream)(InputStream source, OutputStream sink, ulong nbytes)
|
ulong pipe(InputStream, OutputStream)(InputStream source, OutputStream sink,
|
||||||
@blocking @trusted
|
ulong nbytes, PipeMode mode = PipeMode.sequential) @blocking @trusted
|
||||||
if (isOutputStream!OutputStream && isInputStream!InputStream)
|
if (isOutputStream!OutputStream && isInputStream!InputStream)
|
||||||
{
|
{
|
||||||
import vibe.internal.allocator : theAllocator, makeArray, dispose;
|
import vibe.internal.allocator : theAllocator, makeArray, dispose;
|
||||||
|
import vibe.core.core : runTask;
|
||||||
|
import vibe.core.sync : LocalManualEvent, createManualEvent;
|
||||||
|
import vibe.core.task : InterruptException;
|
||||||
|
|
||||||
|
final switch (mode) {
|
||||||
|
case PipeMode.sequential:
|
||||||
|
{
|
||||||
scope buffer = cast(ubyte[]) theAllocator.allocate(64*1024);
|
scope buffer = cast(ubyte[]) theAllocator.allocate(64*1024);
|
||||||
scope (exit) theAllocator.dispose(buffer);
|
scope (exit) theAllocator.dispose(buffer);
|
||||||
|
|
||||||
//logTrace("default write %d bytes, empty=%s", nbytes, stream.empty);
|
|
||||||
ulong ret = 0;
|
ulong ret = 0;
|
||||||
|
|
||||||
if (nbytes == ulong.max) {
|
if (nbytes == ulong.max) {
|
||||||
while (!source.empty) {
|
while (!source.empty) {
|
||||||
size_t chunk = min(source.leastSize, buffer.length);
|
size_t chunk = min(source.leastSize, buffer.length);
|
||||||
|
@ -65,14 +71,123 @@ ulong pipe(InputStream, OutputStream)(InputStream source, OutputStream sink, ulo
|
||||||
ret += chunk;
|
ret += chunk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
}
|
||||||
|
case PipeMode.concurrent:
|
||||||
|
{
|
||||||
|
enum bufcount = 4;
|
||||||
|
enum bufsize = 64*1024;
|
||||||
|
|
||||||
|
static struct ConcurrentPipeState {
|
||||||
|
InputStream source;
|
||||||
|
OutputStream sink;
|
||||||
|
ulong nbytes;
|
||||||
|
ubyte[][bufcount] buffers;
|
||||||
|
size_t[bufcount] bufferFill;
|
||||||
|
// buffer index that is being read/written
|
||||||
|
size_t read_idx = 0, write_idx = 0;
|
||||||
|
Exception readex;
|
||||||
|
bool done = false;
|
||||||
|
LocalManualEvent evt;
|
||||||
|
size_t bytesWritten;
|
||||||
|
|
||||||
|
void readLoop()
|
||||||
|
{
|
||||||
|
while (true) {
|
||||||
|
ulong remaining = nbytes == ulong.max ? source.leastSize : nbytes;
|
||||||
|
if (remaining == 0) break;
|
||||||
|
|
||||||
|
while (read_idx >= write_idx + buffers.length)
|
||||||
|
evt.wait();
|
||||||
|
|
||||||
|
size_t chunk = min(remaining, bufsize);
|
||||||
|
auto bi = read_idx % bufcount;
|
||||||
|
source.read(buffers[bi][0 .. chunk]);
|
||||||
|
if (nbytes != ulong.max) nbytes -= chunk;
|
||||||
|
bytesWritten += chunk;
|
||||||
|
bufferFill[bi] = chunk;
|
||||||
|
if (write_idx >= read_idx++)
|
||||||
|
evt.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeLoop()
|
||||||
|
{
|
||||||
|
while (read_idx > write_idx || !done) {
|
||||||
|
while (read_idx <= write_idx) {
|
||||||
|
if (done) return;
|
||||||
|
evt.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bi = write_idx % bufcount;
|
||||||
|
sink.write(buffers[bi][0 .. bufferFill[bi]]);
|
||||||
|
|
||||||
|
// notify reader that we just made a buffer available
|
||||||
|
if (write_idx++ <= read_idx - buffers.length)
|
||||||
|
evt.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope buffer = cast(ubyte[]) theAllocator.allocate(bufcount * bufsize);
|
||||||
|
scope (exit) theAllocator.dispose(buffer);
|
||||||
|
|
||||||
|
ConcurrentPipeState state;
|
||||||
|
foreach (i; 0 .. bufcount)
|
||||||
|
state.buffers[i] = buffer[i*($/bufcount) .. (i+1)*($/bufcount)];
|
||||||
|
swap(state.source, source);
|
||||||
|
swap(state.sink, sink);
|
||||||
|
state.nbytes = nbytes;
|
||||||
|
state.evt = createManualEvent();
|
||||||
|
|
||||||
|
auto reader = runTask(function(ConcurrentPipeState* state) nothrow {
|
||||||
|
try state.readLoop();
|
||||||
|
catch (InterruptException e) {}
|
||||||
|
catch (Exception e) state.readex = e;
|
||||||
|
state.done = true;
|
||||||
|
state.evt.emit();
|
||||||
|
}, &state);
|
||||||
|
|
||||||
|
scope (failure) {
|
||||||
|
reader.interrupt();
|
||||||
|
reader.joinUninterruptible();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.writeLoop();
|
||||||
|
|
||||||
|
reader.join();
|
||||||
|
|
||||||
|
if (state.readex) throw state.readex;
|
||||||
|
|
||||||
|
return state.bytesWritten;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// ditto
|
/// ditto
|
||||||
ulong pipe(InputStream, OutputStream)(InputStream source, OutputStream sink)
|
ulong pipe(InputStream, OutputStream)(InputStream source, OutputStream sink,
|
||||||
@blocking
|
PipeMode mode = PipeMode.sequential) @blocking
|
||||||
if (isOutputStream!OutputStream && isInputStream!InputStream)
|
if (isOutputStream!OutputStream && isInputStream!InputStream)
|
||||||
{
|
{
|
||||||
return pipe(source, sink, ulong.max);
|
return pipe(source, sink, ulong.max, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PipeMode {
|
||||||
|
/** Sequentially reads into a buffer and writes it out to the sink.
|
||||||
|
|
||||||
|
This mode reads and writes to the same buffer in a ping-pong fashion.
|
||||||
|
The memory overhead is low, but if the source does not support
|
||||||
|
read-ahead buffering, or the sink does not have an internal buffer that
|
||||||
|
is drained asynchronously, the total throghput will be reduced.
|
||||||
|
*/
|
||||||
|
sequential,
|
||||||
|
|
||||||
|
/** Uses a task to concurrently read and write.
|
||||||
|
|
||||||
|
This mode maximizes throughput at the expense of setting up a task and
|
||||||
|
associated sycnronization.
|
||||||
|
*/
|
||||||
|
concurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,10 @@ void runTest()
|
||||||
write(bar, null);
|
write(bar, null);
|
||||||
assert(!watcher.readChanges(changes, 100.msecs));
|
assert(!watcher.readChanges(changes, 100.msecs));
|
||||||
remove(bar);
|
remove(bar);
|
||||||
|
assert(!watcher.readChanges(changes, 1500.msecs));
|
||||||
|
|
||||||
watcher = NativePath(dir).watchDirectory(Yes.recursive);
|
watcher = NativePath(dir).watchDirectory(Yes.recursive);
|
||||||
|
assert(!watcher.readChanges(changes, 1500.msecs));
|
||||||
write(foo, null);
|
write(foo, null);
|
||||||
sleep(sleepTime);
|
sleep(sleepTime);
|
||||||
write(foo, [0, 1]);
|
write(foo, [0, 1]);
|
||||||
|
|
186
tests/vibe.core.stream.pipe.d
Normal file
186
tests/vibe.core.stream.pipe.d
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
/+ dub.sdl:
|
||||||
|
name "test"
|
||||||
|
dependency "vibe-core" path=".."
|
||||||
|
+/
|
||||||
|
module test;
|
||||||
|
|
||||||
|
import vibe.core.core;
|
||||||
|
import vibe.core.stream;
|
||||||
|
import std.algorithm : min;
|
||||||
|
import std.array : Appender, appender;
|
||||||
|
import std.exception;
|
||||||
|
import std.random;
|
||||||
|
import core.time : Duration, msecs;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
auto datau = new uint[](2 * 1024 * 1024);
|
||||||
|
foreach (ref u; datau)
|
||||||
|
u = uniform!uint();
|
||||||
|
auto data = cast(ubyte[])datau;
|
||||||
|
|
||||||
|
test(data, 0, 0.msecs, 0, 0.msecs);
|
||||||
|
test(data, 32768, 1.msecs, 32768, 1.msecs);
|
||||||
|
|
||||||
|
test(data, 32768, 0.msecs, 0, 0.msecs);
|
||||||
|
test(data, 0, 0.msecs, 32768, 0.msecs);
|
||||||
|
test(data, 32768, 0.msecs, 32768, 0.msecs);
|
||||||
|
|
||||||
|
test(data, 1023*967, 10.msecs, 0, 0.msecs);
|
||||||
|
test(data, 0, 0.msecs, 1023*967, 20.msecs);
|
||||||
|
test(data, 1023*967, 10.msecs, 1023*967, 10.msecs);
|
||||||
|
|
||||||
|
test(data, 1023*967, 10.msecs, 32768, 0.msecs);
|
||||||
|
test(data, 32768, 0.msecs, 1023*967, 10.msecs);
|
||||||
|
|
||||||
|
test(data, 1023*967, 10.msecs, 65535, 0.msecs);
|
||||||
|
test(data, 65535, 0.msecs, 1023*967, 10.msecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test(ubyte[] data, ulong read_sleep_freq, Duration read_sleep,
|
||||||
|
ulong write_sleep_freq, Duration write_sleep)
|
||||||
|
{
|
||||||
|
test(data, ulong.max, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, data.length * 2, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, data.length / 2, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, 64 * 1024, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, 64 * 1024 - 57, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, 557, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test(ubyte[] data, ulong chunk_limit, ulong read_sleep_freq,
|
||||||
|
Duration read_sleep, ulong write_sleep_freq, Duration write_sleep)
|
||||||
|
{
|
||||||
|
test(data, ulong.max, chunk_limit, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, 8 * 1024 * 1024, chunk_limit, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, 8 * 1024 * 1024 - 37, chunk_limit, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
test(data, 37, chunk_limit, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test(ubyte[] data, ulong data_limit, ulong chunk_limit, ulong read_sleep_freq,
|
||||||
|
Duration read_sleep, ulong write_sleep_freq, Duration write_sleep)
|
||||||
|
{
|
||||||
|
import std.traits : EnumMembers;
|
||||||
|
foreach (m; EnumMembers!PipeMode)
|
||||||
|
test(data, m, data_limit, chunk_limit, read_sleep_freq, read_sleep, write_sleep_freq, write_sleep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test(ubyte[] data, PipeMode mode, ulong data_limit, ulong chunk_limit,
|
||||||
|
ulong read_sleep_freq, Duration read_sleep, ulong write_sleep_freq,
|
||||||
|
Duration write_sleep)
|
||||||
|
{
|
||||||
|
import vibe.core.log;
|
||||||
|
logInfo("test RF=%s RS=%sms WF=%s WS=%sms CL=%s DL=%s M=%s", read_sleep_freq, read_sleep.total!"msecs", write_sleep_freq, write_sleep.total!"msecs", chunk_limit, data_limit, mode);
|
||||||
|
|
||||||
|
auto input = TestInputStream(data, chunk_limit, read_sleep_freq, read_sleep);
|
||||||
|
auto output = TestOutputStream(write_sleep_freq, write_sleep);
|
||||||
|
auto datacmp = data[0 .. min(data.length, data_limit)];
|
||||||
|
|
||||||
|
input.pipe(output, data_limit, mode);
|
||||||
|
if (output.m_data.data != datacmp) {
|
||||||
|
logError("MISMATCH: %s b vs. %s b ([%(%s, %) ... %(%s, %)] vs. [%(%s, %) ... %(%s, %)])",
|
||||||
|
output.m_data.data.length, datacmp.length,
|
||||||
|
output.m_data.data[0 .. 6], output.m_data.data[$-6 .. $],
|
||||||
|
datacmp[0 .. 6], datacmp[$-6 .. $]);
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid leaking memory due to false pointers
|
||||||
|
output.freeData();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct TestInputStream {
|
||||||
|
private {
|
||||||
|
const(ubyte)[] m_data;
|
||||||
|
ulong m_chunkLimit = size_t.max;
|
||||||
|
ulong m_sleepFrequency = 0;
|
||||||
|
Duration m_sleepAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
this(const(ubyte)[] data, ulong chunk_limit, ulong sleep_frequency, Duration sleep_amount)
|
||||||
|
{
|
||||||
|
m_data = data;
|
||||||
|
m_chunkLimit = chunk_limit;
|
||||||
|
m_sleepFrequency = sleep_frequency;
|
||||||
|
m_sleepAmount = sleep_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@safe:
|
||||||
|
|
||||||
|
@property bool empty() @blocking { return m_data.length == 0; }
|
||||||
|
|
||||||
|
@property ulong leastSize() @blocking { return min(m_data.length, m_chunkLimit); }
|
||||||
|
|
||||||
|
@property bool dataAvailableForRead() { assert(false); }
|
||||||
|
|
||||||
|
const(ubyte)[] peek() { assert(false); } // currently not used by pipe()
|
||||||
|
|
||||||
|
size_t read(scope ubyte[] dst, IOMode mode)
|
||||||
|
@blocking {
|
||||||
|
assert(mode == IOMode.all || mode == IOMode.once);
|
||||||
|
if (mode == IOMode.once)
|
||||||
|
dst = dst[0 .. min($, m_chunkLimit)];
|
||||||
|
auto oldsleeps = m_sleepFrequency ? m_data.length / m_sleepFrequency : 0;
|
||||||
|
dst[] = m_data[0 .. dst.length];
|
||||||
|
m_data = m_data[dst.length .. $];
|
||||||
|
auto newsleeps = m_sleepFrequency ? m_data.length / m_sleepFrequency : 0;
|
||||||
|
if (oldsleeps != newsleeps) {
|
||||||
|
if (m_sleepAmount > 0.msecs)
|
||||||
|
sleep(m_sleepAmount * (oldsleeps - newsleeps));
|
||||||
|
else yield();
|
||||||
|
}
|
||||||
|
return dst.length;
|
||||||
|
}
|
||||||
|
void read(scope ubyte[] dst) @blocking { auto n = read(dst, IOMode.all); assert(n == dst.length); }
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin validateInputStream!TestInputStream;
|
||||||
|
|
||||||
|
struct TestOutputStream {
|
||||||
|
private {
|
||||||
|
Appender!(ubyte[]) m_data;
|
||||||
|
ulong m_sleepFrequency = 0;
|
||||||
|
Duration m_sleepAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
this(ulong sleep_frequency, Duration sleep_amount)
|
||||||
|
{
|
||||||
|
m_data = appender!(ubyte[]);
|
||||||
|
m_data.reserve(2*1024*1024);
|
||||||
|
m_sleepFrequency = sleep_frequency;
|
||||||
|
m_sleepAmount = sleep_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeData()
|
||||||
|
{
|
||||||
|
import core.memory : GC;
|
||||||
|
auto d = m_data.data;
|
||||||
|
m_data.clear();
|
||||||
|
GC.free(d.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@safe:
|
||||||
|
|
||||||
|
void finalize() @safe @blocking {}
|
||||||
|
void flush() @safe @blocking {}
|
||||||
|
|
||||||
|
size_t write(in ubyte[] bytes, IOMode mode)
|
||||||
|
@safe @blocking {
|
||||||
|
assert(mode == IOMode.all);
|
||||||
|
|
||||||
|
auto oldsleeps = m_sleepFrequency ? m_data.data.length / m_sleepFrequency : 0;
|
||||||
|
m_data.put(bytes);
|
||||||
|
auto newsleeps = m_sleepFrequency ? m_data.data.length / m_sleepFrequency : 0;
|
||||||
|
if (oldsleeps != newsleeps) {
|
||||||
|
if (m_sleepAmount > 0.msecs)
|
||||||
|
sleep(m_sleepAmount * (newsleeps - oldsleeps));
|
||||||
|
else yield();
|
||||||
|
}
|
||||||
|
return bytes.length;
|
||||||
|
}
|
||||||
|
void write(in ubyte[] bytes) @blocking { auto n = write(bytes, IOMode.all); assert(n == bytes.length); }
|
||||||
|
void write(in char[] bytes) @blocking { write(cast(const(ubyte)[])bytes); }
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin validateOutputStream!TestOutputStream;
|
Loading…
Reference in a new issue