Où sauvegarder les informations calculées au runtime ?

MayerickMayerick Membre
mai 2018 modifié dans Objective-C, Swift, C, C++ #1

Bonjour à toutes et à tous,

J'ouvre un nouveau post concernant une des questions de mon gros post sur la relation entre modèle et vue.

Pour reprendre le concept des vases, imaginons que j'ai une application qui permette d'ajouter des petits/moyens/grands vases sur une étagère. Voici les règles:

  • L'ordre des vases correspond à un numéro unique.
  • Entre chaque vase il doit y avoir une distance minimale déterminée par la taille du vase.
  • Quand une rangée de l'étagère est pleine, on ajuste le placement des vases pour ne pas avoir de grand trou.

Le problème est que le numéro de la rangée sur laquelle est posé un vase précis, va dépendre de la largeur de l'étagère, en gros la taille de l'écran, et des vases posés précédemment. Le numéro de la rangée pour un vase donné ne fait donc pas vraiment partie du modèle puisqu'il est calculé et est susceptible de changer. Le modèle CoreData lui ne se souci que de la taille du vase et de son numéro, en théorie ça suffit pour reconstruire n'importe quelle étagère de n'importe quelle largeur.

On en arrive au problème suivant: pour connaitre l'emplacement x et y d'un vase donné, je doit connaitre l'emplacement de tous les vases précédents pour déterminer la rangée (y) et la position (x) sur cette rangée.

J'espère que vous me suivez, pour reformuler je suis capable de calculer le x et le y d'un vase quand je connais la largeur des rangées et la position de tous les vases précédents, mais c'est une opération qui peut demander beaucoup de temps.

Je peux sauver ces positions au runtime dans un tableau/dictionnaire mais cela m'oblige à le reconstruire à chaque fois que l'utilisateur charge le projet et je me dis que ce n'est pas l'idéal, peut-être qu'il faudrait garder ces positions quelque part et ne les recalculer que quand c'est nécessaire ? Selon vous quelle est la meilleure stratégie pour ce type de données ?

Merci de m'avoir lu !
(Edit: je me rend compte que ça aurait pu aller dans la catégorie dev iOS.., si vous sentez de bouger le post, cela ne gênera pas :))

Mots clés:

