diff --git a/RecipesPipeline/.github/workflows/SnoopCompile.yml b/RecipesPipeline/.github/workflows/SnoopCompile.yml new file mode 100644 index 000000000..3d71a8e62 --- /dev/null +++ b/RecipesPipeline/.github/workflows/SnoopCompile.yml @@ -0,0 +1,88 @@ +name: SnoopCompile + +on: + push: + branches: + # - 'master' # NOTE: to run the bot only on pushes to master + +defaults: + run: + shell: bash + +jobs: + SnoopCompile: + if: "!contains(github.event.head_commit.message, '[skip ci]')" + env: + GKS_ENCODING: "utf8" + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: # NOTE: the versions below should match those in your botconfig + - '1.5' + os: # NOTE: should match the os setting of your botconfig + - ubuntu-latest + arch: + - x64 + steps: + # Setup environment + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.version }} + + - name: Install dependencies + run: | + julia --project -e 'using Pkg; Pkg.instantiate();' + julia -e 'using Pkg; Pkg.add( PackageSpec(name = "CompileBot", version = "1") ); Pkg.develop(PackageSpec(; path=pwd())); using CompileBot; CompileBot.addtestdep();' + + + # TESTCMD + - name: Default TESTCMD + run: echo ::set-env name=TESTCMD::"julia" + - name: Ubuntu TESTCMD + if: startsWith(matrix.os,'ubuntu') + run: echo ::set-env name=TESTCMD::"xvfb-run --auto-servernum julia" + + # Generate precompile files + - name: Generating precompile files + run: $TESTCMD --project -e 'include("deps/SnoopCompile/snoop_bot.jl")' # NOTE: must match path + + # Run benchmarks + - name: Running Benchmark + run: $TESTCMD --project -e 'include("deps/SnoopCompile/snoop_bench.jl")' # NOTE: optional, if have benchmark file + + - name: Upload all + uses: actions/upload-artifact@v2.0.1 + with: + path: ./ + + Create_PR: + if: "!contains(github.event.head_commit.message, '[skip ci]')" + needs: SnoopCompile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Download all + uses: actions/download-artifact@v2 + + - name: CompileBot postprocess + run: julia -e 'using Pkg; Pkg.add( PackageSpec(name = "CompileBot", version = "1") ); using CompileBot; CompileBot.postprocess();' + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: Update precompile_*.jl file + title: "[AUTO] Update precompiles" + labels: SnoopCompile + branch: "SnoopCompile_AutoPR" + + + Skip: + if: "contains(github.event.head_commit.message, '[skip ci]')" + runs-on: ubuntu-latest + steps: + - name: Skip CI 🚫 + run: echo skip ci diff --git a/RecipesPipeline/Project.toml b/RecipesPipeline/Project.toml index 844630fb9..bd5a02694 100644 --- a/RecipesPipeline/Project.toml +++ b/RecipesPipeline/Project.toml @@ -1,7 +1,7 @@ name = "RecipesPipeline" uuid = "01d81517-befc-4cb6-b9ec-a95719d0359c" authors = ["Michael Krabbe Borregaard "] -version = "0.1.13" +version = "0.2" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/RecipesPipeline/deps/SnoopCompile/precompile/linux/1.5/precompile_RecipesPipeline.jl b/RecipesPipeline/deps/SnoopCompile/precompile/linux/1.5/precompile_RecipesPipeline.jl new file mode 100644 index 000000000..4c03bca95 --- /dev/null +++ b/RecipesPipeline/deps/SnoopCompile/precompile/linux/1.5/precompile_RecipesPipeline.jl @@ -0,0 +1,94 @@ +const __bodyfunction__ = Dict{Method,Any}() + +# Find keyword "body functions" (the function that contains the body +# as written by the developer, called after all missing keyword-arguments +# have been assigned values), in a manner that doesn't depend on +# gensymmed names. +# `mnokw` is the method that gets called when you invoke it without +# supplying any keywords. +function __lookup_kwbody__(mnokw::Method) + function getsym(arg) + isa(arg, Symbol) && return arg + @assert isa(arg, GlobalRef) + return arg.name + end + + f = get(__bodyfunction__, mnokw, nothing) + if f === nothing + fmod = mnokw.module + # The lowered code for `mnokw` should look like + # %1 = mkw(kwvalues..., #self#, args...) + # return %1 + # where `mkw` is the name of the "active" keyword body-function. + ast = Base.uncompressed_ast(mnokw) + if isa(ast, Core.CodeInfo) && length(ast.code) >= 2 + callexpr = ast.code[end-1] + if isa(callexpr, Expr) && callexpr.head == :call + fsym = callexpr.args[1] + if isa(fsym, Symbol) + f = getfield(fmod, fsym) + elseif isa(fsym, GlobalRef) + if fsym.mod === Core && fsym.name === :_apply + f = getfield(mnokw.module, getsym(callexpr.args[2])) + elseif fsym.mod === Core && fsym.name === :_apply_iterate + f = getfield(mnokw.module, getsym(callexpr.args[3])) + else + f = getfield(fsym.mod, fsym.name) + end + else + f = missing + end + else + f = missing + end + else + f = missing + end + __bodyfunction__[mnokw] = f + end + return f +end + +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + Base.precompile(Tuple{typeof(Base.Broadcast.materialize),Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1},Nothing,typeof(RecipesPipeline._scaled_adapted_grid),Tuple{Array{Function,1},Base.RefValue{Symbol},Base.RefValue{Symbol},Float64,Float64}}}) + Base.precompile(Tuple{typeof(RecipesBase.apply_recipe),AbstractDict{Symbol,Any},Array{Function,1},Number,Number}) + Base.precompile(Tuple{typeof(RecipesBase.apply_recipe),AbstractDict{Symbol,Any},RecipesPipeline.GroupBy,Any}) + Base.precompile(Tuple{typeof(RecipesBase.apply_recipe),AbstractDict{Symbol,Any},Type{RecipesPipeline.SliceIt},Any,Any,Any}) + Base.precompile(Tuple{typeof(RecipesPipeline._apply_type_recipe),Any,AbstractArray,Any}) + Base.precompile(Tuple{typeof(RecipesPipeline._apply_type_recipe),Any,Surface,Any}) + Base.precompile(Tuple{typeof(RecipesPipeline._extract_group_attributes),Array{String,1},Array{Float64,1}}) + Base.precompile(Tuple{typeof(RecipesPipeline._map_funcs),Function,StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}}) + Base.precompile(Tuple{typeof(RecipesPipeline._process_plotrecipes!),Any,Any}) + Base.precompile(Tuple{typeof(RecipesPipeline._process_ribbon),Tuple{LinRange{Float64},LinRange{Float64}},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._process_seriesrecipe),Any,Any}) + Base.precompile(Tuple{typeof(RecipesPipeline._process_seriesrecipes!),Any,Any}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{AbstractArray{Float64,1},1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{Array{Float64,1},1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{Array{T,1} where T,1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{Float64,2},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{Function,1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{Int64,1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{Real,1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{String,1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Array{Union{Missing, Int64},1},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Int64,Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),StepRange{Int64,Int64},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Surface{Array{Int64,2}},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline._series_data_vector),Surface{Base.ReshapedArray{Int64,2,UnitRange{Int64},Tuple{}}},Dict{Symbol,Any}}) + Base.precompile(Tuple{typeof(RecipesPipeline.recipe_pipeline!),Any,Any,Any}) + Base.precompile(Tuple{typeof(RecipesPipeline.typerecipe_signature_string),typeof(atan)}) + Base.precompile(Tuple{typeof(RecipesPipeline.unzip),Array{Tuple{Array{Float64,1},Array{Float64,1}},1}}) + Base.precompile(Tuple{typeof(RecipesPipeline.unzip),Array{Tuple{Float64,Float64,Float64},1}}) + Base.precompile(Tuple{typeof(RecipesPipeline.unzip),Array{Tuple{Int64,Real},1}}) + Base.precompile(Tuple{typeof(copy),Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,typeof(string),Tuple{Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,typeof(typeof),Tuple{Tuple{Base.OneTo{Int64},Base.OneTo{Int64},Surface{Array{Int64,2}}}}}}}}) + Base.precompile(Tuple{typeof(copy),Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,typeof(string),Tuple{Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,typeof(typeof),Tuple{Tuple{RecipesPipeline.GroupBy,Array{Float64,1}}}}}}}) + Base.precompile(Tuple{typeof(copy),Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,typeof(string),Tuple{Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,typeof(typeof),Tuple{Tuple{StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}},StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}},Surface{Array{Float64,2}}}}}}}}) + Base.precompile(Tuple{typeof(setindex!),RecipesPipeline.DefaultsDict,StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}},Symbol}) + let fbody = try __lookup_kwbody__(which(RecipesPipeline._extract_group_attributes, (Array{String,1},Array{Float64,1},))) catch missing end + if !ismissing(fbody) + precompile(fbody, (Function,typeof(RecipesPipeline._extract_group_attributes),Array{String,1},Array{Float64,1},)) + end + end +end diff --git a/RecipesPipeline/deps/SnoopCompile/snoop_bench.jl b/RecipesPipeline/deps/SnoopCompile/snoop_bench.jl new file mode 100644 index 000000000..9f1551b37 --- /dev/null +++ b/RecipesPipeline/deps/SnoopCompile/snoop_bench.jl @@ -0,0 +1,10 @@ +using CompileBot + +snoop_bench( + BotConfig( + "RecipesPipeline", + yml_path= "SnoopCompile.yml", + else_os = "linux", + else_version = "1.5", + ) +) diff --git a/RecipesPipeline/deps/SnoopCompile/snoop_bot.jl b/RecipesPipeline/deps/SnoopCompile/snoop_bot.jl new file mode 100644 index 000000000..c5eb2d63d --- /dev/null +++ b/RecipesPipeline/deps/SnoopCompile/snoop_bot.jl @@ -0,0 +1,10 @@ +using CompileBot + +snoop_bot( + BotConfig( + "RecipesPipeline", + yml_path= "SnoopCompile.yml", + else_os = "linux", + else_version = "1.5", + ) +) diff --git a/RecipesPipeline/src/RecipesPipeline.jl b/RecipesPipeline/src/RecipesPipeline.jl index b10bff18a..2bf23af1b 100644 --- a/RecipesPipeline/src/RecipesPipeline.jl +++ b/RecipesPipeline/src/RecipesPipeline.jl @@ -39,8 +39,8 @@ export warn_on_recipe_aliases, slice_series_attributes!, process_sliced_series_attributes! -include("api.jl") include("utils.jl") +include("api.jl") include("series.jl") include("group.jl") include("user_recipe.jl") @@ -59,6 +59,7 @@ contains only the keyword arguments passed in by the user. Then, add all series object `plt` and return it. """ function recipe_pipeline!(plt, plotattributes, args) + @nospecialize plotattributes[:plot_object] = plt # -------------------------------- @@ -102,4 +103,6 @@ function recipe_pipeline!(plt, plotattributes, args) return plt end +include("precompile_includer.jl") + end diff --git a/RecipesPipeline/src/api.jl b/RecipesPipeline/src/api.jl index 124fad5d0..d6c0ee5c7 100644 --- a/RecipesPipeline/src/api.jl +++ b/RecipesPipeline/src/api.jl @@ -8,7 +8,15 @@ Warn if an alias is dedected in `plotattributes` after a recipe of type `recipe_type` is applied to 'args'. `recipe_type` is either `:user`, `:type`, `:plot` or `:series`. """ -function warn_on_recipe_aliases!(plt, plotattributes, recipe_type, args...) end +function warn_on_recipe_aliases!(plt, plotattributes::AKW, recipe_type, signature_string) end +function warn_on_recipe_aliases!(plt, v::AbstractVector, recipe_type, signature_string) + for x in v + warn_on_recipe_aliases!(plt, x, recipe_type, signature_string) + end +end +function warn_on_recipe_aliases!(plt, rd::RecipeData, recipe_type, signature_string) + warn_on_recipe_aliases!(plt, rd.plotattributes, recipe_type, signature_string) +end # ## Grouping diff --git a/RecipesPipeline/src/plot_recipe.jl b/RecipesPipeline/src/plot_recipe.jl index 7f52237a0..70a511e11 100644 --- a/RecipesPipeline/src/plot_recipe.jl +++ b/RecipesPipeline/src/plot_recipe.jl @@ -1,5 +1,7 @@ # # Plot Recipes +@nospecialize + """ _process_plotrecipes!(plt, kw_list) @@ -29,7 +31,7 @@ function _process_plotrecipe(plt, kw, kw_list, still_to_process) st = kw[:seriestype] st = kw[:seriestype] = type_alias(plt, st) datalist = RecipesBase.apply_recipe(kw, Val{st}, plt) - warn_on_recipe_aliases!(plt, datalist, :plot, st) + warn_on_recipe_aliases!(plt, datalist, :plot, plotrecipe_signature_string(st)) for data in datalist preprocess_attributes!(plt, data.plotattributes) if data.plotattributes[:seriestype] == st @@ -46,3 +48,5 @@ function _process_plotrecipe(plt, kw, kw_list, still_to_process) end return end + +@specialize \ No newline at end of file diff --git a/RecipesPipeline/src/precompile_includer.jl b/RecipesPipeline/src/precompile_includer.jl new file mode 100644 index 000000000..10d539b67 --- /dev/null +++ b/RecipesPipeline/src/precompile_includer.jl @@ -0,0 +1,34 @@ +should_precompile = true + + +# Don't edit the following! Instead change the script for `snoop_bot`. +ismultios = true +ismultiversion = true +# precompile_enclosure +@static if !should_precompile + # nothing +elseif !ismultios && !ismultiversion + include("../deps/SnoopCompile/precompile/precompile_RecipesPipeline.jl") + _precompile_() +else + @static if Sys.islinux() + @static if v"1.5.0-DEV" <= VERSION <= v"1.5.9" + include("../deps/SnoopCompile/precompile/linux/1.5/precompile_RecipesPipeline.jl") + _precompile_() + else + include("../deps/SnoopCompile/precompile/linux/1.5/precompile_RecipesPipeline.jl") + _precompile_() + end + + else + @static if v"1.5.0-DEV" <= VERSION <= v"1.5.9" + include("../deps/SnoopCompile/precompile/linux/1.5/precompile_RecipesPipeline.jl") + _precompile_() + else + include("../deps/SnoopCompile/precompile/linux/1.5/precompile_RecipesPipeline.jl") + _precompile_() + end + + end + +end # precompile_enclosure diff --git a/RecipesPipeline/src/series.jl b/RecipesPipeline/src/series.jl index b3b35d028..239ec383f 100644 --- a/RecipesPipeline/src/series.jl +++ b/RecipesPipeline/src/series.jl @@ -124,6 +124,7 @@ struct SliceIt end # It splits processed data into individual series data, stores in copied `plotattributes` # for each series and returns no arguments. @recipe function f(::Type{SliceIt}, x, y, z) + @nospecialize # handle data with formatting attached if typeof(x) <: Formatted diff --git a/RecipesPipeline/src/series_recipe.jl b/RecipesPipeline/src/series_recipe.jl index a56f66e91..45289b2f6 100644 --- a/RecipesPipeline/src/series_recipe.jl +++ b/RecipesPipeline/src/series_recipe.jl @@ -1,5 +1,7 @@ # # Series Recipes +@nospecialize + """ _process_seriesrecipes!(plt, kw_list) @@ -46,7 +48,7 @@ function _process_seriesrecipe(plt, plotattributes) # get a sub list of series for this seriestype x, y, z = plotattributes[:x], plotattributes[:y], plotattributes[:z] datalist = RecipesBase.apply_recipe(plotattributes, Val{st}, x, y, z) - warn_on_recipe_aliases!(plt, datalist, :series, st) + warn_on_recipe_aliases!(plt, datalist, :series, seriesrecipe_signature_string(st)) # assuming there was no error, recursively apply the series recipes for data in datalist @@ -64,3 +66,5 @@ function _process_seriesrecipe(plt, plotattributes) end nothing end + +@specialize \ No newline at end of file diff --git a/RecipesPipeline/src/type_recipe.jl b/RecipesPipeline/src/type_recipe.jl index f0bbd5516..4013611e6 100644 --- a/RecipesPipeline/src/type_recipe.jl +++ b/RecipesPipeline/src/type_recipe.jl @@ -1,5 +1,7 @@ # # Type Recipes +@nospecialize + # this is the default "type recipe"... just pass the object through @recipe f(::Type{T}, v::T) where {T} = v @@ -16,7 +18,7 @@ function _apply_type_recipe(plotattributes, v, letter) plt = plotattributes[:plot_object] preprocess_axis_args!(plt, plotattributes, letter) rdvec = RecipesBase.apply_recipe(plotattributes, typeof(v), v) - warn_on_recipe_aliases!(plotattributes[:plot_object], plotattributes, :type, typeof(v)) + warn_on_recipe_aliases!(plotattributes[:plot_object], plotattributes, :type, typerecipe_signature_string(v)) postprocess_axis_args!(plt, plotattributes, letter) return rdvec[1].args[1] end @@ -29,13 +31,13 @@ function _apply_type_recipe(plotattributes, v::AbstractArray, letter) preprocess_axis_args!(plt, plotattributes, letter) # First we try to apply an array type recipe. w = RecipesBase.apply_recipe(plotattributes, typeof(v), v)[1].args[1] - warn_on_recipe_aliases!(plt, plotattributes, :type, typeof(v)) + warn_on_recipe_aliases!(plt, plotattributes, :type, typerecipe_signature_string(v)) # If the type did not change try it element-wise if typeof(v) == typeof(w) isempty(skipmissing(v)) && return Float64[] x = first(skipmissing(v)) args = RecipesBase.apply_recipe(plotattributes, typeof(x), x)[1].args - warn_on_recipe_aliases!(plt, plotattributes, :type, typeof(x)) + warn_on_recipe_aliases!(plt, plotattributes, :type, typerecipe_signature_string(x)) postprocess_axis_args!(plt, plotattributes, letter) if length(args) == 2 && all(arg -> arg isa Function, args) numfunc, formatter = args @@ -70,3 +72,5 @@ _apply_type_recipe( letter, ) = v _apply_type_recipe(plotattributes, v::Nothing, letter) = v + +@specialize \ No newline at end of file diff --git a/RecipesPipeline/src/user_recipe.jl b/RecipesPipeline/src/user_recipe.jl index 82424e63f..13ff0d8f8 100644 --- a/RecipesPipeline/src/user_recipe.jl +++ b/RecipesPipeline/src/user_recipe.jl @@ -9,6 +9,7 @@ empy `args` is returned pop it from the vector, finish up, and it to vector of ` processed series. When all arguments are processed return the series `Dict`. """ function _process_userrecipes!(plt, plotattributes, args) + @nospecialize still_to_process = _recipedata_vector(plt, plotattributes, args) # For plotting recipes, we swap out the args and update the parameter dictionary. We are keeping a stack of series that still need to be processed. @@ -34,7 +35,7 @@ function _process_userrecipes!(plt, plotattributes, args) else rd_list = RecipesBase.apply_recipe(next_series.plotattributes, next_series.args...) - warn_on_recipe_aliases!(plt, rd_list, :user, next_series.args...) + warn_on_recipe_aliases!(plt, rd_list, :user, userrecipe_signature_string(next_series.args...)) prepend!(still_to_process, rd_list) end end @@ -48,6 +49,7 @@ end # TODO Move this to api.jl? function _recipedata_vector(plt, plotattributes, args) + @nospecialize still_to_process = RecipeData[] # the grouping mechanism is a recipe on a GroupBy object # we simply add the GroupBy object to the front of the args list to allow @@ -76,6 +78,7 @@ function _recipedata_vector(plt, plotattributes, args) end function _expand_seriestype_array(plotattributes, args) + @nospecialize sts = get(plotattributes, :seriestype, :path) if typeof(sts) <: AbstractArray reset_kw!(plotattributes, :seriestype) @@ -107,6 +110,8 @@ end # Fallback user recipes # -------------------------------- +@nospecialize + # These call `_apply_type_recipe` in type_recipe.jl and finally the `SliceIt` recipe in # series.jl. @@ -304,12 +309,6 @@ end n = 200, ) where {F <: Function, G <: Function} = fx, fy, range(umin, stop = umax, length = n) -function _scaled_adapted_grid(f, xscale, yscale, xmin, xmax) - (xf, xinv), (yf, yinv) = ((scale_func(s), inverse_scale_func(s)) for s in (xscale, yscale)) - xs, ys = PlotUtils.adapted_grid(yf ∘ f ∘ xinv, xf.((xmin, xmax))) - xinv.(xs), yinv.(ys) -end - # special handling... 3D parametric function(s) @recipe function f( fx::FuncOrFuncs{F}, @@ -346,3 +345,11 @@ end zguide --> string(K[3]) return Tuple.(ntv) end + +@specialize + +function _scaled_adapted_grid(f, xscale, yscale, xmin, xmax) + (xf, xinv), (yf, yinv) = ((scale_func(s), inverse_scale_func(s)) for s in (xscale, yscale)) + xs, ys = PlotUtils.adapted_grid(yf ∘ f ∘ xinv, xf.((xmin, xmax))) + xinv.(xs), yinv.(ys) +end diff --git a/RecipesPipeline/src/utils.jl b/RecipesPipeline/src/utils.jl index 8b3298ddd..3a45df70d 100644 --- a/RecipesPipeline/src/utils.jl +++ b/RecipesPipeline/src/utils.jl @@ -221,3 +221,19 @@ end _map_funcs(f::Function, u::AVec) = map(f, u) _map_funcs(fs::AVec{F}, u::AVec) where {F <: Function} = [map(f, u) for f in fs] + + +# -------------------------------- +# ## Signature strings +# -------------------------------- + +@nospecialize + +function userrecipe_signature_string(args...) + return string("(::", join(string.(typeof.(args)), ", ::"), ")") +end +typerecipe_signature_string(::T) where T = "(::Type{$T}, ::$T)" +plotrecipe_signature_string(st) = "(::Type{Val{:$st}}, ::AbstractPlot)" +seriesrecipe_signature_string(st) = "(::Type{Val{:$st}}, x, y, z)" + +@specialize \ No newline at end of file