Amélioration du téléchargement de contenu sans sceaux



La livraison de contenu rapide et de haute qualité aux utilisateurs est la tâche la plus importante sur laquelle nous travaillons constamment tout en travaillant sur l'application iFunny. L'absence d'éléments en attente même avec une mauvaise connexion - c'est ce que tout service de visualisation de contenu multimédia cherche à faire.

Nous avons eu plusieurs itérations pour travailler avec la prélecture de contenu. Dans chaque nouvelle version majeure, nous avons inventé quelque chose de nouveau et regardé comment cela fonctionne pour les utilisateurs. Dans la prochaine itération de travail avec la prélecture, il a été décidé de déboguer d'abord les métriques qu'elle affecte sur le stand local, puis de donner le résultat aux utilisateurs.

Dans cet article, je parlerai de ce à quoi ressemble la prélecture dans iFunny et comment le processus de recherche a été automatisé pour affiner ses paramètres.

Prélecture standard


Dans iOS 10, Apple a fourni la possibilité d'exécuter la prélecture hors de la boîte. Pour ce faire, la classe UICollectionView possède un champ:

@property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource; @property (nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled; 

Pour activer la prélecture native, attribuez simplement au champ prefetchDataSource un objet qui implémente le protocole UICollectionViewDatasourcePrefetching et définissez le deuxième champ sur YES.

Pour implémenter le protocole de prélecture, deux méthodes doivent être décrites:

 - (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; 

Dans la première méthode, vous pouvez effectuer tout travail utile sur la préparation du contenu.

Dans le cas d'iFunny, cela ressemblait à ceci:

 NSMutableArray<NSURL *> *urls = [NSMutableArray new]; for (NSIndexPath *indexPath in indexPaths) { NSObject<IFFeedItemProtocol> *item = [self.model itemAtIndex:indexPath.row]; NSURL *downloadURL = item.downloadURL; if (downloadURL) { [urls addObject:downloadURL]; } } [self.downloadManager updateActiveURLs:urls]; [urls enumerateObjectsUsingBlock:^(NSURL *_Nonnull url, NSUInteger idx, BOOL *_Nonnull stop) { [self.downloadManager downloadContentWithURL:url.absoluteString forView:nil withOptions:0]; }]; 

La deuxième méthode est facultative, mais dans le cas de la bande iFunny, elle n'a pas été appelée du tout par le système.

La prélecture fonctionne, mais nous avons uniquement appelé la méthode pour le contenu après le contenu actif.
En général, le travail de prélecture standard pour UICollectionView dépend beaucoup de la façon dont la vue de collection est implémentée. De plus, comme nous ne connaissons pas du tout la mise en œuvre de la prélecture standard, il est impossible de garantir son fonctionnement stable. Par conséquent, nous avons implémenté notre mécanisme de prélecture, qui fonctionnait toujours selon nos besoins.

Notre algorithme de prélecture


Avant de développer l'algorithme de prélecture, nous avons écrit toutes les fonctionnalités du flux iFunny:

  1. Un flux peut être composé de différents types de contenu: photos, vidéos, webapps, publicité native.
  2. La bande fonctionne avec pagination.
  3. La plupart des utilisateurs retournent le flux uniquement vers l'avant.
  4. Dans iFunny, 20% des sessions utilisateur se produisent via LTE.

Sur la base de ces conditions, nous avons obtenu un algorithme simple:

  1. Il y a 1 élément actif dans la bande, tous les autres sont inactifs.
  2. L'élément actif doit toujours télécharger le contenu jusqu'à la fin.
  3. Chaque élément de contenu du flux a son propre poids.
  4. Sur la connexion Internet actuelle, vous pouvez charger des éléments d'un montant de N.
  5. Chaque fois que vous faites défiler la bande, nous changeons l'élément actif et calculons quels éléments sont chargés, et annulons le reste du chargement.

L'architecture dans le code de cet algorithme contient plusieurs classes de base et un protocole:

  • IFPrefetchedCollectionProtocol

 @protocol IFPrefetchedCollectionProtocol @property (nonatomic, readonly) NSUInteger prefetchItemsCount; - (NSObject<IFFeedItemProtocol> *)itemAtIndex:(NSInteger)index; @end 

Ce protocole est nécessaire pour obtenir les paramètres de la collection et du contenu des objets de classe:

  • IFContentPrefetcher

 @interface IFContentPrefetcher : NSObject @property (nonatomic, weak) NSObject<IFPrefetchedCollectionProtocol> *collection; @property (nonatomic, assign) NSInteger activeIndex; @end 

La classe implémente la logique de l'algorithme de prélecture du contenu:

  • IFPrefetchOperation

 @interface IFPrefetchOperation : NSObject @property (nonatomic, readonly) NSUInteger cost; - (void)fetchMinumumBuffer; - (void)fetchEntireBuffer; - (void)pause; - (void)cancel; - (BOOL)isEqualOperation:(IFPrefetchOperation *)object; @end 

Il s'agit de la classe de base d'une opération atomique, qui décrit le travail utile de prélecture d'un contenu spécifique et indique son paramètre - poids.

Pour exécuter l'algorithme, nous avons décrit deux opérations:

  1. L'image. Il a un poids de 1. Toujours complètement chargé;
  2. Vidéo Il a un poids de 2. Charge complètement uniquement lorsqu'il est actif. À l'état inactif, les 200 premiers Ko sont chargés.

Comme métrique pour évaluer le fonctionnement de l'algorithme, nous avons choisi le nombre de hits de l'élément d'interface utilisateur du chargeur pour 1000 éléments de contenu visualisés.

Lors de la prélecture standard de cette statistique, nous avions environ 30 impressions / 1 000 éléments. Après l'introduction du nouvel algorithme, cette métrique est tombée à 25 impressions / 1 000 éléments.

Ainsi, le nombre d'impressions de chargeur a diminué de 20% et le nombre total de contenus consultés par les utilisateurs a légèrement augmenté.

Ensuite, nous avons procédé à la sélection des paramètres optimaux pour Featured - la bande la plus populaire dans iFunny.

Sélection des paramètres de prélecture


L'algorithme de prélecture développé a des paramètres d'entrée:

  1. Le coût total du téléchargement.
  2. Le coût de chargement de chaque article.

Nous continuerons de mesurer le nombre de chargeurs.

Comme outils auxiliaires pour simplifier la collecte de données, nous utiliserons:

  1. Tests gris avec un ensemble de frameworks KIF, OHHTTPStubs.
  2. sh-scripts et xcodebuild pour exécuter des tests avec différents paramètres.
  3. Profil de réseau 3G disponible dans le paramètre Developer - Network Link Conditioner.

Voyons comment chacun de ces outils nous a aidés.

Les tests


Pour émuler la façon dont les utilisateurs voient le contenu, nous avons décidé d'utiliser le cadre KIF, familier aux développeurs iOS sur Objective-C.

KIF fonctionne très bien pour Objective-C et Swift, après quelques-unes des manipulations faciles décrites dans la documentation KIF:
https://github.com/kif-framework/KIF#use-with-swift

Pour tester la bande, nous avons choisi Objective-C, notamment pour pouvoir remplacer les méthodes dont nous avons besoin dans le service d'analyse.

Jetons un œil au code d'un test simple, que nous avons obtenu:

  - (void)setUp { [super setUp]; [self clearCache]; [[NSURLCache sharedURLCache] removeAllCachedResponses]; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *_Nonnull request) { return [request.URL.absoluteString isEqualToString:@"http://fun.co/rp/?feed=featured&limit=30"]; } withStubResponse:^OHHTTPStubsResponse *_Nonnull(NSURLRequest *_Nonnull request) { NSString *path = OHPathForFile(@"featured.json", self.classForCoder); OHHTTPStubsResponse *response = [[OHHTTPStubsResponse alloc] initWithFileAtPath:path statusCode:200 headers:@{ @"Content-Type" : @"application/json" }]; return response; }]; } 

