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

@ -6,6 +6,8 @@ type
waitAsyncClose
dontWaitAsyncClose
const debugHelpers* = defined(debugHelpers)
when faststreams_async_backend == "chronos":
import
chronos

View file

@ -3,93 +3,129 @@ import
stew/[ptrops, ranges/ptr_arith],
async_backend
export
deques
type
PageSpan* = object
startAddr*, endAddr*: ptr byte
Page* = object
startOffset*: Natural
endOffset*: Natural
consumedTo*: Natural
writtenTo*: Natural
data*: ref string
PageRef* = ref Page
PageBuffers* = ref object
pageSize*: Natural
maxWriteSize*: Natural
backPressureLimit*: Natural
maxBufferedBytes*: Natural
queue*: Deque[PageRef]
getters: seq[Future[void]]
putters: seq[Future[void]]
waitingReader*: Future[void]
waitingWriter*: Future[void]
eofReached: bool
totalBytesRead*: Natural
totalBytesWritten*: Natural
eofReached*: bool
const
nimPageSize* = 4096
pageMetadataSize* = offsetof(Page, data)
nimAllocatorMetadataSize* = 32
# TODO: Get this legally from the Nim allocator.
# The goal is to make perfect page-aligned allocations
# that get fast O(0) treatment.
defaultPageSize* = 4096 - (pageMetadataSize + nimAllocatorMetadataSize)
# that go through a fast O(0) path in the allocator.
defaultPageSize* = 4096 - nimAllocatorMetadataSize
maxStackUsage* = 16384
proc openArrayToPair*(a: var openarray[byte]): (ptr byte, Natural) =
when debugHelpers:
proc describeBuffers*(context: static string, buffers: PageBuffers) =
debugEcho context, " :: buffers"
for page in buffers.queue:
debugEcho " page ", page.data[][page.consumedTo ..<
min(page.consumedTo + 16, page.writtenTo)]
debugEcho " len = ", page.data[].len
debugEcho " start = ", page.consumedTo
debugEcho " written to = ", page.writtenTo
func contents*(buffers: PageBuffers): string =
for page in buffers.queue:
result.add page.data[][page.consumedTo ..< page.writtenTo - 1]
else:
template describeBuffers*(context: static string, buffers: PageBuffers) =
discard
func openArrayToPair*(a: var openarray[byte]): (ptr byte, Natural) =
(addr a[0], Natural(a.len))
template pageBaseAddr*(page: PageRef): ptr byte =
template allocationStart*(page: PageRef): ptr byte =
cast[ptr byte](addr page.data[][0])
func pageStartAddr*(page: PageRef): ptr byte =
offset(cast[ptr byte](addr page.data[][0]), page.startOffset)
func readableStart*(page: PageRef): ptr byte =
offset(cast[ptr byte](addr page.data[][0]), page.consumedTo)
func pageEndAddr*(page: PageRef): ptr byte =
offset(cast[ptr byte](addr page.data[][0]), page.endOffset)
func readableEnd*(page: PageRef): ptr byte =
offset(cast[ptr byte](addr page.data[][0]), page.writtenTo)
template writableStart*(page: PageRef): ptr byte =
readableEnd(page)
func allocationEnd*(page: PageRef): ptr byte =
offset(cast[ptr byte](addr page.data[][0]), page.data[].len)
func pageLen*(page: PageRef): Natural =
page.endOffset - page.startOffset
page.writtenTo - page.consumedTo
template pageChars*(page: PageRef): untyped =
let baseAddr = cast[ptr UncheckedArray[char]](pageBaseAddr(page))
toOpenArray(baseAddr, page.startOffset, page.endOffset - 1)
let baseAddr = cast[ptr UncheckedArray[char]](allocationStart(page))
toOpenArray(baseAddr, page.consumedTo, page.writtenTo - 1)
func span*(page: PageRef, writable: static[bool] = false): PageSpan =
let baseAddr = page.pageBaseAddr
PageSpan(startAddr: offset(baseAddr, page.startOffset),
endAddr: offset(baseAddr, when writable: page.data[].len
else: page.endOffset))
func obtainReadableSpan*(page: PageRef, writable: static[bool] = false): PageSpan =
let baseAddr = page.allocationStart
result = PageSpan(startAddr: offset(baseAddr, page.consumedTo),
endAddr: offset(baseAddr, page.writtenTo))
page.consumedTo = page.writtenTo
template writableSpan*(page: PageRef): PageSpan =
span(page, writable = true)
func writableSpan*(page: PageRef): PageSpan =
let baseAddr = allocationStart(page)
PageSpan(startAddr: offset(baseAddr, page.writtenTo),
endAddr: offset(baseAddr, page.data[].len))
func fullSpan*(page: PageRef): PageSpan =
let baseAddr = page.allocationStart
PageSpan(startAddr: baseAddr, endAddr: offset(baseAddr, page.data[].len))
func initPageBuffers*(pageSize: Natural,
maxWriteSize = high(int)): PageBuffers =
maxBufferedBytes: Natural = 0): PageBuffers =
# TODO: remove the unbuferred streams
if pageSize > 0:
return PageBuffers(pageSize: pageSize,
maxWriteSize: maxWriteSize)
maxBufferedBytes: maxBufferedBytes)
template allocRef[T: not ref](x: T): ref T =
let res = new type(x)
res[] = x
res
func trackWrittenToEnd*(buffers: PageBuffers) =
if buffers.queue.len > 0:
let page = buffers.queue.peekLast
page.writtenTo = page.data[].len
func trackWrittenTo*(buffers: PageBuffers, spanHeadPos: ptr byte) =
if buffers != nil and buffers.queue.len > 0:
var topPage = buffers.queue.peekLast
topPage.writtenTo = distance(topPage.allocationStart, spanHeadPos)
func addWritablePage*(buffers: PageBuffers, pageSize: Natural): PageRef =
result = PageRef(data: allocRef newString(pageSize),
endOffset: pageSize)
trackWrittenToEnd(buffers)
result = PageRef(data: allocRef newString(pageSize))
buffers.queue.addLast result
func getWritablePage*(buffers: PageBuffers,
preferredSize: Natural): PageRef =
if buffers.queue.len == 1:
let recycledPage = buffers.queue.peekLast
if recycledPage.endOffset == 0 and
recycledPage.data[].len == preferredSize:
recycledPage.endOffset = recycledPage.data[].len
return recycledPage
if buffers.queue.len > 0:
let lastPage = buffers.queue.peekLast
if lastPage.writtenTo < lastPage.data[].len:
return lastPage
return addWritablePage(buffers, preferredSize)
@ -97,26 +133,120 @@ func addWritablePage*(buffers: PageBuffers): PageRef =
buffers.addWritablePage(buffers.pageSize)
template getWritableSpan*(buffers: PageBuffers): PageSpan =
getWritablePage(buffers, buffers.pageSize).span(writable = true)
let page = getWritablePage(buffers, buffers.pageSize)
writableSpan(page)
proc getReadableSpan*(buffers: PageBuffers): PageSpan =
if buffers.queue.len > 1:
func nextReadableSpan*(buffers: PageBuffers, span: var PageSpan) =
let
firstPage = buffers.queue.peekFirst
pageReadableEnd = firstPage.readableEnd
if span.endAddr == nil:
doAssert buffers.queue.len > 0
span = obtainReadableSpan buffers.queue[0]
elif span.endAddr != pageReadableEnd:
# Check whether the span points within the current page:
doAssert distance(firstPage.allocationStart, span.endAddr) >= 0 and
distance(span.endAddr, pageReadableEnd) >= 0
span.endAddr = pageReadableEnd
firstPage.consumedTo = firstPage.writtenTo
else:
doAssert buffers.queue.len > 1
discard buffers.queue.popFirst
span = obtainReadableSpan buffers.queue[0]
buffers.queue[0].span
func stringFromBytes(src: pointer, srcLen: Natural): string =
result = newString(srcLen)
copyMem(addr result[0], src, srcLen)
func ensureRunway*(buffers: PageBuffers, neededRunway: Natural): PageSpan =
doAssert buffers.queue.len == 0
buffers.pageSize = neededRunway
getWritableSpan(buffers)
func nextAlignedSize*(minSize, pageSize: Natural): Natural =
# TODO: This is not perfectly accurate. Revisit later
((minSize div pageSize) + 1) * pageSize
func appendUnbufferedWrite*(buffers: PageBuffers,
src: pointer, srcLen: Natural) =
if buffers.queue.len == 0:
buffers.queue.addLast PageRef(
data: allocRef stringFromBytes(src, srcLen),
writtenTo: srcLen)
else:
var
src = src
srcLen = srcLen
lastPage = buffers.queue.peekLast
lastPageLen = lastPage.data[].len
unusedBytes = lastPageLen - lastPage.writtenTo
if unusedBytes > 0:
let unusedBytesStart = offset(addr lastPage.data[0], lastPage.writtenTo)
if unusedBytes >= srcLen:
copyMem(unusedBytesStart, src, srcLen)
lastPage.writtenTo += srcLen
return
else:
copyMem(unusedBytesStart, src, unusedBytes)
lastPage.writtenTo = lastPageLen
src = offset(src, unusedBytes)
srcLen -= unusedBytes
let nextPageSize = nextAlignedSize(srcLen, buffers.pageSize)
let nextPage = buffers.addWritablePage(nextPageSize)
copyMem(addr nextPage.data[0], src, srcLen)
nextPage.writtenTo = srcLen
template hasDelayedWritesAtPageStart(page: PageRef): bool =
page.consumedTo < 0
func ensureRunway*(buffers: PageBuffers,
currentHeadPos: var PageSpan,
neededRunway: Natural) =
if currentHeadPos.startAddr == nil:
# This is a brand new stream, just like we recomend.
let page = buffers.addWritablePage(neededRunway)
currentHeadPos = page.fullSpan
else:
# This is a more complicated path that should almost never
# trigger in practice in a typically implemented code that
# calls `ensureRunway` at the beggining of a transformation.
doAssert buffers.queue.len > 0
let currPage = buffers.queue.peekLast
if currPage.hasDelayedWritesAtPageStart:
# There is not much we can do here. The outstanding cursors
# may point to the current page. We won't honor the runway
# request.
return
let
oldData = currPage.data
bytesWrittenToCurrPage = distance(currPage.readableStart,
currentHeadPos.startAddr)
replacementPageSize = neededRunway + bytesWrittenToCurrPage
currPage.data = allocRef newString(replacementPageSize)
currPage.consumedTo = bytesWrittenToCurrPage
currPage.writtenTo = bytesWrittenToCurrPage
# We copy the old data over the new page
copyMem(addr currPage.data[][0], addr oldData[][0],
bytesWrittenToCurrPage)
currentHeadPos = currPage.writableSpan
template len*(buffers: PageBuffers): Natural =
buffers.queue.len
func totalBufferredBytes*(buffers: PageBuffers): Natural =
for i in 1 ..< buffers.queue.len:
func totalBufferedBytes*(buffers: PageBuffers): Natural =
for i in 0 ..< buffers.queue.len:
result += buffers.queue[i].pageLen
func canAcceptWrite*(buffers: PageBuffers, writeSize: Natural): bool =
true or # TODO Remove this line
buffers.queue.len == 0 or
buffers.maxBufferedBytes == 0 or
buffers.totalBufferedBytes < buffers.maxBufferedBytes
template popFirst*(buffers: PageBuffers): PageRef =
buffers.queue.popFirst
@ -126,24 +256,15 @@ template `[]`*(buffers: PageBuffers, idx: Natural): PageRef =
func splitLastPageAt*(buffers: PageBuffers, address: ptr byte) =
var
topPage = buffers.queue.peekLast
newPage = PageRef()
splitPosition = distance(topPage.pageBaseAddr, address)
newPage[] = topPage[]
topPage.endOffset = splitPosition
newPage.startOffset = splitPosition
splitPosition = distance(topPage.allocationStart, address)
newPage = PageRef(
data: topPage.data,
consumedTo: splitPosition,
writtenTo: splitPosition)
topPage.writtenTo = splitPosition
buffers.queue.addLast newPage
func endLastPageAt*(buffers: PageBuffers, address: ptr byte) =
if buffers != nil and buffers.queue.len > 0:
var topPage = buffers.queue.peekLast
topPage.endOffset = distance(topPage.pageBaseAddr, address)
func nextAlignedSize*(minSize, pageSize: Natural): Natural =
# TODO: This is not perfectly accurate. Revisit later
((minSize div pageSize) + 1) * pageSize
iterator consumePages*(buffers: PageBuffers): PageRef =
doAssert buffers != nil
@ -161,17 +282,14 @@ iterator consumePages*(buffers: PageBuffers): PageRef =
discard buffers.queue.popFirst
if recycledPage != nil:
recycledPage.startOffset = 0
recycledPage.endOffset = 0
recycledPage.consumedTo = 0
recycledPage.writtenTo = 0
buffers.queue.addLast recycledPage
iterator consumePageBuffers*(buffers: PageBuffers): (ptr byte, Natural) =
for page in consumePages(buffers):
yield (page.pageStartAddr,
Natural(page.endOffset - page.startOffset))
template wasEofReached*(buffers: PageBuffers): bool =
buffers.eofReached
yield (page.readableStart,
Natural(page.writtenTo - page.consumedTo))
# BEWARE! These templates violate the double evaluation
# safety measures in order to produce better inlined
@ -217,15 +335,11 @@ template implementWrites*(buffersParam: PageBuffers,
let bytesWritten = writeBlock
# TODO: Can we repair the buffers here?
if bytesWritten != writeLenVar: raiseError()
buffers.totalBytesWritten += bytesWritten
if srcLen > 0:
doAssert src != nil
let bytesWritten = writeBlock
if bytesWritten != writeLenVar: raiseError()
# TODO: Fix this after removing the unbuffered streams
if buffers != nil:
buffers.totalBytesWritten += bytesWritten
type
ReadFlag* = enum
@ -253,19 +367,17 @@ template implementSingleRead*(buffersParam: PageBuffers,
bestPageSize = nextAlignedSize(readLenVar, buffers.pageSize)
page = getWritablePage(buffers, bestPageSize)
readStartVar = page.pageStartAddr
readLenVar = page.endOffset - page.startOffset
readStartVar = writableStart(page)
readLenVar = page.data[].len - page.writtenTo
# TODO: what if we exit with an exception here?
# Are the side-effects of `getWritablePage` above OK to keep?
bytesRead = readBlock
page.endOffset = page.startOffset + bytesRead
page.writtenTo += bytesRead
if (bytesRead == 0 and zeroReadIsNotEof notin flags) or
(partialReadIsEof in flags and bytesRead < readLenVar):
buffers.eofReached = true
else:
buffers.totalBytesRead += bytesRead
bytesRead

View file

@ -20,26 +20,28 @@ const
closingErrMsg = "Failed to close Chronos transport"
writeIncompleteErrMsg = "Failed to write all bytes to Chronos transport"
proc chronosCloseWait(t: StreamTransport) {.async, raises: [Defect, IOError].} =
proc chronosCloseWait(t: StreamTransport)
{.async, raises: [Defect, IOError].} =
fsTranslateErrors closingErrMsg:
await t.closeWait()
proc chronosReadOnce(s: ChronosInputStream, dst: pointer, dstLen: Natural): Future[Natural]
{.raises: [IOError, Defect], async.} =
proc chronosReadOnce(s: ChronosInputStream,
dst: pointer, dstLen: Natural): Future[Natural]
{.async, raises: [IOError, Defect].} =
fsTranslateErrors readingErrMsg:
return implementSingleRead(s.buffers, dst, dstLen, {},
readStartAddr, readLen):
await s.transport.readOnce(readStartAddr, readLen)
proc chronosWrites(s: ChronosOutputStream, src: pointer, srcLen: Natural)
{.raises: [IOError, Defect], async.} =
{.async, raises: [IOError, Defect].} =
fsTranslateErrors writeIncompleteErrMsg:
implementWrites(s.buffers, src, srcLen, "StreamTransport"
writeStartAddr, writeLen):
await s.transport.write(writeStartAddr, writeLen)
# TODO: Use the Raising type here
let ChronosInputStreamVTable = InputStreamVTable(
let chronosInputVTable = InputStreamVTable(
readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural
{.nimcall, gcsafe, raises: [IOError, Defect].} =
var cs = ChronosInputStream(s)
@ -61,14 +63,14 @@ let ChronosInputStreamVTable = InputStreamVTable(
)
func chronosInput*(s: StreamTransport,
pageSize = buffers.defaultPageSize,
pageSize = defaultPageSize,
allowWaitFor = false): InputStreamHandle =
InputStreamHandle(s: ChronosInputStream(
vtable: vtableAddr ChronosInputStreamVTable,
pageSize: pageSize,
allowWaitFor: allowWaitFor))
makeHandle ChronosInputStream(
vtable: vtableAddr chronosInputVTable,
buffers: initPageBuffers(pageSize),
allowWaitFor: allowWaitFor)
let ChronosOutputStreamVTable = OutputStreamVTable(
let chronosOutputVTable = OutputStreamVTable(
writeSync: proc (s: OutputStream, src: pointer, srcLen: Natural)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
var cs = ChronosOutputStream(s)
@ -90,17 +92,11 @@ let ChronosOutputStreamVTable = OutputStreamVTable(
)
func chronosOutput*(s: StreamTransport,
pageSize = buffers.defaultPageSize,
pageSize = defaultPageSize,
allowWaitFor = false): OutputStreamHandle =
var stream = ChronosOutputStream(
vtable: vtableAddr(SnappyStreamVTable),
pageSize: pageSize,
minWriteSize: 1,
maxWriteSize: high(int),
makeHandle ChronosOutputStream(
vtable: vtableAddr(chronosOuputVTable),
buffers: initPageBuffers(pageSize)
transport: s,
allowWaitFor: allowWaitFor)
stream.initWithSinglePage()
OutputStreamHandle(s: stream)

View file

@ -8,14 +8,17 @@ export
type
InputStream* = ref object of RootObj
vtable: ptr InputStreamVTable # This is nil for unsafe memory inputs
buffers: PageBuffers # This is nil for unsafe memory inputs
span: PageSpan
spanEndPos: Natural
closeFut: Future[void] # This is nil before `close` is called
vtable*: ptr InputStreamVTable # This is nil for unsafe memory inputs
buffers*: PageBuffers # This is nil for unsafe memory inputs
span*: PageSpan
spanEndPos*: Natural
closeFut: Future[void] # This is nil before `close` is called
when debugHelpers:
name*: string
LayeredInputStream* = ref object of InputStream
subStream*: InputStream
source*: InputStream
allowWaitFor*: bool
InputStreamHandle* = object
s*: InputStream
@ -34,7 +37,7 @@ type
CloseAsyncProc* = proc (s: InputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
GetLenSyncProc* = proc (s: InputStream): Natural
GetLenSyncProc* = proc (s: InputStream): Option[Natural]
{.nimcall, gcsafe, raises: [IOError, Defect].}
InputStreamVTable* = object
@ -50,6 +53,12 @@ type
FileInputStream = ref object of InputStream
file: File
template Async*(s: InputStream): AsyncInputStream =
AsyncInputStream(s)
template Sync*(s: AsyncInputStream): InputStream =
InputStream(s)
proc disconnectInputDevice(s: InputStream) =
# TODO
# Document the behavior that closeAsync is preferred
@ -92,14 +101,17 @@ proc close*(s: InputStream,
else:
asyncCheck s.closeFut
proc close*(s: AsyncInputStream): Future[void]
{.raises: [IOError, Defect].} =
template close*(sp: AsyncInputStream) =
## Starts the asychronous closing of the stream and returns a future that
## tracks the closing operation.
s.disconnectInputDevice()
s.preventFurtherReading()
result = InputStream(s).closeFut
doAssert result != nil
let s = InputStream sp
disconnectInputDevice(s)
preventFurtherReading(s)
if s.closeFut != nil:
await s.closeFut
proc closeAsync*(s: AsyncInputStream) {.async.} =
close s
template closeNoWait*(sp: AsyncInputStream|InputStream) =
## Close the stream without waiting even if's async.
@ -143,7 +155,7 @@ template vtableAddr*(vtable: InputStreamVTable): ptr InputStreamVTable =
{.noSideEffect.}:
unsafeAddr vtable
let MemFileInputVTable = InputStreamVTable(
let memFileInputVTable = InputStreamVTable(
closeSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
try:
@ -151,9 +163,9 @@ let MemFileInputVTable = InputStreamVTable(
except OSError as err:
raise newException(IOError, "Failed to close file", err)
,
getLenSync: proc (s: InputStream): Natural
getLenSync: proc (s: InputStream): Option[Natural]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
s.span.len
some s.span.len
)
proc memFileInput*(filename: string, mappedSize = -1, offset = 0): InputStreamHandle
@ -191,7 +203,7 @@ proc memFileInput*(filename: string, mappedSize = -1, offset = 0): InputStreamHa
mappedSize = memFile.size
makeHandle MemFileInputStream(
vtable: vtableAddr MemFileInputVTable,
vtable: vtableAddr memFileInputVTable,
span: PageSpan(
startAddr: head,
endAddr: offset(head, mappedSize)),
@ -203,19 +215,37 @@ proc readableNow*(s: InputStream): bool =
template readableNow*(s: AsyncInputStream): bool =
readableNow InputStream(s)
func flipPage(s: InputStream) =
doAssert s.buffers.len > 1
discard s.buffers.popFirst
s.span = obtainReadableSpan s.buffers[0]
s.spanEndPos += s.span.len
func getBestContiguousRunway(s: InputStream): Natural =
result = s.span.len
if result == 0:
if s.buffers != nil and s.buffers.len > 1:
flipPage s
result = s.span.len
func totalUnconsumedBytes*(s: InputStream): Natural =
## Returns the number of bytes that are currently sitting within the stream
## buffers and that can be consumed with `read` or `advance`.
result = s.span.len
if s.buffers != nil:
result += s.buffers.totalBufferredBytes
# result += s.buffers.totalBytesRead - s.spanEndPos
let
localRunway = s.span.len
runwayInBuffers = if s.buffers == nil: 0
else: s.buffers.totalBufferedBytes
if localRunway == 0 and runwayInBuffers > 0:
flipPage s
localRunway + runwayInBuffers
template totalUnconsumedBytes*(s: AsyncInputStream): Natural =
## Alias for InputStream.totalUnconsumedBytes
totalUnconsumedBytes InputStream(s)
let FileInputVTable = InputStreamVTable(
let fileInputVTable = InputStreamVTable(
readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural
{.nimcall, gcsafe, raises: [IOError, Defect].} =
let file = FileInputStream(s).file
@ -224,7 +254,7 @@ let FileInputVTable = InputStreamVTable(
readStartAddr, readLen):
file.readBuffer(readStartAddr, readLen)
,
getLenSync: proc (s: InputStream): Natural
getLenSync: proc (s: InputStream): Option[Natural]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
let
s = FileInputStream(s)
@ -235,7 +265,7 @@ let FileInputVTable = InputStreamVTable(
let endPos = getFilePos(s.file)
setFilePos(s.file, preservedPos)
endPos - preservedPos + runway
some Natural(endPos - preservedPos + runway)
,
closeSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
@ -266,7 +296,7 @@ proc fileInput*(filename: string,
setFilePos(file, offset)
makeHandle FileInputStream(
vtable: vtableAddr FileInputVTable,
vtable: vtableAddr fileInputVTable,
buffers: initPageBuffers(pageSize),
file: file)
@ -284,20 +314,44 @@ proc unsafeMemoryInput*(str: string): InputStreamHandle =
proc len*(s: InputStream): Option[Natural] {.raises: [Defect, IOError].} =
if s.vtable == nil:
some s.span.len
some s.totalUnconsumedBytes
elif s.vtable.getLenSync != nil:
some s.vtable.getLenSync(s)
s.vtable.getLenSync(s)
else:
none Natural
template len*(s: AsyncInputStream): int =
template len*(s: AsyncInputStream): Option[Natural] =
len InputStream(s)
proc flipPage(s: InputStream) =
doAssert s.buffers.len > 1
discard s.buffers.popFirst
s.span = s.buffers[0].span
s.spanEndPos += s.span.len
func memoryInput*(buffers: PageBuffers): InputStreamHandle =
var span = if buffers.len == 0: default(PageSpan)
else: obtainReadableSpan buffers.queue[0]
makeHandle InputStream(buffers: buffers,
span: span,
spanEndPos: span.len)
func memoryInput*(data: openarray[byte]): InputStreamHandle =
let
buffers = initPageBuffers(data.len)
page = buffers.addWritablePage(data.len)
pageSpan = page.fullSpan
copyMem(pageSpan.startAddr, unsafeAddr data[0], data.len)
makeHandle InputStream(buffers: buffers,
span: pageSpan,
spanEndPos: data.len)
func memoryInput*(data: openarray[char]): InputStreamHandle =
memoryInput charsToBytes(data)
proc resetBuffers*(s: InputStream, buffers: PageBuffers) =
# This should be used only on safe memory input streams
doAssert s.vtable == nil and s.buffers != nil and buffers.len > 0
s.buffers = buffers
s.span = obtainReadableSpan buffers.queue[0]
s.spanEndPos = s.span.len
proc continueAfterRead(s: InputStream, bytesRead: Natural): bool =
# Please note that this is extracted into a proc only to reduce the code
@ -308,11 +362,11 @@ proc continueAfterRead(s: InputStream, bytesRead: Natural): bool =
# The read might have been incomplete which signals the EOF of the stream.
# If this is the case, we disconnect the input device which prevents any
# further attempts to read from it:
if wasEofReached(s.buffers):
if s.buffers.eofReached:
disconnectInputDevice(s)
if bytesRead > 0:
s.span = s.buffers.getReadableSpan()
s.buffers.nextReadableSpan(s.span)
s.spanEndPos += s.span.len
return true
else:
@ -395,21 +449,20 @@ template readable*(sp: AsyncInputStream): bool =
## Async version of `readable`.
## The intended API usage is the same. Instead of blocking, an async
## stream will use `await` while waiting for more data.
let s = sp
let s = InputStream sp
if hasRunway(s.span):
true
else:
bufferMoreDataImpl(s, fsAsync, readAsync)
bufferMoreDataImpl(s, fsAwait, readAsync)
func continueAfterReadN(s: InputStream,
runwayBeforeRead, bytesRead: Natural) =
if runwayBeforeRead == 0 and bytesRead > 0:
s.span = s.buffers.getReadableSpan()
s.buffers.nextReadableSpan(s.span)
s.spanEndPos += s.span.len
template readableNImpl(s, n, awaiter, readOp: untyped): bool =
let runway = totalUnconsumedBytes(s)
if runway >= n:
true
elif s.buffers == nil or s.vtable == nil or s.vtable.readOp == nil:
@ -423,7 +476,7 @@ template readableNImpl(s, n, awaiter, readOp: untyped): bool =
while true:
bytesRead += awaiter s.vtable.readOp(s, nil, bytesDeficit)
if wasEofReached(s.buffers):
if s.buffers.eofReached:
disconnectInputDevice(s)
res = bytesRead >= bytesDeficit
break
@ -472,7 +525,7 @@ template readable*(sp: AsyncInputStream, np: int): bool =
## The intended API usage is the same. Instead of blocking, an async
## stream will use `await` while waiting for more data.
let
s = sp
s = InputStream sp
n = np
readableNImpl(s, n, fsAwait, readAsync)
@ -491,7 +544,7 @@ proc peekAt*(s: InputStream, pos: int): byte {.inline.} =
return peekHead[]
template peekAt*(s: AsyncInputStream, pos: int): byte =
peekAt InputStream(s)
peekAt InputStream(s), pos
proc advance*(s: InputStream) =
if hasRunway(s.span):
@ -509,7 +562,7 @@ proc read*(s: InputStream): byte =
template read*(s: AsyncInputStream): byte =
read InputStream(s)
proc drainBuffersInto(s: InputStream, dstAddr: ptr byte, dstLen: Natural): Natural =
proc drainBuffersInto*(s: InputStream, dstAddr: ptr byte, dstLen: Natural): Natural =
var
dst = dstAddr
remainingBytes = dstLen
@ -527,25 +580,36 @@ proc drainBuffersInto(s: InputStream, dstAddr: ptr byte, dstLen: Natural): Natur
if s.buffers != nil:
# Since we reached the end of the current page,
# we have to do the equivalent of `flipPage`:
# TODO: what if the page was extended?
if s.buffers.len > 0:
discard s.buffers.popFirst()
for page in consumePages(s.buffers):
let
pageStart = page.pageStartAddr
pageLen = page.endOffset - page.startOffset
pageStart = page.readableStart
pageLen = page.writtenTo - page.consumedTo
# There are two possible scenarios ahead:
# 1) We'll either stop at this page in which case our span will
# point to the end of the page (so, it's fully consumed)
# 2) We are going to copy the entire page to the destination
# buffer and we'll continue (so, it's fully consumed again)
page.consumedTo = page.writtenTo
if pageLen > remainingBytes:
# This page has enough data to fill the rest of the buffer:
copyMem(dst, pageStart, remainingBytes)
page.startOffset += remainingBytes
# This page is partially consumed now and we must set our
# span to point to its remaining contents. We also need to
# know how much our position in the stream has advanced:
let bytesDrainedFromBufers = dstLen - runway
s.span = page.span
s.spanEndPos += bytesDrainedFromBufers + s.span.len
# span to point to its remaining contents:
s.span = PageSpan(startAddr: offset(pageStart, remainingBytes),
endAddr: page.readableEnd)
# We also need to know how much our position in the stream
# has advanced:
let bytesDrainedFromBuffers = dstLen - runway
s.spanEndPos += bytesDrainedFromBuffers + s.span.len
# We return the length of the buffer, which means that is
# has been fully populated:
@ -575,7 +639,7 @@ template readIntoExImpl(s: InputStream,
bytesRead += awaiter s.vtable.readOp(s, adjustedDst, bytesDeficit)
if wasEofReached(s.buffers):
if s.buffers.eofReached:
disconnectInputDevice(s)
break
@ -630,13 +694,6 @@ proc readOnce*(sp: AsyncInputStream): Future[Natural] =
doAssert s.buffers != nil and s.vtable != nil
s.vtable.readAsync(s, nil, 0)
proc getBestRunway(s: InputStream): Natural =
result = s.span.len
if result == 0:
if s.buffers != nil and s.buffers.len > 1:
s.flipPage
result = s.span.len
when defined(windows):
proc alloca(n: int): ptr byte {.importc, header: "<malloc.h>".}
else:
@ -655,7 +712,7 @@ template readNImpl(sp: InputStream,
let
s = sp
n = np
runway = getBestRunway(s)
runway = getBestContiguousRunway(s)
# Since Nim currently doesn't allow the `makeOpenArray` calls bellow
# to appear in different branches of an if statement, the code must

View file

@ -21,12 +21,12 @@ macro fsMultiSync*(body: untyped) =
for i in 1 ..< asyncProcParams.len:
let paramsDef = asyncProcParams[i]
let typ = paramsDef[^2]
if sameType(typ, bindSym"InputStream"):
if eqIdent(typ, "InputStream"):
paramsDef[^2] = bindSym "AsyncInputStream"
elif sameType(typ, bindSym"OutputStream"):
elif eqIdent(typ, "OutputStream"):
paramsDef[^2] = bindSym "AsyncOutputStream"
result = newStmtList(body, asyncProcBody)
if defined(debugSupportAsync):
when defined(debugSupportAsync):
echo result.repr

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

View file

@ -1,28 +1,340 @@
import
macros,
inputs, outputs
inputs, outputs, buffers, async_backend
export
inputs, outputs
inputs, outputs, async_backend
macro executePipeline*(start: InputStream, steps: varargs[untyped]) =
var input = start
result = newStmtList()
template clearAndWait(ep: AsyncEvent) =
let e = ep
clear e
await e.wait()
for i in 0 .. steps.len - 2:
type
FsAsyncPipe* = ref object
# TODO: Make these stream handles
input*: AsyncInputStream
output*: AsyncOutputStream
buffers*: PageBuffers
template enterWait(fut: var Future, context: static string) =
let wait = newFuture[void](context)
fut = wait
try: await wait
finally: fut = nil
template awake(fp: Future) =
let f = fp
if f != nil and not finished(f):
complete f
proc pipeRead(s: LayeredInputStream,
dst: pointer, dstLen: Natural): Future[Natural] {.async.} =
let buffers = s.buffers
if buffers.eofReached: return 0
var
bytesInBuffersAtStart = buffers.totalBufferedBytes
minBytesExpected = max(1, dstLen)
bytesInBuffersNow = bytesInBuffersAtStart
describeBuffers "at start", buffers
while bytesInBuffersNow < minBytesExpected:
awake buffers.waitingWriter
echo "About to wait for writer"
buffers.waitingReader.enterWait "waiting for writer to buffer more data"
echo "Awaken from wait"
bytesInBuffersNow = buffers.totalBufferedBytes
if buffers.eofReached:
echo "read bytes ", bytesInBuffersNow - bytesInBuffersAtStart
describeBuffers "at end", buffers
return bytesInBuffersNow - bytesInBuffersAtStart
if dst != nil:
doAssert drainBuffersInto(s, cast[ptr byte](dst), dstLen) == dstLen
awake buffers.waitingWriter
return bytesInBuffersNow - bytesInBuffersAtStart
proc pipeWrite(s: LayeredOutputStream, src: pointer, srcLen: Natural) {.async.} =
let buffers = s.buffers
echo "pipe write"
while buffers.canAcceptWrite(srcLen) == false:
buffers.waitingWriter.enterWait "waiting for reader to drain the buffers"
if src != nil:
buffers.appendUnbufferedWrite(src, srcLen)
awake buffers.waitingReader
describeBuffers "pipeWrite", buffers
template completedFuture(name: static string): untyped =
let fut = newFuture[void](name)
complete fut
fut
let pipeInputVTable = InputStreamVTable(
readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural
{.nimcall, gcsafe, raises: [IOError, Defect].} =
fsTranslateErrors "Failed to read from pipe":
let ls = LayeredInputStream(s)
doAssert ls.allowWaitFor
return waitFor pipeRead(ls, dst, dstLen)
,
readAsync: proc (s: InputStream, dst: pointer, dstLen: Natural): Future[Natural]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
fsTranslateErrors "Unexpected error from the async macro":
let ls = LayeredInputStream(s)
return pipeRead(ls, dst, dstLen)
,
getLenSync: proc (s: InputStream): Option[Natural]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
let source = LayeredInputStream(s).source
if source != nil:
return source.len
,
closeSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
let source = LayeredInputStream(s).source
if source != nil:
close source
,
closeAsync: proc (s: InputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
fsTranslateErrors "Unexpected error from the async macro":
let source = LayeredInputStream(s).source
if source != nil:
return closeAsync(Async source)
else:
return completedFuture("pipeInput.closeAsync")
)
let pipeOutputVTable = OutputStreamVTable(
writeSync: proc (s: OutputStream, src: pointer, srcLen: Natural)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
fsTranslateErrors "Failed to write all bytes to pipe":
var ls = LayeredOutputStream(s)
doAssert ls.allowWaitFor
waitFor pipeWrite(ls, src, srcLen)
,
writeAsync: proc (s: OutputStream, src: pointer, srcLen: Natural): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
# TODO: The async macro is raising exceptions even when
# merely forwarding a future:
fsTranslateErrors "Unexpected error from the async macro":
return pipeWrite(LayeredOutputStream s, src, srcLen)
,
flushSync: proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
let destination = LayeredOutputStream(s).destination
if destination != nil:
flush destination
,
flushAsync: proc (s: OutputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
fsTranslateErrors "Unexpected error from the async macro":
let destination = LayeredOutputStream(s).destination
if destination != nil:
return flushAsync(Async destination)
else:
return completedFuture("pipeOutput.flushAsync")
,
closeSync: proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
s.buffers.eofReached = true
echo "writer closes the stream"
fsTranslateErrors "Unexpected error from Future.complete":
awake s.buffers.waitingReader
let destination = LayeredOutputStream(s).destination
if destination != nil:
close destination
,
closeAsync: proc (s: OutputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
s.buffers.eofReached = true
fsTranslateErrors "Unexpected error from Future.complete":
awake s.buffers.waitingReader
fsTranslateErrors "Unexpected error from the async macro":
let destination = LayeredOutputStream(s).destination
if destination != nil:
return closeAsync(Async destination)
else:
return completedFuture("pipeOutput.closeAsync")
)
func pipeInput*(source: InputStream,
pageSize = defaultPageSize,
allowWaitFor = false): AsyncInputStream =
doAssert pageSize > 0
AsyncInputStream LayeredInputStream(
vtable: vtableAddr pipeInputVTable,
buffers: initPageBuffers pageSize,
allowWaitFor: allowWaitFor,
source: source)
func pipeInput*(buffers: PageBuffers,
allowWaitFor = false,
source: InputStream = nil): AsyncInputStream =
var span = if buffers.len == 0: default(PageSpan)
else: obtainReadableSpan buffers.queue[0]
AsyncInputStream LayeredInputStream(
vtable: vtableAddr pipeInputVTable,
buffers: buffers,
span: span,
spanEndPos: span.len,
allowWaitFor: allowWaitFor,
source: source)
proc pipeOutput*(destination: OutputStream,
pageSize = defaultPageSize,
maxBufferedBytes = defaultPageSize * 4,
allowWaitFor = false): AsyncOutputStream =
doAssert pageSize > 0
var
buffers = initPageBuffers pageSize
span = buffers.getWritableSpan()
AsyncOutputStream LayeredOutputStream(
vtable: vtableAddr pipeOutputVTable,
buffers: buffers,
span: span,
spanEndPos: span.len,
allowWaitFor: allowWaitFor,
destination: destination)
proc pipeOutput*(buffers: PageBuffers,
allowWaitFor = false,
destination: OutputStream = nil): AsyncOutputStream =
var span = buffers.getWritableSpan()
AsyncOutputStream LayeredOutputStream(
vtable: vtableAddr pipeOutputVTable,
buffers: buffers,
span: span,
# TODO What if the buffers are partially populated?
# Should we adjust the spanEndPos? This would
# need the old buffers.totalBytesWritten var.
spanEndPos: span.len,
allowWaitFor: allowWaitFor,
destination: destination)
func asyncPipe*(pageSize = defaultPageSize,
maxBufferedBytes = defaultPageSize * 4): FsAsyncPipe =
doAssert pageSize > 0
FsAsyncPipe(buffers: initPageBuffers(pageSize, maxBufferedBytes))
func initReader*(pipe: FsAsyncPipe): AsyncInputStream =
result = pipeInput(pipe.buffers)
pipe.input = result
func initWriter*(pipe: FsAsyncPipe): AsyncOutputStream =
result = pipeOutput(pipe.buffers)
pipe.output = result
proc exchangeBuffersAfterPipilineStep(input: InputStream, output: OutputStream) =
let formerInputBuffers = input.buffers
let formerOutputBuffers = output.getBuffers
input.resetBuffers formerOutputBuffers
output.recycleBuffers formerInputBuffers
macro executePipeline*(start: InputStream, steps: varargs[untyped]): untyped =
result = newTree(nnkStmtListExpr)
var
inputVal = start
outputVal = newCall(bindSym"memoryOutput")
inputVar = genSym(nskVar, "input")
outputVar = genSym(nskVar, "output")
step0 = steps[0]
result.add quote do:
var
step = steps[i]
outputVar = genSym(nskVar, "out")
output = if i == steps.len - 2: steps[^1]
else: newCall(bindSym"memoryOutput")
`inputVar` = `inputVal`
`outputVar` = OutputStream `outputVal`
`step0`(`inputVar`, `outputVar`)
if steps.len > 2:
let step1 = steps[1]
result.add quote do:
var `outputVar` = `output`
`step`(`input`, `outputVar`)
let formerInputBuffers = `inputVar`.buffers
`inputVar` = memoryInput(getBuffers `outputVar`)
recycleBuffers(`outputVar`, formerInputBuffers)
`step1`(`inputVar`, `outputVar`)
input = quote do:
unsafeMemoryInput(getOutput(`outputVar`))
for i in 2 .. steps.len - 2:
let step = steps[i]
result.add quote do:
exchangeBuffersAfterPipilineStep(`inputVar`, `outputVar`)
`step`(`inputVar`, `outputVar`)
var closingCall = steps[^1]
closingCall.insert(1, outputVar)
result.add closingCall
if defined(debugMacros) or defined(debugPipelines):
echo result.repr
macro executePipeline*(start: AsyncInputStream, steps: varargs[untyped]): untyped =
var
stream = ident "stream"
pipelineSteps = ident "pipelineSteps"
pipelineBody = newTree(nnkStmtList)
step0 = steps[0]
stepOutput = genSym(nskVar, "pipe")
pipelineBody.add quote do:
var `pipelineSteps` = newSeq[Future[void]]()
var `stepOutput` = asyncPipe()
add `pipelineSteps`, `step0`(`stream`, initWriter(`stepOutput`))
var
stepInput = stepOutput
for i in 1 .. steps.len - 2:
var step = steps[i]
stepOutput = genSym(nskVar, "pipe")
pipelineBody.add quote do:
var `stepOutput` = asyncPipe()
add `pipelineSteps`, `step`(initReader(`stepInput`), initWriter(`stepOutput`))
stepInput = stepOutput
var RetTypeExpr = copy steps[^1]
RetTypeExpr.insert(1, newCall("default", ident"AsyncOutputStream"))
var closingCall = steps[^1]
closingCall.insert(1, newDotExpr(stepInput, ident"output"))
pipelineBody.add quote do:
await allFutures(`pipelineSteps`)
return `closingCall`
result = quote do:
type RetType = type(`RetTypeExpr`)
proc pipelineProc(`stream`: AsyncInputStream): Future[RetType] {.async.} =
`pipelineBody`
pipelineProc(`start`)
when defined(debugMacros):
echo result.repr