Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented customizable growth rate to string streams #17977

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 23 additions & 12 deletions lib/pure/streams.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1125,23 +1125,25 @@ type
data*: string ## A string data.
## This is updated when called `writeLine` etc.
pos: int
endPos: int # Where the buffer is ended, only moves right never left
growthRate: int # User defineable growthrate to allow allocations change

when (NimMajor, NimMinor) < (1, 3) and defined(js):
proc ssAtEnd(s: Stream): bool {.compileTime.} =
var s = StringStream(s)
return s.pos >= s.data.len
return s.pos >= s.endPos

proc ssSetPosition(s: Stream, pos: int) {.compileTime.} =
var s = StringStream(s)
s.pos = clamp(pos, 0, s.data.len)
s.pos = clamp(pos, 0, s.endPos)

proc ssGetPosition(s: Stream): int {.compileTime.} =
var s = StringStream(s)
return s.pos

proc ssReadDataStr(s: Stream, buffer: var string, slice: Slice[int]): int {.compileTime.} =
var s = StringStream(s)
result = min(slice.b + 1 - slice.a, s.data.len - s.pos)
result = min(slice.b + 1 - slice.a, s.endPos - s.pos)
if result > 0:
buffer[slice.a..<slice.a+result] = s.data[s.pos..<s.pos+result]
inc(s.pos, result)
Expand All @@ -1152,9 +1154,11 @@ when (NimMajor, NimMinor) < (1, 3) and defined(js):
var s = StringStream(s)
s.data = ""

proc newStringStream*(s: string = ""): owned StringStream {.compileTime.} =
proc newStringStream*(s: string = "", growthRate = 0): owned StringStream {.compileTime.} =
new(result)
result.data = s
result.endPos = s.len
result.growthRate = growthRate
result.pos = 0
result.closeImpl = ssClose
result.atEndImpl = ssAtEnd
Expand All @@ -1179,19 +1183,19 @@ when (NimMajor, NimMinor) < (1, 3) and defined(js):
else: # after 1.3 or JS not defined
proc ssAtEnd(s: Stream): bool =
var s = StringStream(s)
return s.pos >= s.data.len
return s.pos >= s.endPos

proc ssSetPosition(s: Stream, pos: int) =
var s = StringStream(s)
s.pos = clamp(pos, 0, s.data.len)
s.pos = clamp(pos, 0, s.endPos)

proc ssGetPosition(s: Stream): int =
var s = StringStream(s)
return s.pos

proc ssReadDataStr(s: Stream, buffer: var string, slice: Slice[int]): int =
var s = StringStream(s)
result = min(slice.b + 1 - slice.a, s.data.len - s.pos)
result = min(slice.b + 1 - slice.a, s.endPos - s.pos)
if result > 0:
jsOrVmBlock:
buffer[slice.a..<slice.a+result] = s.data[s.pos..<s.pos+result]
Expand All @@ -1203,7 +1207,7 @@ else: # after 1.3 or JS not defined

proc ssReadData(s: Stream, buffer: pointer, bufLen: int): int =
var s = StringStream(s)
result = min(bufLen, s.data.len - s.pos)
result = min(bufLen, s.endPos - s.pos)
if result > 0:
when defined(js):
try:
Expand All @@ -1219,7 +1223,7 @@ else: # after 1.3 or JS not defined

proc ssPeekData(s: Stream, buffer: pointer, bufLen: int): int =
var s = StringStream(s)
result = min(bufLen, s.data.len - s.pos)
result = min(bufLen, s.endPos - s.pos)
if result > 0:
when defined(js):
try:
Expand All @@ -1237,7 +1241,7 @@ else: # after 1.3 or JS not defined
if bufLen <= 0:
return
if s.pos + bufLen > s.data.len:
setLen(s.data, s.pos + bufLen)
setLen(s.data, max((s.data.len + 1) * s.growthRate, s.pos + bufLen)) # 0 * 2 == 0 so we offset left by one
when defined(js):
try:
s.data[s.pos..<s.pos+bufLen] = cast[ptr string](buffer)[][0..<bufLen]
Expand All @@ -1247,14 +1251,19 @@ else: # after 1.3 or JS not defined
elif not defined(nimscript):
copyMem(addr(s.data[s.pos]), buffer, bufLen)
inc(s.pos, bufLen)
if s.pos > s.endPos: # When we do `s.setPosition` dont want to move end pos until it's reached
inc(s.endPos, bufLen)

proc ssClose(s: Stream) =
var s = StringStream(s)
s.data = ""

proc newStringStream*(s: sink string = ""): owned StringStream =
proc newStringStream*(s: sink string = "", growthRate = 0): owned StringStream =
## Creates a new stream from the string `s`.
##
##
## If `growthRate` is less than or equal to `1` the internal data is sized exactly,
## otherwise it grows by the provided rate everytime it needs to grow saving allocations.
## In the case data written is larger than the growth rate it's sized exactly regardless.
## See also:
## * `newFileStream proc <#newFileStream,File>`_ creates a file stream from
## opened File.
Expand All @@ -1272,6 +1281,8 @@ else: # after 1.3 or JS not defined
new(result)
result.data = s
result.pos = 0
result.endPos = s.len
result.growthRate = growthRate
result.closeImpl = ssClose
result.atEndImpl = ssAtEnd
result.setPositionImpl = ssSetPosition
Expand Down
71 changes: 71 additions & 0 deletions tests/benchmarks/tstringstreams.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
discard """
action: compile
"""
#[
nimble install benchy
nim r -d:danger -d:lto tests/benchmarks/stringstreams.nim
]#

import pkg/benchy
import std/streams

type
PacketID = enum
jump, shoot, crouch, hit
Entity = object
id: int

proc writePacket(str: StringStream, kind: PacketID, ent: Entity, target: Entity = Entity(id: -1)) =
str.write kind
str.write ent.id
case kind:
of shoot:
str.write 10
str.write target.id
of hit:
str.write 3
else: discard

# Benchmark using string streams as a network buffer shows ~`0.005ms` improvement in this small example
timeIt "Normal growth rate Network", 1000:
var s = newStringStream()
for i in 0..1000:
let event = i.mod(PacketID.high.ord).PacketID
if event == shoot:
s.writePacket(event, Entity(id: i), Entity(id: i + 1))
else:
s.writePacket(event, Entity(id: i))
keep s.data

timeIt "Twice growth rate Network", 1000:
var s = newStringStream(growthRate = 2)
for i in 0..1000:
let event = i.mod(PacketID.high.ord).PacketID
if event == shoot:
s.writePacket(event, Entity(id: i), Entity(id: i + 1))
else:
s.writePacket(event, Entity(id: i))
keep s.data


let data = newSeq[(byte, byte, byte)](300 * 500)

# Basic pseudo image example using a header and data
# Results on my machine shows constant 2 times growth is roughly ~`.5ms` faster
timeIt "Normal growth rate image saving", 1000:
var s = newStringStream()
s.write("Width:300, Height:500") # Simulated header format
for x in data:
s.write(x)

timeIt "Twice growth rate image saving", 1000:
var s = newStringStream(growthRate = 2)
s.write("Width:300, Height:500") # Simulated header format
for x in data:
s.write(x)

timeIt "Four times growth rate image saving", 1000:
var s = newStringStream(growthRate = 3)
s.write("Width:300, Height:500") # Simulated header format
for x in data:
s.write(x)