Skip to content

Commit cdd1c9f

Browse files
authored
Merge pull request #557 from avinxshKD/julia-mmap-backend
concore.jl: add mmap backend
2 parents df783b6 + 99bf29a commit cdd1c9f

3 files changed

Lines changed: 379 additions & 6 deletions

File tree

concore.jl

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
module Concore
1515

16+
using Mmap
17+
1618
# -----------------------------------------------------------------------------
1719
# Backend selection
1820
# -----------------------------------------------------------------------------
@@ -23,8 +25,19 @@ abstract type AbstractBackend end
2325
"""Default local file transport backend."""
2426
struct FileBackend <: AbstractBackend end
2527

28+
"""Memory-mapped local file transport backend."""
29+
struct MmapBackend <: AbstractBackend
30+
segment_size::Int
31+
32+
function MmapBackend(segment_size::Int = 4096)
33+
segment_size > 1 || throw(ArgumentError("mmap segment size must be greater than 1"))
34+
new(segment_size)
35+
end
36+
end
37+
2638
# Compatibility name for proposal/docs wording.
2739
const FileTransport = FileBackend
40+
const MmapTransport = MmapBackend
2841

2942
# -----------------------------------------------------------------------------
3043
# Path configuration
@@ -82,10 +95,146 @@ const _S_MAX_LEN = 65_536
8295

8396
_backend_inpath(::FileBackend) = inpath
8497
_backend_outpath(::FileBackend) = outpath
98+
_backend_inpath(::MmapBackend) = inpath
99+
_backend_outpath(::MmapBackend) = outpath
85100

86101
_input_dir(port::Int) = _backend_inpath(_backend) * string(port)
87102
_output_dir(port::Int) = _backend_outpath(_backend) * string(port)
88103

