web-dev-qa-db-fra.com

L'utilisation totale de la mémoire de la toile dépasse la limite maximale (Safari 12)

Nous travaillons sur une application web de visualisation qui utilise d3-force pour dessiner un réseau sur une toile. Chaque nœud contenant de nombreuses informations et nécessitant beaucoup de ressources en processeur pour tout redessiner, nous avons mis en place une sorte de cache dans lequel chaque nœud est dessiné sur son canevas (non lié au DOM), une fois pour chaque niveau de zoom. Ces toiles sont ensuite dessinées sur le canevas principal (lié au DOM) à la position du nœud.

Nous sommes satisfaits du gain de vitesse, même si le résultat nécessite désormais beaucoup de mémoire (en particulier sur les écrans hidpi (rétine), où la densité de pixels peut être de 2 ou 3).

Mais nous avons maintenant un problème avec les navigateurs sur iOS, où le processus se bloque après quelques interactions avec l’interface. Si je me souviens bien, ce n'était pas un problème avec l'ancienne version (antérieure à iOS12), mais je n'ai pas d'appareil non mis à jour pour le confirmer.

Je pense que ce code résume le problème:

const { range } = require('d3-array')

// create a 1MB image
const createImage = () => {
    const size = 512

    const canvas = document.createElement('canvas')
    canvas.height = size
    canvas.width = size

    const ctx = canvas.getContext('2d')
    ctx.strokeRect(0, 0, size, size)
    return canvas
}

const createImages = i => {
    // create i * 1MB images
    let ctxs = range(i).map(() => {
        return createImage()
    })
    console.log(`done for ${ctxs.length} MB`)
    ctxs = null
}

window.cis = createImages

Puis sur un iPad et dans l'inspecteur:

> cis(256)
[Log] done for 256 MB (main-a9168dc888c2e24bbaf3.bundle.js, line 11317)
< undefined
> cis(1)
[Warning] Total canvas memory use exceeds the maximum limit (256 MB). (main-a9168dc888c2e24bbaf3.bundle.js, line 11307)
< TypeError: null is not an object (evaluating 'ctx.strokeRect')

Etant donné que je crée un canevas de 256 x 1 Mo, tout se passe bien, mais j'en crée un de plus et le canevas.getContext renvoie un pointeur nul. Il est alors impossible de créer une autre toile.

La limite semble être liée à l'appareil, car sur l'iPad, elle est de 256 Mo et sur un iPhone X, de 288 Mo.

> cis(288)
[Log] done for 288 MB (main-a9168dc888c2e24bbaf3.bundle.js, line 11317)
< undefined
> cis(1)
[Warning] Total canvas memory use exceeds the maximum limit (288 MB). (main-a9168dc888c2e24bbaf3.bundle.js, line 11307)
< TypeError: null is not an object (evaluating 'ctx.strokeRect')

Comme il s’agit d’un cache, je devrais pouvoir supprimer certains éléments, mais ce n’est pas mon cas (car définir ctxs ou ctx sur null devrait déclencher le GC, mais cela ne résout pas le problème).

La seule page pertinente que j'ai trouvée sur ce problème est une page de code source du webkit: HTMLCanvasElement.cpp .

Je soupçonne que le problème pourrait provenir de Webkit lui-même, mais je voudrais en être sûr avant de poster sur Webkit.

Y a-t-il un autre moyen de détruire les contextes de la toile?

Merci d'avance pour toute idée, pointeur, ...

EDITS:

Pour ajouter des informations, j'ai essayé d'autres navigateurs. Safari 12 présente le même problème sous macOS, même si la limite est supérieure (1/4 de la mémoire de l’ordinateur, comme indiqué dans les sources du kit Web). J'ai également essayé avec la dernière version du kit Webkit (236590) sans plus de chance. Mais le code fonctionne sur Firefox 62 et Chrome 69.

J'ai affiné le code de test afin qu'il puisse être exécuté directement à partir de la console du débogueur. Ce serait vraiment utile si quelqu'un pouvait tester le code sur un safari plus ancien (comme 11).

let counter = 0

// create a 1MB image
const createImage = () => {
    const size = 512

    const canvas = document.createElement('canvas')
    canvas.height = size
    canvas.width = size

    const ctx = canvas.getContext('2d')
    ctx.strokeRect(0, 0, size, size)
    return canvas
}

