Оптимизируем работу SQLite с NSCalendar

в 6:16, , рубрики: iOS, ios development, ipad, iphone, sqlite, разработка под iOS, метки: , , , ,

Оптимизируем работу SQLite с NSCalendar Оптимизируем работу SQLite с NSCalendar

В предыдущей статье мы решили проблему некорректного использования SQLite week based calendar, написав свое расширение для этой СУБД.

Наш расчет сошелся, однако скорость его работы оставляла желать лучшего. Обработка таблицы, содержащей всего лишь 2500 записей занимала около 6 секунд. В то время как запросы, использующие strftime() исполнялись за десятые доли секунды.

Прежде чем начать, хочу отметить что здесь и далее для упрощения изложения опущена обработка ошибок и выделение некоторых классов. Прежде чем обсуждать приведенный код в комментариях, не поленитесь заглянуть на страницу проекта dodikk/ESLocale.

В первом приближении наша функция выглядела так:

  1. void ObjcFormatAnsiDateUsingLocale( sqlite3_context* ctx_, int argc_, sqlite3_value** argv_ )
  2. {
  3.     assert( ctx_ ); // на всякий случай
  4.     
  5.     @autoreleasepool // гарантируем возврат ObjC ресурсов.
  6.     {
  7.         // тут могли быть ваши проверки корректности argc_, argv_
  8.         const unsigned char* rawFormat_ = sqlite3_value_text( argv_[0] );
  9.         const unsigned char* rawDate_  = sqlite3_value_text( argv_[1] );
  10.         const unsigned char* rawLocaleIdentifier_ = sqlite3_value_text( argv_[2] );
  11.  
  12.         //эта проверка необходима, дабы избежать crash при переводе строк в NSString
  13.         if ( NULL == rawFormat_ || NULL == rawDate_ || NULL == rawLocaleIdentifier_ )
  14.         {
  15.             sqlite3_result_error( ctx_, "ObjcFormatAnsiDate - NULL argument passed", 3 );
  16.             return;        
  17.         }
  18.         
  19.  
  20.         // Оборачиваем параметры в NSString
  21.         NSString* strDate_ = [ [ NSString alloc ] initWithBytesNoCopy: (void*)rawDate_
  22.                                                               length: strlen( (const char*)rawDate_ )
  23.                                                              encoding: NSUTF8StringEncoding
  24.                                                          freeWhenDone: NO ];
  25.  
  26.         NSString* format_ = [ [ NSString alloc ] initWithBytesNoCopy: (void*)rawFormat_
  27.                                                              length: strlen( (const char*)rawFormat_ )
  28.                                                             encoding: NSUTF8StringEncoding
  29.                                                         freeWhenDone: NO ];
  30.  
  31.         NSString* localeIdentifier_ = [ [ NSString alloc ] initWithBytesNoCopy: (void*)rawLocaleIdentifier_
  32.                                                             length: strlen( (const char*)rawLocaleIdentifier_ )
  33.                                                          encoding: NSUTF8StringEncoding
  34.                                                      freeWhenDone: NO ];
  35.  
  36.  
  37.      
  38.         // для входных данных. Имеет локаль en_US_POSIX и формат даты yyyy-MM-dd
  39.         NSDateFormatter* ansiFormatter_ = [ ESLocaleFactory ansiDateFormatter ];
  40.      
  41.      
  42.         // Для форматирования результата. Имеет локаль и формат, переданный извне
  43.         NSLocale* locale_ = [ [ NSLocale alloc ] initWithLocaleIdentifier: localeIdentifier_ ];
  44.         NSDateFormatter* targetFormatter_ = [ ESLocaleFactory gregorianDateFormatterWithLocale: locale_ ];
  45.         targetFormatter_.dateFormat = format_;
  46.      
  47.         // собственно, преобразование дат
  48.         NSDate* date_ = [ ansiFormatter_ dateFromString: strDate_ ];
  49.         NSString* result_ = [ targetFormatter_ stringFromDate: date_ ];
  50.  
  51.      
  52.      
  53.         // возврат результата
  54.         if ( nil == result_ || [ result_ isEqualToString: @"" ] )
  55.         {    
  56.             sqlite3_result_null( ctx_ );
  57.         }
  58.         else
  59.         {
  60.             sqlite3_result_text
  61.             (
  62.                 ctx_,
  63.                 (const char*)[ result_ cStringUsingEncoding     : NSUTF8StringEncoding ],
  64.                 (int        )[ result_ lengthOfBytesUsingEncoding: NSUTF8StringEncoding ],
  65.                 SQLITE_TRANSIENT // просим SQLite сделать копию строки-результата
  66.             );
  67.         }
  68.     }
  69. }

