Slide 1

Slide 1 text

CoreData наносит ответный удар с вертушки

Slide 2

Slide 2 text

CoreData • The Core Data framework provides generalized and automated solutions to common tasks associated with object life-cycle and object graph management, including persistence. • Не база данных

Slide 3

Slide 3 text

CoreData performance

Slide 4

Slide 4 text

Откуда ждать проблем? • Игнорироание рекомендаций Apple • Многопоточность • Неоптимизированные запросы • CoreData cache misses • Неожиданные выборки • Неожиданное поведение

Slide 5

Slide 5 text

Импорт данных

Slide 6

Slide 6 text

Есть JSON @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] };

Slide 7

Slide 7 text

@{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; Есть JSON

Slide 8

Slide 8 text

@{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; Есть JSON @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; … @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] };

Slide 9

Slide 9 text

@{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; Есть JSON @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; … @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; @{ /*Person*/ @"id": @123456789, @"name": @"Johny Ive", @"email": @"[email protected]", @"phones": @[ @{ @"id": @12345, @"number": @"+3801234567", @"timestamp": @"12-11-2014" }, ] }; Всего 1500 уникальных объектов • 250 Person • 1250 Phones

Slide 10

Slide 10 text

Есть MagicalRecord • https://github.com/magicalpanda/MagicalRecord/ • 4523 start • 666 commits • 52 contributors • 782 forks

Slide 11

Slide 11 text

Есть MagicalRecord • https://github.com/magicalpanda/MagicalRecord/ • 4523 start • 666 commits • 52 contributors • 782 forks

Slide 12

Slide 12 text

Также имеется в наличии • Xcode 5.1.1 • iPhone 5s • Схема Release • 20 итераций для каждого теста • Time Profiler

Slide 13

Slide 13 text

Модель Person ————————— int64 personID (PK, no index); NSString *name; NSString *email; ————————— Phones Phone ————————— int64 phoneID (PK, no index) NSString *ddi; NSString *ddd; NSString *number; NSString *timestamp; 1 *

Slide 14

Slide 14 text

Время импорта Insert Update t, sec 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 4.375 4.861

Slide 15

Slide 15 text

– Time Profile “Ну и тормозит”

Slide 16

Slide 16 text

Код импорта (псевдо) + (id)importObject:(NSDictionary *)JSON { id object = [self goToDatabaseAndFindObjectForPrimaryKey:JSON[@"primaryKey"]]; if (object == nil) { object = [self createObject]; } [object importAttributes:JSON]; [object importRelationships:JSON]; return object; } ! + (void)importObjects:(NSArray *)objects { for (id object in objects) { [self importObject:object]; } }

Slide 17

Slide 17 text

Time profiler #1

Slide 18

Slide 18 text

Time profiler #2

Slide 19

Slide 19 text

Improvement #1 • Проиндексируем Primary keys

Slide 20

Slide 20 text

Improvement #1 • Проиндексируем Primary keys

Slide 21

Slide 21 text

Время импорта PK Indexed Insert Update t, sec 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 4.375 4.294

Slide 22

Slide 22 text

Код импорта даты NSDate * dateFromString(NSString *value, NSString *format) { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setTimeZone:[NSTimeZone localTimeZone]]; [formatter setLocale:[NSLocale currentLocale]]; [formatter setDateFormat:format]; NSDate *parsedDate = [formatter dateFromString:value]; return parsedDate; }

Slide 23

Slide 23 text

подправим..

Slide 24

Slide 24 text

подправим.. следующий код: Потоконебезопасный Не освобождает память Это только для примера ! пожалуйста, не используйте его

Slide 25

Slide 25 text

подправим.. NSDate * dateFromString(NSString *value, NSString *format) { static NSMutableDictionary *map = nil; static dispatch_once_t token; dispatch_once(&token, ^{ map = [NSMutableDictionary new]; }); NSDateFormatter *formatter = [map objectForKey:format]; if (!formatter) { formatter = [[NSDateFormatter alloc] init]; [formatter setTimeZone:[NSTimeZone localTimeZone]]; [formatter setLocale:[NSLocale currentLocale]]; [formatter setDateFormat:format]; [map setObject:formatter forKey:format]; } NSDate *parsedDate = [formatter dateFromString:value]; return parsedDate; }