const createImages = n => {
    // create n * 1MB images
    const ctxs = []

    for( let i=0 ; i<n ; i++ ){
        ctxs.Push(createImage())
    }

    console.log(`done for ${ctxs.length} MB`)
}

const process = (frequency,size) => {
    setInterval(()=>{
        createImages(size)
        counter+=size
        console.log(`total ${counter}`)
    },frequency)
}


process(2000,1000)
20
Ogier Maitre

J'ai passé le week-end à créer une simple page Web pouvant rapidement montrer le problème. J'ai soumis des rapports de bogues à Google et à Apple. La page affiche une carte. Vous pouvez faire un panoramique et un zoom sur tout ce que vous voulez, et l'inspecteur Safari (web sur iPad, utilisant MacBook Pro pour voir les toiles) ne voit aucune toile.

Vous pouvez ensuite appuyer sur un bouton et dessiner une polyligne. Quand vous faites cela, vous voyez 41 toiles. Faites un panoramique ou un zoom et vous en verrez plus. Chaque toile représente 1 Mo. Par conséquent, une fois que vous avez 256 des toiles orphelines, les erreurs commencent lorsque la mémoire de la toile est saturée sur l’iPad.

Rechargez la page, appuyez sur un bouton pour placer un polygone, et la même chose se produit.

Tout aussi intéressant, j’ai ajouté des boutons pour styler la carte de jour comme de nuit. Vous pouvez aller et venir lorsqu'il ne s'agit que d'une carte (ou d'une carte avec uniquement des marqueurs, un bouton permet d'afficher des marqueurs sur la carte). Aucune toile orpheline. Mais tracez une ligne, puis lorsque vous modifiez le style, vous voyez plus de toiles orphelines.

En regardant Safari sur le MacBook dans Active Monitor, la taille continue de défiler lorsque vous effectuez un panoramique et un zoom sur la carte après avoir tracé un poly *

J'espère Apple et Google peuvent résoudre ce problème et ne pas prétendre que c'est le problème de l'autre société. Tout cela a changé avec les pages Web fonctionnant sous IOS12 qui sont stables depuis des années et qui fonctionnent toujours = IOS 9 et 10 iPads que je conserve pour effectuer des tests afin de vérifier que les appareils plus anciens peuvent afficher les pages Web actuelles. Nous espérons que ce test/cette expérience vous aidera.

6
eepete

Quelqu'un a posté une réponse indiquant une solution de contournement. L'idée est de régler la hauteur et la largeur à 0 avant de supprimer les toiles. Ce n'est pas vraiment une solution appropriée, mais cela fonctionnera dans mon système de cache.

J'ajoute un petit exemple qui crée des toiles jusqu'à ce qu'une exception soit levée, puis vide le cache et continue.

Merci à la personne maintenant anonyme qui a posté cette réponse.

let counter = 0

// create a 1MB image
const createImage = () => {
    const size = 512

    const canvas = document.createElement('canvas')
    canvas.height = size
    canvas.width = size

    const ctx = canvas.getContext('2d')
    ctx.strokeRect(0, 0, size, size)
    return canvas
}

const createImages = nbImage => {
    // create i * 1MB images
    const canvases = []

    for (let i = 0; i < nbImage; i++) {
        canvases.Push(createImage())
    }

    console.log(`done for ${canvases.length} MB`)
    return canvases
}

const deleteCanvases = canvases => {
    canvases.forEach((canvas, i, a) => {
        canvas.height = 0
        canvas.width = 0
    })
}

let canvases = []
const process = (frequency, size) => {
    setInterval(() => {
        try {
            canvases.Push(...createImages(size))
            counter += size
            console.log(`total ${counter}`)
        }
        catch (e) {
            deleteCanvases(canvases)
            canvases = []
        }
    }, frequency)
}


process(2000, 1000)
4
Ogier Maitre

Probablement ce changement récent dans WebKit devrait être à l'origine de ces problèmes https://github.com/WebKit/webkit/commit/5d5b478917c685e50d1032ccf761ca53fc8f1b74#diff-b411cd4839e4bbccbbc17b00570bfa8f

4
Rathaiah

