1
votes

améliorer la boucle sur un grand tableau d'objets

J'ai un gros fichier JSON de 7 Mo (11 000 objets). Chaque objet contient plusieurs éléments qui ressemblent à ceci:

let formatTrips = function (data:any) {
    var arr = [], i, j;
    for(i=0; i<24; i++) {
        for(j=0; j<60; j++) {
            arr.push((i<=9 ? "0"+i : i )+ ":" + (j<=9 ? "0"+j : j) );
        }
    }
    var start = new Date().getTime();
    var arrFormated = arr.map(time=> {
        let schedulesFiltered=[];
        let nowFormat = moment(time, 'HH:mm');
         schedulesFiltered= data.filter(elm => {
        let before = moment(elm.tp_org_r, 'HH:mm:ss'); //tp_org_r = startingTime
        let after = moment(elm.tp_des_r, 'HH:mm:ss');//tp_des_r = endingTime

        let checkifBetween = nowFormat.isBetween(before, after);

        if (checkifBetween) {
            return elm;
        }
        });
        //console.log(schedulesFiltered)
        return schedulesFiltered

    })
    var end = new Date().getTime();
    var time = end - start;
    console.log('Execution time: ' + time);

}

Dans mon HTML, j'ai un curseur qui contient des heures quotidiennes: minutes.

Lorsque l'utilisateur déplace le curseur, je dois mettre à jour le DOM en filtrant les données du fichier afin de trouver:

StartingTime

Puis utilisez les données filtrées pour imprimer des formes SVG sur une carte (tubeMap + trains). (sans oublier de supprimer () les formes précédentes du DOM avant d'en dessiner de nouvelles).

Le problème est que le filtrage de tous les objets chaque fois que l'utilisateur fait glisser le curseur rend l'interface utilisateur non fluide.

J'ai donc essayé une autre approche: Utilisez un backend pour générer un nouveau JSON avec ce format

[  
   {  
      "00:00":{  
         "data":""
      },
      "00:01":{  
         "data":""
      },
      "00:02":{  
         "data":""
      }
   }
]

Ensuite, envoyez le nouveau JSON au premier plan. Ainsi, lorsque l'utilisateur a sélectionné une heure de jour, je ne filtre plus tous les objets (c'est fait dans le back-end) - je sélectionne simplement la clé ObjectKey de selectedTimeOnSlider du nouveau format puis ajoute les données à le DOM.

Mais j'ai remarqué qu'effectuer ces calculs en amont est également trop long (plus de 25 secondes à effectuer de 00h00 à 02h00) => le front-end attend aussi les données long.

Alors, je demanderais s'il existe un moyen d'augmenter les performances de filtrage? ou une autre approche? (indexation?)

Pour plus d'informations, le gros fichier JSON n'est pas statique, cela dépend du jour sélectionné sur un calendrier.

Mettre à jour Voici le code que j'utilise pour filtrer les données

[  
   {  ...
      "startingTime":"Date1",
      "endingTime":"Date2"
      ...
   }
]

Informations supplémentaires:

  • la requête http a pris 9 secondes pour obtenir le fichier mais ce n'est pas le problème. Je me concentre sur le filtre temporel qui a pris trop de temps. Boucle 24 * 60 sur un tableau de plus de 11 000 objets.

  • Dans ma première approche, je ne charge qu'une seule fois le fichier json et je l'enregistre dans un magasin (ngrx angular store) puis filtre chaque changement de valeur de diapositive.


18 commentaires

Avez-vous mesuré la performance? (1) Combien de temps faut-il pour filtrer? (2) Combien de temps faut-il pour effectuer un nouveau rendu? (3) 25 secondes, c'est beaucoup trop long, vous faites quelque chose de mal là-bas. Pouvez-vous montrer ce code?


Faites-vous une requête HTTP pour les données JSON chaque fois que l'utilisateur modifie le curseur? Ou le faites-vous une fois lors du chargement de la page en tant que variable JS?


Ajoutez votre code pour le filtrage, on ne sait pas si je suis trié d'une manière ou d'une autre?


Vous vous rendez compte que vous filtrez les données 120 fois ...


