Общее использование памяти в холсте превышает максимальный предел (Safari 12)

Мы работаем над веб-приложением визуализации, использующим силу d3 для рисования сети на холсте. Поскольку каждый узел содержит много информации, и поскольку он интенсивно перегружал процессор для каждого кадра, мы реализовали кеш, где каждый узел нарисован на его холсте (не связан с DOM) один раз для каждого уровня масштабирования. Затем эти холсты нарисованы на основном холсте (связанном с DOM) в позиции узла.

Мы довольны усилением скорости, даже если результат в настоящее время интенсивный (особенно на дисплеях hidpi (сетчатка), где плотность пикселей может составлять 2 или 3).

Но теперь у нас возникла проблема с браузерами на iOS, где процесс выходит из строя после нескольких взаимодействий с интерфейсом. К моему воспоминанию, это не проблема со старой версией (до iOS12), но у меня нет никакого обновленного устройства, чтобы подтвердить это.

Я думаю, что этот код суммирует проблему:

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

Затем на iPad и в инспекторе:

> 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')

Будучи, я создаю холст размером 256 х 1 МБ, все идет хорошо, но я создаю еще один, а canvas.getContext возвращает нулевой указатель. Тогда невозможно создать еще один холст.

Предел, похоже, связан с устройством, так как на iPad его 256 МБ, а на iPhone X - 288 МБ.

> 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')

Поскольку это кеш, я должен удалять некоторые элементы, но Im not (поскольку установка ctxs или ctx в null должна запускать GC, но это не решает проблему).

Единственной релевантной страницей, которую я нашел по этой проблеме, является страница исходного кода webkit: HTMLCanvasElement.cpp.

Я подозреваю, что проблема может исходить от самого веб-кита, но Im хотел бы быть уверен, прежде чем отправлять на веб-сайт вопрос трекера.

Есть ли другой способ уничтожить контексты холста?

Заранее благодарю за любую идею, указатель,...

редактирует:

Чтобы добавить некоторую информацию, я попробовал другие браузеры. Safari 12 имеет ту же проблему на macOS, даже если предел выше (1/4 от памяти компьютера, как указано в источниках webkit). Я также пробовал с последней версией webkit (236590) без лишней удачи. Но код работает на Firefox 62 и Chrome 69.

Я уточнил тестовый код, поэтому его можно выполнить непосредственно с консоли отладчика. Было бы очень полезно, если бы кто-то мог проверить код на более раннем сафари (например, 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)

Ответ 1

Я провел выходные, сделав простую веб-страницу, которая может быстро показать проблему. Я отправил отчеты об ошибках в Google и Apple. На странице отображается карта. Вы можете панорамировать и масштабировать все, что хотите, и инспектор Safari (работающий на iPad на iPad, используя MacBook Pro для просмотра холстов) не видит холста.

Затем вы можете нажать кнопку и нарисовать одну полилинию. Когда вы это сделаете, вы увидите 41 полотно. Панорамирование или масштабирование, и вы увидите больше. Каждый холст составляет 1 МБ, поэтому после того, как у вас есть 256 сиротских холстов, ошибки начинаются с того, что память на холсте заполнена.

Перезагрузите страницу, нажмите кнопку, чтобы поместить один полигон, и то же самое происходит.

Не менее интересным является добавление кнопок для создания карты дня и ночи. Вы можете идти туда и обратно, когда это просто карта (или карта с только маркерами, есть кнопка для отображения некоторых маркеров на карте). Нет сиротских полотен. Но нарисуйте линию, а затем, когда вы измените стиль, вы увидите больше осиротевших холстов.

Глядя на Safari на MacBook в Active Monitor, размер просто продолжается, когда вы панорамируете и приближаетесь к карте после рисования poly *

Я надеюсь, что Apple и Google смогут понять это и не утверждать, что это проблема другой компании. Все это изменилось с помощью веб-страниц IOS12, которые были стабильными в течение многих лет, и которые все еще работают на IOS 9 и 10 iPads, которые я продолжаю тестировать, чтобы убедиться, что более старые устройства могут показывать текущие веб-страницы. Надеюсь, что этот тест/эксперимент поможет.