Je peux confirmer ce problème. Aucun changement au code existant qui a fonctionné pendant des années. Cependant, dans mon cas, la toile est dessinée une seule fois lorsque la page est chargée. Les utilisateurs peuvent ensuite naviguer entre les différentes toiles et le navigateur recharge une page.

Mes tentatives de débogage jusqu'à présent montrent que Safari 12 perd apparemment de la mémoire entre les rechargements de page . Le profilage de la consommation de mémoire via Web Inspector montre que la mémoire continue de croître à chaque rechargement de page. Chrome et Firefox, d’autre part, semblent maintenir la consommation de mémoire au même niveau.

Du point de vue de l'utilisateur, il est utile d'attendre 20 à 30 secondes et de recharger une page. Safari efface la mémoire entre-temps.

Edit: Voici une preuve de concept minimale montrant comment Safari 12 perd de la mémoire entre les chargements de pages.

01.html

<a href="02.html">02</a>
<canvas id="test" width="10000" height="1000"></canvas>
<script>
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#0000ff";
ctx.fillRect(0,0,10000,1000);
</script>

02.html

<a href="01.html">01</a>
<canvas id="test" width="10000" height="1000"></canvas>
<script>
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#00FF00";
ctx.fillRect(0,0,10000,1000);
</script>

Étapes pour reproduire:

  • Télécharger les deux fichiers sur un serveur Web
  • Cliquez sur le lien en haut à plusieurs reprises pour basculer entre les pages
  • Observez la consommation de mémoire de Web Inspector à chaque chargement de page.

J'ai soumis un rapport de bogue à Apple. Va voir comment ça marche.

enter image description here

Edit: J'ai mis à jour les dimensions du canevas à 10000x1000 pour une meilleure preuve de concept. Si vous chargez maintenant les deux fichiers sur un serveur et que vous l'exécutez sur votre appareil iOS, si vous changez rapidement de page, le canevas ne sera pas tracé après plusieurs rechargements de page. Si vous attendez 30 à 60 secondes, une partie du cache semble être effacée et un rechargement affichera à nouveau le canevas.

2
ntaso

Un autre point de données: j'ai constaté que Safari Web Inspector (12.1 - 14607.1.40.1.4) conservait tous les objets Canvas créés pendant leur ouverture, même s'ils seraient autrement vidés. Fermez l'inspecteur Web et rouvrez-le. La plupart des vieilles toiles disparaissent.

Cela ne résout pas le problème initial: dépasser la mémoire de la toile lorsque vous n'exécutez PAS Web Inspector, mais sans connaître ce petit détail, j'ai perdu un temps fou à me tromper de route en pensant que je ne publiais aucune de mes toiles temporaires. .

2

Je voulais juste dire que nous avons une application Web utilisant Three.js qui plantait sur iPad Pro (1ère génération) sur iOS 12. Mise à niveau vers iOS 13 Public Beta 7 correction du problème. L'application ne plante plus.

1
Shay Cojo

J'ai soumis un nouveau rapport de bogue à Apple, pas encore de réponse. J'ai ajouté la possibilité d'exécuter le code ci-dessous après avoir dessiné une ligne à l'aide de polylignes dans Google Maps:

function makeItSo(){
  var foo = document.getElementsByTagName("canvas");
  console.log(foo);
  for(var i=0;i < foo.length;i++){
    foo[i].width = 32;
    foo[i].height = 32;
  }
}

En regardant la sortie de la console, seuls 4 éléments de toile ont été trouvés. Mais en regardant le panneau "canvas" dans le débogueur Safari, 33 toiles étaient affichées (leur montant dépend de la taille de la page Web que vous avez ouverte).
Une fois que le code ci-dessus a été exécuté, l’affichage des canevas montre les 4 canevas qui ont été trouvés avec une taille plus petite, comme on pouvait s’y attendre. Toutes les autres toiles "orphelines" sont toujours affichées dans le débogueur.
Je suppose que cela confirme la théorie de la "fuite de mémoire" - des canevas qui existent mais ne figurent pas dans le document. Lorsque la quantité de mémoire de la zone de dessin que vous possédez est dépassée, vous ne pouvez plus obtenir d’aide sur les toiles.
Encore une fois, tout cela a fonctionné jusqu'à IOS12. Mon ancien iPad fonctionnant IOS 10 fonctionne toujours.

0
eepete