pendant 2 heures, c'est 120 fois oui, mais je devrais le faire pendant 24 heures donc 1440 fois (dans ma deuxième approche ..) Mais dans ma première approche, c'est une fois chaque changement de curseur, mais si l'utilisateur glisse rapidement le curseur, l'interface utilisateur ne peut pas fluide


Si vous définissez un délai d'expiration de 200 ms lors du changement de curseur avant le filtrage, l'interface utilisateur ne s'obstruera pas, laissant le temps à l'utilisateur de glisser en douceur et lorsqu'il s'arrête, affichez les enregistrements filtrés pour une durée donnée


Et vous pouvez mémoriser le résultat de ce filtrage à chaque fois, mais seulement après que l'utilisateur le demande. Jusque-là, vous gaspillez de précieux cycles CPU


@JonasWilms dans ma première approche, le filtrage sur une journée sélectionnée a pris moins d'une seconde, mais lorsque l'utilisateur glisse rapidement, le curseur ne glisse pas de manière fluide.


@BrettGregson J'ai mis à jour mon message, vous avez la réponse.


Le fichier @ CarlosSá json n'est pas trié, je devrais donc le parcourir en entier. Et oui, je pourrais définir un délai pour lisser la navigation du curseur (je le ferai si aucune autre solution), mais ce que je préfère faire est de mettre à jour dom lorsque l'utilisateur maintient la souris sur le curseur


Les dates de début et de fin se chevauchent-elles? C'est à dire. y a-t-il 0:00 - 1:30 et 0:30 - 2:00?


@JonasWilms comme dans mon code c'est toutes les minutes


@infodev Je veux dire le jeu de données ... Deux ormes se chevauchent-ils? Sinon, le filtrage peut être effectué beaucoup plus rapidement.


Oui ça se chevauche, concrètement c'est un métro donc beaucoup de trains circulent en même temps


Ah d'accord, donc en d'autres termes: quand tu glisses, les trains bougent?


oui le but est de montrer les trains en mouvement sur une carte, sinon de mettre à jour la position des trains sur la souris


D'accord, cela change tout, je vais mettre à jour ma réponse.


Jetez un œil à ma modification.


3 Réponses :


0
votes