Dans la méthode de configuration du test, nous devons vider le cache afin qu'à chaque lancement le contenu soit chargé à partir du réseau et vider complètement le dossier Caches dans l'application.

Pour garantir la stabilité des données dans chacun des tests, nous avons utilisé la bibliothèque OHHTTPStubs, qui facilite la substitution des réponses aux demandes du réseau en quelques étapes simples:

  1. Définissez les paramètres de requête. Pour nous, il s'agit de l'URL de la demande de flux en vedette à l'API - http://fun.co/rp/?feed=featured&limit=30
  2. Enregistrez la réponse nécessaire et enregistrez-la dans un fichier, attachez-la à la cible avec le test.
  3. Définissez les options de réponse. Dans le code ci-dessus, il s'agit de l'en-tête Content-Type et du code de réponse.
  4. Consultez les instructions pour OHHTTPStubs.

Vous pouvez en savoir plus sur l'utilisation des OHHTTPStubs dans la documentation:
http://cocoadocs.org/docsets/OHHTTPSPSs/

Le test lui-même ressemble à ceci:

 - (void)testFeed { KIFUIViewTestActor *feed = [viewTester usingLabel:@"ScrolledFeed"]; [feed waitForView]; [self setupCustomPrefetchParams]; for (NSInteger i = 1; i <= 1000; i++) { [feed waitForCellInCollectionViewAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]; [viewTester waitForTimeInterval:1.0f]; } [self appendStatisticLine]; } 

