Skip to content

Commit bd89772

Browse files
authored
Merge pull request #75 from JuliaData/jps/memory-bloat
poolset: Call GC when free mem is low
2 parents 93ae638 + 7a41006 commit bd89772

File tree

5 files changed

+75
-4
lines changed

5 files changed

+75
-4
lines changed

.github/workflows/test.yml

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ jobs:
1313
strategy:
1414
matrix:
1515
julia-version:
16-
- '~1.7'
1716
- '~1.8'
1817
- '~1.9'
1918
- 'nightly'

Project.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
99
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
1010
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
1111
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
12+
ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63"
1213
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
1314
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
1415

1516
[compat]
1617
DataStructures = "0.18"
17-
julia = "1.7"
18+
ScopedValues = "1"
19+
julia = "1.8"
1820

1921
[extras]
2022
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"

src/MemPool.jl

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using Serialization, Sockets, Random
44
import Serialization: serialize, deserialize
55
export DRef, FileRef, poolset, poolget, mmwrite, mmread, cleanup
66
import .Threads: ReentrantLock
7+
using ScopedValues
78

89
## Wrapping-unwrapping of payloads:
910

@@ -117,6 +118,10 @@ function __init__()
117118
DISKCACHE_CONFIG[] = diskcache_config = DiskCacheConfig()
118119
setup_global_device!(diskcache_config)
119120

121+
if haskey(ENV, "JULIA_MEMPOOL_MEMORY_RESERVED")
122+
MEM_RESERVED[] = parse(UInt, ENV["JULIA_MEMPOOL_MEMORY_RESERVED"])
123+
end
124+
120125
# Ensure we cleanup all references
121126
atexit(exit_hook)
122127
end

src/datastore.jl

+65-1
Original file line numberDiff line numberDiff line change
@@ -203,15 +203,18 @@ end
203203
mutable struct SendQueue
204204
queue::Channel{Any}
205205
@atomic task::Union{Task,Nothing}
206+
processing::Bool
206207
end
207-
const SEND_QUEUE = SendQueue(Channel(typemax(Int)), nothing)
208+
const SEND_QUEUE = SendQueue(Channel(typemax(Int)), nothing, false)
208209
function _enqueue_work(f, args...; gc_context=false)
209210
if SEND_QUEUE.task === nothing
210211
task = Task() do
211212
while true
212213
try
213214
work, _args = take!(SEND_QUEUE.queue)
215+
SEND_QUEUE.processing = true
214216
work(_args...)
217+
SEND_QUEUE.processing = false
215218
catch err
216219
exit_flag[] && continue
217220
err isa ProcessExitedException && continue # TODO: Remove proc from counters
@@ -348,12 +351,73 @@ isondisk(id::Int) =
348351
isinmemory(x::DRef) = isinmemory(x.id)
349352
isondisk(x::DRef) = isondisk(x.id)
350353

354+
const MEM_RESERVED = Ref{UInt}(512 * (1024^2)) # Reserve 512MB of RAM for OS
355+
const MEM_RESERVE_LOCK = Threads.ReentrantLock()
356+
357+
"""
358+
When called, ensures that at least `MEM_RESERVED[] + size` bytes are available
359+
to the OS. If there is not enough memory available, then a variety of calls to
360+
the GC are performed to free up memory until either the reservation limit is
361+
satisfied, or `max_sweeps` number of cycles have elapsed.
362+
"""
363+
function ensure_memory_reserved(size::Integer=0; max_sweeps::Integer=5)
364+
sat_sub(x::T, y::T) where T = x < y ? zero(T) : x-y
365+
366+
# Check whether the OS is running tight on memory
367+
sweep_ctr = 0
368+
while true
369+
with(QUERY_MEM_OVERRIDE => true) do
370+
Int(storage_available(CPURAMResource())) - size < MEM_RESERVED[]
371+
end || break
372+
373+
# We need more memory! Let's encourage the GC to clear some memory...
374+
sweep_start = time_ns()
375+
mem_used = with(QUERY_MEM_OVERRIDE => true) do
376+
storage_utilized(CPURAMResource())
377+
end
378+
if sweep_ctr == 0
379+
@debug "Not enough memory to continue! Sweeping up unused memory..."
380+
GC.gc(false)
381+
elseif sweep_ctr == 1
382+
GC.gc(true)
383+
else
384+
@everywhere GC.gc(true)
385+
end
386+
387+
# Let finalizers run
388+
yield()
389+
390+
# Wait for send queue to clear
391+
while SEND_QUEUE.processing
392+
yield()
393+
end
394+
395+
with(QUERY_MEM_OVERRIDE => true) do
396+
mem_freed = sat_sub(mem_used, storage_utilized(CPURAMResource()))
397+
@debug "Freed $(Base.format_bytes(mem_freed)) bytes, available: $(Base.format_bytes(storage_available(CPURAMResource())))"
398+
end
399+
400+
sweep_ctr += 1
401+
if sweep_ctr == max_sweeps
402+
@debug "Made too many sweeps, bailing out..."
403+
break
404+
end
405+
end
406+
if sweep_ctr > 0
407+
@debug "Swept for $sweep_ctr cycles"
408+
end
409+
end
410+
351411
function poolset(@nospecialize(x), pid=myid(); size=approx_size(x),
352412
retain=false, restore=false,
353413
device=GLOBAL_DEVICE[], leaf_device=initial_leaf_device(device),
354414
tag=nothing, leaf_tag=Tag(),
355415
destructor=nothing)
356416
if pid == myid()
417+
if !restore
418+
@lock MEM_RESERVE_LOCK ensure_memory_reserved(size)
419+
end
420+
357421
id = atomic_add!(id_counter, 1)
358422
sstate = if !restore
359423
StorageState(Some{Any}(x),

src/storage.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ QueriedMemInfo() = QueriedMemInfo(UInt64(0), UInt64(0))
186186
const QUERY_MEM_AVAILABLE = Ref(QueriedMemInfo())
187187
const QUERY_MEM_CAPACITY = Ref(QueriedMemInfo())
188188
const QUERY_MEM_PERIOD = 10 * 1000^2 # 10ms
189+
const QUERY_MEM_OVERRIDE = ScopedValue(false)
189190
function _query_mem_periodically(kind::Symbol)
190191
if !(kind in (:available, :capacity))
191192
throw(ArgumentError("Invalid memory query kind: $kind"))
@@ -197,7 +198,7 @@ function _query_mem_periodically(kind::Symbol)
197198
end
198199
mem_info = mem_bin[]
199200
now_ns = time_ns()
200-
if mem_info.last_ns < now_ns - QUERY_MEM_PERIOD
201+
if QUERY_MEM_OVERRIDE[] || mem_info.last_ns < now_ns - QUERY_MEM_PERIOD
201202
if kind == :available
202203
new_mem_info = QueriedMemInfo(free_memory(), now_ns)
203204
elseif kind == :capacity

0 commit comments

Comments
 (0)