104+
const _mmap_segments = Dict{String, Tuple{IOStream, Vector{UInt8}}}()
105+
const _mmap_cleanup_registered = Ref(false)
106+
107+
function _register_mmap_cleanup()
108+
if !_mmap_cleanup_registered[]
109+
atexit(mmap_cleanup)
110+
_mmap_cleanup_registered[] = true
111+
end
112+
end
113+
114+
function _close_mmap_segment(path::AbstractString)
115+
segment = pop!(_mmap_segments, String(path), nothing)
116+
segment === nothing && return nothing
117+
118+
io, buf = segment
119+
try
120+
Mmap.sync!(buf)
121+
catch
122+
end
123+
try
124+
finalize(buf)
125+
catch
126+
end
127+
try
128+
isopen(io) && close(io)
129+
catch
130+
end
131+
GC.gc()
132+
return nothing
133+
end
134+
135+
function _mmap_segment(path::AbstractString, size::Int)::Vector{UInt8}
136+
segment = get(_mmap_segments, String(path), nothing)
137+
if segment !== nothing && isopen(segment[1]) && length(segment[2]) >= size
138+
return segment[2]
139+
end
140+
if segment !== nothing
141+
_close_mmap_segment(path)
142+
end
143+
144+
_register_mmap_cleanup()
145+
mkpath(dirname(path))
146+
147+
io = open(path, isfile(path) ? "r+" : "w+")
148+
if filesize(path) < size
149+
seek(io, size - 1)
150+
write(io, UInt8(0))
151+
seekstart(io)
152+
end
153+
154+
buf = try
155+
Mmap.mmap(io, Vector{UInt8}, size)
156+
catch
157+
close(io)
158+
rethrow()
159+
end
160+
_mmap_segments[String(path)] = (io, buf)
161+
return buf
162+
end
163+
164+
function _mmap_content(buf::Vector{UInt8})::String
165+
nullpos = findfirst(iszero, buf)
166+
last = nullpos === nothing ? length(buf) : nullpos - 1
167+
last <= 0 && return ""
168+
return String(copy(buf[1:last]))
169+
end
170+
171+
function _mmap_read(path::AbstractString, initstr::AbstractString, size::Int)::String
172+
isfile(path) || return String(initstr)
173+
buf = _mmap_segment(path, size)
174+
ins = _mmap_content(buf)
175+
return isempty(ins) ? String(initstr) : ins
176+
end
177+
178+
function _mmap_write(path::AbstractString, wire::AbstractString, size::Int)
179+
bytes = Vector{UInt8}(wire)
180+
if length(bytes) >= size
181+
throw(ArgumentError("mmap payload exceeds segment size"))
182+
end
183+
184+
buf = _mmap_segment(path, size)
185+
n = length(bytes)
186+
buf[1:n] .= bytes
187+
buf[n + 1] = 0x00
188+
if n + 2 <= size
189+
buf[n + 2:size] .= 0x00
190+
end
191+
Mmap.sync!(buf)
192+
return nothing
193+
end
194+
195+
function _write_wire_file(path::AbstractString, wire::AbstractString)
196+
segment = get(_mmap_segments, String(path), nothing)
197+
if segment !== nothing && isopen(segment[1])
198+
return _mmap_write(path, wire, length(segment[2]))
199+
end
200+
201+
open(path, "w") do f
202+
write(f, wire)
203+
end
204+
return nothing
205+
end
206+
207+
"""Close open memory-mapped segment handles."""
208+
function mmap_cleanup()
209+
segments = collect(values(_mmap_segments))
210+
empty!(_mmap_segments)
211+
212+
for (io, buf) in segments
213+
try
214+
Mmap.sync!(buf)
215+
catch
216+
end
217+
try
218+
finalize(buf)
219+
catch
220+
end
221+
try
222+
isopen(io) && close(io)
223+
catch
224+
end
225+
end
226+
227+
segments = nothing
228+
GC.gc()
229+
return nothing
230+
end
231+
232+
function _wire_content(raw::AbstractString)::String
233+
nullpos = findfirst(==('\0'), raw)
234+
nullpos === nothing && return String(raw)
235+
return String(raw[1:nullpos - 1])
236+
end
237+
89238
function _port_file_path(base::AbstractString, port::Int, name::AbstractString)::String
90239
port_dir = abspath(base * string(port))
91240
filepath = abspath(joinpath(port_dir, String(name)))
@@ -312,7 +461,11 @@ function concore_read(
312461

313462
ins = ""
314463
try
315-
ins = read(filepath, String)
464+
if _backend isa MmapBackend
465+
ins = _mmap_read(filepath, initstr, _backend.segment_size)
466+
else
467+
ins = _wire_content(read(filepath, String))
468+
end
316469
catch
317470
ins = initstr
318471
end
@@ -322,7 +475,11 @@ function concore_read(
322475
while isempty(ins) && attempts < 5
323476
sleep(delay)
324477
try
325-
ins = read(filepath, String)
478+
if _backend isa MmapBackend
479+
ins = _mmap_read(filepath, initstr, _backend.segment_size)
480+
else
481+
ins = _wire_content(read(filepath, String))
482+
end
326483
catch
327484
end
328485
attempts += 1
@@ -359,8 +516,10 @@ function concore_write(
359516
outval = Float64[simtime + Float64(delta); Float64.(val)]
360517
wire = _format_wire(outval)
361518

362-
open(filepath, "w") do f
363-
write(f, wire)
519+
if _backend isa MmapBackend
520+
_mmap_write(filepath, wire, _backend.segment_size)
521+
else
522+
_write_wire_file(filepath, wire)
364523
end
365524

366525
return nothing
@@ -380,8 +539,10 @@ function concore_write(
380539
sleep(2 * delay)
381540
filepath = _port_file_path(_backend_outpath(_backend), port, name)
382541
mkpath(dirname(filepath))
383-
open(filepath, "w") do f
384-
write(f, val)
542+
if _backend isa MmapBackend
543+
_mmap_write(filepath, val, _backend.segment_size)
544+
else
545+
_write_wire_file(filepath, val)
385546
end
386547
return nothing
387548
end
@@ -432,6 +593,13 @@ function concore_init!()
432593
return nothing
433594
end
434595

596+
function concore_init!(backend::AbstractBackend)
597+
global _backend
598+
_backend = backend
599+
concore_init!()
600+
return nothing
601+
end
602+
435603
# Backward-compatible aliases (without !)
436604
const load_iport = load_iport!
437605
const load_oport = load_oport!
@@ -448,6 +616,7 @@ export tryparam, default_maxtime!, safe_parse_list
448616
export load_iport!, load_oport!, load_params!, concore_init!
449617
export load_iport, load_oport, load_params, default_maxtime, concore_init
450618
export AbstractBackend, FileBackend, FileTransport
619+
export MmapBackend, MmapTransport, mmap_cleanup
451620

452621
# -----------------------------------------------------------------------------
453622
# Auto-initialize on load

tests/julia/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ using .Concore
1010
include("test_protocol.jl")
1111
include("test_wire_compat.jl")
1212
include("test_interop.jl")
13+
include("test_mmap.jl")
1314
end

0 commit comments

Comments
 (0)