(Three.js v0.88.0, Angular 4.x)

В моем текущем проекте я работаю над средством трехмерного просмотра, поддерживаемым three.js и Angular 2. У нас есть список сцен, по которым вы можете пройти, и мы используем сервисы Angular 2 для обработки таких вещей, как навигация, рендеринг, получение моделей и т. Д. Одна из проблем, с которыми мы столкнулись, заключалась в том, что при перемещении по программе просмотра вперед и назад использование памяти графического процессора не снижалось. В этом посте я поделюсь своим опытом решения этой проблемы.

Благодарим: https://stackoverflow.com/a/19726918 за то, что направили меня на правильный путь.

Нашим первым решением проблемы было предоставление кнопки закрытия, которая перезагружала приложение по заданному URL-адресу, например <a href="/scans/123"></a>

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

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

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

Второй снимок сделан после загрузки вьювера.

И затем окончательный снимок будет после возврата на страницу сведений.

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

Используя сделанные снимки, мы можем провести сравнение. Мы можем проверить, какие объекты все еще присутствуют в третьем снимке, но были сделаны между первым и вторым (например, загрузка средства просмотра):

При сравнении по сохраненному размеру мы видим, что многие объекты BufferGeometry, BufferAttribute и Mesh сохраняются и не собираются. Это объекты three.js, которые нам больше не нужны вне программы просмотра.

Мы также можем видеть, что состояние служб, используемых зрителем, сохраняется в памяти:

Это приводит к двум вещам, которые мы можем сделать. Все наши услуги предоставляются модулем, что делает их эффективными одиночками. Это означает, что навигация по ним не удаляет их. Учитывая, что в нашем случае все сервисы в этом модуле нужны только компоненту, который показывает рендеринг viewer / threejs, мы можем изолировать сервисы для этого компонента, только позволив компоненту выполнять работу по их предоставлению.

@NgModule({
    imports: [
        ...
    ],
    declarations: [
        ....
    ],
    entryComponents: [
    ],
    providers: [
        SnapshotService,
        AnimationService,
        RenderService,
        ...
    ]
})
export class ViewerModule {
}
public destroy() {
    this._scene = undefined;
}

Мы удаляем провайдеров из модуля и добавляем их в компонент просмотра.

@Component({
    template: `...`,
    providers: [
        SnapshotService,
        AnimationService,
        RenderService,
        ...
    ]
})

Это дает нам преимущество в том, что Angular уничтожает сервисы, когда компонент просмотра больше не находится в DOM + предоставляет ловушку для дополнительной очистки, когда эти сервисы уничтожаются с помощью предоставляемой им ловушки ngOnDestroy ().

Это первая часть, о которой мы позаботились. Вторая часть была посвящена тому, как three.js очищает память графического процессора. Некоторые поисковые запросы учит вас, что он не делает этого за вас, каждую сетку, геометрию или материал, которые вы создаете и добавляете в сцену, нужно очищать вручную. Итак, наш текущий код не совсем его сокращает:

public destroy() {
    this._scene = undefined;
}

Это только разыменует сцену, но ничего не сделает со стороны графического процессора. Видя, что теперь у нас есть сервисы, которые ограничены только для зрителя, мы можем воспользоваться ловушкой destroy, позволив вашей службе реализовать OnDestroy. После того, как средство просмотра удалено из DOM (путем перехода), ему больше не нужны службы, поэтому angular уничтожает их и запускает ловушку.

@Injectable()
export class WorldService implements OnDestroy {
    private _scene: Scene;

    public ngOnDestroy() {
        this._scene = undefined;
    }

}

Далее осуществляется очистка сцены с помощью графического процессора
(Источники: https://stackoverflow.com/a/33199591)

ngOnDestroy(): void {
    console.log("OnDestroy - WorldService");
    while (this._scene.children.length > 0) {
        let obj = this._scene.children[0];
        this._scene.remove(obj);
        this.disposeHierarchy(obj, this.disposeNode);

    }
    this._scene = null;
}

Мы перебираем дочерние объекты сцены, удаляем их, а затем избавляемся от них на стороне графического процессора.

private disposeHierarchy(node, callback) {
    for (var i = node.children.length - 1; i >= 0; i--) {
        var child = node.children[i];
        this.disposeHierarchy(child, callback);
        callback(child);
    }
}

Это будет перебирать все дочерние элементы, которые есть у узла, а затем удалять сам дочерний элемент.

private disposeNode(node) {
    if (node instanceof Mesh) {
        node.parent = undefined;
        if (node.geometry) {
            node.geometry.dispose();
        }

        let material: any = node.material;
        if (material) {

            if (material.map) material.map.dispose();
            if (material.lightMap) material.lightMap.dispose();
            if (material.bumpMap) material.bumpMap.dispose();
            if (material.normalMap) material.normalMap.dispose();
            if (material.specularMap) material.specularMap.dispose();
            if (material.envMap) material.envMap.dispose();

            material.dispose();
        }
    } else if (node instanceof Object3D) {
        node.parent.remove(node);
        node.parent = undefined;
    }
}

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

Создание новых снимков после этих изменений показывает, что память графического процессора возвращается в исходное состояние после навигации:

Мы можем заметить, что данных еще на ~ 5 МБ. Мы можем использовать инструмент памяти, чтобы проверить, что еще сохраняется и чем.

Например, мы не ожидаем, что что-то вроде FurnitureService все еще сохранится после уничтожения компонента. На нижней панели вы можете видеть, что ViewportComponent все еще ссылается на него, несмотря на то, что он был уничтожен.

После некоторой навигации по фиксаторам и обзорам объектов я обнаружил, что Angular 2 сохраняет некоторые ссылки на предыдущее представление.

После некоторой навигации по фиксаторам и обзорам объектов я обнаружил, что Angular 2 сохраняет некоторые ссылки на предыдущее представление. Это приводит к сохранению цепочки объектов, но если мы перейдем к еще одной странице, новый снимок покажет, что больше нет ссылок на компоненты или службы, которые были созданы средством просмотра.

Другие вещи, которые сохраняются, - это подписки RX и прослушиватели событий DOM, они также могут задерживаться, если вы не unsubscribe() or removeEventListener(...).

Функционально память / снимок были новы для меня, так как мне никогда не приходилось использовать их раньше, но они определенно помогли мне в этом случае указать мне правильное направление. Он также научил меня кое-чему о внутреннем устройстве и о том, как хранятся некоторые ссылки, так что на это интересно взглянуть.

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

Надеюсь, что обмен этим опытом поможет вам, и не стесняйтесь оставлять после себя предложения или советы!