DDAbstractDatabaseLogger.m 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. // Software License Agreement (BSD License)
  2. //
  3. // Copyright (c) 2010-2019, Deusty, LLC
  4. // All rights reserved.
  5. //
  6. // Redistribution and use of this software in source and binary forms,
  7. // with or without modification, are permitted provided that the following conditions are met:
  8. //
  9. // * Redistributions of source code must retain the above copyright notice,
  10. // this list of conditions and the following disclaimer.
  11. //
  12. // * Neither the name of Deusty nor the names of its contributors may be used
  13. // to endorse or promote products derived from this software without specific
  14. // prior written permission of Deusty, LLC.
  15. #import "DDAbstractDatabaseLogger.h"
  16. #if !__has_feature(objc_arc)
  17. #error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
  18. #endif
  19. @interface DDAbstractDatabaseLogger ()
  20. - (void)destroySaveTimer;
  21. - (void)destroyDeleteTimer;
  22. @end
  23. #pragma mark -
  24. @implementation DDAbstractDatabaseLogger
  25. - (instancetype)init {
  26. if ((self = [super init])) {
  27. _saveThreshold = 500;
  28. _saveInterval = 60; // 60 seconds
  29. _maxAge = (60 * 60 * 24 * 7); // 7 days
  30. _deleteInterval = (60 * 5); // 5 minutes
  31. }
  32. return self;
  33. }
  34. - (void)dealloc {
  35. [self destroySaveTimer];
  36. [self destroyDeleteTimer];
  37. }
  38. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  39. #pragma mark Override Me
  40. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  41. - (BOOL)db_log:(__unused DDLogMessage *)logMessage {
  42. // Override me and add your implementation.
  43. //
  44. // Return YES if an item was added to the buffer.
  45. // Return NO if the logMessage was ignored.
  46. return NO;
  47. }
  48. - (void)db_save {
  49. // Override me and add your implementation.
  50. }
  51. - (void)db_delete {
  52. // Override me and add your implementation.
  53. }
  54. - (void)db_saveAndDelete {
  55. // Override me and add your implementation.
  56. }
  57. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  58. #pragma mark Private API
  59. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  60. - (void)performSaveAndSuspendSaveTimer {
  61. if (_unsavedCount > 0) {
  62. if (_deleteOnEverySave) {
  63. [self db_saveAndDelete];
  64. } else {
  65. [self db_save];
  66. }
  67. }
  68. _unsavedCount = 0;
  69. _unsavedTime = 0;
  70. if (_saveTimer && !_saveTimerSuspended) {
  71. dispatch_suspend(_saveTimer);
  72. _saveTimerSuspended = YES;
  73. }
  74. }
  75. - (void)performDelete {
  76. if (_maxAge > 0.0) {
  77. [self db_delete];
  78. _lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0);
  79. }
  80. }
  81. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  82. #pragma mark Timers
  83. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  84. - (void)destroySaveTimer {
  85. if (_saveTimer) {
  86. dispatch_source_cancel(_saveTimer);
  87. if (_saveTimerSuspended) {
  88. // Must resume a timer before releasing it (or it will crash)
  89. dispatch_resume(_saveTimer);
  90. _saveTimerSuspended = NO;
  91. }
  92. #if !OS_OBJECT_USE_OBJC
  93. dispatch_release(_saveTimer);
  94. #endif
  95. _saveTimer = NULL;
  96. }
  97. }
  98. - (void)updateAndResumeSaveTimer {
  99. if ((_saveTimer != NULL) && (_saveInterval > 0.0) && (_unsavedTime > 0.0)) {
  100. uint64_t interval = (uint64_t)(_saveInterval * (NSTimeInterval) NSEC_PER_SEC);
  101. dispatch_time_t startTime = dispatch_time(_unsavedTime, (int64_t)interval);
  102. dispatch_source_set_timer(_saveTimer, startTime, interval, 1ull * NSEC_PER_SEC);
  103. if (_saveTimerSuspended) {
  104. dispatch_resume(_saveTimer);
  105. _saveTimerSuspended = NO;
  106. }
  107. }
  108. }
  109. - (void)createSuspendedSaveTimer {
  110. if ((_saveTimer == NULL) && (_saveInterval > 0.0)) {
  111. _saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue);
  112. dispatch_source_set_event_handler(_saveTimer, ^{ @autoreleasepool {
  113. [self performSaveAndSuspendSaveTimer];
  114. } });
  115. _saveTimerSuspended = YES;
  116. }
  117. }
  118. - (void)destroyDeleteTimer {
  119. if (_deleteTimer) {
  120. dispatch_source_cancel(_deleteTimer);
  121. #if !OS_OBJECT_USE_OBJC
  122. dispatch_release(_deleteTimer);
  123. #endif
  124. _deleteTimer = NULL;
  125. }
  126. }
  127. - (void)updateDeleteTimer {
  128. if ((_deleteTimer != NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) {
  129. int64_t interval = (int64_t)(_deleteInterval * (NSTimeInterval) NSEC_PER_SEC);
  130. dispatch_time_t startTime;
  131. if (_lastDeleteTime > 0) {
  132. startTime = dispatch_time(_lastDeleteTime, interval);
  133. } else {
  134. startTime = dispatch_time(DISPATCH_TIME_NOW, interval);
  135. }
  136. dispatch_source_set_timer(_deleteTimer, startTime, (uint64_t)interval, 1ull * NSEC_PER_SEC);
  137. }
  138. }
  139. - (void)createAndStartDeleteTimer {
  140. if ((_deleteTimer == NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) {
  141. _deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue);
  142. if (_deleteTimer != NULL) {
  143. dispatch_source_set_event_handler(_deleteTimer, ^{ @autoreleasepool {
  144. [self performDelete];
  145. } });
  146. [self updateDeleteTimer];
  147. if (_deleteTimer != NULL) {
  148. dispatch_resume(_deleteTimer);
  149. }
  150. }
  151. }
  152. }
  153. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  154. #pragma mark Configuration
  155. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  156. - (NSUInteger)saveThreshold {
  157. // The design of this method is taken from the DDAbstractLogger implementation.
  158. // For extensive documentation please refer to the DDAbstractLogger implementation.
  159. // Note: The internal implementation MUST access the colorsEnabled variable directly,
  160. // This method is designed explicitly for external access.
  161. //
  162. // Using "self." syntax to go through this method will cause immediate deadlock.
  163. // This is the intended result. Fix it by accessing the ivar directly.
  164. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  165. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  166. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  167. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  168. __block NSUInteger result;
  169. dispatch_sync(globalLoggingQueue, ^{
  170. dispatch_sync(self.loggerQueue, ^{
  171. result = self->_saveThreshold;
  172. });
  173. });
  174. return result;
  175. }
  176. - (void)setSaveThreshold:(NSUInteger)threshold {
  177. dispatch_block_t block = ^{
  178. @autoreleasepool {
  179. if (self->_saveThreshold != threshold) {
  180. self->_saveThreshold = threshold;
  181. // Since the saveThreshold has changed,
  182. // we check to see if the current unsavedCount has surpassed the new threshold.
  183. //
  184. // If it has, we immediately save the log.
  185. if ((self->_unsavedCount >= self->_saveThreshold) && (self->_saveThreshold > 0)) {
  186. [self performSaveAndSuspendSaveTimer];
  187. }
  188. }
  189. }
  190. };
  191. // The design of the setter logic below is taken from the DDAbstractLogger implementation.
  192. // For documentation please refer to the DDAbstractLogger implementation.
  193. if ([self isOnInternalLoggerQueue]) {
  194. block();
  195. } else {
  196. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  197. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  198. dispatch_async(globalLoggingQueue, ^{
  199. dispatch_async(self.loggerQueue, block);
  200. });
  201. }
  202. }
  203. - (NSTimeInterval)saveInterval {
  204. // The design of this method is taken from the DDAbstractLogger implementation.
  205. // For extensive documentation please refer to the DDAbstractLogger implementation.
  206. // Note: The internal implementation MUST access the colorsEnabled variable directly,
  207. // This method is designed explicitly for external access.
  208. //
  209. // Using "self." syntax to go through this method will cause immediate deadlock.
  210. // This is the intended result. Fix it by accessing the ivar directly.
  211. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  212. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  213. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  214. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  215. __block NSTimeInterval result;
  216. dispatch_sync(globalLoggingQueue, ^{
  217. dispatch_sync(self.loggerQueue, ^{
  218. result = self->_saveInterval;
  219. });
  220. });
  221. return result;
  222. }
  223. - (void)setSaveInterval:(NSTimeInterval)interval {
  224. dispatch_block_t block = ^{
  225. @autoreleasepool {
  226. // C99 recommended floating point comparison macro
  227. // Read: isLessThanOrGreaterThan(floatA, floatB)
  228. if (/* saveInterval != interval */ islessgreater(self->_saveInterval, interval)) {
  229. self->_saveInterval = interval;
  230. // There are several cases we need to handle here.
  231. //
  232. // 1. If the saveInterval was previously enabled and it just got disabled,
  233. // then we need to stop the saveTimer. (And we might as well release it.)
  234. //
  235. // 2. If the saveInterval was previously disabled and it just got enabled,
  236. // then we need to setup the saveTimer. (Plus we might need to do an immediate save.)
  237. //
  238. // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date.
  239. //
  240. // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date.
  241. // (Plus we might need to do an immediate save.)
  242. if (self->_saveInterval > 0.0) {
  243. if (self->_saveTimer == NULL) {
  244. // Handles #2
  245. //
  246. // Since the saveTimer uses the unsavedTime to calculate it's first fireDate,
  247. // if a save is needed the timer will fire immediately.
  248. [self createSuspendedSaveTimer];
  249. [self updateAndResumeSaveTimer];
  250. } else {
  251. // Handles #3
  252. // Handles #4
  253. //
  254. // Since the saveTimer uses the unsavedTime to calculate it's first fireDate,
  255. // if a save is needed the timer will fire immediately.
  256. [self updateAndResumeSaveTimer];
  257. }
  258. } else if (self->_saveTimer) {
  259. // Handles #1
  260. [self destroySaveTimer];
  261. }
  262. }
  263. }
  264. };
  265. // The design of the setter logic below is taken from the DDAbstractLogger implementation.
  266. // For documentation please refer to the DDAbstractLogger implementation.
  267. if ([self isOnInternalLoggerQueue]) {
  268. block();
  269. } else {
  270. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  271. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  272. dispatch_async(globalLoggingQueue, ^{
  273. dispatch_async(self.loggerQueue, block);
  274. });
  275. }
  276. }
  277. - (NSTimeInterval)maxAge {
  278. // The design of this method is taken from the DDAbstractLogger implementation.
  279. // For extensive documentation please refer to the DDAbstractLogger implementation.
  280. // Note: The internal implementation MUST access the colorsEnabled variable directly,
  281. // This method is designed explicitly for external access.
  282. //
  283. // Using "self." syntax to go through this method will cause immediate deadlock.
  284. // This is the intended result. Fix it by accessing the ivar directly.
  285. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  286. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  287. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  288. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  289. __block NSTimeInterval result;
  290. dispatch_sync(globalLoggingQueue, ^{
  291. dispatch_sync(self.loggerQueue, ^{
  292. result = self->_maxAge;
  293. });
  294. });
  295. return result;
  296. }
  297. - (void)setMaxAge:(NSTimeInterval)interval {
  298. dispatch_block_t block = ^{
  299. @autoreleasepool {
  300. // C99 recommended floating point comparison macro
  301. // Read: isLessThanOrGreaterThan(floatA, floatB)
  302. if (/* maxAge != interval */ islessgreater(self->_maxAge, interval)) {
  303. NSTimeInterval oldMaxAge = self->_maxAge;
  304. NSTimeInterval newMaxAge = interval;
  305. self->_maxAge = interval;
  306. // There are several cases we need to handle here.
  307. //
  308. // 1. If the maxAge was previously enabled and it just got disabled,
  309. // then we need to stop the deleteTimer. (And we might as well release it.)
  310. //
  311. // 2. If the maxAge was previously disabled and it just got enabled,
  312. // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
  313. //
  314. // 3. If the maxAge was increased,
  315. // then we don't need to do anything.
  316. //
  317. // 4. If the maxAge was decreased,
  318. // then we should do an immediate delete.
  319. BOOL shouldDeleteNow = NO;
  320. if (oldMaxAge > 0.0) {
  321. if (newMaxAge <= 0.0) {
  322. // Handles #1
  323. [self destroyDeleteTimer];
  324. } else if (oldMaxAge > newMaxAge) {
  325. // Handles #4
  326. shouldDeleteNow = YES;
  327. }
  328. } else if (newMaxAge > 0.0) {
  329. // Handles #2
  330. shouldDeleteNow = YES;
  331. }
  332. if (shouldDeleteNow) {
  333. [self performDelete];
  334. if (self->_deleteTimer) {
  335. [self updateDeleteTimer];
  336. } else {
  337. [self createAndStartDeleteTimer];
  338. }
  339. }
  340. }
  341. }
  342. };
  343. // The design of the setter logic below is taken from the DDAbstractLogger implementation.
  344. // For documentation please refer to the DDAbstractLogger implementation.
  345. if ([self isOnInternalLoggerQueue]) {
  346. block();
  347. } else {
  348. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  349. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  350. dispatch_async(globalLoggingQueue, ^{
  351. dispatch_async(self.loggerQueue, block);
  352. });
  353. }
  354. }
  355. - (NSTimeInterval)deleteInterval {
  356. // The design of this method is taken from the DDAbstractLogger implementation.
  357. // For extensive documentation please refer to the DDAbstractLogger implementation.
  358. // Note: The internal implementation MUST access the colorsEnabled variable directly,
  359. // This method is designed explicitly for external access.
  360. //
  361. // Using "self." syntax to go through this method will cause immediate deadlock.
  362. // This is the intended result. Fix it by accessing the ivar directly.
  363. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  364. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  365. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  366. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  367. __block NSTimeInterval result;
  368. dispatch_sync(globalLoggingQueue, ^{
  369. dispatch_sync(self.loggerQueue, ^{
  370. result = self->_deleteInterval;
  371. });
  372. });
  373. return result;
  374. }
  375. - (void)setDeleteInterval:(NSTimeInterval)interval {
  376. dispatch_block_t block = ^{
  377. @autoreleasepool {
  378. // C99 recommended floating point comparison macro
  379. // Read: isLessThanOrGreaterThan(floatA, floatB)
  380. if (/* deleteInterval != interval */ islessgreater(self->_deleteInterval, interval)) {
  381. self->_deleteInterval = interval;
  382. // There are several cases we need to handle here.
  383. //
  384. // 1. If the deleteInterval was previously enabled and it just got disabled,
  385. // then we need to stop the deleteTimer. (And we might as well release it.)
  386. //
  387. // 2. If the deleteInterval was previously disabled and it just got enabled,
  388. // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
  389. //
  390. // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date.
  391. //
  392. // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date.
  393. // (Plus we might need to do an immediate delete.)
  394. if (self->_deleteInterval > 0.0) {
  395. if (self->_deleteTimer == NULL) {
  396. // Handles #2
  397. //
  398. // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
  399. // if a delete is needed the timer will fire immediately.
  400. [self createAndStartDeleteTimer];
  401. } else {
  402. // Handles #3
  403. // Handles #4
  404. //
  405. // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
  406. // if a save is needed the timer will fire immediately.
  407. [self updateDeleteTimer];
  408. }
  409. } else if (self->_deleteTimer) {
  410. // Handles #1
  411. [self destroyDeleteTimer];
  412. }
  413. }
  414. }
  415. };
  416. // The design of the setter logic below is taken from the DDAbstractLogger implementation.
  417. // For documentation please refer to the DDAbstractLogger implementation.
  418. if ([self isOnInternalLoggerQueue]) {
  419. block();
  420. } else {
  421. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  422. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  423. dispatch_async(globalLoggingQueue, ^{
  424. dispatch_async(self.loggerQueue, block);
  425. });
  426. }
  427. }
  428. - (BOOL)deleteOnEverySave {
  429. // The design of this method is taken from the DDAbstractLogger implementation.
  430. // For extensive documentation please refer to the DDAbstractLogger implementation.
  431. // Note: The internal implementation MUST access the colorsEnabled variable directly,
  432. // This method is designed explicitly for external access.
  433. //
  434. // Using "self." syntax to go through this method will cause immediate deadlock.
  435. // This is the intended result. Fix it by accessing the ivar directly.
  436. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  437. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  438. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  439. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  440. __block BOOL result;
  441. dispatch_sync(globalLoggingQueue, ^{
  442. dispatch_sync(self.loggerQueue, ^{
  443. result = self->_deleteOnEverySave;
  444. });
  445. });
  446. return result;
  447. }
  448. - (void)setDeleteOnEverySave:(BOOL)flag {
  449. dispatch_block_t block = ^{
  450. self->_deleteOnEverySave = flag;
  451. };
  452. // The design of the setter logic below is taken from the DDAbstractLogger implementation.
  453. // For documentation please refer to the DDAbstractLogger implementation.
  454. if ([self isOnInternalLoggerQueue]) {
  455. block();
  456. } else {
  457. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  458. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  459. dispatch_async(globalLoggingQueue, ^{
  460. dispatch_async(self.loggerQueue, block);
  461. });
  462. }
  463. }
  464. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  465. #pragma mark Public API
  466. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  467. - (void)savePendingLogEntries {
  468. dispatch_block_t block = ^{
  469. @autoreleasepool {
  470. [self performSaveAndSuspendSaveTimer];
  471. }
  472. };
  473. if ([self isOnInternalLoggerQueue]) {
  474. block();
  475. } else {
  476. dispatch_async(self.loggerQueue, block);
  477. }
  478. }
  479. - (void)deleteOldLogEntries {
  480. dispatch_block_t block = ^{
  481. @autoreleasepool {
  482. [self performDelete];
  483. }
  484. };
  485. if ([self isOnInternalLoggerQueue]) {
  486. block();
  487. } else {
  488. dispatch_async(self.loggerQueue, block);
  489. }
  490. }
  491. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  492. #pragma mark DDLogger
  493. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  494. - (void)didAddLogger {
  495. // If you override me be sure to invoke [super didAddLogger];
  496. [self createSuspendedSaveTimer];
  497. [self createAndStartDeleteTimer];
  498. }
  499. - (void)willRemoveLogger {
  500. // If you override me be sure to invoke [super willRemoveLogger];
  501. [self performSaveAndSuspendSaveTimer];
  502. [self destroySaveTimer];
  503. [self destroyDeleteTimer];
  504. }
  505. - (void)logMessage:(DDLogMessage *)logMessage {
  506. if ([self db_log:logMessage]) {
  507. BOOL firstUnsavedEntry = (++_unsavedCount == 1);
  508. if ((_unsavedCount >= _saveThreshold) && (_saveThreshold > 0)) {
  509. [self performSaveAndSuspendSaveTimer];
  510. } else if (firstUnsavedEntry) {
  511. _unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0);
  512. [self updateAndResumeSaveTimer];
  513. }
  514. }
  515. }
  516. - (void)flush {
  517. // This method is invoked by DDLog's flushLog method.
  518. //
  519. // It is called automatically when the application quits,
  520. // or if the developer invokes DDLog's flushLog method prior to crashing or something.
  521. [self performSaveAndSuspendSaveTimer];
  522. }
  523. @end