Vous pouvez revenir à la structure d'origine et pré-calculer la même structure d'aide côté client. Pour l'optimiser davantage, vous pourriez peut-être mettre à jour paresseusement la structure du "cache" en anticipant les actions de l'utilisateur (et si le cache n'était pas encore rempli, bloquer l'interface utilisateur jusqu'à ce que vous le fassiez).


1 commentaires

merci, pensez-vous que le pré-calcul sur le front-end est plus rapide que le back-end?



3
votes

l'objectif est de montrer les trains se déplaçant sur une carte

Et c'est ainsi que nous devrions commencer:

Regroupez les horaires par train . Cela peut être fait en O (n), et cela n'augmentera pas la taille des datasrts (cela pourrait donc être fait sur le backend également, voir ci-dessous pour des considérations).

Maintenant, pourquoi cela aide-t-il? Cela réduit la taille des ensembles de données que nous devons beaucoup filtrer. Les 11 000 événements ne concernent probablement que quelques centaines de trains.

Ensuite, commencez par générer ce qui suit:

  • Une carte (identifiant de train - train) des trains actuellement affichés

  • Un tableau trié de trains par heure de départ

  • Un tableau trié de trains par heure d'arrivée.

  • La position du dernier train parti à la position actuelle du curseur

  • La position du dernier train est arrivée à la position actuelle du curseur

Désormais, chaque fois que le curseur bouge, tout ce que vous avez à faire est:

  • Déplacez la position de départ sur le tableau des départs, pour chaque train que vous franchissez, ajoutez le train à la carte et au DOM.

  • Déplacez la position d'arrivée sur le tableau des arrivées, pour chaque train que vous franchissez, retirez le train de la carte et du DOM.

(Si le curseur se déplace vers l'arrière, la procédure est l'inverse).

Ensuite, pour chaque train actuellement affiché, ajustez la position si nécessaire . p>

Ce sera beaucoup plus rapide, j'imagine que pour un pas de curseur typique, ~ 20 trains se déplacent, ~ 1 part, ~ 1 arrive, donc vous devez faire 22 mises à jour , au lieu de 11000.


Divers autres conseils d'optimisation que j'ai écrits pendant que la question évoluait, ils pourraient toujours être utiles, bien que non pertinents si le conseil supérieur était appliqué

Le code que vous utilisez pour grouper sur le backend n'est vraiment pas optimisé car il .filter est le tableau data 120 fois . Voici comment vous pouvez faire cela en itérant une seule fois sur l'ensemble de données, puis pour chaque entrée, parcourez toutes les minutes auxquelles elle appartient et ajoutez-la ici:

   const toMinutes = date => {
     const [hours, mins] = date.split(":");
     return (+hours * 60 + +mins);
   };
   const fromMinutes = minutes => {
     const mins = minutes % 60;
     const hours = (minutes - mins) / 60;
     return hours + ":" + mins;
   };

   const START = "tp_org_r", END = "tp_des_r";
   const groupByDate = { /* "01:20": [...] */ };

   for(const elm of data) {
     const start = toMinutes(elm[START]);
     const end = toMinutes(elm[END]);
     for(let minutes = start; minutes <= end; minutes++) {
        const id = fromMinutes(minutes);
        if(!groupByDate[id]) groupByDate[id] = [];
        groupByDate[id].push(elm);
     }
 }

Mais cette approche a un gros défaut: il duplique massivement les éléments . Je ne ferais jamais cela sur le backend, car cela augmentera vraiment la taille du fichier envoyé au frontend, et ainsi vous perdrez le léger avantage de performances que vous pourriez gagner en augmentant le temps nécessaire pour envoyer les données au frontend. De plus, comme il semble qu'il n'y a aucun moyen de le mettre en cache sur le backend, vous devez donc refaire les calculs pour chaque client. En d'autres termes: vous perdez votre temps de calcul (vous payez le serveur, le client est "gratuit") et vous n'en tirez aucun bénéfice (les calculs sur le serveur ont du sens si vous ( 1) réduire la quantité de données envoyées au client, diminuant ainsi le temps de chargement de la page et / ou (2) calculer les données une fois, puis les diffuser à des centaines de clients).

Vous pouvez le faire sur le frontend, mais je ne pense pas que la construction de ce lookuptable en vaille la peine (car filtrer la date est en fait assez facile).

Au lieu de cela, envoyez tous les éléments au frontend, puis filtrez-y. Si l'ensemble de données est ordonné, le filtrage est simple et vous n'avez même pas à parcourir les 11000 éléments:

1) Triez le tableau par heure de début.

2) Trouvez le première position où l'heure de début est plus petite que la durée du curseur moins la durée maximale (je suppose que ce n'est que quelques minutes). Faites de même pour la dernière position où l'heure de fin est plus grande que la durée du curseur plus la durée maximale. Grâce à cela, nous éliminons beaucoup de ces 11000, la plage résultante peut alors être facilement filtrée.

L'avantage n'est peut-être pas encore clair, mais si le curseur bouge , il ne nous reste plus qu'à déplacer les positions de départ et de fin. En d'autres termes, nous n'avons pas besoin de regarder à nouveau les 11000 éléments, nous déplaçons simplement la plage légèrement vers la droite / gauche, puis nous filtrons cette petite plage.

Mais dans ma première approche, c'est une fois chaque changement de curseur, mais si l'utilisateur fait glisser rapidement le curseur, l'interface utilisateur ne devient pas fluide

C'est assez facile à réparer. Si l'utilisateur glisse rapidement, il ne veut pas et n'a pas besoin de voir les événements sur lesquels il glisse . Procédez comme suit:

(1) Lorsque l'utilisateur commence à glisser, masquez les éléments actuels et affichez une sorte d'indicateur de chargement.

(2) Lorsque l'utilisateur arrête de glisser, attendez un peu de temps, puis générez les résultats filtrés.

Grâce à cela, la page ne se rendra que deux fois , ce qui réduira considérablement la quantité de calculs effectués, et l'interface utilisateur est beaucoup plus fluide ( car la mise à jour se produit lorsqu'aucune interaction de l'utilisateur n'est effectuée, donc même si le calcul est lourd, l'utilisateur ne le remarquera pas car il n'y a pas de lag).


7 commentaires

Donc, faire du filtrage sur le front-end est plus rapide que le backend? En fait, l'ensemble de données n'est pas filtré. btw je n'utilise pas de secondes