Réponses

  • DrakenDraken Membre

    @Mayerick a dit :

    On en arrive au problème suivant: pour connaitre l'emplacement x et y d'un vase donné, je doit connaitre l'emplacement de tous les vases précédents pour déterminer la rangée (y) et la position (x) sur cette rangée.

    J'espère que vous me suivez, pour reformuler je suis capable de calculer le x et le y d'un vase quand je connais la largeur des rangées et la position de tous les vases précédents, mais c'est une opération qui peut demander beaucoup de temps.

    Tu raisonnes comme si ton logiciel devais gérer simultanément les positions des notes sur l'ensemble de la partition. Ce n'est pas le cas. Tu ne dois tenir compte que des vues affichées à l'écran, dans ta fenêtre de visualisation. Ta référence graphique est la note affichée dans le coin haut gauche de l'écran. La position d'affichage sur l'écran des autres notes VISIBLES doit être calculée uniquement par rapport à cette note de référence.

  • Bonjour Draken,

    Merci pour ta réponse !

    Oui en effet c'est un peu ça, je raisonne comme si à tout moment l'utilisateur pouvait imprimer un PDF de tout le projet sans avoir à recalculer quoi que ce soit.

    Je vois bien l'avantage de s'occuper des vues à l'écran uniquement, mais cela amène le paradoxe suivant: admettons que l'utilisateur affiche les vases 10 à 20, je calcule la position, puis il scroll de 21 à 40, je calcule la position, enfin il revient sur les vases 10 à 20, je dois recalculer exactement la même position que la première fois, rien n'aura changé, donc le processeur passe son temps à calculer la position absolue de choses qui ne changent pas, ça me semble être un gros gâchis de ressources et de batterie..

    Tu crois que je focalise encore trop sur une optimisation non essentielle ?

    Pour éviter ce gâchis j'aurais bien gardé les positions dans un cache/store core data qui resterait local. Lors d'un partage par exemple, seul le modèle des vases et de leurs numéro seraient envoyés.
    Est-ce que cette solution semble envisageable ?

  • DrakenDraken Membre
    mai 2018 modifié #4

    @Mayerick a dit :

    Tu crois que je focalise encore trop sur une optimisation non essentielle ?

    Une optimisation non essentielle, source potentielle de ralentissements et de lags !


    Hypothèse d'une partition de 10.000 Notes.

    Tu la charges en mémoire, l'application doit commencer à calculer en une seule fois les positions des 10.000 Notes sur l'écran virtuel. Et paf .. blocage du processeur pendant un temps faible mais non négligeable pour faire ces 10.000 calculs. L'utilisateur se fiche de la subtilité des mécanismes internes, ce qu'il constate juste c'est un ralentissement lors du premier affichage. C'est très frustrant pour lui.

    A un moment donné, tu supprimes la Note numéro 4800. Cela force l'application a faire une ENORME BOUCLE pour recalculer la position des milliers de Notes situées entre la position 4800 et la fin de la partition. Encore un ralentissement potentiel de l'interface en cours d'utilisation .. Même chose en cas d'insertion d'une note ou de déplacement d'une série de Notes.

    Un PDF de plusieurs dizaines de pages peut avoir une mise en page pré-calculé parce qu'il est en consultation pur. Ce n'est pas le cas de ton éditeur de partitions, où les données sont modifiables a tout instant.

  • Franchement ton exemple est parfait et tu as simplement raison, c'est comme ça que ça se passe. À moins de se débrouiller pour faire les opérations qui sont en dehors de l'écran en background et par paquets, cela va créer des ralentissements.

    Ça me semble terriblement compliqué de calculer la position relativement à ce qui est affiché à l'écran, il faut que je réfléchisse à ça, peut-être que je me trompe..

    En tous cas ok j'abandonne l'idée de stocker la position. Merci pour tes conseils encore une fois c'est très apprécié !

  • LarmeLarme Membre
    mai 2018 modifié #6

    Je n'ai pas suivi toute la précédente discussion, cependant, dans ton cas, voici comment je ferais dans une première approche.

    Plutôt que de tout recalculer les tailles précédentes, je créerais un modèle :

    Class Étagère :

    • [vasesIdentifier]
    • row //En bref pour savoir si c'est la première, deuxième, etc., ce qui semble correspondre à ton "y", mais je ne suis pas sûr, là ton "y" devrait peut-être être recalculé, car il s'agit de graphique ?
    • width
    • Avec des méthodes de calculs pour repositionner les vases (éviter les gros trous si j'ai bien suivi), pouvoir en supprimer/ajouter au besoin, peut-être une méthode qui renvoie un booléen (isFull?), ou alors un calcul qui permet de savoir quelle width encore il reste avant d'en ajouter un, etc. Ces méthodes seraient potentiellement dans une category de cette classe CoreData (cf. suite de mon message) afin de laisser cette classe plus propre pour ton modèle, et il me semble que parfois à la regénération des classes (.h/.m) d'après le modèleCoreData, il y avait un replace plutôt qu'un ajout (si tu changes le modèle).

    Par rapport à un outil qu'on utilise plus souvent sur iOS UITableView/UICollectionView, un étagère correspondrait à une section, et les vases des rows.

    Potentiellement, cela pourrait faire parti de ton modèle dans CoreData, avec les liens étagère <-1,n->vases, car je suppose que tu gardes ça en mémoire si l'utilisateur ferme l'application.

    À toi de voir quand tu sauvegardes les modifications (bouton ? à chaque changement ?).

  • MayerickMayerick Membre
    mai 2018 modifié #7

    Bonjour @Larme et merci pour ta réponse.

    En effet on peut le voir comme tu le fais, c'est à dire comme une simili tableView/collectionView. Mais c'est un peu plus compliqué que ça j'ai l'impression, pour un même projet, l'entité étagère ne contiendra pas le même nombre de vases si elle est affichée sur un iPhone X ou sur un iPad pro 12".
    Pour prendre un exemple concret sur iPad pro, mettons que la rangée 1 contienne les vases 1 à 8, je sauve donc la relation entre les vases 1 à 8 et la rangée 1. Puis je m'envois le projet par mail pour l'ouvrir sur mon iPhone, la rangée 1 contient maintenant les vases 1 à 4, et le reste est dans les rangées suivantes. Le nombre de vase que contient une rangée/étagère donnée est donc susceptible de varier.
    D'où ma question, dois-je sauvegarder dans CoreData des données qui sont susceptible de changer entre devices ?

    À la base en effet je gardais une correspondance entre la rangée et le vase dans une entité CoreData pour charger le projet plus rapidement. Seulement cela amène le problème soulevé par Draken, et il me semble avoir lu une fois qu'Apple déconseille d'encombrer la mémoire de l'utilisateur avec des choses qui peuvent être calculé au runtime.

    En soit je n'ai aucune raison d'envoyer la correspondance entre rangée et vases lors d'un partage de projet, il faut reconstruire la relation en fonction de la taille de l'appareil c'est certain. Par contre pour plusieurs lancement sur un même appareil je me disais qu'avoir les positions pré-calculées pourraient améliorer la réactivité, ce que Draken tend à mettre à mal.

    En y réfléchissant je me dis que je pourrais peut-être faire un cache/store coreData des positions qui serait rempli lorsque l'utilisateur affiche la vue. Quand l'utilisateur quitte l'application, scroll.., si rien n'a changé je peux les réutiliser sinon je les recalcule au moment où elles ont besoin d'être affichée. C'est un peu un mix des solutions, mais j'ai conscience que ça demande pas mal de magie noire pour garder les stores synchronisés. Un avis la dessus ?

    Merci !

  • LexxisLexxis Membre
    mai 2018 modifié #8

    Les calculs sont-ils si long à réaliser pour vouloir les mettre en cache disque ? Un cache mémoire ne suffit-il pas ?
    Un benchmark serait intéressant à faire. Et si cela prend trop de temps à faire sur la queue principale et bien il faut déporter l'opération sur une autre queue.
    Il serait aussi intéressant de voir le bénéficie temps de chargement/sauvegarde des infos lié au cache et temps de calcul du dit cache.

  • Bonjour Lexxis et merci pour tes remarques.

    Oui les calculs peuvent être vraiment long à réaliser. Je souhaite faire un éditeur de partition et la position des éléments est quelque chose de (vraiment) compliqué en musique. Ça dépends du nombre d'instruments, des notes sur un temps donnée, des fioritures qu'on peut rajouter en marge de la partition et qui vont en changer drastiquement le layout, ou encore pleins de subtilités évidentes à faire sur papier mais bien moins de manière logicielle.

    En effet ce serait intéressant d'avoir un benchmark et le bénéfice/coût total de l'opération, malheureusement je dois faire un choix avant d'aller vers l'une ou l'autre des solutions. Et en effet la question se pose de savoir si je dois privilégier un cache in-memory ou sur le disque, si on admet que seule la partie visible est calculée ou réutilisé, et que donc le cache est construit au fur et à mesure, alors il ne devrait pas y avoir trop de différence entre les deux je pense (si on excepte les performances propre à in-memory). Pour moi c'est vrai que ça devait être sur le disque, parce que vraiment ça m'embête de recalculer des choses qui ne changent pas. L'exemple vaut ce qu'il vaut, mais si je cherche à connaitre la masse d'une planète, et bien je la calcule une fois et je la stocke jusqu'à ce qu'un astéroïde ou une planète érante vienne s'écraser dessus. Je n'ai pas de raison de tout recalculer à chaque fois s'il n'y a pas eu d'évènement susceptible d'apporter un changement.

    @Draken
    Je pense à cette histoire de cache depuis longtemps, à la base il devait contenir toutes les informations à tout moment, mais si comme tu le suggères on ne calcule que ce qui est visible et qu'on met ça en cache, est-ce qu'on aurait pas finalement le meilleur des deux mondes ?

    Je stockerai les positions quand même, mais lorsqu'elles sont invalidées, on ne recalcule pas systématiquement toutes les postions suivantes, elles sont reconstruites au fur et à mesure que l'utilisateur scroll dans la vue. Est-ce que ça sent l'architecture moisie ou pas ? J'ai l'impression que ça se tient..

  • Rien de t'empêche de faire un cache en memoire et, dans un second temps, de sauver les informations en mémoire de masse.
    Le cache en mémoire est de toute façon obligatoire si tu ne veux pas recalculer chaque fois. Les deux solutions sont complémentaires pour moi.
    Avant de faire de la mise en cache, as tu déjà écrit l'algorithme de répartition des notes ?

    Il y aurait peut être plusieurs trucs pour éviter de toute calculer:

    • D'une part, comme dit Draken, calculer uniquement ce qui est affiché. J'imagine qu'au chargement d'une partition, seul le début de celle ci sera affiché: Les premier calculs s'en trouvent vraiment réduit.
    • Lors d'un scroll "il suffit" d'ajouter les calcul supplémentaires.
    • Lors d'un scroll à une position donné, il suffit d'ajouter les calcul supplémentaire jusqu'à la dernier note affiché
    • Lors d'une modification (si elle s'opère uniquement sur la partie visible de la partition. Seule les calculs à partir de la note et jusqu'à la dernière note affiché seront à refaire...
    • etc...
  • LarmeLarme Membre

    Tu parles de partage de ta partition.
    Je ne sais pas à quel point ton projet est avancé, et à quel point tu as testé l'essentiel (en bref, que l'ensemble marche, à des lenteurs et finitions près).

    Donc en soit, lors de l'import, en tant qu'utilisateur je ne vois pas de soucis à avoir un p'tit HUD qui fait les calcules de chaque "étagère" au départ.
    Maintenant, dans un soucis d'amélioration, je ferais en sorte de décoreller bien ton code afin qu'il soit plus facilement modification.
    En mettant une méthode genre -(Etagere)retrieveEtagereNumber(NSInteger)etagereNumber, qui elle pourra faire soit un appel Cache/RAM, Disque, CoreData, etc, avec peut-être alors des blocks/closures (-(void)retrieveEtagereNumber:(NSInteger)etagereNumber withSuccess:(void(^)(Etagere))successBlock) plus que des retours directs.
    Cela te permettra de tout tester rapidement. Tu pourras ainsi faire des benchmarks.

    Il faut aussi éviter tout ce qui est un problème d'optimisation trop tôt (premature otpimization) dans le développement, car tu perdras du temps à penser à comment faire au mieux, mais au final, un changement ailleurs rendra ton implémentation obsolète.

  • @Lexxis

    Oui oui et oui c'est exactement la logique que je propose :). Ce serait valide alors ?
    Il a peut-être un léger malentendu sur in-memory, je parle bien d'un store coreData qui vit "in-memory", à la différence d'un "persistent" store, mais c'est du détail !

    @Larme

    En fait je réécris tout en Swift, j'avais un projet fonctionnel mais en Objective-C et qui utilisait NSKeyedArchiver. J'utilisais ça justement dans une logique de ne pas optimiser prématurément mais ça m'a couté cher au final. J'ai maintenant besoin d'un undo/redo manager et d'avoir plus de lisibilité sur les données au lieu de travailler avec des dictionnaires de dictionnaire, donc réécriture.. :#

    En tout cas je garde l'idée générale de tes méthodes pour tester plus facilement. Merci.

  • Pour un cache j'éviterai d'emblée CoreData (sauf pour le sauver le cache sous forme de NSData)

  • C'est à dire, tu privilégierais quoi ? Les donnée mémoire dans le viewController uniquement ?
    Ce qui me fait aller vers CoreData est qu'il possède un undo/redo manager qui est built-in, et j'aurais besoin d'une fonctionnalité comme ça.

  • Joanna CarterJoanna Carter Membre, Modérateur

    @Lexxis a dit :
    Pour un cache j'éviterai d'emblée CoreData (sauf pour le sauver le cache sous forme de NSData)

    Si on utilisait NSData, pourquoi le mettre dans CoreData quand on peut le sauvegarder tout simplement dans un fichier ?

  • Joanna CarterJoanna Carter Membre, Modérateur
    mai 2018 modifié #16

    @Mayerick a dit :
    C'est à dire, tu privilégierais quoi ? Les donnée mémoire dans le viewController uniquement ?
    Ce qui me fait aller vers CoreData est qu'il possède un undo/redo manager qui est built-in, et j'aurais besoin d'une fonctionnalité comme ça.

    1. Ne mets pas les données dans le viewController.
    2. Si tu prévoyais utiliser undo/redo dans CoreData, il faut lire ceci avant ; http://mikeabdullah.net/core_data_undo_management.html (c'est pas du tout facile)
  • @Joanna Carter

    merci pour le lien, je vais regarder ça avec attention !

Connectez-vous ou Inscrivez-vous pour répondre.