tlienart / Franklin.jl

(yet another) static site generator. Simple, customisable, fast, maths with KaTeX, code evaluation, optional pre-rendering, in Julia.
https://franklinjl.org
MIT License
957 stars 112 forks source link

SnoopCompile tricks #752

Open tlienart opened 3 years ago

tlienart commented 3 years ago

Kindly suggested by Tim Holy: https://github.com/timholy/SnoopCompile.jl/issues/197#issuecomment-754916697

timholy commented 3 years ago

I've been poking at this because it's proving to be a useful case for deeper understanding. The amount of information that SnoopCompile returns is now so vast that I suspect it will be 6 or more months before the community (including myself) fully understands how best to use it. So when I enounter interesting things, I dig into the relevant package(s).

One of your more expensive inference costs is the keyword function of convert_md, aka,

julia> methods(Core.kwfunc(Franklin.convert_md))
# 4 methods for anonymous function "convert_md##kw":
[1] (var"#s52"::Franklin.var"#convert_md##kw")(::Any, ::typeof(Franklin.convert_md), mds::String) in Franklin at /home/tim/.julia/dev/Franklin/src/converter/markdown/md.jl:23
[2] (var"#s52"::Franklin.var"#convert_md##kw")(::Any, ::typeof(Franklin.convert_md), mds::String, pre_lxdefs::Vector{Franklin.LxDef}) in Franklin at /home/tim/.julia/dev/Franklin/src/converter/markdown/md.jl:23
[3] (::Franklin.var"#convert_md##kw")(::Any, ::typeof(Franklin.convert_md), mds::AbstractString) in Franklin at /home/tim/.julia/dev/Franklin/src/converter/markdown/md.jl:183
[4] (::Franklin.var"#convert_md##kw")(::Any, ::typeof(Franklin.convert_md), mds::AbstractString, pre_lxdefs) in Franklin at /home/tim/.julia/dev/Franklin/src/converter/markdown/md.jl:185

(Let me first note that I'm running with #753; I didn't want to spoil your fun in figuring this stuff out for yourself, but that's a necessary platform for this discussion so I thought I'd better submit it.) Using some of the still-not-very-accessible internals of SnoopCompile, I can extract every inference run on the first of these methods:

20-element Vector{SnoopCompileCore.InferenceTimingNode}:
 InferenceTimingNode: 0.044216/0.512847 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 11 direct children
 InferenceTimingNode: 0.000066/0.000862 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000058/0.019381 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000058/0.000311 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.109543/0.195334 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 6 direct children
 InferenceTimingNode: 0.000073/0.000576 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000210/0.002537 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isconfig,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 3 direct children
 InferenceTimingNode: 0.000153/0.000870 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")((isconfig = true,)::NamedTuple{(:isconfig,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000064/0.001739 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 3 direct children
 InferenceTimingNode: 0.000051/0.000475 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.048367/0.147750 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 6 direct children
 InferenceTimingNode: 0.000053/0.000482 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000057/0.023800 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000043/0.000203 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.117287/0.204037 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 6 direct children
 InferenceTimingNode: 0.000050/0.000463 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000183/0.002179 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")((pagevar = true,)::NamedTuple{(:pagevar,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 3 direct children
 InferenceTimingNode: 0.000219/0.002384 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isconfig,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 3 direct children
 InferenceTimingNode: 0.000169/0.001002 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")((isconfig = true,)::NamedTuple{(:isconfig,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 1 direct children
 InferenceTimingNode: 0.000268/0.003616 on InferenceFrameInfo for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isrecursive,), Tuple{Bool}}, convert_md::typeof(Franklin.convert_md), ::String) with 3 direct children

As a reminder, the first number is the exclusive time and the second is the inclusive time. Of these 20 passes, you'll note that three start with a NamedTuple value, e.g., (isconfig = true,), which is Julia's constant propagation at work. You probably don't really want constant propagation on this method, but these three don't look very expensive (together they cost you ~4ms) so I am not particularly worried about them.

The bigger concern is that after the first expensive inference run (~0.5s inclusive time) you have three additional expensive runs on the same method. In some cases these are even with the identical keywords. That led me to suspect that it's getting invalidated after you initially compile the method; sure enough,

