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

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

Мы довольны увеличением скорости, даже если результат теперь требует большого объема памяти (особенно на дисплеях хидпи (retina), где плотность пикселей может быть 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 x 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')

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

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

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

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

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

РЕДАКТИРОВАТЬ:

Чтобы добавить некоторую информацию, я пробовал другие браузеры.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)

Ответы [ 6 ]

0 голосов
/ 10 июля 2019

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

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

0 голосов
/ 26 мая 2019

Я отправил новый отчет об ошибке в 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 холста, которыебыли найдены в меньшем размере, как и следовало ожидать.Все остальные «осиротевшие» полотна все еще отображаются в отладчике.
Я подозреваю, что это подтверждает теорию «утечки памяти» - полотна, которые существуют, но отсутствуют в документе.Когда объем памяти холста, который у вас есть, превышен, ничто больше не может быть отрисовано с помощью холстов.
Опять же, все это работало до IOS12.Мой старый iPad под управлением IOS 10 все еще работает.

0 голосов
/ 26 октября 2018

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

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

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

Редактировать: вот минимальное подтверждение концепции, показывающей, как 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.

0 голосов
/ 12 октября 2018

Возможно, это недавнее изменение в WebKit должно вызывать эти проблемы https://github.com/WebKit/webkit/commit/5d5b478917c685e50d1032ccf761ca53fc8f1b74#diff-b411cd4839e4bbc17b00570536abfa8f

0 голосов
/ 08 октября 2018

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

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

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

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

При просмотре Safari на MacBook в активном мониторе, размер продолжает расти, когда вы перемещаетесь и масштабируете карту послерисование поли *

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

0 голосов
/ 01 октября 2018

Кто-то опубликовал ответ, который показал обходной путь для этого.Идея состоит в том, чтобы установить высоту и ширину равными 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)
...