ArrĂȘtez d'Ă©mettre autre chose comme une fuite de mĂ©moire

À ce jour, de nombreux articles ont Ă©tĂ© Ă©crits sur la nĂ©cessitĂ© de se dĂ©sabonner des abonnements Observable RxJS, sinon une fuite de mĂ©moire se produira. Pour la plupart des lecteurs de tels articles, la rĂšgle ferme «signĂ©? - signez!» Était Ă©tablie dans ma tĂȘte. Mais, malheureusement, souvent dans ces articles, les informations sont dĂ©formĂ©es ou quelque chose n'est pas nĂ©gociĂ©, et pire encore lorsque les concepts sont remplacĂ©s. Nous en parlerons.



Prenons par exemple cet article: https://medium.com/ngx/why-do-you-need-unsubscribe-ee0c62b5d21f



Quand ils me parlent de « l' opportunité potentielle d'obtenir une régression des performances», je pense tout de suite à une optimisation prématurée .




Nous continuons Ă  lire l'article de l'homme sous le surnom de Reactive Fox :



De plus, il contient des informations et des conseils utiles. Je suis d'accord que vous devez toujours vous désabonner des flux sans fin dans RxJS . Mais je me concentre uniquement sur les informations nuisibles (à mon avis).


Wow ... rattrapé l'horreur. Une telle intimidation sans fondement (pas de métriques, de chiffres ...) à l'heure actuelle a conduit au fait que pour un trÚs grand nombre de front-end, l'absence de désinscription est comme un chiffon rouge pour un taureau. Quand ils tombent dessus, ils ne voient plus rien d'autre que ce chiffon.


L'auteur de l'article a mĂȘme fait une demande de dĂ©monstration, oĂč il a essayĂ© de prouver ses pensĂ©es:
https://stackblitz.com/edit/why-you-have-to-unsubscribe-from-observable-material


En effet, sur son stand, vous pouvez voir comment le processeur fait un travail inutile (quand je ne clique sur rien) et comment la consommation de mémoire augmente (un petit changement):



Pour confirmer le fait que vous devez toujours vous dĂ©sabonner des abonnements aux requĂȘtes HttpClient observables, il a ajoutĂ© un intercepteur de requĂȘte qui affiche "toujours vivant ... toujours vivant ... toujours vivant ..." dans la console:

C'est-à-dire la personne a intercepté le flux final, l'a rendu infini (en cas d'erreur, la demande est répétée, mais l'erreur se produit toujours) et donne cela comme preuve que vous devez vous désinscrire des flux finaux.


StackBlitz n'est pas trÚs approprié pour mesurer les performances d'une application, il y a une synchronisation automatique pendant la mise à niveau et elle prend des ressources. J'ai donc fait ma demande de test: https://github.com/andchir/test-angular-app


Il y a deux fenĂȘtres lĂ -bas. Lorsque vous ouvrez chacun, une demande est envoyĂ©e Ă  action.php , dans laquelle il y a un dĂ©lai de 3 secondes comme une imitation d'une opĂ©ration trĂšs gourmande en ressources. Action.php enregistre Ă©galement toutes les demandes dans le fichier log.txt .


Code Action.php
<?php header('Content-Type: application/json'); function logging($str, $fileName = 'log.txt') { if (is_array($str)) { $str = json_encode($str); } $rootPath = __DIR__; $logFilePath = $rootPath . DIRECTORY_SEPARATOR . $fileName; $options = [ 'max_log_size' => 200 * 1024 ]; if (!is_dir(dirname($logFilePath))) { mkdir(dirname($logFilePath)); } if (file_exists($logFilePath) && filesize($logFilePath) >= $options['max_log_size']) { unlink($logFilePath); } $fp = fopen( $logFilePath, 'a' ); $dateFormat = 'd/m/YH:i:s'; $str = PHP_EOL . PHP_EOL . date($dateFormat) . PHP_EOL . $str; fwrite( $fp, $str ); fclose( $fp ); return true; } $actionName = isset($_GET['a']) && !is_array($_GET['a']) ? $_GET['a'] : '1'; logging("STARTED-{$actionName}"); sleep(3);// Very resource-intensive operation that takes 3 seconds logging("COMPLETED-{$actionName}"); echo json_encode([ 'success' => true, 'data' => ['name' => 'test', 'title' => 'This is a test'] ]); 

Mais d'abord, une petite digression. Dans l'image ci-dessous (cliquable), vous pouvez voir un exemple simple du fonctionnement du ramasse-miettes JavaScript dans le navigateur Chrome. PUSH s'est produit, mais setTimeout n'a pas empĂȘchĂ© le garbage collector de vider la mĂ©moire.


N'oubliez pas d'appeler le ramasse-miettes en appuyant sur un bouton lorsque vous expérimentez.


Revenons Ă  mon application de test. Voici le code pour les deux fenĂȘtres:


Code BadModalComponent
 @Component({ selector: 'app-bad-modal', templateUrl: './bad-modal.component.html', styleUrls: ['./bad-modal.component.css'], providers: [HttpClient] }) export class BadModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); destroyed$ = new Subject<void>(); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=2').pipe( takeUntil(this.destroyed$), catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); this.destroyed$.next(); this.destroyed$.complete(); } } 

Comme vous pouvez le voir, il y a un désabonnement (takeUntil). Tout comme le "professeur" nous l'a conseillé. Il existe également un large éventail.


GoodModalComponent Code
 @Component({ selector: 'app-good-modal', templateUrl: './good-modal.component.html', styleUrls: ['./good-modal.component.css'] }) export class GoodModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=1').pipe( catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); } } 

Il y a exactement la mĂȘme propriĂ©tĂ© avec un grand tableau, mais il n'y a pas de dĂ©sabonnement. Et cela ne m'empĂȘche pas d'appeler cette fenĂȘtre une bonne. Pourquoi - plus tard.


Regardez la vidéo:



Comme vous pouvez le voir, dans les deux cas, aprĂšs ĂȘtre passĂ© au deuxiĂšme composant, le garbage collector a rĂ©ussi Ă  ramener la mĂ©moire Ă  des valeurs normales. Oui, on pourrait permettre de vider la mĂ©moire Ă©galement aprĂšs la fermeture des fenĂȘtres, mais dans notre expĂ©rience ce n'est pas important. Il s'avĂšre que le "professeur" avait tort quand il a dit:


Par exemple, vous avez fait une demande, mais lorsque la réponse n'est pas encore arrivée du backend, vous détruisez le composant comme inutile, votre abonnement conservera le lien vers le composant , créant ainsi une fuite de mémoire potentielle.

Oui, il parle d'une fuite «potentielle» . Mais, si le flux est fini, il n'y aura pas de fuite de mémoire.


Je prévois les exclamations indignées de ces «professeurs». Ils vont certainement nous dire quelque chose comme: "ok, il n'y a pas de fuite de mémoire, mais en vous désinscrivant, nous annulons également la demande , ce qui signifie que nous serons sûrs qu'aucun code ne sera plus exécuté aprÚs avoir reçu une réponse du serveur . " PremiÚrement, je ne dis pas que la désinscription est toujours mauvaise, je dis simplement que vous remplacez les concepts . Oui, le fait qu'une fois la réponse arrivée, une autre opération inutile sera effectuée est mauvais, mais vous ne pouvez vous protéger contre une véritable fuite de mémoire qu'en vous désinscrivant (dans ce cas), et vous pouvez vous protéger contre d' autres effets indésirables par d' autres moyens . Pas besoin d'intimider les lecteurs et de leur imposer leur propre style d'écriture de code.


Avons-nous toujours besoin d'annuler la demande si l'utilisateur change d'avis? Pas toujours! N'oubliez pas que vous annulez la demande, mais vous n'annulez pas l'opĂ©ration sur le serveur . Imaginez qu'un utilisateur a ouvert un composant, que quelque chose se charge depuis longtemps et qu'il passe Ă  un autre composant. Il est possible que le serveur soit chargĂ© et ne gĂšre pas toutes les demandes et opĂ©rations. Dans ce cas, l'utilisateur peut piquer frĂ©nĂ©tiquement tous les liens dans la navigation et crĂ©er une charge encore plus importante sur le serveur , car la requĂȘte ne s'arrĂȘte pas cĂŽtĂ© serveur (dans la plupart des cas).


Regardez la vidéo suivante:



J'ai fait attendre l'utilisateur une réponse. Dans la plupart des cas, la réponse viendra rapidement et l'utilisateur ne rencontrera aucun inconvénient. Mais de cette façon, nous éviterons au serveur d'effectuer des opérations lourdes répétées, le cas échéant.


Résumé:


  • Je ne dis pas que vous n'avez pas besoin de vous dĂ©sabonner des abonnements RxJS des requĂȘtes HttpClient. Je dis simplement qu'il y a des moments oĂč cela n'est pas nĂ©cessaire. Pas besoin de remplacer les concepts. Si vous parlez d'une fuite de mĂ©moire, montrez cette fuite. Pas votre console.log sans fin, Ă  savoir une fuite. Dans quoi la mĂ©moire est-elle mesurĂ©e? Quel est le temps de fonctionnement mesurĂ©? C'est ce qu'il faut montrer.
  • Je n'appelle pas ma solution, que j'ai appliquĂ©e dans l'application de test, une «solution miracle». Au contraire, j'exhorte le soumissionnaire Ă  donner plus de libertĂ©. Laissez-le dĂ©cider comment Ă©crire son code. Pas besoin de l'intimider et d'imposer son propre style de dĂ©veloppement.
  • Je suis contre le fanatisme et l'optimisation prĂ©maturĂ©e. J'en ai trop vu rĂ©cemment.
  • Le navigateur dispose de mĂ©thodes plus avancĂ©es pour dĂ©tecter les fuites de mĂ©moire que celle que j'ai montrĂ©e. Je pense que dans mon cas, l'application de cette mĂ©thode simple est suffisante. Mais je vous recommande de vous familiariser avec le sujet plus en dĂ©tail, par exemple, dans cet article: https://habr.com/en/post/309318/ .

UPD # 1
Pour le moment, le poste s'est affaissĂ© pendant presque une journĂ©e. Au dĂ©but, il s'est tournĂ© vers le pour et le contre, puis l'Ă©valuation s'est arrĂȘtĂ©e Ă  zĂ©ro. Cela signifie que le public Ă©tait divisĂ© exactement en deux camps. Je ne sais pas si c'est bon ou mauvais.


UPD # 2
Dans les commentaires, Jet Fox est apparu (l'auteur de l'article analysĂ©). Au dĂ©but, il m'a remerciĂ©, il Ă©tait trĂšs poli. Mais, voyant la passivitĂ© du public, il a commencĂ© Ă  faire pression. Il est arrivĂ© au point oĂč il a Ă©crit que je devais m'excuser. C'est-Ă -dire il a menti (mensonges encadrĂ©s d'un cadre jaune au-dessus), et je dois m'excuser.
Au dĂ©but, je pensais que l'intercepteur de flux avec des rĂ©pĂ©titions sans fin (enfin, 2-3 rĂ©pĂ©titions), qu'il a Ă©crit dans son application de dĂ©monstration, est uniquement destinĂ© aux tests et Ă  l'information. Mais il s'est avĂ©rĂ© qu'il le considĂ©rait comme un exemple de la vie. C'est-Ă -dire bloquer le bouton d'une fenĂȘtre - c'est impossible . Et pour crĂ©er de tels intercepteurs, violant les principes de SOLID, violant la modularitĂ© de l'application (les modules et les composants doivent ĂȘtre indĂ©pendants les uns des autres), laissant les tests unitaires de vos unitĂ©s (composants, services) Ă  travers la forĂȘt - vous pouvez. Imaginez la situation: vous avez Ă©crit un composant, Ă©crit des tests unitaires pour celui-ci. Et puis un tel Fox apparaĂźt, ajoute un intercepteur similaire Ă  votre application, et vos tests deviennent inutiles. Puis il vous dit toujours: "Pourquoi n'avez-vous pas prĂ©vu que je pourrais vouloir ajouter un tel intercepteur. Eh bien, corrigez votre code." C'est peut-ĂȘtre une rĂ©alitĂ© dans son Ă©quipe, mais je ne pense pas que cela devrait ĂȘtre encouragĂ© ou fermĂ© les yeux.


UPD # 3
Les commentaires traitent principalement des abonnements et des désabonnements. Un article intitulé "Se désabonner du mal"? Non. Je ne vous exhorte pas à ne pas vous désinscrire. Faites comme avant. Mais vous devez comprendre pourquoi vous faites cela. La désinscription n'est pas une optimisation prématurée. Mais, en marchant sur le chemin de la protection contre les menaces potentielles (comme nous appelle l'auteur de l'article analysé), vous pouvez franchir la ligne. Ensuite, votre code peut devenir surchargé et difficile à maintenir.
Cet article porte sur le fanatisme, qui conduit à la diffusion d'informations non vérifiées. Dans certains cas, il est nécessaire de relier plus calmement l'absence de désinscription (il faut bien comprendre si un problÚme existe dans un cas particulier).


UPD # 4


Au contraire, j'exhorte le soumissionnaire à se voir accorder plus de liberté. Laissez-le décider comment écrire son code.

Ici, vous devez clarifier. Je suis pour les standards. Mais la norme peut ĂȘtre fixĂ©e par l'auteur de la bibliothĂšque ou son Ă©quipe, alors que ce n'est pas le cas (dans la documentation et officiellement). Par exemple, la documentation du framework Symfony a une section Meilleures pratiques . S'il en Ă©tait de mĂȘme dans la documentation de RxJS et qu'il disait "dĂ©sinscription-signature", je n'aurais pas envie de discuter avec lui.


UPD # 5
Commentaire important avec des réponses de personnes réputées:
https://habr.com/en/post/479732/#comment_21012620
La recommandation de respecter le contrat de «désinscription signée» du développeur RxJS existe , mais officieusement.

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


All Articles