Slide 26

Slide 26 text

Время импорта PK Index, NSDateFormatter cache Insert Update t, sec 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 3.356 3.254

Slide 27

Slide 27 text

Взгляд со стороны CoreData Instruments

Slide 28

Slide 28 text

CoreData Instruments #1 Fetches CPU Usage

Slide 29

Slide 29 text

CoreData Instruments #1 Fetches CPU Usage

Slide 30

Slide 30 text

Есть MagicalRecord fork • https://github.com/seepel/MagicalRecord Fetches CPU Usage fork

Slide 31

Slide 31 text

Есть MagicalRecord fork • https://github.com/seepel/MagicalRecord Fetches CPU Usage fork Fetches CPU Usage original

Slide 32

Slide 32 text

Время импорта PK Index, NSDateFormatter cache, batch fetch Insert Update t, sec 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 1.337 1.236

Slide 33

Slide 33 text

Когда уже будет что-то интересное..

Slide 34

Slide 34 text

Insert Person *object = // find or create [object setPersonID:JSON[@"personIdentifier"]]; // . . . ! [object.managedObjectContext save:&error];

Slide 35

Slide 35 text

Insert // Find sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZEMAIL, t0.ZGENDER, t0.ZNAME, t0.ZPERSONID, t0.ZTIMESTAMP FROM ZPERSON t0 WHERE t0.ZPERSONID IN (?) ! // Save sql: INSERT INTO ZPERSON(Z_PK, Z_ENT, Z_OPT, ZEMAIL, ZGENDER, ZNAME, ZPERSONID, ZTIMESTAMP) VALUES(?, ?, ?, ?, ?, ?, ?, ?) Person *object = // find or create [object setPersonID:JSON[@"personIdentifier"]]; // . . . ! [object.managedObjectContext save:&error];

Slide 36

Slide 36 text

Update // Find sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZEMAIL, t0.ZGENDER, t0.ZNAME, t0.ZPERSONID, t0.ZTIMESTAMP FROM ZPERSON t0 WHERE t0.ZPERSONID IN (?) ! // Save sql: UPDATE ZPERSON SET Z_OPT = ? WHERE Z_PK = ? AND Z_OPT = ? Person *object = // find [object setPersonID:equalPersonID]; // . . . ! [object.managedObjectContext save:&error];

Slide 37

Slide 37 text

Проверка на равенство - (void)MR_setValueIfDifferent:(id)value forKey:(NSString *)key { id currentValue = [self valueForKey:key]; if(currentValue == nil && value == nil) { return; } if((currentValue == nil && value != nil) || (currentValue != nil && value == nil)) { [self setValue:value forKey:key]; return; } if(![currentValue isEqual:value]) { [self setValue:value forKey:key]; } }

Slide 38

Slide 38 text

Update // Find sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZEMAIL, t0.ZGENDER, t0.ZNAME, t0.ZPERSONID, t0.ZTIMESTAMP FROM ZPERSON t0 WHERE t0.ZPERSONID IN (?) ! // Save Person *object = // fetch from database [object setPersonID:equalPersonID]; // . . . ! [object.managedObjectContext save:&error]; incl. equality check

Slide 39

Slide 39 text

Время импорта PK Index, NSDateFormatter cache, batch fetch, equality check Insert Update t, sec 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 1.339 0.756

Slide 40

Slide 40 text

Что у нас есть Fetches CPU Usage MagicalRecord fork

Slide 41

Slide 41 text

Что у нас есть Fetches CPU Usage MagicalRecord fork

Slide 42

Slide 42 text

Почему бы не сделать так? Fetches CPU Usage

Slide 43

Slide 43 text

Как работает MR fork: @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 44

Slide 44 text

Как работает MR fork: @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 45

Slide 45 text

Как работает MR fork: @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 46

Slide 46 text

Как работает MR fork: @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 47

Slide 47 text

Как работает MR fork: @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 48

Slide 48 text

Как можно ускорить @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 49

Slide 49 text

Как можно ускорить @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 50

Slide 50 text

