Implement the async pipelines and fsMultiSync

This commit is contained in:
Zahary Karadjov 2020-05-04 02:47:01 +03:00
commit cb36a6d4db
No known key found for this signature in database
GPG key ID: C8936F8A3073D609
12 changed files with 920 additions and 299 deletions

View file

@ -16,19 +16,22 @@ export
type
OutputStream* = ref object of RootObj
vtable*: ptr OutputStreamVTable # This is nil for any memory output
buffers*: PageBuffers # This is nil for unsafe memory outputs
span: PageSpan
spanEndPos: Natural
vtable*: ptr OutputStreamVTable # This is nil for any memory output
buffers*: PageBuffers # This is nil for unsafe memory outputs
span*: PageSpan
spanEndPos*: Natural
extCursorsCount: int
closeFut: Future[void]
closeFut: Future[void] # This is nil before `close` is called
when debugHelpers:
name*: string
WriteCursor* = object
span: PageSpan
stream: OutputStream
LayeredOutputStream* = ref object of OutputStream
subStream*: OutputStream
destination*: OutputStream
allowWaitFor*: bool
OutputStreamHandle* = object
s*: OutputStream
@ -66,10 +69,11 @@ type
FileOutputStream = ref object of OutputStream
file: File
const
nimAllocatorMetadataSize* = 0
# TODO: Get this from Nim's allocator.
# The goal is to make perfect page-aligned allocations
template Async*(s: OutputStream): AsyncOutputStream =
AsyncOutputStream(s)
template Sync*(s: AsyncOutputStream): OutputStream =
OuputStream(s)
proc disconnectOutputDevice(s: OutputStream) =
if s.vtable != nil:
@ -82,9 +86,30 @@ proc disconnectOutputDevice(s: OutputStream) =
template disconnectOutputDevice(s: AsyncOutputStream) =
disconnectOutputDevice OutputStream(s)
template flushImpl(s: OutputStream, awaiter, writeOp, flushOp: untyped) =
doAssert s.extCursorsCount == 0
if s.vtable != nil:
if s.buffers != nil:
trackWrittenTo(s.buffers, s.span.startAddr)
awaiter s.vtable.writeOp(s, nil, 0)
if s.vtable.flushOp != nil:
awaiter s.vtable.flushOp(s)
proc flush*(s: OutputStream) =
flushImpl(s, noAwait, writeSync, flushSync)
template flush*(sp: AsyncOutputStream) =
let s = OutputStream sp
flushImpl(s, fsAwait, writeAsync, flushAsync)
proc flushAsync*(s: AsyncOutputStream) {.async.} =
flush s
proc close*(s: OutputStream,
behavior = dontWaitAsyncClose)
{.raises: [IOError, Defect].} =
flush s
disconnectOutputDevice(s)
if s.closeFut != nil:
fsTranslateErrors "Stream closing failed":
@ -93,11 +118,15 @@ proc close*(s: OutputStream,
else:
asyncCheck s.closeFut
proc close*(s: AsyncOutputStream): Future[void]
{.raises: [IOError, Defect].} =
template close*(sp: AsyncOutputStream) =
let s = OutputStream sp
flush(Async s)
disconnectOutputDevice(s)
result = OutputStream(s).closeFut
doAssert result != nil
if s.closeFut != nil:
await s.closeFut
proc closeAsync*(s: AsyncOutputStream) {.async.} =
close s
template closeNoWait*(sp: AsyncOutputStream|OutputStream) =
## Close the stream without waiting even if's async.
@ -135,8 +164,11 @@ template isExternalCursor(c: var WriteCursor): bool =
addr(c) != addr(c.stream.cursor)
proc addPage(s: OutputStream) =
s.span = s.buffers.addWritablePage().writableSpan
s.spanEndPos += s.span.len
let
nextPageSize = s.buffers.pageSize
nextPage = s.buffers.addWritablePage(nextPageSize)
s.span = nextPage.fullSpan
s.spanEndPos += nextPageSize
template makeHandle*(sp: OutputStream): OutputStreamHandle =
let s = sp
@ -144,6 +176,8 @@ template makeHandle*(sp: OutputStream): OutputStreamHandle =
proc memoryOutput*(pageSize = defaultPageSize): OutputStreamHandle =
doAssert pageSize > 0
# We are not creating an initial output page, because `ensureRunway`
# can determine the most appropriate size.
makeHandle OutputStream(buffers: initPageBuffers(pageSize))
proc unsafeMemoryOutput*(buffer: pointer, len: Natural): OutputStreamHandle =
@ -158,22 +192,18 @@ proc ensureRunway*(s: OutputStream, neededRunway: Natural) =
## hint specified at stream creation with `pageSize`.
let runway = s.span.len
# This is a temporary requirement.
# ensureRunway should be called immediately after creating the OutputStream
# In the future, we'll relax this by implementing more logic in buffers.nim
doAssert runway == 0, "call ensureRunway immediately after stream creation"
if neededRunway > runway:
# If you use an unsafe memory output, you must ensure that
# it will have a large enough size to hold the data you are
# feeding to it.
doAssert s.buffers != nil, "Unsafe memory output of insufficient size"
s.span = s.buffers.ensureRunway(neededRunway - runway)
s.buffers.ensureRunway(s.span, neededRunway)
s.spanEndPos += (s.span.len - runway)
template ensureRunway*(s: AsyncOutputStream, neededRunway: Natural) =
ensureRunway OutputStream(s, neededRunway)
ensureRunway OutputStream(s), neededRunway
let FileOutputVTable = OutputStreamVTable(
let fileOutputVTable = OutputStreamVTable(
writeSync: proc (s: OutputStream, src: pointer, srcLen: Natural)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
var file = FileOutputStream(s).file
@ -205,7 +235,7 @@ proc fileOutput*(filename: string,
let f = open(filename, fileMode)
makeHandle FileOutputStream(
vtable: vtableAddr FileOutputVTable,
vtable: vtableAddr fileOutputVTable,
buffers: initPageBuffers(pageSize),
file: f)
@ -215,6 +245,33 @@ proc pos*(s: OutputStream): int =
template pos*(s: AsyncOutputStream): int =
pos OutputStream(s)
proc getBuffers*(s: OutputStream): PageBuffers =
doAssert s.buffers != nil
s.buffers.trackWrittenTo s.span.startAddr
return s.buffers
proc recycleBuffers*(s: OutputStream, buffers: PageBuffers) =
if buffers != nil:
s.buffers = buffers
let len = buffers.queue.len
if len > 0:
if len > 1:
buffers.queue.shrink(fromLast = len - 1)
let bufferPage = buffers.queue[0]
bufferPage.writtenTo = 0
bufferPage.consumedTo = 0
s.span = bufferPage.fullSpan
s.spanEndPos = s.span.len
return
else:
s.buffers = initPageBuffers(defaultPageSize)
s.span = default(PageSpan)
s.spanEndPos = 0
#
# Pre-conditions for `drainAllBuffers(Sync/Async)`
# * The cursor has reached the current span end
@ -264,13 +321,13 @@ proc delayFixedSizeWrite*(s: OutputStream, size: Natural): WriteCursor =
runwayDeficit = size - runway
nextPageSize = nextAlignedSize(runwayDeficit, s.buffers.pageSize)
nextPage = s.buffers.addWritablePage(nextPageSize)
nextPageSpan = nextPage.writableSpan
nextPageSpan = nextPage.fullSpan
s.span = PageSpan(startAddr: offset(nextPageSpan.startAddr, runwayDeficit),
endAddr: nextPageSpan.endAddr)
# See the explanation about split cursors above
nextPage.startOffset = -runwayDeficit
nextPage.consumedTo = -runwayDeficit
s.spanEndPos += nextPageSize
@ -293,12 +350,14 @@ proc delayVarSizeWrite*(s: OutputStream, maxSize: Natural): VarSizeWriteCursor =
s.span.startAddr = endAddr
else:
s.buffers.endLastPageAt(s.span.startAddr)
let
nextPageSize = nextAlignedSize(maxSize, s.buffers.pageSize)
nextPageSpan = s.buffers.addWritablePage(nextPageSize).writableSpan
nextPage = s.buffers.addWritablePage(nextPageSize)
nextPageSpan = nextPage.fullSpan
cursorEndAddr = offset(nextPageSpan.startAddr, maxSize)
nextPage.consumedTo = -maxSize
result = VarSizeWriteCursor WriteCursor(
stream: s,
span: PageSpan(startAddr: nextPageSpan.startAddr,
@ -324,17 +383,17 @@ proc finalWrite*(c: var VarSizeWriteCursor, data: openArray[byte]) =
doAssert overestimatedBytes >= 0
for page in items(cursor.stream.buffers.queue):
let baseAddr = page.pageBaseAddr
if page.pageEndAddr == cursor.span.endAddr:
let baseAddr = page.allocationStart
if page.allocationEnd == cursor.span.endAddr:
# This is a page ending cursor
page.endOffset = distance(baseAddr, cursor.span.startAddr) + data.len
page.writtenTo = distance(baseAddr, cursor.span.startAddr) + data.len
copyMem(cursor.span.startAddr, unsafeAddr data[0], data.len)
finalize cursor
return
if cursor.span.startAddr == baseAddr:
# This is page starting cursor
page.startOffset = overestimatedBytes
page.consumedTo = overestimatedBytes
copyMem(offset(baseAddr, overestimatedBytes), unsafeAddr data[0], data.len)
finalize cursor
return
@ -351,10 +410,10 @@ proc tryMovingToNextPage(c: var WriteCursor) =
# page is big enough to hold all the data. When we created the cursor,
# we've taken a note regarding the number of bytes on the second page
# that are reserved by writing them as a negative value for the page
# `startOffset`.
# `consumedTo`.
#
# All we need to do here is update the cursor span to point to the next
# page and set the now final `endAddr`. The page `startOffset` is updated
# page and set the now final `endAddr`. The page `consumedTo` is updated
# to 0 to indicate that the cursor has made the flip.
#
# If you are wondering, var-sized cursors cannot be split, because our
@ -363,13 +422,13 @@ proc tryMovingToNextPage(c: var WriteCursor) =
# When we try to create a var-sized cursor, we check if there are enough
# bytes on the current page to contain the worst case scenario (the var
# sized cursor has an upper size limit). If there are enough bytes, we
# end the page prematurely (it will end up with an `endOffset`). We can
# end the page prematurely (it will end up with an `writtenTo`). We can
# then recycle the same memory for the next page that will use an adjusted
# `startOffset`. The `endOffset` of the first page will be written when
# `consumedTo`. The `writtenTo` of the first page will be written when
# the cursor is finalized and its final size becomes known.
#
# If there weren't enough bytes (a much more rare event), we allocate a
# new page. We adjust the `endOffset` of the current page to mark it's
# new page. We adjust the `writtenTo` of the current page to mark it's
# premature end and we mark the cursor as special by writing a
# The split cursor is definetely not on the last page, so we can iterate
@ -377,11 +436,11 @@ proc tryMovingToNextPage(c: var WriteCursor) =
var prevPage = c.stream.buffers.queue[0]
for i in 1 ..< c.stream.buffers.queue.len:
let page = c.stream.buffers.queue[i]
if c.span.endAddr == prevPage.pageEndAddr and page.startOffset < 0:
if c.span.endAddr == prevPage.allocationEnd and page.consumedTo < 0:
# We found what we need, so let's get to business:
c.span.startAddr = page.pageBaseAddr
c.span.endAddr = offset(c.span.startAddr, -page.startOffset)
page.startOffset = 0
c.span.startAddr = page.allocationStart
c.span.endAddr = offset(c.span.startAddr, -page.consumedTo)
page.consumedTo = 0
return
prevPage = page
@ -390,27 +449,8 @@ proc tryMovingToNextPage(c: var WriteCursor) =
# pre-allocated cursor span, which is considered a Defect (a range error)
doAssert false, "Attempt to write past the end of a cursor"
template flushImpl(s: OutputStream, awaiter, writeOp, flushOp: untyped) =
doAssert s.extCursorsCount == 0
if s.vtable != nil:
if s.buffers != nil:
s.buffers.endLastPageAt s.span.startAddr
awaiter s.vtable.writeOp(s, nil, 0)
s.span = s.buffers.getWritableSpan()
s.spanEndPos += s.span.len
if s.vtable.flushOp != nil:
awaiter s.vtable.flushOp(s)
proc flush*(s: OutputStream) =
flushImpl(s, noAwait, writeSync, flushSync)
template flush*(s: AsyncOutputStream) =
let s = sp
flushImpl(s, fsAwait, writeAsync, flushAsync)
template writeByteImpl(s: OutputStream, b: byte, awaiter, writeOp, drainOp: untyped) =
if s.span.atEnd:
if atEnd(s.span):
# Unsafe memory outputs don't use pages at all, so if our cursor
# reached here, this is a range violation defect:
doAssert canExtendOutput(s)
@ -424,12 +464,13 @@ template writeByteImpl(s: OutputStream, b: byte, awaiter, writeOp, drainOp: unty
elif s.buffers == nil:
awaiter s.vtable.writeOp(nil, unsafeAddr b, 1)
else:
trackWrittenToEnd(s.buffers)
awaiter drainOp(s, nil, 0)
writeByte(s.span, b)
proc write*(c: var WriteCursor, b: byte) =
if c.span.atEnd:
if atEnd(c.span):
# The cursor has reached the end of its buffer, but it may be a
# split cursor. If that's the case, the following function will
# succeed. If that's not a split cursor, we'll raise a Defect.
@ -440,16 +481,20 @@ proc write*(c: var WriteCursor, b: byte) =
proc write*(s: OutputStream, b: byte) =
writeByteImpl(s, b, noAwait, writeSync, drainAllBuffersSync)
template write*(s: AsyncOutputStream, b: byte) =
# TODO: I should do something with the write async Futures
bind write
write OutputStream(s)
proc write*(sp: AsyncOutputStream, b: byte) =
let s = OutputStream sp
if atEnd(s.span):
addPage(s)
writeByte(s.span, b)
template writeAndWait*(sp: AsyncOutputStream, b: byte) =
let s = sp
writeByteImpl(s, b, fsAwait, writeAsync, drainAllBuffersAsync)
template write*(s: OutputStream|AsyncOutputStream|var WriteCursor, x: char) =
template write*(s: AsyncOutputStream, x: char) =
write s, byte(x)
template write*(s: OutputStream|var WriteCursor, x: char) =
bind write
write s, byte(x)
@ -472,8 +517,8 @@ proc writeToANewPage(s: OutputStream, bytes: openArray[byte]) =
let nextPageSize = nextAlignedSize(inputLen, s.buffers.pageSize)
let nextPage = s.buffers.addWritablePage(nextPageSize)
s.span = nextPage.writableSpan
s.spanEndPos += s.span.len
s.span = nextPage.fullSpan
s.spanEndPos += nextPageSize
copyMem(s.span.startAddr, inputPos, inputLen)
s.span.startAddr = offset(s.span.startAddr, inputLen)
@ -496,7 +541,7 @@ template writeBytesImpl(s: OutputStream,
# We'll try to create them as large as possible:
s.writeToANewPage(bytes)
else:
s.buffers.endLastPageAt(s.span.startAddr)
trackWrittenTo(s.buffers, s.span.startAddr)
drainOp
proc write*(s: OutputStream, bytes: openArray[byte]) =
@ -506,7 +551,7 @@ proc write*(s: OutputStream, bytes: openArray[byte]) =
proc write*(s: OutputStream, chars: openArray[char]) =
write s, charsToBytes(chars)
proc write*(s: OutputStream, value: string) {.inline.} =
proc write*(s: OutputStream|AsyncOutputStream, value: string) {.inline.} =
write s, value.toOpenArrayByte(0, value.len - 1)
template memCopyToBytes(value: auto): untyped =
@ -519,30 +564,30 @@ proc writeMemCopy*(s: OutputStream, value: auto) =
bind write
write s, memCopyToBytes(value)
proc writeBytesAsyncImpl(sp: AsyncOutputStream,
proc writeBytesAsyncImpl(sp: OutputStream,
bytes: openarray[byte]): Future[void] =
let s = OutputStream(sp)
let s = sp
writeBytesImpl(s, bytes):
return s.vtable.writeAsync(s, unsafeAddr bytes[0], bytes.len)
proc writeBytesAsyncImpl(s: AsyncOutputStream,
proc writeBytesAsyncImpl(s: OutputStream,
chars: openarray[char]): Future[void] =
writeBytesAsyncImpl s, charsToBytes(chars)
proc writeBytesAsyncImpl(s: AsyncOutputStream,
proc writeBytesAsyncImpl(s: OutputStream,
str: string): Future[void] =
writeBytesAsyncImpl s, toOpenArray(str, 0, str.len - 1)
template writeAndWait*(sp: AsyncOutputStream, value: auto) =
template writeAndWait*(sp: AsyncOutputStream, value: untyped) =
bind writeBytesAsyncImpl
let
s = sp
s = OutputStream sp
f = writeBytesAsyncImpl(s, value)
if f != nil:
fsAwait(f)
s.span = s.buffers.getWritableSpan()
s.span = getWritableSpan s.buffers
s.spanEndPos += s.span.len
template writeMemCopyAndWait*(sp: AsyncOutputStream, value: auto) =
@ -601,9 +646,9 @@ template consumeOutputs*(sp: OutputStream, bytesVar, body: untyped) =
let s = sp
doAssert s.extCursorsCount == 0 and s.buffers != nil
for pageStartAddr, pageLen in consumePageBuffers(s.buffers):
for pageReadableStart, pageLen in consumePageBuffers(s.buffers):
template bytesVar: untyped =
makeOpenArray(pageStartAddr, pageLen)
makeOpenArray(pageReadableStart, pageLen)
body
@ -632,16 +677,16 @@ template consumeContiguousOutput*(sp: OutputStream, bytesVar, body: untyped) =
if s.buffers.queue.len == 1:
let page = s.buffers.queue[0]
bytesPtr = page.pageStartAddr
bytesLen = page.endOffset - pageStartOffset
bytesPtr = page.readableStart
bytesLen = page.writtenTo - page.consumedTo
# We need to reset the page to an empty state, so it can be reused
page.startOffset = 0
page.endOffset = 0
page.consumedTo = 0
page.writtenTo = 0
else:
contigiousBytes = newStringOfCap(s.pos)
for pageStartAddr, pageLen in consumePageBuffers(s.buffers):
contigiousBytes.add makeOpenArray(cast[ptr char](pageStartAddr), pageLen)
for pageReadableStart, pageLen in consumePageBuffers(s.buffers):
contigiousBytes.add makeOpenArray(cast[ptr char](pageReadableStart), pageLen)
bytesPtr = addr contigiousBytes[0]
bytesLen = contigiousBytes.len
@ -658,13 +703,13 @@ proc getOutput*(s: OutputStream, T: type string): string =
## Before consuming the output, all outstanding delayed writes must be finalized.
##
doAssert s.extCursorsCount == 0 and s.buffers != nil
s.buffers.endLastPageAt s.span.startAddr
s.buffers.trackWrittenTo s.span.startAddr
if s.buffers.queue.len == 1:
let page = s.buffers.queue[0]
if page.startOffset == 0:
if page.consumedTo == 0:
result.swap page.data[]
result.setLen page.endOffset
result.setLen page.writtenTo
# We clear the buffers, so the stream will be in pristine state.
# The next write is going to create a fresh new starting page.
s.buffers.queue.clear()
@ -680,3 +725,9 @@ template getOutput*(s: OutputStream, T: type seq[byte]): seq[byte] =
template getOutput*(s: OutputStream): seq[byte] =
cast[seq[byte]](s.getOutput(string))
template getOutput*(s: AsyncOutputStream): seq[byte] =
getOutput OutputStream(s)
template getOutput*(s: AsyncOutputStream, T: type): untyped =
getOutput OutputStream(s), T