diff --git a/figures/fromeffectloop.dot b/figures/fromeffectloop.dot new file mode 100644 index 0000000..cb76777 --- /dev/null +++ b/figures/fromeffectloop.dot @@ -0,0 +1,13 @@ +digraph { + { + edge [arrowhead=dot] + { getCategory categories } -> useEffect + cache -> getCategory + } + + { + getCategory -> useEffect [dir=back] + cache -> getCategory [dir=back,style=dashed] + fetchCategory -> getCategory [dir=back,len=3] + } +} diff --git a/figures/fromeffectloop.tex b/figures/fromeffectloop.tex new file mode 100644 index 0000000..5e8b417 --- /dev/null +++ b/figures/fromeffectloop.tex @@ -0,0 +1,52 @@ +\begin{figure} + \begin{subfigure}[t]{\textwidth} + \begin{lstlisting}[style=JavaScript] +function useCategoryCache() { + const [cache, setCache] = React.useState>({}); + + // useCallback permet de retourner la même référence à la fonction + // si sa closure reste inchangée. Contrairement à d'autres librairies, + // React requiet qu'on passe manuellement les références présentes + // dans la closure de `getCategory`, ici `cache` + // (`fetchCategory` se trouve en dehors du hook) + const getCategory = React.useCallback(async (id) => { + if (cache[id]) return cache[id]; + + return fetchCategory(id).then((movie) => { + cache[id] = movie; + return movie; + }); + }, [cache]); + + return getCategory; +} + +const Page = (props) => { + const categories = props.categories; + const [fullCategories, setFullCategories] = React.useState(); + const getCategory = useCategoryCache(); + + // useEffect éxecute le callback dès qu'une des références (aka "dépendance") + // de la liste change; ici, comme appeler `getCategory` a une chance de changer + // la référence à cette fonction, le useEffect est appelé plusieures fois + React.useEffect(() => { + const promises = categories.map((category) => getCategory(category)); + + Promise.all(promises).then((fullCategories) => { + setFullCategories(fullCategories); + }); + }, [getCategory, categories]); + // ... +}; + \end{lstlisting} + \caption{Code susceptible à un bug causant les requêtes à être envoyées plusieures fois} + \end{subfigure} + + \begin{subfigure}[t]{\textwidth} + \centering + \includegraphics[width=0.5\textwidth]{fromeffectloop} + \caption{Les flèches correspondent à un appel, les points à une dépendance} + \end{subfigure} + \caption{Version simplifiée du bug causant plusieures requêtes à être executées} + \label{fromeffectloop} +\end{figure} diff --git a/figures/moodtvperf.dot b/figures/moodtvperf.dot index 988f4eb..7ff144e 100644 --- a/figures/moodtvperf.dot +++ b/figures/moodtvperf.dot @@ -10,7 +10,7 @@ digraph { db -> stat } - mood -> perf + mood -> perf [style=dotted] perf -> db db -> perf } diff --git a/figures/perf3.tex b/figures/perf3.tex index 8b81cee..a50f7f3 100644 --- a/figures/perf3.tex +++ b/figures/perf3.tex @@ -6,6 +6,6 @@ ready & $6517.79$ & $\pm 521.00$ & ms & faible diminution & $7198.81$ & $\rightarrow 6517.79$ \end{tabular} \end{center} - \caption{Performances après avoir dédupliqué les requêtes} + \caption{Performances après avoir minimisé la taille du bundle} \label{perf3} \end{figure} diff --git a/figures/perf4.tex b/figures/perf4.tex new file mode 100644 index 0000000..bbb63bc --- /dev/null +++ b/figures/perf4.tex @@ -0,0 +1,11 @@ +\begin{figure}[H] + \begin{center} + \begin{tabular}{r | c c l | r r l} + hydration & $1338.76$ & $\pm 291.44$ & ms & diminution & $2437.44$ $\rightarrow 1338.76$ \\ + mount & $1717.74$ & $\pm 348.80$ & ms & diminution & $3033.36$ $\rightarrow 1717.74$ \\ + ready & $4785.31$ & $\pm 743.51$ & ms & diminution & $6517.79$ $\rightarrow 4785.31$ + \end{tabular} + \end{center} + \caption{Performances après avoir optimisé \texttt{fromPlain}} + \label{perf3} +\end{figure} diff --git a/references.bib b/references.bib index 9194135..263dba2 100644 --- a/references.bib +++ b/references.bib @@ -51,7 +51,7 @@ note="[En ligne; accédé le 02 Février 2023]" } -@inproceedings{amazonsestemplate, +@misc{amazonsestemplate, author={Amazon Web Services}, title={Using templates to send personalized email with the Amazon SES API}, booktitle={Amazon Simple Email Service - Developer Guide}, @@ -168,7 +168,7 @@ note="[En ligne; accédé le 05 Février 2023]" } -@article{oberloemailclients, +@misc{oberloemailclients, title={Most Used Email Clients Worldwide}, author={Oberlo}, year={2023}, @@ -184,7 +184,7 @@ note="[En ligne; accédé le 06 Février 2023]" } -@article{mdncontentcategories, +@misc{mdncontentcategories, title={Content Categories}, author={Mozilla Developer Network}, howpublished="\url{https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories}", @@ -203,5 +203,40 @@ author={Mozilla Developer Network}, howpublished="\url{https://developer.mozilla.org/en-US/docs/Web/API/Performance}", note="[En ligne; accédé le 07 Février 2023]" +} + +@misc{classtransformer, + title={class-transformer}, + author={Node Package Manager}, + howpublished="\url{https://www.npmjs.com/package/class-transformer}", + note="[En ligne; accédé le 08 Février 2023]" +} + +@misc{classtransformerbenchmark, + title={Runtype Benchmarks}, + author={moltar}, + howpublished="\url{https://moltar.github.io/typescript-runtime-type-benchmarks/}", + note="[En ligne; accédé le 08 Février 2023]" +} + +@misc{ttest, + title={Two-Sample t-Test for Equal Means}, + author={NIST}, + howpublished="\url{https://www.itl.nist.gov/div898/handbook/eda/section3/eda353.htm}", + note="[En ligne; accédé le 08 Février 2023]" +} + +@misc{webvitals, + title={Web Vitals}, + author={Philip Walton}, + journal={Fast Load Times - Techniques for improving site performance}, + howpublished="\url{https://web.dev/vitals/}", + note="[En ligne; accédé le 08 Février 2023]" +} +@misc{nextjsvitals, + title={Advanced Features: Measuring Performance}, + author={Next.JS}, + howpublished="\url{https://nextjs.org/docs/advanced-features/measuring-performance}", + note="[En ligne; accédé le 08 Février 2023]" } diff --git a/report.tex b/report.tex index aa67133..8d1d981 100644 --- a/report.tex +++ b/report.tex @@ -16,12 +16,14 @@ \usepackage{subcaption} \usepackage{wrapfig} \usepackage{svg} +\usepackage{ragged2e} \usepackage{hyperref} \hypersetup{ colorlinks=true, linkcolor=blue, urlcolor=blue, + citecolor=red, pdftitle={Adrien Burgun - Rapport de stage ST40 - A2022} } @@ -209,48 +211,6 @@ différents pôles: \newpage -% Moment -> Moment + Moment Care -% Fondateur? (TODO:) -% Chiffres? (TODO:) -% Équipes -% Agile, réunions -% Jira -% Gitlab - -% TODO: valider tout ça -% Le groupe \entity{Moment} a été fondé en 2013, et est constitué à ce jour de deux entreprises: - -% \begin{itemize} -% \item \entity{Moment}, qui se spécialise dans le développement de solutions de divertissement pour l'aviation, le secteur maritime, -% les chemins de fer et les aéroports. -% \item \entity{Moment Care}, qui se spécialise dans le développement de solutions de divertissement pour le domaine de la santé. -% \end{itemize} - -% À ce jour, les deux entreprises partagent les mêmes bureaux à Paris. - -% J'ai réalisé mon stage dans l'équipe \entity{développement full-stack}, qui maintient et développe les applications front-end pour -% \entityb{Moment} et \entityb{Moment Care}, ainsi qu'une partie des applications back-end (en grande partie pour \entityb{Moment Care}). - -% Notre équipe est menée par mon tuteur de stage, \person{Pierre}{Perrin}, et nous travaillons en collaboration avec les chefs de projets -% de \entityb{Moment} et de \entityb{Moment Care}, l'équipe \entity{\og SysOps \fg} et l'équipe \entity{Design}. - -% % TODO: trouver une source sur agile, utiliser la nomenclature associée -% Notre équipe fonctionne avec la méthode \term{agile}: chaque semaine, nous faisons une réunion présentant les sujets et taches de la semaine, -% ainsi que leurs priorités. -% Chaque matin, nous faisons également une courte réunion où chaqu'un présente le travail qu'iel a réalisé la veille, -% et ce qu'iel va faire ce jour. - -% La distribution du travail se fait via \entity{Jira}: en début de semaine, le \term{backlog} est mis à jour pour contenir l'ensemble -% des taches retenues pour la semaine, ainsi que celles non-réalisées de la semaine dernière. Chaqu'un peut ensuite s'assigner -% des taches, travailler dessus, puis marquer la tache comme prête à être revue. - -% L'assurance de qualité se fait via des revues sur \entity{GitLab}: chaque modification au code doit être mise dans une \term{Merge Request} -% (l'équivalent des \term{Pull Request} sur \entity{GitHub}), et un membre de l'équipe n'ayant pas contribué à cette modification -% doit approuver celle-ci avant qu'elle ne puisse être ajoutée à la branche principale de développement. - -% Tests unitaires -% - \subsection{Produits de Moment} \subsubsection{Flymingo} @@ -431,6 +391,12 @@ et propose de nombreux outils pour gérer les requêtes, les bases de données e \subsection{Planning} + +% Afin de pouvoir développer \entity{Mint TV} sans duplication de code, +% nous avons décidé de prendre l'ensemble des composants de \entity{Mood TV}, +% de les nettoyer et des les mettre dans une librairie au sein de la monorepository. + + \section{Travail réalisé sur Mint Service} Mint Service est une application construite avec \entity{NestJS}. @@ -772,14 +738,23 @@ alors on peut calculer la moyenne empirique ($\mu^{\star}_n$) et la variance ($( Afin de pouvoir comparer différents ensembles de valeurs, je fais l'hypothèse que les valeurs suivent une loi normale ($X \hookrightarrow \mathcal{N}(\mu, \sigma^2)$). % TODO: refine -Dans ce cas, on peut utiliser une approximation de la distribution de student pour calculer un intervale de confiance pour $\mu$: +Dans ce cas, on peut utiliser une approximation de la distribution de student pour calculer un intervale de confiance pour $\mu$. +J'utilise comme taux de confiance $\alpha = 0.95$: \begin{align*} \mu &\in [\mu^{\star}_n - z_{1-\alpha/2} \frac{\sigma^{\star}_n}{\sqrt{n}}, \mu^{\star}_n + z_{1-\alpha/2} \frac{\sigma^{\star}_n}{\sqrt{n}}] \\ z_a \;&\text{est tel que} \int_{-\infty}^{z_a}{\frac{e^{-\frac{(x-\mu)^2}{2\sigma^2}}}{\sigma\sqrt{2\pi}}}=a \end{align*} -Enfin, on peut utiliser le t-test pour estimer si deux valeurs différentes $X$ et $Y$ diffèrent bien de moyenne. \figref{ttest} +Enfin, on peut utiliser le t-test pour estimer si deux valeurs différentes $X$ et $Y$ diffèrent bien de moyenne. \cite{ttest} + +Pour \entity{Mood TV}, les valeurs mesurées sont les suivantes, en millisecondes: + +\begin{itemize} + \item Le temps jusqu'au début de l'hydration: \textbf{hydration} + \item Le temps jusqu'à la première éxecution des \texttt{useEffect}: \textbf{mount} + \item Le temps jusqu'à ce que l'entièreté des requêtes pour charger les informations des films soient finies: \textbf{ready} +\end{itemize} \inputfig{perf1} @@ -789,49 +764,100 @@ Enfin, on peut utiliser le t-test pour estimer si deux valeurs différentes $X$ \subsection{Optimisation du temps de chargement} % - [x] Optimisation du nombre de requêtes faites (avec un dessin? ~ plus tard) -% - [ ] Optimisation de la taille du bundle -% - [ ] Optimisation du parsing des requêtes -% Avoir de jolis nombres, et un tableau final avec toutes les mesures +% - [x] Optimisation de la taille du bundle +% - [x] Optimisation du parsing des requêtes +% - [x] Avoir de jolis nombres +% - [ ] tableau final avec toutes les mesures + +\subsubsection{Optimisation du nombre de requêtes} La première optimisation a été d'éviter que les requêtes pour récupérer les informations sur les films ne soient envoyées plusieures fois. -La fonction pour faire la requête pour un seul film (\texttt{getItem}) utilisait un \texttt{useCallback} et un cache gêré par React, -ce qui faisait que dès que les informations d'un film était récupéré, React assignait une nouvelle référence à \texttt{getItem}, -causant le \texttt{useEffect} responsable pour charger l'ensemble des informations des films à tourner à nouveau, -et de nouvelles requêtes sont envoyées en conséquence. +La fonction pour faire la requête pour une catégorie (\texttt{getItems}) utilisait un \texttt{useCallback} et un cache gêré par React, +ce qui faisait que dès que les informations d'un film était récupéré et stocké dans le cache, React assignait une nouvelle référence à \texttt{getItems}, +causant le \texttt{useEffect} responsable pour charger l'ensemble des informations des films à s'éxecuter à nouveau, +et de nouvelles requêtes sont envoyées en conséquence. \figref{fromeffectloop} Pour résoudre ce problème, j'ai créé une nouvelle fonction qui récupère l'entièreté des informations des films, et qui seulement une fois que toutes les requêtes soient finies stoque ces informations dans le cache. \inputfig{perf2} +\subsubsection{Optimisation de la taille du bundle} + Ensuite, j'ai optimisé la taille du bundle javascript, pour passer de $350$ kilooctets à $243$ kilooctets, en enlèvant les librairies qu'on n'utilisait peu et qui prenaient beaucoup de place, comme \texttt{browserify-crypto}. Réduire la taille du bundle permet de réduire le temps passé à télécharger le code javascript de la page, -et le temps passé à compiler ce code et à laisser webpack charger. +et le temps passé à compiler ce code et à charger les différents modules avec \entity{webpack}. \inputfig{perf3} -Enfin, j'ai optimisé la logique de traitement des réponses +\subsubsection{Optimisation du traitement des réponses} + +Enfin, j'ai optimisé la logique de traitement des réponses des requêtes pour récupérer les informations sur les films. +Ce traitement, jusqu'à ce moment, se faisait grâce à la librairie \texttt{class-transformer} \cite{classtransformer}, +qui permet de facilement transformer des objets ordinaires en instances de classe. -\subsection{Refactors} +Cette librairie est utilisée pour transformer les résultats des requêtes, qui sont sous forme d'objets ordinaires, +en instances de classes proposant des fonctions pour facilement traiter les informations contenues dedans. +Malheureusement, \texttt{class-transformer} ajoute beaucoup de temps de calcul, qui sur le hardware de la télévision connectée, +se traduit en secondes de chargement en plus. \cite{classtransformerbenchmark} + +J'ai donc remplacé \texttt{class-transformer} par une transformation manuelle pour les listes de films, +qui copie manuellement les données des films et des catégories en instances de classe. + +Avant de faire ce changement, j'ai d'abords écrit des tests unitaires pour vérifier le comportement du système déjà en place. +Puis, j'ai progressivement modifié les fonctions de traitement de requête affectées pour utiliser ma transformation manuelle à la place. + +% Les seuls tests présents auparavant étaient des tests d'intégration, qui avaient cassé au fil des mois et qui ne fonctionnent plus ajourd'hui. +% J'ai donc dû nettoyer les tests d'intégration existants pour les faire tourner séparemment des tests unitaires. + +\inputfig{perf4} + +\subsection{Résultats et rétrospective} + +Avec ces optimisations, la page prend désormais entre 4 et 6 secondes pour charger, contre 10 à 15 secondes auparavant. +Le temps d'éxecution sur le processeur de la télévision n'est aussi plus un engorgement, +et on peut voir sur le profil des performances qu'une bonne partie du temps est passé à attendre que les différentes requêtes se finissent. \figref{moodtvafter} + +\begin{figure} + \includegraphics[width=\textwidth]{mood-tv-after.png} + \caption{Profil du chargement de la page, après les optimisations; annoté} + \label{moodtvafter} +\end{figure} + +Faire ce travail m'a permis d'avoir une compréhension plus profonde sur différents sujets: + +\begin{itemize} + \item Les techniques d'optimisation logicielle (profilage, benchmarking) + \item Le modèle de propagation de mises à jour de React (\texttt{useEffect}, les boucles d'évenements infinies \figref{fromeffectloop}) + \item L'optimisation de la taille des bundle javascript + \item L'extraction de données de performances depuis le navigateur (web vitals \cite{webvitals}, \texttt{reportWebVitals} sur Next.JS \cite{nextjsvitals}) + \item L'analyse de données de performances (z-test, t-test, intervale de confiance) +\end{itemize} -% - keyboard handler -% - customisations (si ça passe à temps) -% - remise en place de l'ISR, avec un vrai système pour l'activer et le désactiver \makeutbmbackcover{} \newpage \section{Annexes} +\begingroup +\raggedright +\bibliographystyle{plain} +\bibliography{references}{} +\endgroup + +\newpage + +\subsection{Listings de code} + \inputfig{nestjs1} \inputfig{nestjs2} \inputfig{email-src-hbs} \inputfig{email-src-mjml} -\inputfig{ttest} +% \inputfig{ttest} +\inputfig{fromeffectloop} -\bibliographystyle{plain} -\bibliography{references}{} \end{document}