Как можно ускорить @[ @{ @"id": @123456, /* ... */ @"phones": @[ @{@"id": @123, /* ... */ }, @{@"id": @1234, /* ... */ }, @{@"id": @12345, /* ... */ }, ], }, @{ @"id": @1300156, /* ... */ @"phones": @[ @{@"id": @5664523, /* ... */ }, @{@"id": @777412, /* ... */ }, @{@"id": @33345, /* ... */ }, ], }, /* ... */ ];

Slide 51

Slide 51 text

Время импорта PK Index, NSDateFormatter cache, improved batch fetch, equality check Insert Update t, sec 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 0.173 0.081

Slide 52

Slide 52 text

FastMappingKit • https://github.com/Yalantis/FastEasyMapping • pod "FastEasyMapping", :git => "https://github.com/Yalantis/FastEasyMapping.git"

Slide 53

Slide 53 text

FastMappingKit • https://github.com/Yalantis/FastEasyMapping • pod "FastEasyMapping", :git => "https://github.com/Yalantis/FastEasyMapping.git" + (EMKManagedObjectMapping *)personWithPhonesMapping { return [EMKManagedObjectMapping mappingForEntityName:@"Person" configuration:^(EMKManagedObjectMapping *mapping) { [mapping setPrimaryKey:@"personID"]; [mapping addAttributeMappingDictionary:@{@"personID": @"id"}]; [mapping addAttributeMappingFromArray:@[@"name", @"email"]]; [mapping addAttributeMapping:[EMKAttributeMapping mappingOfProperty:@"timestamp" keyPath:@"timestamp" dateFormat:@"dd-MM-yyyy"]]; ! [mapping addToManyRelationshipMapping:[self phoneMapping] forProperty:@"phones" keyPath:@"phones"]; }]; }

Slide 54

Slide 54 text

Неожиданные запросы

Slide 55

Slide 55 text

CoreData faults NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Phone"]; NSArray *phones = [context executeFetchRequest:request error:NULL]; ! for (Phone *phone in phones) { phone.person; } Faults Fetches

Slide 56

Slide 56 text

CoreData faults NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Phone"]; NSArray *phones = [context executeFetchRequest:request error:NULL]; [request setRelationshipKeyPathsForPrefetching:@[@"person"]]; ! for (Phone *phone in phones) { phone.person; } Faults Fetches

Slide 57

Slide 57 text

Время No prefetch Prefetch t, ms 0.000 1.000 2.000 3.000 4.000 5.000 6.000 5.831 0.352

Slide 58

Slide 58 text

sql NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Phone"]; ! sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZDDD, t0.ZDDI, t0.ZNUMBER, t0.ZPHONEID, t0.ZTIMESTAMP, t0.ZPERSON FROM ZPHONE t0 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Phone"]; [request setRelationshipKeyPathsForPrefetching:@[@"person"]]; ! sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZDDD, t0.ZDDI, t0.ZNUMBER, t0.ZPHONEID, t0.ZTIMESTAMP, t0.ZPERSON FROM ZPHONE t0 . . . sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZEMAIL, t0.ZGENDER, t0.ZNAME, t0.ZPERSONID, t0.ZTIMESTAMP FROM ZPERSON t0 WHERE t0.Z_PK IN (SELECT * FROM _Z_intarray0)

Slide 59

Slide 59 text

CoreData stack и производительность

Slide 60

Slide 60 text

CoreData stacks Persistent Store Coordinator Root Context ! (Private queue concurrency type) Main Context! (Main queue concurrency type) Background Context ! (Private queue concurrency type) stack #1

Slide 61

Slide 61 text

CoreData stacks Persistent Store Coordinator Root Context ! (Private queue concurrency type) Main Context! (Main queue concurrency type) Background Context ! (Private queue concurrency type) stack #1 Persistent Store Coordinator Main Context ! (Main queue concurrency type) Background Context ! (Private queue concurrency type) stack #2

Slide 62

Slide 62 text

Time Profiler Main thread Background thread stack #1 Main thread Background thread stack #2

Slide 63

Slide 63 text

Time Profiler

Slide 64

Slide 64 text

Тут надо что-то умное сказать

Slide 65

Slide 65 text

Ссылки • Подопытные: • MagicalRecord • MagicalRecord fork • FastEasyMapping ! • Почитать: • CoreData performance • CD Stack performance shootout • Backstage with nested managed object contexts

Slide 66

Slide 66 text

Спасибо