* This source code was highlighted with Source Code Highlighter.

Прогнав через profiler, мы увидели что большую часть времени занимало не форматирование текста, но создание экземпляров NSDateFormatter (line 39..45).

Модель использования данной функции предполагает смену локали и формата только между запросами. В рамках одного запроса эти параметры скорее всего меняться не будут. Это наводит нас на простую идею оптимизации.

Ресурсозатратные NSDateFormatter мы поместим в некий Singletone объект. Тогда блок форматирования будет иметь следующий вид:

  1. SqlitePersistentDateFormatter* fmt_ = [ SqlitePersistentDateFormatter instance ]; // создаем singletone
  2. NSString* result_ = nil;
  3. @synchronized( fmt_ ) // форматирование должно быть атомарным и потокобезопасным
  4. {
  5.   // обновляем формат и локаль при необходимости
  6.   [ fmt_ setFormat: format_
  7.              locale: localeIdentifier_ ];
  8.  
  9.   // форматируем результат
  10.   result_ = [ fmt_ getFormattedDate: strDate_ ];
  11. }

* This source code was highlighted with Source Code Highlighter.

А теперь пришло время заглянуть под капот. Реализацию потокобезопасного singletone опустим как классическую задачу. В остатке получим следующее:

  1. @implementation SqlitePersistentDateFormatter
  2. {
  3. @private // наши тяжелые объекты
  4.     NSDateFormatter* ansiFormatter ;
  5.     NSDateFormatter* targetFormatter;
  6. }
  7.  
  8. // ansiFormatter не меняется, поскольку он стандартный
  9. // потому создадим его внутри init.
  10. -(id)init
  11. {
  12.     self = [ super init ];
  13.     if ( nil == self )
  14.     {
  15.         return nil;
  16.     }
  17.     
  18.     self->ansiFormatter = [ ESLocaleFactory ansiDateFormatter ];
  19.     
  20.     return self;
  21. }
  22.  
  23.  
  24. // самое интересное тут
  25. -(BOOL)setFormat:( NSString* )dateFormat_
  26.          locale:( NSString* )locale_
  27. {
  28.     NSParameterAssert( nil != locale_ );
  29.     
  30.     BOOL isNoFormatter_ = ( nil == self->targetFormatter );
  31.     BOOL isOtherLocale_ = ![ self->targetFormatter.locale.localeIdentifier isEqualToString: locale_ ];
  32.     
  33.     
  34.     // создаем новый NSDateFormatter только если локаль поменялась
  35.     if ( isNoFormatter_ || isOtherLocale_ )
  36.     {
  37.         NSCalendar* cal_ = [ ESLocaleFactory gregorianCalendarWithLocaleId: locale_ ];     
  38.         
  39.         self->targetFormatter = [ NSDateFormatter new ];
  40.         [ ESLocaleFactory setCalendar: cal_
  41.                      forDateFormatter: self->targetFormatter ]; 
  42.     }
  43.  
  44.     // выставляем дату    
  45.     self->targetFormatter.dateFormat = dateFormat_;
  46.  
  47.     return YES;
  48. }
  49.  
  50.  
  51. // основную работу уже сделали.
  52. // Осталось лишь применить заготовленные NSDateFormatter
  53. -(NSString*)getFormattedDate:( NSString* )strDate_;
  54. {
  55.     NSDate*  date_  = [ self->ansiFormatter  dateFromString: strDate_ ];
  56.     NSString* result_ = [ self->targetFormatter stringFromDate: date_    ];
  57.  
  58.     return result_;
  59. }

* This source code was highlighted with Source Code Highlighter.

Итак, мы получили потокобезопасную, почти чистую в терминах ФП функцию ( pure function ), которая работает за время, сравнимое с strftime.
Полную версию кода вы сможете найти на github

Автор: moborb


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js