Implement the async pipelines and fsMultiSync
This commit is contained in:
parent
bf930b6f1b
commit
cb36a6d4db
12 changed files with 920 additions and 299 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import
|
import
|
||||||
faststreams/[inputs, outputs]
|
faststreams/[inputs, outputs, pipelines, multisync]
|
||||||
|
|
||||||
export
|
export
|
||||||
inputs, outputs
|
inputs, outputs, pipelines, multisync
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ type
|
||||||
waitAsyncClose
|
waitAsyncClose
|
||||||
dontWaitAsyncClose
|
dontWaitAsyncClose
|
||||||
|
|
||||||
|
const debugHelpers* = defined(debugHelpers)
|
||||||
|
|
||||||
when faststreams_async_backend == "chronos":
|
when faststreams_async_backend == "chronos":
|
||||||
import
|
import
|
||||||
chronos
|
chronos
|
||||||
|
|
|
||||||
|
|
@ -3,93 +3,129 @@ import
|
||||||
stew/[ptrops, ranges/ptr_arith],
|
stew/[ptrops, ranges/ptr_arith],
|
||||||
async_backend
|
async_backend
|
||||||
|
|
||||||
|
export
|
||||||
|
deques
|
||||||
|
|
||||||
type
|
type
|
||||||
PageSpan* = object
|
PageSpan* = object
|
||||||
startAddr*, endAddr*: ptr byte
|
startAddr*, endAddr*: ptr byte
|
||||||
|
|
||||||
Page* = object
|
Page* = object
|
||||||
startOffset*: Natural
|
consumedTo*: Natural
|
||||||
endOffset*: Natural
|
writtenTo*: Natural
|
||||||
data*: ref string
|
data*: ref string
|
||||||
|
|
||||||
PageRef* = ref Page
|
PageRef* = ref Page
|
||||||
|
|
||||||
PageBuffers* = ref object
|
PageBuffers* = ref object
|
||||||
pageSize*: Natural
|
pageSize*: Natural
|
||||||
maxWriteSize*: Natural
|
maxBufferedBytes*: Natural
|
||||||
backPressureLimit*: Natural
|
|
||||||
|
|
||||||
queue*: Deque[PageRef]
|
queue*: Deque[PageRef]
|
||||||
getters: seq[Future[void]]
|
waitingReader*: Future[void]
|
||||||
putters: seq[Future[void]]
|
waitingWriter*: Future[void]
|
||||||
|
|
||||||
eofReached: bool
|
eofReached*: bool
|
||||||
|
|
||||||
totalBytesRead*: Natural
|
|
||||||
totalBytesWritten*: Natural
|
|
||||||
|
|
||||||
const
|
const
|
||||||
nimPageSize* = 4096
|
nimPageSize* = 4096
|
||||||
pageMetadataSize* = offsetof(Page, data)
|
|
||||||
nimAllocatorMetadataSize* = 32
|
nimAllocatorMetadataSize* = 32
|
||||||
# TODO: Get this legally from the Nim allocator.
|
# TODO: Get this legally from the Nim allocator.
|
||||||
# The goal is to make perfect page-aligned allocations
|
# The goal is to make perfect page-aligned allocations
|
||||||
# that get fast O(0) treatment.
|
# that go through a fast O(0) path in the allocator.
|
||||||
defaultPageSize* = 4096 - (pageMetadataSize + nimAllocatorMetadataSize)
|
defaultPageSize* = 4096 - nimAllocatorMetadataSize
|
||||||
maxStackUsage* = 16384
|
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))
|
(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])
|
cast[ptr byte](addr page.data[][0])
|
||||||
|
|
||||||
func pageStartAddr*(page: PageRef): ptr byte =
|
func readableStart*(page: PageRef): ptr byte =
|
||||||
offset(cast[ptr byte](addr page.data[][0]), page.startOffset)
|
offset(cast[ptr byte](addr page.data[][0]), page.consumedTo)
|
||||||
|
|
||||||
func pageEndAddr*(page: PageRef): ptr byte =
|
func readableEnd*(page: PageRef): ptr byte =
|
||||||
offset(cast[ptr byte](addr page.data[][0]), page.endOffset)
|
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 =
|
func pageLen*(page: PageRef): Natural =
|
||||||
page.endOffset - page.startOffset
|
page.writtenTo - page.consumedTo
|
||||||
|
|
||||||
template pageChars*(page: PageRef): untyped =
|
template pageChars*(page: PageRef): untyped =
|
||||||
let baseAddr = cast[ptr UncheckedArray[char]](pageBaseAddr(page))
|
let baseAddr = cast[ptr UncheckedArray[char]](allocationStart(page))
|
||||||
toOpenArray(baseAddr, page.startOffset, page.endOffset - 1)
|
toOpenArray(baseAddr, page.consumedTo, page.writtenTo - 1)
|
||||||
|
|
||||||
func span*(page: PageRef, writable: static[bool] = false): PageSpan =
|
func obtainReadableSpan*(page: PageRef, writable: static[bool] = false): PageSpan =
|
||||||
let baseAddr = page.pageBaseAddr
|
let baseAddr = page.allocationStart
|
||||||
PageSpan(startAddr: offset(baseAddr, page.startOffset),
|
result = PageSpan(startAddr: offset(baseAddr, page.consumedTo),
|
||||||
endAddr: offset(baseAddr, when writable: page.data[].len
|
endAddr: offset(baseAddr, page.writtenTo))
|
||||||
else: page.endOffset))
|
page.consumedTo = page.writtenTo
|
||||||
|
|
||||||
template writableSpan*(page: PageRef): PageSpan =
|
func writableSpan*(page: PageRef): PageSpan =
|
||||||
span(page, writable = true)
|
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,
|
func initPageBuffers*(pageSize: Natural,
|
||||||
maxWriteSize = high(int)): PageBuffers =
|
maxBufferedBytes: Natural = 0): PageBuffers =
|
||||||
|
# TODO: remove the unbuferred streams
|
||||||
if pageSize > 0:
|
if pageSize > 0:
|
||||||
return PageBuffers(pageSize: pageSize,
|
return PageBuffers(pageSize: pageSize,
|
||||||
maxWriteSize: maxWriteSize)
|
maxBufferedBytes: maxBufferedBytes)
|
||||||
|
|
||||||
template allocRef[T: not ref](x: T): ref T =
|
template allocRef[T: not ref](x: T): ref T =
|
||||||
let res = new type(x)
|
let res = new type(x)
|
||||||
res[] = x
|
res[] = x
|
||||||
res
|
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 =
|
func addWritablePage*(buffers: PageBuffers, pageSize: Natural): PageRef =
|
||||||
result = PageRef(data: allocRef newString(pageSize),
|
trackWrittenToEnd(buffers)
|
||||||
endOffset: pageSize)
|
result = PageRef(data: allocRef newString(pageSize))
|
||||||
buffers.queue.addLast result
|
buffers.queue.addLast result
|
||||||
|
|
||||||
func getWritablePage*(buffers: PageBuffers,
|
func getWritablePage*(buffers: PageBuffers,
|
||||||
preferredSize: Natural): PageRef =
|
preferredSize: Natural): PageRef =
|
||||||
if buffers.queue.len == 1:
|
if buffers.queue.len > 0:
|
||||||
let recycledPage = buffers.queue.peekLast
|
let lastPage = buffers.queue.peekLast
|
||||||
if recycledPage.endOffset == 0 and
|
if lastPage.writtenTo < lastPage.data[].len:
|
||||||
recycledPage.data[].len == preferredSize:
|
return lastPage
|
||||||
recycledPage.endOffset = recycledPage.data[].len
|
|
||||||
return recycledPage
|
|
||||||
|
|
||||||
return addWritablePage(buffers, preferredSize)
|
return addWritablePage(buffers, preferredSize)
|
||||||
|
|
||||||
|
|
@ -97,26 +133,120 @@ func addWritablePage*(buffers: PageBuffers): PageRef =
|
||||||
buffers.addWritablePage(buffers.pageSize)
|
buffers.addWritablePage(buffers.pageSize)
|
||||||
|
|
||||||
template getWritableSpan*(buffers: PageBuffers): PageSpan =
|
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 =
|
func nextReadableSpan*(buffers: PageBuffers, span: var PageSpan) =
|
||||||
if buffers.queue.len > 1:
|
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
|
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 =
|
func nextAlignedSize*(minSize, pageSize: Natural): Natural =
|
||||||
doAssert buffers.queue.len == 0
|
# TODO: This is not perfectly accurate. Revisit later
|
||||||
buffers.pageSize = neededRunway
|
((minSize div pageSize) + 1) * pageSize
|
||||||
getWritableSpan(buffers)
|
|
||||||
|
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 =
|
template len*(buffers: PageBuffers): Natural =
|
||||||
buffers.queue.len
|
buffers.queue.len
|
||||||
|
|
||||||
func totalBufferredBytes*(buffers: PageBuffers): Natural =
|
func totalBufferedBytes*(buffers: PageBuffers): Natural =
|
||||||
for i in 1 ..< buffers.queue.len:
|
for i in 0 ..< buffers.queue.len:
|
||||||
result += buffers.queue[i].pageLen
|
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 =
|
template popFirst*(buffers: PageBuffers): PageRef =
|
||||||
buffers.queue.popFirst
|
buffers.queue.popFirst
|
||||||
|
|
||||||
|
|
@ -126,24 +256,15 @@ template `[]`*(buffers: PageBuffers, idx: Natural): PageRef =
|
||||||
func splitLastPageAt*(buffers: PageBuffers, address: ptr byte) =
|
func splitLastPageAt*(buffers: PageBuffers, address: ptr byte) =
|
||||||
var
|
var
|
||||||
topPage = buffers.queue.peekLast
|
topPage = buffers.queue.peekLast
|
||||||
newPage = PageRef()
|
splitPosition = distance(topPage.allocationStart, address)
|
||||||
splitPosition = distance(topPage.pageBaseAddr, address)
|
newPage = PageRef(
|
||||||
|
data: topPage.data,
|
||||||
newPage[] = topPage[]
|
consumedTo: splitPosition,
|
||||||
topPage.endOffset = splitPosition
|
writtenTo: splitPosition)
|
||||||
newPage.startOffset = splitPosition
|
|
||||||
|
|
||||||
|
topPage.writtenTo = splitPosition
|
||||||
buffers.queue.addLast newPage
|
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 =
|
iterator consumePages*(buffers: PageBuffers): PageRef =
|
||||||
doAssert buffers != nil
|
doAssert buffers != nil
|
||||||
|
|
||||||
|
|
@ -161,17 +282,14 @@ iterator consumePages*(buffers: PageBuffers): PageRef =
|
||||||
discard buffers.queue.popFirst
|
discard buffers.queue.popFirst
|
||||||
|
|
||||||
if recycledPage != nil:
|
if recycledPage != nil:
|
||||||
recycledPage.startOffset = 0
|
recycledPage.consumedTo = 0
|
||||||
recycledPage.endOffset = 0
|
recycledPage.writtenTo = 0
|
||||||
buffers.queue.addLast recycledPage
|
buffers.queue.addLast recycledPage
|
||||||
|
|
||||||
iterator consumePageBuffers*(buffers: PageBuffers): (ptr byte, Natural) =
|
iterator consumePageBuffers*(buffers: PageBuffers): (ptr byte, Natural) =
|
||||||
for page in consumePages(buffers):
|
for page in consumePages(buffers):
|
||||||
yield (page.pageStartAddr,
|
yield (page.readableStart,
|
||||||
Natural(page.endOffset - page.startOffset))
|
Natural(page.writtenTo - page.consumedTo))
|
||||||
|
|
||||||
template wasEofReached*(buffers: PageBuffers): bool =
|
|
||||||
buffers.eofReached
|
|
||||||
|
|
||||||
# BEWARE! These templates violate the double evaluation
|
# BEWARE! These templates violate the double evaluation
|
||||||
# safety measures in order to produce better inlined
|
# safety measures in order to produce better inlined
|
||||||
|
|
@ -217,15 +335,11 @@ template implementWrites*(buffersParam: PageBuffers,
|
||||||
let bytesWritten = writeBlock
|
let bytesWritten = writeBlock
|
||||||
# TODO: Can we repair the buffers here?
|
# TODO: Can we repair the buffers here?
|
||||||
if bytesWritten != writeLenVar: raiseError()
|
if bytesWritten != writeLenVar: raiseError()
|
||||||
buffers.totalBytesWritten += bytesWritten
|
|
||||||
|
|
||||||
if srcLen > 0:
|
if srcLen > 0:
|
||||||
doAssert src != nil
|
doAssert src != nil
|
||||||
let bytesWritten = writeBlock
|
let bytesWritten = writeBlock
|
||||||
if bytesWritten != writeLenVar: raiseError()
|
if bytesWritten != writeLenVar: raiseError()
|
||||||
# TODO: Fix this after removing the unbuffered streams
|
|
||||||
if buffers != nil:
|
|
||||||
buffers.totalBytesWritten += bytesWritten
|
|
||||||
|
|
||||||
type
|
type
|
||||||
ReadFlag* = enum
|
ReadFlag* = enum
|
||||||
|
|
@ -253,19 +367,17 @@ template implementSingleRead*(buffersParam: PageBuffers,
|
||||||
bestPageSize = nextAlignedSize(readLenVar, buffers.pageSize)
|
bestPageSize = nextAlignedSize(readLenVar, buffers.pageSize)
|
||||||
page = getWritablePage(buffers, bestPageSize)
|
page = getWritablePage(buffers, bestPageSize)
|
||||||
|
|
||||||
readStartVar = page.pageStartAddr
|
readStartVar = writableStart(page)
|
||||||
readLenVar = page.endOffset - page.startOffset
|
readLenVar = page.data[].len - page.writtenTo
|
||||||
|
|
||||||
# TODO: what if we exit with an exception here?
|
# TODO: what if we exit with an exception here?
|
||||||
# Are the side-effects of `getWritablePage` above OK to keep?
|
# Are the side-effects of `getWritablePage` above OK to keep?
|
||||||
bytesRead = readBlock
|
bytesRead = readBlock
|
||||||
page.endOffset = page.startOffset + bytesRead
|
page.writtenTo += bytesRead
|
||||||
|
|
||||||
if (bytesRead == 0 and zeroReadIsNotEof notin flags) or
|
if (bytesRead == 0 and zeroReadIsNotEof notin flags) or
|
||||||
(partialReadIsEof in flags and bytesRead < readLenVar):
|
(partialReadIsEof in flags and bytesRead < readLenVar):
|
||||||
buffers.eofReached = true
|
buffers.eofReached = true
|
||||||
else:
|
|
||||||
buffers.totalBytesRead += bytesRead
|
|
||||||
|
|
||||||
bytesRead
|
bytesRead
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,26 +20,28 @@ const
|
||||||
closingErrMsg = "Failed to close Chronos transport"
|
closingErrMsg = "Failed to close Chronos transport"
|
||||||
writeIncompleteErrMsg = "Failed to write all bytes to 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:
|
fsTranslateErrors closingErrMsg:
|
||||||
await t.closeWait()
|
await t.closeWait()
|
||||||
|
|
||||||
proc chronosReadOnce(s: ChronosInputStream, dst: pointer, dstLen: Natural): Future[Natural]
|
proc chronosReadOnce(s: ChronosInputStream,
|
||||||
{.raises: [IOError, Defect], async.} =
|
dst: pointer, dstLen: Natural): Future[Natural]
|
||||||
|
{.async, raises: [IOError, Defect].} =
|
||||||
fsTranslateErrors readingErrMsg:
|
fsTranslateErrors readingErrMsg:
|
||||||
return implementSingleRead(s.buffers, dst, dstLen, {},
|
return implementSingleRead(s.buffers, dst, dstLen, {},
|
||||||
readStartAddr, readLen):
|
readStartAddr, readLen):
|
||||||
await s.transport.readOnce(readStartAddr, readLen)
|
await s.transport.readOnce(readStartAddr, readLen)
|
||||||
|
|
||||||
proc chronosWrites(s: ChronosOutputStream, src: pointer, srcLen: Natural)
|
proc chronosWrites(s: ChronosOutputStream, src: pointer, srcLen: Natural)
|
||||||
{.raises: [IOError, Defect], async.} =
|
{.async, raises: [IOError, Defect].} =
|
||||||
fsTranslateErrors writeIncompleteErrMsg:
|
fsTranslateErrors writeIncompleteErrMsg:
|
||||||
implementWrites(s.buffers, src, srcLen, "StreamTransport"
|
implementWrites(s.buffers, src, srcLen, "StreamTransport"
|
||||||
writeStartAddr, writeLen):
|
writeStartAddr, writeLen):
|
||||||
await s.transport.write(writeStartAddr, writeLen)
|
await s.transport.write(writeStartAddr, writeLen)
|
||||||
|
|
||||||
# TODO: Use the Raising type here
|
# TODO: Use the Raising type here
|
||||||
let ChronosInputStreamVTable = InputStreamVTable(
|
let chronosInputVTable = InputStreamVTable(
|
||||||
readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural
|
readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
var cs = ChronosInputStream(s)
|
var cs = ChronosInputStream(s)
|
||||||
|
|
@ -61,14 +63,14 @@ let ChronosInputStreamVTable = InputStreamVTable(
|
||||||
)
|
)
|
||||||
|
|
||||||
func chronosInput*(s: StreamTransport,
|
func chronosInput*(s: StreamTransport,
|
||||||
pageSize = buffers.defaultPageSize,
|
pageSize = defaultPageSize,
|
||||||
allowWaitFor = false): InputStreamHandle =
|
allowWaitFor = false): InputStreamHandle =
|
||||||
InputStreamHandle(s: ChronosInputStream(
|
makeHandle ChronosInputStream(
|
||||||
vtable: vtableAddr ChronosInputStreamVTable,
|
vtable: vtableAddr chronosInputVTable,
|
||||||
pageSize: pageSize,
|
buffers: initPageBuffers(pageSize),
|
||||||
allowWaitFor: allowWaitFor))
|
allowWaitFor: allowWaitFor)
|
||||||
|
|
||||||
let ChronosOutputStreamVTable = OutputStreamVTable(
|
let chronosOutputVTable = OutputStreamVTable(
|
||||||
writeSync: proc (s: OutputStream, src: pointer, srcLen: Natural)
|
writeSync: proc (s: OutputStream, src: pointer, srcLen: Natural)
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
var cs = ChronosOutputStream(s)
|
var cs = ChronosOutputStream(s)
|
||||||
|
|
@ -90,17 +92,11 @@ let ChronosOutputStreamVTable = OutputStreamVTable(
|
||||||
)
|
)
|
||||||
|
|
||||||
func chronosOutput*(s: StreamTransport,
|
func chronosOutput*(s: StreamTransport,
|
||||||
pageSize = buffers.defaultPageSize,
|
pageSize = defaultPageSize,
|
||||||
allowWaitFor = false): OutputStreamHandle =
|
allowWaitFor = false): OutputStreamHandle =
|
||||||
var stream = ChronosOutputStream(
|
makeHandle ChronosOutputStream(
|
||||||
vtable: vtableAddr(SnappyStreamVTable),
|
vtable: vtableAddr(chronosOuputVTable),
|
||||||
pageSize: pageSize,
|
buffers: initPageBuffers(pageSize)
|
||||||
minWriteSize: 1,
|
|
||||||
maxWriteSize: high(int),
|
|
||||||
transport: s,
|
transport: s,
|
||||||
allowWaitFor: allowWaitFor)
|
allowWaitFor: allowWaitFor)
|
||||||
|
|
||||||
stream.initWithSinglePage()
|
|
||||||
|
|
||||||
OutputStreamHandle(s: stream)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,17 @@ export
|
||||||
|
|
||||||
type
|
type
|
||||||
InputStream* = ref object of RootObj
|
InputStream* = ref object of RootObj
|
||||||
vtable: ptr InputStreamVTable # This is nil for unsafe memory inputs
|
vtable*: ptr InputStreamVTable # This is nil for unsafe memory inputs
|
||||||
buffers: PageBuffers # This is nil for unsafe memory inputs
|
buffers*: PageBuffers # This is nil for unsafe memory inputs
|
||||||
span: PageSpan
|
span*: PageSpan
|
||||||
spanEndPos: Natural
|
spanEndPos*: Natural
|
||||||
closeFut: Future[void] # This is nil before `close` is called
|
closeFut: Future[void] # This is nil before `close` is called
|
||||||
|
when debugHelpers:
|
||||||
|
name*: string
|
||||||
|
|
||||||
LayeredInputStream* = ref object of InputStream
|
LayeredInputStream* = ref object of InputStream
|
||||||
subStream*: InputStream
|
source*: InputStream
|
||||||
|
allowWaitFor*: bool
|
||||||
|
|
||||||
InputStreamHandle* = object
|
InputStreamHandle* = object
|
||||||
s*: InputStream
|
s*: InputStream
|
||||||
|
|
@ -34,7 +37,7 @@ type
|
||||||
CloseAsyncProc* = proc (s: InputStream): Future[void]
|
CloseAsyncProc* = proc (s: InputStream): Future[void]
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].}
|
{.nimcall, gcsafe, raises: [IOError, Defect].}
|
||||||
|
|
||||||
GetLenSyncProc* = proc (s: InputStream): Natural
|
GetLenSyncProc* = proc (s: InputStream): Option[Natural]
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].}
|
{.nimcall, gcsafe, raises: [IOError, Defect].}
|
||||||
|
|
||||||
InputStreamVTable* = object
|
InputStreamVTable* = object
|
||||||
|
|
@ -50,6 +53,12 @@ type
|
||||||
FileInputStream = ref object of InputStream
|
FileInputStream = ref object of InputStream
|
||||||
file: File
|
file: File
|
||||||
|
|
||||||
|
template Async*(s: InputStream): AsyncInputStream =
|
||||||
|
AsyncInputStream(s)
|
||||||
|
|
||||||
|
template Sync*(s: AsyncInputStream): InputStream =
|
||||||
|
InputStream(s)
|
||||||
|
|
||||||
proc disconnectInputDevice(s: InputStream) =
|
proc disconnectInputDevice(s: InputStream) =
|
||||||
# TODO
|
# TODO
|
||||||
# Document the behavior that closeAsync is preferred
|
# Document the behavior that closeAsync is preferred
|
||||||
|
|
@ -92,14 +101,17 @@ proc close*(s: InputStream,
|
||||||
else:
|
else:
|
||||||
asyncCheck s.closeFut
|
asyncCheck s.closeFut
|
||||||
|
|
||||||
proc close*(s: AsyncInputStream): Future[void]
|
template close*(sp: AsyncInputStream) =
|
||||||
{.raises: [IOError, Defect].} =
|
|
||||||
## Starts the asychronous closing of the stream and returns a future that
|
## Starts the asychronous closing of the stream and returns a future that
|
||||||
## tracks the closing operation.
|
## tracks the closing operation.
|
||||||
s.disconnectInputDevice()
|
let s = InputStream sp
|
||||||
s.preventFurtherReading()
|
disconnectInputDevice(s)
|
||||||
result = InputStream(s).closeFut
|
preventFurtherReading(s)
|
||||||
doAssert result != nil
|
if s.closeFut != nil:
|
||||||
|
await s.closeFut
|
||||||
|
|
||||||
|
proc closeAsync*(s: AsyncInputStream) {.async.} =
|
||||||
|
close s
|
||||||
|
|
||||||
template closeNoWait*(sp: AsyncInputStream|InputStream) =
|
template closeNoWait*(sp: AsyncInputStream|InputStream) =
|
||||||
## Close the stream without waiting even if's async.
|
## Close the stream without waiting even if's async.
|
||||||
|
|
@ -143,7 +155,7 @@ template vtableAddr*(vtable: InputStreamVTable): ptr InputStreamVTable =
|
||||||
{.noSideEffect.}:
|
{.noSideEffect.}:
|
||||||
unsafeAddr vtable
|
unsafeAddr vtable
|
||||||
|
|
||||||
let MemFileInputVTable = InputStreamVTable(
|
let memFileInputVTable = InputStreamVTable(
|
||||||
closeSync: proc (s: InputStream)
|
closeSync: proc (s: InputStream)
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
try:
|
try:
|
||||||
|
|
@ -151,9 +163,9 @@ let MemFileInputVTable = InputStreamVTable(
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise newException(IOError, "Failed to close file", 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].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
s.span.len
|
some s.span.len
|
||||||
)
|
)
|
||||||
|
|
||||||
proc memFileInput*(filename: string, mappedSize = -1, offset = 0): InputStreamHandle
|
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
|
mappedSize = memFile.size
|
||||||
|
|
||||||
makeHandle MemFileInputStream(
|
makeHandle MemFileInputStream(
|
||||||
vtable: vtableAddr MemFileInputVTable,
|
vtable: vtableAddr memFileInputVTable,
|
||||||
span: PageSpan(
|
span: PageSpan(
|
||||||
startAddr: head,
|
startAddr: head,
|
||||||
endAddr: offset(head, mappedSize)),
|
endAddr: offset(head, mappedSize)),
|
||||||
|
|
@ -203,19 +215,37 @@ proc readableNow*(s: InputStream): bool =
|
||||||
template readableNow*(s: AsyncInputStream): bool =
|
template readableNow*(s: AsyncInputStream): bool =
|
||||||
readableNow InputStream(s)
|
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 =
|
func totalUnconsumedBytes*(s: InputStream): Natural =
|
||||||
## Returns the number of bytes that are currently sitting within the stream
|
## Returns the number of bytes that are currently sitting within the stream
|
||||||
## buffers and that can be consumed with `read` or `advance`.
|
## buffers and that can be consumed with `read` or `advance`.
|
||||||
result = s.span.len
|
let
|
||||||
if s.buffers != nil:
|
localRunway = s.span.len
|
||||||
result += s.buffers.totalBufferredBytes
|
runwayInBuffers = if s.buffers == nil: 0
|
||||||
# result += s.buffers.totalBytesRead - s.spanEndPos
|
else: s.buffers.totalBufferedBytes
|
||||||
|
|
||||||
|
if localRunway == 0 and runwayInBuffers > 0:
|
||||||
|
flipPage s
|
||||||
|
|
||||||
|
localRunway + runwayInBuffers
|
||||||
|
|
||||||
template totalUnconsumedBytes*(s: AsyncInputStream): Natural =
|
template totalUnconsumedBytes*(s: AsyncInputStream): Natural =
|
||||||
## Alias for InputStream.totalUnconsumedBytes
|
## Alias for InputStream.totalUnconsumedBytes
|
||||||
totalUnconsumedBytes InputStream(s)
|
totalUnconsumedBytes InputStream(s)
|
||||||
|
|
||||||
let FileInputVTable = InputStreamVTable(
|
let fileInputVTable = InputStreamVTable(
|
||||||
readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural
|
readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
let file = FileInputStream(s).file
|
let file = FileInputStream(s).file
|
||||||
|
|
@ -224,7 +254,7 @@ let FileInputVTable = InputStreamVTable(
|
||||||
readStartAddr, readLen):
|
readStartAddr, readLen):
|
||||||
file.readBuffer(readStartAddr, readLen)
|
file.readBuffer(readStartAddr, readLen)
|
||||||
,
|
,
|
||||||
getLenSync: proc (s: InputStream): Natural
|
getLenSync: proc (s: InputStream): Option[Natural]
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
let
|
let
|
||||||
s = FileInputStream(s)
|
s = FileInputStream(s)
|
||||||
|
|
@ -235,7 +265,7 @@ let FileInputVTable = InputStreamVTable(
|
||||||
let endPos = getFilePos(s.file)
|
let endPos = getFilePos(s.file)
|
||||||
setFilePos(s.file, preservedPos)
|
setFilePos(s.file, preservedPos)
|
||||||
|
|
||||||
endPos - preservedPos + runway
|
some Natural(endPos - preservedPos + runway)
|
||||||
,
|
,
|
||||||
closeSync: proc (s: InputStream)
|
closeSync: proc (s: InputStream)
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
|
|
@ -266,7 +296,7 @@ proc fileInput*(filename: string,
|
||||||
setFilePos(file, offset)
|
setFilePos(file, offset)
|
||||||
|
|
||||||
makeHandle FileInputStream(
|
makeHandle FileInputStream(
|
||||||
vtable: vtableAddr FileInputVTable,
|
vtable: vtableAddr fileInputVTable,
|
||||||
buffers: initPageBuffers(pageSize),
|
buffers: initPageBuffers(pageSize),
|
||||||
file: file)
|
file: file)
|
||||||
|
|
||||||
|
|
@ -284,20 +314,44 @@ proc unsafeMemoryInput*(str: string): InputStreamHandle =
|
||||||
|
|
||||||
proc len*(s: InputStream): Option[Natural] {.raises: [Defect, IOError].} =
|
proc len*(s: InputStream): Option[Natural] {.raises: [Defect, IOError].} =
|
||||||
if s.vtable == nil:
|
if s.vtable == nil:
|
||||||
some s.span.len
|
some s.totalUnconsumedBytes
|
||||||
elif s.vtable.getLenSync != nil:
|
elif s.vtable.getLenSync != nil:
|
||||||
some s.vtable.getLenSync(s)
|
s.vtable.getLenSync(s)
|
||||||
else:
|
else:
|
||||||
none Natural
|
none Natural
|
||||||
|
|
||||||
template len*(s: AsyncInputStream): int =
|
template len*(s: AsyncInputStream): Option[Natural] =
|
||||||
len InputStream(s)
|
len InputStream(s)
|
||||||
|
|
||||||
proc flipPage(s: InputStream) =
|
func memoryInput*(buffers: PageBuffers): InputStreamHandle =
|
||||||
doAssert s.buffers.len > 1
|
var span = if buffers.len == 0: default(PageSpan)
|
||||||
discard s.buffers.popFirst
|
else: obtainReadableSpan buffers.queue[0]
|
||||||
s.span = s.buffers[0].span
|
|
||||||
s.spanEndPos += s.span.len
|
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 =
|
proc continueAfterRead(s: InputStream, bytesRead: Natural): bool =
|
||||||
# Please note that this is extracted into a proc only to reduce the code
|
# 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.
|
# 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
|
# If this is the case, we disconnect the input device which prevents any
|
||||||
# further attempts to read from it:
|
# further attempts to read from it:
|
||||||
if wasEofReached(s.buffers):
|
if s.buffers.eofReached:
|
||||||
disconnectInputDevice(s)
|
disconnectInputDevice(s)
|
||||||
|
|
||||||
if bytesRead > 0:
|
if bytesRead > 0:
|
||||||
s.span = s.buffers.getReadableSpan()
|
s.buffers.nextReadableSpan(s.span)
|
||||||
s.spanEndPos += s.span.len
|
s.spanEndPos += s.span.len
|
||||||
return true
|
return true
|
||||||
else:
|
else:
|
||||||
|
|
@ -395,21 +449,20 @@ template readable*(sp: AsyncInputStream): bool =
|
||||||
## Async version of `readable`.
|
## Async version of `readable`.
|
||||||
## The intended API usage is the same. Instead of blocking, an async
|
## The intended API usage is the same. Instead of blocking, an async
|
||||||
## stream will use `await` while waiting for more data.
|
## stream will use `await` while waiting for more data.
|
||||||
let s = sp
|
let s = InputStream sp
|
||||||
if hasRunway(s.span):
|
if hasRunway(s.span):
|
||||||
true
|
true
|
||||||
else:
|
else:
|
||||||
bufferMoreDataImpl(s, fsAsync, readAsync)
|
bufferMoreDataImpl(s, fsAwait, readAsync)
|
||||||
|
|
||||||
func continueAfterReadN(s: InputStream,
|
func continueAfterReadN(s: InputStream,
|
||||||
runwayBeforeRead, bytesRead: Natural) =
|
runwayBeforeRead, bytesRead: Natural) =
|
||||||
if runwayBeforeRead == 0 and bytesRead > 0:
|
if runwayBeforeRead == 0 and bytesRead > 0:
|
||||||
s.span = s.buffers.getReadableSpan()
|
s.buffers.nextReadableSpan(s.span)
|
||||||
s.spanEndPos += s.span.len
|
s.spanEndPos += s.span.len
|
||||||
|
|
||||||
template readableNImpl(s, n, awaiter, readOp: untyped): bool =
|
template readableNImpl(s, n, awaiter, readOp: untyped): bool =
|
||||||
let runway = totalUnconsumedBytes(s)
|
let runway = totalUnconsumedBytes(s)
|
||||||
|
|
||||||
if runway >= n:
|
if runway >= n:
|
||||||
true
|
true
|
||||||
elif s.buffers == nil or s.vtable == nil or s.vtable.readOp == nil:
|
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:
|
while true:
|
||||||
bytesRead += awaiter s.vtable.readOp(s, nil, bytesDeficit)
|
bytesRead += awaiter s.vtable.readOp(s, nil, bytesDeficit)
|
||||||
|
|
||||||
if wasEofReached(s.buffers):
|
if s.buffers.eofReached:
|
||||||
disconnectInputDevice(s)
|
disconnectInputDevice(s)
|
||||||
res = bytesRead >= bytesDeficit
|
res = bytesRead >= bytesDeficit
|
||||||
break
|
break
|
||||||
|
|
@ -472,7 +525,7 @@ template readable*(sp: AsyncInputStream, np: int): bool =
|
||||||
## The intended API usage is the same. Instead of blocking, an async
|
## The intended API usage is the same. Instead of blocking, an async
|
||||||
## stream will use `await` while waiting for more data.
|
## stream will use `await` while waiting for more data.
|
||||||
let
|
let
|
||||||
s = sp
|
s = InputStream sp
|
||||||
n = np
|
n = np
|
||||||
|
|
||||||
readableNImpl(s, n, fsAwait, readAsync)
|
readableNImpl(s, n, fsAwait, readAsync)
|
||||||
|
|
@ -491,7 +544,7 @@ proc peekAt*(s: InputStream, pos: int): byte {.inline.} =
|
||||||
return peekHead[]
|
return peekHead[]
|
||||||
|
|
||||||
template peekAt*(s: AsyncInputStream, pos: int): byte =
|
template peekAt*(s: AsyncInputStream, pos: int): byte =
|
||||||
peekAt InputStream(s)
|
peekAt InputStream(s), pos
|
||||||
|
|
||||||
proc advance*(s: InputStream) =
|
proc advance*(s: InputStream) =
|
||||||
if hasRunway(s.span):
|
if hasRunway(s.span):
|
||||||
|
|
@ -509,7 +562,7 @@ proc read*(s: InputStream): byte =
|
||||||
template read*(s: AsyncInputStream): byte =
|
template read*(s: AsyncInputStream): byte =
|
||||||
read InputStream(s)
|
read InputStream(s)
|
||||||
|
|
||||||
proc drainBuffersInto(s: InputStream, dstAddr: ptr byte, dstLen: Natural): Natural =
|
proc drainBuffersInto*(s: InputStream, dstAddr: ptr byte, dstLen: Natural): Natural =
|
||||||
var
|
var
|
||||||
dst = dstAddr
|
dst = dstAddr
|
||||||
remainingBytes = dstLen
|
remainingBytes = dstLen
|
||||||
|
|
@ -527,25 +580,36 @@ proc drainBuffersInto(s: InputStream, dstAddr: ptr byte, dstLen: Natural): Natur
|
||||||
if s.buffers != nil:
|
if s.buffers != nil:
|
||||||
# Since we reached the end of the current page,
|
# Since we reached the end of the current page,
|
||||||
# we have to do the equivalent of `flipPage`:
|
# we have to do the equivalent of `flipPage`:
|
||||||
|
|
||||||
|
# TODO: what if the page was extended?
|
||||||
if s.buffers.len > 0:
|
if s.buffers.len > 0:
|
||||||
discard s.buffers.popFirst()
|
discard s.buffers.popFirst()
|
||||||
|
|
||||||
for page in consumePages(s.buffers):
|
for page in consumePages(s.buffers):
|
||||||
let
|
let
|
||||||
pageStart = page.pageStartAddr
|
pageStart = page.readableStart
|
||||||
pageLen = page.endOffset - page.startOffset
|
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:
|
if pageLen > remainingBytes:
|
||||||
# This page has enough data to fill the rest of the buffer:
|
# This page has enough data to fill the rest of the buffer:
|
||||||
copyMem(dst, pageStart, remainingBytes)
|
copyMem(dst, pageStart, remainingBytes)
|
||||||
page.startOffset += remainingBytes
|
|
||||||
|
|
||||||
# This page is partially consumed now and we must set our
|
# This page is partially consumed now and we must set our
|
||||||
# span to point to its remaining contents. We also need to
|
# span to point to its remaining contents:
|
||||||
# know how much our position in the stream has advanced:
|
s.span = PageSpan(startAddr: offset(pageStart, remainingBytes),
|
||||||
let bytesDrainedFromBufers = dstLen - runway
|
endAddr: page.readableEnd)
|
||||||
s.span = page.span
|
|
||||||
s.spanEndPos += bytesDrainedFromBufers + s.span.len
|
# 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
|
# We return the length of the buffer, which means that is
|
||||||
# has been fully populated:
|
# has been fully populated:
|
||||||
|
|
@ -575,7 +639,7 @@ template readIntoExImpl(s: InputStream,
|
||||||
|
|
||||||
bytesRead += awaiter s.vtable.readOp(s, adjustedDst, bytesDeficit)
|
bytesRead += awaiter s.vtable.readOp(s, adjustedDst, bytesDeficit)
|
||||||
|
|
||||||
if wasEofReached(s.buffers):
|
if s.buffers.eofReached:
|
||||||
disconnectInputDevice(s)
|
disconnectInputDevice(s)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -630,13 +694,6 @@ proc readOnce*(sp: AsyncInputStream): Future[Natural] =
|
||||||
doAssert s.buffers != nil and s.vtable != nil
|
doAssert s.buffers != nil and s.vtable != nil
|
||||||
s.vtable.readAsync(s, nil, 0)
|
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):
|
when defined(windows):
|
||||||
proc alloca(n: int): ptr byte {.importc, header: "<malloc.h>".}
|
proc alloca(n: int): ptr byte {.importc, header: "<malloc.h>".}
|
||||||
else:
|
else:
|
||||||
|
|
@ -655,7 +712,7 @@ template readNImpl(sp: InputStream,
|
||||||
let
|
let
|
||||||
s = sp
|
s = sp
|
||||||
n = np
|
n = np
|
||||||
runway = getBestRunway(s)
|
runway = getBestContiguousRunway(s)
|
||||||
|
|
||||||
# Since Nim currently doesn't allow the `makeOpenArray` calls bellow
|
# Since Nim currently doesn't allow the `makeOpenArray` calls bellow
|
||||||
# to appear in different branches of an if statement, the code must
|
# to appear in different branches of an if statement, the code must
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,12 @@ macro fsMultiSync*(body: untyped) =
|
||||||
for i in 1 ..< asyncProcParams.len:
|
for i in 1 ..< asyncProcParams.len:
|
||||||
let paramsDef = asyncProcParams[i]
|
let paramsDef = asyncProcParams[i]
|
||||||
let typ = paramsDef[^2]
|
let typ = paramsDef[^2]
|
||||||
if sameType(typ, bindSym"InputStream"):
|
if eqIdent(typ, "InputStream"):
|
||||||
paramsDef[^2] = bindSym "AsyncInputStream"
|
paramsDef[^2] = bindSym "AsyncInputStream"
|
||||||
elif sameType(typ, bindSym"OutputStream"):
|
elif eqIdent(typ, "OutputStream"):
|
||||||
paramsDef[^2] = bindSym "AsyncOutputStream"
|
paramsDef[^2] = bindSym "AsyncOutputStream"
|
||||||
|
|
||||||
result = newStmtList(body, asyncProcBody)
|
result = newStmtList(body, asyncProcBody)
|
||||||
if defined(debugSupportAsync):
|
when defined(debugSupportAsync):
|
||||||
echo result.repr
|
echo result.repr
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,22 @@ export
|
||||||
|
|
||||||
type
|
type
|
||||||
OutputStream* = ref object of RootObj
|
OutputStream* = ref object of RootObj
|
||||||
vtable*: ptr OutputStreamVTable # This is nil for any memory output
|
vtable*: ptr OutputStreamVTable # This is nil for any memory output
|
||||||
buffers*: PageBuffers # This is nil for unsafe memory outputs
|
buffers*: PageBuffers # This is nil for unsafe memory outputs
|
||||||
span: PageSpan
|
span*: PageSpan
|
||||||
spanEndPos: Natural
|
spanEndPos*: Natural
|
||||||
extCursorsCount: int
|
extCursorsCount: int
|
||||||
closeFut: Future[void]
|
closeFut: Future[void] # This is nil before `close` is called
|
||||||
|
when debugHelpers:
|
||||||
|
name*: string
|
||||||
|
|
||||||
WriteCursor* = object
|
WriteCursor* = object
|
||||||
span: PageSpan
|
span: PageSpan
|
||||||
stream: OutputStream
|
stream: OutputStream
|
||||||
|
|
||||||
LayeredOutputStream* = ref object of OutputStream
|
LayeredOutputStream* = ref object of OutputStream
|
||||||
subStream*: OutputStream
|
destination*: OutputStream
|
||||||
|
allowWaitFor*: bool
|
||||||
|
|
||||||
OutputStreamHandle* = object
|
OutputStreamHandle* = object
|
||||||
s*: OutputStream
|
s*: OutputStream
|
||||||
|
|
@ -66,10 +69,11 @@ type
|
||||||
FileOutputStream = ref object of OutputStream
|
FileOutputStream = ref object of OutputStream
|
||||||
file: File
|
file: File
|
||||||
|
|
||||||
const
|
template Async*(s: OutputStream): AsyncOutputStream =
|
||||||
nimAllocatorMetadataSize* = 0
|
AsyncOutputStream(s)
|
||||||
# TODO: Get this from Nim's allocator.
|
|
||||||
# The goal is to make perfect page-aligned allocations
|
template Sync*(s: AsyncOutputStream): OutputStream =
|
||||||
|
OuputStream(s)
|
||||||
|
|
||||||
proc disconnectOutputDevice(s: OutputStream) =
|
proc disconnectOutputDevice(s: OutputStream) =
|
||||||
if s.vtable != nil:
|
if s.vtable != nil:
|
||||||
|
|
@ -82,9 +86,30 @@ proc disconnectOutputDevice(s: OutputStream) =
|
||||||
template disconnectOutputDevice(s: AsyncOutputStream) =
|
template disconnectOutputDevice(s: AsyncOutputStream) =
|
||||||
disconnectOutputDevice OutputStream(s)
|
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,
|
proc close*(s: OutputStream,
|
||||||
behavior = dontWaitAsyncClose)
|
behavior = dontWaitAsyncClose)
|
||||||
{.raises: [IOError, Defect].} =
|
{.raises: [IOError, Defect].} =
|
||||||
|
flush s
|
||||||
disconnectOutputDevice(s)
|
disconnectOutputDevice(s)
|
||||||
if s.closeFut != nil:
|
if s.closeFut != nil:
|
||||||
fsTranslateErrors "Stream closing failed":
|
fsTranslateErrors "Stream closing failed":
|
||||||
|
|
@ -93,11 +118,15 @@ proc close*(s: OutputStream,
|
||||||
else:
|
else:
|
||||||
asyncCheck s.closeFut
|
asyncCheck s.closeFut
|
||||||
|
|
||||||
proc close*(s: AsyncOutputStream): Future[void]
|
template close*(sp: AsyncOutputStream) =
|
||||||
{.raises: [IOError, Defect].} =
|
let s = OutputStream sp
|
||||||
|
flush(Async s)
|
||||||
disconnectOutputDevice(s)
|
disconnectOutputDevice(s)
|
||||||
result = OutputStream(s).closeFut
|
if s.closeFut != nil:
|
||||||
doAssert result != nil
|
await s.closeFut
|
||||||
|
|
||||||
|
proc closeAsync*(s: AsyncOutputStream) {.async.} =
|
||||||
|
close s
|
||||||
|
|
||||||
template closeNoWait*(sp: AsyncOutputStream|OutputStream) =
|
template closeNoWait*(sp: AsyncOutputStream|OutputStream) =
|
||||||
## Close the stream without waiting even if's async.
|
## 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)
|
addr(c) != addr(c.stream.cursor)
|
||||||
|
|
||||||
proc addPage(s: OutputStream) =
|
proc addPage(s: OutputStream) =
|
||||||
s.span = s.buffers.addWritablePage().writableSpan
|
let
|
||||||
s.spanEndPos += s.span.len
|
nextPageSize = s.buffers.pageSize
|
||||||
|
nextPage = s.buffers.addWritablePage(nextPageSize)
|
||||||
|
s.span = nextPage.fullSpan
|
||||||
|
s.spanEndPos += nextPageSize
|
||||||
|
|
||||||
template makeHandle*(sp: OutputStream): OutputStreamHandle =
|
template makeHandle*(sp: OutputStream): OutputStreamHandle =
|
||||||
let s = sp
|
let s = sp
|
||||||
|
|
@ -144,6 +176,8 @@ template makeHandle*(sp: OutputStream): OutputStreamHandle =
|
||||||
|
|
||||||
proc memoryOutput*(pageSize = defaultPageSize): OutputStreamHandle =
|
proc memoryOutput*(pageSize = defaultPageSize): OutputStreamHandle =
|
||||||
doAssert pageSize > 0
|
doAssert pageSize > 0
|
||||||
|
# We are not creating an initial output page, because `ensureRunway`
|
||||||
|
# can determine the most appropriate size.
|
||||||
makeHandle OutputStream(buffers: initPageBuffers(pageSize))
|
makeHandle OutputStream(buffers: initPageBuffers(pageSize))
|
||||||
|
|
||||||
proc unsafeMemoryOutput*(buffer: pointer, len: Natural): OutputStreamHandle =
|
proc unsafeMemoryOutput*(buffer: pointer, len: Natural): OutputStreamHandle =
|
||||||
|
|
@ -158,22 +192,18 @@ proc ensureRunway*(s: OutputStream, neededRunway: Natural) =
|
||||||
## hint specified at stream creation with `pageSize`.
|
## hint specified at stream creation with `pageSize`.
|
||||||
let runway = s.span.len
|
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 neededRunway > runway:
|
||||||
# If you use an unsafe memory output, you must ensure that
|
# If you use an unsafe memory output, you must ensure that
|
||||||
# it will have a large enough size to hold the data you are
|
# it will have a large enough size to hold the data you are
|
||||||
# feeding to it.
|
# feeding to it.
|
||||||
doAssert s.buffers != nil, "Unsafe memory output of insufficient size"
|
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) =
|
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)
|
writeSync: proc (s: OutputStream, src: pointer, srcLen: Natural)
|
||||||
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
{.nimcall, gcsafe, raises: [IOError, Defect].} =
|
||||||
var file = FileOutputStream(s).file
|
var file = FileOutputStream(s).file
|
||||||
|
|
@ -205,7 +235,7 @@ proc fileOutput*(filename: string,
|
||||||
let f = open(filename, fileMode)
|
let f = open(filename, fileMode)
|
||||||
|
|
||||||
makeHandle FileOutputStream(
|
makeHandle FileOutputStream(
|
||||||
vtable: vtableAddr FileOutputVTable,
|
vtable: vtableAddr fileOutputVTable,
|
||||||
buffers: initPageBuffers(pageSize),
|
buffers: initPageBuffers(pageSize),
|
||||||
file: f)
|
file: f)
|
||||||
|
|
||||||
|
|
@ -215,6 +245,33 @@ proc pos*(s: OutputStream): int =
|
||||||
template pos*(s: AsyncOutputStream): int =
|
template pos*(s: AsyncOutputStream): int =
|
||||||
pos OutputStream(s)
|
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)`
|
# Pre-conditions for `drainAllBuffers(Sync/Async)`
|
||||||
# * The cursor has reached the current span end
|
# * The cursor has reached the current span end
|
||||||
|
|
@ -264,13 +321,13 @@ proc delayFixedSizeWrite*(s: OutputStream, size: Natural): WriteCursor =
|
||||||
runwayDeficit = size - runway
|
runwayDeficit = size - runway
|
||||||
nextPageSize = nextAlignedSize(runwayDeficit, s.buffers.pageSize)
|
nextPageSize = nextAlignedSize(runwayDeficit, s.buffers.pageSize)
|
||||||
nextPage = s.buffers.addWritablePage(nextPageSize)
|
nextPage = s.buffers.addWritablePage(nextPageSize)
|
||||||
nextPageSpan = nextPage.writableSpan
|
nextPageSpan = nextPage.fullSpan
|
||||||
|
|
||||||
s.span = PageSpan(startAddr: offset(nextPageSpan.startAddr, runwayDeficit),
|
s.span = PageSpan(startAddr: offset(nextPageSpan.startAddr, runwayDeficit),
|
||||||
endAddr: nextPageSpan.endAddr)
|
endAddr: nextPageSpan.endAddr)
|
||||||
|
|
||||||
# See the explanation about split cursors above
|
# See the explanation about split cursors above
|
||||||
nextPage.startOffset = -runwayDeficit
|
nextPage.consumedTo = -runwayDeficit
|
||||||
|
|
||||||
s.spanEndPos += nextPageSize
|
s.spanEndPos += nextPageSize
|
||||||
|
|
||||||
|
|
@ -293,12 +350,14 @@ proc delayVarSizeWrite*(s: OutputStream, maxSize: Natural): VarSizeWriteCursor =
|
||||||
s.span.startAddr = endAddr
|
s.span.startAddr = endAddr
|
||||||
|
|
||||||
else:
|
else:
|
||||||
s.buffers.endLastPageAt(s.span.startAddr)
|
|
||||||
let
|
let
|
||||||
nextPageSize = nextAlignedSize(maxSize, s.buffers.pageSize)
|
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)
|
cursorEndAddr = offset(nextPageSpan.startAddr, maxSize)
|
||||||
|
|
||||||
|
nextPage.consumedTo = -maxSize
|
||||||
|
|
||||||
result = VarSizeWriteCursor WriteCursor(
|
result = VarSizeWriteCursor WriteCursor(
|
||||||
stream: s,
|
stream: s,
|
||||||
span: PageSpan(startAddr: nextPageSpan.startAddr,
|
span: PageSpan(startAddr: nextPageSpan.startAddr,
|
||||||
|
|
@ -324,17 +383,17 @@ proc finalWrite*(c: var VarSizeWriteCursor, data: openArray[byte]) =
|
||||||
doAssert overestimatedBytes >= 0
|
doAssert overestimatedBytes >= 0
|
||||||
|
|
||||||
for page in items(cursor.stream.buffers.queue):
|
for page in items(cursor.stream.buffers.queue):
|
||||||
let baseAddr = page.pageBaseAddr
|
let baseAddr = page.allocationStart
|
||||||
if page.pageEndAddr == cursor.span.endAddr:
|
if page.allocationEnd == cursor.span.endAddr:
|
||||||
# This is a page ending cursor
|
# 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)
|
copyMem(cursor.span.startAddr, unsafeAddr data[0], data.len)
|
||||||
finalize cursor
|
finalize cursor
|
||||||
return
|
return
|
||||||
|
|
||||||
if cursor.span.startAddr == baseAddr:
|
if cursor.span.startAddr == baseAddr:
|
||||||
# This is page starting cursor
|
# This is page starting cursor
|
||||||
page.startOffset = overestimatedBytes
|
page.consumedTo = overestimatedBytes
|
||||||
copyMem(offset(baseAddr, overestimatedBytes), unsafeAddr data[0], data.len)
|
copyMem(offset(baseAddr, overestimatedBytes), unsafeAddr data[0], data.len)
|
||||||
finalize cursor
|
finalize cursor
|
||||||
return
|
return
|
||||||
|
|
@ -351,10 +410,10 @@ proc tryMovingToNextPage(c: var WriteCursor) =
|
||||||
# page is big enough to hold all the data. When we created the cursor,
|
# 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
|
# 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
|
# 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
|
# 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.
|
# to 0 to indicate that the cursor has made the flip.
|
||||||
#
|
#
|
||||||
# If you are wondering, var-sized cursors cannot be split, because our
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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.
|
# the cursor is finalized and its final size becomes known.
|
||||||
#
|
#
|
||||||
# If there weren't enough bytes (a much more rare event), we allocate a
|
# 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
|
# 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
|
# 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]
|
var prevPage = c.stream.buffers.queue[0]
|
||||||
for i in 1 ..< c.stream.buffers.queue.len:
|
for i in 1 ..< c.stream.buffers.queue.len:
|
||||||
let page = c.stream.buffers.queue[i]
|
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:
|
# We found what we need, so let's get to business:
|
||||||
c.span.startAddr = page.pageBaseAddr
|
c.span.startAddr = page.allocationStart
|
||||||
c.span.endAddr = offset(c.span.startAddr, -page.startOffset)
|
c.span.endAddr = offset(c.span.startAddr, -page.consumedTo)
|
||||||
page.startOffset = 0
|
page.consumedTo = 0
|
||||||
return
|
return
|
||||||
prevPage = page
|
prevPage = page
|
||||||
|
|
||||||
|
|
@ -390,27 +449,8 @@ proc tryMovingToNextPage(c: var WriteCursor) =
|
||||||
# pre-allocated cursor span, which is considered a Defect (a range error)
|
# pre-allocated cursor span, which is considered a Defect (a range error)
|
||||||
doAssert false, "Attempt to write past the end of a cursor"
|
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) =
|
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
|
# Unsafe memory outputs don't use pages at all, so if our cursor
|
||||||
# reached here, this is a range violation defect:
|
# reached here, this is a range violation defect:
|
||||||
doAssert canExtendOutput(s)
|
doAssert canExtendOutput(s)
|
||||||
|
|
@ -424,12 +464,13 @@ template writeByteImpl(s: OutputStream, b: byte, awaiter, writeOp, drainOp: unty
|
||||||
elif s.buffers == nil:
|
elif s.buffers == nil:
|
||||||
awaiter s.vtable.writeOp(nil, unsafeAddr b, 1)
|
awaiter s.vtable.writeOp(nil, unsafeAddr b, 1)
|
||||||
else:
|
else:
|
||||||
|
trackWrittenToEnd(s.buffers)
|
||||||
awaiter drainOp(s, nil, 0)
|
awaiter drainOp(s, nil, 0)
|
||||||
|
|
||||||
writeByte(s.span, b)
|
writeByte(s.span, b)
|
||||||
|
|
||||||
proc write*(c: var WriteCursor, b: byte) =
|
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
|
# 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
|
# split cursor. If that's the case, the following function will
|
||||||
# succeed. If that's not a split cursor, we'll raise a Defect.
|
# 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) =
|
proc write*(s: OutputStream, b: byte) =
|
||||||
writeByteImpl(s, b, noAwait, writeSync, drainAllBuffersSync)
|
writeByteImpl(s, b, noAwait, writeSync, drainAllBuffersSync)
|
||||||
|
|
||||||
template write*(s: AsyncOutputStream, b: byte) =
|
proc write*(sp: AsyncOutputStream, b: byte) =
|
||||||
# TODO: I should do something with the write async Futures
|
let s = OutputStream sp
|
||||||
bind write
|
if atEnd(s.span):
|
||||||
write OutputStream(s)
|
addPage(s)
|
||||||
|
writeByte(s.span, b)
|
||||||
|
|
||||||
template writeAndWait*(sp: AsyncOutputStream, b: byte) =
|
template writeAndWait*(sp: AsyncOutputStream, b: byte) =
|
||||||
let s = sp
|
let s = sp
|
||||||
writeByteImpl(s, b, fsAwait, writeAsync, drainAllBuffersAsync)
|
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
|
bind write
|
||||||
write s, byte(x)
|
write s, byte(x)
|
||||||
|
|
||||||
|
|
@ -472,8 +517,8 @@ proc writeToANewPage(s: OutputStream, bytes: openArray[byte]) =
|
||||||
let nextPageSize = nextAlignedSize(inputLen, s.buffers.pageSize)
|
let nextPageSize = nextAlignedSize(inputLen, s.buffers.pageSize)
|
||||||
let nextPage = s.buffers.addWritablePage(nextPageSize)
|
let nextPage = s.buffers.addWritablePage(nextPageSize)
|
||||||
|
|
||||||
s.span = nextPage.writableSpan
|
s.span = nextPage.fullSpan
|
||||||
s.spanEndPos += s.span.len
|
s.spanEndPos += nextPageSize
|
||||||
|
|
||||||
copyMem(s.span.startAddr, inputPos, inputLen)
|
copyMem(s.span.startAddr, inputPos, inputLen)
|
||||||
s.span.startAddr = offset(s.span.startAddr, 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:
|
# We'll try to create them as large as possible:
|
||||||
s.writeToANewPage(bytes)
|
s.writeToANewPage(bytes)
|
||||||
else:
|
else:
|
||||||
s.buffers.endLastPageAt(s.span.startAddr)
|
trackWrittenTo(s.buffers, s.span.startAddr)
|
||||||
drainOp
|
drainOp
|
||||||
|
|
||||||
proc write*(s: OutputStream, bytes: openArray[byte]) =
|
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]) =
|
proc write*(s: OutputStream, chars: openArray[char]) =
|
||||||
write s, charsToBytes(chars)
|
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)
|
write s, value.toOpenArrayByte(0, value.len - 1)
|
||||||
|
|
||||||
template memCopyToBytes(value: auto): untyped =
|
template memCopyToBytes(value: auto): untyped =
|
||||||
|
|
@ -519,30 +564,30 @@ proc writeMemCopy*(s: OutputStream, value: auto) =
|
||||||
bind write
|
bind write
|
||||||
write s, memCopyToBytes(value)
|
write s, memCopyToBytes(value)
|
||||||
|
|
||||||
proc writeBytesAsyncImpl(sp: AsyncOutputStream,
|
proc writeBytesAsyncImpl(sp: OutputStream,
|
||||||
bytes: openarray[byte]): Future[void] =
|
bytes: openarray[byte]): Future[void] =
|
||||||
let s = OutputStream(sp)
|
let s = sp
|
||||||
writeBytesImpl(s, bytes):
|
writeBytesImpl(s, bytes):
|
||||||
return s.vtable.writeAsync(s, unsafeAddr bytes[0], bytes.len)
|
return s.vtable.writeAsync(s, unsafeAddr bytes[0], bytes.len)
|
||||||
|
|
||||||
proc writeBytesAsyncImpl(s: AsyncOutputStream,
|
proc writeBytesAsyncImpl(s: OutputStream,
|
||||||
chars: openarray[char]): Future[void] =
|
chars: openarray[char]): Future[void] =
|
||||||
writeBytesAsyncImpl s, charsToBytes(chars)
|
writeBytesAsyncImpl s, charsToBytes(chars)
|
||||||
|
|
||||||
proc writeBytesAsyncImpl(s: AsyncOutputStream,
|
proc writeBytesAsyncImpl(s: OutputStream,
|
||||||
str: string): Future[void] =
|
str: string): Future[void] =
|
||||||
writeBytesAsyncImpl s, toOpenArray(str, 0, str.len - 1)
|
writeBytesAsyncImpl s, toOpenArray(str, 0, str.len - 1)
|
||||||
|
|
||||||
template writeAndWait*(sp: AsyncOutputStream, value: auto) =
|
template writeAndWait*(sp: AsyncOutputStream, value: untyped) =
|
||||||
bind writeBytesAsyncImpl
|
bind writeBytesAsyncImpl
|
||||||
|
|
||||||
let
|
let
|
||||||
s = sp
|
s = OutputStream sp
|
||||||
f = writeBytesAsyncImpl(s, value)
|
f = writeBytesAsyncImpl(s, value)
|
||||||
|
|
||||||
if f != nil:
|
if f != nil:
|
||||||
fsAwait(f)
|
fsAwait(f)
|
||||||
s.span = s.buffers.getWritableSpan()
|
s.span = getWritableSpan s.buffers
|
||||||
s.spanEndPos += s.span.len
|
s.spanEndPos += s.span.len
|
||||||
|
|
||||||
template writeMemCopyAndWait*(sp: AsyncOutputStream, value: auto) =
|
template writeMemCopyAndWait*(sp: AsyncOutputStream, value: auto) =
|
||||||
|
|
@ -601,9 +646,9 @@ template consumeOutputs*(sp: OutputStream, bytesVar, body: untyped) =
|
||||||
let s = sp
|
let s = sp
|
||||||
doAssert s.extCursorsCount == 0 and s.buffers != nil
|
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 =
|
template bytesVar: untyped =
|
||||||
makeOpenArray(pageStartAddr, pageLen)
|
makeOpenArray(pageReadableStart, pageLen)
|
||||||
|
|
||||||
body
|
body
|
||||||
|
|
||||||
|
|
@ -632,16 +677,16 @@ template consumeContiguousOutput*(sp: OutputStream, bytesVar, body: untyped) =
|
||||||
|
|
||||||
if s.buffers.queue.len == 1:
|
if s.buffers.queue.len == 1:
|
||||||
let page = s.buffers.queue[0]
|
let page = s.buffers.queue[0]
|
||||||
bytesPtr = page.pageStartAddr
|
bytesPtr = page.readableStart
|
||||||
bytesLen = page.endOffset - pageStartOffset
|
bytesLen = page.writtenTo - page.consumedTo
|
||||||
# We need to reset the page to an empty state, so it can be reused
|
# We need to reset the page to an empty state, so it can be reused
|
||||||
page.startOffset = 0
|
page.consumedTo = 0
|
||||||
page.endOffset = 0
|
page.writtenTo = 0
|
||||||
else:
|
else:
|
||||||
contigiousBytes = newStringOfCap(s.pos)
|
contigiousBytes = newStringOfCap(s.pos)
|
||||||
|
|
||||||
for pageStartAddr, pageLen in consumePageBuffers(s.buffers):
|
for pageReadableStart, pageLen in consumePageBuffers(s.buffers):
|
||||||
contigiousBytes.add makeOpenArray(cast[ptr char](pageStartAddr), pageLen)
|
contigiousBytes.add makeOpenArray(cast[ptr char](pageReadableStart), pageLen)
|
||||||
|
|
||||||
bytesPtr = addr contigiousBytes[0]
|
bytesPtr = addr contigiousBytes[0]
|
||||||
bytesLen = contigiousBytes.len
|
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.
|
## Before consuming the output, all outstanding delayed writes must be finalized.
|
||||||
##
|
##
|
||||||
doAssert s.extCursorsCount == 0 and s.buffers != nil
|
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:
|
if s.buffers.queue.len == 1:
|
||||||
let page = s.buffers.queue[0]
|
let page = s.buffers.queue[0]
|
||||||
if page.startOffset == 0:
|
if page.consumedTo == 0:
|
||||||
result.swap page.data[]
|
result.swap page.data[]
|
||||||
result.setLen page.endOffset
|
result.setLen page.writtenTo
|
||||||
# We clear the buffers, so the stream will be in pristine state.
|
# We clear the buffers, so the stream will be in pristine state.
|
||||||
# The next write is going to create a fresh new starting page.
|
# The next write is going to create a fresh new starting page.
|
||||||
s.buffers.queue.clear()
|
s.buffers.queue.clear()
|
||||||
|
|
@ -680,3 +725,9 @@ template getOutput*(s: OutputStream, T: type seq[byte]): seq[byte] =
|
||||||
template getOutput*(s: OutputStream): seq[byte] =
|
template getOutput*(s: OutputStream): seq[byte] =
|
||||||
cast[seq[byte]](s.getOutput(string))
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,340 @@
|
||||||
import
|
import
|
||||||
macros,
|
macros,
|
||||||
inputs, outputs
|
inputs, outputs, buffers, async_backend
|
||||||
|
|
||||||
export
|
export
|
||||||
inputs, outputs
|
inputs, outputs, async_backend
|
||||||
|
|
||||||
macro executePipeline*(start: InputStream, steps: varargs[untyped]) =
|
template clearAndWait(ep: AsyncEvent) =
|
||||||
var input = start
|
let e = ep
|
||||||
result = newStmtList()
|
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
|
var
|
||||||
step = steps[i]
|
`inputVar` = `inputVal`
|
||||||
outputVar = genSym(nskVar, "out")
|
`outputVar` = OutputStream `outputVal`
|
||||||
output = if i == steps.len - 2: steps[^1]
|
|
||||||
else: newCall(bindSym"memoryOutput")
|
|
||||||
|
|
||||||
|
`step0`(`inputVar`, `outputVar`)
|
||||||
|
|
||||||
|
if steps.len > 2:
|
||||||
|
let step1 = steps[1]
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
var `outputVar` = `output`
|
let formerInputBuffers = `inputVar`.buffers
|
||||||
`step`(`input`, `outputVar`)
|
`inputVar` = memoryInput(getBuffers `outputVar`)
|
||||||
|
recycleBuffers(`outputVar`, formerInputBuffers)
|
||||||
|
`step1`(`inputVar`, `outputVar`)
|
||||||
|
|
||||||
input = quote do:
|
for i in 2 .. steps.len - 2:
|
||||||
unsafeMemoryInput(getOutput(`outputVar`))
|
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):
|
if defined(debugMacros) or defined(debugPipelines):
|
||||||
echo result.repr
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ const
|
||||||
invalidChar = 255
|
invalidChar = 255
|
||||||
paddingByte = byte('=')
|
paddingByte = byte('=')
|
||||||
|
|
||||||
proc base64encode*(i: InputStream, o: OutputStream) =
|
template encodeSize(size: int): int = (size * 4 div 3) + 6
|
||||||
|
|
||||||
|
import
|
||||||
|
../faststreams/buffers
|
||||||
|
|
||||||
|
proc base64encode*(i: InputStream, o: OutputStream) {.fsMultiSync.} =
|
||||||
var
|
var
|
||||||
n: uint32
|
n: uint32
|
||||||
b: uint32
|
b: uint32
|
||||||
|
|
@ -25,6 +30,10 @@ proc base64encode*(i: InputStream, o: OutputStream) =
|
||||||
template outputChar(x: typed) =
|
template outputChar(x: typed) =
|
||||||
o.write cb64[x and 63]
|
o.write cb64[x and 63]
|
||||||
|
|
||||||
|
let inputLen = i.len
|
||||||
|
if inputLen.isSome:
|
||||||
|
o.ensureRunway encodeSize(inputLen.get)
|
||||||
|
|
||||||
while i.readable(3):
|
while i.readable(3):
|
||||||
inputByte(b shl 16)
|
inputByte(b shl 16)
|
||||||
inputByte(n or b shl 8)
|
inputByte(n or b shl 8)
|
||||||
|
|
@ -48,6 +57,8 @@ proc base64encode*(i: InputStream, o: OutputStream) =
|
||||||
o.write paddingByte
|
o.write paddingByte
|
||||||
o.write paddingByte
|
o.write paddingByte
|
||||||
|
|
||||||
|
close o
|
||||||
|
|
||||||
proc initDecodeTable*(): array[256, char] =
|
proc initDecodeTable*(): array[256, char] =
|
||||||
# computes a decode table at compile time
|
# computes a decode table at compile time
|
||||||
for i in 0 ..< 256:
|
for i in 0 ..< 256:
|
||||||
|
|
@ -63,7 +74,7 @@ proc initDecodeTable*(): array[256, char] =
|
||||||
const
|
const
|
||||||
decodeTable = initDecodeTable()
|
decodeTable = initDecodeTable()
|
||||||
|
|
||||||
proc base64decode*(i: InputStream, o: OutputStream) =
|
proc base64decode*(i: InputStream, o: OutputStream) {.fsMultiSync.} =
|
||||||
proc decodeSize(size: int): int =
|
proc decodeSize(size: int): int =
|
||||||
return (size * 3 div 4) + 6
|
return (size * 3 div 4) + 6
|
||||||
|
|
||||||
|
|
@ -112,3 +123,5 @@ proc base64decode*(i: InputStream, o: OutputStream) =
|
||||||
elif i.readable:
|
elif i.readable:
|
||||||
raise newException(ValueError, "The input stream has insufficient nymber of bytes for base64 decoding")
|
raise newException(ValueError, "The input stream has insufficient nymber of bytes for base64 decoding")
|
||||||
|
|
||||||
|
close o
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,11 @@ procSuite "input stream":
|
||||||
finally:
|
finally:
|
||||||
close input
|
close input
|
||||||
|
|
||||||
|
# TODO: fileInput with offset
|
||||||
|
# - in the middle of the
|
||||||
|
# - right at the end of the file
|
||||||
|
# - past the end of the file
|
||||||
|
|
||||||
asciiTableFileTest "count lines":
|
asciiTableFileTest "count lines":
|
||||||
check countLines(input) == 34
|
check countLines(input) == 34
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,10 +81,7 @@ suite "output stream":
|
||||||
|
|
||||||
template checkOutputsMatch(showResults = false,
|
template checkOutputsMatch(showResults = false,
|
||||||
skipUnbufferedFile = false) =
|
skipUnbufferedFile = false) =
|
||||||
flush fileStream
|
|
||||||
close fileStream
|
close fileStream
|
||||||
|
|
||||||
flush unbufferedFileStream
|
|
||||||
close unbufferedFileStream
|
close unbufferedFileStream
|
||||||
|
|
||||||
check fileExists(fileOutputPath) and
|
check fileExists(fileOutputPath) and
|
||||||
|
|
@ -122,15 +119,15 @@ suite "output stream":
|
||||||
|
|
||||||
let outputsMatch =
|
let outputsMatch =
|
||||||
nimSeq == makeOpenArray(cast[ptr byte](buffer),
|
nimSeq == makeOpenArray(cast[ptr byte](buffer),
|
||||||
streamWritingToExistingBuffer.pos) and
|
streamWritingToExistingBuffer.pos)
|
||||||
nimSeq == memStreamRes and
|
|
||||||
nimSeq == readFileRes and
|
|
||||||
nimSeq == fileInputRes and
|
|
||||||
nimSeq == memFileInputRes and
|
|
||||||
nimSeq == fileInputWithSmallPagesRes
|
|
||||||
|
|
||||||
check outputsMatch
|
check outputsMatch
|
||||||
|
|
||||||
|
check nimSeq == memStreamRes
|
||||||
|
check nimSeq == readFileRes
|
||||||
|
check nimSeq == fileInputRes
|
||||||
|
check nimSeq == memFileInputRes
|
||||||
|
check nimSeq == fileInputWithSmallPagesRes
|
||||||
|
|
||||||
when not skipUnbufferedFile:
|
when not skipUnbufferedFile:
|
||||||
let unbufferedFileRes = readFile(unbufferedFileOutputPath).string.bytes
|
let unbufferedFileRes = readFile(unbufferedFileOutputPath).string.bytes
|
||||||
check nimSeq == unbufferedFileRes
|
check nimSeq == unbufferedFileRes
|
||||||
|
|
@ -236,5 +233,6 @@ suite "output stream":
|
||||||
finalize dw.cursor
|
finalize dw.cursor
|
||||||
|
|
||||||
# The final outputs are the same
|
# The final outputs are the same
|
||||||
check nimSeq == memStream.getOutput
|
let resultsAreEqual = nimSeq == memStream.getOutput
|
||||||
|
check resultsAreEqual
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
{.used.}
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[unittest, strutils, base64],
|
# Std lib:
|
||||||
../faststreams/pipelines,
|
std/[strutils, random, base64, terminal],
|
||||||
|
# Other packages:
|
||||||
|
testutils/unittests,
|
||||||
|
# FastStreams modules:
|
||||||
|
../faststreams/[pipelines, multisync],
|
||||||
|
# Testing modules:
|
||||||
./base64 as fsBase64
|
./base64 as fsBase64
|
||||||
|
|
||||||
include system/timers
|
include system/timers
|
||||||
|
|
@ -13,17 +18,29 @@ type
|
||||||
fsAsyncPipeline: Nanos
|
fsAsyncPipeline: Nanos
|
||||||
stdFunctionCalls: Nanos
|
stdFunctionCalls: Nanos
|
||||||
|
|
||||||
proc upcaseAllCharacters(i: InputStream, o: OutputStream) =
|
proc upcaseAllCharacters(i: InputStream, o: OutputStream) {.fsMultiSync.} =
|
||||||
|
let inputLen = i.len
|
||||||
|
if inputLen.isSome:
|
||||||
|
o.ensureRunway inputLen.get
|
||||||
|
|
||||||
while i.readable:
|
while i.readable:
|
||||||
o.write toUpperAscii(char i.read())
|
o.write toUpperAscii(i.read.char)
|
||||||
|
|
||||||
|
echo "closing upcase"
|
||||||
|
close o
|
||||||
|
|
||||||
|
proc printTimes(t: TestTimes) =
|
||||||
|
styledEcho " cpu time [FS Sync ]: ", styleBright, $t.fsPipeline, "ms"
|
||||||
|
styledEcho " cpu time [FS Async ]: ", styleBright, $t.fsAsyncPipeline, "ms"
|
||||||
|
styledEcho " cpu time [Std Lib ]: ", styleBright, $t.stdFunctionCalls, "ms"
|
||||||
|
|
||||||
template timeit(timerVar: var Nanos, code: untyped) =
|
template timeit(timerVar: var Nanos, code: untyped) =
|
||||||
let t0 = getTicks()
|
let t0 = getTicks()
|
||||||
code
|
code
|
||||||
timerVar = int(getTicks() - t0) div 1000000
|
timerVar = int(getTicks() - t0) div 1000000
|
||||||
|
|
||||||
suite "pipelines":
|
procSuite "pipelines":
|
||||||
var loremIpsum = """
|
let loremIpsum = """
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
||||||
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
|
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
|
||||||
|
|
@ -32,25 +49,83 @@ suite "pipelines":
|
||||||
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
|
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
|
||||||
est laborum.
|
est laborum.
|
||||||
|
|
||||||
""".repeat(100)
|
"""
|
||||||
|
|
||||||
test "upper-case/base64 pipeline":
|
#[
|
||||||
|
test "upper-case/base64 pipeline benchmark":
|
||||||
var
|
var
|
||||||
times: TestTimes
|
times: TestTimes
|
||||||
stdRes: string
|
stdRes: string
|
||||||
fsRes: string
|
fsRes: string
|
||||||
|
fsAsyncRes: string
|
||||||
|
|
||||||
|
let inputText = loremIpsum.repeat(5000)
|
||||||
|
|
||||||
|
when debugHelpers:
|
||||||
|
echo "Input len: ", inputText.len
|
||||||
|
echo "Base 64 len: ", base64.encode(inputText).len
|
||||||
|
|
||||||
timeIt times.fsPipeline:
|
timeIt times.fsPipeline:
|
||||||
var memOut = memoryOutput()
|
fsRes = executePipeline(unsafeMemoryInput(inputText),
|
||||||
executePipeline(unsafeMemoryInput(loremIpsum),
|
upcaseAllCharacters,
|
||||||
upcaseAllCharacters,
|
base64encode,
|
||||||
base64encode,
|
base64decode,
|
||||||
base64decode,
|
getOutput string)
|
||||||
memOut)
|
|
||||||
fsRes = memOut.getOutput(string)
|
timeIt times.fsAsyncPipeline:
|
||||||
|
fsAsyncRes = waitFor executePipeline(Async unsafeMemoryInput(inputText),
|
||||||
|
upcaseAllCharacters,
|
||||||
|
base64encode,
|
||||||
|
base64decode,
|
||||||
|
getOutput string)
|
||||||
|
|
||||||
timeIt times.stdFunctionCalls:
|
timeIt times.stdFunctionCalls:
|
||||||
stdRes = base64.decode(base64.encode(toUpperAscii(loremIpsum)))
|
stdRes = base64.decode(base64.encode(toUpperAscii(inputText)))
|
||||||
|
|
||||||
|
check fsAsyncRes == stdRes
|
||||||
check fsRes == stdRes
|
check fsRes == stdRes
|
||||||
|
|
||||||
|
printTimes times
|
||||||
|
]#
|
||||||
|
|
||||||
|
asyncTest "upper-case/base64 async pipeline":
|
||||||
|
let pipe = asyncPipe()
|
||||||
|
let inputText = repeat(loremIpsum, 8)
|
||||||
|
|
||||||
|
when debugHelpers:
|
||||||
|
echo "Input len: ", inputText.len
|
||||||
|
|
||||||
|
proc pipeFeeder(s: AsyncOutputStream) {.gcsafe, async.} =
|
||||||
|
randomize 1234
|
||||||
|
var pos = 0
|
||||||
|
|
||||||
|
while pos != inputText.len:
|
||||||
|
let bytesToWrite = rand(15)
|
||||||
|
|
||||||
|
if bytesToWrite == 0:
|
||||||
|
s.write inputText[pos]
|
||||||
|
inc pos
|
||||||
|
else:
|
||||||
|
let endPos = min(pos + bytesToWrite, inputText.len)
|
||||||
|
s.writeAndWait inputText[pos ..< endPos]
|
||||||
|
pos = endPos
|
||||||
|
|
||||||
|
let sleep = rand(50) - 45
|
||||||
|
if sleep > 0:
|
||||||
|
echo "written ", pos
|
||||||
|
await sleepAsync(sleep.milliseconds)
|
||||||
|
|
||||||
|
close s
|
||||||
|
|
||||||
|
asyncCheck pipeFeeder(pipe.initWriter)
|
||||||
|
|
||||||
|
let f = executePipeline(pipe.initReader,
|
||||||
|
upcaseAllCharacters,
|
||||||
|
base64encode,
|
||||||
|
base64decode,
|
||||||
|
getOutput string)
|
||||||
|
|
||||||
|
let fsAsyncres = await f
|
||||||
|
|
||||||
|
check fsAsyncRes == toUpperAscii(inputText)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue