@@ -29,9 +29,6 @@ type DesignCacheKey =
2929 string * // SSDT Path
3030 string)) //typeName
3131
32- module DesignTimeCacheSchema =
33- let schemaMap = System.Collections.Concurrent.ConcurrentDictionary< DesignCacheKey* string, ProvidedTypeDefinition>()
34-
3532type internal ParameterValue =
3633 | UserProvided of string * string * Type
3734 | Default of Expr
@@ -466,11 +463,6 @@ module DesignTimeUtils =
466463 |> Seq.map snd
467464 | sproc:: rest -> generateTypeTree con prov ( walkSproc con prov [] None createdTypes sproc) rest
468465
469- let getOrAddSchema ( args : DesignCacheKey ) ( name : string ) =
470- DesignTimeCacheSchema.schemaMap.GetOrAdd(( args, name), fun ( a , nme ) ->
471- let pt = ProvidedTypeDefinition( nme + " Schema" , Some typeof< obj>, isErased= true )
472- pt)
473-
474466 let createDesignTimeCommands ( prov : ISqlProvider ) contextSchemaPath recreate ( invalidate : _ -> Unit )=
475467 let designTimeCommandsContainer = ProvidedTypeDefinition( " DesignTimeCommands" , Some typeof< obj>, isErased= true )
476468 let designTime = ProvidedProperty( " Design Time Commands" , designTimeCommandsContainer, getterCode = empty)
@@ -525,7 +517,6 @@ module DesignTimeUtils =
525517 schemacache.SprocsParams.Clear()
526518 schemacache.Packages.Clear()
527519 schemacache.Individuals.Clear()
528- DesignTimeCacheSchema.schemaMap.Clear()
529520 invalidate()
530521 let pf = recreate()
531522 saveInProcess <- false
@@ -741,6 +732,13 @@ module DesignTimeUtils =
741732 sprocList
742733 generateTypeTree con prov Map.empty sprocs
743734
735+ // Schema container types are per-build on purpose: sharing them across rebuilds would
736+ // mutate types that another (VS) thread may concurrently enumerate, and would
737+ // accumulate duplicate members on every rebuild.
738+ let schemaTypes = System.Collections.Concurrent.ConcurrentDictionary< string, ProvidedTypeDefinition>()
739+ let getOrAddSchema ( name : string ) =
740+ schemaTypes.GetOrAdd( name, fun nme -> ProvidedTypeDefinition( nme + " Schema" , Some typeof< obj>, isErased= true ))
741+
744742 let addServiceTypeMembers ( isReadonly : bool ) =
745743 [
746744 if not isReadonly then
@@ -758,7 +756,7 @@ module DesignTimeUtils =
758756 for ( KeyValue( key,( entityType, desc,_, schema))) in tableTypes do
759757 // collection type, individuals type
760758 let ( ct , it ) = baseCollectionTypes.Force().[ key]
761- let schemaType = getOrAddSchema args schema
759+ let schemaType = getOrAddSchema schema
762760
763761 let templateTable = ProvidedTypeDefinition( ct.Name+ " Template" , Some typeof< obj>, isErased= true )
764762 templateTable.AddMemberDelayed( fun () ->
@@ -1061,7 +1059,6 @@ module DesignTimeUtils =
10611059 | :? ObjectDisposedException -> ()
10621060
10631061 invalidate()
1064- DesignTimeCacheSchema.schemaMap.Clear()
10651062
10661063 GC.Collect()
10671064
@@ -1082,12 +1079,11 @@ module DesignTimeUtils =
10821079 yield designTime :> MemberInfo
10831080
10841081 ] @ [
1085- for KeyValue(( cachedargs, name), pt) in DesignTimeCacheSchema.schemaMap do
1086- if args = cachedargs then
1087- yield pt :> MemberInfo
1088- yield ProvidedProperty( SchemaProjections.buildTableName( name), pt, getterCode = fun args ->
1089- let a0 = args.[ 0 ]
1090- <@@ ((%% a0 : obj) :?> ISqlDataContext) @@> ) :> MemberInfo
1082+ for KeyValue( name, pt) in schemaTypes do
1083+ yield pt :> MemberInfo
1084+ yield ProvidedProperty( SchemaProjections.buildTableName( name), pt, getterCode = fun args ->
1085+ let a0 = args.[ 0 ]
1086+ <@@ ((%% a0 : obj) :?> ISqlDataContext) @@> ) :> MemberInfo
10911087 ]
10921088
10931089 serviceType.AddMembers( addServiceTypeMembers false )
@@ -1292,8 +1288,23 @@ type SqlRuntimeInfo (config : TypeProviderConfig) =
12921288 //| Choice2Of2(paths, errors) -> Assembly.GetExecutingAssembly()
12931289 member __.RuntimeAssembly = runtimeAssembly
12941290
1291+ /// One generation of provided types for one set of static parameters.
1292+ /// Reference equality so that ConcurrentDictionary.TryUpdate swaps compare generations,
1293+ /// not their (concurrently mutated) field values.
1294+ [<ReferenceEquality>]
1295+ type DesignCacheEntry =
1296+ { /// The provided root type, when it was built, and how old the build may get before a
1297+ /// background refresh is started (adapted to build duration on slow systems).
1298+ Root : Lazy < ProvidedTypeDefinition * DateTime * TimeSpan >
1299+ /// Last access in UTC ticks. Updated lock-free on every instantiation request;
1300+ /// entries idle longer than the expiration are dropped to free memory.
1301+ mutable LastAccess : int64
1302+ /// 1 while a background refresh build is in flight: at most one refresh per entry,
1303+ /// no matter how many Visual Studio threads request the type concurrently.
1304+ mutable Refreshing : int }
1305+
12951306module DesignTimeCache =
1296- let cache = System.Collections.Concurrent.ConcurrentDictionary< DesignCacheKey, Lazy < ProvidedTypeDefinition > * DateTime >()
1307+ let cache = System.Collections.Concurrent.ConcurrentDictionary< DesignCacheKey, DesignCacheEntry >()
12971308
12981309/// The idea of this is trying to avoid case where compile-time has loaded non-runtime assembly. (Happens in .NET 8.0, not in .NET Framework.)
12991310/// So let's load compile-time (and design-time) manually the required runtime assembly.
@@ -1450,8 +1461,6 @@ type public SqlTypeProvider(config: TypeProviderConfig) as this =
14501461 let _ = FixReferenceAssemblies.manualLoadNet8Runtime.Force()
14511462#endif
14521463 let sqlRuntimeInfo = SqlRuntimeInfo( config)
1453- let mySaveLock = new Object();
1454- let mutable saveInProcess = false
14551464
14561465 let empty = fun ( _ : Expr list ) -> <@@ () @@>
14571466
@@ -1514,52 +1523,90 @@ type public SqlTypeProvider(config: TypeProviderConfig) as this =
15141523 args.[ 12 ] :?> string, // SSDT Path
15151524 typeName)
15161525
1517- let addCache args =
1518- lazy
1519- let struct ( connectionString , conStringName , dbVendor , resolutionPath , individualsAmount , useOptionTypes , owner , caseSensitivity , tableNames , contextSchemaPath , odbcquote , sqliteLibrary , ssdtPath , rootTypeName ) = args
1520-
1521- let rootType = ProvidedTypeDefinition( sqlRuntimeInfo.RuntimeAssembly, FSHARP_ DATA_ SQL, rootTypeName, Some typeof< obj>, isErased= true )
1522- let serviceType = ProvidedTypeDefinition( " dataContext" , Some typeof< obj>, isErased= true )
1523- let readServiceType = ProvidedTypeDefinition( " readDataContext" , Some typeof< obj>, isErased= true )
1524-
1525- createTypes rootType serviceType readServiceType config sqlRuntimeInfo invalidate registerDispose args
1526- createConstructors config ( rootType, serviceType, readServiceType, args)
1527-
1528- // This is not a perfect cache-invalidation solution, it can remove a valid item from
1529- // cache after the time-out, causing one extra hit, but this is only a design-time cache
1530- // and it will work well enough to deal with Visual Studio's multi-threading problems
1531- let expiration = TimeSpan.FromMinutes 3.0
1532- let rec invalidationFunction key =
1526+ // Entries idle longer than this are dropped to free memory. An actively used entry is
1527+ // never dropped on a timer; instead it is refreshed in the background once its build is
1528+ // older than its staleness interval, so database schema changes are still picked up
1529+ // without IntelliSense ever stalling on a synchronous rebuild.
1530+ let idleExpiration = TimeSpan.FromMinutes 3.0
1531+
1532+ let buildRoot ( args : DesignCacheKey ) =
1533+ let struct ( _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , rootTypeName ) = args
1534+ let buildStarted = DateTime.UtcNow
1535+
1536+ let rootType = ProvidedTypeDefinition( sqlRuntimeInfo.RuntimeAssembly, FSHARP_ DATA_ SQL, rootTypeName, Some typeof< obj>, isErased= true )
1537+ let serviceType = ProvidedTypeDefinition( " dataContext" , Some typeof< obj>, isErased= true )
1538+ let readServiceType = ProvidedTypeDefinition( " readDataContext" , Some typeof< obj>, isErased= true )
1539+
1540+ createTypes rootType serviceType readServiceType config sqlRuntimeInfo invalidate registerDispose args
1541+ createConstructors config ( rootType, serviceType, readServiceType, args)
1542+
1543+ // Refresh no sooner than the idle expiration, and on a slow system no sooner than
1544+ // 5x the time a build takes, so refreshes cannot put the system under pressure.
1545+ let buildDuration = DateTime.UtcNow - buildStarted
1546+ let staleAfter = max idleExpiration ( TimeSpan.FromTicks( buildDuration.Ticks * 5 L))
1547+ rootType, DateTime.UtcNow, staleAfter
1548+
1549+ let dropDesignTimeDcProvider ( key : DesignCacheKey ) =
1550+ // Release the design-time data context provider (used by the Individuals feature)
1551+ // together with its type tree generation, so it cannot pin stale schema in memory.
1552+ let struct ( _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , rootTypeName ) = key
1553+ DcCache.providerCache.TryRemove rootTypeName |> ignore
1554+
1555+ let rec idleWatcher ( key : DesignCacheKey ) =
1556+ async {
1557+ do ! Async.Sleep ( int idleExpiration.TotalMilliseconds)
1558+ match DesignTimeCache.cache.TryGetValue key with
1559+ | true , entry ->
1560+ let lastAccess = DateTime( System.Threading.Interlocked.Read(& entry.LastAccess), DateTimeKind.Utc)
1561+ if DateTime.UtcNow - lastAccess >= idleExpiration then
1562+ DesignTimeCache.cache.TryRemove key |> ignore
1563+ dropDesignTimeDcProvider key
1564+ else
1565+ do ! idleWatcher key
1566+ | _ -> ()
1567+ }
1568+
1569+ let addCache ( key : DesignCacheKey ) =
1570+ { Root =
1571+ lazy
1572+ let generation = buildRoot key
1573+ idleWatcher key |> Async.Start
1574+ generation
1575+ LastAccess = DateTime.UtcNow.Ticks
1576+ Refreshing = 0 }
1577+
1578+ try
1579+ let entry = DesignTimeCache.cache.GetOrAdd( arguments, addCache)
1580+ System.Threading.Interlocked.Exchange(& entry.LastAccess, DateTime.UtcNow.Ticks) |> ignore
1581+
1582+ // Stale-while-revalidate: always serve the current tree; if it has gone stale,
1583+ // rebuild it once in the background and swap the entry atomically. Callers never
1584+ // wait on a refresh, and concurrent VS threads can never start a second one.
1585+ if entry.Root.IsValueCreated then
1586+ let _ , builtAt , staleAfter = entry.Root.Value
1587+ if DateTime.UtcNow - builtAt >= staleAfter
1588+ && System.Threading.Interlocked.CompareExchange(& entry.Refreshing, 1 , 0 ) = 0 then
15331589 async {
1534- do ! Async.Sleep ( int expiration.TotalMilliseconds)
1590+ try
1591+ try
1592+ let freshGeneration = buildRoot arguments
1593+ let freshEntry =
1594+ { Root = Lazy<_>. CreateFromValue freshGeneration
1595+ LastAccess = DateTime.UtcNow.Ticks
1596+ Refreshing = 0 }
1597+ if DesignTimeCache.cache.TryUpdate( arguments, freshEntry, entry) then
1598+ dropDesignTimeDcProvider arguments
1599+ with
1600+ | _ -> () // keep serving the previous generation; retried on a later access
1601+ finally
1602+ System.Threading.Interlocked.Exchange(& entry.Refreshing, 0 ) |> ignore
1603+ } |> Async.Start
15351604
1536- match DesignTimeCache.cache.TryGetValue key with
1537- | true , (_, timestamp) ->
1538- if DateTime.UtcNow - timestamp >= expiration then
1539- DesignTimeCache.cache.TryRemove key |> ignore
1540- else
1541- do ! invalidationFunction key
1542- | _ -> ()
1543-
1544- }
1545- invalidationFunction args |> Async.Start
1546- rootType
1547- , DateTime.UtcNow
1548- try ( DesignTimeCache.cache.GetOrAdd( arguments, addCache) |> fst) .Value
1605+ let root , _ , _ = entry.Root.Value
1606+ root
15491607 with
15501608 | e ->
1551- let _ = DesignTimeCache.cache.TryRemove( arguments)
1552- let _ =
1553- lock mySaveLock ( fun () ->
1554- let keysToClear =
1555- DesignTimeCacheSchema.schemaMap.Keys
1556- |> Seq.toList
1557- |> List.filter( fun ( a , k ) -> a = arguments)
1558- keysToClear |> Seq.iter( fun ak ->
1559- let _ = DesignTimeCacheSchema.schemaMap.TryRemove ak
1560- ()
1561- )
1562- )
1609+ DesignTimeCache.cache.TryRemove( arguments) |> ignore
15631610 reraise()
15641611 )
15651612
0 commit comments