using SnoopCompile, Franklin
invs = @snoopr try include("runtests.jl") catch end
trees = invalidation_trees(invs)
m = which(Core.kwfunc(Franklin.convert_md), (Any, typeof(Franklin.convert_md), String))

julia> methinvs = findcaller(m, trees)
inserting zip(s0::DataStructures.SparseIntSet, s::DataStructures.SparseIntSet...; kwargs...) in DataStructures at /home/tim/.julia/packages/DataStructures/DLSxi/src/sparse_int_set.jl:213 invalidated:
   backedges: 1: superseding zip(a...) in Base.Iterators at iterators.jl:314 with MethodInstance for zip(::Any, ::Any) (12 children)
   17 mt_cache

julia> show(methinvs.backedges[1]; maxdepth=20, minchildren=0)
MethodInstance for zip(::Any, ::Any) (12 children)
 MethodInstance for process_html_for(::String, ::Vector{Franklin.AbstractBlock}, ::Int64) (11 children)
  MethodInstance for process_html_qblocks(::String, ::Vector{Franklin.AbstractBlock}, ::Int64, ::Int64) (10 children)
   MethodInstance for process_html_qblocks(::String, ::Vector{Franklin.AbstractBlock}) (9 children)
    MethodInstance for convert_html(::String) (8 children)
     MethodInstance for var"#fd2html_v#178"(::Bool, ::String, ::Bool, ::typeof(Franklin.fd2html_v), ::SubString{String}) (7 children)
      MethodInstance for (::Franklin.var"#fd2html_v##kw")(::NamedTuple{(:internal,), Tuple{Bool}}, ::typeof(Franklin.fd2html_v), ::SubString{String}) (6 children)
       MethodInstance for var"#fd2html#179"(::Base.Iterators.Pairs{Symbol, Bool, Tuple{Symbol}, NamedTuple{(:internal,), Tuple{Bool}}}, ::typeof(fd2html), ::SubString{String}) (5 children)
        MethodInstance for (::Franklin.var"#fd2html##kw")(::NamedTuple{(:internal,), Tuple{Bool}}, ::typeof(fd2html), ::SubString{String}) (4 children)
         MethodInstance for validate_and_store_link_defs!(::Vector{Franklin.OCBlock}) (3 children)
          MethodInstance for var"#convert_md#146"(::Bool, ::Bool, ::Bool, ::Bool, ::Bool, ::Bool, ::typeof(Franklin.convert_md), ::String, ::Vector{Franklin.LxDef}) (2 children)
           MethodInstance for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, ::typeof(Franklin.convert_md), ::String, ::Vector{Franklin.LxDef}) (1 children)
            MethodInstance for (::Franklin.var"#convert_md##kw")(::NamedTuple{(:isinternal,), Tuple{Bool}}, ::typeof(Franklin.convert_md), ::String) (0 children)

So loading DataStructures is invalidating convert_md (among others) via process_html_for. I suspect it's being invalidated repeatedly (3 separate times), and this is just the first of them. The other two presumably come from packages besides DataStructures that you load at separate times.

It's possible these invalidations affect only the tests (you may load packages that you wouldn't use in more typical circumstances), but you'd have a much better sense of that than I.

The way to fix invalidations is to improve inferrability, as I did in #753. I've got some pretty extensive docs (SnoopCompile) on this topic, which even link to a youtube video, but let me know if you want some pointers.

timholy commented 3 years ago

If you can, you should also consider replacing AS with just plain String, at least when it's used as a type parameter (e.g., PAGE_HEADERS). That will both standardize types and improve inferrability (julia is not good at inference with Union type-parameters, see https://github.com/JuliaLang/julia/issues/36454).

tlienart commented 3 years ago

Thanks so much for all this insight, this is invaluable! it will take me some time to digest all that you've done and to figure out how to best implement fixes 😄

Also this couldn't come at a better time as I'm working on a significant refactoring of all this convert_md business

timholy commented 3 years ago

I thought I'd check back in because SnoopCompile 2.4.0 is being released now (specifically, 15 minutes from now...). Seems like you've made some progress! In terms of invalidations,

julia> thinned = filtermod(Franklin, trees)
1-element Vector{SnoopCompile.MethodInvalidations}:
 inserting convert(::Type{S}, x::CategoricalArrays.CategoricalValue) where S<:Union{AbstractChar, AbstractString, Number} in CategoricalArrays at /home/tim/.julia/packages/CategoricalArrays/ZjBSI/src/value.jl:73 invalidated:
   mt_backedges: 1: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for convert_html_fblock(::Franklin.HFun) (0 children)
                 2: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for var"#resolve_lxobj#164"(::Bool, ::typeof(Franklin.resolve_lxobj), ::Franklin.LxEnv, ::Vector{Franklin.LxDef}) (0 children)
                 3: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for var"#resolve_lxobj#164"(::Bool, ::typeof(Franklin.resolve_lxobj), ::Franklin.LxObj, ::Vector{Franklin.LxDef}) (0 children)
                 4: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for hfun_fill(::Vector{String}) (0 children)
                 5: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for var"#resolve_lxobj#164"(::Bool, ::typeof(Franklin.resolve_lxobj), ::Franklin.LxCom, ::Vector{Franklin.LxDef}) (0 children)
                 6: signature Tuple{typeof(convert), Type{Union{SubString{String}, String}}, Any} triggered MethodInstance for convert_block(::Franklin.AbstractBlock, ::Vector{Franklin.LxDef}) (0 children)
                 7: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for Franklin.RSSItem(::Any, ::String, ::String, ::String, ::String, ::String, ::String, ::Any) (1 children)
                 8: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for getname(::Franklin.LxCom) (2 children)
                 9: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for resolve_rpath(::String, ::String) (6 children)

indicates that resolve_rpath is the remaining invalidation that causes the most "damage." ascend indicates that it's a case of julia#15276, which can be fixed by the let trick here. I just submitted https://github.com/timholy/SnoopCompile.jl/pull/215 to document this better.