Peut-être que si, au lieu de sauvegarder les données complètes, vous enregistrez une référence? par exemple. Vous enregistrez toutes les données dans une carte et stockez leurs clés dans votre objet groupByDate


Pouvez-vous modifier votre code s'il vous plaît car je n'utilise que des heures: minutes et ajoutez quelques commentaires car je ne comprends pas exactement ce que vous faites. Merci


@infodev non, en supposant que vous visitez la page en chrome et que votre backend fonctionne sur NodeJS, peu importe que vous le calculiez à l'avant ou à l'arrière, cela prendra à peu près le même temps que le même moteur JS fonctionnant sous le capot . Mais le temps de chargement est également une fonction de performance. Si la structure de données "optimisée" est, disons, 14 Mo, le client passera deux fois plus de temps à charger / analyser les données.


merci pour votre explication, je vais essayer d'utiliser votre algorithme d'itération et de voir comment les performances sont


Merci pour votre explication détaillée! Une dernière chose que je vous demanderais dans l'algorithme groupbyDate, c'est que les id objectKeys ne sont pas triés ni au format HH: mm et pendant 24h pas 2, pouvez-vous le modifier pour qu'il conditionne ces conditions? Je pense que dans un prochain niveau, je mettrai à jour mon programme avec votre dernière proposition de mise à jour.


Les clés d'objet ne sont pas vraiment triées. Je ne vois pas pourquoi vous voulez le trier, mais si vous le souhaitez, transformez l'objet en tableau, puis triez-le.



1
votes

Pour de meilleures performances, vous devez savoir que le gros problème est le rendu et non filtrage.

Votre problème vient de l'utilisation de l'objet Date et des dates de comparaison. pour de meilleures performances, vous pouvez d'abord convertir vos données du type Date en type Number (en millisecondes), puis comparer les nombres les uns avec les autres. J'ai changé votre code et vous pouvez voir l'exemple.

Et sachez que les performances des boucles en javascript sont différentes les unes des autres:

J'ai comparé la somme d'éléments aléatoires de 10k en utilisant pour , for-of, while, forEach et réduire. L'exécution des tests 10000 fois a renvoyé les résultats suivants:

<script src="https://momentjs.com/downloads/moment.min.js"></script>

et vous pouvez utiliser Object.keys et Object.values ​​ et Array.map et Array.filter de la bonne manière si vous en avez besoin.

let formatTrips = function (data) {
    var arr = [], i, j;
    for(i=0; i<24; i++) {
        for(j=0; j<60; j++) {
            arr.push(i*60*60*1000 + j*60*1000 );
        }
    }

    var start = new Date().getTime();
    const dataWithTimeStamp = data.map((d) => (
      {
        ...d,
        tp_org_r: (new Date(d.startingTime)).valueOf() % (24*60*60*1000), // just get time in miliseconds
        tp_des_r: (new Date(d.endingTime)).valueOf() % (24*60*60*1000) // just get time in miliseconds
      }
    ));

    var arrFormated = arr.map((time)=> {
        let schedulesFiltered=[];
        schedulesFiltered = dataWithTimeStamp.filter(elm => (elm.tp_org_r <= time && elm.tp_des_r >= time));
        //console.log(schedulesFiltered)
        return schedulesFiltered

    });
    var end = new Date().getTime();
    var time = end - start;
    console.log('Execution time: ' + time);
}

// Create sample data
const sampleData = [];
const date2019ToMiliseconds = (new Date("2019-01-01")).valueOf();
const miliSecondsFrom2019 = (new Date()).valueOf() - date2019ToMiliseconds;
for(let i = 0;i<10000; i++){
  const randomDate = new Date(Math.floor(date2019ToMiliseconds+Math.random()*miliSecondsFrom2019)).valueOf(); // Random Date between 2019-01-01 till now
   sampleData.push({
      "startingTime": randomDate,
      "endingTime":randomDate + (1000*60)
   })
}
formatTrips(sampleData);
For Loop, average loop time: ~10 microseconds
For-Of, average loop time: ~110 microseconds
ForEach, average loop time: ~77 microseconds
While, average loop time: ~11 microseconds
Reduce, average loop time: ~113 microseconds

0 commentaires