< Summary - FSharp.Interop.Dlr

Information
Class: FSharp.Interop.Dlr.SiteCache<T>
Assembly: FSharp.Interop.Dlr
File(s): /home/runner/work/FSharp.Interop.Dlr/FSharp.Interop.Dlr/FSharp.Interop.Dlr/SiteCaches.fs
Line coverage
100%
Covered lines: 25
Uncovered lines: 0
Coverable lines: 25
Total lines: 198
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Sites(...)100%11100%
get_Count()100%11100%
Get(...)100%22100%
At(...)100%11100%

File(s)

/home/runner/work/FSharp.Interop.Dlr/FSharp.Interop.Dlr/FSharp.Interop.Dlr/SiteCaches.fs

#LineLine coverage
 1namespace FSharp.Interop.Dlr
 2
 3open System
 4open System.Dynamic
 5open System.Linq.Expressions
 6open System.Reflection
 7open System.Runtime.CompilerServices
 8open Microsoft.CSharp.RuntimeBinder
 9open FSharp.Quotations
 10
 11/// The call sites of an operation whose binder inputs are only known at run time (`(?) x name`
 12/// with `name` a variable): one set of sites per distinct key, created on first use from a
 13/// quotation template the translator built for the operation, and read back out of it. The
 14/// delegate that invokes them is compiled once, at translation time, with the sites as
 15/// parameters — so a new key costs the binders and sites (~µs, a few hundred bytes), not a
 16/// `LambdaExpression.Compile()`, and a repeated key costs one dictionary lookup. Bounded: at
 17/// `Capacity` entries the cache is cleared and refills, a miss being cheap.
 18/// Not part of the supported API: public only because compiled blocks call it, and it may change in any release.
 19[<System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)>]
 20type SiteCache<'Key when 'Key: equality>(template: 'Key -> Expr) =
 126521    let entries = System.Collections.Concurrent.ConcurrentDictionary<'Key, CallSite[]>()
 22
 23    /// The `CallSite` constants in a template's expression, one per distinct site, in traversal
 24    /// order — the order the translator's parameters follow.
 25    static member Sites(e: Expr) : CallSite list =
 2456526        let found = System.Collections.Generic.List<CallSite>()
 27        let rec walk (e: Expr) =
 28            match e with
 29            | Patterns.Value(v, _) ->
 30                match v with
 31                | :? CallSite as site when not (found |> Seq.exists (fun s -> obj.ReferenceEquals(s, site))) -> found.Ad
 32                | _ -> ()
 33            | ExprShape.ShapeVar _ -> ()
 34            | ExprShape.ShapeLambda(_, body) -> walk body
 35            | ExprShape.ShapeCombination(_, args) -> List.iter walk args
 2456536        walk e
 2456537        List.ofSeq found
 38
 39    /// Entries kept per cache before it is cleared.
 40    static member val Capacity = 256 with get, set
 41
 1242    member _.Count = entries.Count
 43
 44    member _.Get(key: 'Key) : CallSite[] =
 2745645        match entries.TryGetValue key with
 413146        | true, sites -> sites
 47        | _ ->
 48            // Misses only: the admission (clear at capacity, then add) is one critical section,
 49            // so concurrent misses cannot each pass the check and push the count past the bound.
 2332550            lock entries (fun () ->
 2332551                match entries.TryGetValue key with
 552                | true, sites -> sites
 2332553                | _ ->
 2332554                    // A key from data: a null name or type is an argument error here, not a
 2332555                    // NullReferenceException from inside the binder.
 2332056                    match box key with
 2332557                    | :? (string * Type list) as k ->
 331258                        let name, types = k
 331659                        if isNull name then nullArg "a computed member name is null"
 331260                        if isNull (box types) then nullArg "Dlr.typeArgsOf: the list is null"
 661261                        if types |> List.exists isNull then invalidArg "types" "Dlr.typeArgsOf: a type in the list is nu
 2000862                    | _ -> ()
 2338463                    if entries.Count >= SiteCache<'Key>.Capacity then entries.Clear()
 2330864                    let sites = Array.ofList (SiteCache<'Key>.Sites(template key))
 2330865                    entries.[key] <- sites
 4663366                    sites)
 67
 68    /// `sites.[i]`, for the quotation (array indexing has no direct quotation form the converter takes).
 749669    static member At(sites: CallSite[], i: int) : CallSite = sites.[i]
 70
 71/// One compiled `NamedOfCache` shape: its names (an empty name per positional), their hash, the delegate.
 72/// Not part of the supported API: public only because compiled blocks call it, and it may change in any release.
 73[<AllowNullLiteral; System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)>]
 74type NamedOfEntry(names: string[], hash: int, d: Delegate) =
 75    member _.Names = names
 76    member _.Hash = hash
 77    member _.Delegate = d
 78
 79/// For `Dlr.argsOf` / `Dlr.namedOf`: the site's operation compiled once per distinct argument
 80/// shape — the ordered names, an empty name standing for a positional argument (`argsOf`
 81/// values first, then `namedOf` names) — since the shape changes the site's arity, so the
 82/// whole delegate is per key, not only its sites; bounded like `SiteCache`. The delegate takes
 83/// the target, the fixed arguments and the splatted values as one `obj[]` (positional, then
 84/// named).
 85/// Not part of the supported API: public only because compiled blocks call it, and it may change in any release.
 86[<System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)>]
 87type NamedOfCache(compile: string list -> Delegate) =
 88    /// The entries by the hash of their names, an immutable snapshot replaced whole under the
 89    /// lock on a miss, so a lookup reads it without locking; the hash is computed in place from
 90    /// the argument lists, so a lookup allocates nothing.
 91    let mutable entries : System.Collections.Generic.Dictionary<int, NamedOfEntry list> = System.Collections.Generic.Dic
 92    let mutable count = 0
 93    /// The last two shapes served: a site that repeats or alternates shapes hits here in one or
 94    /// two name compares, whatever the capacity.
 95    let mutable last : NamedOfEntry = null
 96    let mutable previous : NamedOfEntry = null
 97
 98    /// Entries kept per cache before it is cleared: a miss is a `Compile()`. The lookup is a
 99    /// hash, so hits cost the same at any size; the bound is memory on a site that fills it
 100    /// (a shape is ~13 KB at one arity: 256 is ~3.5 MB), the same 256 as `SiteCache`.
 101    static member val Capacity = 256 with get, set
 102
 103    /// The most positional arguments `Dlr.argsOf` accepts. Each distinct count is a new call-site
 104    /// arity — a delegate type (past Func's 17 type parameters, one emitted into a non-collectible dynamic
 105    /// assembly), a binder Microsoft.CSharp interns for the life of the process, a `Compile()` —
 106    /// so a count from data must not be unbounded, as a C# call site's arity is fixed by its
 107    /// source. A collection that could be long is one argument, not many.
 108    static member val MaxPositional = 64 with get, set
 109
 110    member _.Count = count
 111
 112    /// The hash of a shape: the positional count, then each name in order.
 113    static member private HashOf(positional: obj list, pairs: (string * obj) list) =
 114        let mutable h = 17
 115        for _ in positional do h <- h * 31
 116        for (n, _) in pairs do h <- h * 31 + (if isNull n then 0 else n.GetHashCode())
 117        h
 118
 119    /// Whether an entry's names are the shape of these arguments: `positional.Length` empty
 120    /// names, then the pairs' names in order. Loops, no closures: the hit path allocates nothing.
 121    static member private Matches(names: string[], positional: obj list, pairs: (string * obj) list) =
 122        let mutable i = 0
 123        let mutable ok = true
 124        let mutable ps = positional
 125        while ok && not ps.IsEmpty do
 126            if i < names.Length && names.[i].Length = 0 then i <- i + 1; ps <- ps.Tail
 127            else ok <- false
 128        let mutable rest = pairs
 129        while ok && not rest.IsEmpty do
 130            let (n, _) = rest.Head
 131            // An empty name is the positional marker in `names`: a pair never matches one.
 132            if i < names.Length && not (String.IsNullOrEmpty n) && String.Equals(n, names.[i]) then i <- i + 1; rest <- 
 133            else ok <- false
 134        ok && i = names.Length
 135
 136    /// The entry of this shape in a bucket, or null: a loop, no closure, no option, so the hash
 137    /// path allocates nothing.
 138    static member private Find(bucket: NamedOfEntry list, hash: int, positional: obj list, pairs: (string * obj) list) :
 139        let mutable rest = bucket
 140        let mutable found : NamedOfEntry = null
 141        while isNull found && not rest.IsEmpty do
 142            let e = rest.Head
 143            if e.Hash = hash && NamedOfCache.Matches(e.Names, positional, pairs) then found <- e
 144            rest <- rest.Tail
 145        found
 146
 147    /// The delegate for these arguments' shape.
 148    member this.Get(positional: obj list, pairs: (string * obj) list) : Delegate =
 149        if isNull (box positional) then nullArg "Dlr.argsOf: the list is null"
 150        if isNull (box pairs) then nullArg "Dlr.namedOf: the list is null"
 151        // The last two shapes served, compared by name first: cheaper than hashing the names.
 152        let l = last
 153        if not (isNull l) && NamedOfCache.Matches(l.Names, positional, pairs) then l.Delegate
 154        else
 155        let p = previous
 156        if not (isNull p) && NamedOfCache.Matches(p.Names, positional, pairs) then previous <- l; last <- p; p.Delegate
 157        else
 158        let hash = NamedOfCache.HashOf(positional, pairs)
 159        let mutable bucket = Unchecked.defaultof<NamedOfEntry list>   // out parameter: no tuple
 160        let hit = if entries.TryGetValue(hash, &bucket) then NamedOfCache.Find(bucket, hash, positional, pairs) else nul
 161        if not (isNull hit) then previous <- last; last <- hit; hit.Delegate
 162        else
 163            lock this (fun () ->
 164                let current = entries
 165                let again = match current.TryGetValue hash with | true, b -> NamedOfCache.Find(b, hash, positional, pair
 166                if not (isNull again) then previous <- last; last <- again; again.Delegate
 167                else
 168                    // Names from data: a null or empty one would be taken for a positional slot.
 169                    for (n, _) in pairs do
 170                        if isNull n then nullArg "Dlr.namedOf: an argument name is null"
 171                        if n.Length = 0 then invalidArg "pairs" "Dlr.namedOf: an argument name is empty (positional argu
 172                    let positionalCount = List.length positional
 173                    if positionalCount > NamedOfCache.MaxPositional then
 174                        invalidArg "positional" (sprintf "Dlr.argsOf: %d positional arguments; at most %d. Each distinct
 175                    let names = (positional |> List.map (fun _ -> "")) @ (pairs |> List.map fst)
 176                    let e = NamedOfEntry(Array.ofList names, hash, compile names)
 177                    let next =
 178                        if count >= NamedOfCache.Capacity then (count <- 0; System.Collections.Generic.Dictionary())
 179                        else System.Collections.Generic.Dictionary(current)
 180                    next.[hash] <- e :: (match next.TryGetValue hash with | true, b -> b | _ -> [])
 181                    count <- count + 1
 182                    entries <- next
 183                    previous <- last
 184                    last <- e
 185                    e.Delegate)
 186
 187    /// The splatted values, positional then named, for the quotation.
 188    static member Values(positional: obj list, pairs: (string * obj) list) : obj[] =
 189        let values = Array.zeroCreate (List.length positional + List.length pairs)
 190        let mutable i = 0
 191        for v in positional do
 192            values.[i] <- v
 193            i <- i + 1
 194        for (_, v) in pairs do
 195            values.[i] <- v
 196            i <- i + 1
 197        values
 198    static member At(values: obj[], i: int) : obj = values.[i]