Browser crash when loading model (out of memory)

Hi!

I am showing 3D models in the browser with the @speckle/viewer npm package.

Most of the models load fine, but there are a few larger ones that sometimes crashes the browser when using the viewer package, but loads fine in Speckle app otherwise.

It is a Nuxt 2 web app with Node 18 and @speckle/viewer@2.18.16.

I have tried increasing the memory of the nodejs process with NODE_OPTIONS='--max-old-space-size=8192', but it did not help.

Any idea how can I solve this?

Thanks in advance,
Adam

1 Like

Hi @Adam_Mezovari

There is a known issue where some large models crash the object-loader (which is implicitly used by the viewer) when loading. Of course, the source for out of memory crashes can be a lot of things, including the viewer library per se if things get large enough.

However I find it a little weird that you are only having this issue in your own app and not the speckle app. Can you perhaps share a link with me with one of the offending models so I can have a better look?

Cheers

Hi Alex,

Can I somehow write you directly?

I’d rather not share the link here.

Hey @Adam_Mezovari,

If you click the Profile Picture you can send us a direct message. Otherwise, send an email to hello@speckle.systems.

Thank you

1 Like

Hey, I could not find the private message, but I have sent you an email.

Ok sorry for that. We received your email and will look into that and get back to you.

Thank you

1 Like

Thanks! Looking forward to it!

1 Like

Hi @Adam_Mezovari

I’ve tried running your model in both the web app and the sandbox and I couldn’t reproduce the issue you are describing. I tried on various browsers and OSs, including Safari on MacOS which is by far the most paranoid when it comes to memory allocation.

I think we’ll need more information:

  • Can you please specify which platforms and OSs you get the issue
  • Is it something specific that your app does that causes the crash? Say like loading multiple models in parallel?
  • Is your application publicly accessible?
  • Any other details that you can give us would be of much help

I don’t think it’s the known issue that’s causing you crashes since those are consistent and perfectly repeatable for the streams in question. With your stream I never got a crash no matter how hard I tried, so there’s probably something else going on

Cheers

1 Like

Hey,

Thanks for looking into it!

  • I am using MacOS with Chrome and Arc, but I have seen it crashing on Windows in Chrome.
  • It’s just loading a single model, I attached the component code.
  • The app is not publicly accessible and it is hosted as a Docker image in Google Cloud Run. The node has sufficient memory and cpu allocated.
  • It does not crash always, I would say 70% of time.
<template>
  <v-row>
    <v-col cols="12">
      <div class="speckle-viewer" ref="speckle-viewer" :id="viewerId">
        <loading v-if="loading"></loading>
        <template v-else>
          <v-btn
            class="clear-button"
            small
            color="primary"
            v-if="selectedSpeckle"
            >CLEAR SELECTION</v-btn
          >
          <template v-if="showUpdate">
            <v-btn
              icon
              color="primary"
              class="update-button"
              @click.native="$emit('update-model')">
              <v-icon>mdi-tray-arrow-up</v-icon>
            </v-btn>
          </template>

          <template v-if="showMeasure">
            <v-btn
              icon
              color="primary"
              class="measure-button"
              @click.native="measurements.enabled = true"
              v-if="!measurements.enabled">
              <v-icon>mdi-ruler</v-icon>
            </v-btn>
            <template v-if="measurements.enabled">
              <v-btn
                icon
                color="primary"
                class="measure-button"
                @click.native="toggleMeasurements">
                <v-icon>mdi-close</v-icon>
              </v-btn>
              <v-btn
                icon
                color="primary"
                class="clear-measurements-button"
                @click.native="clearMeasurements">
                <v-icon>mdi-delete</v-icon>
              </v-btn>
            </template>
          </template>
        </template>
      </div>
    </v-col>
  </v-row>
</template>

<script>
import {
  CameraController,
  DefaultViewerParams,
  FilteringExtension,
  MeasurementsExtension,
  SelectionExtension,
  SpeckleLoader,
  Viewer,
  ViewerEvent,
} from "@speckle/viewer";
import Loading from "~/components/common/Loading.vue";

