Skip to content

Conversation

@timholy
Copy link
Member

@timholy timholy commented Mar 30, 2025

This is a rewrite of the invalidations infrastructure based on Julia 1.12+. Julia 1.12 separates the data into two logging streams, one for immediate method insertion/deletion and the other for edge-validation during package loading. This allows a cleaner implementation of the parsing. I've also invested in a more extensive and systematic test suite, attempting to cover all code paths. There are still some missing cases though, so far notably the binding invalidations and "method_globalref" invalidations (which I've not yet succeeded in generating).

A key feature of the new test architecture is that it's designed to try to check that you can get the same invalidation trees in either immediate or edge-validation mode. Currently, though, the edge-validation is failing due to a failure to cache all the required compilation in the InvalidB test module.

There are also a couple of changes directed at @snoop_inference, but once I discovered that the invalidations needed updating too, I decided to start there. I'll probably strip those out before merging, and submit those changes in a separate PR.

@timholy timholy marked this pull request as draft March 30, 2025 13:04
@timholy
Copy link
Member Author

timholy commented Mar 30, 2025

CC @vtjnash

@timholy
Copy link
Member Author

timholy commented Apr 3, 2025

With JuliaLang/julia#57999, this is getting there. Here's what the pattern of invalidations looks like, at a high level, for method insertion invalidations:

3-element Vector{SnoopCompile.MethodInvalidations}:
 inserting f(::String) @ Main ~/.julia/dev/SnoopCompile/test/snoop_invalidations_new.jl:162 invalidated:
   mt_backedges: 1: signature Tuple{typeof(Main.MethodLogs.f), String} triggered MethodInstance for Main.MethodLogs.callsf(::String) (1 children)

 inserting f(::Signed) @ Main ~/.julia/dev/SnoopCompile/test/snoop_invalidations_new.jl:163 invalidated:
   backedges: 1: superseding f(::Integer) @ Main.MethodLogs ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for Main.MethodLogs.f(::Integer) (0 children)
              2: superseding f(::Integer) @ Main.MethodLogs ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for Main.MethodLogs.f(::Signed) (0 children)
              3: superseding f(::Integer) @ Main.MethodLogs ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for Main.MethodLogs.f(::Int64) (1 children)

 inserting f(::Int64) @ Main ~/.julia/dev/SnoopCompile/test/snoop_invalidations_new.jl:161 invalidated:
   mt_backedges: 1: signature Tuple{typeof(Main.MethodLogs.f), Real} triggered MethodInstance for Main.MethodLogs.callsfrtr(::Int64) (0 children)
                 2: signature Tuple{typeof(Main.MethodLogs.f), Any} triggered MethodInstance for Main.MethodLogs.callsfrta(::Int64) (1 children)
   backedges: 1: superseding f(::Integer) @ Main.MethodLogs ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for Main.MethodLogs.f(::Signed) (1 children)
              2: superseding f(::Integer) @ Main.MethodLogs ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for Main.MethodLogs.f(::Integer) (2 children)
              3: superseding f(::Integer) @ Main.MethodLogs ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for Main.MethodLogs.f(::Int64) (3 children)
   1 mt_cache

and here is the same thing for the corresponding edge invalidations:

3-element Vector{SnoopCompile.MethodInvalidations}:
 inserting f(::String) @ InvalidC ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidC/src/InvalidC.jl:5 invalidated:
   mt_backedges: 1: signature Tuple{typeof(InvalidA.f), String} triggered MethodInstance for InvalidA.callsf(::String) (1 children)

 inserting f(::Int64) @ InvalidC ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidC/src/InvalidC.jl:4 invalidated:
   mt_backedges: 1: signature Tuple{typeof(InvalidA.f), Signed} triggered MethodInstance for InvalidA.invokesfs(::Int64) (0 children)
   backedges: 1: superseding f(::Integer) @ InvalidA ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for InvalidA.f(::Signed) (1 children)
              2: superseding f(::Integer) @ InvalidA ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for InvalidA.f(::Integer) (3 children)
              3: superseding f(::Integer) @ InvalidA ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for InvalidA.f(::Int64) (3 children)

 inserting f(::Signed) @ InvalidC ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidC/src/InvalidC.jl:6 invalidated:
   mt_backedges: 1: signature Tuple{typeof(InvalidA.f), Signed} triggered MethodInstance for InvalidA.invokesfs(::Int64) (0 children)
   backedges: 1: superseding f(::Integer) @ InvalidA ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for InvalidA.f(::Signed) (1 children)
              2: superseding f(::Integer) @ InvalidA ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for InvalidA.f(::Integer) (3 children)
              3: superseding f(::Integer) @ InvalidA ~/.julia/dev/SnoopCompile/test/testmodules/Invalidation/InvalidA/src/pkgdef.jl:1 with MethodInstance for InvalidA.f(::Int64) (3 children)

