Using javascript to detect device CPU/GPU performance?
Asked Answered
G

4

22

(The question is not specific to three.js but I will use it as an example)

I have been using three.js to develop a web app interface lately and written some nice fallback between WebGL and Canvas renderer (for desktop browsers).

But now the problem becomes how to properly detect device capability, there are 2 aspects to the problem:

  1. browser features (static features like webgl/canvas): this is largely solved within web community using simple feature detect.
  2. device capability: this is the hard part, without direct access to device's hardware information, we need some ways of telling whether we should fallback to less hardware-demanding code.

A notable example: Firefox mobile/Opera mobile claims support of WebGL but are buggy or limited by device hardware.

A few workarounds I come up with so far:

  1. Use a common feature as performance indicator - touch device, for example, has less powerful hardware in general. The con: it's not future-proof.
  2. Blacklist known buggy browser/device - UA sniffing will be unavoidable, and it can be hard to maintain.
  3. Performance test - hence the question, besides running the code and measure framerate, are there better options?

Or maybe it doesn't have to be this hard, are there other suggestions?

Gouda answered 22/12, 2012 at 10:54 Comment(0)
S
21

I've ended up using a performance measurement approach on a project where we wanted to utilise canvas features that are available on high spec CPU/GPU desktops, as well as lower speed devices such as tables and phones.

Basically we start at a minimum scene complexity, and if the renderloop takes less than 33ms we add complexity (we also reduce complexity if the renderloop starts taking too long at a later time).

I suppose in your case you might have to run a quick canvas and webgl performance test and then choose one. Having spent some time researching this I've not come across some tricky non-obvious technique that solves this better.

Sundown answered 20/2, 2014 at 3:29 Comment(2)
Love the solution you landed on. Thanks for sharing.Porras
+1, Dynamic detection and adjustment is the only way to go. Especially if you want to support any device that uses battery and may switch to low performance mode mid-fly to conserve battery. If you do a static detection once, you will miss such a change in the environment.Sanctimonious
U
1

You can use http://webglstats.com/ for WebGL hardware support and feature detection.

Urolith answered 22/12, 2012 at 11:56 Comment(3)
While the stats are interesting, I don't see how this can be applied to individual users of my site (to enable proper fallback, without a device/browser blacklist). Also from the code it seems the detection is done in an iframe, I reserve my doubt on the choice of method.Gouda
Indeed. This is very interesting information, however it's just feature detection, something you could do for each of your users on your own. It won't tell you performance, unless you want to draw some pretty dodgy correlations between feature implementation and performance.Volteface
Not anymore. Website is dead.Wilbert
J
1

This is the snippet I wrote to benchmark webgl, using three.js:

import {
  Mesh,
  MeshStandardMaterial,
  PerspectiveCamera,
  Scene,
  SphereGeometry,
  WebGLRenderer,
} from "three";

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export enum GPUPerformanceLevel {
  HIGH = "HIGH",
  LOW = "LOW",
}

function approxRollingAverage(
  average: number,
  value: number,
  history = 50
) {
  average -= average / history;
  average += value / history;

  return average;
}

/**
 * Three.js based webgl benchmark
 *
 * In summary, the `run` method adds `meshPerStep` new spheres every step (frame)
 * and measures the fps. If we're able to perform >=`thresholds.steps` of these
 * steps, without the fps dropping below `thresholds.fps`, then we label the device
 * `GPUPerformanceLevel.HIGH`.
 */
export class GPUBenchmark {
  scene = new Scene();
  material = new MeshStandardMaterial();
  geometry = new SphereGeometry();

  static thresholds = { fps: 10, steps: 50 };
  static meshPerFrame = 1000;
  static framesPerStep = 5;

  async run(debug = false): Promise<GPUPerformanceLevel> {
    const camera = new PerspectiveCamera(75);
    const renderer = new WebGLRenderer();

    let tPrev = performance.now() / 1000;
    let steps = 0;
    let meshCnt = 0;
    let fps = 30;

    let passedThreshold = false;

    const animate = async () => {
      const time = performance.now() / 1000;
      const fpsMeasured = Math.min(1 / (time - tPrev), 120);
      tPrev = time;

      fps = approxRollingAverage(fps, fpsMeasured, 5);

      if (debug) {
        console.log(`fps: ${fps} fpsMeasured: ${fpsMeasured} steps: ${steps} meshCnt: ${meshCnt}`);
      }

      if (
        fps > GPUBenchmark.thresholds.fps &&
        steps < GPUBenchmark.thresholds.steps
      ) {
        requestAnimationFrame(animate);

        if (steps++ % GPUBenchmark.framesPerStep == 0) {
          meshCnt += this.step();
        }
        renderer.render(this.scene, camera);
      } else {
        passedThreshold = true;
      }
    };

    animate();

    while (!passedThreshold) {
      await sleep(1);
    }

    this.cleanup();
    renderer.dispose();
    const level = GPUBenchmark.stepsToPerfLevel(steps);

    if (debug) {
      console.log("device benchmarked at level:", level);
    }

    return level;
  }

  private step(): number {
    const meshPerStep = GPUBenchmark.meshPerFrame * GPUBenchmark.framesPerStep;
    for (let i = 0; i < meshPerStep; i++) {
      const sphere = new Mesh(this.geometry, this.material);
      sphere.frustumCulled = false;

      this.scene.add(sphere);
    }

    return meshPerStep;
  }

  private cleanup() {
    for (const obj of this.scene.children) {
      this.scene.remove(obj);
    }

    //@ts-ignore
    this.scene = null;

    this.material.dispose();
    this.geometry.dispose();

    //@ts-ignore
    this.material = null;
    //@ts-ignore
    this.geometry = null;
  }

  private static stepsToPerfLevel(numSteps: number): GPUPerformanceLevel {
    if (numSteps >= GPUBenchmark.thresholds.steps) {
      return GPUPerformanceLevel.HIGH;
    } else {
      return GPUPerformanceLevel.LOW;
    }
  }
}
Jacaranda answered 15/8, 2022 at 19:24 Comment(0)
V
0

If your using three.js you can just use the detector.js to find out if webgl is enabled. also staying away from the canvasrenderer will help. use the webglrenderer or the softwarerender. as they allow for more vertices. the softwarerender is fairly new and needs some work but can be used.

Verst answered 29/5, 2014 at 5:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.