If you do some profiling and discover some runtime-dispatch hotspots, the new suggest framework in 2.4.0 might be helpful. For instance, I noticed that resolve_code_block had a cascade of inference triggers (I have no idea whether it's a performance hotspot). Referencing this rewritten docs page,

# Grab the sub-tree that results from calling `resolve_code_block` non-inferrably
julia> idx = findfirst(node -> Method(node.itrig.node).name === :resolve_code_block, itree.children)
59

julia> node = itree.children[idx]
TriggerNode for MethodInstance for resolve_code_block(::SubString{String}) with 8 direct children

julia> print_tree(node)
MethodInstance for resolve_code_block(::SubString{String})
├─ MethodInstance for joinpath(::SubString{String}, ::SubString{String})
├─ MethodInstance for var"#open#318"(::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, ::typeof(open), ::Franklin.var"#96#98"{String}, ::String, ::Vararg{String, N} where N)
│  └─ MethodInstance for redirect_stderr(::Franklin.var"#97#99"{String}, ::IOStream)
│     └─ MethodInstance for redirect_stderr(::Base.TTY)
├─ MethodInstance for isnothing(::Expr)
├─ MethodInstance for var"#open#318"(::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, ::typeof(open), ::Franklin.var"#101#103"{Module, String, Int64, Vector{Any}}, ::String, ::Vararg{String, N} where N)
│  └─ MethodInstance for redirect_stdout(::Franklin.var"#102#104"{Module, Int64, Vector{Any}}, ::IOStream)
│     ├─ MethodInstance for DateTime(::String)
│     └─ MethodInstance for fd_date(::DateTime)
│        ├─ MethodInstance for getproperty(::Pair{Vector{String}, Tuple{DataType}}, ::Symbol)
│        ├─ MethodInstance for combine_styles(::NTuple{4, Vector{String}})
│        ├─ MethodInstance for broadcasted(::Base.Broadcast.Style{Tuple}, ::Function, ::NTuple{4, Vector{String}})
│        │  └─ MethodInstance for (Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple}, Axes, F, Args} where Args<:Tuple where F where Axes)(::typeof(isempty), ::Tuple{NTuple{4, Vector{String}}})
│        │     ⋮
│        │     
│        ├─ MethodInstance for instantiate(::Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple}, Nothing, typeof(isempty), Tuple{NTuple{4, Vector{String}}}})
│        ├─ MethodInstance for copy(::Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple}, Nothing, typeof(isempty), Tuple{NTuple{4, Vector{String}}}})
│        ├─ MethodInstance for all(::NTuple{4, Bool})
│        ├─ MethodInstance for broadcasted(::Function, ::Vector{String}, ::Int64)
│        │  └─ MethodInstance for (Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Axes, F, Args} where Args<:Tuple where F where Axes)(::typeof(first), ::Tuple{Vector{String}, Int64})
│        │     ⋮
│        │     
│        ├─ MethodInstance for materialize(::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Nothing, typeof(first), Tuple{Vector{String}, Int64}})
│        ├─ MethodInstance for (::Dates.var"#format##kw")(::NamedTuple{(:locale,), Tuple{String}}, ::typeof(Dates.format), ::DateTime, ::String)
│        │  ├─ MethodInstance for DateFormat{Symbol("e, d u Y"), Tuple{Dates.DatePart{'e'}, Dates.Delim{String, 2}, Dates.DatePart{'d'}, Dates.Delim{Char, 1}, Dates.DatePart{'u'}, Dates.Delim{Char, 1}, Dates.DatePart{'Y'}}}(::Tuple{Dates.DatePart{'e'}, Dates.Delim{String, 2}, Dates.DatePart{'d'}, Dates.Delim{Char, 1}, Dates.DatePart{'u'}, Dates.Delim{Char, 1}, Dates.DatePart{'Y'}}, ::Dates.DateLocale)
│        │  │  ⋮
│        │  │  
│        │  └─ MethodInstance for format(::DateTime, ::DateFormat{Symbol("e, d u Y"), Tuple{Dates.DatePart{'e'}, Dates.Delim{String, 2}, Dates.DatePart{'d'}, Dates.Delim{Char, 1}, Dates.DatePart{'u'}, Dates.Delim{Char, 1}, Dates.DatePart{'Y'}}})
│        │     ⋮
│        │     
│        └─ MethodInstance for println(::IOStream, ::String, ::Vararg{String, N} where N)
├─ MethodInstance for show(::IOBuffer, ::String, ::Nothing)
├─ MethodInstance for var"#open#318"(::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, ::typeof(open), ::Base.var"#323#324"{Vector{UInt8}, Tuple{}}, ::String, ::Vararg{String, N} where N)
├─ MethodInstance for htmlesc(::IOBuffer, ::SubString{String})
└─ MethodInstance for get(::LittleDict{String, Tuple{String, String}, Vector{String}, Vector{Tuple{String, String}}}, ::SubString{String}, ::Symbol)

Each one of those is a separate inference trigger, so your first call to resolve_code_block triggers a whole cascade of non-inferrable calls.

You can call suggest(node), which will print a tree of suggestions, or suggest(node.itrig) for suggestions about a single inference trigger. The suggestion for the root of this tree,

julia> suggest(node.itrig)
/home/tim/.julia/dev/Franklin/src/converter/markdown/blocks.jl:16: non-inferrable call, perhaps annotate convert_block(β::Franklin.AbstractBlock, lxdefs::Vector{Franklin.LxDef}) in Franklin at /home/tim/.julia/dev/Franklin/src/converter/markdown/blocks.jl:8 with type MethodInstance for resolve_code_block(::SubString{String})
If a noninferrable argument is a type or function, Julia's specialization heuristics may be responsible.
immediate caller(s):
1-element Vector{Base.StackTraces.StackFrame}:
 convert_inter_html(ihtml::String, blocks::Vector{Franklin.AbstractBlock}, lxdefs::Vector{Franklin.LxDef}) at md.jl:396
From test at macro expansion at misc.jl:130 [inlined]

indicates that for https://github.com/tlienart/Franklin.jl/blob/7f60ced957512a77b2cda1058aa4349124a48fe7/src/converter/markdown/blocks.jl#L16

