From 66ef0834ff54faaacc6d9d460d7ba9b14ca67aae Mon Sep 17 00:00:00 2001 From: Alex Arslan Date: Thu, 19 Oct 2017 17:11:12 -0700 Subject: [PATCH] Migrate serialization format to JSON --- REQUIRE | 2 +- src/BenchmarkTools.jl | 12 +--- src/serialization.jl | 143 +++++++++++++++++++++++++++++-------- test/SerializationTests.jl | 66 +++++++++++++++-- test/data_pre_v006.jld | Bin 1155653 -> 0 bytes 5 files changed, 178 insertions(+), 45 deletions(-) delete mode 100644 test/data_pre_v006.jld diff --git a/REQUIRE b/REQUIRE index 001b7ddd..49bc36ea 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,3 +1,3 @@ julia 0.6 Compat 0.26 -JLD 0.6.6 +JSON diff --git a/src/BenchmarkTools.jl b/src/BenchmarkTools.jl index 028b66b3..210c99ea 100644 --- a/src/BenchmarkTools.jl +++ b/src/BenchmarkTools.jl @@ -1,16 +1,8 @@ module BenchmarkTools using Compat -import JLD - -# `show` compatibility for pre-JuliaLang/julia#16354 builds -if VERSION < v"0.5.0-dev+4305" - Base.get(io::IO, setting::Symbol, default::Bool) = default -end - -if VERSION >= v"0.6.0-dev.1015" - using Base.Iterators -end +using JSON +using Base.Iterators const BENCHMARKTOOLS_VERSION = v"0.0.6" diff --git a/src/serialization.jl b/src/serialization.jl index 526bb145..80851fd2 100644 --- a/src/serialization.jl +++ b/src/serialization.jl @@ -1,6 +1,16 @@ -const VERSION_KEY = "__versions__" +const VERSIONS = Dict("Julia" => string(VERSION), + "BenchmarkTools" => string(BENCHMARKTOOLS_VERSION)) -const VERSIONS = Dict("Julia" => string(VERSION), "BenchmarkTools" => string(BENCHMARKTOOLS_VERSION)) +# TODO: Add any new types as they're added +const supported_types = [Benchmark, BenchmarkGroup, Parameters, TagFilter, Trial, + TrialEstimate, TrialJudgement, TrialRatio] + +for T in supported_types + @eval function JSON.lower(x::$T) + S = typeof(x) # T could be a UnionAll + [string(S), Dict(fieldname($T, i) => getfield(x, i) for i = 1:fieldcount($T))] + end +end mutable struct ParametersPreV006 seconds::Float64 @@ -21,36 +31,113 @@ mutable struct TrialPreV006 allocs::Int end -function JLD.readas(p::ParametersPreV006) - return Parameters(p.seconds, p.samples, p.evals, Float64(p.overhead), p.gctrial, - p.gcsample, p.time_tolerance, p.memory_tolerance) +function recover(x::Vector) + length(x) == 2 || throw(ArgumentError("Expecting a vector of length 2")) + typename = x[1]::String + fields = x[2]::Dict + T = eval(parse(typename))::Type + fc = fieldcount(T) + xs = Vector{Any}(fc) + for i = 1:fc + ft = fieldtype(T, i) + fn = String(fieldname(T, i)) + xs[i] = if ft in supported_types + recover(fields[fn]) + else + convert(ft, fields[fn]) + end + end + T(xs...) end -function JLD.readas(t::TrialPreV006) - new_times = convert(Vector{Float64}, t.times) - new_gctimes = convert(Vector{Float64}, t.gctimes) - return Trial(t.params, new_times, new_gctimes, t.memory, t.allocs) +function save(filename::AbstractString, args...) + isempty(args) && throw(ArgumentError("Nothing to save")) + if !endswith(filename, ".json") + noext, ext = splitext(filename) + msg = if ext == ".jld" + "JLD serialization is no longer supported. Benchmarks should now be saved\n" * + "in JSON format using `save(\"$noext\".json, args...)`. You will need to\n" * + "convert existing saved benchmarks to JSON in order to deserialize them with\n" * + "this version of BenchmarkTools." + else + "Only JSON serialization is supported. Use `save(\"$noext.json\", args...)`." + end + throw(ArgumentError(msg)) + end + goodargs = Any[] + for arg in args + if arg isa String + warn("Naming variables in serialization is no longer supported.\nThe name " * + "will be ignored and the object will be serialized in the order it appears " * + "in the input.") + continue + elseif !any(T->arg isa T, supported_types) + throw(ArgumentError("Only BenchmarkTools types can be serialized.")) + end + push!(goodargs, arg) + end + isempty(goodargs) && error("Nothing to save") + open(filename, "w") do io + JSON.print(io, [VERSIONS, goodargs]) + end end -function save(filename, args...) - JLD.save(filename, VERSION_KEY, VERSIONS, args...) - JLD.jldopen(filename, "r+") do io - JLD.addrequire(io, BenchmarkTools) +function load(filename::AbstractString, args...) + if !endswith(filename, ".json") + noext, ext = splitext(filename) + msg = if ext == ".jld" + "JLD deserialization is no longer supported. Benchmarks should now be saved\n" * + "in JSON format using `save(\"$noext\".json, args...)`. You will need to\n" * + "convert existing saved benchmarks to JSON in order to deserialize them with\n" * + "this version of BenchmarkTools." + else + "Only JSON deserialization is supported. Use `load(\"$noext.json\", args...)`." + end + throw(ArgumentError(msg)) + end + if !isempty(args) + throw(ArgumentError("Looking up deserialized values by name is no longer supported, " * + "as names are no longer saved.")) end - return nothing + parsed = open(JSON.parse, filename, "r") + if !isa(parsed, Vector) || length(parsed) != 2 || !isa(parsed[1], Dict) || !isa(parsed[2], Vector) + error("Unexpected JSON format. Was this file produced by BenchmarkTools?") + end + versions = parsed[1]::Dict + values = parsed[2]::Vector + map!(recover, values, values) end -@inline function load(filename, args...) - # no version-based rules are needed for now, we just need - # to check that version information exists in the file. - if JLD.jldopen(file -> JLD.exists(file, VERSION_KEY), filename, "r") - result = JLD.load(filename, args...) - else - JLD.translate("BenchmarkTools.Parameters", "BenchmarkTools.ParametersPreV006") - JLD.translate("BenchmarkTools.Trial", "BenchmarkTools.TrialPreV006") - result = JLD.load(filename, args...) - JLD.translate("BenchmarkTools.Parameters", "BenchmarkTools.Parameters") - JLD.translate("BenchmarkTools.Trial", "BenchmarkTools.Trial") - end - return result -end +#function JLD.readas(p::ParametersPreV006) +# return Parameters(p.seconds, p.samples, p.evals, Float64(p.overhead), p.gctrial, +# p.gcsample, p.time_tolerance, p.memory_tolerance) +#end +# +#function JLD.readas(t::TrialPreV006) +# new_times = convert(Vector{Float64}, t.times) +# new_gctimes = convert(Vector{Float64}, t.gctimes) +# return Trial(t.params, new_times, new_gctimes, t.memory, t.allocs) +#end +# +#function save(filename, args...) +# JLD.save(filename, VERSION_KEY, VERSIONS, args...) +# JLD.jldopen(filename, "r+") do io +# JLD.addrequire(io, BenchmarkTools) +# end +# return nothing +#end +# +#@inline function load(filename, args...) +# # no version-based rules are needed for now, we just need +# # to check that version information exists in the file. +# if JLD.jldopen(file -> JLD.exists(file, VERSION_KEY), filename, "r") +# result = JLD.load(filename, args...) +# else +# JLD.translate("BenchmarkTools.Parameters", "BenchmarkTools.ParametersPreV006") +# JLD.translate("BenchmarkTools.Trial", "BenchmarkTools.TrialPreV006") +# result = JLD.load(filename, args...) +# JLD.translate("BenchmarkTools.Parameters", "BenchmarkTools.Parameters") +# JLD.translate("BenchmarkTools.Trial", "BenchmarkTools.Trial") +# end +# return result +#end diff --git a/test/SerializationTests.jl b/test/SerializationTests.jl index db70a931..6051f2e8 100644 --- a/test/SerializationTests.jl +++ b/test/SerializationTests.jl @@ -1,14 +1,68 @@ module SerializationTests -using Base.Test +using Compat +using Compat.Test using BenchmarkTools -old_data = BenchmarkTools.load(joinpath(dirname(@__FILE__), "data_pre_v006.jld"), "results") -BenchmarkTools.save(joinpath(dirname(@__FILE__), "tmp.jld"), "results", old_data) -new_data = BenchmarkTools.load(joinpath(dirname(@__FILE__), "tmp.jld"), "results") +eq(x::T, y::T) where {T<:Union{BenchmarkTools.supported_types...}} = + all(i->eq(getfield(x, i), getfield(y, i)), 1:fieldcount(T)) +eq(x::T, y::T) where {T} = isapprox(x, y) -@test old_data == new_data +tmp = joinpath(@__DIR__, "tmp.json") +isfile(tmp) && rm(tmp) -rm(joinpath(dirname(@__FILE__), "tmp.jld")) +@testset "Successful (de)serialization" begin + b = @benchmarkable sin(1) + tune!(b) + bb = run(b) + + BenchmarkTools.save(tmp, b, bb) + @test isfile(tmp) + rm(tmp) + + results = BenchmarkTools.load(tmp) + @test results isa Vector{Any} + @test length(results) == 2 + @test eq(results[1], b) + @test eq(results[2], bb) +end + +@testset "Deprecated behaviors" begin + @test_throws ArgumentError BenchmarkTools.save("x.jld", b) + @test_throws ArgumentError BenchmarkTools.save("x.txt", b) + @test_throws ArgumentError BenchmarkTools.save("x.json") + @test_throws ArgumentError BenchmarkTools.save("x.json", 1) + + @test_warn "Naming variables" BenchmarkTools.save(tmp, "b", b) + @test isfile(tmp) + results = BenchmarkTools.load(tmp) + @test length(results) == 1 + @test eq(results[1], b) + rm(tmp) + + @test_throws ArgumentError BenchmarkTools.load("x.jld") + @test_throws ArgumentError BenchmarkTools.load("x.txt") + @test_throws ArgumentError BenchmarkTools.load("x.json", "b") +end + +@testset "Error checking" begin + isfile(tmp) && rm(tmp) + open(tmp, "w") do f + print(f, """ + {"never":1,[{"gonna":2,"give":3,"you":4,"up":5}]} + """) + end + try + BenchmarkTools.load(tmp) + error("madness") + catch err + # This function thows a bunch of ArgumentErrors, so test for this specifically + @test err isa ArgumentError + @test contains(err.msg, "Unexpected JSON format") + end + rm(tmp) + + @test_throws ArgumentError BenchmarkTools.recover([1]) +end end # module diff --git a/test/data_pre_v006.jld b/test/data_pre_v006.jld deleted file mode 100644 index cf126f9b3f02ff640a622fad405d3f903997391d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1155653 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