@ -41,6 +41,12 @@ open class Storage {
// / t h i s s h o u l d b e t a k e n i n t o c o n s i d e r a t i o n w h e n u s e d
public private ( set ) var isSuspendedUnsafe : Bool = false
// / T h i s p r o p e r t y g e t s s e t t h e f i r s t t i m e w e s u c c e s s f u l l y r e a d f r o m t h e d a t a b a s e
public private ( set ) var hasSuccessfullyRead : Bool = false
// / T h i s p r o p e r t y g e t s s e t t h e f i r s t t i m e w e s u c c e s s f u l l y w r i t e t o t h e d a t a b a s e
public private ( set ) var hasSuccessfullyWritten : Bool = false
public var hasCompletedMigrations : Bool { migrationsCompleted . wrappedValue }
public var currentlyRunningMigration : ( identifier : TargetMigrations . Identifier , migration : Migration . Type ) ? {
internalCurrentlyRunningMigration . wrappedValue
@ -456,37 +462,60 @@ open class Storage {
// MARK: - L o g g i n g F u n c t i o n s
typealias CallInfo = ( file : String , function : String , line : Int )
private static func logSlowWrites < T > (
private enum Action {
case read
case write
case logIfSlow
}
private typealias CallInfo = ( storage : Storage ? , actions : [ Action ] , file : String , function : String , line : Int )
private static func perform < T > (
info : CallInfo ,
updates : @ escaping ( Database ) throws -> T
) -> ( Database ) throws -> T {
return { db in
let start : CFTimeInterval = CACurrentMediaTime ( )
let actionName : String = ( info . actions . contains ( . write ) ? " write " : " read " )
let fileName : String = ( info . file . components ( separatedBy : " / " ) . last . map { " \( $0 ) : \( info . line ) " } ? ? " " )
let timeout : Timer = Timer . scheduledTimerOnMainThread ( withTimeInterval : writeWarningThreadshold ) {
$0 . invalidate ( )
let timeout : Timer ? = {
guard info . actions . contains ( . logIfSlow ) else { return nil }
// D o n ' t w a n t t o l o g o n t h e m a i n t h r e a d a s t o a v o i d c o n f u s i o n w h e n d e b u g g i n g i s s u e s
DispatchQueue . global ( qos : . default ) . async {
SNLog ( " [Storage \( fileName ) ] Slow write taking longer than \( writeWarningThreadshold , format : " .2 " , omitZeroDecimal : true ) s - \( info . function ) " )
return Timer . scheduledTimerOnMainThread ( withTimeInterval : Storage . writeWarningThreadshold ) {
$0 . invalidate ( )
// D o n ' t w a n t t o l o g o n t h e m a i n t h r e a d a s t o a v o i d c o n f u s i o n w h e n d e b u g g i n g i s s u e s
DispatchQueue . global ( qos : . default ) . async {
SNLog ( " [Storage \( fileName ) ] Slow \( actionName ) taking longer than \( Storage . writeWarningThreadshold , format : " .2 " , omitZeroDecimal : true ) s - \( info . function ) " )
}
}
}
} ( )
// I f w e t i m e d o u t a n d a r e l o g g i n g s l o w a c t i o n s t h e n l o g t h e a c t u a l d u r a t i o n t o h e l p u s
// p r i o r i t i s e p e r f o r m a n c e i s s u e s
defer {
// I f w e t i m e d o u t t h e n l o g t h e a c t u a l d u r a t i o n t o h e l p u s p r i o r i t i s e p e r f o r m a n c e i s s u e s
if ! timeout . isValid {
if timeout != nil && timeout ? . isValid = = false {
let end : CFTimeInterval = CACurrentMediaTime ( )
DispatchQueue . global ( qos : . default ) . async {
SNLog ( " [Storage \( fileName ) ] Slow write completed after \( end - start , format : " .2 " , omitZeroDecimal : true ) s " )
SNLog ( " [Storage \( fileName ) ] Slow \( actionName ) completed after \( end - start , format : " .2 " , omitZeroDecimal : true ) s " )
}
}
timeout .invalidate ( )
timeout ? .invalidate ( )
}
return try updates ( db )
// G e t t h e r e s u l t
let result : T = try updates ( db )
// U p d a t e t h e s t a t e f l a g s
switch info . actions {
case [ . write ] , [ . write , . logIfSlow ] : info . storage ? . hasSuccessfullyWritten = true
case [ . read ] , [ . read , . logIfSlow ] : info . storage ? . hasSuccessfullyRead = true
default : break
}
return result
}
}
@ -516,9 +545,8 @@ open class Storage {
) -> T ? {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else { return nil }
let info : CallInfo = ( fileName , functionName , lineNumber )
do { return try dbWriter . write ( Storage . logSlowWrites ( info : info , updates : updates ) ) }
let info : CallInfo = { [ weak self ] in ( self , [ . write , . logIfSlow ] , fileName , functionName , lineNumber ) } ( )
do { return try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) }
catch { return Storage . logIfNeeded ( error , isWrite : true ) }
}
@ -549,10 +577,10 @@ open class Storage {
) {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else { return }
let info : CallInfo = (fileName , functionName , lineNumber )
let info : CallInfo = { [ weak self ] in (self , [ . write , . logIfSlow ] , fileName , functionName , lineNumber ) } ( )
dbWriter . asyncWrite (
Storage . logSlowWrites ( info : info , updates : updates ) ,
Storage . perform ( info : info , updates : updates ) ,
completion : { db , result in
switch result {
case . failure ( let error ) : Storage . logIfNeeded ( error , isWrite : true )
@ -576,7 +604,7 @@ open class Storage {
. eraseToAnyPublisher ( )
}
let info : CallInfo = (fileName , functionName , lineNumber )
let info : CallInfo = { [ weak self ] in (self , [ . write , . logIfSlow ] , fileName , functionName , lineNumber ) } ( )
// / * * N o t e : * * G R D B d o e s h a v e a ` w r i t e P u b l i s h e r ` m e t h o d b u t i t a p p e a r s t o a s y n c h r o n o u s l y t r i g g e r
// / b o t h t h e ` o u t p u t ` a n d ` c o m p l e t e ` c l o s u r e s a t t h e s a m e t i m e w h i c h c a u s e s a l o t o f u n e x p e c t e d
@ -587,7 +615,7 @@ open class Storage {
// / w h i c h b e h a v e s i n a m u c h m o r e e x p e c t e d w a y t h a n t h e G R D B ` w r i t e P u b l i s h e r ` d o e s
return Deferred {
Future { resolver in
do { resolver ( Result . success ( try dbWriter . write ( Storage . logSlowWrites ( info : info , updates : updates ) ) ) ) }
do { resolver ( Result . success ( try dbWriter . write ( Storage . perform ( info : info , updates : updates ) ) ) ) }
catch {
Storage . logIfNeeded ( error , isWrite : true )
resolver ( Result . failure ( error ) )
@ -597,6 +625,9 @@ open class Storage {
}
open func readPublisher < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
value : @ escaping ( Database ) throws -> T
) -> AnyPublisher < T , Error > {
@ -605,6 +636,8 @@ open class Storage {
. eraseToAnyPublisher ( )
}
let info : CallInfo = { [ weak self ] in ( self , [ . read ] , fileName , functionName , lineNumber ) } ( )
// / * * N o t e : * * G R D B d o e s h a v e a ` r e a d P u b l i s h e r ` m e t h o d b u t i t a p p e a r s t o a s y n c h r o n o u s l y t r i g g e r
// / b o t h t h e ` o u t p u t ` a n d ` c o m p l e t e ` c l o s u r e s a t t h e s a m e t i m e w h i c h c a u s e s a l o t o f u n e x p e c t e d
// / b e h a v i o u r s ( t h i s b e h a v i o u r i s a p p a r e n t l y e x p e c t e d b u t s t i l l c a u s e s a n u m b e r o f o d d b e h a v i o u r s i n o u r c o d e
@ -614,7 +647,7 @@ open class Storage {
// / w h i c h b e h a v e s i n a m u c h m o r e e x p e c t e d w a y t h a n t h e G R D B ` r e a d P u b l i s h e r ` d o e s
return Deferred {
Future { resolver in
do { resolver ( Result . success ( try dbWriter . read ( value) ) ) }
do { resolver ( Result . success ( try dbWriter . read ( Storage. perform ( info : info , updates : value) ) ) ) }
catch {
Storage . logIfNeeded ( error , isWrite : false )
resolver ( Result . failure ( error ) )
@ -624,12 +657,16 @@ open class Storage {
}
@ discardableResult public func read < T > (
fileName : String = #file ,
functionName : String = #function ,
lineNumber : Int = #line ,
using dependencies : Dependencies = Dependencies ( ) ,
_ value : ( Database ) throws -> T ?
_ value : @escaping (Database ) throws -> T ?
) -> T ? {
guard isValid , let dbWriter : DatabaseWriter = dbWriter else { return nil }
do { return try dbWriter . read ( value ) }
let info : CallInfo = { [ weak self ] in ( self , [ . read ] , fileName , functionName , lineNumber ) } ( )
do { return try dbWriter . read ( Storage . perform ( info : info , updates : value ) ) }
catch { return Storage . logIfNeeded ( error , isWrite : false ) }
}