From e9fab94492a2c04b946a3c3e0673ba3c9b58900f Mon Sep 17 00:00:00 2001 From: Stefan Krastanov Date: Wed, 2 Aug 2023 20:47:14 -0400 Subject: [PATCH 1/2] implement TrackedResource --- CHANGELOG.md | 4 ++ Project.toml | 4 +- docs/Project.toml | 1 + src/ConcurrentSim.jl | 2 + src/resources/tracked.jl | 101 +++++++++++++++++++++++++++++++++++++++ test/Project.toml | 1 + 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100755 src/resources/tracked.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5028d..e59ae5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # News +## v1.2.0 - 2023-08-03 + +- Rudimentary utilization tracker for resources is now implemented as the `TrackedResource` wrapper, together with a converter `DataFrame(::TrackedResource)` (thanks to implementing the `Tables.jl` API). + ## v1.1.0 - 2023-08-02 - Start using `Base`'s API: `Base.unlock`, `Base.islocked`, `Base.isready`, `Base.put!`, `Base.take!`. Deprecate `put`, `release`. Moreover, consider using `Base.take!` instead of `Base.get` (which was not deprecated yet, as we decide which semantics to follow). Lastly, `Base.lock` and `Base.trylock` are **not** implement -- they are superficially similar to `request` and `tryrequest`, but have to be explicitly `@yield`-ed. diff --git a/Project.toml b/Project.toml index 2fcce6e..1bc54dd 100644 --- a/Project.toml +++ b/Project.toml @@ -5,14 +5,16 @@ license = "MIT" desc = "A discrete event process oriented simulation framework." authors = ["Ben Lauwens and SimJulia and ConcurrentSim contributors"] repo = "https://github.com/JuliaDynamics/ConcurrentSim.jl.git" -version = "1.1.0" +version = "1.2.0" [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" ResumableFunctions = "c5292f4c-5179-55e1-98c5-05642aab7184" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] DataStructures = "0.18" ResumableFunctions = "0.6" +Tables = "1.10.1" julia = "1.6" diff --git a/docs/Project.toml b/docs/Project.toml index 743406f..c2ee50c 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,6 @@ [deps] ConcurrentSim = "6ed1e86c-fcaf-46a9-97e0-2b26a2cdb499" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" ResumableFunctions = "c5292f4c-5179-55e1-98c5-05642aab7184" diff --git a/src/ConcurrentSim.jl b/src/ConcurrentSim.jl index d0b73d3..15c518d 100755 --- a/src/ConcurrentSim.jl +++ b/src/ConcurrentSim.jl @@ -18,6 +18,7 @@ module ConcurrentSim export AbstractProcess, Simulation, run, now, active_process, StopSimulation export Process, @process, interrupt export Container, Resource, Store, put!, get, cancel, request, tryrequest + export TrackedResource export nowDatetime include("base.jl") @@ -28,6 +29,7 @@ module ConcurrentSim include("resources/base.jl") include("resources/containers.jl") include("resources/stores.jl") + include("resources/tracked.jl") include("utils/time.jl") include("deprecated.jl") end diff --git a/src/resources/tracked.jl b/src/resources/tracked.jl new file mode 100755 index 0000000..5953d29 --- /dev/null +++ b/src/resources/tracked.jl @@ -0,0 +1,101 @@ +import Tables + +""" +A wrapper around a resource that tracks the time at which the resource is interacted with. + +```jldoctest +julia> using ConcurrentSim, DataFrames + +julia> env = Simulation(); r = TrackedResource(Resource(env)); + +julia> run(env, 1); now(env) +1.0 + +julia> request(r); run(env, 2); now(env) +2.0 + +julia> request(r); run(env, 3); unlock(r); run(env, 4); now(env) +4.0 + +julia> DataFrame(r) +3×2 DataFrame + Row │ events times + │ Symbol Float64 +─────┼─────────────────── + 1 │ increase 1.0 + 2 │ increase 2.0 + 3 │ decrease 3.0 +``` +""" +struct TrackedResource{T} + resource::T + events::Vector{Symbol} + times::Vector{Float64} +end + +TrackedResource(resource) = TrackedResource(resource, Symbol[], Float64[]) + +function Base.take!(tr::TrackedResource, args...; kwargs...) + r = take!(tr.resource, args...; kwargs...) + push!(tr.events, :decrease) + push!(tr.times, now(tr.resource.env)) + return r +end + +function Base.put!(tr::TrackedResource, args...; kwargs...) + r = put!(tr.resource, args...; kwargs...) + push!(tr.events, :increase) + push!(tr.times, now(tr.resource.env)) + return r +end + +function Base.unlock(tr::TrackedResource, args...; kwargs...) + r = unlock(tr.resource, args...; kwargs...) + push!(tr.events, :decrease) + push!(tr.times, now(tr.resource.env)) + return r +end + +function request(tr::TrackedResource, args...; kwargs...) + r = request(tr.resource, args...; kwargs...) + push!(tr.events, :increase) + push!(tr.times, now(tr.resource.env)) + return r +end + +## +# Tables interface +## + +Tables.istable(::Type{<:TrackedResource}) = true +Tables.schema(tr::TrackedResource) = Tables.Schema(Tables.columnnames(tr), [Symbol, Float64]) + +Tables.columnaccess(::Type{<:TrackedResource}) = true + +Tables.columns(tr::TrackedResource) = tr + +function Tables.getcolumn(tr::TrackedResource, i::Int) + if i == 1 + return tr.events + elseif i == 2 + return tr.times + else + error("`TrackedResource` has only two columns, but you are attempting to access column $(i).") + end +end + +function Tables.getcolumn(tr::TrackedResource, s::Symbol) + if s == :events + return tr.events + elseif s == :times + return tr.times + else + error("`TrackedResource` has only two columns (events and times), but you are attempting to access column $(s).") + end +end + +function Tables.columnnames(tr::TrackedResource) + return (:events, :times) +end + +# TODO Makie recipes diff --git a/test/Project.toml b/test/Project.toml index 6c376a1..0e6d785 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,6 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" From a5f209f6b458698f4c20b982a817283c397dae42 Mon Sep 17 00:00:00 2001 From: Stefan Krastanov Date: Wed, 2 Aug 2023 21:00:46 -0400 Subject: [PATCH 2/2] more tests --- test/Project.toml | 1 + test/runtests.jl | 1 + test/test_resources_tracked.jl | 42 ++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100755 test/test_resources_tracked.jl diff --git a/test/Project.toml b/test/Project.toml index 0e6d785..53160d9 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -10,4 +10,5 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ResumableFunctions = "c5292f4c-5179-55e1-98c5-05642aab7184" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/runtests.jl b/test/runtests.jl index 2c53e56..e8c1509 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -35,6 +35,7 @@ println("Starting tests with $(Threads.nthreads()) threads out of `Sys.CPU_THREA @doset "resources_containers_deprecated" @doset "resources_stores" @doset "resources_stores_deprecated" +@doset "resources_tracked" @doset "utils_time" VERSION >= v"1.9" && @doset "doctests" VERSION >= v"1.9" && @doset "aqua" diff --git a/test/test_resources_tracked.jl b/test/test_resources_tracked.jl new file mode 100755 index 0000000..adb142c --- /dev/null +++ b/test/test_resources_tracked.jl @@ -0,0 +1,42 @@ +using ConcurrentSim +using ResumableFunctions +using Test +using DataFrames, Tables + +struct StoreObject + i :: Int +end + +@resumable function my_consumer(sim::Simulation, sto) + for j in 1:10 + @yield timeout(sim, rand()) + println("$(now(sim)), consumer is demanding object") + obj = @yield take!(sto) + println("$(now(sim)), consumer is being served with object ", obj.i) + end +end + +@resumable function my_producer(sim::Simulation, sto) + for j in 1:10 + println("$(now(sim)), producer is offering object $j") + @yield put!(sto, StoreObject(j)) + println("$(now(sim)), producer is being served") + @yield timeout(sim, 2*rand()) + end +end + +sim = Simulation() +sto = TrackedResource(Store{StoreObject}(sim)) +@process my_consumer(sim, sto) +@process my_producer(sim, sto) +run(sim) + +df = DataFrame(sto) +@test df[!,:events] == Tables.getcolumn(sto, 1) +@test df[!,:times] == Tables.getcolumn(sto, 2) + +@test_throws ErrorException Tables.getcolumn(sto, 3) +@test_throws ErrorException Tables.getcolumn(sto, :lala) + +@test Tables.columnaccess(typeof(sto)) +Tables.schema(sto)