Julia didn't know the argument type passed to resolve_code_block was going to be a SubString{String}. Since AbstractBlock is an abstract type, if convert_block gets passed such blocks with unknown types (e.g., elements of a Vector{AbstractBlock}), then you can use https://timholy.github.io/SnoopCompile.jl/stable/snoopr/#Inferrable-field-access-for-abstract-types. I noticed, though, that you're using SubString as a field type for subtypes of AbstractBlock; you might want to declare all those field types String. The time to convert a single SubString{String} to a String (~11ns) is smaller than the typical amount of time needed for runtime dispatch (~30ns), and when you can't infer the type of a variable you might have several runtime dispatches using that same variable. See this performance tip; most the same things that help precompilability and reduce latency also improve runtime performance.

You might be able to get away without conversions by using Union{String,SubString{String}} but that will place higher demands on you if you also want to make things type-stable. And it will inevitably increase latency because you'll have to compile things for both String and SubString{String}.

tlienart commented 3 years ago

Hahaha this is amazing!!! thanks so much Tim, I'm only sorry I cannot be faster at digesting all the insight.

All the resolve_* always work on SubString{String} (Franklin takes a big input string then "discovers" what are the parts to consider (= SubString) and then calls specific functions on these parts) so it should be easy to help the compiler here.

Again this is timely and highly appreciated given that I'm refactoring all this stuff, it's pretty useful to know what the compiler doesn't like to see!

timholy commented 3 years ago

Yeah, if you just declare those fields SubString{String} rather than SubString that will fix a lot of inference problems. No need to use String specifically. Just check isconcretetype on all the types you specify as fields of structs:

julia> isconcretetype(SubString)
false

julia> isconcretetype(SubString{String})
true

When you see true, you're good.

timholy commented 3 years ago

On the master branch of SnoopCompile, you can now filter invalidations on a module recursively, meaning you find any branch of any tree that hits your module. Demo:

julia> using Franklin, SnoopCompileCore

julia> invs = @snoopr include("runtests.jl");
# lots of output, suppressed

julia> using SnoopCompile

julia> trees = invalidation_trees(invs)
# lots of output, suppressed

julia> thinned = filtermod(Franklin, trees; recursive=true)
5-element Vector{SnoopCompile.MethodInvalidations}:
 inserting similar(A::AbstractRange, ::Type{Union{Missing, CategoricalArrays.CategoricalValue{T, R} where R<:Integer}}, dims::Tuple{Vararg{Int64, N}}) where {T, N} in CategoricalArrays at /home/tim/.julia/packages/CategoricalArrays/ZjBSI/src/array.jl:677 invalidated:
   backedges: 1: superseding similar(a::AbstractArray, ::Type{T}, dims::Tuple{Vararg{Int64, N}}) where {T, N} in Base at abstractarray.jl:744 with MethodInstance for similar(::UnitRange{Int64}, ::Type, ::Tuple{Int64}) (1 children)

 inserting similar(A::Vector{T} where T, ::Type{CategoricalArrays.CategoricalValue{T, R} where R<:Integer}) where T in CategoricalArrays at /home/tim/.julia/packages/CategoricalArrays/ZjBSI/src/array.jl:671 invalidated:
   backedges: 1: superseding similar(a::Vector{T}, S::Type) where T in Base at array.jl:355 with MethodInstance for similar(::Vector{_A} where _A, ::Type) (7 children)

 inserting convert(::Type{S}, x::CategoricalArrays.CategoricalValue) where S<:Union{AbstractChar, AbstractString, Number} in CategoricalArrays at /home/tim/.julia/packages/CategoricalArrays/ZjBSI/src/value.jl:73 invalidated:
   mt_backedges: 1: signature Tuple{typeof(convert), Type{Union{SubString{String}, String}}, Any} triggered MethodInstance for convert_block(::Franklin.AbstractBlock, ::Vector{Franklin.LxDef}) (0 children)
                 2: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for var"#resolve_lxobj#164"(::Bool, ::typeof(Franklin.resolve_lxobj), ::Franklin.LxObj, ::Vector{Franklin.LxDef}) (0 children)
                 3: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for hfun_fill(::Vector{String}) (0 children)
                 4: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for var"#resolve_lxobj#164"(::Bool, ::typeof(Franklin.resolve_lxobj), ::Franklin.LxCom, ::Vector{Franklin.LxDef}) (0 children)
                 5: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for convert_html_fblock(::Franklin.HFun) (0 children)
                 6: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for var"#resolve_lxobj#164"(::Bool, ::typeof(Franklin.resolve_lxobj), ::Franklin.LxEnv, ::Vector{Franklin.LxDef}) (0 children)
                 7: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for Franklin.RSSItem(::Any, ::String, ::String, ::String, ::String, ::String, ::String, ::Any) (1 children)
                 8: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for getname(::Franklin.LxCom) (2 children)
                 9: signature Tuple{typeof(convert), Type{String}, Any} triggered MethodInstance for resolve_rpath(::SubString{String}, ::String) (5 children)

 inserting zip(s0::DataStructures.SparseIntSet, s::DataStructures.SparseIntSet...; kwargs...) in DataStructures at /home/tim/.julia/packages/DataStructures/DLSxi/src/sparse_int_set.jl:213 invalidated:
   backedges: 1: superseding zip(a...) in Base.Iterators at iterators.jl:314 with MethodInstance for zip(::Any, ::Any) (186 children)

 inserting similar(::Type{T}, dims::Tuple{Vararg{Int64, N}} where N) where {U, T<:(Array{Union{Missing, CategoricalArrays.CategoricalValue{U, R} where R<:Integer}, N} where N)} in CategoricalArrays at /home/tim/.julia/packages/CategoricalArrays/ZjBSI/src/array.jl:688 invalidated:
   backedges: 1: superseding similar(::Type{T}, dims::Tuple{Vararg{Int64, N}} where N) where T<:AbstractArray in Base at abstractarray.jl:779 with MethodInstance for similar(::Type{Vector{_A}} where _A, ::Tuple{Int64}) (202 children)