Ответ 2

Кто-то отправил ответ, который показал обходной путь для этого. Идея состоит в том, чтобы установить высоту и ширину до 0 перед удалением холстов. Это не совсем правильное решение, но оно будет работать в моей кеш-системе.

Я добавляю небольшой пример, который создает холсты до тех пор, пока не будет выбрано исключение, а затем очистит кеш и продолжится.

Спасибо анонимному человеку, который отправил этот ответ.

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

Еще одна точка данных: я обнаружил, что веб-инспектор Safari (12.1 - 14607.1.40.1.4) поддерживает каждый объект Canvas, созданный в то время, когда он открыт, даже если в противном случае он будет собираться мусором. Закройте веб-инспектор и снова откройте его, и большинство старых полотен исчезнет.

Это не решает исходную проблему - превышение памяти холста, когда НЕ работает веб-инспектор, но, не зная этого маленького кусочка, я потратил кучу времени, идя по неверному пути, думая, что не выпускаю ни одного из своих временных холстов,

Ответ 5

Я могу подтвердить эту проблему. Никаких изменений в существующем коде, которые работали годами. Однако, в моем случае, холст рисуется только один раз, когда страница загружается. Затем пользователи могут просматривать разные холсты, а браузер перезагружает страницу.

Мои попытки отладки пока показывают, что Safari 12, по- видимому, теряет память между перезагрузкой страницы. Профилирование потребления памяти через Web Inspector показывает, что память продолжает расти для каждой перезагрузки страницы. С другой стороны, Chrome и Firefox, похоже, сохраняют потребление памяти на одном уровне.

С точки зрения пользователя, это позволяет просто подождать 20-30 секунд и перезагрузить страницу. Тем временем Safari очищает память.

Edit: Здесь минимальное доказательство концепции, показывающее, как Safari 12 теряет память между загрузками страниц.

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>

Действия по воспроизведению:

  • Загрузка обоих файлов на веб-сервер
  • Нажмите ссылку вверху, чтобы переключаться между страницами
  • Наблюдайте за потреблением памяти Web Inspector для каждой загрузки страницы

Я отправил отчет Apple об ошибке. Посмотрим, как это получится.

enter image description here

Изменение: я обновил размеры Canvas до 10000x1000 как лучшее доказательство концепции. Если теперь вы загружаете оба файла на сервер и запускаете его на своем устройстве iOS, если вы быстро переключаетесь между страницами, Canvas не будет отображаться после нескольких перезагрузок страниц. Если вы затем дожидаетесь 30-60 секунд, некоторый кеш, кажется, очищается, и перезагрузка снова покажет Canvas.

Ответ 6

Просто хотел сказать, что у нас есть веб-приложение, использующее сбой Three.js на iPad Pro (1-го поколения) на iOS 12. Обновление до iOS 13 Public Beta 7 устранило проблему. Приложение больше не падает.

Ответ 7

Я отправил новый отчет об ошибке в Apple, пока нет ответа. Я добавил возможность выполнять код, показанный ниже, после того, как нарисовал линию, используя Полилинии в 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;
  }
}

Глядя на вывод консоли, было найдено только 4 элемента canvas. Но, глядя на панель "холст" в отладчике Safari, было отображено 33 полотна (количество зависит от размера открытой веб-страницы).
 После выполнения приведенного выше кода на дисплее отображаются 4 полотна, которые были найдены в меньшем размере, как и следовало ожидать. Все остальные "осиротевшие" полотна по-прежнему отображаются в отладчике.
 Я подозреваю, что это подтверждает "утечку памяти" theory- холстов, которые существуют, но отсутствуют в документе. При превышении объема памяти холста ничто больше не может быть отрисовано с помощью холстов.
 Опять же, все это работало до IOS12. Мой старый iPad под управлением IOS 10 все еще работает.