En utilisant KIF, nous obtenons un flux, puis parcourons 1000 éléments de contenu avec une attente d'une seconde.

La méthode setupCustomPrefetchParams sera discutée un peu plus loin.

Pour déterminer le nombre de chargeurs affichés, nous utiliserons le runtime Objective-C et remplacerons la méthode du service d'analyse par la méthode de test:

 + (void)load { [self swizzleSelector:@selector(trackEventLoaderViewedVideo:) ofClass:[IFAnalyticService class]]; } + (void)swizzleSelector:(SEL)originalSelector ofClass:(Class) class { Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod([self class], originalSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, originalSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } - (void)trackEventLoaderViewedVideo : (BOOL)onVideo { if (onVideo) { [IFTestFeed trackLoaderOnVideo]; } else { [IFTestFeed trackLoaderOnImage]; } } 

Nous avons maintenant un test automatique dans lequel l'application reçoit toujours le même contenu et fait défiler le même nombre d'éléments. Et selon ses résultats, il écrit une ligne avec des statistiques d'exécution dans le journal.

Étant donné que la connexion Internet influence principalement le téléchargement de contenu, le test avec un ensemble de paramètres doit être répété plusieurs fois.

Automatisation de démarrage


Pour automatiser et paramétrer les tests, nous avons décidé d'utiliser le lancement via xcodebuild avec le transfert des paramètres nécessaires.

Pour passer des paramètres au code, nous devons écrire le nom de l'argument dans les paramètres cibles pour les tests dans les macros de préprocesseur:



Pour accéder à un paramètre à partir du code Objective-C, deux macros doivent être déclarées:

 #define STRINGIZE(x) #x #define BUILD_PARAM(x) STRINGIZE(x) 

Maintenant, lors du démarrage à partir du terminal à l'aide de xcodebuild:

 xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="5" VIDEO_COST="2" IMAGE_COST="2" 

Dans le code, vous pouvez lire les paramètres passés:

 - (void)setupCustomPrefetchParams { NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; formatter.numberStyle = NSNumberFormatterNoStyle; [IFAppController instance].prefetchParams.goodNetMaxCost = [formatter numberFromString:@BUILD_PARAM(MAX_PREFETCH_COST)]; [IFAppController instance].prefetchParams.videoCost = [formatter numberFromString:@BUILD_PARAM(VIDEO_COST)]; [IFAppController instance].prefetchParams.imageCost = [formatter numberFromString:@BUILD_PARAM(IMAGE_COST)]; } 

Maintenant, tout est prêt pour exécuter ces tests hors ligne à l'aide de scripts shell.

Exécuter xcodebuild avec un ensemble de paramètres 10 fois de suite:

 max=10 for i in `seq 1 $max` do xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="$1" VIDEO_COST="$2" IMAGE_COST="$3" done 

Nous avons également généré un script avec le lancement de différents ensembles de paramètres. Tous les tests ont duré plusieurs jours. Les données obtenues ont été résumées dans un seul tableau et nous les avons comparées à la version de travail actuelle.

En conséquence, la prélecture la plus simple de cinq éléments s'est avérée être la meilleure pour les rubans iFunny en vedette, sans égard au format de contenu (vidéo ou image).

Selon le résultat


L'article décrit l'approche qui vous permettra d'explorer et de surveiller n'importe quelle partie critique de l'application, sans changer le code principal du projet.

Voici ce qui aidera à mener de telles études:

  • Utilisation de frameworks de test pour des actions monotones.
  • Automatisation via xcodebuild pour paramétrer les startups.
  • Runtime Objective-C pour changer la logique nécessaire, si possible.

Sur la base de cette approche pour tester l'application, nous avons commencé à ajouter la surveillance des modules importants sur le stand local et avons déjà préparé plusieurs tests que nous exécutons périodiquement pour vérifier la qualité de l'application.

PS: Selon les résultats de nos tests, les nouveaux paramètres de pré-lecture par rapport à l'option de production gagnent environ 8%, en réalité, ils ont reçu une diminution de l'affichage des chargeurs de 3%, ce qui signifie que nous avons commencé à livrer des sourires à iFunny 3% plus souvent :)

PPS: Nous n'allons pas nous arrêter là, nous continuerons à améliorer davantage la prélecture du contenu.

Source: https://habr.com/ru/post/fr417059/


All Articles