# start with the worst one
julia> methinvs = thinned[end]
inserting similar(::Type{T}, dims::Tuple{Vararg{Int64, N}} where N) where {U, T<:(Array{Union{Missing, CategoricalArrays.CategoricalValue{U, R} where R<:Integer}, N} where N)} in CategoricalArrays at /home/tim/.julia/packages/CategoricalArrays/ZjBSI/src/array.jl:688 invalidated:
   backedges: 1: superseding similar(::Type{T}, dims::Tuple{Vararg{Int64, N}} where N) where T<:AbstractArray in Base at abstractarray.jl:779 with MethodInstance for similar(::Type{Vector{_A}} where _A, ::Tuple{Int64}) (202 children)

julia> root = methinvs.backedges[1]
MethodInstance for similar(::Type{Vector{_A}} where _A, ::Tuple{Int64}) at depth 0 with 202 children

julia> ascend(root)
Choose a call for analysis (q to quit):
 >   similar(::Type{Vector{_A}} where _A, ::Tuple{Int64})
       similar(::Type{Vector{_A}} where _A, ::Tuple{Base.OneTo{Int64}})
         _array_for(::Type{T}, ::UnitRange{Int64}, ::Base.HasShape{1}) where T
           process_html_cond(::String, ::Vector{Franklin.AbstractBlock}, ::Int64)
             process_html_qblocks(::String, ::Vector{Franklin.AbstractBlock}, ::Int64, ::Int64)
               process_html_qblocks(::String, ::Vector{Franklin.AbstractBlock})
                 convert_html(::String)
                   #fd2html_v#178(::Bool, ::String, ::Bool, ::typeof(Franklin.fd2html_v), ::SubString{String})
                     fd2html_v(::SubString{String})
v                      #fd2html#179(::Base.Iterators.Pairs, ::typeof(fd2html), ::SubString{String})

You'll recognize process_html_cond as the first method in Franklin. While you can't see it in the above, in the REPL some argument types will be highlighted in red, but process_html_cond does not have any red argument types. That indicates that it is the source of a type-instability that leaves it (and all of its callers) vulnerable to invalidation.

Scroll down to process_html_cond and hit enter, you'll see something like this (posted as screen shot for the color):

image

That Core.Box is your first problem to solve. https://timholy.github.io/SnoopCompile.jl/dev/snoopr/#Fixing-invalidations is the doc page to consult, and there's even a section on Core.Box issues. If you're unfamiliar with Cthulhu, watch the video I link.