They are very similar, for example

julia> SnoopCompile.AbstractTrees.print_tree(etrees[2].backedges[end])  # etrees is the list of trees from edge invalidations
MethodInstance for InvalidA.f(::Int64) at depth 1 with 3 children
├─ MethodInstance for InvalidA.alsocallsf(::Int64) at depth 2 with 0 children
└─ MethodInstance for InvalidA.callsf(::Int64) at depth 2 with 1 children
   └─ MethodInstance for InvalidA.callscallsf(::Int64) at depth 3 with 0 children

julia> SnoopCompile.AbstractTrees.print_tree(mtrees[3].backedges[end])   # mtrees is the list of trees from method-insertion invalidations
MethodInstance for Main.MethodLogs.f(::Int64) at depth 0 with 3 children
├─ MethodInstance for Main.MethodLogs.alsocallsf(::Int64) at depth 1 with 0 children
└─ MethodInstance for Main.MethodLogs.callsf(::Int64) at depth 1 with 1 children
   └─ MethodInstance for Main.MethodLogs.callscallsf(::Int64) at depth 2 with 0 children

but

julia> SnoopCompile.AbstractTrees.print_tree(etrees[2].backedges[end-1])
MethodInstance for InvalidA.f(::Integer) at depth 1 with 3 children
├─ MethodInstance for InvalidA.callsfrtr(::Int64) at depth 2 with 0 children
└─ MethodInstance for InvalidA.callsfrta(::Int64) at depth 2 with 1 children
   └─ MethodInstance for InvalidA.callscallsfrta(::Int64) at depth 3 with 0 children

julia> SnoopCompile.AbstractTrees.print_tree(mtrees[3].backedges[end-1])
MethodInstance for Main.MethodLogs.f(::Integer) at depth 0 with 2 children
├─ MethodInstance for Main.MethodLogs.callsfrta(::Int64) at depth 1 with 0 children
└─ MethodInstance for Main.MethodLogs.callsfrtr(::Int64) at depth 1 with 0 children

Some of the differences make sense: for example, for method insertion, the first-to-be-inserted method triggers the invalidation, whereas for edge invalidations we attribute the invalidation to a list of matches. This is why for the edge invalidation, the trees for f(::Int) and f(::Signed) are identical, because either could have triggered all the same invalidations.

I'm wondering if some of the other differences, like signatures in mt_backedges, are worth harmonizing, or whether this is fine as is.

These will be a separate PR
@timholy timholy force-pushed the teh/julia_nightly branch from a33fb9f to 469db0f Compare April 10, 2025 12:30
@timholy timholy marked this pull request as ready for review July 16, 2025 16:45
timholy added 2 commits July 16, 2025 11:58
Enable this after 1.12 is out
@timholy timholy changed the title WIP: new invalidations New invalidations Jul 16, 2025
@timholy timholy merged commit 169a290 into dev Jul 16, 2025
1 of 8 checks passed
@timholy timholy deleted the teh/julia_nightly branch July 16, 2025 17:27
timholy added a commit that referenced this pull request Sep 3, 2025
This is a rewrite of the invalidations infrastructure based on Julia 1.12+. Julia 1.12 separates the data into two logging streams, one for immediate method insertion/deletion and the other for edge-validation during package loading. This allows a cleaner implementation of the parsing. I've also invested in a more extensive and systematic test suite, attempting to cover all code paths, and added tests for binding invalidation. Finally, this expands the documentation on developer topics.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants