Pare de emitir outra coisa como um vazamento de memória

Até a presente data, muitos artigos foram escritos e você precisa cancelar a assinatura das assinaturas do Observable RxJS; caso contrário, ocorrerá um vazamento de memória . A maioria dos leitores desses artigos tinha a firme regra de "se inscrever? - assinar!" Infelizmente, porém, frequentemente nesses artigos as informações são distorcidas ou algo não é negociado, e ainda pior quando os conceitos são substituídos. Nós vamos falar sobre isso.



Tomemos, por exemplo, este artigo: https://medium.com/ngx/why-do-you-need-unsubscribe-ee0c62b5d21f



Quando me dizem sobre a "oportunidade potencial de obter uma regressão da produtividade", penso imediatamente na otimização prematura .




Continuamos a ler o artigo do homem sob o apelido de Raposa Reativa :



Além disso, há informações e dicas úteis. Concordo que você sempre deve cancelar a inscrição de fluxos intermináveis ​​no RxJS . Mas eu me concentro apenas em informações prejudiciais (na minha opinião).


Uau ... apanhados com o horror. Tal intimidação sem fundamento (sem métricas, números ...) no momento atual levou ao fato de que, para um número muito grande de participantes, a falta de cancelamento de inscrição é como um pano vermelho para um touro. Quando tropeçam nisso, não vêem mais nada a não ser esse trapo.


O autor do artigo até fez um aplicativo de demonstração, onde tentou provar seus pensamentos:
https://stackblitz.com/edit/why-you-have-to-unsubscribe-from-observable-material


De fato, por seu lado, você pode ver como o processador funciona desnecessariamente (quando não clico em nada) e como o consumo de memória aumenta (pequenas alterações):



Como confirmação do fato de que você sempre precisa cancelar a inscrição nas solicitações do Observable HttpClient , ele adicionou um interceptador de solicitação que exibe "ainda vivo ... ainda vivo ... ainda vivo ..." no console:

I.e. a pessoa interceptou o fluxo final, o tornou infinito (no caso de um erro, a solicitação é repetida, mas o erro sempre ocorre) e fornece isso como evidência de que você precisa cancelar a inscrição dos finais.


O StackBlitz não é muito adequado para medir o desempenho de aplicativos, pois há sincronização automática durante a atualização e consome recursos. Então, eu fiz meu aplicativo de teste: https://github.com/andchir/test-angular-app


Existem duas janelas lá. Quando você abre cada um, uma solicitação é enviada para action.php , na qual há um atraso de 3 segundos como uma imitação de uma operação que consome muitos recursos. O action.php também registra todas as solicitações no arquivo log.txt .


Código 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'] ]); 

Mas primeiro, uma pequena digressão. Na imagem abaixo (clicável), você pode ver um exemplo simples de como o coletor de lixo JavaScript funciona no navegador Chrome. PUSH ocorreu, mas setTimeout não impediu o coletor de lixo de limpar a memória.


Não se esqueça de ligar para o coletor de lixo com o toque de um botão ao experimentar.


Vamos voltar ao meu aplicativo de teste. Aqui está o código para ambas as janelas:


Código 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(); } } 

Como você pode ver, há um cancelamento de assinatura (takeUntil). Tudo como o "professor" nos aconselhou. Há também uma grande variedade.


Código GoodModalComponent
 @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'); } } 

Existe exatamente a mesma propriedade com uma matriz grande, mas não há cancelamento de inscrição. E isso não me impede de chamar essa janela de boa. Por que - depois.


Assista ao vídeo:



Como você pode ver, em ambos os casos, após alternar para o segundo componente, o coletor de lixo retornou com êxito a memória aos valores normais. Sim, seria possível limpar a memória também após o fechamento das janelas, mas em nosso experimento isso não é importante. Acontece que o "professor" estava errado quando ele disse:


Por exemplo, você fez uma solicitação, mas quando a resposta ainda não chegou do back-end, você destrói o componente como desnecessário; sua assinatura manterá o link para o componente , criando um possível vazamento de memória.

Sim, ele está falando sobre um vazamento "potencial" . Mas, se o fluxo for finito, não haverá vazamento de memória.


Prevejo as exclamações indignadas de tais "professores". Definitivamente, eles nos dizem algo como: "ok, não há vazamento de memória, mas, ao cancelar a inscrição , também cancelamos a solicitação , o que significa que teremos certeza de que nenhum código será mais executado após receber uma resposta do servidor" . Em primeiro lugar, não estou dizendo que cancelar a inscrição é sempre ruim, apenas estou dizendo que você está substituindo conceitos . Sim, o fato de que, depois que a resposta chegar, alguma operação mais inútil será executada é ruim, mas você só pode se proteger de um vazamento de memória real cancelando a inscrição (nesse caso) e pode se proteger de outros efeitos indesejáveis ​​de outras maneiras . Não há necessidade de intimidar os leitores e impor seu próprio estilo de escrever código neles.


Sempre precisamos cancelar a solicitação se o usuário mudar de idéia? Nem sempre! Não esqueça que você cancelou a solicitação, mas não cancelou a operação no servidor . Imagine que um usuário abriu um componente, algo carregou por um longo tempo e ele alternou para outro componente. É possível que o servidor esteja carregado e não lide com todas as solicitações e operações. Nesse caso, o usuário pode puxar freneticamente todos os links na navegação e criar uma carga ainda maior no servidor , porque a solicitação não para no lado do servidor (na maioria dos casos).


Assista ao vídeo a seguir:



Eu fiz o usuário esperar por uma resposta. Na maioria dos casos, a resposta virá rapidamente e o usuário não experimentará nenhum inconveniente. Mas, dessa forma, evitaremos que o servidor execute operações pesadas repetidas, se houver.


Resumo:


  • Não estou dizendo que você não precisa cancelar a inscrição nas assinaturas RxJS de solicitações HttpClient. Eu apenas digo que há momentos em que isso não é necessário. Não há necessidade de substituir conceitos. Se você está falando sobre um vazamento de memória, mostre esse vazamento. Não é o seu console.log interminável, ou seja, um vazamento. Em que a memória é medida? Em que tempo é medido o tempo de operação? É isso que precisa ser mostrado.
  • Não chamo minha solução, que apliquei no aplicativo de teste, de “bala de prata”. Pelo contrário, exorto o candidato a ter mais liberdade. Deixe-o decidir como escrever seu código. Não há necessidade de intimidá-lo e impor seu próprio estilo de desenvolvimento.
  • Sou contra o fanatismo e a otimização prematura. Eu tenho visto muito disso ultimamente.
  • O navegador possui métodos mais avançados para encontrar vazamentos de memória do que o que eu mostrei. Penso que, no meu caso, a aplicação deste método simples é suficiente. Mas eu recomendo que você se familiarize com o tópico com mais detalhes, por exemplo, neste artigo: https://habr.com/en/post/309318/ .

UPD # 1
No momento, o post cedeu por quase um dia. No começo, ele foi aos prós e contras, depois a avaliação parou em zero. Isso significa que o público foi dividido exatamente em dois campos. Não sei se isso é bom ou ruim.


UPD # 2
Nos comentários, Jet Fox apareceu (o autor do artigo sendo analisado). No começo, ele me agradeceu, ele foi muito educado. Mas, vendo a passividade do público, ele começou a pressionar. Chegou ao ponto em que ele escreveu que eu deveria me desculpar. I.e. ele mentiu (encontra-se delineado com uma moldura amarela acima), e eu deveria me desculpar.
No começo, pensei que o interceptador de fluxos com repetições sem fim (bem, 2-3 repetições), que ele escreveu em seu aplicativo de demonstração, é apenas para testes e informações. Mas acabou que ele o considerou um exemplo da vida. I.e. bloquear o botão de uma janela - é impossível . E para criar esses interceptores, violando os princípios do SOLID, violando a modularidade do aplicativo (módulos e componentes devem ser independentes um do outro), permitindo que os testes de unidade de suas unidades (componentes, serviços) sejam realizados na floresta - você pode. Imagine a situação: você escreveu um componente, escreveu testes de unidade para ele. E então esse Fox aparece, adiciona um interceptador semelhante ao seu aplicativo e seus testes se tornam inúteis. Depois, ele ainda diz: "Por que você não previu que eu gostaria de adicionar um interceptador desse tipo? Bem, corrija seu código". Talvez isso possa ser uma realidade em sua equipe, mas não acho que isso deva ser incentivado ou fazer vista grossa para ele.


UPD # 3
Os comentários discutem principalmente assinaturas e cancelamentos de inscrição. É um post chamado "Cancelar a inscrição no mal"? Não. Não peço que você não cancele sua inscrição. Faça como você fez antes. Mas você deve entender por que está fazendo isso. Cancelar a inscrição não é uma otimização prematura. Mas, entrando no caminho da proteção contra ameaças em potencial (como o autor do artigo em análise nos chama), você pode cruzar a linha. Então, seu código pode ficar sobrecarregado e difícil de manter.
Este artigo é sobre fanatismo, o que leva à distribuição de informações não verificadas. Em alguns casos, é necessário relacionar-se com a ausência de cancelamento de inscrição com mais calma (você precisa entender claramente se existe um problema em um caso específico).


UPD # 4


Pelo contrário, exorto o candidato a ter mais liberdade. Deixe-o decidir como escrever seu código.

Aqui você precisa esclarecer. Eu sou a favor dos padrões. Mas o padrão pode ser definido pelo autor da biblioteca ou por sua equipe, enquanto isso não é (na documentação e oficialmente). Por exemplo, a documentação da estrutura do Symfony possui uma seção Melhores práticas . Se fosse o mesmo na documentação do RxJS e dissesse "assinatura cancelada", eu não teria o desejo de discutir com ele.


UPD # 5
Comentário importante com respostas de pessoas respeitáveis:
https://habr.com/en/post/479732/#comment_21012620
A recomendação para cumprir o contrato de "assinatura com cancelamento de assinatura" do desenvolvedor do RxJS existe , mas não oficialmente.

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


All Articles