export default {
  components: { Loading },
  props: {
    viewerId: {
      type: String,
      required: true,
    },
    speckleUrl: {
      type: String,
      default: null,
    },
    objectIds: {
      type: Array,
      default: () => [],
    },
    isolate: {
      type: Boolean,
      default: false,
    },
    hide: {
      type: Boolean,
      default: false,
    },
    setSpeckle: {
      type: Boolean,
      default: false,
    },
    showMeasure: {
      type: Boolean,
      default: false,
    },
    showUpdate: {
      type: Boolean,
      default: false,
    },
  },
  watch: {
    selectedSpeckle: {
      handler(value) {
        this.selector.clearSelection();
        // this.camera.zoom();

        if (value) {
          if (this.viewer) {
            this.selector.selectObjects([value]);
            // this.camera.zoom([value], true, 100);
          }
        }
      },
    },

    hide: {
      handler(value) {
        if (value) {
          this.filter.hideObjects(this.objectIds);
        }
      },
    },
    speckleUrl: {
      async handler() {
        if (this.viewer) {
          await this.viewer.unloadAll();
          await this.loadModel();
        }
      },
    },
  },
  data() {
    return {
      loading: true,
      viewer: null,
      camera: null,
      filter: null,
      selector: null,
      measurements: null,
    };
  },
  computed: {
    selectedSpeckle: {
      get() {
        return this.$store.getters["twins/selectedSpeckle"];
      },
      set(value) {
        this.$store.commit("twins/setSelectedSpeckle", value);
      },
    },
  },
  methods: {
    async initViewer() {
      try {
        this.loading = true;
        /** Get the HTML container */
        const container = document.getElementById(this.viewerId);

        /** Configure the viewer params */
        const params = DefaultViewerParams;
        // params.showStats = true;
        // params.verbose = true;

        /** Create Viewer instance and initalize */
        const viewer = new Viewer(container, params);
        await viewer.init();

        this.camera = viewer.createExtension(CameraController);
        this.selector = viewer.createExtension(SelectionExtension);
        this.filter = viewer.createExtension(FilteringExtension);

        if (this.showMeasure)
          this.measurements = viewer.createExtension(MeasurementsExtension);

        viewer.on(ViewerEvent.ObjectClicked, this.handleObjectClicked);

        this.viewer = viewer;

        await this.loadModel();

        this.loading = false;
      } catch (err) {
        console.log(err);
      }
    },
    async loadModel() {
      try {
        if (!this.viewer) {
          this.initViewer();
        }
        /** Create a loader for the speckle stream */
        const loader = new SpeckleLoader(
          this.viewer.getWorldTree(),
          this.speckleUrl,
          process.env.SPECKLE_KEY,
          true
        );

        /** Load the speckle data */
        await this.viewer.unloadAll(); // Unload all
        await this.viewer.loadObject(loader, true);

        if (this.selectedSpeckle) {
          this.selector.selectObjects([this.selectedSpeckle]);
        }
      } catch (err) {
        console.log(err);
      }
    },
    handleObjectClicked(selectionInfo) {
      if (this.measurements && this.measurements.enabled) return;
      if (selectionInfo && selectionInfo.hits.length > 0) {
        if (this.setSpeckle)
          this.selectedSpeckle = selectionInfo.hits[0].node.model.id;
        this.$emit("object-clicked", selectionInfo.hits[0].node.model);
      } else {
        // No object clicked. Restore focus to entire scene
        if (this.setSpeckle) this.selectedSpeckle = null;
        this.$emit("object-clicked", null);
      }
    },
    toggleMeasurements() {
      this.measurements.enabled = !this.measurements.enabled;
    },
    clearMeasurements() {
      this.measurements.clearMeasurements();
    },
  },
  async beforeDestroy() {
    if (this.viewer) {
      await this.viewer.unloadAll();
      this.viewer.dispose();
      this.viewer = null;
      this.camera = null;
      this.selector = null;
      this.filter = null;
      this.measurements = null;
    }
  },
  async mounted() {
    await this.initViewer();
    if (this.objectIds.length > 0 && this.hide) {
      this.filter.hideObjects(this.objectIds);
    }
  },
};
</script>

Hi @Adam_Mezovari

Thank you for the details. I’m not familiar with Google Cloud Run, but it’d like to exclude it first as a possible cause for your issue. Can you run your application in a local dev environment? So no docker image running anywhere, just as plainly as possible running locally on your machine. I’m curious if you experience the issues the same way.

Another thing that might help us understand what’s going on, is enabling verbose mode, so basically uncomment params.verbose = true; and provide us the console log.

Cheers

Hey,

Sorry, I have not mentioned, but certainly I tried in local and I had the same crashes.

So when I run in verbose mode, I get the following log for a successful load:

Browser crash model

When it crashes, I get the log until the red line, but nothing after and the page is redirected to Chrome’s error page below. Error code: 5 might be interesting.

Thanks!

@Adam_Mezovari

Its hard to help without actually being able to see or run the app we’re discussing here. The issue might have nothing to do with Speckle Viewer at the end of the day.

I’d suggest using Chrome’s performance profiler (in the dev tools) to record a run of the app during runtime, and then looking through it to see where the CPU and/or memory bottleneck is. You could upload the profiler trace and we could take a look, but even that could be difficult without seeing the source code.

2 Likes

Further to this, do be aware viewer intialisation requires a bit of special care in SPAs - whenever you’re navigating away from the page and back again you risk re-initialising the viewer in the same element, but the old one stays somewhere hidden, so you can end up with many instances.

since i think you’re using vue 2.0, check how we are doing it in the old frontend that’s also on v2:

3 Likes